diff options
116 files changed, 1185 insertions, 443 deletions
diff --git a/RELEASING_RAILS.md b/RELEASING_RAILS.md index 83586c22c0..037aa8c119 100644 --- a/RELEASING_RAILS.md +++ b/RELEASING_RAILS.md @@ -36,7 +36,7 @@ Obviously Rails cannot be released when it depends on unreleased code. Contact the authors of those particular gems and work out a release date that suits them. -### Contact the security team (either Koz or tenderlove) +### Contact the security team (either tenderlove or rafaelfranca) Let them know of your plans to release. There may be security issues to be addressed, and that can impact your release date. diff --git a/actioncable/README.md b/actioncable/README.md index 6e74551483..c85d59a1c8 100644 --- a/actioncable/README.md +++ b/actioncable/README.md @@ -17,7 +17,7 @@ The client of a WebSocket connection is called the consumer. Each consumer can in turn subscribe to multiple cable channels. Each channel encapsulates a logical unit of work, similar to what a controller does in a regular MVC setup. For example, -you could have a `ChatChannel` and a `AppearancesChannel`, and a consumer could be subscribed to either +you could have a `ChatChannel` and an `AppearancesChannel`, and a consumer could be subscribed to either or to both of these channels. At the very least, a consumer should be subscribed to one channel. When the consumer is subscribed to a channel, they act as a subscriber. The connection between @@ -188,7 +188,7 @@ can be reached as remote procedure calls via a subscription's `perform` method. The appearance example was all about exposing server functionality to client-side invocation over the WebSocket connection. But the great thing about WebSockets is that it's a two-way street. So now let's show an example where the server invokes -action on the client. +an action on the client. This is a web notification channel that allows you to trigger client-side web notifications when you broadcast to the right streams: @@ -298,22 +298,24 @@ See the [rails/actioncable-examples](http://github.com/rails/actioncable-example ## Configuration -Action Cable has three required configurations: the Redis connection, allowed request origins, and the cable server url (which can optionally be set on the client side). +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). ### Redis By default, `ActionCable::Server::Base` will look for a configuration file in `Rails.root.join('config/cable.yml')`. -This file must specify a Redis url for each Rails environment. It may use the following format: +This file must specify an adapter and a URL for each Rails environment. It may use the following format: ```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 Redis config file in a Rails initializer with something like: +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" @@ -324,7 +326,7 @@ Rails.application.paths.add "config/cable", with: "somewhere/else/cable.yml" 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 -ActionCable.server.config.allowed_request_origins = ['http://rubyonrails.com', /http:\/\/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". @@ -332,7 +334,7 @@ When running in the development environment, this defaults to "http://localhost: To disable and allow requests from any origin: ```ruby -ActionCable.server.config.disable_request_forgery_protection = true +Rails.application.config.action_cable.disable_request_forgery_protection = true ``` ### Consumer Configuration @@ -374,7 +376,7 @@ App.cable = ActionCable.createConsumer() 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: ```ruby -ActionCable.server.config.log_tags = [ +Rails.application.config.action_cable.log_tags = [ -> request { request.env['bc.account_id'] || "no-account" }, :action_cable, -> request { request.uuid } @@ -389,7 +391,7 @@ Also note that your server must provide at least the same number of database con ## 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 +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 @@ -431,12 +433,9 @@ The WebSocket server doesn't have access to the session, but it has access to th ## Dependencies -Action Cable is currently tied to Redis through its use of the pubsub feature to route -messages back and forth over the WebSocket cable connection. This dependency may well -be alleviated in the future, but for the moment that's what it is. So be sure to have -Redis installed and running. +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 ActionCable as example implementations. -The Ruby side of things is built on top of [faye-websocket](https://github.com/faye/faye-websocket-ruby) and [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby). +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 diff --git a/actioncable/lib/action_cable/connection/base.rb b/actioncable/lib/action_cable/connection/base.rb index b5f898436a..1acef93025 100644 --- a/actioncable/lib/action_cable/connection/base.rb +++ b/actioncable/lib/action_cable/connection/base.rb @@ -35,7 +35,7 @@ module ActionCable # # First, we declare that this connection can be identified by its current_user. This allows us later to be able to find all connections # established for that current_user (and potentially disconnect them if the user was removed from an account). You can declare as many - # identification indexes as you like. Declaring an identification means that a attr_accessor is automatically set for that key. + # identification indexes as you like. Declaring an identification means that an attr_accessor is automatically set for that key. # # Second, we rely on the fact that the WebSocket connection is established with the cookies from the domain being sent along. This makes # it easy to use signed cookies that were set when logging in via a web interface to authorize the WebSocket connection. @@ -185,12 +185,14 @@ module ActionCable end def respond_to_successful_request + logger.info successful_request_message websocket.rack_response end def respond_to_invalid_request close if websocket.alive? + logger.error invalid_request_message logger.info finished_request_message [ 404, { 'Content-Type' => 'text/plain' }, [ 'Page not found' ] ] end @@ -205,7 +207,7 @@ module ActionCable 'Started %s "%s"%s for %s at %s' % [ request.request_method, request.filtered_path, - websocket.possible? ? ' [WebSocket]' : '', + websocket.possible? ? ' [WebSocket]' : '[non-WebSocket]', request.ip, Time.now.to_s ] end @@ -213,10 +215,22 @@ module ActionCable def finished_request_message 'Finished "%s"%s for %s at %s' % [ request.filtered_path, - websocket.possible? ? ' [WebSocket]' : '', + websocket.possible? ? ' [WebSocket]' : '[non-WebSocket]', request.ip, Time.now.to_s ] end + + def invalid_request_message + 'Failed to upgrade to WebSocket (REQUEST_METHOD: %s, HTTP_CONNECTION: %s, HTTP_UPGRADE: %s)' % [ + env["REQUEST_METHOD"], env["HTTP_CONNECTION"], env["HTTP_UPGRADE"] + ] + end + + def successful_request_message + 'Successfully upgraded to WebSocket (REQUEST_METHOD: %s, HTTP_CONNECTION: %s, HTTP_UPGRADE: %s)' % [ + env["REQUEST_METHOD"], env["HTTP_CONNECTION"], env["HTTP_UPGRADE"] + ] + end end end end diff --git a/actioncable/lib/action_cable/server/broadcasting.rb b/actioncable/lib/action_cable/server/broadcasting.rb index 7e8aef45f4..b87232671b 100644 --- a/actioncable/lib/action_cable/server/broadcasting.rb +++ b/actioncable/lib/action_cable/server/broadcasting.rb @@ -23,7 +23,7 @@ module ActionCable broadcaster_for(broadcasting).broadcast(message) end - # Returns a broadcaster for a named <tt>broadcasting</tt> that can be reused. Useful when you have a object that + # Returns a broadcaster for a named <tt>broadcasting</tt> that can be reused. Useful when you have an object that # may need multiple spots to transmit to a specific broadcasting over and over. def broadcaster_for(broadcasting) Broadcaster.new(self, broadcasting) diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index bb3cb1be45..4259eb0bee 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -664,7 +664,7 @@ module ActionMailer # # You can also specify overrides if you want by passing a hash instead of a string: # - # mail.attachments['filename.jpg'] = {mime_type: 'application/x-gzip', + # mail.attachments['filename.jpg'] = {mime_type: 'application/gzip', # content: File.read('/path/to/filename.jpg')} # # If you want to use encoding other than Base64 then you will need to pass encoding @@ -672,7 +672,7 @@ module ActionMailer # data: # # file_content = SpecialEncode(File.read('/path/to/filename.jpg')) - # mail.attachments['filename.jpg'] = {mime_type: 'application/x-gzip', + # mail.attachments['filename.jpg'] = {mime_type: 'application/gzip', # encoding: 'SpecialEncoding', # content: file_content } # diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 809b735deb..931313612c 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,10 +1,60 @@ +* Add request encoding and response parsing to integration tests. + + What previously was: + + ```ruby + require 'test_helper' + + class ApiTest < ActionDispatch::IntegrationTest + test 'creates articles' do + assert_difference -> { Article.count } do + post articles_path(format: :json), + params: { article: { title: 'Ahoy!' } }.to_json, + headers: { 'Content-Type' => 'application/json' } + end + + assert_equal({ 'id' => Article.last.id, 'title' => 'Ahoy!' }, JSON.parse(response.body)) + end + end + ``` + + Can now be written as: + + ```ruby + require 'test_helper' + + class ApiTest < ActionDispatch::IntegrationTest + test 'creates articles' do + assert_difference -> { Article.count } do + post articles_path, params: { article: { title: 'Ahoy!' } }, as: :json + end + + assert_equal({ 'id' => Article.last.id, 'title' => 'Ahoy!' }, response.parsed_body) + end + end + ``` + + Passing `as: :json` to integration test request helpers will set the format, + content type and encode the parameters as JSON. + + Then on the response side, `parsed_body` will parse the body according to the + content type the response has. + + Currently JSON is the only supported MIME type. Add your own with + `ActionDispatch::IntegrationTest.register_encoder`. + + *Kasper Timm Hansen* + +* Add image/svg+xml as a default mime type. + + *DHH* + ## Rails 5.0.0.beta2 (February 01, 2016) ## -* Add `-g` and `-c` (short for _grep_ and _controller_ respectively) options - to `bin/rake routes`. These options return the url `name`, `verb` and +* Add `-g` and `-c` options to `bin/rails routes`. These options return the url `name`, `verb` and `path` field that match the pattern or match a specific controller. - Deprecate `CONTROLLER` env variable in `bin/rake routes`. + Deprecate `CONTROLLER` env variable in `bin/rails routes`. See #18902. diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index d1d6acac26..a0917b4fdb 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -25,8 +25,8 @@ module ActionController status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name) end message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms" - message << " (#{additions.join(" | ".freeze)})" unless additions.blank? - message << "\n\n" if Rails.env.development? + message << " (#{additions.join(" | ".freeze)})" unless additions.empty? + message << "\n\n" if defined?(Rails.env) && Rails.env.development? message end diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb index 3dbf34eb2a..bf74b39ac4 100644 --- a/actionpack/lib/action_controller/metal/instrumentation.rb +++ b/actionpack/lib/action_controller/metal/instrumentation.rb @@ -19,9 +19,9 @@ module ActionController :controller => self.class.name, :action => self.action_name, :params => request.filtered_parameters, - :format => request.format.try(:ref), + :format => request.format.ref, :method => request.request_method, - :path => (request.fullpath rescue "unknown") + :path => request.fullpath } ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload.dup) diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index acc4507b2d..fc20e7a421 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -238,34 +238,39 @@ module ActionController # response code and headers back up the rack stack, and still process # the body in parallel with sending data to the client new_controller_thread { - t2 = Thread.current - - # Since we're processing the view in a different thread, copy the - # thread locals from the main thread to the child thread. :'( - locals.each { |k,v| t2[k] = v } - - begin - super(name) - rescue => e - if @_response.committed? - begin - @_response.stream.write(ActionView::Base.streaming_completion_on_exception) if request.format == :html - @_response.stream.call_on_error - rescue => exception - log_error(exception) - ensure - log_error(e) - @_response.stream.close + ActiveSupport::Dependencies.interlock.running do + t2 = Thread.current + + # Since we're processing the view in a different thread, copy the + # thread locals from the main thread to the child thread. :'( + locals.each { |k,v| t2[k] = v } + + begin + super(name) + rescue => e + if @_response.committed? + begin + @_response.stream.write(ActionView::Base.streaming_completion_on_exception) if request.format == :html + @_response.stream.call_on_error + rescue => exception + log_error(exception) + ensure + log_error(e) + @_response.stream.close + end + else + error = e end - else - error = e + ensure + @_response.commit! end - ensure - @_response.commit! end } - @_response.await_commit + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + @_response.await_commit + end + raise error if error end diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 91b3403ad5..6586985ff5 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -378,7 +378,8 @@ module ActionController #:nodoc: end def xor_byte_strings(s1, s2) - s1.bytes.zip(s2.bytes).map { |(c1,c2)| c1 ^ c2 }.pack('c*') + s2_bytes = s2.bytes + s1.bytes.map.with_index { |c1, i| c1 ^ s2_bytes[i] }.pack('c*') end # The form's authenticity parameter. Override to provide your own. diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index ac45b2e363..0c4b661214 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -17,6 +17,7 @@ module ActionController # the database on the main thread, so they could open a txn, then the # controller thread will open a new connection and try to access data # that's only visible to the main thread's txn. This is the problem in #23483 + remove_method :new_controller_thread def new_controller_thread # :nodoc: yield end diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb index 87715205d9..8356d1a238 100644 --- a/actionpack/lib/action_dispatch/http/mime_types.rb +++ b/actionpack/lib/action_dispatch/http/mime_types.rb @@ -14,6 +14,7 @@ Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe pjpeg) Mime::Type.register "image/gif", :gif, [], %w(gif) Mime::Type.register "image/bmp", :bmp, [], %w(bmp) Mime::Type.register "image/tiff", :tiff, [], %w(tif tiff) +Mime::Type.register "image/svg+xml", :svg Mime::Type.register "video/mpeg", :mpeg, [], %w(mpg mpeg mpe) diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index 6cde5b2900..79f9283f83 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -239,8 +239,7 @@ module ActionDispatch # # rails routes # - # Target specific controllers by prefixing the command with <tt>--controller</tt> option - # - or its <tt>-c</tt> shorthand. + # Target specific controllers by prefixing the command with <tt>-c</tt> option. # module Routing extend ActiveSupport::Autoload diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index b806ee015b..983f1daeb3 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -84,14 +84,15 @@ module ActionDispatch if filter.is_a?(Hash) && filter[:controller] { controller: /#{filter[:controller].downcase.sub(/_?controller\z/, '').sub('::', '/')}/ } elsif filter - { controller: /#{filter}/, action: /#{filter}/ } + { controller: /#{filter}/, action: /#{filter}/, verb: /#{filter}/, name: /#{filter}/, path: /#{filter}/ } end end def filter_routes(filter) if filter @routes.select do |route| - filter.any? { |default, value| route.defaults[default] =~ value } + route_wrapper = RouteWrapper.new(route) + filter.any? { |default, value| route_wrapper.send(default) =~ value } end else @routes diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 6f51accee7..f4534b4173 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -321,7 +321,9 @@ module ActionDispatch end # Performs the actual request. - def process(method, path, params: nil, headers: nil, env: nil, xhr: false) + def process(method, path, params: nil, headers: nil, env: nil, xhr: false, as: nil) + request_encoder = RequestEncoder.encoder(as) + if path =~ %r{://} location = URI.parse(path) https! URI::HTTPS === location if location.scheme @@ -330,14 +332,17 @@ module ActionDispatch url_host += ":#{location.port}" if default != location.port host! url_host end - path = location.query ? "#{location.path}?#{location.query}" : location.path + path = request_encoder.append_format_to location.path + path = location.query ? "#{path}?#{location.query}" : path + else + path = request_encoder.append_format_to path end hostname, port = host.split(':') request_env = { :method => method, - :params => params, + :params => request_encoder.encode_params(params), "SERVER_NAME" => hostname, "SERVER_PORT" => port || (https? ? "443" : "80"), @@ -347,7 +352,7 @@ module ActionDispatch "REQUEST_URI" => path, "HTTP_HOST" => host, "REMOTE_ADDR" => remote_addr, - "CONTENT_TYPE" => "application/x-www-form-urlencoded", + "CONTENT_TYPE" => request_encoder.content_type, "HTTP_ACCEPT" => accept } @@ -376,6 +381,7 @@ module ActionDispatch response = _mock_session.last_response @response = ActionDispatch::TestResponse.from_response(response) @response.request = @request + @response.response_parser = RequestEncoder.parser(@response.content_type) @html_document = nil @url_options = nil @@ -387,6 +393,56 @@ module ActionDispatch def build_full_uri(path, env) "#{env['rack.url_scheme']}://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{path}" end + + class RequestEncoder # :nodoc: + @encoders = {} + + attr_reader :response_parser + + def initialize(mime_name, param_encoder, response_parser, url_encoded_form = false) + @mime = Mime[mime_name] + + unless @mime + raise ArgumentError, "Can't register a request encoder for " \ + "unregistered MIME Type: #{mime_name}. See `Mime::Type.register`." + end + + @url_encoded_form = url_encoded_form + @path_format = ".#{@mime.symbol}" unless @url_encoded_form + @response_parser = response_parser || -> body { body } + @param_encoder = param_encoder || :"to_#{@mime.symbol}".to_proc + end + + def append_format_to(path) + path << @path_format unless @url_encoded_form + path + end + + def content_type + @mime.to_s + end + + def encode_params(params) + @param_encoder.call(params) + end + + def self.parser(content_type) + mime = Mime::Type.lookup(content_type) + encoder(mime ? mime.ref : nil).response_parser + end + + def self.encoder(name) + @encoders[name] || WWWFormEncoder + end + + def self.register_encoder(mime_name, param_encoder: nil, response_parser: nil) + @encoders[mime_name] = new(mime_name, param_encoder, response_parser) + end + + register_encoder :json, response_parser: -> body { JSON.parse(body) } + + WWWFormEncoder = new(:url_encoded_form, -> params { params }, nil, true) + end end module Runner @@ -643,6 +699,39 @@ module ActionDispatch # end # end # + # You can also test your JSON API easily by setting what the request should + # be encoded as: + # + # require 'test_helper' + # + # class ApiTest < ActionDispatch::IntegrationTest + # test 'creates articles' do + # assert_difference -> { Article.count } do + # post articles_path, params: { article: { title: 'Ahoy!' } }, as: :json + # end + # + # assert_response :success + # assert_equal({ id: Arcticle.last.id, title: 'Ahoy!' }, response.parsed_body) + # end + # end + # + # The `as` option sets the format to JSON, sets the content type to + # 'application/json' and encodes the parameters as JSON. + # + # Calling `parsed_body` on the response parses the response body as what + # the last request was encoded as. If the request wasn't encoded `as` something, + # it's the same as calling `body`. + # + # For any custom MIME Types you've registered, you can even add your own encoders with: + # + # ActionDispatch::IntegrationTest.register_encoder :wibble, + # param_encoder: -> params { params.to_wibble }, + # response_parser: -> body { body } + # + # Where `param_encoder` defines how the params should be encoded and + # `response_parser` defines how the response body should be parsed through + # `parsed_body`. + # # Consult the Rails Testing Guide for more. class IntegrationTest < ActiveSupport::TestCase @@ -671,5 +760,9 @@ module ActionDispatch def document_root_element html_document.root end + + def self.register_encoder(*args) + Integration::Session::RequestEncoder.register_encoder(*args) + end end end diff --git a/actionpack/lib/action_dispatch/testing/test_response.rb b/actionpack/lib/action_dispatch/testing/test_response.rb index 4b79a90242..9d4b73a43d 100644 --- a/actionpack/lib/action_dispatch/testing/test_response.rb +++ b/actionpack/lib/action_dispatch/testing/test_response.rb @@ -18,5 +18,11 @@ module ActionDispatch # Was there a server-side error? alias_method :error?, :server_error? + + attr_writer :response_parser # :nodoc: + + def parsed_body + @parsed_body ||= @response_parser.call(body) + end end end diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index 74c78dfa8e..7556f984f2 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -390,6 +390,11 @@ class CollectionCacheController < ActionController::Base @customers = [Customer.new('david', 1)] render partial: 'customers/commented_customer', collection: @customers, as: :customer end + + def index_with_callable_cache_key + @customers = [Customer.new('david', 1)] + render @customers, cache: -> customer { 'cached_david' } + end end class AutomaticCollectionCacheTest < ActionController::TestCase @@ -405,6 +410,7 @@ class AutomaticCollectionCacheTest < ActionController::TestCase def test_collection_fetches_cached_views get :index assert_equal 1, @controller.partial_rendered_times + assert_customer_cached 'david/1', 'david, 1' get :index assert_equal 1, @controller.partial_rendered_times @@ -412,8 +418,11 @@ class AutomaticCollectionCacheTest < ActionController::TestCase def test_preserves_order_when_reading_from_cache_plus_rendering get :index, params: { id: 2 } - get :index_ordered + assert_equal 1, @controller.partial_rendered_times + assert_select ':root', 'david, 2' + get :index_ordered + assert_equal 3, @controller.partial_rendered_times assert_select ':root', "david, 1\n david, 2\n david, 3" end @@ -430,6 +439,18 @@ class AutomaticCollectionCacheTest < ActionController::TestCase get :index_with_comment assert_equal 1, @controller.partial_rendered_times end + + def test_caching_with_callable_cache_key + get :index_with_callable_cache_key + assert_customer_cached 'cached_david', 'david, 1' + assert_customer_cached 'david/1', 'david, 1' + end + + private + def assert_customer_cached(key, content) + assert_match content, + ActionView::PartialRenderer.collection_cache.read("views/#{key}/7c228ab609f0baf0b1f2367469210937") + end end class FragmentCacheKeyTestController < CachingController diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index d0a1d1285f..ea50f05f4d 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -1126,3 +1126,69 @@ class IntegrationRequestsWithSessionSetup < ActionDispatch::IntegrationTest assert_equal({"user_name"=>"david"}, cookies.to_hash) end end + +class IntegrationRequestEncodersTest < ActionDispatch::IntegrationTest + class FooController < ActionController::Base + def foos_json + render json: params.permit(:foo) + end + + def foos_wibble + render plain: 'ok' + end + end + + def test_encoding_as_json + post_to_foos as: :json do + assert_response :success + assert_match 'foos_json.json', request.path + assert_equal 'application/json', request.content_type + assert_equal({ 'foo' => 'fighters' }, request.request_parameters) + assert_equal({ 'foo' => 'fighters' }, response.parsed_body) + end + end + + def test_encoding_as_without_mime_registration + assert_raise ArgumentError do + ActionDispatch::IntegrationTest.register_encoder :wibble + end + end + + def test_registering_custom_encoder + Mime::Type.register 'text/wibble', :wibble + + ActionDispatch::IntegrationTest.register_encoder(:wibble, + param_encoder: -> params { params }) + + post_to_foos as: :wibble do + assert_response :success + assert_match 'foos_wibble.wibble', request.path + assert_equal 'text/wibble', request.content_type + assert_equal Hash.new, request.request_parameters # Unregistered MIME Type can't be parsed. + assert_equal 'ok', response.parsed_body + end + ensure + Mime::Type.unregister :wibble + end + + def test_parsed_body_without_as_option + with_routing do |routes| + routes.draw { get ':action' => FooController } + + get '/foos_json.json', params: { foo: 'heyo' } + + assert_equal({ 'foo' => 'heyo' }, response.parsed_body) + end + end + + private + def post_to_foos(as:) + with_routing do |routes| + routes.draw { post ':action' => FooController } + + post "/foos_#{as}", params: { foo: 'fighters' }, as: as + + yield + end + end +end diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 256b90784a..bebe78c360 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -68,7 +68,7 @@ *Vasiliy Ermolovich* -* Add a `hidden_field` on the `collection_radio_buttons` to avoid raising a error +* Add a `hidden_field` on the `collection_radio_buttons` to avoid raising an error when the only input on the form is the `collection_radio_buttons`. *Mauro George* diff --git a/actionview/lib/action_view/dependency_tracker.rb b/actionview/lib/action_view/dependency_tracker.rb index 5a4c3ea3fe..7731773040 100644 --- a/actionview/lib/action_view/dependency_tracker.rb +++ b/actionview/lib/action_view/dependency_tracker.rb @@ -7,18 +7,20 @@ module ActionView def self.find_dependencies(name, template, view_paths = nil) tracker = @trackers[template.handler] - return [] unless tracker.present? + return [] unless tracker - if tracker.respond_to?(:supports_view_paths?) && tracker.supports_view_paths? - tracker.call(name, template, view_paths) - else - tracker.call(name, template) - end + tracker.call(name, template, view_paths) end def self.register_tracker(extension, tracker) handler = Template.handler_for_extension(extension) - @trackers[handler] = tracker + if tracker.respond_to?(:supports_view_paths?) + @trackers[handler] = tracker + else + @trackers[handler] = lambda { |name, template, _| + tracker.call(name, template) + } + end end def self.remove_tracker(handler) @@ -151,11 +153,11 @@ module ActionView def resolve_directories(wildcard_dependencies) return [] unless @view_paths - wildcard_dependencies.each_with_object([]) do |query, templates| - @view_paths.find_all_with_query(query).each do |template| - templates << "#{File.dirname(query)}/#{File.basename(template).split('.').first}" + wildcard_dependencies.flat_map { |query, templates| + @view_paths.find_all_with_query(query).map do |template| + "#{File.dirname(query)}/#{File.basename(template).split('.').first}" end - end + }.sort end def explicit_dependencies diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index 6f2f9ca53c..657026fa14 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -22,23 +22,23 @@ module ActionView # * <tt>finder</tt> - An instance of <tt>ActionView::LookupContext</tt> # * <tt>dependencies</tt> - An array of dependent views # * <tt>partial</tt> - Specifies whether the template is a partial - def digest(options) - options.assert_valid_keys(:name, :finder, :dependencies, :partial) + def digest(name:, finder:, **options) + options.assert_valid_keys(:dependencies, :partial) - cache_key = ([ options[:name], options[:finder].details_key.hash ].compact + Array.wrap(options[:dependencies])).join('.') + cache_key = ([ name, finder.details_key.hash ].compact + Array.wrap(options[:dependencies])).join('.') # this is a correctly done double-checked locking idiom # (Concurrent::Map's lookups have volatile semantics) @@cache[cache_key] || @@digest_monitor.synchronize do @@cache.fetch(cache_key) do # re-check under lock - compute_and_store_digest(cache_key, options) + compute_and_store_digest(cache_key, name, finder, options) end end end private - def compute_and_store_digest(cache_key, options) # called under @@digest_monitor lock - klass = if options[:partial] || options[:name].include?("/_") + def compute_and_store_digest(cache_key, name, finder, options) # called under @@digest_monitor lock + klass = if options[:partial] || name.include?("/_") # Prevent re-entry or else recursive templates will blow the stack. # There is no need to worry about other threads seeing the +false+ value, # as they will then have to wait for this thread to let go of the @@digest_monitor lock. @@ -48,7 +48,7 @@ module ActionView Digestor end - @@cache[cache_key] = stored_digest = klass.new(options).digest + @@cache[cache_key] = stored_digest = klass.new(name, finder, options).digest ensure # something went wrong or ActionView::Resolver.caching? is false, make sure not to corrupt the @@cache @@cache.delete_pair(cache_key, false) if pre_stored && !stored_digest @@ -57,37 +57,42 @@ module ActionView attr_reader :name, :finder, :options - def initialize(options) - @name, @finder = options.values_at(:name, :finder) - @options = options.except(:name, :finder) + def initialize(name, finder, options = {}) + @name, @finder = name, finder + @options = options end def digest Digest::MD5.hexdigest("#{source}-#{dependency_digest}").tap do |digest| - logger.try :debug, " Cache digest for #{template.inspect}: #{digest}" + logger.debug " Cache digest for #{template.inspect}: #{digest}" end rescue ActionView::MissingTemplate - logger.try :error, " Couldn't find template for digesting: #{name}" + logger.error " Couldn't find template for digesting: #{name}" '' end def dependencies DependencyTracker.find_dependencies(name, template, finder.view_paths) rescue ActionView::MissingTemplate - logger.try :error, " '#{name}' file doesn't exist, so no dependencies" + logger.error " '#{name}' file doesn't exist, so no dependencies" [] end def nested_dependencies dependencies.collect do |dependency| - dependencies = PartialDigestor.new(name: dependency, finder: finder).nested_dependencies + dependencies = PartialDigestor.new(dependency, finder).nested_dependencies dependencies.any? ? { dependency => dependencies } : dependency end end private + class NullLogger + def self.debug(_); end + def self.error(_); end + end + def logger - ActionView::Base.logger + ActionView::Base.logger || NullLogger end def logical_name diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb index 2562504896..42e7358a1d 100644 --- a/actionview/lib/action_view/helpers/tag_helper.rb +++ b/actionview/lib/action_view/helpers/tag_helper.rb @@ -154,6 +154,7 @@ module ActionView 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 diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb index 6a76d80c47..126f289f55 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -22,7 +22,7 @@ module ActionView def self.register_detail(name, &block) self.registered_details << name - initialize = registered_details.map { |n| "@details[:#{n}] = details[:#{n}] || default_#{n}" } + Accessors::DEFAULT_PROCS[name] = block Accessors.send :define_method, :"default_#{name}", &block Accessors.module_eval <<-METHOD, __FILE__, __LINE__ + 1 @@ -34,16 +34,12 @@ module ActionView value = value.present? ? Array(value) : default_#{name} _set_detail(:#{name}, value) if value != @details[:#{name}] end - - remove_possible_method :initialize_details - def initialize_details(details) - #{initialize.join("\n")} - end METHOD end # Holds accessors for the registered details. module Accessors #:nodoc: + DEFAULT_PROCS = {} end register_detail(:locale) do @@ -195,15 +191,23 @@ module ActionView include ViewPaths def initialize(view_paths, details = {}, prefixes = []) - @details, @details_key = {}, nil + @details_key = nil @cache = true @prefixes = prefixes @rendered_format = nil + @details = initialize_details({}, details) self.view_paths = view_paths - initialize_details(details) end + def initialize_details(target, details) + registered_details.each do |k| + target[k] = details[k] || Accessors::DEFAULT_PROCS[k].call + end + target + end + private :initialize_details + # Override formats= to expand ["*/*"] values and automatically # add :html as fallback to :js. def formats=(values) diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb index bdbf03191a..a9bd257e76 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -428,6 +428,8 @@ module ActionView layout = find_template(layout, @template_keys) end + collection_cache_eligible = automatic_cache_eligible? + partial_iteration = PartialIteration.new(@collection.size) locals[iteration] = partial_iteration @@ -436,6 +438,11 @@ module ActionView locals[counter] = partial_iteration.index content = template.render(view, locals) + + if collection_cache_eligible + collection_cache_rendered_partial(content, object) + end + content = layout.render(view, locals) { content } if layout partial_iteration.iterate! content diff --git a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb index 1147963882..4860f00243 100644 --- a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb +++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb @@ -15,13 +15,16 @@ module ActionView return yield unless cache_collection? keyed_collection = collection_by_cache_keys - partial_cache = collection_cache.read_multi(*keyed_collection.keys) + cached_partials = collection_cache.read_multi(*keyed_collection.keys) - @collection = keyed_collection.reject { |key, _| partial_cache.key?(key) }.values - rendered_partials = @collection.any? ? yield.dup : [] + @collection = keyed_collection.reject { |key, _| cached_partials.key?(key) }.values + rendered_partials = @collection.empty? ? [] : yield - fetch_or_cache_partial(partial_cache, order_by: keyed_collection.each_key) do - rendered_partials.shift + index = 0 + keyed_collection.map do |cache_key, _| + cached_partials.fetch(cache_key) do + rendered_partials[index].tap { index += 1 } + end end end @@ -30,12 +33,7 @@ module ActionView end def automatic_cache_eligible? - single_template_render? && !callable_cache_key? && - @template.eligible_for_collection_caching?(as: @options[:as]) - end - - def single_template_render? - @template # Template is only set when a collection renders one template. + @template && @template.eligible_for_collection_caching?(as: @options[:as]) end def callable_cache_key? @@ -55,15 +53,10 @@ module ActionView key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0. end - def fetch_or_cache_partial(cached_partials, order_by:) - cache_options = @options[:cache_options] || @locals[:cache_options] || {} - - order_by.map do |key| - cached_partials.fetch(key) do - yield.tap do |rendered_partial| - collection_cache.write(key, rendered_partial, cache_options) - end - end + def collection_cache_rendered_partial(rendered_partial, key_by) + if callable_cache_key? + key = expanded_cache_key(@options[:cache].call(key_by)) + collection_cache.write(key, rendered_partial, @options[:cache_options]) end end end diff --git a/actionview/lib/action_view/tasks/dependencies.rake b/actionview/lib/action_view/tasks/dependencies.rake index f394c319c1..9932ff8b6d 100644 --- a/actionview/lib/action_view/tasks/dependencies.rake +++ b/actionview/lib/action_view/tasks/dependencies.rake @@ -2,13 +2,13 @@ namespace :cache_digests do desc 'Lookup nested dependencies for TEMPLATE (like messages/show or comments/_comment.html)' task :nested_dependencies => :environment do abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present? - puts JSON.pretty_generate ActionView::Digestor.new(name: CacheDigests.template_name, finder: CacheDigests.finder).nested_dependencies + puts JSON.pretty_generate ActionView::Digestor.new(CacheDigests.template_name, CacheDigests.finder).nested_dependencies end desc 'Lookup first-level dependencies for TEMPLATE (like messages/show or comments/_comment.html)' task :dependencies => :environment do abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present? - puts JSON.pretty_generate ActionView::Digestor.new(name: CacheDigests.template_name, finder: CacheDigests.finder).dependencies + puts JSON.pretty_generate ActionView::Digestor.new(CacheDigests.template_name, CacheDigests.finder).dependencies end class CacheDigests diff --git a/actionview/lib/action_view/template/error.rb b/actionview/lib/action_view/template/error.rb index b03b197cb5..ccee785d3e 100644 --- a/actionview/lib/action_view/template/error.rb +++ b/actionview/lib/action_view/template/error.rb @@ -59,6 +59,9 @@ module ActionView class Error < ActionViewError #:nodoc: SOURCE_CODE_RADIUS = 3 + # Override to prevent #cause resetting during re-raise. + attr_reader :cause + def initialize(template, original_exception = nil) if original_exception ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \ @@ -67,6 +70,7 @@ module ActionView super($!.message) set_backtrace($!.backtrace) + @cause = $! @template, @sub_templates = template, nil end diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb index dde757b5a2..bfab97cf1e 100644 --- a/actionview/test/template/digestor_test.rb +++ b/actionview/test/template/digestor_test.rb @@ -13,34 +13,11 @@ class FixtureTemplate end end -class FixtureFinder +class FixtureFinder < ActionView::LookupContext FIXTURES_DIR = "#{File.dirname(__FILE__)}/../fixtures/digestor" - attr_reader :details, :view_paths - attr_accessor :formats - attr_accessor :variants - - def initialize - @details = {} - @view_paths = ActionView::PathSet.new(['digestor']) - @formats = [] - @variants = [] - end - - def details_key - details.hash - end - - def find(name, prefixes = [], partial = false, keys = [], options = {}) - partial_name = partial ? name.gsub(%r|/([^/]+)$|, '/_\1') : name - format = @formats.first.to_s - format += "+#{@variants.first}" if @variants.any? - - FixtureTemplate.new("digestor/#{partial_name}.#{format}.erb") - end - - def disable_cache(&block) - yield + def initialize(details = {}) + super(ActionView::PathSet.new(['digestor']), details, []) end end @@ -206,13 +183,14 @@ class TemplateDigestorTest < ActionView::TestCase def test_details_are_included_in_cache_key # Cache the template digest. + @finder = FixtureFinder.new({:formats => [:html]}) old_digest = digest("events/_event") # Change the template; the cached digest remains unchanged. change_template("events/_event") # The details are changed, so a new cache key is generated. - finder.details[:foo] = "bar" + @finder = FixtureFinder.new # The cache is busted. assert_not_equal old_digest, digest("events/_event") @@ -317,25 +295,24 @@ class TemplateDigestorTest < ActionView::TestCase yield - assert previous_digest != digest(template_name, options), "digest didn't change" + assert_not_equal previous_digest, digest(template_name, options), "digest didn't change" ActionView::Digestor.cache.clear end def digest(template_name, options = {}) options = options.dup - finder.formats = [:html] finder.variants = options.delete(:variants) || [] ActionView::Digestor.digest({ name: template_name, finder: finder }.merge(options)) end def dependencies(template_name) - ActionView::Digestor.new({ name: template_name, finder: finder }).dependencies + ActionView::Digestor.new(template_name, finder).dependencies end def nested_dependencies(template_name) - ActionView::Digestor.new({ name: template_name, finder: finder }).nested_dependencies + ActionView::Digestor.new(template_name, finder).nested_dependencies end def finder diff --git a/actionview/test/template/tag_helper_test.rb b/actionview/test/template/tag_helper_test.rb index 6f7a78ccef..f3956a31f6 100644 --- a/actionview/test/template/tag_helper_test.rb +++ b/actionview/test/template/tag_helper_test.rb @@ -173,4 +173,10 @@ class TagHelperTest < ActionView::TestCase 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 + + def test_link_to_data_nil_equal + div_type1 = content_tag(:div, 'test', { 'data-tooltip' => nil }) + div_type2 = content_tag(:div, 'test', { data: {tooltip: nil} }) + assert_dom_equal div_type1, div_type2 + end end diff --git a/activejob/lib/active_job/async_job.rb b/activejob/lib/active_job/async_job.rb index ed7a6e8d9b..417ace21d2 100644 --- a/activejob/lib/active_job/async_job.rb +++ b/activejob/lib/active_job/async_job.rb @@ -6,7 +6,7 @@ require 'concurrent/utility/processor_counter' module ActiveJob # == Active Job Async Job # - # When enqueueing jobs with Async Job each job will be executed asynchronously + # When enqueuing jobs with Async Job each job will be executed asynchronously # on a +concurrent-ruby+ thread pool. All job data is retained in memory. # Because job data is not saved to a persistent datastore there is no # additional infrastructure needed and jobs process quickly. The lack of diff --git a/activejob/lib/active_job/queue_adapters/async_adapter.rb b/activejob/lib/active_job/queue_adapters/async_adapter.rb index 3fc27f56e7..3d3c749883 100644 --- a/activejob/lib/active_job/queue_adapters/async_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/async_adapter.rb @@ -4,7 +4,7 @@ module ActiveJob module QueueAdapters # == Active Job Async adapter # - # When enqueueing jobs with the Async adapter the job will be executed + # When enqueuing jobs with the Async adapter the job will be executed # asynchronously using {AsyncJob}[http://api.rubyonrails.org/classes/ActiveJob/AsyncJob.html]. # # To use +AsyncJob+ set the queue_adapter config to +:async+. diff --git a/activejob/lib/active_job/queue_adapters/inline_adapter.rb b/activejob/lib/active_job/queue_adapters/inline_adapter.rb index 8ad5f4de07..0496f8449e 100644 --- a/activejob/lib/active_job/queue_adapters/inline_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/inline_adapter.rb @@ -2,7 +2,7 @@ module ActiveJob module QueueAdapters # == Active Job Inline adapter # - # When enqueueing jobs with the Inline adapter the job will be executed + # When enqueuing jobs with the Inline adapter the job will be executed # immediately. # # To use the Inline set the queue_adapter config to +:inline+. diff --git a/activejob/lib/active_job/test_helper.rb b/activejob/lib/active_job/test_helper.rb index 44ddfa5f69..ed0c05c1e5 100644 --- a/activejob/lib/active_job/test_helper.rb +++ b/activejob/lib/active_job/test_helper.rb @@ -58,7 +58,7 @@ module ActiveJob # The number of times a specific job is enqueued can be asserted. # # def test_logging_job - # assert_enqueued_jobs 2, only: LoggingJob do + # assert_enqueued_jobs 1, only: LoggingJob do # LoggingJob.perform_later # HelloJob.perform_later('jeremy') # end diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 0832652d32..f13a80195a 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,44 @@ +* Rework `ActiveRecord::Relation#last` + + 1. Never perform additional SQL on loaded relation + 2. Use SQL reverse order instead of loading relation if relation doesn't have limit + 3. Deprecated relation loading when SQL order can not be automatically reversed + + Topic.order("title").load.last(3) + # before: SELECT ... + # after: No SQL + + Topic.order("title").last + # before: SELECT * FROM `topics` + # after: SELECT * FROM `topics` ORDER BY `topics`.`title` DESC LIMIT 1 + + Topic.order("coalesce(author, title)").last + # before: SELECT * FROM `topics` + # after: Deprecation Warning for irreversible order + + *Bogdan Gusiev* + + +* Allow `joins` to be unscoped. + + Closes #13775. + + *Takashi Kokubun* + +* Add ActiveRecord `#second_to_last` and `#third_to_last` methods. + + *Brian Christian* + +* Added `numeric` helper into migrations. + + Example: + + create_table(:numeric_types) do |t| + t.numeric :numeric_type, precision: 10, scale: 2 + end + + *Mehmet Emin İNAÇ* + * Bumped the minimum supported version of PostgreSQL to >= 9.1. Both PG 9.0 and 8.4 are past their end of life date: http://www.postgresql.org/support/versioning/ @@ -602,7 +643,7 @@ *Ben Murphy*, *Matthew Draper* -* `bin/rake db:migrate` uses +* `bin/rails db:migrate` uses `ActiveRecord::Tasks::DatabaseTasks.migrations_paths` instead of `Migrator.migrations_paths`. diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 9f2c7292ea..2dca6b612e 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -136,6 +136,14 @@ module ActiveRecord first_nth_or_last(:forty_two, *args) end + def third_to_last(*args) + first_nth_or_last(:third_to_last, *args) + end + + def second_to_last(*args) + first_nth_or_last(:second_to_last, *args) + end + def last(*args) first_nth_or_last(:last, *args) end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index fe693cfbb6..2a9627a474 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -197,6 +197,16 @@ module ActiveRecord @association.forty_two(*args) end + # Same as #first except returns only the third-to-last record. + def third_to_last(*args) + @association.third_to_last(*args) + end + + # Same as #first except returns only the second-to-last record. + def second_to_last(*args) + @association.second_to_last(*args) + end + # Returns the last record, or the last +n+ records, from the collection. # If the collection is empty, the first form returns +nil+, and the second # form returns an empty array. diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index 708b3af5bd..c5fbe0d1d1 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -54,12 +54,18 @@ module ActiveRecord end scope_chain_index += 1 - relation = ActiveRecord::Relation.create( - klass, - table, - predicate_builder, - ) - scope_chain_items.concat [klass.send(:build_default_scope, relation)].compact + klass_scope = + if klass.current_scope + klass.current_scope.clone + else + relation = ActiveRecord::Relation.create( + klass, + table, + predicate_builder, + ) + klass.send(:build_default_scope, relation) + end + scope_chain_items.concat [klass_scope].compact rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right| left.merge right diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 423a93964e..e902eb7531 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -34,30 +34,6 @@ module ActiveRecord BLACKLISTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass) - class AttributeMethodCache - def initialize - @module = Module.new - @method_cache = Concurrent::Map.new - end - - def [](name) - @method_cache.compute_if_absent(name) do - safe_name = name.unpack('h*'.freeze).first - temp_method = "__temp__#{safe_name}" - ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name - @module.module_eval method_body(temp_method, safe_name), __FILE__, __LINE__ - @module.instance_method temp_method - end - end - - private - - # Override this method in the subclasses for method body. - def method_body(method_name, const_name) - raise NotImplementedError, "Subclasses must implement a method_body(method_name, const_name) method." - end - end - class GeneratedAttributeMethods < Module; end # :nodoc: module ClassMethods diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 5197e21fa4..ab2ecaa7c5 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -1,8 +1,11 @@ module ActiveRecord module AttributeMethods module Read - ReaderMethodCache = Class.new(AttributeMethodCache) { - private + extend ActiveSupport::Concern + + module ClassMethods + protected + # We want to generate the methods via module_eval rather than # define_method, because define_method is slower on dispatch. # Evaluating many similar methods may use more memory as the instruction @@ -21,21 +24,6 @@ module ActiveRecord # to allocate an object on each call to the attribute method. # Making it frozen means that it doesn't get duped when used to # key the @attributes in read_attribute. - def method_body(method_name, const_name) - <<-EOMETHOD - def #{method_name} - name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name} - _read_attribute(name) { |n| missing_attribute(n, caller) } - end - EOMETHOD - end - }.new - - extend ActiveSupport::Concern - - module ClassMethods - protected - def define_method_attribute(name) safe_name = name.unpack('h*'.freeze).first temp_method = "__temp__#{safe_name}" diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index bbf2a51a0e..5599b590ca 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -1,19 +1,6 @@ module ActiveRecord module AttributeMethods module Write - WriterMethodCache = Class.new(AttributeMethodCache) { - private - - def method_body(method_name, const_name) - <<-EOMETHOD - def #{method_name}(value) - name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name} - write_attribute(name, value) - end - EOMETHOD - end - }.new - extend ActiveSupport::Concern included do diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 854f9776a3..1f1b11eb68 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -179,7 +179,7 @@ module ActiveRecord # # If the +before_validation+ callback throws +:abort+, the process will be # aborted and {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] will return +false+. - # If {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] is called it will raise a ActiveRecord::RecordInvalid exception. + # If {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] is called it will raise an ActiveRecord::RecordInvalid exception. # Nothing will be appended to the errors object. # # == Canceling callbacks diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 7e0c9f7837..bb5119d64e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -27,10 +27,10 @@ module ActiveRecord end # Returns an ActiveRecord::Result instance. - def select_all(arel, name = nil, binds = []) + def select_all(arel, name = nil, binds = [], preparable: nil) arel, binds = binds_from_relation arel, binds sql = to_sql(arel, binds) - if arel.is_a?(String) + if arel.is_a?(String) && preparable.nil? preparable = false else preparable = visitor.preparable diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index 5e27cfe507..33dbab41cb 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -61,11 +61,11 @@ module ActiveRecord @query_cache.clear end - def select_all(arel, name = nil, binds = []) + def select_all(arel, name = nil, binds = [], preparable: nil) if @query_cache_enabled && !locked?(arel) arel, binds = binds_from_relation arel, binds sql = to_sql(arel, binds) - cache_sql(sql, binds) { super(sql, name, binds) } + cache_sql(sql, binds) { super(sql, name, binds, preparable: visitor.preparable) } else super end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 690e0ba957..cb10ca9929 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -182,6 +182,7 @@ module ActiveRecord end CODE end + alias_method :numeric, :decimal end # Represents the schema of an SQL table in an abstract way. This class @@ -436,6 +437,7 @@ module ActiveRecord # t.bigint # t.float # t.decimal + # t.numeric # t.datetime # t.timestamp # t.time diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index a95109fdae..b1b6044e72 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -14,7 +14,7 @@ module ActiveRecord def column_spec_for_primary_key(column) return if column.type == :integer - spec = { id: column.type.inspect } + spec = { id: schema_type(column).inspect } spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type].include?(key) }) end @@ -24,7 +24,7 @@ module ActiveRecord def prepare_column_options(column) spec = {} spec[:name] = column.name.inspect - spec[:type] = schema_type(column) + spec[:type] = schema_type(column).to_s spec[:null] = 'false' unless column.null if limit = schema_limit(column) @@ -57,7 +57,7 @@ module ActiveRecord private def schema_type(column) - column.type.to_s + column.type end def schema_limit(column) 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 983c4340c6..f0f855963a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -459,7 +459,7 @@ module ActiveRecord # The +type+ parameter is normally one of the migrations native types, # which is one of the following: # <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>, - # <tt>:integer</tt>, <tt>:bigint</tt>, <tt>:float</tt>, <tt>:decimal</tt>, + # <tt>:integer</tt>, <tt>:bigint</tt>, <tt>:float</tt>, <tt>:decimal</tt>, <tt>:numeric</tt>, # <tt>:datetime</tt>, <tt>:time</tt>, <tt>:date</tt>, # <tt>:binary</tt>, <tt>:boolean</tt>. # @@ -477,9 +477,9 @@ module ActiveRecord # Allows or disallows +NULL+ values in the column. This option could # have been named <tt>:null_allowed</tt>. # * <tt>:precision</tt> - - # Specifies the precision for a <tt>:decimal</tt> column. + # Specifies the precision for the <tt>:decimal</tt> and <tt>:numeric</tt> columns. # * <tt>:scale</tt> - - # Specifies the scale for a <tt>:decimal</tt> column. + # Specifies the scale for the <tt>:decimal</tt> and <tt>:numeric</tt> columns. # # Note: The precision is the total number of significant digits # and the scale is the number of digits that can be stored following diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb index 9dee3172f4..ccf5b6cadc 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb @@ -12,7 +12,7 @@ module ActiveRecord spec[:unsigned] = 'true' if column.unsigned? return if spec.empty? else - spec[:id] = column.type.inspect + spec[:id] = schema_type(column).inspect spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) }) end spec @@ -32,7 +32,7 @@ module ActiveRecord def schema_type(column) if column.sql_type == 'tinyblob' - 'blob' + :blob else super end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index c3c5b660fd..23b33a3555 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -42,7 +42,7 @@ module ActiveRecord end def supports_json? - version >= '5.7.8' + !mariadb? && version >= '5.7.8' end # HELPER METHODS =========================================== diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb index cc7721ddd8..b82bdb8b0c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb @@ -11,7 +11,7 @@ module ActiveRecord spec[:id] = ':uuid' spec[:default] = schema_default(column) || 'nil' else - spec[:id] = column.type.inspect + spec[:id] = schema_type(column).inspect spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) }) end spec @@ -35,9 +35,9 @@ module ActiveRecord return super unless column.serial? if column.bigint? - 'bigserial' + :bigserial else - 'serial' + :serial end end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 1f429cfd94..de5b42e987 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -1,7 +1,7 @@ module ActiveRecord module Querying delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, to: :all - delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, to: :all + delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!, to: :all delegate :first_or_create, :first_or_create!, :first_or_initialize, to: :all delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all delegate :find_by, :find_by!, to: :all @@ -35,8 +35,8 @@ module ActiveRecord # # Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date] # Post.find_by_sql ["SELECT body FROM comments WHERE author = :user_id OR approved_by = :user_id", { :user_id => user_id }] - def find_by_sql(sql, binds = []) - result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds) + def find_by_sql(sql, binds = [], preparable: nil) + result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds, preparable: preparable) column_types = result_set.column_types.dup columns_hash.each_key { |k| column_types.delete k } message_bus = ActiveSupport::Notifications.instrumenter diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index ab93d97eb3..956fe7c51e 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -124,8 +124,19 @@ module ActiveRecord end end - # Holds all the methods that are shared between MacroReflection, AssociationReflection - # and ThroughReflection + # Holds all the methods that are shared between MacroReflection and ThroughReflection. + # + # AbstractReflection + # MacroReflection + # AggregateReflection + # AssociationReflection + # HasManyReflection + # HasOneReflection + # BelongsToReflection + # HasAndBelongsToManyReflection + # ThroughReflection + # PolymorphicReflection + # RuntimeReflection class AbstractReflection # :nodoc: def table_name klass.table_name @@ -228,18 +239,14 @@ module ActiveRecord def alias_candidate(name) "#{plural_name}_#{name}" end + + def chain + collect_join_chain + end end # Base class for AggregateReflection and AssociationReflection. Objects of # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. - # - # MacroReflection - # AggregateReflection - # AssociationReflection - # HasManyReflection - # HasOneReflection - # BelongsToReflection - # ThroughReflection class MacroReflection < AbstractReflection # Returns the name of the macro. # @@ -418,7 +425,7 @@ module ActiveRecord # A chain of reflections from this one back to the owner. For more see the explanation in # ThroughReflection. - def chain + def collect_join_chain [self] end @@ -492,6 +499,18 @@ module ActiveRecord VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to] INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :polymorphic, :foreign_key] + def add_as_source(seed) + seed + end + + def add_as_polymorphic_through(reflection, seed) + seed + [PolymorphicReflection.new(self, reflection)] + end + + def add_as_through(seed) + seed + [self] + end + protected def actual_source_reflection # FIXME: this is a horrible name @@ -739,19 +758,8 @@ module ActiveRecord # # => [<ActiveRecord::Reflection::ThroughReflection: @delegate_reflection=#<ActiveRecord::Reflection::HasManyReflection: @name=:tags...>, # <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @options={}, @active_record=Post>] # - def chain - @chain ||= begin - a = source_reflection.chain - b = through_reflection.chain.map(&:dup) - - if options[:source_type] - b[0] = PolymorphicReflection.new(b[0], self) - end - - chain = a + b - chain[0] = self # Use self so we don't lose the information from :source_type - chain - end + def collect_join_chain + collect_join_reflections [self] end # This is for clearing cache on the reflection. Useful for tests that need to compare @@ -910,6 +918,27 @@ module ActiveRecord scope_chain end + def add_as_source(seed) + collect_join_reflections seed + end + + def add_as_polymorphic_through(reflection, seed) + collect_join_reflections(seed + [PolymorphicReflection.new(self, reflection)]) + end + + def add_as_through(seed) + collect_join_reflections(seed + [self]) + end + + def collect_join_reflections(seed) + a = source_reflection.add_as_source seed + if options[:source_type] + through_reflection.add_as_polymorphic_through self, a + else + through_reflection.add_as_through a + end + end + protected def actual_source_reflection # FIXME: this is a horrible name diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index d48bcea28a..8b114c6bdf 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -145,15 +145,21 @@ module ActiveRecord # # [#<Person id:4>, #<Person id:3>, #<Person id:2>] def last(limit = nil) - if limit - if order_values.empty? && primary_key - order(arel_attribute(primary_key).desc).limit(limit).reverse - else - to_a.last(limit) - end - else - find_last - end + return find_last(limit) if loaded? || limit_value + + result = limit(limit || 1) + result.order!(arel_attribute(primary_key)) if order_values.empty? && primary_key + result = result.reverse_order! + + limit ? result.reverse : result.first + rescue ActiveRecord::IrreversibleOrderError + ActiveSupport::Deprecation.warn(<<-WARNING.squish) + Finding a last element by loading the relation when SQL ORDER + can not be reversed is deprecated. + Rails 5.1 will raise ActiveRecord::IrreversibleOrderError in this case. + Please call `to_a.last` if you still want to load the relation. + WARNING + find_last(limit) end # Same as #last but raises ActiveRecord::RecordNotFound if no record @@ -242,6 +248,38 @@ module ActiveRecord find_nth! 41 end + # Find the third-to-last record. + # If no order is defined it will order by primary key. + # + # Person.third_to_last # returns the third-to-last object fetched by SELECT * FROM people + # Person.offset(3).third_to_last # returns the third-to-last object from OFFSET 3 + # Person.where(["user_name = :u", { u: user_name }]).third_to_last + def third_to_last + find_nth(-3) + end + + # Same as #third_to_last but raises ActiveRecord::RecordNotFound if no record + # is found. + def third_to_last! + find_nth!(-3) + end + + # Find the second-to-last record. + # If no order is defined it will order by primary key. + # + # Person.second_to_last # returns the second-to-last object fetched by SELECT * FROM people + # Person.offset(3).second_to_last # returns the second-to-last object from OFFSET 3 + # Person.where(["user_name = :u", { u: user_name }]).second_to_last + def second_to_last + find_nth(-2) + end + + # Same as #second_to_last but raises ActiveRecord::RecordNotFound if no record + # is found. + def second_to_last! + find_nth!(-2) + end + # Returns true if a record exists in the table that matches the +id+ or # conditions given, or false otherwise. The argument can take six forms: # @@ -298,7 +336,7 @@ module ActiveRecord end # This method is called whenever no records are found with either a single - # id or multiple ids and raises a ActiveRecord::RecordNotFound exception. + # id or multiple ids and raises an ActiveRecord::RecordNotFound exception. # # The error message is different depending on whether a single id or # multiple ids are provided. If multiple ids are provided, then the number @@ -546,5 +584,9 @@ module ActiveRecord find_nth_with_limit(index, limit) end end + + def find_last(limit) + limit ? to_a.last(limit) : to_a.last + end end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 91bfa4d131..91d486e902 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1112,6 +1112,8 @@ module ActiveRecord order_query.flat_map do |o| case o + when Arel::Attribute + o.desc when Arel::Nodes::Ordering o.reverse when String diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 2bfc5ff7ae..a9e1fd0dad 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -60,7 +60,7 @@ module ActiveRecord end # Accepts an array, or string of SQL conditions and sanitizes - # them into a valid SQL fragment for a ORDER clause. + # them into a valid SQL fragment for an ORDER clause. # # sanitize_sql_for_order(["field(id, ?)", [1,3,2]]) # # => "field(id, 1,3,2)" diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb index f6b0efb88a..6c896ccea6 100644 --- a/activerecord/lib/active_record/statement_cache.rb +++ b/activerecord/lib/active_record/statement_cache.rb @@ -106,7 +106,7 @@ module ActiveRecord sql = query_builder.sql_for bind_values, connection - klass.find_by_sql sql, bind_values + klass.find_by_sql(sql, bind_values, preparable: true) end alias :call :execute end diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 6677e6dc5f..ecaf04e39e 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -45,7 +45,7 @@ module ActiveRecord end # Attempts to save the record just like {ActiveRecord::Base#save}[rdoc-ref:Base#save] but - # will raise a ActiveRecord::RecordInvalid exception instead of returning +false+ if the record is not valid. + # will raise an ActiveRecord::RecordInvalid exception instead of returning +false+ if the record is not valid. def save!(options={}) perform_validations(options) ? super : raise_validation_error end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index a376e2a17f..f0aa4521b5 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -19,7 +19,7 @@ module ActiveRecord relation = build_relation(finder_class, table, attribute, value) if record.persisted? && finder_class.primary_key.to_s != attribute.to_s if finder_class.primary_key - relation = relation.where.not(finder_class.primary_key => record.id) + relation = relation.where.not(finder_class.primary_key => record.id_was) else raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.") end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index ecaa521283..e975f4fbdd 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -408,6 +408,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end assert_no_queries do + bulbs.third_to_last() + bulbs.third_to_last({}) + end + + assert_no_queries do + bulbs.second_to_last() + bulbs.second_to_last({}) + end + + assert_no_queries do bulbs.last() bulbs.last({}) end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 75a74c052d..5b41630cd8 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -101,7 +101,7 @@ class FinderTest < ActiveRecord::TestCase def test_find_with_ids_where_and_limit # Please note that Topic 1 is the only not approved so - # if it were among the first 3 it would raise a ActiveRecord::RecordNotFound + # if it were among the first 3 it would raise an ActiveRecord::RecordNotFound records = Topic.where(approved: true).limit(3).find([3,2,5,1,4]) assert_equal 3, records.size assert_equal 'The Third Topic of the day', records[0].title @@ -516,16 +516,44 @@ class FinderTest < ActiveRecord::TestCase assert_equal Topic.order("title").to_a.last(2), Topic.order("title").last(2) end - def test_last_with_integer_and_order_should_not_use_sql_limit - query = assert_sql { Topic.order("title").last(5).entries } - assert_equal 1, query.length - assert_no_match(/LIMIT/, query.first) + def test_last_with_integer_and_order_should_use_sql_limit + relation = Topic.order("title") + assert_queries(1) { relation.last(5) } + assert !relation.loaded? end - def test_last_with_integer_and_reorder_should_not_use_sql_limit - query = assert_sql { Topic.reorder("title").last(5).entries } - assert_equal 1, query.length - assert_no_match(/LIMIT/, query.first) + def test_last_with_integer_and_reorder_should_use_sql_limit + relation = Topic.reorder("title") + assert_queries(1) { relation.last(5) } + assert !relation.loaded? + end + + def test_last_on_loaded_relation_should_not_use_sql + relation = Topic.limit(10).load + assert_no_queries do + relation.last + relation.last(2) + end + end + + def test_last_with_irreversible_order + assert_deprecated do + Topic.order("coalesce(author_name, title)").last + end + end + + def test_last_on_relation_with_limit_and_offset + post = posts('sti_comments') + + comments = post.comments.order(id: :asc) + assert_equal comments.limit(2).to_a.last, comments.limit(2).last + assert_equal comments.limit(2).to_a.last(2), comments.limit(2).last(2) + assert_equal comments.limit(2).to_a.last(3), comments.limit(2).last(3) + + comments = comments.offset(1) + assert_equal comments.limit(2).to_a.last, comments.limit(2).last + assert_equal comments.limit(2).to_a.last(2), comments.limit(2).last(2) + assert_equal comments.limit(2).to_a.last(3), comments.limit(2).last(3) end def test_take_and_first_and_last_with_integer_should_return_an_array diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 9b4394377f..bae0467e72 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -192,8 +192,6 @@ class MigrationTest < ActiveRecord::TestCase # of 0, they take on the compile-time limit for precision and scale, # so the following should succeed unless you have used really wacky # compilation options - # - SQLite2 has the default behavior of preserving all data sent in, - # so this happens there too assert_kind_of BigDecimal, b.value_of_e assert_equal BigDecimal("2.7182818284590452353602875"), b.value_of_e elsif current_adapter?(:SQLite3Adapter) diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index ad5ca70f36..c918cbdef5 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -374,6 +374,18 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal 10, DeveloperCalledJamis.unscoped { DeveloperCalledJamis.poor }.length end + def test_default_scope_with_joins + assert_equal Comment.where(post_id: SpecialPostWithDefaultScope.pluck(:id)).count, + Comment.joins(:special_post_with_default_scope).count + assert_equal Comment.where(post_id: Post.pluck(:id)).count, + Comment.joins(:post).count + end + + def test_unscoped_with_joins_should_not_have_default_scope + assert_equal SpecialPostWithDefaultScope.unscoped { Comment.joins(:special_post_with_default_scope).to_a }, + Comment.joins(:post).to_a + end + def test_default_scope_select_ignored_by_aggregations assert_equal DeveloperWithSelect.all.to_a.count, DeveloperWithSelect.count end diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 7502a55391..e601c53dbf 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -469,4 +469,15 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert_match(/\AUnknown primary key for table dashboards in model/, e.message) assert_match(/Can not validate uniqueness for persisted record without primary key.\z/, e.message) end + + def test_validate_uniqueness_ignores_itself_when_primary_key_changed + Topic.validates_uniqueness_of(:title) + + t = Topic.new("title" => "This is a unique title") + assert t.save, "Should save t as unique" + + t.id += 1 + assert t.valid?, "Should be valid" + assert t.save, "Should still save t as unique" + end end diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index b38b17e90e..dcc5c5a310 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -14,6 +14,7 @@ class Comment < ActiveRecord::Base has_many :ratings belongs_to :first_post, :foreign_key => :post_id + belongs_to :special_post_with_default_scope, foreign_key: :post_id has_many :children, :class_name => 'Comment', :foreign_key => :parent_id belongs_to :parent, :class_name => 'Comment', :counter_cache => :children_count diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index bd333da081..3d4cc8fae6 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,7 @@ +* Add `Array#second_to_last` and `Array#third_to_last` methods. + + *Brian Christian* + * Fix regression in `Hash#dig` for HashWithIndifferentAccess. *Jon Moss* diff --git a/activesupport/lib/active_support/concurrency/share_lock.rb b/activesupport/lib/active_support/concurrency/share_lock.rb index 8e4ca272ba..54244317e4 100644 --- a/activesupport/lib/active_support/concurrency/share_lock.rb +++ b/activesupport/lib/active_support/concurrency/share_lock.rb @@ -6,12 +6,6 @@ module ActiveSupport # A share/exclusive lock, otherwise known as a read/write lock. # # https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock - #-- - # Note that a pending Exclusive lock attempt does not block incoming - # Share requests (i.e., we are "read-preferring"). That seems - # consistent with the behavior of "loose" upgrades, but may be the - # wrong choice otherwise: it nominally reduces the possibility of - # deadlock by risking starvation instead. class ShareLock include MonitorMixin @@ -51,7 +45,7 @@ module ActiveSupport if busy_for_exclusive?(purpose) return false if no_wait - yield_shares(purpose, compatible) do + yield_shares(purpose: purpose, compatible: compatible, block_share: true) do @cv.wait_while { busy_for_exclusive?(purpose) } end end @@ -73,18 +67,28 @@ module ActiveSupport if @exclusive_depth == 0 @exclusive_thread = nil - yield_shares(nil, compatible) do - @cv.broadcast - @cv.wait_while { @exclusive_thread || eligible_waiters?(compatible) } + if eligible_waiters?(compatible) + yield_shares(compatible: compatible, block_share: true) do + @cv.wait_while { @exclusive_thread || eligible_waiters?(compatible) } + end end + @cv.broadcast end end end - def start_sharing(purpose: :share) + def start_sharing synchronize do - if @sharing[Thread.current] == 0 && @exclusive_thread != Thread.current && busy_for_sharing?(purpose) - @cv.wait_while { busy_for_sharing?(purpose) } + if @sharing[Thread.current] > 0 || @exclusive_thread == Thread.current + # We already hold a lock; nothing to wait for + elsif @waiting[Thread.current] + # We're nested inside a +yield_shares+ call: we'll resume as + # soon as there isn't an exclusive lock in our way + @cv.wait_while { @exclusive_thread } + else + # This is an initial / outermost share call: any outstanding + # requests for an exclusive lock get to go first + @cv.wait_while { busy_for_sharing?(false) } end @sharing[Thread.current] += 1 end @@ -127,6 +131,40 @@ module ActiveSupport end end + # Temporarily give up all held Share locks while executing the + # supplied block, allowing any +compatible+ exclusive lock request + # to proceed. + def yield_shares(purpose: nil, compatible: [], block_share: false) + loose_shares = previous_wait = nil + synchronize do + if loose_shares = @sharing.delete(Thread.current) + if previous_wait = @waiting[Thread.current] + purpose = nil unless purpose == previous_wait[0] + compatible &= previous_wait[1] + end + compatible |= [false] unless block_share + @waiting[Thread.current] = [purpose, compatible] + + @cv.broadcast + end + end + + begin + yield + ensure + synchronize do + @cv.wait_while { @exclusive_thread && @exclusive_thread != Thread.current } + + if previous_wait + @waiting[Thread.current] = previous_wait + else + @waiting.delete Thread.current + end + @sharing[Thread.current] = loose_shares if loose_shares + end + end + end + private # Must be called within synchronize @@ -143,18 +181,6 @@ module ActiveSupport def eligible_waiters?(compatible) @waiting.any? { |t, (p, _)| compatible.include?(p) && @waiting.all? { |t2, (_, c2)| t == t2 || c2.include?(p) } } end - - def yield_shares(purpose, compatible) - loose_shares = @sharing.delete(Thread.current) - @waiting[Thread.current] = [purpose, compatible] if loose_shares - - begin - yield - ensure - @waiting.delete Thread.current - @sharing[Thread.current] = loose_shares if loose_shares - end - end end end end diff --git a/activesupport/lib/active_support/core_ext/array/access.rb b/activesupport/lib/active_support/core_ext/array/access.rb index 3177d8498e..37d833887a 100644 --- a/activesupport/lib/active_support/core_ext/array/access.rb +++ b/activesupport/lib/active_support/core_ext/array/access.rb @@ -73,4 +73,18 @@ class Array def forty_two self[41] end + + # Equal to <tt>self[-3]</tt>. + # + # %w( a b c d e ).third_to_last # => "c" + def third_to_last + self[-3] + end + + # Equal to <tt>self[-2]</tt>. + # + # %w( a b c d e ).second_to_last # => "d" + def second_to_last + self[-2] + end end diff --git a/activesupport/lib/active_support/core_ext/string/output_safety.rb b/activesupport/lib/active_support/core_ext/string/output_safety.rb index 6251f34daf..43b9fd4bf7 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -171,7 +171,7 @@ module ActiveSupport #:nodoc: original_concat(value) end - def initialize(*) + def initialize(str = '') @html_safe = true super end diff --git a/activesupport/lib/active_support/dependencies/interlock.rb b/activesupport/lib/active_support/dependencies/interlock.rb index b6a1b25eee..47bcecbb35 100644 --- a/activesupport/lib/active_support/dependencies/interlock.rb +++ b/activesupport/lib/active_support/dependencies/interlock.rb @@ -42,6 +42,12 @@ module ActiveSupport #:nodoc: yield end end + + def permit_concurrent_loads + @lock.yield_shares(compatible: [:load]) do + yield + end + end end end end diff --git a/activesupport/lib/active_support/deprecation/behaviors.rb b/activesupport/lib/active_support/deprecation/behaviors.rb index 0de891f1a2..dc24e2d0e1 100644 --- a/activesupport/lib/active_support/deprecation/behaviors.rb +++ b/activesupport/lib/active_support/deprecation/behaviors.rb @@ -11,7 +11,7 @@ module ActiveSupport DEFAULT_BEHAVIORS = { raise: ->(message, callstack) { e = DeprecationException.new(message) - e.set_backtrace(callstack) + e.set_backtrace(callstack.map(&:to_s)) raise e }, diff --git a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb index 6f0ad445fc..0cb2d4d22e 100644 --- a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb @@ -80,7 +80,7 @@ module ActiveSupport # example.old_request.to_s # # => DEPRECATION WARNING: @request is deprecated! Call request.to_s instead of # @request.to_s - # (Bactrace information…) + # (Backtrace information…) # "special_request" # # example.request.to_s @@ -118,7 +118,7 @@ module ActiveSupport # # PLANETS.map { |planet| planet.capitalize } # # => DEPRECATION WARNING: PLANETS is deprecated! Use PLANETS_POST_2006 instead. - # (Bactrace information…) + # (Backtrace information…) # ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"] class DeprecatedConstantProxy < DeprecationProxy def initialize(old_const, new_const, deprecator = ActiveSupport::Deprecation.instance) diff --git a/activesupport/test/core_ext/array/access_test.rb b/activesupport/test/core_ext/array/access_test.rb index 3f1e0c4cb4..1d834667f0 100644 --- a/activesupport/test/core_ext/array/access_test.rb +++ b/activesupport/test/core_ext/array/access_test.rb @@ -26,6 +26,8 @@ class AccessTest < ActiveSupport::TestCase assert_equal array[3], array.fourth assert_equal array[4], array.fifth assert_equal array[41], array.forty_two + assert_equal array[-3], array.third_to_last + assert_equal array[-2], array.second_to_last end def test_without diff --git a/activesupport/test/deprecation_test.rb b/activesupport/test/deprecation_test.rb index 58a0a3964d..45c88b79cb 100644 --- a/activesupport/test/deprecation_test.rb +++ b/activesupport/test/deprecation_test.rb @@ -105,13 +105,13 @@ class DeprecationTest < ActiveSupport::TestCase ActiveSupport::Deprecation.behavior = :raise message = 'Revise this deprecated stuff now!' - callstack = %w(foo bar baz) + callstack = caller_locations e = assert_raise ActiveSupport::DeprecationException do ActiveSupport::Deprecation.behavior.first.call(message, callstack) end assert_equal message, e.message - assert_equal callstack, e.backtrace + assert_equal callstack.map(&:to_s), e.backtrace.map(&:to_s) end def test_default_stderr_behavior diff --git a/activesupport/test/share_lock_test.rb b/activesupport/test/share_lock_test.rb index 12953d99a6..acefa185a8 100644 --- a/activesupport/test/share_lock_test.rb +++ b/activesupport/test/share_lock_test.rb @@ -287,6 +287,173 @@ class ShareLockTest < ActiveSupport::TestCase assert_threads_not_stuck threads end + def test_manual_yield + ready = Concurrent::CyclicBarrier.new(2) + done = Concurrent::CyclicBarrier.new(2) + + threads = [ + Thread.new do + @lock.sharing do + ready.wait + @lock.exclusive(purpose: :x) {} + done.wait + end + end, + + Thread.new do + @lock.sharing do + ready.wait + @lock.yield_shares(compatible: [:x]) do + done.wait + end + end + end, + ] + + assert_threads_not_stuck threads + end + + def test_manual_incompatible_yield + ready = Concurrent::CyclicBarrier.new(2) + done = Concurrent::CyclicBarrier.new(2) + + threads = [ + Thread.new do + @lock.sharing do + ready.wait + @lock.exclusive(purpose: :x) {} + done.wait + end + end, + + Thread.new do + @lock.sharing do + ready.wait + @lock.yield_shares(compatible: [:y]) do + done.wait + end + end + end, + ] + + assert_threads_stuck threads + ensure + threads.each(&:kill) if threads + end + + def test_manual_recursive_yield + ready = Concurrent::CyclicBarrier.new(2) + done = Concurrent::CyclicBarrier.new(2) + do_nesting = Concurrent::CountDownLatch.new + + threads = [ + Thread.new do + @lock.sharing do + ready.wait + @lock.exclusive(purpose: :x) {} + done.wait + end + end, + + Thread.new do + @lock.sharing do + @lock.yield_shares(compatible: [:x]) do + @lock.sharing do + ready.wait + do_nesting.wait + @lock.yield_shares(compatible: [:x, :y]) do + done.wait + end + end + end + end + end + ] + + assert_threads_stuck threads + do_nesting.count_down + + assert_threads_not_stuck threads + end + + def test_manual_recursive_yield_cannot_expand_outer_compatible + ready = Concurrent::CyclicBarrier.new(2) + do_compatible_nesting = Concurrent::CountDownLatch.new + in_compatible_nesting = Concurrent::CountDownLatch.new + + incompatible_thread = Thread.new do + @lock.sharing do + ready.wait + @lock.exclusive(purpose: :x) {} + end + end + + yield_shares_thread = Thread.new do + @lock.sharing do + ready.wait + @lock.yield_shares(compatible: [:y]) do + do_compatible_nesting.wait + @lock.sharing do + @lock.yield_shares(compatible: [:x, :y]) do + in_compatible_nesting.wait + end + end + end + end + end + + assert_threads_stuck incompatible_thread + do_compatible_nesting.count_down + assert_threads_stuck incompatible_thread + in_compatible_nesting.count_down + assert_threads_not_stuck [yield_shares_thread, incompatible_thread] + end + + def test_manual_recursive_yield_restores_previous_compatible + ready = Concurrent::CyclicBarrier.new(2) + do_nesting = Concurrent::CountDownLatch.new + after_nesting = Concurrent::CountDownLatch.new + + incompatible_thread = Thread.new do + ready.wait + @lock.exclusive(purpose: :z) {} + end + + recursive_yield_shares_thread = Thread.new do + @lock.sharing do + ready.wait + @lock.yield_shares(compatible: [:y]) do + do_nesting.wait + @lock.sharing do + @lock.yield_shares(compatible: [:x, :y]) {} + end + after_nesting.wait + end + end + end + + assert_threads_stuck incompatible_thread + do_nesting.count_down + assert_threads_stuck incompatible_thread + + compatible_thread = Thread.new do + @lock.exclusive(purpose: :y) {} + end + assert_threads_not_stuck compatible_thread + + post_nesting_incompatible_thread = Thread.new do + @lock.exclusive(purpose: :x) {} + end + assert_threads_stuck post_nesting_incompatible_thread + + after_nesting.count_down + assert_threads_not_stuck recursive_yield_shares_thread + # post_nesting_incompatible_thread can now proceed + assert_threads_not_stuck post_nesting_incompatible_thread + # assert_threads_not_stuck can now proceed + assert_threads_not_stuck incompatible_thread + end + def test_in_shared_section_incompatible_non_upgrading_threads_cannot_preempt_upgrading_threads scratch_pad = [] scratch_pad_mutex = Mutex.new diff --git a/guides/source/4_2_release_notes.md b/guides/source/4_2_release_notes.md index 8a59007420..73e6c2c05b 100644 --- a/guides/source/4_2_release_notes.md +++ b/guides/source/4_2_release_notes.md @@ -405,7 +405,7 @@ Please refer to the [Changelog][railties] for detailed changes. url: http://localhost:3001 namespace: my_app_development - # config/production.rb + # config/environments/production.rb Rails.application.configure do config.middleware.use ExceptionNotifier, config_for(:exception_notification) end diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index cd2c13e8c1..91ea4efb55 100644 --- a/guides/source/action_mailer_basics.md +++ b/guides/source/action_mailer_basics.md @@ -222,7 +222,7 @@ class SendWeeklySummary end ``` -The method `welcome_email` returns a `ActionMailer::MessageDelivery` object which +The method `welcome_email` returns an `ActionMailer::MessageDelivery` object which can then just be told `deliver_now` or `deliver_later` to send itself out. The `ActionMailer::MessageDelivery` object is just a wrapper around a `Mail::Message`. If you want to inspect, alter or do anything else with the `Mail::Message` object you can @@ -278,7 +278,7 @@ different, encode your content and pass in the encoded content and encoding in a ```ruby encoded_content = SpecialEncode(File.read('/path/to/filename.jpg')) attachments['filename.jpg'] = { - mime_type: 'application/x-gzip', + mime_type: 'application/gzip', encoding: 'SpecialEncoding', content: encoded_content } diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md index 543937f8e5..5e6eae1071 100644 --- a/guides/source/action_view_overview.md +++ b/guides/source/action_view_overview.md @@ -1524,7 +1524,7 @@ Localized Views Action View has the ability to render different templates depending on the current locale. -For example, suppose you have a `ArticlesController` with a show action. By default, calling this action will render `app/views/articles/show.html.erb`. But if you set `I18n.locale = :de`, then `app/views/articles/show.de.html.erb` will be rendered instead. If the localized template isn't present, the undecorated version will be used. This means you're not required to provide localized views for all cases, but they will be preferred and used if available. +For example, suppose you have an `ArticlesController` with a show action. By default, calling this action will render `app/views/articles/show.html.erb`. But if you set `I18n.locale = :de`, then `app/views/articles/show.de.html.erb` will be rendered instead. If the localized template isn't present, the undecorated version will be used. This means you're not required to provide localized views for all cases, but they will be preferred and used if available. You can use the same technique to localize the rescue files in your public directory. For example, setting `I18n.locale = :de` and creating `public/500.de.html` and `public/404.de.html` would allow you to have localized rescue pages. diff --git a/guides/source/active_model_basics.md b/guides/source/active_model_basics.md index c05e20aceb..a8199e5d02 100644 --- a/guides/source/active_model_basics.md +++ b/guides/source/active_model_basics.md @@ -319,7 +319,7 @@ person.serializable_hash # => {"name"=>"Bob"} #### ActiveModel::Serializers -Rails provides a `ActiveModel::Serializers::JSON` serializer. +Rails provides an `ActiveModel::Serializers::JSON` serializer. This module automatically include the `ActiveModel::Serialization`. ##### ActiveModel::Serializers::JSON diff --git a/guides/source/active_record_migrations.md b/guides/source/active_record_migrations.md index 83f4b951ee..bd7dbd0f11 100644 --- a/guides/source/active_record_migrations.md +++ b/guides/source/active_record_migrations.md @@ -883,7 +883,7 @@ Changing Existing Migrations ---------------------------- Occasionally you will make a mistake when writing a migration. If you have -already run the migration then you cannot just edit the migration and run the +already run the migration, then you cannot just edit the migration and run the migration again: Rails thinks it has already run the migration and so will do nothing when you run `rails db:migrate`. You must rollback the migration (for example with `bin/rails db:rollback`), edit your migration and then run @@ -933,7 +933,7 @@ There are two ways to dump the schema. This is set in `config/application.rb` by the `config.active_record.schema_format` setting, which may be either `:sql` or `:ruby`. -If `:ruby` is selected then the schema is stored in `db/schema.rb`. If you look +If `:ruby` is selected, then the schema is stored in `db/schema.rb`. If you look at this file you'll find that it looks an awful lot like one very big migration: diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index 63658e7c8b..1235c04c50 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -1296,6 +1296,28 @@ Using a class method is the preferred way to accept arguments for scopes. These category.articles.created_before(time) ``` +### Using conditionals + +Your scope can utilize conditionals: + +```ruby +class Article < ApplicationRecord + scope :created_before, ->(time) { where("created_at < ?", time) if time.present? } +end +``` + +Like the other examples, this will behave similarly to a class method. + +```ruby +class Article < ApplicationRecord + def self.created_before(time) + where("created_at < ?", time) if time.present? + end +end +``` + +However, there is one important caveat: A scope will always return an `ActiveRecord::Relation` object, even if the conditional evaluates to `false`, whereas a class method, will return `nil`. This can cause `NoMethodError` when chaining class methods with conditionals, if any of the conditionals return `false`. + ### Applying a default scope If we wish for a scope to be applied across all queries to the model we can use the diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md index 0aca6db9b6..10122629b2 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -2240,7 +2240,7 @@ Similarly, `from` returns the tail from the element at the passed index to the e [].from(0) # => [] ``` -The methods `second`, `third`, `fourth`, and `fifth` return the corresponding element (`first` is built-in). Thanks to social wisdom and positive constructiveness all around, `forty_two` is also available. +The methods `second`, `third`, `fourth`, and `fifth` return the corresponding element, as do `second_to_last` and `third_to_last` (`first` and `last` are built-in). Thanks to social wisdom and positive constructiveness all around, `forty_two` is also available. ```ruby %w(a b c d).third # => "c" diff --git a/guides/source/api_app.md b/guides/source/api_app.md index 64b6bb64f2..563214896a 100644 --- a/guides/source/api_app.md +++ b/guides/source/api_app.md @@ -135,6 +135,8 @@ If you're building a Rails application that will be an API server first and foremost, you can start with a more limited subset of Rails and add in features as needed. +### Creating a new application + You can generate a new api Rails app: ```bash @@ -153,6 +155,8 @@ This will do three main things for you: - Configure the generators to skip generating views, helpers and assets when you generate a new resource. +### Changing an existing application + If you want to take an existing application and make it an API one, read the following steps. diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index 439f2bef3a..5dd54bf8ad 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -21,8 +21,11 @@ What is the Asset Pipeline? The asset pipeline provides a framework to concatenate and minify or compress JavaScript and CSS assets. It also adds the ability to write these assets in other languages and pre-processors such as CoffeeScript, Sass and ERB. +It allows assets in your application to be automatically combined with assets +from other gems. For example, jquery-rails includes a copy of jquery.js +and enables AJAX features in Rails. -The asset pipeline is technically no longer a core feature of Rails 4, it has +The asset pipeline is technically no longer a core feature from Rails 4 onwards -- it has been extracted out of the framework into the [sprockets-rails](https://github.com/rails/sprockets-rails) gem. @@ -35,7 +38,7 @@ passing the `--skip-sprockets` option. rails new appname --skip-sprockets ``` -Rails 4 automatically adds the `sass-rails`, `coffee-rails` and `uglifier` +Rails automatically adds the `sass-rails`, `coffee-rails` and `uglifier` gems to your Gemfile, which are used by Sprockets for asset compression: ```ruby @@ -44,8 +47,8 @@ gem 'uglifier' gem 'coffee-rails' ``` -Using the `--skip-sprockets` option will prevent Rails 4 from adding -`sass-rails` and `uglifier` to your Gemfile, so if you later want to enable +Using the `--skip-sprockets` option will prevent Rails from adding +them to your Gemfile, so if you later want to enable the asset pipeline you will have to add those gems to your Gemfile. Also, creating an application with the `--skip-sprockets` option will generate a slightly different `config/application.rb` file, with a require statement @@ -327,7 +330,7 @@ familiar `javascript_include_tag` and `stylesheet_link_tag`: <%= javascript_include_tag "application" %> ``` -If using the turbolinks gem, which is included by default in Rails 4, then +If using the turbolinks gem, which is included by default in Rails, then include the 'data-turbolinks-track' option which causes turbolinks to check if an asset has been updated and if so loads it into the page: @@ -443,7 +446,7 @@ makes fewer requests. Compression also reduces file size, enabling the browser to download them faster. -For example, a new Rails 4 application includes a default +For example, a new Rails application includes a default `app/assets/javascripts/application.js` file containing the following lines: ```js @@ -484,7 +487,7 @@ which contains these lines: */ ``` -Rails 4 creates both `app/assets/javascripts/application.js` and +Rails creates both `app/assets/javascripts/application.js` and `app/assets/stylesheets/application.css` regardless of whether the --skip-sprockets option is used when creating a new rails application. This is so you can easily add asset pipelining later if you like. @@ -898,7 +901,7 @@ your CDN server, you need to tell browsers to use your CDN to grab assets instead of your Rails server directly. You can do this by configuring Rails to set your CDN as the asset host instead of using a relative path. To set your asset host in Rails, you need to set `config.action_controller.asset_host` in -`config/production.rb`: +`config/environments/production.rb`: ```ruby config.action_controller.asset_host = 'mycdnsubdomain.fictional-cdn.com' @@ -1108,7 +1111,7 @@ supported runtime in order to use `uglifier`. If you are using Mac OS X or Windows you have a JavaScript runtime installed in your operating system. NOTE: The `config.assets.compress` initialization option is no longer used in -Rails 4 to enable either CSS or JavaScript compression. Setting it will have no +Rails to enable either CSS or JavaScript compression. Setting it will have no effect on the application. Instead, setting `config.assets.css_compressor` and `config.assets.js_compressor` will control compression of CSS and JavaScript assets. @@ -1290,7 +1293,7 @@ config.assets.digest = true # config.assets.precompile += %w( search.js ) ``` -Rails 4 no longer sets default config values for Sprockets in `test.rb`, so +Rails 4 and above no longer set default config values for Sprockets in `test.rb`, so `test.rb` now requires Sprockets configuration. The old defaults in the test environment are: `config.assets.compile = true`, `config.assets.compress = false`, `config.assets.debug = false` and `config.assets.digest = false`. diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index accce5a904..09ab64837a 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -713,7 +713,7 @@ By default, Active Record doesn't know about the connection between these associ ```ruby a = Author.first -b = c.books.first +b = a.books.first a.first_name == b.author.first_name # => true a.first_name = 'Manny' a.first_name == b.author.first_name # => false @@ -726,7 +726,7 @@ class Author < ApplicationRecord has_many :books, inverse_of: :author end -class book < ApplicationRecord +class Book < ApplicationRecord belongs_to :author, inverse_of: :books end ``` @@ -735,7 +735,7 @@ With these changes, Active Record will only load one copy of the author object, ```ruby a = author.first -b = c.books.first +b = a.books.first a.first_name == b.author.first_name # => true a.first_name = 'Manny' a.first_name == b.author.first_name # => true diff --git a/guides/source/configuring.md b/guides/source/configuring.md index d9c345fb71..a5fb396f15 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -998,7 +998,7 @@ Below is a comprehensive list of all the initializers found in Rails in the orde * `initialize_cache` If `Rails.cache` isn't set yet, initializes the cache by referencing the value in `config.cache_store` and stores the outcome as `Rails.cache`. If this object responds to the `middleware` method, its middleware is inserted before `Rack::Runtime` in the middleware stack. -* `set_clear_dependencies_hook` Provides a hook for `active_record.set_dispatch_hooks` to use, which will run before this initializer. This initializer - which runs only if `cache_classes` is set to `false` - uses `ActionDispatch::Callbacks.after` to remove the constants which have been referenced during the request from the object space so that they will be reloaded during the following request. +* `set_clear_dependencies_hook` This initializer - which runs only if `cache_classes` is set to `false` - uses `ActionDispatch::Callbacks.after` to remove the constants which have been referenced during the request from the object space so that they will be reloaded during the following request. * `initialize_dependency_mechanism` If `config.cache_classes` is true, configures `ActiveSupport::Dependencies.mechanism` to `require` dependencies rather than `load` them. @@ -1012,13 +1012,17 @@ Below is a comprehensive list of all the initializers found in Rails in the orde * `active_support.initialize_beginning_of_week` Sets the default beginning of week for the application based on `config.beginning_of_week` setting, which defaults to `:monday`. +* `active_support.set_configs` Sets up Active Support by using the settings in `config.active_support` by `send`'ing the method names as setters to `ActiveSupport` and passing the values through. + * `action_dispatch.configure` Configures the `ActionDispatch::Http::URL.tld_length` to be set to the value of `config.action_dispatch.tld_length`. * `action_view.set_configs` Sets up Action View by using the settings in `config.action_view` by `send`'ing the method names as setters to `ActionView::Base` and passing the values through. -* `action_controller.logger` Sets `ActionController::Base.logger` - if it's not already set - to `Rails.logger`. +* `action_controller.assets_config` Initializes the `config.actions_controller.assets_dir` to the app's public directory if not explicitly configured + +* `action_controller.set_helpers_path` Sets Action Controller's helpers_path to the application's helpers_path -* `action_controller.initialize_framework_caches` Sets `ActionController::Base.cache_store` - if it's not already set - to `Rails.cache`. +* `action_controller.parameters_config` Configures strong parameters options for `ActionController::Parameters` * `action_controller.set_configs` Sets up Action Controller by using the settings in `config.action_controller` by `send`'ing the method names as setters to `ActionController::Base` and passing the values through. @@ -1028,13 +1032,21 @@ Below is a comprehensive list of all the initializers found in Rails in the orde * `active_record.logger` Sets `ActiveRecord::Base.logger` - if it's not already set - to `Rails.logger`. +* `active_record.migration_error` Configures middleware to check for pending migrations + +* `active_record.check_schema_cache_dump` Loads the schema cache dump if configured and available + +* `active_record.warn_on_records_fetched_greater_than` Enables warnings when queries return large numbers of records + * `active_record.set_configs` Sets up Active Record by using the settings in `config.active_record` by `send`'ing the method names as setters to `ActiveRecord::Base` and passing the values through. * `active_record.initialize_database` Loads the database configuration (by default) from `config/database.yml` and establishes a connection for the current environment. * `active_record.log_runtime` Includes `ActiveRecord::Railties::ControllerRuntime` which is responsible for reporting the time taken by Active Record calls for the request back to the logger. -* `active_record.set_dispatch_hooks` Resets all reloadable connections to the database if `config.cache_classes` is set to `false`. +* `active_record.set_reloader_hooks` Resets all reloadable connections to the database if `config.cache_classes` is set to `false`. + +* `active_record.add_watchable_files` Adds `schema.rb` and `structure.sql` files to watchable files * `active_job.logger` Sets `ActiveJob::Base.logger` - if it's not already set - to `Rails.logger`. diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml index fdd6d4d33d..2cf613f47f 100644 --- a/guides/source/documents.yaml +++ b/guides/source/documents.yaml @@ -137,7 +137,6 @@ description: This guide explains how to profile your Rails applications to improve performance. - name: Using Rails for API-only Applications - work_in_progress: true url: api_app.html description: This guide explains how to effectively use Rails to develop a JSON API application. diff --git a/guides/source/engines.md b/guides/source/engines.md index 415def8367..db50ad278f 100644 --- a/guides/source/engines.md +++ b/guides/source/engines.md @@ -461,7 +461,7 @@ model, a comment controller and then modify the articles scaffold to display comments and allow people to create new ones. From the application root, run the model generator. Tell it to generate a -`Comment` model, with the related table having two columns: a `article_id` integer +`Comment` model, with the related table having two columns: an `article_id` integer and `text` text column. ```bash diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index 2cbc591629..8eb3b6190f 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -653,7 +653,7 @@ run this command in your terminal: $ bin/rails generate model Article title:string text:text ``` -With that command we told Rails that we want a `Article` model, together +With that command we told Rails that we want an `Article` model, together with a _title_ attribute of type string, and a _text_ attribute of type text. Those attributes are automatically added to the `articles` table in the database and mapped to the `Article` model. diff --git a/guides/source/layout.html.erb b/guides/source/layout.html.erb index 1f81ea4694..6db76b528e 100644 --- a/guides/source/layout.html.erb +++ b/guides/source/layout.html.erb @@ -24,7 +24,17 @@ <% end %> <div id="topNav"> <div class="wrapper"> - <strong class="more-info-label">←<a href="http://rubyonrails.org/">Back to rubyonrails.org:</a> </strong> + <strong class="more-info-label">More at <a href="http://rubyonrails.org/">rubyonrails.org:</a> </strong> + <span class="red-button more-info-button"> + More Ruby on Rails + </span> + <ul class="more-info-links s-hidden"> + <li class="more-info"><a href="http://weblog.rubyonrails.org/">Blog</a></li> + <li class="more-info"><a href="http://guides.rubyonrails.org/">Guides</a></li> + <li class="more-info"><a href="http://api.rubyonrails.org/">API</a></li> + <li class="more-info"><a href="http://stackoverflow.com/questions/tagged/ruby-on-rails">Ask for help</a></li> + <li class="more-info"><a href="https://github.com/rails/rails">Contribute on GitHub</a></li> + </ul> </div> </div> <div id="header"> diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md index d55e1007ee..83173e8d75 100644 --- a/guides/source/layouts_and_rendering.md +++ b/guides/source/layouts_and_rendering.md @@ -555,7 +555,7 @@ class Admin::ProductsController < AdminController end ``` -The lookup order for a `admin/products#index` action will be: +The lookup order for an `admin/products#index` action will be: * `app/views/admin/products/` * `app/views/admin/` @@ -700,7 +700,7 @@ This would detect that there are no books with the specified ID, populate the `@ ### Using `head` To Build Header-Only Responses -The `head` method can be used to send responses with only headers to the browser. It provides a more obvious alternative to calling `render :nothing`. The `head` method accepts a number or symbol (see [reference table](#the-status-option)) representing a HTTP status code. The options argument is interpreted as a hash of header names and values. For example, you can return only an error header: +The `head` method can be used to send responses with only headers to the browser. The `head` method accepts a number or symbol (see [reference table](#the-status-option)) representing a HTTP status code. The options argument is interpreted as a hash of header names and values. For example, you can return only an error header: ```ruby head :bad_request diff --git a/guides/source/routing.md b/guides/source/routing.md index 777d1d24b6..bd3e236a2b 100644 --- a/guides/source/routing.md +++ b/guides/source/routing.md @@ -1136,19 +1136,19 @@ For example, here's a small section of the `rails routes` output for a RESTful r edit_user GET /users/:id/edit(.:format) users#edit ``` -You can search through your routes with the --grep option (-g for short). This outputs any routes that partially match the URL helper method name, the HTTP verb, or the URL path. +You can search through your routes with the grep option: -g. This outputs any routes that partially match the URL helper method name, the HTTP verb, or the URL path. ``` -$ bin/rails routes --grep new_comment +$ bin/rails routes -g new_comment $ bin/rails routes -g POST $ bin/rails routes -g admin ``` -If you only want to see the routes that map to a specific controller, there's the --controller option (-c for short). +If you only want to see the routes that map to a specific controller, there's the -c option. ``` -$ bin/rails routes --controller users -$ bin/rails routes --controller admin/users +$ bin/rails routes -c users +$ bin/rails routes -c admin/users $ bin/rails routes -c Comments $ bin/rails routes -c Articles::CommentsController ``` diff --git a/guides/source/security.md b/guides/source/security.md index 96b9f4bcce..98324141cc 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -23,7 +23,7 @@ Web application frameworks are made to help developers build web applications. S In general there is no such thing as plug-n-play security. Security depends on the people using the framework, and sometimes on the development method. And it depends on all layers of a web application environment: The back-end storage, the web server and the web application itself (and possibly other layers or applications). -The Gartner Group however estimates that 75% of attacks are at the web application layer, and found out "that out of 300 audited sites, 97% are vulnerable to attack". This is because web applications are relatively easy to attack, as they are simple to understand and manipulate, even by the lay person. +The Gartner Group, however, estimates that 75% of attacks are at the web application layer, and found out "that out of 300 audited sites, 97% are vulnerable to attack". This is because web applications are relatively easy to attack, as they are simple to understand and manipulate, even by the lay person. The threats against web applications include user account hijacking, bypass of access control, reading or modifying sensitive data, or presenting fraudulent content. Or an attacker might be able to install a Trojan horse program or unsolicited e-mail sending software, aim at financial enrichment or cause brand name damage by modifying company resources. In order to prevent attacks, minimize their impact and remove points of attack, first of all, you have to fully understand the attack methods in order to find the correct countermeasures. That is what this guide aims at. @@ -62,7 +62,7 @@ Many web applications have an authentication system: a user provides a user name Hence, the cookie serves as temporary authentication for the web application. Anyone who seizes a cookie from someone else, may use the web application as this user - with possibly severe consequences. Here are some ways to hijack a session, and their countermeasures: -* Sniff the cookie in an insecure network. A wireless LAN can be an example of such a network. In an unencrypted wireless LAN it is especially easy to listen to the traffic of all connected clients. For the web application builder this means to _provide a secure connection over SSL_. In Rails 3.1 and later, this could be accomplished by always forcing SSL connection in your application config file: +* Sniff the cookie in an insecure network. A wireless LAN can be an example of such a network. In an unencrypted wireless LAN, it is especially easy to listen to the traffic of all connected clients. For the web application builder this means to _provide a secure connection over SSL_. In Rails 3.1 and later, this could be accomplished by always forcing SSL connection in your application config file: ```ruby config.force_ssl = true @@ -787,7 +787,7 @@ The following is an excerpt from the [Js.Yamanner@m](http://www.symantec.com/sec var IDList = ''; var CRumb = ''; function makeRequest(url, Func, Method,Param) { ... ``` -The worms exploits a hole in Yahoo's HTML/JavaScript filter, which usually filters all target and onload attributes from tags (because there can be JavaScript). The filter is applied only once, however, so the onload attribute with the worm code stays in place. This is a good example why blacklist filters are never complete and why it is hard to allow HTML/JavaScript in a web application. +The worms exploit a hole in Yahoo's HTML/JavaScript filter, which usually filters all targets and onload attributes from tags (because there can be JavaScript). The filter is applied only once, however, so the onload attribute with the worm code stays in place. This is a good example why blacklist filters are never complete and why it is hard to allow HTML/JavaScript in a web application. Another proof-of-concept webmail worm is Nduja, a cross-domain worm for four Italian webmail services. Find more details on [Rosario Valotta's paper](http://www.xssed.com/news/37/Nduja_Connection_A_cross_webmail_worm_XWW/). Both webmail worms have the goal to harvest email addresses, something a criminal hacker could make money with. diff --git a/guides/source/testing.md b/guides/source/testing.md index 1c64b2c0ac..13f4446751 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -830,7 +830,7 @@ end If we run our test now, we should see a failure: ```bash -$ bin/rails test test/controllers/articles_controller_test.rb test_should_create_article +$ bin/rails test test/controllers/articles_controller_test.rb -n test_should_create_article Run options: -n test_should_create_article --seed 32266 # Running: @@ -868,7 +868,7 @@ end Now if we run our tests, we should see it pass: ```bash -$ bin/rails test test/controllers/articles_controller_test.rb test_should_create_article +$ bin/rails test test/controllers/articles_controller_test.rb -n test_should_create_article Run options: -n test_should_create_article --seed 18981 # Running: diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index e631445492..0dfa4f1cb8 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -16,6 +16,21 @@ Before attempting to upgrade an existing application, you should be sure you hav The best way to be sure that your application still works after upgrading is to have good test coverage before you start the process. If you don't have automated tests that exercise the bulk of your application, you'll need to spend time manually exercising all the parts that have changed. In the case of a Rails upgrade, that will mean every single piece of functionality in the application. Do yourself a favor and make sure your test coverage is good _before_ you start an upgrade. +### The Upgrade Process + +When changing Rails versions, it's best to move slowly, one minor version at a time, in order to make good use of the deprecation warnings. Rails version numbers are in the form Major.Minor.Patch. Major and Minor versions are allowed to make changes to the public API, so this may cause errors in your application. Patch versions only include bug fixes, and don't change any public API. + +The process should go as follows: + +1. Write tests and make sure they pass +2. Move to the latest patch version after your current version +3. Fix tests and deprecated features +4. Move to the latest patch version of the next minor version + +Repeat this process until you reach your target Rails version. Each time you move versions, you will need to change the Rails version number in the Gemfile (and possibly other gem versions) and run `bundle update`. Then run the Update rake task mentioned below to update configuration files, then run your tests. + +You can find a list of all released Rails versions [here](https://rubygems.org/gems/rails/versions). + ### Ruby Versions Rails generally stays close to the latest released Ruby version when it's released: @@ -168,7 +183,7 @@ the logs. In the next version, these errors will no longer be suppressed. Instead, the errors will propagate normally just like in other Active Record callbacks. -When you define a `after_rollback` or `after_commit` callback, you +When you define an `after_rollback` or `after_commit` callback, you will receive a deprecation warning about this upcoming change. When you are ready, you can opt into the new behavior and remove the deprecation warning by adding following configuration to your diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md index 48fc6bc9c0..26ff5da7a3 100644 --- a/guides/source/working_with_javascript_in_rails.md +++ b/guides/source/working_with_javascript_in_rails.md @@ -148,10 +148,10 @@ and Rails has got your back in those cases. Because of Unobtrusive JavaScript, the Rails "Ajax helpers" are actually in two parts: the JavaScript half and the Ruby half. +Unless you have disabled the Asset Pipeline, [rails.js](https://github.com/rails/jquery-ujs/blob/master/src/rails.js) provides the JavaScript half, and the regular Ruby view helpers add appropriate -tags to your DOM. The CoffeeScript in rails.js then listens for these -attributes, and attaches appropriate handlers. +tags to your DOM. ### form_for @@ -350,7 +350,7 @@ $("<%= escape_javascript(render @user) %>").appendTo("#users"); Turbolinks ---------- -Rails 4 ships with the [Turbolinks gem](https://github.com/rails/turbolinks). +Rails 4 ships with the [Turbolinks gem](https://github.com/turbolinks/turbolinks). This gem uses Ajax to speed up page rendering in most applications. ### How Turbolinks Works @@ -395,7 +395,7 @@ $(document).on "page:change", -> For more details, including other events you can bind to, check out [the Turbolinks -README](https://github.com/rails/turbolinks/blob/master/README.md). +README](https://github.com/turbolinks/turbolinks/blob/master/README.md). Other Resources --------------- diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 8f4dc736a8..2506baac16 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,13 @@ +* The Gemfiles of new applications include spring-watcher-listen on Linux and + Mac OS X (unless --skip-spring). + + *Xavier Noria* + +* New applications are generated with the evented file system monitor enabled + on Linux and Mac OS X. + + *Xavier Noria* + * Add dummy files for apple-touch-icon.png and apple-touch-icon.png. GH#23427 *Alexey Zabelin* diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index cac31e1eed..bff33ff42e 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -214,7 +214,7 @@ module Rails # url: http://localhost:3001 # namespace: my_app_development # - # # config/production.rb + # # config/environments/production.rb # Rails.application.configure do # config.middleware.use ExceptionNotifier, config_for(:exception_notification) # end diff --git a/railties/lib/rails/commands.rb b/railties/lib/rails/commands.rb index 7627fcf5a0..fa47c52b96 100644 --- a/railties/lib/rails/commands.rb +++ b/railties/lib/rails/commands.rb @@ -14,6 +14,5 @@ command = ARGV.shift command = aliases[command] || command require 'rails/command' -require 'rails/commands/dev_cache' Rails::Command.run(command, ARGV) diff --git a/railties/lib/rails/commands/dev_cache.rb b/railties/lib/rails/commands/dev_cache.rb deleted file mode 100644 index ec96e8f630..0000000000 --- a/railties/lib/rails/commands/dev_cache.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'rails/command' - -module Rails - module Commands - # This is a wrapper around the Rails dev:cache command - class DevCache < Command - set_banner :dev_cache, 'Toggle development mode caching on/off' - def dev_cache - if File.exist? 'tmp/caching-dev.txt' - File.delete 'tmp/caching-dev.txt' - puts 'Development mode is no longer being cached.' - else - FileUtils.touch 'tmp/caching-dev.txt' - puts 'Development mode is now being cached.' - end - - FileUtils.touch 'tmp/restart.txt' - end - end - end -end diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 9b1e16a7a3..8f8c2ec9e1 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -390,6 +390,10 @@ module Rails !options[:skip_spring] && !options.dev? && Process.respond_to?(:fork) && !RUBY_PLATFORM.include?("cygwin") end + def os_supports_listen_out_of_the_box? + RbConfig::CONFIG['host_os'] =~ /darwin|linux/ + end + def run_bundle bundle_command('install') if bundle_install? end diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index 3825dc4e38..c3fad31f23 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -38,9 +38,15 @@ group :development do gem 'web-console', '~> 3.0' <%- end -%> <%- end -%> +<% if os_supports_listen_out_of_the_box? -%> + gem 'listen', '~> 3.0.5' +<% end -%> <% if spring_install? -%> # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' +<% if os_supports_listen_out_of_the_box? -%> + gem 'spring-watcher-listen', '~> 2.0.0' +<% end -%> <% end -%> end <% end -%> diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml index d51b2ec199..bd5c0b10f6 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml @@ -19,7 +19,7 @@ default: &default encoding: unicode # For details on connection pooling, see rails configuration guide # http://guides.rubyonrails.org/configuring.html#database-pooling - pool: 5 + pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> development: <<: *default 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 fd41372d9c..3451ade158 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 @@ -58,5 +58,5 @@ Rails.application.configure do # Use an evented file watcher to asynchronously detect changes in source code, # routes, locales, etc. This feature depends on the listen gem. - # config.file_watcher = ActiveSupport::EventedFileUpdateChecker + <%= '# ' unless os_supports_listen_out_of_the_box? %>config.file_watcher = ActiveSupport::EventedFileUpdateChecker end diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt index 83807f14b4..abbacd9bec 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt +++ b/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt @@ -1,6 +1,6 @@ <%= wrap_in_modules <<-rb.strip_heredoc class ApplicationController < ActionController::#{api? ? "API" : "Base"} - #{ api? ? '# ' : '' }protect_from_forgery :with => :exception + #{ api? ? '# ' : '' }protect_from_forgery with: :exception end rb %> diff --git a/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml index 2656767eb4..0681780c97 100644 --- a/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml +++ b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml @@ -6,7 +6,7 @@ <%- if attribute.password_digest? -%> password_digest: <%%= BCrypt::Password.create('secret') %> <%- elsif attribute.reference? -%> - <%= yaml_key_value(attribute.column_name.sub(/_id$/, ''), attribute.default) %> + <%= yaml_key_value(attribute.column_name.sub(/_id$/, ''), attribute.default || name) %> <%- else -%> <%= yaml_key_value(attribute.column_name, attribute.default) %> <%- end -%> diff --git a/railties/lib/rails/rack/logger.rb b/railties/lib/rails/rack/logger.rb index aa7d3ca6c6..b63d3a58d2 100644 --- a/railties/lib/rails/rack/logger.rb +++ b/railties/lib/rails/rack/logger.rb @@ -72,10 +72,6 @@ module Rails instrumenter.finish 'request.action_dispatch', request: request end - def development? - Rails.env.development? - end - def logger Rails.logger end diff --git a/railties/lib/rails/tasks.rb b/railties/lib/rails/tasks.rb index d60eaf6f4f..d3e33584d7 100644 --- a/railties/lib/rails/tasks.rb +++ b/railties/lib/rails/tasks.rb @@ -3,6 +3,7 @@ require 'rake' # Load Rails Rakefile extensions %w( annotations + dev framework initializers log diff --git a/railties/lib/rails/tasks/dev.rake b/railties/lib/rails/tasks/dev.rake new file mode 100644 index 0000000000..4593100465 --- /dev/null +++ b/railties/lib/rails/tasks/dev.rake @@ -0,0 +1,14 @@ +namespace :dev do + desc 'Toggle development mode caching on/off' + task :cache do + if File.exist? 'tmp/caching-dev.txt' + File.delete 'tmp/caching-dev.txt' + puts 'Development mode is no longer being cached.' + else + FileUtils.touch 'tmp/caching-dev.txt' + puts 'Development mode is now being cached.' + end + + FileUtils.touch 'tmp/restart.txt' + end +end diff --git a/railties/lib/rails/tasks/routes.rake b/railties/lib/rails/tasks/routes.rake index 939fa59c75..69103aa5d9 100644 --- a/railties/lib/rails/tasks/routes.rake +++ b/railties/lib/rails/tasks/routes.rake @@ -2,7 +2,7 @@ require 'active_support/deprecation' require 'active_support/core_ext/string/strip' # for strip_heredoc require 'optparse' -desc 'Print out all defined routes in match order, with names. Target specific controller with --controller option - or its -c shorthand.' +desc 'Print out all defined routes in match order, with names. Target specific controller with -c option, or grep routes using -g option' task routes: :environment do all_routes = Rails.application.routes.routes require 'action_dispatch/routing/inspector' @@ -19,11 +19,11 @@ task routes: :environment do OptionParser.new do |opts| opts.banner = "Usage: rails routes [options]" - opts.on("-c", "--controller [CONTROLLER]") do |controller| + opts.on("-c CONTROLLER") do |controller| routes_filter = { controller: controller } end - opts.on("-g", "--grep [PATTERN]") do |pattern| + opts.on("-g PATTERN") do |pattern| routes_filter = pattern end diff --git a/railties/test/application/rake/dev_test.rb b/railties/test/application/rake/dev_test.rb new file mode 100644 index 0000000000..43d7a5e156 --- /dev/null +++ b/railties/test/application/rake/dev_test.rb @@ -0,0 +1,34 @@ +require 'isolation/abstract_unit' + +module ApplicationTests + module RakeTests + class RakeDevTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + end + + def teardown + teardown_app + end + + test 'dev:cache creates file and outputs message' do + Dir.chdir(app_path) do + output = `rake dev:cache` + assert File.exist?('tmp/caching-dev.txt') + assert_match(/Development mode is now being cached/, output) + end + end + + test 'dev:cache deletes file and outputs message' do + Dir.chdir(app_path) do + `rails dev:cache` # Create caching file. + output = `rails dev:cache` # Delete caching file. + assert_not File.exist?('tmp/caching-dev.txt') + assert_match(/Development mode is no longer being cached/, output) + end + end + end + end +end diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index 745a3e3ec5..d1c828b509 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -179,12 +179,20 @@ module ApplicationTests app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get '/cart', to: 'cart#show' - get '/basketball', to: 'basketball#index' + post '/cart', to: 'cart#create' + get '/basketballs', to: 'basketball#index' end RUBY output = Dir.chdir(app_path){ `bin/rails routes -g show` } assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output + + output = Dir.chdir(app_path){ `bin/rails routes -g POST` } + assert_equal "Prefix Verb URI Pattern Controller#Action\n POST /cart(.:format) cart#create\n", output + + output = Dir.chdir(app_path){ `bin/rails routes -g basketballs` } + assert_equal " Prefix Verb URI Pattern Controller#Action\n" \ + "basketballs GET /basketballs(.:format) basketball#index\n", output end def test_rake_routes_with_controller_search_key @@ -291,12 +299,11 @@ module ApplicationTests assert_no_match(/Errors running/, output) end - def test_scaffold_with_references_columns_tests_pass_when_belongs_to_is_optional - app_file "config/initializers/active_record_belongs_to_required_by_default.rb", - "Rails.application.config.active_record.belongs_to_required_by_default = false" - + def test_scaffold_with_references_columns_tests_pass_by_default output = Dir.chdir(app_path) do - `bin/rails generate scaffold LineItems product:references cart:belongs_to; + `bin/rails generate model Product; + bin/rails generate model Cart; + bin/rails generate scaffold LineItems product:references cart:belongs_to; RAILS_ENV=test bin/rails db:migrate test` end diff --git a/railties/test/commands/dev_cache_test.rb b/railties/test/commands/dev_cache_test.rb deleted file mode 100644 index 1b7a72e7fc..0000000000 --- a/railties/test/commands/dev_cache_test.rb +++ /dev/null @@ -1,32 +0,0 @@ -require_relative '../isolation/abstract_unit' - -module CommandsTests - class DevCacheTest < ActiveSupport::TestCase - include ActiveSupport::Testing::Isolation - - def setup - build_app - end - - def teardown - teardown_app - end - - test 'dev:cache creates file and outputs message' do - Dir.chdir(app_path) do - output = `rails dev:cache` - assert File.exist?('tmp/caching-dev.txt') - assert_match(%r{Development mode is now being cached}, output) - end - end - - test 'dev:cache deletes file and outputs message' do - Dir.chdir(app_path) do - `rails dev:cache` # Create caching file. - output = `rails dev:cache` # Delete caching file. - assert_not File.exist?('tmp/caching-dev.txt') - assert_match(%r{Development mode is no longer being cached}, output) - end - end - end -end diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index f483a0bcbd..be05e779ea 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -479,6 +479,29 @@ class AppGeneratorTest < Rails::Generators::TestCase end end + def test_inclusion_of_listen_related_gems + run_generator + if RbConfig::CONFIG['host_os'] =~ /darwin|linux/ + assert_gem 'listen' + assert_gem 'spring-watcher-listen' + else + assert_file 'Gemfile' do |content| + assert_no_match(/listen/, content) + end + end + end + + def test_evented_file_update_checker_config + run_generator + assert_file 'config/environments/development.rb' do |content| + if RbConfig::CONFIG['host_os'] =~ /darwin|linux/ + assert_match(/^\s*config.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content) + else + assert_match(/^\s*# config.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content) + end + end + end + def test_template_from_dir_pwd FileUtils.cd(Rails.root) assert_match(/It works from file!/, run_generator([destination_root, "-m", "lib/template.rb"])) diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb index 814f4c050e..c8c8f0aa3b 100644 --- a/railties/test/generators/model_generator_test.rb +++ b/railties/test/generators/model_generator_test.rb @@ -298,18 +298,18 @@ class ModelGeneratorTest < Rails::Generators::TestCase def test_fixtures_use_the_references_ids run_generator ["LineItem", "product:references", "cart:belongs_to"] - assert_file "test/fixtures/line_items.yml", /product: \n cart: / + assert_file "test/fixtures/line_items.yml", /product: one\n cart: one/ assert_generated_fixture("test/fixtures/line_items.yml", - {"one"=>{"product"=>nil, "cart"=>nil}, "two"=>{"product"=>nil, "cart"=>nil}}) + {"one"=>{"product"=>"one", "cart"=>"one"}, "two"=>{"product"=>"two", "cart"=>"two"}}) end def test_fixtures_use_the_references_ids_and_type run_generator ["LineItem", "product:references{polymorphic}", "cart:belongs_to"] - assert_file "test/fixtures/line_items.yml", /product: \n product_type: Product\n cart: / + assert_file "test/fixtures/line_items.yml", /product: one\n product_type: Product\n cart: one/ assert_generated_fixture("test/fixtures/line_items.yml", - {"one"=>{"product"=>nil, "product_type"=>"Product", "cart"=>nil}, - "two"=>{"product"=>nil, "product_type"=>"Product", "cart"=>nil}}) + {"one"=>{"product"=>"one", "product_type"=>"Product", "cart"=>"one"}, + "two"=>{"product"=>"two", "product_type"=>"Product", "cart"=>"two"}}) end def test_fixtures_respect_reserved_yml_keywords diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index 6e5132e849..4111a30664 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -335,7 +335,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "hyphenated-name/lib/hyphenated/name/engine.rb", /module Hyphenated\n module Name\n class Engine < ::Rails::Engine\n isolate_namespace Hyphenated::Name\n end\n end\nend/ assert_file "hyphenated-name/lib/hyphenated/name.rb", /require "hyphenated\/name\/engine"/ assert_file "hyphenated-name/test/dummy/config/routes.rb", /mount Hyphenated::Name::Engine => "\/hyphenated-name"/ - assert_file "hyphenated-name/app/controllers/hyphenated/name/application_controller.rb", /module Hyphenated\n module Name\n class ApplicationController < ActionController::Base\n protect_from_forgery :with => :exception\n end\n end\nend\n/ + assert_file "hyphenated-name/app/controllers/hyphenated/name/application_controller.rb", /module Hyphenated\n module Name\n class ApplicationController < ActionController::Base\n protect_from_forgery with: :exception\n end\n end\nend\n/ assert_file "hyphenated-name/app/models/hyphenated/name/application_record.rb", /module Hyphenated\n module Name\n class ApplicationRecord < ActiveRecord::Base\n self\.abstract_class = true\n end\n end\nend/ assert_file "hyphenated-name/app/jobs/hyphenated/name/application_job.rb", /module Hyphenated\n module Name\n class ApplicationJob < ActiveJob::Base/ assert_file "hyphenated-name/app/mailers/hyphenated/name/application_mailer.rb", /module Hyphenated\n module Name\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example.com'\n layout 'mailer'\n end\n end\nend/ @@ -357,7 +357,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "my_hyphenated-name/lib/my_hyphenated/name/engine.rb", /module MyHyphenated\n module Name\n class Engine < ::Rails::Engine\n isolate_namespace MyHyphenated::Name\n end\n end\nend/ assert_file "my_hyphenated-name/lib/my_hyphenated/name.rb", /require "my_hyphenated\/name\/engine"/ assert_file "my_hyphenated-name/test/dummy/config/routes.rb", /mount MyHyphenated::Name::Engine => "\/my_hyphenated-name"/ - assert_file "my_hyphenated-name/app/controllers/my_hyphenated/name/application_controller.rb", /module MyHyphenated\n module Name\n class ApplicationController < ActionController::Base\n protect_from_forgery :with => :exception\n end\n end\nend\n/ + assert_file "my_hyphenated-name/app/controllers/my_hyphenated/name/application_controller.rb", /module MyHyphenated\n module Name\n class ApplicationController < ActionController::Base\n protect_from_forgery with: :exception\n end\n end\nend\n/ assert_file "my_hyphenated-name/app/models/my_hyphenated/name/application_record.rb", /module MyHyphenated\n module Name\n class ApplicationRecord < ActiveRecord::Base\n self\.abstract_class = true\n end\n end\nend/ assert_file "my_hyphenated-name/app/jobs/my_hyphenated/name/application_job.rb", /module MyHyphenated\n module Name\n class ApplicationJob < ActiveJob::Base/ assert_file "my_hyphenated-name/app/mailers/my_hyphenated/name/application_mailer.rb", /module MyHyphenated\n module Name\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example.com'\n layout 'mailer'\n end\n end\nend/ @@ -379,7 +379,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "deep-hyphenated-name/lib/deep/hyphenated/name/engine.rb", /module Deep\n module Hyphenated\n module Name\n class Engine < ::Rails::Engine\n isolate_namespace Deep::Hyphenated::Name\n end\n end\n end\nend/ assert_file "deep-hyphenated-name/lib/deep/hyphenated/name.rb", /require "deep\/hyphenated\/name\/engine"/ assert_file "deep-hyphenated-name/test/dummy/config/routes.rb", /mount Deep::Hyphenated::Name::Engine => "\/deep-hyphenated-name"/ - assert_file "deep-hyphenated-name/app/controllers/deep/hyphenated/name/application_controller.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationController < ActionController::Base\n protect_from_forgery :with => :exception\n end\n end\n end\nend\n/ + assert_file "deep-hyphenated-name/app/controllers/deep/hyphenated/name/application_controller.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationController < ActionController::Base\n protect_from_forgery with: :exception\n end\n end\n end\nend\n/ assert_file "deep-hyphenated-name/app/models/deep/hyphenated/name/application_record.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationRecord < ActiveRecord::Base\n self\.abstract_class = true\n end\n end\n end\nend/ assert_file "deep-hyphenated-name/app/jobs/deep/hyphenated/name/application_job.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationJob < ActiveJob::Base/ assert_file "deep-hyphenated-name/app/mailers/deep/hyphenated/name/application_mailer.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example.com'\n layout 'mailer'\n end\n end\n end\nend/ diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index dddf8bd257..e7a261fa1f 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -154,6 +154,8 @@ module TestHelpers config.action_controller.allow_forgery_protection = false config.log_level = :info RUBY + + remove_from_env_config('development', 'config.file_watcher.*') end def teardown_app @@ -272,10 +274,17 @@ module TestHelpers end def remove_from_config(str) - file = "#{app_path}/config/application.rb" + remove_from_file("#{app_path}/config/application.rb", str) + end + + def remove_from_env_config(env, str) + remove_from_file("#{app_path}/config/environments/#{env}.rb", str) + end + + def remove_from_file(file, str) contents = File.read(file) - contents.sub!(/#{str}/, "") - File.open(file, "w+") { |f| f.puts contents } + contents.sub!(/#{str}/, '') + File.write(file, contents) end def app_file(path, contents, mode = 'w') diff --git a/tools/README.md b/tools/README.md index 25ab798bd5..1f3d6c59d9 100644 --- a/tools/README.md +++ b/tools/README.md @@ -5,3 +5,4 @@ They aren't used by Rails apps directly. * `console` drops you in irb and loads local Rails repos * `profile` profiles `Kernel#require` to help reduce startup time + * `line_statistics` provides CodeTools module and LineStatistics class to count lines
\ No newline at end of file |