diff options
172 files changed, 1649 insertions, 673 deletions
diff --git a/.travis.yml b/.travis.yml index 0038668ae7..6fa8c491a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,6 +27,7 @@ rvm: matrix: allow_failures: - env: "GEM=ar:mysql" + - env: "GEM=aj:integration" - rvm: ruby-head - rvm: rbx-2 - rvm: jruby-head @@ -17,10 +17,12 @@ gem 'turbolinks' gem 'arel', github: 'rails/arel', branch: 'master' gem 'mail', github: 'mikel/mail' +gem 'sprockets', '~> 3.0.0.rc.1' + # require: false so bcrypt is loaded only when has_secure_password is used. # This is to avoid ActiveModel (and by extension the entire framework) # being dependent on a binary library. -gem 'bcrypt', '~> 3.1.7', require: false +gem 'bcrypt', '~> 3.1.10', require: false # This needs to be with require false to avoid # it being automatically loaded by sprockets @@ -28,7 +30,7 @@ gem 'uglifier', '>= 1.3.0', require: false group :doc do gem 'sdoc', '~> 0.4.0' - gem 'redcarpet', '~> 3.2.2', platforms: :ruby + gem 'redcarpet', '~> 3.2.3', platforms: :ruby gem 'w3c_validators' gem 'kindlerb', '0.1.1' end @@ -61,18 +63,11 @@ group :test do # FIX: Our test suite isn't ready to run in random order yet gem 'minitest', '< 5.3.4' - platforms :mri_19 do - gem 'ruby-prof', '~> 0.11.2' - end - - platforms :mri_21, :mri_22 do + platforms :mri do gem 'stackprof' + # gem 'byebug' end - # platforms :mri do - # gem 'byebug' - # end - gem 'benchmark-ips' end diff --git a/Gemfile.lock b/Gemfile.lock index cd360958d3..543cfaf3da 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -99,6 +99,8 @@ GEM beaneater (~> 0.3.1) dante (~> 0.1.5) bcrypt (3.1.10) + bcrypt (3.1.10-x64-mingw32) + bcrypt (3.1.10-x86-mingw32) beaneater (0.3.3) benchmark-ips (2.1.1) builder (3.2.2) @@ -125,8 +127,8 @@ GEM execjs (2.3.0) globalid (0.3.3) activesupport (>= 4.1.0) - hike (1.2.3) hitimes (1.2.2) + hitimes (1.2.2-x86-mingw32) i18n (0.7.0) json (1.8.2) kindlerb (0.1.1) @@ -142,12 +144,16 @@ GEM mocha (0.14.0) metaclass (~> 0.0.1) mono_logger (1.1.0) - multi_json (1.10.1) + multi_json (1.11.0) mustache (1.0.0) mysql (2.9.1) mysql2 (0.3.18) nokogiri (1.6.6.2) mini_portile (~> 0.6.0) + nokogiri (1.6.6.2-x64-mingw32) + mini_portile (~> 0.6.0) + nokogiri (1.6.6.2-x86-mingw32) + mini_portile (~> 0.6.0) pg (0.18.1) psych (2.0.13) que (0.9.2) @@ -171,7 +177,7 @@ GEM loofah (~> 2.0) rake (10.4.2) rdoc (4.2.0) - redcarpet (3.2.2) + redcarpet (3.2.3) redis (3.2.1) redis-namespace (1.5.1) redis (~> 3.0, >= 3.0.4) @@ -186,7 +192,6 @@ GEM redis (~> 3.0) resque (~> 1.25) rufus-scheduler (~> 3.0) - ruby-prof (0.11.3) rufus-scheduler (3.0.9) tzinfo sdoc (0.4.1) @@ -211,11 +216,8 @@ GEM serverengine thor thread - sprockets (2.12.3) - hike (~> 1.2) - multi_json (~> 1.0) + sprockets (3.0.0.rc.1) rack (~> 1.0) - tilt (~> 1.1, != 1.3.0) sprockets-rails (2.2.4) actionpack (>= 3.0) activesupport (>= 3.0) @@ -245,6 +247,8 @@ GEM PLATFORMS ruby + x64-mingw32 + x86-mingw32 DEPENDENCIES activerecord-jdbcmysql-adapter (>= 1.3.0) @@ -252,7 +256,7 @@ DEPENDENCIES activerecord-jdbcsqlite3-adapter (>= 1.3.0) arel! backburner - bcrypt (~> 3.1.7) + bcrypt (~> 3.1.10) benchmark-ips coffee-rails (~> 4.1.0) dalli (>= 2.2.1) @@ -277,14 +281,14 @@ DEPENDENCIES rack-cache (~> 1.2) rails! rake (>= 10.3) - redcarpet (~> 3.2.2) + redcarpet (~> 3.2.3) resque resque-scheduler - ruby-prof (~> 0.11.2) sdoc (~> 0.4.0) sequel sidekiq sneakers (= 0.1.1.pre) + sprockets (~> 3.0.0.rc.1) sqlite3 (~> 1.3.6) stackprof sucker_punch diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index c7f09ed192..6ddc4c9596 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -134,25 +134,28 @@ module ActionMailer # # = Sending mail # - # Once a mailer action and template are defined, you can deliver your message or create it and save it - # for delivery later: + # Once a mailer action and template are defined, you can deliver your message or defer its creation and + # delivery for later: # # NotifierMailer.welcome(User.first).deliver_now # sends the email # mail = NotifierMailer.welcome(User.first) # => an ActionMailer::MessageDelivery object - # mail.deliver_now # sends the email + # mail.deliver_now # generates and sends the email now # - # The <tt>ActionMailer::MessageDelivery</tt> class is a wrapper around a <tt>Mail::Message</tt> object. If - # you want direct access to the <tt>Mail::Message</tt> object you can call the <tt>message</tt> method on - # the <tt>ActionMailer::MessageDelivery</tt> object. + # The <tt>ActionMailer::MessageDelivery</tt> class is a wrapper around a delegate that will call + # your method to generate the mail. If you want direct access to delegator, or <tt>Mail::Message</tt>, + # you can call the <tt>message</tt> method on the <tt>ActionMailer::MessageDelivery</tt> object. # # NotifierMailer.welcome(User.first).message # => a Mail::Message object # - # Action Mailer is nicely integrated with Active Job so you can send emails in the background (example: outside - # of the request-response cycle, so the user doesn't have to wait on it): + # Action Mailer is nicely integrated with Active Job so you can generate and send emails in the background + # (example: outside of the request-response cycle, so the user doesn't have to wait on it): # # NotifierMailer.welcome(User.first).deliver_later # enqueue the email sending to Active Job # + # Note that <tt>deliver_later</tt> will execute your method from the background job. + # # You never instantiate your mailer class. Rather, you just call the method you defined on the class itself. + # All instance method are expected to return a message object to be sent. # # = Multipart Emails # diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 2cbe09e3e7..4ab0857a66 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,25 @@ +* For actions with no corresponding templates, render `head :no_content` + instead of raising an error. This allows for slimmer API controller + methods that simply work, without needing further instructions. + + See #19036. + + *Stephen Bussey* + +* Provide friendlier access to request variants. + + request.variant = :phone + request.variant.phone? # true + request.variant.tablet? # false + + request.variant = [:phone, :tablet] + request.variant.phone? # true + request.variant.desktop? # false + request.variant.any?(:phone, :desktop) # true + request.variant.any?(:desktop, :watch) # false + + *George Claghorn* + * Fix regression where a gzip file response would have a Content-type, even when it was a 304 status code. @@ -14,7 +36,7 @@ *Adam Forsyth* * Drop request class from RouteSet constructor. - + If you would like to use a custom request class, please subclass and implement the `request_class` method. diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index 59ffb0a19e..13795f0dd8 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -62,9 +62,9 @@ module AbstractController # using #skip_action_callback def skip_action_callback(*names) ActiveSupport::Deprecation.warn('`skip_action_callback` is deprecated and will be removed in the next major version of Rails. Please use skip_before_action, skip_after_action or skip_around_action instead.') - skip_before_action(*names) - skip_after_action(*names) - skip_around_action(*names) + skip_before_action(*names, raise: false) + skip_after_action(*names, raise: false) + skip_around_action(*names, raise: false) end def skip_filter(*names) diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 2273406948..c492b7fb64 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -118,7 +118,7 @@ module ActionController end def authentication_request(controller, realm) - controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.gsub(/"/, "")}") + controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.gsub('"'.freeze, "".freeze)}") controller.status = 401 controller.response_body = "HTTP Basic: Access denied.\n" end @@ -499,7 +499,7 @@ module ActionController # # Returns nothing. def authentication_request(controller, realm) - controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.gsub(/"/, "")}") + controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.gsub('"'.freeze, "".freeze)}") controller.__send__ :render, :text => "HTTP Token: Access denied.\n", :status => :unauthorized end end diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb index ae04b53825..1573ea7099 100644 --- a/actionpack/lib/action_controller/metal/implicit_render.rb +++ b/actionpack/lib/action_controller/metal/implicit_render.rb @@ -7,7 +7,12 @@ module ActionController end def default_render(*args) - render(*args) + if template_exists?(action_name.to_s, _prefixes, variants: request.variant) + render(*args) + else + logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger + head :no_content + end end def method_for_action(action_name) diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index 7590fb6843..58150cd9a9 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -102,7 +102,7 @@ module ActionController end end - message = json.gsub(/\n/, "\ndata: ") + message = json.gsub("\n".freeze, "\ndata: ".freeze) @stream.write "data: #{message}\n\n" end end diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 7dae171215..fab1be3459 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -288,16 +288,17 @@ module ActionController #:nodoc: end def variant - if @variant.nil? + if @variant.empty? @variants[:none] || @variants[:any] - elsif (@variants.keys & @variant).any? - @variant.each do |v| - return @variants[v] if @variants.key?(v) - end else - @variants[:any] + @variants[variant_key] end end + + private + def variant_key + @variant.find { |variant| @variants.key?(variant) } || :any + end end end end diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 367b736035..663a969f72 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -13,9 +13,14 @@ module ActionController #:nodoc: # by including a token in the rendered HTML for your application. This token is # stored as a random string in the session, to which an attacker does not have # access. When a request reaches your application, \Rails verifies the received - # token with the token in the session. Only HTML and JavaScript requests are checked, - # so this will not protect your XML API (presumably you'll have a different - # authentication scheme there anyway). + # token with the token in the session. All requests are checked except GET requests + # as these should be idempotent. Keep in mind that all session-oriented requests + # should be CSRF protected, including Javascript and HTML requests. + # + # Since HTML and Javascript requests are typically made from the browser, we + # need to ensure to verify request authenticity for the web browser. We can + # use session-oriented authentication for these types requests, by using + # the `protect_form_forgery` method in our controllers. # # GET requests are not protected since they don't have side effects like writing # to the database and don't leak sensitive information. JavaScript requests are @@ -26,15 +31,20 @@ module ActionController #:nodoc: # Ajax) requests are allowed to make GET requests for JavaScript responses. # # It's important to remember that XML or JSON requests are also affected and if - # you're building an API you'll need something like: + # you're building an API you should change forgery protection method in + # <tt>ApplicationController</tt> (by default: <tt>:exception</tt>): # # class ApplicationController < ActionController::Base # protect_from_forgery unless: -> { request.format.json? } # end # - # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method, - # which checks the token and resets the session if it doesn't match what was expected. - # A call to this method is generated for new \Rails applications by default. + # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method. + # By default <tt>protect_from_forgery</tt> protects your session with + # <tt>:null_session</tt> method, which provides an empty session during request + # + # We may want to disable CSRF protection for APIs since they are typically + # designed to be state-less. That is, the requestion API client will handle + # the session for you instead of Rails. # # The token parameter is named <tt>authenticity_token</tt> by default. The name and # value of this token must be added to every layout that renders forms by including diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index f19c4201ba..c98e937423 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -117,7 +117,7 @@ module ActionController self.always_permitted_parameters = %w( controller action ) def self.const_missing(const_name) - super unless const_name == :NEVER_UNPERMITTED_PARAMS + return super unless const_name == :NEVER_UNPERMITTED_PARAMS ActiveSupport::Deprecation.warn(<<-MSG.squish) `ActionController::Parameters::NEVER_UNPERMITTED_PARAMS` has been deprecated. Use `ActionController::Parameters.always_permitted_parameters` instead. @@ -268,7 +268,7 @@ module ActionController # # params.permit(:name) # - # +:name+ passes it is a key of +params+ whose associated value is of type + # +:name+ passes if it is a key of +params+ whose associated value is of type # +String+, +Symbol+, +NilClass+, +Numeric+, +TrueClass+, +FalseClass+, # +Date+, +Time+, +DateTime+, +StringIO+, +IO+, # +ActionDispatch::Http::UploadedFile+ or +Rack::Test::UploadedFile+. diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index fbaa90d521..5a0e5c62e4 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -5,9 +5,9 @@ module ActionController # In addition to <tt>AbstractController::UrlFor</tt>, this module accesses the HTTP layer to define # url options like the +host+. In order to do so, this module requires the host class # to implement +env+ which needs to be Rack-compatible and +request+ - # which is either instance of +ActionDispatch::Request+ or an object - # that responds to <tt>host</tt>, <tt>optional_port</tt>, <tt>protocol</tt> and - # <tt>symbolized_path_parameter</tt> methods. + # which is either an instance of +ActionDispatch::Request+ or an object + # that responds to the +host+, +optional_port+, +protocol+ and + # +symbolized_path_parameter+ methods. # # class RootUrl # include ActionController::UrlFor diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index 53a98c5d0a..ff336b7354 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -10,8 +10,6 @@ module ActionDispatch self.ignore_accept_header = false end - attr_reader :variant - # The MIME type of the HTTP request, such as Mime::XML. # # For backward compatibility, the post \format is extracted from the @@ -75,18 +73,22 @@ module ActionDispatch # Sets the \variant for template. def variant=(variant) - if variant.is_a?(Symbol) - @variant = [variant] - elsif variant.nil? || variant.is_a?(Array) && variant.any? && variant.all?{ |v| v.is_a?(Symbol) } - @variant = variant + variant = Array(variant) + + if variant.all? { |v| v.is_a?(Symbol) } + @variant = ActiveSupport::ArrayInquirer.new(variant) else - raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols, not a #{variant.class}. " \ + raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols. " \ "For security reasons, never directly set the variant to a user-provided value, " \ "like params[:variant].to_sym. Check user-provided value against a whitelist first, " \ "then set the variant: request.variant = :tablet if params[:variant] == 'tablet'" end end + def variant + @variant ||= ActiveSupport::ArrayInquirer.new + end + # Sets the \format by string extension, which can be used to force custom formats # that are not controlled by the extension. # diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 732ee67268..a1f84e5ace 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -340,7 +340,7 @@ module ActionDispatch end protected - def parse_query(qs) + def parse_query(*) Utils.deep_munge(super) end diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb index 992c1a9efe..c0566c6fc9 100644 --- a/actionpack/lib/action_dispatch/journey/formatter.rb +++ b/actionpack/lib/action_dispatch/journey/formatter.rb @@ -39,7 +39,7 @@ module ActionDispatch return [route.format(parameterized_parts), params] end - message = "No route matches #{Hash[constraints.sort].inspect}" + message = "No route matches #{Hash[constraints.sort_by{|k,v| k.to_s}].inspect}" message << " missing required keys: #{missing_keys.sort.inspect}" unless missing_keys.empty? raise ActionController::UrlGenerationError, message diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index fdd1bc4e69..9a92b690c7 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -28,7 +28,7 @@ module ActionDispatch paths = [path, "#{path}#{ext}", "#{path}/index#{ext}"] if match = paths.detect { |p| - path = File.join(@root, p) + path = File.join(@root, p.force_encoding('UTF-8')) begin File.file?(path) && File.readable?(path) rescue SystemCallError diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb index 973627f106..9a1a05e971 100644 --- a/actionpack/lib/action_dispatch/request/session.rb +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -9,7 +9,8 @@ module ActionDispatch # Singleton object used to determine if an optional param wasn't specified Unspecified = Object.new - + + # Creates a session hash, merging the properties of the previous session if any def self.create(store, env, default_options) session_was = find env session = Request::Session.new(store, env) diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 34b5b48f3a..49009a45cc 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1678,7 +1678,7 @@ module ActionDispatch end def shallow_nesting_depth #:nodoc: - @nesting.select(&:shallow?).size + @nesting.count(&:shallow?) end def param_constraint? #:nodoc: diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 0f3734dd74..d0d8ded515 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -226,7 +226,7 @@ module ActionDispatch params = parameterize_args(args) { |missing_key| missing_keys << missing_key } - constraints = Hash[@route.requirements.merge(params).sort] + constraints = Hash[@route.requirements.merge(params).sort_by{|k,v| k.to_s}] message = "No route matches #{constraints.inspect}" message << " missing required keys: #{missing_keys.sort.inspect}" diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 62ff1be5c9..c1be2c9afe 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -14,7 +14,11 @@ silence_warnings do end require 'drb' -require 'drb/unix' +begin + require 'drb/unix' +rescue LoadError + puts "'drb/unix' is not available" +end require 'tempfile' PROCESS_COUNT = (ENV['N'] || 4).to_i diff --git a/actionpack/test/controller/force_ssl_test.rb b/actionpack/test/controller/force_ssl_test.rb index 04222745d9..5639abdc56 100644 --- a/actionpack/test/controller/force_ssl_test.rb +++ b/actionpack/test/controller/force_ssl_test.rb @@ -315,7 +315,7 @@ class RedirectToSSLTest < ActionController::TestCase assert_equal "https://secure.cheeseburger.host/redirect_to_ssl/cheeseburger", redirect_to_url end - def test_banana_does_not_redirect_if_already_https + def test_cheeseburgers_does_not_redirect_if_already_https request.env['HTTPS'] = 'on' get :cheeseburger assert_response 200 diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb index 1f5f66dc80..7aef8a50ce 100644 --- a/actionpack/test/controller/mime/respond_to_test.rb +++ b/actionpack/test/controller/mime/respond_to_test.rb @@ -1,4 +1,5 @@ require 'abstract_unit' +require "active_support/log_subscriber/test_helper" class RespondToController < ActionController::Base layout :set_layout @@ -608,19 +609,29 @@ class RespondToControllerTest < ActionController::TestCase end def test_invalid_variant + logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + old_logger, ActionController::Base.logger = ActionController::Base.logger, logger + @request.variant = :invalid - assert_raises(ActionView::MissingTemplate) do - get :variant_with_implicit_rendering - end + get :variant_with_implicit_rendering + assert_response :no_content + assert_equal 1, logger.logged(:info).select{ |s| s =~ /No template found/ }.size, "Implicit head :no_content not logged" + ensure + ActionController::Base.logger = old_logger end def test_variant_not_set_regular_template_missing - assert_raises(ActionView::MissingTemplate) do - get :variant_with_implicit_rendering - end + get :variant_with_implicit_rendering + assert_response :no_content end def test_variant_with_implicit_rendering + @request.variant = :implicit + get :variant_with_implicit_rendering + assert_response :no_content + end + + def test_variant_with_implicit_template_rendering @request.variant = :mobile get :variant_with_implicit_rendering assert_equal "text/html", @response.content_type diff --git a/actionpack/test/controller/parameters/always_permitted_parameters_test.rb b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb index 059f310d49..59be08db54 100644 --- a/actionpack/test/controller/parameters/always_permitted_parameters_test.rb +++ b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb @@ -1,5 +1,6 @@ require 'abstract_unit' require 'action_controller/metal/strong_parameters' +require 'minitest/mock' class AlwaysPermittedParametersTest < ActiveSupport::TestCase def setup @@ -14,7 +15,13 @@ class AlwaysPermittedParametersTest < ActiveSupport::TestCase test "shows deprecations warning on NEVER_UNPERMITTED_PARAMS" do assert_deprecated do - ActionController::Parameters::NEVER_UNPERMITTED_PARAMS + ActionController::Parameters::NEVER_UNPERMITTED_PARAMS + end + end + + test "returns super on missing constant other than NEVER_UNPERMITTED_PARAMS" do + ActionController::Parameters.superclass.stub :const_missing, "super" do + assert_equal "super", ActionController::Parameters::NON_EXISTING_CONSTANT end end diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index 61cc4dcd7e..f208cfda89 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -1128,35 +1128,47 @@ class RequestEtag < BaseRequestTest end class RequestVariant < BaseRequestTest - test "setting variant" do - request = stub_request + def setup + super + @request = stub_request + end - request.variant = :mobile - assert_equal [:mobile], request.variant + test 'setting variant to a symbol' do + @request.variant = :phone - request.variant = [:phone, :tablet] - assert_equal [:phone, :tablet], request.variant + assert @request.variant.phone? + assert_not @request.variant.tablet? + assert @request.variant.any?(:phone, :tablet) + assert_not @request.variant.any?(:tablet, :desktop) + end - assert_raise ArgumentError do - request.variant = [:phone, "tablet"] - end + test 'setting variant to an array of symbols' do + @request.variant = [:phone, :tablet] - assert_raise ArgumentError do - request.variant = "yolo" - end + assert @request.variant.phone? + assert @request.variant.tablet? + assert_not @request.variant.desktop? + assert @request.variant.any?(:tablet, :desktop) + assert_not @request.variant.any?(:desktop, :watch) end - test "reset variant" do - request = stub_request + test 'clearing variant' do + @request.variant = nil - request.variant = nil - assert_equal nil, request.variant + assert @request.variant.empty? + assert_not @request.variant.phone? + assert_not @request.variant.any?(:phone, :tablet) end - test "setting variant with non symbol value" do - request = stub_request + test 'setting variant to a non-symbol value' do + assert_raise ArgumentError do + @request.variant = 'phone' + end + end + + test 'setting variant to an array containing a non-symbol value' do assert_raise ArgumentError do - request.variant = "mobile" + @request.variant = [:phone, 'tablet'] end end end diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 55fc160ac8..62c99a2edc 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -4476,6 +4476,19 @@ class TestUrlGenerationErrors < ActionDispatch::IntegrationTest error = assert_raises(ActionController::UrlGenerationError, message){ product_path(id: nil) } assert_equal message, error.message end + + test "url helpers raise message with mixed parameters when generation fails " do + url, missing = { action: 'show', controller: 'products', id: nil, "id"=>"url-tested"}, [:id] + message = "No route matches #{url.inspect} missing required keys: #{missing.inspect}" + + # Optimized url helper + error = assert_raises(ActionController::UrlGenerationError){ product_path(nil, 'id'=>'url-tested') } + assert_equal message, error.message + + # Non-optimized url helper + error = assert_raises(ActionController::UrlGenerationError, message){ product_path(id: nil, 'id'=>'url-tested') } + assert_equal message, error.message + end end class TestDefaultUrlOptions < ActionDispatch::IntegrationTest diff --git a/actionpack/test/dispatch/static_test.rb b/actionpack/test/dispatch/static_test.rb index 288a2084f6..f153030675 100644 --- a/actionpack/test/dispatch/static_test.rb +++ b/actionpack/test/dispatch/static_test.rb @@ -2,6 +2,16 @@ require 'abstract_unit' require 'zlib' module StaticTests + def setup + @default_internal_encoding = Encoding.default_internal + @default_external_encoding = Encoding.default_external + end + + def teardown + Encoding.default_internal = @default_internal_encoding + Encoding.default_external = @default_external_encoding + end + def test_serves_dynamic_content assert_equal "Hello, World!", get("/nofile").body end @@ -10,6 +20,16 @@ module StaticTests assert_equal "Hello, World!", get("/doorkeeper%E3E4").body end + def test_handles_urls_with_ascii_8bit + assert_equal "Hello, World!", get("/doorkeeper%E3E4".force_encoding('ASCII-8BIT')).body + end + + def test_handles_urls_with_ascii_8bit_on_win_31j + Encoding.default_internal = "Windows-31J" + Encoding.default_external = "Windows-31J" + assert_equal "Hello, World!", get("/doorkeeper%E3E4".force_encoding('ASCII-8BIT')).body + end + def test_sets_cache_control response = get("/index.html") assert_html "/index.html", response @@ -208,6 +228,7 @@ class StaticTest < ActiveSupport::TestCase } def setup + super @root = "#{FIXTURE_LOAD_PATH}/public" @app = ActionDispatch::Static.new(DummyApp, @root, "public, max-age=60") end @@ -237,6 +258,7 @@ end class StaticEncodingTest < StaticTest def setup + super @root = "#{FIXTURE_LOAD_PATH}/公共" @app = ActionDispatch::Static.new(DummyApp, @root, "public, max-age=60") end diff --git a/actionpack/test/journey/router_test.rb b/actionpack/test/journey/router_test.rb index 19c61b5914..a134e343cc 100644 --- a/actionpack/test/journey/router_test.rb +++ b/actionpack/test/journey/router_test.rb @@ -401,6 +401,33 @@ module ActionDispatch assert_equal({:id => 1, :relative_url_root => nil}, params) end + def test_generate_missing_keys_no_matches_different_format_keys + path = Path::Pattern.from_string '/:controller/:action/:name' + @router.routes.add_route @app, path, {}, {}, {} + primarty_parameters = { + :id => 1, + :controller => "tasks", + :action => "show", + :relative_url_root => nil + } + redirection_parameters = { + 'action'=>'show', + } + missing_key = 'name' + missing_parameters ={ + missing_key => "task_1" + } + request_parameters = primarty_parameters.merge(redirection_parameters).merge(missing_parameters) + + message = "No route matches #{Hash[request_parameters.sort_by{|k,v|k.to_s}].inspect} missing required keys: #{[missing_key.to_sym].inspect}" + + error = assert_raises(ActionController::UrlGenerationError) do + @formatter.generate( + nil, request_parameters, request_parameters) + end + assert_equal message, error.message + end + def test_generate_uses_recall_if_needed path = Path::Pattern.from_string '/:controller(/:action(/:id))' @router.routes.add_route @app, path, {}, {}, {} diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 74a677968f..80aacf7234 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,13 @@ +* Accept lambda as `child_index` option in `fields_for` method. + + *Karol Galanciak* + +* `translate` allows `default: [[]]` again for a default value of `[]`. + + Fixes #19640. + + *Adam Prescott* + * `translate` should accept nils as members of the `:default` parameter without raising a translation missing error. Fixes a regression introduced 362557e. diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index 5c28043f8a..60fc9ee1a2 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -127,7 +127,7 @@ module ActionView # auto_discovery_link_tag(:rss, {controller: "news", action: "feed"}) # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/news/feed" /> # auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {title: "Example RSS"}) - # # => <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed" /> + # # => <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed.rss" /> def auto_discovery_link_tag(type = :rss, url_options = {}, tag_options = {}) if !(type == :rss || type == :atom) && tag_options[:type].blank? raise ArgumentError.new("You should pass :type tag_option key explicitly, because you have passed #{type} type other than :rss or :atom.") diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 891cc53765..ece117b547 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -1928,7 +1928,11 @@ module ActionView explicit_child_index = options[:child_index] output = ActiveSupport::SafeBuffer.new association.each do |child| - options[:child_index] = nested_child_index(name) unless explicit_child_index + if explicit_child_index + options[:child_index] = explicit_child_index.call if explicit_child_index.respond_to?(:call) + else + options[:child_index] = nested_child_index(name) + end output << fields_for_nested_model("#{name}[#{options[:child_index]}]", child, options, block) end output diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb index 29a0860c00..9d7390f1fd 100644 --- a/actionview/lib/action_view/helpers/translation_helper.rb +++ b/actionview/lib/action_view/helpers/translation_helper.rb @@ -41,7 +41,7 @@ module ActionView remaining_defaults = Array(options.delete(:default)).compact if has_default && !remaining_defaults.first.kind_of?(Symbol) - options[:default] = remaining_defaults.shift + options[:default] = remaining_defaults end # If the user has explicitly decided to NOT raise errors, pass that option to I18n. diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index 89b96ac42c..afb1265ad9 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -172,6 +172,11 @@ module ActionView # # link_to "Visit Other Site", "http://www.rubyonrails.org/", data: { confirm: "Are you sure?" } # # => <a href="http://www.rubyonrails.org/" data-confirm="Are you sure?">Visit Other Site</a> + # + # Also you can set any link attributes such as <tt>target</tt>, <tt>rel</tt>, <tt>type</tt>: + # + # link_to "External link", "http://www.rubyonrails.org/", target: "_blank", rel: "nofollow" + # # => <a href="http://www.rubyonrails.org/" target="_blank" rel="nofollow">External link</a> def link_to(name = nil, options = nil, html_options = nil, &block) html_options, options, name = options, name, block if block_given? options ||= {} @@ -471,57 +476,45 @@ module ActionView # True if the current request URI was generated by the given +options+. # # ==== Examples - # Let's say we're in the <tt>http://www.example.com/shop/checkout?order=desc</tt> action. + # Let's say we're in the <tt>http://www.example.com/shop/checkout?order=desc&page=1</tt> action. # # current_page?(action: 'process') # # => false # - # current_page?(controller: 'shop', action: 'checkout') - # # => true - # - # current_page?(controller: 'shop', action: 'checkout', order: 'asc') - # # => false - # # current_page?(action: 'checkout') # # => true # # current_page?(controller: 'library', action: 'checkout') # # => false # - # current_page?('http://www.example.com/shop/checkout') - # # => true - # - # current_page?('/shop/checkout') + # current_page?(controller: 'shop', action: 'checkout') # # => true # - # Let's say we're in the <tt>http://www.example.com/shop/checkout?order=desc&page=1</tt> action. - # - # current_page?(action: 'process') + # current_page?(controller: 'shop', action: 'checkout', order: 'asc') # # => false # - # current_page?(controller: 'shop', action: 'checkout') - # # => true - # # current_page?(controller: 'shop', action: 'checkout', order: 'desc', page: '1') # # => true # # current_page?(controller: 'shop', action: 'checkout', order: 'desc', page: '2') # # => false # - # current_page?(controller: 'shop', action: 'checkout', order: 'desc') - # # => false + # current_page?('http://www.example.com/shop/checkout') + # # => true # - # current_page?(action: 'checkout') + # current_page?('/shop/checkout') # # => true # - # current_page?(controller: 'library', action: 'checkout') - # # => false + # current_page?('http://www.example.com/shop/checkout?order=desc&page=1') + # # => true # # Let's say we're in the <tt>http://www.example.com/products</tt> action with method POST in case of invalid product. # # current_page?(controller: 'product', action: 'index') # # => false # + # We can also pass in the symbol arguments instead of strings. + # def current_page?(options) unless request raise "You cannot use helpers that need to determine the current " \ diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb index 9d636c8c9e..1fc609f2cd 100644 --- a/actionview/lib/action_view/layouts.rb +++ b/actionview/lib/action_view/layouts.rb @@ -315,16 +315,25 @@ module ActionView name_clause end - self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def _layout - if _conditional_layout? + if self._layout_conditions.empty? + self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def _layout #{layout_definition} - else - #{name_clause} end - end - private :_layout - RUBY + private :_layout + RUBY + else + self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def _layout + if _conditional_layout? + #{layout_definition} + else + #{name_clause} + end + end + private :_layout + RUBY + end end private diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb index 36855ec3d0..4452dcfed5 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -126,7 +126,7 @@ module ActionView @view_paths.find_all(*args_for_lookup(name, prefixes, partial, keys, options)) end - def exists?(name, prefixes = [], partial = false, keys = [], options = {}) + def exists?(name, prefixes = [], partial = false, keys = [], **options) @view_paths.exists?(*args_for_lookup(name, prefixes, partial, keys, options)) end alias :template_exists? :exists? diff --git a/actionview/lib/action_view/renderer/renderer.rb b/actionview/lib/action_view/renderer/renderer.rb index 964b18337e..1bee35d80d 100644 --- a/actionview/lib/action_view/renderer/renderer.rb +++ b/actionview/lib/action_view/renderer/renderer.rb @@ -37,7 +37,7 @@ module ActionView end end - # Direct accessor to template rendering. + # Direct access to template rendering. def render_template(context, options) #:nodoc: TemplateRenderer.new(@lookup_context).render(context, options) end diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb index cd21d7ab47..dbb4855e39 100644 --- a/actionview/lib/action_view/renderer/template_renderer.rb +++ b/actionview/lib/action_view/renderer/template_renderer.rb @@ -40,7 +40,7 @@ module ActionView find_template(options[:template], options[:prefixes], false, keys, @details) end else - raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :text or :body option." + raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :html, :text or :body option." end end diff --git a/actionview/lib/action_view/template/handlers/raw.rb b/actionview/lib/action_view/template/handlers/raw.rb index 397c86014a..b08fb0870f 100644 --- a/actionview/lib/action_view/template/handlers/raw.rb +++ b/actionview/lib/action_view/template/handlers/raw.rb @@ -2,7 +2,7 @@ module ActionView module Template::Handlers class Raw def call(template) - escaped = template.source.gsub(/:/, '\:') + escaped = template.source.gsub(':'.freeze, '\:'.freeze) '%q:' + escaped + ':;' end diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb index bc0db330ea..955118a554 100644 --- a/actionview/lib/action_view/template/resolver.rb +++ b/actionview/lib/action_view/template/resolver.rb @@ -270,7 +270,7 @@ module ActionView # # ActionController::Base.view_paths = FileSystemResolver.new( # Rails.root.join("app/views"), - # ":prefix{/:locale}/:action{.:formats,}{+:variants,}{.:handlers,}" + # ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}", # ) # # ==== Pattern format and variables diff --git a/actionview/test/actionpack/controller/layout_test.rb b/actionview/test/actionpack/controller/layout_test.rb index 7b8a83e2fe..64ab125637 100644 --- a/actionview/test/actionpack/controller/layout_test.rb +++ b/actionview/test/actionpack/controller/layout_test.rb @@ -122,6 +122,14 @@ class PrependsViewPathController < LayoutTest end end +class ParentController < LayoutTest + layout 'item' +end + +class ChildController < ParentController + layout 'layout_test', only: :hello +end + class OnlyLayoutController < LayoutTest layout 'item', :only => "hello" end @@ -225,6 +233,12 @@ class LayoutSetInResponseTest < ActionController::TestCase get :hello assert_equal "layout_test.erb hello.erb", @response.body.strip end + + def test_respect_to_parent_layout + @controller = ChildController.new + get :goodbye + assert_template :layout => "layouts/item" + end end class SetsNonExistentLayoutFile < LayoutTest diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb index 4e336bea63..5c55b154d3 100644 --- a/actionview/test/template/form_helper_test.rb +++ b/actionview/test/template/form_helper_test.rb @@ -2878,6 +2878,23 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end + def test_nested_fields_for_with_child_index_as_lambda_option_override_on_a_nested_attributes_collection_association + @post.comments = [] + + form_for(@post) do |f| + concat f.fields_for(:comments, Comment.new(321), child_index: -> { 'abc' } ) { |cf| + concat cf.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' + + '<input id="post_comments_attributes_abc_id" name="post[comments_attributes][abc][id]" type="hidden" value="321" />' + end + + assert_dom_equal expected, output_buffer + end + class FakeAssociationProxy def to_ary [1, 2, 3] diff --git a/actionview/test/template/translation_helper_test.rb b/actionview/test/template/translation_helper_test.rb index c4daaae221..df096b3c3a 100644 --- a/actionview/test/template/translation_helper_test.rb +++ b/actionview/test/template/translation_helper_test.rb @@ -195,6 +195,11 @@ class TranslationHelperTest < ActiveSupport::TestCase assert_equal 'A Generic String', translation end + def test_translate_with_array_of_array_default + translation = translate(:'translations.missing', default: [[]]) + assert_equal [], translation + end + def test_translate_does_not_change_options options = {} translate(:'translations.missing', options) diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md index d4e19274fa..85a437a1dd 100644 --- a/activejob/CHANGELOG.md +++ b/activejob/CHANGELOG.md @@ -1,3 +1,7 @@ +* A generated job now inherents from `app/jobs/application_job.rb` by default. + + *Jeroen van Baarsen* + * Add an `:only` option to `perform_enqueued_jobs` to filter jobs based on type. diff --git a/activejob/lib/active_job/queue_adapters.rb b/activejob/lib/active_job/queue_adapters.rb index b3d91dc562..8aa85979f6 100644 --- a/activejob/lib/active_job/queue_adapters.rb +++ b/activejob/lib/active_job/queue_adapters.rb @@ -29,11 +29,75 @@ module ActiveJob # | Active Job Inline | No | Yes | N/A | N/A | N/A | N/A | # | Active Job | Yes | Yes | Yes | No | No | No | # + # ==== Async + # + # Yes: The Queue Adapter runs the jobs in a separate or forked process. + # + # No: The job is run in the same process. + # + # ==== Queues + # + # Yes: Jobs may set which queue they are run in with queue_as or by using the set + # method. + # + # ==== Delayed + # + # Yes: The adapter will run the job in the future through perform_later. + # + # (Gem): An additional gem is required to use perform_later with this adapter. + # + # No: The adapter will run jobs at the next opportunity and cannot use perform_later. + # + # N/A: The adapter does not support queueing. + # # NOTE: - # queue_classic does not support Job scheduling. However you can implement this - # yourself or you can use the queue_classic-later gem. See the documentation for - # ActiveJob::QueueAdapters::QueueClassicAdapter. + # queue_classic does not support job scheduling. + # However, you can use the queue_classic-later gem. + # See the documentation for ActiveJob::QueueAdapters::QueueClassicAdapter. + # + # ==== Priorities + # + # The order in which jobs are processed can be configured differently depending + # on the adapter. + # + # Job: Any class inheriting from the adapter may set the priority on the job + # object relative to other jobs. + # + # Queue: The adapter can set the priority for job queues, when setting a queue + # with Active Job this will be respected. + # + # Yes: Allows the priority to be set on the job object, at the queue level or + # as default configuration option. + # + # No: Does not allow the priority of jobs to be configured. + # + # N/A: The adapter does not support queueing, and therefore sorting them. + # + # ==== Timeout + # + # When a job will stop after the allotted time. + # + # Job: The timeout can be set for each instance of the job class. + # + # Queue: The timeout is set for all jobs on the queue. + # + # Global: The adapter is configured that all jobs have a maximum run time. + # + # N/A: This adapter does not run in a separate process, and therefore timeout + # is unsupported. + # + # ==== Retries + # + # Job: The number of retries can be set per instance of the job class. + # + # Yes: The Number of retries can be configured globally, for each instance or + # on the queue. This adapter may also present failed instances of the job class + # that can be restarted. + # + # Global: The adapter has a global number of retries. # + # N/A: The adapter does not run in a separate process, and therefore doesn't + # support retries. module QueueAdapters extend ActiveSupport::Autoload diff --git a/activejob/lib/rails/generators/job/job_generator.rb b/activejob/lib/rails/generators/job/job_generator.rb index 979ffcb748..86e4c5266c 100644 --- a/activejob/lib/rails/generators/job/job_generator.rb +++ b/activejob/lib/rails/generators/job/job_generator.rb @@ -18,7 +18,6 @@ module Rails def create_job_file template 'job.rb', File.join('app/jobs', class_path, "#{file_name}_job.rb") end - end end end diff --git a/activejob/lib/rails/generators/job/templates/job.rb b/activejob/lib/rails/generators/job/templates/job.rb index 462c71d917..4ad2914a45 100644 --- a/activejob/lib/rails/generators/job/templates/job.rb +++ b/activejob/lib/rails/generators/job/templates/job.rb @@ -1,5 +1,5 @@ <% module_namespacing do -%> -class <%= class_name %>Job < ActiveJob::Base +class <%= class_name %>Job < ApplicationJob queue_as :<%= options[:queue] %> def perform(*args) diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 80b42859e2..32a2cb4517 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,3 +1,8 @@ +* Deprecate the `:tokenizer` option for `validates_length_of`, in favor of + plain Ruby. + + *Sean Griffin* + * Deprecate `ActiveModel::Errors#add_on_empty` and `ActiveModel::Errors#add_on_blank` with no replacement. @@ -35,11 +40,11 @@ cat = Cat.new cat.assign_attributes(name: "Gorby", status: "yawning") - cat.name # => 'Gorby' - cat.status => 'yawning' + cat.name # => 'Gorby' + cat.status # => 'yawning' cat.assign_attributes(status: "sleeping") - cat.name # => 'Gorby' - cat.status => 'sleeping' + cat.name # => 'Gorby' + cat.status # => 'sleeping' *Bogdan Gusiev* diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index e35ed03e74..f843b279ce 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -102,9 +102,7 @@ module ActiveModel def include?(attribute) messages[attribute].present? end - # aliases include? alias :has_key? :include? - # aliases include? alias :key? :include? # Get messages for +key+. @@ -199,6 +197,7 @@ module ActiveModel def size values.flatten.size end + alias :count :size # Returns all message values. # @@ -216,35 +215,15 @@ module ActiveModel messages.keys end - # Returns an array of error messages, with the attribute name included. - # - # person.errors.add(:name, :blank, message: "can't be blank") - # person.errors.add(:name, :not_specified, message: "must be specified") - # person.errors.to_a # => ["name can't be blank", "name must be specified"] - def to_a - full_messages - end - - # Returns the number of error messages. - # - # person.errors.add(:name, :blank, message: "can't be blank") - # person.errors.count # => 1 - # person.errors.add(:name, :not_specified, message: "must be specified") - # person.errors.count # => 2 - def count - to_a.size - end - # Returns +true+ if no errors are found, +false+ otherwise. # If the error message is a string it can be empty. # # person.errors.full_messages # => ["name cannot be nil"] # person.errors.empty? # => false def empty? - all? { |k, v| v && v.empty? && !v.is_a?(String) } + size.zero? end - # aliases empty? - alias_method :blank?, :empty? + alias :blank? :empty? # Returns an xml formatted representation of the Errors hash. # @@ -407,6 +386,7 @@ module ActiveModel def full_messages map { |attribute, message| full_message(attribute, message) } end + alias :to_a :full_messages # Returns all the full error messages for a given attribute in an array. # diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index 22010b517c..1f1749af4e 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -1,6 +1,7 @@ require 'active_support/core_ext/hash/except' require 'active_support/core_ext/module/introspection' require 'active_support/core_ext/module/remove_method' +require 'active_support/core_ext/module/delegation' module ActiveModel class Name diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 176d4c0607..74d60327d6 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -401,7 +401,7 @@ module ActiveModel protected def run_validations! #:nodoc: - _run_validate_callbacks + run_callbacks :validate errors.empty? end diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb index 4b58ef66e3..b4301c23e4 100644 --- a/activemodel/lib/active_model/validations/callbacks.rb +++ b/activemodel/lib/active_model/validations/callbacks.rb @@ -109,7 +109,7 @@ module ActiveModel # Overwrite run validations to include callbacks. def run_validations! #:nodoc: - _run_validation_callbacks { super } + run_callbacks(:validation) { super } end end end diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index 23201b264a..c22a58f9e1 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -1,3 +1,5 @@ +require "active_support/core_ext/string/strip" + module ActiveModel # == Active \Model Length Validator @@ -18,6 +20,27 @@ module ActiveModel options[:minimum] = 1 end + if options[:tokenizer] + ActiveSupport::Deprecation.warn(<<-EOS.strip_heredoc) + The `:tokenizer` option is deprecated, and will be removed in Rails 5.1. + You can achieve the same functionality by defining an instance method + with the value that you want to validate the length of. For example, + + validates_length_of :essay, minimum: 100, + tokenizer: ->(str) { str.scan(/\w+/) } + + should be written as + + validates_length_of :words_in_essay, minimum: 100 + + private + + def words_in_essay + essay.scan(/\w+/) + end + EOS + end + super end @@ -88,8 +111,13 @@ module ActiveModel # validates_length_of :user_name, within: 6..20, too_long: 'pick a shorter name', too_short: 'pick a longer name' # validates_length_of :zip_code, minimum: 5, too_short: 'please enter at least 5 characters' # validates_length_of :smurf_leader, is: 4, message: "papa is spelled with 4 characters... don't play me." - # validates_length_of :essay, minimum: 100, too_short: 'Your essay must be at least 100 words.', - # tokenizer: ->(str) { str.scan(/\w+/) } + # validates_length_of :words_in_essay, minimum: 100, too_short: 'Your essay must be at least 100 words.' + # + # private + # + # def words_in_essay + # essay.scan(/\w+/) + # end # end # # Configuration options: @@ -112,12 +140,6 @@ module ActiveModel # * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>, # <tt>:maximum</tt>, or <tt>:is</tt> violation. An alias of the appropriate # <tt>too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message. - # * <tt>:tokenizer</tt> - A method (as a symbol), proc or string to - # specify how to split up the attribute string. (e.g. - # <tt>tokenizer: :word_tokenizer</tt> to call the +word_tokenizer+ method - # or <tt>tokenizer: ->(str) { str.scan(/\w+/) }</tt> to count words as in - # above example). Defaults to <tt>->(value) { value.split(//) }</tt> which - # counts individual characters. # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+ and +:strict+. diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index ddd003d53f..f781a0017f 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -212,6 +212,12 @@ class ErrorsTest < ActiveModel::TestCase assert_equal 1, person.errors.size end + test "count calculates the number of error messages" do + person = Person.new + person.errors.add(:name, "cannot be blank") + assert_equal 1, person.errors.count + end + test "to_a returns the list of errors with complete messages containing the attribute names" do person = Person.new person.errors.add(:name, "cannot be blank") diff --git a/activemodel/test/cases/validations/length_validation_test.rb b/activemodel/test/cases/validations/length_validation_test.rb index 209903898e..ee901b75fb 100644 --- a/activemodel/test/cases/validations/length_validation_test.rb +++ b/activemodel/test/cases/validations/length_validation_test.rb @@ -319,8 +319,14 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_with_block - Topic.validates_length_of :content, minimum: 5, too_short: "Your essay must be at least %{count} words.", - tokenizer: lambda {|str| str.scan(/\w+/) } + assert_deprecated do + Topic.validates_length_of( + :content, + minimum: 5, + too_short: "Your essay must be at least %{count} words.", + tokenizer: lambda {|str| str.scan(/\w+/) }, + ) + end t = Topic.new(content: "this content should be long enough") assert t.valid? @@ -332,8 +338,14 @@ class LengthValidationTest < ActiveModel::TestCase def test_validates_length_of_with_symbol - Topic.validates_length_of :content, minimum: 5, too_short: "Your essay must be at least %{count} words.", - tokenizer: :my_word_tokenizer + assert_deprecated do + Topic.validates_length_of( + :content, + minimum: 5, + too_short: "Your essay must be at least %{count} words.", + tokenizer: :my_word_tokenizer, + ) + end t = Topic.new(content: "this content should be long enough") assert t.valid? diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 6c129a1c79..904ac5c26a 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,39 @@ +* Fixed a bug where uniqueness validations would error on out of range values, + even if an validation should have prevented it from hitting the database. + + *Andrey Voronkov* + +* MySQL: `:charset` and `:collation` support for string and text columns. + + Example: + + create_table :foos do |t| + t.string :string_utf8_bin, charset: 'utf8', collation: 'utf8_bin' + t.text :text_ascii, charset: 'ascii' + end + + *Ryuta Kamizono* + +* Foreign key related methods in the migration DSL respect + `ActiveRecord::Base.pluralize_table_names = false`. + + Fixes #19643. + + *Mehmet Emin İNAÇ* + +* Reduce memory usage from loading types on pg. + + Fixes #19578. + + *Sean Griffin* + +* Add `config.active_record.warn_on_records_fetched_greater_than` option + + When set to an integer, a warning will be logged whenever a result set + larger than the specified size is returned by a query. Fixes #16463 + + *Jason Nochlin* + * Ignore psqlrc when loading database structure. *Jason Weathered* diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 58a4694880..1844b29ccb 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -49,6 +49,7 @@ module ActiveRecord autoload :ModelSchema autoload :NestedAttributes autoload :NoTouching + autoload :TouchLater autoload :Persistence autoload :QueryCache autoload :Querying diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 5e3e5f709b..c5c2178ee2 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1565,10 +1565,7 @@ module ActiveRecord # # class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration # def change - # create_table :developers_projects, id: false do |t| - # t.integer :developer_id - # t.integer :project_id - # end + # create_join_table :developers, :projects # end # end # @@ -1718,10 +1715,8 @@ module ActiveRecord join_model = builder.through_model - # FIXME: we should move this to the internal constants. Also people - # should never directly access this constant so I'm not happy about - # setting it. const_set join_model.name, join_model + private_constant join_model.name middle_reflection = builder.middle_reflection join_model diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index ec135d49b7..97eb007f62 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -60,7 +60,7 @@ module ActiveRecord::Associations::Builder klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly) end - def self.touch_record(o, foreign_key, name, touch) # :nodoc: + def self.touch_record(o, foreign_key, name, touch, touch_method) # :nodoc: old_foreign_id = o.changed_attributes[foreign_key] if old_foreign_id @@ -75,9 +75,9 @@ module ActiveRecord::Associations::Builder if old_record if touch != true - old_record.touch touch + old_record.send(touch_method, touch) else - old_record.touch + old_record.send(touch_method) end end end @@ -85,9 +85,9 @@ module ActiveRecord::Associations::Builder record = o.send name if record && record.persisted? if touch != true - record.touch touch + record.send(touch_method, touch) else - record.touch + record.send(touch_method) end end end @@ -98,7 +98,8 @@ module ActiveRecord::Associations::Builder touch = reflection.options[:touch] callback = lambda { |record| - BelongsTo.touch_record(record, foreign_key, n, touch) + touch_method = touching_delayed_records? ? :touch : :touch_later + BelongsTo.touch_record(record, foreign_key, n, touch, touch_method) } model.after_save callback, if: :changed? diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index 93dc4ae118..97b57a6a55 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -85,7 +85,7 @@ module ActiveRecord::Associations::Builder def middle_reflection(join_model) middle_name = [lhs_model.name.downcase.pluralize, - association_name].join('_').gsub(/::/, '_').to_sym + association_name].join('_'.freeze).gsub('::'.freeze, '_'.freeze).to_sym middle_options = middle_options join_model HasMany.create_reflection(lhs_model, diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 88531205a1..6caadb4ce8 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -370,6 +370,8 @@ module ActiveRecord replace_common_records_in_memory(other_array, original_target) if other_array != original_target transaction { replace_records(other_array, original_target) } + else + other_array end end end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 4897ec44e9..29e8a0edc1 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -135,7 +135,7 @@ module ActiveRecord if scope.klass.primary_key count = scope.destroy_all.length else - scope.each(&:_run_destroy_callbacks) + scope.each { |record| record.run_callbacks :destroy } arel = scope.arel diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 93550a69f1..67490ecd97 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -309,6 +309,7 @@ module ActiveRecord #:nodoc: include Aggregations include Transactions include NoTouching + include TouchLater include Reflection include Serialization include Store diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index f44e5af5de..2fcba8e309 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -289,25 +289,24 @@ module ActiveRecord end def destroy #:nodoc: - _run_destroy_callbacks { super } + run_callbacks(:destroy) { super } end def touch(*) #:nodoc: - _run_touch_callbacks { super } + run_callbacks(:touch) { super } end private - def create_or_update(*) #:nodoc: - _run_save_callbacks { super } + run_callbacks(:save) { super } end def _create_record #:nodoc: - _run_create_callbacks { super } + run_callbacks(:create) { super } end def _update_record(*) #:nodoc: - _run_update_callbacks { super } + run_callbacks(:update) { super } end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index d99dc9a5db..8c50f3d1a3 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -358,7 +358,7 @@ module ActiveRecord synchronize do owner = conn.owner - conn._run_checkin_callbacks do + conn.run_callbacks :checkin do conn.expire end @@ -449,7 +449,7 @@ module ActiveRecord end def checkout_and_verify(c) - c._run_checkout_callbacks do + c.run_callbacks :checkout do c.verify! end c diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index d2840b9498..91c7298983 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -77,7 +77,7 @@ module ActiveRecord # Quotes a string, escaping any ' (single quote) and \ (backslash) # characters. def quote_string(s) - s.gsub(/\\/, '\&\&').gsub(/'/, "''") # ' (for ruby-mode) + s.gsub('\\'.freeze, '\&\&'.freeze).gsub("'".freeze, "''".freeze) # ' (for ruby-mode) end # Quotes the column name. Defaults to no quoting. 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 a768ee2d70..cb83d0022c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -138,7 +138,7 @@ module ActiveRecord end def foreign_table_name - name.to_s.pluralize + Base.pluralize_table_names ? name.to_s.pluralize : name end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 72e019066e..ecb4868c13 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -377,6 +377,9 @@ module ActiveRecord # [<tt>:force</tt>] # Set to +:cascade+ to drop dependent objects as well. # Defaults to false. + # [<tt>:if_exists</tt>] + # Set to +true+ to only drop the table if it exists. + # Defaults to false. # # Although this command ignores most +options+ and the block if one is given, # it can be helpful to provide these in a migration's +change+ method so it can be reverted. @@ -387,6 +390,10 @@ module ActiveRecord # Adds a new column to the named table. # See TableDefinition#column for details of the options you can use. + # + # Note: Not all options will be available, generally this command should + # ignore most of them. In favor of doing a low-level call to simply + # create a column. def add_column(table_name, column_name, type, options = {}) at = create_alter_table table_name at.add_column(column_name, type, options) @@ -530,6 +537,8 @@ module ActiveRecord # # CREATE UNIQUE INDEX index_accounts_on_branch_id_and_party_id ON accounts(branch_id, party_id) WHERE active # + # Note: Partial indexes are only supported for PostgreSQL and SQLite 3.8.0+. + # # ====== Creating an index with a specific method # # add_index(:developers, :name, using: 'btree') @@ -667,7 +676,10 @@ module ActiveRecord # remove_reference(:products, :user, index: true, foreign_key: true) # def remove_reference(table_name, ref_name, options = {}) - remove_foreign_key table_name, ref_name.to_s.pluralize if options[:foreign_key] + if options[:foreign_key] + reference_name = Base.pluralize_table_names ? ref_name.to_s.pluralize : ref_name + remove_foreign_key(table_name, reference_name) + end remove_column(table_name, "#{ref_name}_id") remove_column(table_name, "#{ref_name}_type") if options[:polymorphic] @@ -684,8 +696,8 @@ module ActiveRecord # +to_table+ contains the referenced primary key. # # The foreign key will be named after the following pattern: <tt>fk_rails_<identifier></tt>. - # +identifier+ is a 10 character long random string. A custom name can be specified with - # the <tt>:name</tt> option. + # +identifier+ is a 10 character long string which is deterministically generated from the + # +from_table+ and +column+. A custom name can be specified with the <tt>:name</tt> option. # # ====== Creating a simple foreign key # diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index b7c7ff1187..76aee452ca 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -13,6 +13,10 @@ module ActiveRecord end end + class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition + attr_accessor :charset, :collation + end + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition include ColumnMethods @@ -23,8 +27,16 @@ module ActiveRecord column.type = :integer column.auto_increment = true end + column.charset = options[:charset] + column.collation = options[:collation] column end + + private + + def create_column_definition(name, type) + ColumnDefinition.new(name, type) + end end class Table < ActiveRecord::ConnectionAdapters::Table @@ -60,6 +72,23 @@ module ActiveRecord add_column_position!(change_column_sql, column_options(o.column)) end + def column_options(o) + column_options = super + column_options[:charset] = o.charset + column_options[:collation] = o.collation + column_options + end + + def add_column_options!(sql, options) + if options[:charset] + sql << " CHARACTER SET #{options[:charset]}" + end + if options[:collation] + sql << " COLLATE #{options[:collation]}" + end + super + end + def add_column_position!(sql, options) if options[:first] sql << " FIRST" @@ -99,9 +128,18 @@ module ActiveRecord spec = super spec.delete(:precision) if /time/ === column.sql_type && column.precision == 0 spec.delete(:limit) if :boolean === column.type + if column.collation && table_name = column.instance_variable_get(:@table_name) + @collation_cache ||= {} + @collation_cache[table_name] ||= select_one("SHOW TABLE STATUS LIKE '#{table_name}'")["Collation"] + spec[:collation] = column.collation.inspect if column.collation != @collation_cache[table_name] + end spec end + def migration_keys + super + [:collation] + end + class Column < ConnectionAdapters::Column # :nodoc: delegate :strict, :collation, :extra, to: :sql_type_metadata, allow_nil: true @@ -562,6 +600,21 @@ module ActiveRecord rename_table_indexes(table_name, new_name) end + # Drops a table from the database. + # + # [<tt>:force</tt>] + # Set to +:cascade+ to drop dependent objects as well. + # Defaults to false. + # [<tt>:if_exists</tt>] + # Set to +true+ to only drop the table if it exists. + # Defaults to false. + # [<tt>:temporary</tt>] + # Set to +true+ to drop temporary table. + # Defaults to false. + # + # Although this command ignores most +options+ and the block if one is given, + # it can be helpful to provide these in a migration's +change+ method so it can be reverted. + # In that case, +options+ and the block will be used by create_table. def drop_table(table_name, options = {}) execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" end diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index a67127bd71..f4dda5154e 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -28,6 +28,7 @@ module ActiveRecord @null = null @default = default @default_function = default_function + @table_name = nil end def has_default? diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb deleted file mode 100644 index 1b74c039ce..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb +++ /dev/null @@ -1,93 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module ArrayParser # :nodoc: - - DOUBLE_QUOTE = '"' - BACKSLASH = "\\" - COMMA = ',' - BRACKET_OPEN = '{' - BRACKET_CLOSE = '}' - - def parse_pg_array(string) # :nodoc: - local_index = 0 - array = [] - while(local_index < string.length) - case string[local_index] - when BRACKET_OPEN - local_index,array = parse_array_contents(array, string, local_index + 1) - when BRACKET_CLOSE - return array - end - local_index += 1 - end - - array - end - - private - - def parse_array_contents(array, string, index) - is_escaping = false - is_quoted = false - was_quoted = false - current_item = '' - - local_index = index - while local_index - token = string[local_index] - if is_escaping - current_item << token - is_escaping = false - else - if is_quoted - case token - when DOUBLE_QUOTE - is_quoted = false - was_quoted = true - when BACKSLASH - is_escaping = true - else - current_item << token - end - else - case token - when BACKSLASH - is_escaping = true - when COMMA - add_item_to_array(array, current_item, was_quoted) - current_item = '' - was_quoted = false - when DOUBLE_QUOTE - is_quoted = true - when BRACKET_OPEN - internal_items = [] - local_index,internal_items = parse_array_contents(internal_items, string, local_index + 1) - array.push(internal_items) - when BRACKET_CLOSE - add_item_to_array(array, current_item, was_quoted) - return local_index,array - else - current_item << token - end - end - end - - local_index += 1 - end - return local_index,array - end - - def add_item_to_array(array, current_item, quoted) - return if !quoted && current_item.length == 0 - - if !quoted && current_item == 'NULL' - array.push nil - else - array.push current_item - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb index f486c5ecb7..3de794f797 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -5,29 +5,20 @@ module ActiveRecord class Array < Type::Value # :nodoc: include Type::Helpers::Mutable - # Loads pg_array_parser if available. String parsing can be - # performed quicker by a native extension, which will not create - # a large amount of Ruby objects that will need to be garbage - # collected. pg_array_parser has a C and Java extension - begin - require 'pg_array_parser' - include PgArrayParser - rescue LoadError - require 'active_record/connection_adapters/postgresql/array_parser' - include PostgreSQL::ArrayParser - end - attr_reader :subtype, :delimiter delegate :type, :user_input_in_time_zone, :limit, to: :subtype def initialize(subtype, delimiter = ',') @subtype = subtype @delimiter = delimiter + + @pg_encoder = PG::TextEncoder::Array.new name: "#{type}[]", delimiter: delimiter + @pg_decoder = PG::TextDecoder::Array.new name: "#{type}[]", delimiter: delimiter end def deserialize(value) if value.is_a?(::String) - type_cast_array(parse_pg_array(value), :deserialize) + type_cast_array(@pg_decoder.decode(value), :deserialize) else super end @@ -35,14 +26,14 @@ module ActiveRecord def cast(value) if value.is_a?(::String) - value = parse_pg_array(value) + value = @pg_decoder.decode(value) end type_cast_array(value, :cast) end def serialize(value) if value.is_a?(::Array) - cast_value_for_database(value) + @pg_encoder.encode(type_cast_array(value, :serialize)) else super end @@ -63,41 +54,6 @@ module ActiveRecord @subtype.public_send(method, value) end end - - def cast_value_for_database(value) - if value.is_a?(::Array) - casted_values = value.map { |item| cast_value_for_database(item) } - "{#{casted_values.join(delimiter)}}" - else - quote_and_escape(subtype.serialize(value)) - end - end - - ARRAY_ESCAPE = "\\" * 2 * 2 # escape the backslash twice for PG arrays - - def quote_and_escape(value) - case value - when ::String - if string_requires_quoting?(value) - value = value.gsub(/\\/, ARRAY_ESCAPE) - value.gsub!(/"/,"\\\"") - %("#{value}") - else - value - end - when nil then "NULL" - else value - end - end - - # See http://www.postgresql.org/docs/9.2/static/arrays.html#ARRAYS-IO - # for a list of all cases in which strings will be quoted. - def string_requires_quoting?(string) - string.empty? || - string == "NULL" || - string =~ /[\{\}"\\\s]/ || - string.include?(delimiter) - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb index 9b3de41fab..191c828e60 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb @@ -15,11 +15,11 @@ module ActiveRecord def run(records) nodes = records.reject { |row| @store.key? row['oid'].to_i } mapped, nodes = nodes.partition { |row| @store.key? row['typname'] } - ranges, nodes = nodes.partition { |row| row['typtype'] == 'r' } - enums, nodes = nodes.partition { |row| row['typtype'] == 'e' } - domains, nodes = nodes.partition { |row| row['typtype'] == 'd' } - arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' } - composites, nodes = nodes.partition { |row| row['typelem'] != '0' } + ranges, nodes = nodes.partition { |row| row['typtype'] == 'r'.freeze } + enums, nodes = nodes.partition { |row| row['typtype'] == 'e'.freeze } + domains, nodes = nodes.partition { |row| row['typtype'] == 'd'.freeze } + arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in'.freeze } + composites, nodes = nodes.partition { |row| row['typelem'].to_i != 0 } mapped.each { |row| register_mapped_type(row) } enums.each { |row| register_enum_type(row) } @@ -29,6 +29,18 @@ module ActiveRecord composites.each { |row| register_composite_type(row) } end + def query_conditions_for_initial_load(type_map) + known_type_names = type_map.keys.map { |n| "'#{n}'" } + known_type_types = %w('r' 'e' 'd') + <<-SQL % [known_type_names.join(", "), known_type_types.join(", ")] + WHERE + t.typname IN (%s) + OR t.typtype IN (%s) + OR t.typinput::varchar = 'array_in' + OR t.typelem != 0 + SQL + end + private def register_mapped_type(row) alias_type row['oid'], row['typname'] diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index eeb141dd1e..168180cfd3 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -87,7 +87,7 @@ module ActiveRecord SQL end - def drop_table(table_name, options = {}) + def drop_table(table_name, options = {}) # :nodoc: execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" end @@ -129,8 +129,8 @@ module ActiveRecord result.map do |row| index_name = row[0] - unique = row[1] == 't' - indkey = row[2].split(" ") + unique = row[1] + indkey = row[2].split(" ").map(&:to_i) inddef = row[3] oid = row[4] @@ -164,7 +164,7 @@ module ActiveRecord type_metadata = fetch_type_metadata(column_name, type, oid, fmod) default_value = extract_value_from_default(default) default_function = extract_default_function(default_value, default) - new_column(column_name, default_value, type_metadata, notnull == 'f', default_function) + new_column(column_name, default_value, type_metadata, !notnull, default_function) end end @@ -422,7 +422,7 @@ module ActiveRecord end # Changes the default value of a table column. - def change_column_default(table_name, column_name, default) + def change_column_default(table_name, column_name, default) # :nodoc: clear_cache! column = column_for(table_name, column_name) return unless column diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 96a3ac7c31..332ac9d88c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,3 +1,7 @@ +# Make sure we're using pg high enough for type casts and Ruby 2.2+ compatibility +gem 'pg', '~> 0.18' +require 'pg' + require "active_record/connection_adapters/abstract_adapter" require "active_record/connection_adapters/postgresql/column" require "active_record/connection_adapters/postgresql/database_statements" @@ -12,10 +16,6 @@ require "active_record/connection_adapters/statement_pool" require 'arel/visitors/bind_visitor' -# Make sure we're using pg high enough for Ruby 2.2+ compatibility -gem 'pg', '~> 0.18' -require 'pg' - require 'ipaddr' module ActiveRecord @@ -278,8 +278,7 @@ module ActiveRecord @table_alias_length = nil connect - add_pg_decoders - + add_pg_encoders @statements = StatementPool.new @connection, self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }) @@ -287,6 +286,8 @@ module ActiveRecord raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!" end + add_pg_decoders + @type_map = Type::HashLookupTypeMap.new initialize_type_map(type_map) @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"] @@ -567,9 +568,9 @@ module ActiveRecord case default # Quoted types when /\A[\(B]?'(.*)'::/m - $1.gsub(/''/, "'") + $1.gsub("''".freeze, "'".freeze) # Boolean types - when 'true', 'false' + when 'true'.freeze, 'false'.freeze default # Numeric types when /\A\(?(-?\d+(\.\d*)?)\)?(::bigint)?\z/ @@ -593,6 +594,8 @@ module ActiveRecord end def load_additional_types(type_map, oids = nil) # :nodoc: + initializer = OID::TypeMapInitializer.new(type_map) + if supports_ranges? query = <<-SQL SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype @@ -608,11 +611,13 @@ module ActiveRecord if oids query += "WHERE t.oid::integer IN (%s)" % oids.join(", ") + else + query += initializer.query_conditions_for_initial_load(type_map) end - initializer = OID::TypeMapInitializer.new(type_map) - records = execute(query, 'SCHEMA') - initializer.run(records) + execute_and_clear(query, 'SCHEMA', []) do |records| + initializer.run(records) + end end FEATURE_NOT_SUPPORTED = "0A000" #:nodoc: @@ -798,11 +803,20 @@ module ActiveRecord ) end_sql execute_and_clear(sql, "SCHEMA", []) do |result| - result.getvalue(0, 0) == 't' + result.getvalue(0, 0) end end end + def add_pg_encoders + map = PG::TypeMapByClass.new + map[Integer] = PG::TextEncoder::Integer.new + map[TrueClass] = PG::TextEncoder::Boolean.new + map[FalseClass] = PG::TextEncoder::Boolean.new + map[Float] = PG::TextEncoder::Float.new + @connection.type_map_for_queries = map + end + def add_pg_decoders coders_by_name = { 'int2' => PG::TextDecoder::Integer, @@ -813,13 +827,15 @@ module ActiveRecord 'float8' => PG::TextDecoder::Float, 'bool' => PG::TextDecoder::Boolean, } - query = <<-SQL - SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, t.typtype, t.typbasetype + known_coder_types = coders_by_name.keys.map { |n| quote(n) } + query = <<-SQL % known_coder_types.join(", ") + SELECT t.oid, t.typname FROM pg_type as t + WHERE t.typname IN (%s) SQL coders = execute_and_clear(query, "SCHEMA", []) do |result| result - .map { |row| construct_coder(row, coders_by_name['typname']) } + .map { |row| construct_coder(row, coders_by_name[row['typname']]) } .compact end @@ -830,7 +846,7 @@ module ActiveRecord def construct_coder(row, coder_class) return unless coder_class - coder_class.new(oid: row['oid'], name: row['typname']) + coder_class.new(oid: row['oid'].to_i, name: row['typname']) end ActiveRecord::Type.add_modifier({ array: true }, OID::Array, adapter: :postgresql) diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 9a39a0e919..1ad910c4bc 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -94,6 +94,15 @@ module ActiveRecord mattr_accessor :dump_schemas, instance_writer: false self.dump_schemas = :schema_search_path + ## + # :singleton-method: + # Specify a threshold for the size of query result sets. If the number of + # records in the set exceeds the threshold, a warning is logged. This can + # be used to identify queries which load thousands of records and + # potentially cause memory bloat. + mattr_accessor :warn_on_records_fetched_greater_than, instance_writer: false + self.warn_on_records_fetched_greater_than = nil + mattr_accessor :maintain_test_schema, instance_accessor: false mattr_accessor :belongs_to_required_by_default, instance_accessor: false @@ -293,18 +302,22 @@ module ActiveRecord assign_attributes(attributes) if attributes yield self if block_given? - _run_initialize_callbacks + run_callbacks :initialize end - # Initialize an empty model object from +coder+. +coder+ must contain - # the attributes necessary for initializing an empty model object. For - # example: + # Initialize an empty model object from +coder+. +coder+ should be + # the result of previously encoding an Active Record model, using + # `encode_with` # # class Post < ActiveRecord::Base # end # + # old_post = Post.new(title: "hello world") + # coder = {} + # old_post.encode_with(coder) + # # post = Post.allocate - # post.init_with('attributes' => { 'title' => 'hello world' }) + # post.init_with(coder) # post.title # => 'hello world' def init_with(coder) coder = LegacyYamlAdapter.convert(self.class, coder) @@ -316,8 +329,8 @@ module ActiveRecord self.class.define_attribute_methods - _run_find_callbacks - _run_initialize_callbacks + run_callbacks :find + run_callbacks :initialize self end @@ -353,7 +366,7 @@ module ActiveRecord @attributes = @attributes.dup @attributes.reset(self.class.primary_key) - _run_initialize_callbacks + run_callbacks(:initialize) @new_record = true @destroyed = false diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 6b26d7be78..af816a278e 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -31,9 +31,10 @@ module ActiveRecord end def sql(event) - self.class.runtime += event.duration return unless logger.debug? + self.class.runtime += event.duration + payload = event.payload return if IGNORE_PAYLOAD_NAMES.include?(payload[:name]) diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 75adcccce6..3674f672cb 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -328,12 +328,8 @@ module ActiveRecord @default_attributes = nil @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column @attributes_builder = nil - @column_names = nil - @attribute_types = nil @columns = nil @columns_hash = nil - @content_columns = nil - @default_attributes = nil @attribute_names = nil end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index a6176dffdb..a1e1073792 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -462,9 +462,10 @@ module ActiveRecord # ball = Ball.new # ball.touch(:updated_at) # => raises ActiveRecordError # - def touch(*names, time: current_time_from_proper_timezone) + def touch(*names, time: nil) raise ActiveRecordError, "cannot touch on a new record object" unless persisted? + time ||= current_time_from_proper_timezone attributes = timestamp_attributes_for_update_in_model attributes.concat(names) diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index f1bdbc845c..7e907beec0 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -102,6 +102,14 @@ module ActiveRecord end end + initializer "active_record.warn_on_records_fetched_greater_than" do + if config.active_record.warn_on_records_fetched_greater_than + ActiveSupport.on_load(:active_record) do + require 'active_record/relation/record_fetch_warning' + end + end + end + initializer "active_record.set_configs" do |app| ActiveSupport.on_load(:active_record) do app.config.active_record.each do |k,v| diff --git a/activerecord/lib/active_record/railties/controller_runtime.rb b/activerecord/lib/active_record/railties/controller_runtime.rb index af4840476c..8727e46cb3 100644 --- a/activerecord/lib/active_record/railties/controller_runtime.rb +++ b/activerecord/lib/active_record/railties/controller_runtime.rb @@ -19,7 +19,7 @@ module ActiveRecord end def cleanup_view_runtime - if ActiveRecord::Base.connected? + if logger.info? && ActiveRecord::Base.connected? db_rt_before_render = ActiveRecord::LogSubscriber.reset_runtime self.db_runtime = (db_runtime || 0) + db_rt_before_render runtime = super diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 8f16de3519..402b317d9c 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -130,9 +130,9 @@ module ActiveRecord # the plucked column names, if they can be deduced. Plucking an SQL fragment # returns String values by default. # - # Person.pluck(:id) - # # SELECT people.id FROM people - # # => [1, 2, 3] + # Person.pluck(:name) + # # SELECT people.name FROM people + # # => ['David', 'Jeremy', 'Jose'] # # Person.pluck(:id, :name) # # SELECT people.id, people.name FROM people @@ -150,6 +150,8 @@ module ActiveRecord # # SELECT DATEDIFF(updated_at, created_at) FROM people # # => ['0', '27761', '173'] # + # See also +ids+. + # def pluck(*column_names) column_names.map! do |column_name| if column_name.is_a?(Symbol) && attribute_alias?(column_name) diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 3f747a20b3..6a3a56f1cc 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -5,7 +5,7 @@ module ActiveRecord ONE_AS_ONE = '1 AS one' # Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]). - # If no record can be found for all of the listed ids, then RecordNotFound will be raised. If the primary key + # If one or more records can not be found for the requested ids, then RecordNotFound will be raised. If the primary key # is an integer, find by id coerces its arguments using +to_i+. # # Person.find(1) # returns the object for ID = 1 @@ -16,8 +16,6 @@ module ActiveRecord # Person.find([1]) # returns an array for the object with ID = 1 # Person.where("administrator = 1").order("created_on DESC").find(1) # - # <tt>ActiveRecord::RecordNotFound</tt> will be raised if one or more ids are not found. - # # NOTE: The returned records may not be in the same order as the ids you # provide since database rows are unordered. You'd need to provide an explicit <tt>order</tt> # option if you want the results are sorted. diff --git a/activerecord/lib/active_record/relation/record_fetch_warning.rb b/activerecord/lib/active_record/relation/record_fetch_warning.rb new file mode 100644 index 0000000000..14e1bf89fa --- /dev/null +++ b/activerecord/lib/active_record/relation/record_fetch_warning.rb @@ -0,0 +1,49 @@ +module ActiveRecord + class Relation + module RecordFetchWarning + # When this module is prepended to ActiveRecord::Relation and + # `config.active_record.warn_on_records_fetched_greater_than` is + # set to an integer, if the number of records a query returns is + # greater than the value of `warn_on_records_fetched_greater_than`, + # a warning is logged. This allows for the detection of queries that + # return a large number of records, which could cause memory bloat. + # + # In most cases, fetching large number of records can be performed + # efficiently using the ActiveRecord::Batches methods. + # See active_record/lib/relation/batches.rb for more information. + def exec_queries + QueryRegistry.reset + + super.tap do + if logger && warn_on_records_fetched_greater_than + if @records.length > warn_on_records_fetched_greater_than + logger.warn "Query fetched #{@records.size} #{@klass} records: #{QueryRegistry.queries.join(";")}" + end + end + end + end + + ActiveSupport::Notifications.subscribe("sql.active_record") do |*args| + payload = args.last + + QueryRegistry.queries << payload[:sql] + end + + class QueryRegistry # :nodoc: + extend ActiveSupport::PerThreadRegistry + + attr_accessor :queries + + def initialize + reset + end + + def reset + @queries = [] + end + end + end + end +end + +ActiveRecord::Relation.prepend ActiveRecord::Relation::RecordFetchWarning diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index da95920571..eaeaf0321b 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -105,7 +105,10 @@ HEADER end def table(table, stream) - columns = @connection.columns(table) + columns = @connection.columns(table).map do |column| + column.instance_variable_set(:@table_name, table) + column + end begin tbl = StringIO.new diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 5ec2c88b47..3590b8846e 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -35,7 +35,7 @@ module ActiveRecord # Are there attributes associated with this scope? def scope_attributes? # :nodoc: - super || default_scopes.any? + super || default_scopes.any? || respond_to?(:default_scope) end def before_remove_const #:nodoc: diff --git a/activerecord/lib/active_record/touch_later.rb b/activerecord/lib/active_record/touch_later.rb new file mode 100644 index 0000000000..4352a0ffea --- /dev/null +++ b/activerecord/lib/active_record/touch_later.rb @@ -0,0 +1,50 @@ +module ActiveRecord + # = Active Record Touch Later + module TouchLater + extend ActiveSupport::Concern + + included do + before_commit_without_transaction_enrollment :touch_deferred_attributes + end + + def touch_later(*names) # :nodoc: + raise ActiveRecordError, "cannot touch on a new record object" unless persisted? + + @_defer_touch_attrs ||= timestamp_attributes_for_update_in_model + @_defer_touch_attrs |= names + @_touch_time = current_time_from_proper_timezone + + surreptitiously_touch @_defer_touch_attrs + self.class.connection.add_transaction_record self + end + + def touch(*names, time: nil) # :nodoc: + if has_defer_touch_attrs? + names |= @_defer_touch_attrs + end + super(*names, time: time) + end + + private + def surreptitiously_touch(attrs) + attrs.each { |attr| write_attribute attr, @_touch_time } + clear_attribute_changes attrs + end + + def touch_deferred_attributes + if has_defer_touch_attrs? && persisted? + @_touching_delayed_records = true + touch(*@_defer_touch_attrs, time: @_touch_time) + @_touching_delayed_records, @_defer_touch_attrs, @_touch_time = nil, nil, nil + end + end + + def has_defer_touch_attrs? + defined?(@_defer_touch_attrs) && @_defer_touch_attrs.present? + end + + def touching_delayed_records? + defined?(@_touching_delayed_records) && @_touching_delayed_records + end + end +end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 2293d1b258..311dacb449 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -319,8 +319,8 @@ module ActiveRecord end def before_committed! # :nodoc: - _run_before_commit_without_transaction_enrollment_callbacks - _run_before_commit_callbacks + run_callbacks :before_commit_without_transaction_enrollment + run_callbacks :before_commit end # Call the +after_commit+ callbacks. @@ -329,8 +329,8 @@ module ActiveRecord # but call it after the commit of a destroyed object. def committed!(should_run_callbacks: true) #:nodoc: if should_run_callbacks && destroyed? || persisted? - _run_commit_without_transaction_enrollment_callbacks - _run_commit_callbacks + run_callbacks :commit_without_transaction_enrollment + run_callbacks :commit end ensure force_clear_transaction_record_state @@ -340,8 +340,8 @@ module ActiveRecord # state should be rolled back to the beginning or just to the last savepoint. def rolledback!(force_restore_state: false, should_run_callbacks: true) #:nodoc: if should_run_callbacks - _run_rollback_without_transaction_enrollment_callbacks - _run_rollback_callbacks + run_callbacks :rollback + run_callbacks :rollback_without_transaction_enrollment end ensure restore_transaction_record_state(force_restore_state) diff --git a/activerecord/lib/active_record/type/hash_lookup_type_map.rb b/activerecord/lib/active_record/type/hash_lookup_type_map.rb index 82d9327fc0..3b01e3f8ca 100644 --- a/activerecord/lib/active_record/type/hash_lookup_type_map.rb +++ b/activerecord/lib/active_record/type/hash_lookup_type_map.rb @@ -1,12 +1,18 @@ module ActiveRecord module Type class HashLookupTypeMap < TypeMap # :nodoc: - delegate :key?, to: :@mapping - def alias_type(type, alias_type) register_type(type) { |_, *args| lookup(alias_type, *args) } end + def key?(key) + @mapping.key?(key) + end + + def keys + @mapping.keys + end + private def perform_fetch(type, *args, &block) diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb index 75d5bd5a35..a9b791397b 100644 --- a/activerecord/lib/active_record/validations/presence.rb +++ b/activerecord/lib/active_record/validations/presence.rb @@ -43,6 +43,10 @@ module ActiveRecord # deletes the associated object, thus putting the parent object into an invalid # state. # + # NOTE: This validation will not fail while using it with an association + # if the latter was assigned but not valid. If you want to ensure that + # it is both present and valid, you also need to use +validates_associated+. + # # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "can't be blank"). # * <tt>:on</tt> - Specifies the contexts where this validation is active. diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 9be4b10a55..5106f4e127 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -76,6 +76,8 @@ module ActiveRecord klass.connection.case_sensitive_comparison(table, attribute, column, value) end klass.unscoped.where(comparison) + rescue RangeError + klass.none end def scope_relation(record, table, relation) diff --git a/activerecord/test/cases/adapters/mysql/charset_collation_test.rb b/activerecord/test/cases/adapters/mysql/charset_collation_test.rb new file mode 100644 index 0000000000..c8dd49d00a --- /dev/null +++ b/activerecord/test/cases/adapters/mysql/charset_collation_test.rb @@ -0,0 +1,54 @@ +require "cases/helper" +require 'support/schema_dumping_helper' + +class CharsetCollationTest < ActiveRecord::TestCase + include SchemaDumpingHelper + self.use_transactional_tests = false + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table :charset_collations, force: true do |t| + t.string :string_ascii_bin, charset: 'ascii', collation: 'ascii_bin' + t.text :text_ucs2_unicode_ci, charset: 'ucs2', collation: 'ucs2_unicode_ci' + end + end + + teardown do + @connection.drop_table :charset_collations, if_exists: true + end + + test "string column with charset and collation" do + column = @connection.columns(:charset_collations).find { |c| c.name == 'string_ascii_bin' } + assert_equal :string, column.type + assert_equal 'ascii_bin', column.collation + end + + test "text column with charset and collation" do + column = @connection.columns(:charset_collations).find { |c| c.name == 'text_ucs2_unicode_ci' } + assert_equal :text, column.type + assert_equal 'ucs2_unicode_ci', column.collation + end + + test "add column with charset and collation" do + @connection.add_column :charset_collations, :title, :string, charset: 'utf8', collation: 'utf8_bin' + + column = @connection.columns(:charset_collations).find { |c| c.name == 'title' } + assert_equal :string, column.type + assert_equal 'utf8_bin', column.collation + end + + test "change column with charset and collation" do + @connection.add_column :charset_collations, :description, :string, charset: 'utf8', collation: 'utf8_unicode_ci' + @connection.change_column :charset_collations, :description, :text, charset: 'utf8', collation: 'utf8_general_ci' + + column = @connection.columns(:charset_collations).find { |c| c.name == 'description' } + assert_equal :text, column.type + assert_equal 'utf8_general_ci', column.collation + end + + test "schema dump includes collation" do + output = dump_table_schema("charset_collations") + assert_match %r{t.string\s+"string_ascii_bin",\s+limit: 255,\s+collation: "ascii_bin"$}, output + assert_match %r{t.text\s+"text_ucs2_unicode_ci",\s+limit: 65535,\s+collation: "ucs2_unicode_ci"$}, output + end +end diff --git a/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb new file mode 100644 index 0000000000..c8dd49d00a --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb @@ -0,0 +1,54 @@ +require "cases/helper" +require 'support/schema_dumping_helper' + +class CharsetCollationTest < ActiveRecord::TestCase + include SchemaDumpingHelper + self.use_transactional_tests = false + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table :charset_collations, force: true do |t| + t.string :string_ascii_bin, charset: 'ascii', collation: 'ascii_bin' + t.text :text_ucs2_unicode_ci, charset: 'ucs2', collation: 'ucs2_unicode_ci' + end + end + + teardown do + @connection.drop_table :charset_collations, if_exists: true + end + + test "string column with charset and collation" do + column = @connection.columns(:charset_collations).find { |c| c.name == 'string_ascii_bin' } + assert_equal :string, column.type + assert_equal 'ascii_bin', column.collation + end + + test "text column with charset and collation" do + column = @connection.columns(:charset_collations).find { |c| c.name == 'text_ucs2_unicode_ci' } + assert_equal :text, column.type + assert_equal 'ucs2_unicode_ci', column.collation + end + + test "add column with charset and collation" do + @connection.add_column :charset_collations, :title, :string, charset: 'utf8', collation: 'utf8_bin' + + column = @connection.columns(:charset_collations).find { |c| c.name == 'title' } + assert_equal :string, column.type + assert_equal 'utf8_bin', column.collation + end + + test "change column with charset and collation" do + @connection.add_column :charset_collations, :description, :string, charset: 'utf8', collation: 'utf8_unicode_ci' + @connection.change_column :charset_collations, :description, :text, charset: 'utf8', collation: 'utf8_general_ci' + + column = @connection.columns(:charset_collations).find { |c| c.name == 'description' } + assert_equal :text, column.type + assert_equal 'utf8_general_ci', column.collation + end + + test "schema dump includes collation" do + output = dump_table_schema("charset_collations") + assert_match %r{t.string\s+"string_ascii_bin",\s+limit: 255,\s+collation: "ascii_bin"$}, output + assert_match %r{t.text\s+"text_ucs2_unicode_ci",\s+limit: 65535,\s+collation: "ucs2_unicode_ci"$}, output + end +end diff --git a/activerecord/test/cases/adapters/postgresql/numbers_test.rb b/activerecord/test/cases/adapters/postgresql/numbers_test.rb index 093b81fe8d..d8e01e3b89 100644 --- a/activerecord/test/cases/adapters/postgresql/numbers_test.rb +++ b/activerecord/test/cases/adapters/postgresql/numbers_test.rb @@ -31,7 +31,7 @@ class PostgresqlNumberTest < ActiveRecord::TestCase assert_equal 123456.789, first.double assert_equal(-::Float::INFINITY, second.single) assert_equal ::Float::INFINITY, second.double - assert_same ::Float::NAN, third.double + assert_send [third.double, :nan?] end def test_update diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index a934180a43..9a1b889d4d 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -68,7 +68,7 @@ module ActiveRecord def test_insert_sql_with_proprietary_returning_clause with_example_table do id = @connection.insert_sql("insert into ex (number) values(5150)", nil, "number") - assert_equal "5150", id + assert_equal 5150, id end end @@ -106,21 +106,21 @@ module ActiveRecord connection = connection_without_insert_returning id = connection.insert_sql("insert into postgresql_partitioned_table_parent (number) VALUES (1)") expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first - assert_equal expect, id + assert_equal expect.to_i, id end def test_exec_insert_with_returning_disabled connection = connection_without_insert_returning result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], 'id', 'postgresql_partitioned_table_parent_id_seq') expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first - assert_equal expect, result.rows.first.first + assert_equal expect.to_i, result.rows.first.first end def test_exec_insert_with_returning_disabled_and_no_sequence_name_given connection = connection_without_insert_returning result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], 'id') expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first - assert_equal expect, result.rows.first.first + assert_equal expect.to_i, result.rows.first.first end def test_sql_for_insert_with_returning_disabled @@ -238,7 +238,7 @@ module ActiveRecord result = @connection.exec_query('SELECT number FROM ex WHERE number = 10') assert_equal 1, result.rows.length - assert_equal "10", result.rows.last.last + assert_equal 10, result.rows.last.last end end @@ -274,7 +274,7 @@ module ActiveRecord assert_equal 1, result.rows.length assert_equal 2, result.columns.length - assert_equal [['1', 'foo']], result.rows + assert_equal [[1, 'foo']], result.rows end end @@ -288,7 +288,7 @@ module ActiveRecord assert_equal 1, result.rows.length assert_equal 2, result.columns.length - assert_equal [['1', 'foo']], result.rows + assert_equal [[1, 'foo']], result.rows end end @@ -304,7 +304,7 @@ module ActiveRecord assert_equal 1, result.rows.length assert_equal 2, result.columns.length - assert_equal [['1', 'foo']], result.rows + assert_equal [[1, 'foo']], result.rows end end diff --git a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb index d76e762815..7200ed2771 100644 --- a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb +++ b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb @@ -106,6 +106,6 @@ class PostgreSQLReferentialIntegrityTest < ActiveRecord::TestCase private def assert_transaction_is_not_broken - assert_equal "1", @connection.select_value("SELECT 1") + assert_equal 1, @connection.select_value("SELECT 1") end end diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index c1be340e7c..f925dcad97 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -384,16 +384,16 @@ class SchemaTest < ActiveRecord::TestCase def test_reset_pk_sequence sequence_name = "#{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}" @connection.execute "SELECT setval('#{sequence_name}', 123)" - assert_equal "124", @connection.select_value("SELECT nextval('#{sequence_name}')") + assert_equal 124, @connection.select_value("SELECT nextval('#{sequence_name}')") @connection.reset_pk_sequence!("#{SCHEMA_NAME}.#{UNMATCHED_PK_TABLE_NAME}") - assert_equal "1", @connection.select_value("SELECT nextval('#{sequence_name}')") + assert_equal 1, @connection.select_value("SELECT nextval('#{sequence_name}')") end def test_set_pk_sequence table_name = "#{SCHEMA_NAME}.#{PK_TABLE_NAME}" _, sequence_name = @connection.pk_and_sequence_for table_name @connection.set_pk_sequence! table_name, 123 - assert_equal "124", @connection.select_value("SELECT nextval('#{sequence_name}')") + assert_equal 124, @connection.select_value("SELECT nextval('#{sequence_name}')") @connection.reset_pk_sequence! table_name end diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 47fd7345c8..95d00ab3a9 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -422,6 +422,24 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_queries(1) { line_item.touch } end + def test_belongs_to_with_touch_on_multiple_records + line_item = LineItem.create!(amount: 1) + line_item2 = LineItem.create!(amount: 2) + Invoice.create!(line_items: [line_item, line_item2]) + + assert_queries(1) do + LineItem.transaction do + line_item.touch + line_item2.touch + end + end + + assert_queries(2) do + line_item.touch + line_item2.touch + end + end + def test_belongs_to_with_touch_option_on_touch_without_updated_at_attributes assert_not LineItem.column_names.include?("updated_at") diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 7d8b933992..0ecf2ddfd1 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -759,6 +759,23 @@ class EagerAssociationTest < ActiveRecord::TestCase end end + def test_eager_with_default_scope_as_class_method_using_find_method + david = developers(:david) + developer = EagerDeveloperWithClassMethodDefaultScope.find(david.id) + projects = Project.order(:id).to_a + assert_no_queries do + assert_equal(projects, developer.projects) + end + end + + def test_eager_with_default_scope_as_class_method_using_find_by_method + developer = EagerDeveloperWithClassMethodDefaultScope.find_by(name: 'David') + projects = Project.order(:id).to_a + assert_no_queries do + assert_equal(projects, developer.projects) + end + end + def test_eager_with_default_scope_as_lambda developer = EagerDeveloperWithLambdaDefaultScope.where(:name => 'David').first projects = Project.order(:id).to_a diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 290b2a0d6b..171cfbde44 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -1503,6 +1503,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_queries(0, ignore_none: true) do firm.clients = [] end + + assert_equal [], firm.send('clients=', []) end def test_transactions_when_replacing_on_persisted diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index 9e428098e4..0791dde1f2 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -190,8 +190,9 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_use_any_column_as_primary_key_when_start_is_not_specified assert_queries(Subscriber.count + 1) do - Subscriber.find_each(:batch_size => 1) do |subscriber| - assert_kind_of Subscriber, subscriber + Subscriber.find_in_batches(batch_size: 1) do |batch| + assert_kind_of Array, batch + assert_kind_of Subscriber, batch.first end end end diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb index 17ac72a109..87348d0f90 100644 --- a/activerecord/test/cases/migration/references_foreign_key_test.rb +++ b/activerecord/test/cases/migration/references_foreign_key_test.rb @@ -105,6 +105,28 @@ module ActiveRecord @connection.remove_reference :testings, :testing_parent, foreign_key: true end end + + test "foreign key methods respect pluralize_table_names" do + begin + original_pluralize_table_names = ActiveRecord::Base.pluralize_table_names + ActiveRecord::Base.pluralize_table_names = false + @connection.create_table :testing + @connection.change_table :testing_parents do |t| + t.references :testing, foreign_key: true + end + + fk = @connection.foreign_keys("testing_parents").first + assert_equal "testing_parents", fk.from_table + assert_equal "testing", fk.to_table + + assert_difference "@connection.foreign_keys('testing_parents').size", -1 do + @connection.remove_reference :testing_parents, :testing, foreign_key: true + end + ensure + ActiveRecord::Base.pluralize_table_names = original_pluralize_table_names + @connection.drop_table "testing", if_exists: true + end + end end end end diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 2370077eb0..1e93e2a05c 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -924,7 +924,8 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal instance.created_at, created_at assert_equal instance.updated_at, updated_at ensure - ActiveRecord::Base.connection.drop_table :widgets + ActiveRecord::Base.connection.drop_table widget.table_name + widget.reset_column_information end end end diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 3664a2af70..83be9a75d8 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -260,7 +260,8 @@ if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :Mysql2Adapter) end teardown do - @connection.drop_table 'widgets', if_exists: true + @connection.drop_table :widgets, if_exists: true + Widget.reset_column_information end test "primary key column type with bigserial" do diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 744f9edc47..2f0b5df286 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -184,7 +184,7 @@ class QueryCacheTest < ActiveRecord::TestCase # Oracle adapter returns count() as Fixnum or Float if current_adapter?(:OracleAdapter) assert_kind_of Numeric, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") - elsif current_adapter?(:SQLite3Adapter, :Mysql2Adapter) + elsif current_adapter?(:SQLite3Adapter, :Mysql2Adapter, :PostgreSQLAdapter) # Future versions of the sqlite3 adapter will return numeric assert_instance_of Fixnum, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") diff --git a/activerecord/test/cases/relation/record_fetch_warning_test.rb b/activerecord/test/cases/relation/record_fetch_warning_test.rb new file mode 100644 index 0000000000..62f0a7cc49 --- /dev/null +++ b/activerecord/test/cases/relation/record_fetch_warning_test.rb @@ -0,0 +1,28 @@ +require 'cases/helper' +require 'models/post' + +module ActiveRecord + class RecordFetchWarningTest < ActiveRecord::TestCase + fixtures :posts + + def test_warn_on_records_fetched_greater_than + original_logger = ActiveRecord::Base.logger + orginal_warn_on_records_fetched_greater_than = ActiveRecord::Base.warn_on_records_fetched_greater_than + + log = StringIO.new + ActiveRecord::Base.logger = ActiveSupport::Logger.new(log) + ActiveRecord::Base.logger.level = Logger::WARN + + require 'active_record/relation/record_fetch_warning' + + ActiveRecord::Base.warn_on_records_fetched_greater_than = 1 + + Post.all.to_a + + assert_match(/Query fetched/, log.string) + ensure + ActiveRecord::Base.logger = original_logger + ActiveRecord::Base.warn_on_records_fetched_greater_than = orginal_warn_on_records_fetched_greater_than + end + end +end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 6c099719c0..63612e33af 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -73,7 +73,7 @@ class SchemaDumperTest < ActiveRecord::TestCase next if column_set.empty? lengths = column_set.map do |column| - if match = column.match(/t\.(?:integer|decimal|float|datetime|timestamp|time|date|text|binary|string|boolean|xml|uuid|point)\s+"/) + if match = column.match(/\bt\.\w+\s+"/) match[0].length end end.compact diff --git a/activerecord/test/cases/touch_later_test.rb b/activerecord/test/cases/touch_later_test.rb new file mode 100644 index 0000000000..11804ff90b --- /dev/null +++ b/activerecord/test/cases/touch_later_test.rb @@ -0,0 +1,93 @@ +require 'cases/helper' +require 'models/invoice' +require 'models/line_item' +require 'models/topic' + +class TouchLaterTest < ActiveRecord::TestCase + + def test_touch_laster_raise_if_non_persisted + invoice = Invoice.new + Invoice.transaction do + refute invoice.persisted? + assert_raises(ActiveRecord::ActiveRecordError) do + invoice.touch_later + end + end + end + + def test_touch_later_dont_set_dirty_attributes + invoice = Invoice.create! + invoice.touch_later + refute invoice.changed? + end + + def test_touch_later_update_the_attributes + time = Time.now.utc - 25.days + topic = Topic.create!(updated_at: time, created_at: time) + assert_equal time.to_i, topic.updated_at.to_i + assert_equal time.to_i, topic.created_at.to_i + + Topic.transaction do + topic.touch_later(:created_at) + assert_not_equal time.to_i, topic.updated_at.to_i + assert_not_equal time.to_i, topic.created_at.to_i + + assert_equal time.to_i, topic.reload.updated_at.to_i + assert_equal time.to_i, topic.reload.created_at.to_i + end + assert_not_equal time.to_i, topic.reload.updated_at.to_i + assert_not_equal time.to_i, topic.reload.created_at.to_i + end + + def test_touch_touches_immediately + time = Time.now.utc - 25.days + topic = Topic.create!(updated_at: time, created_at: time) + assert_equal time.to_i, topic.updated_at.to_i + assert_equal time.to_i, topic.created_at.to_i + + Topic.transaction do + topic.touch_later(:created_at) + topic.touch + + assert_not_equal time, topic.reload.updated_at + assert_not_equal time, topic.reload.created_at + end + end + + def test_touch_later_an_association_dont_autosave_parent + time = Time.now.utc - 25.days + line_item = LineItem.create!(amount: 1) + invoice = Invoice.create!(line_items: [line_item]) + invoice.touch(time: time) + + Invoice.transaction do + line_item.update(amount: 2) + assert_equal time.to_i, invoice.reload.updated_at.to_i + end + + assert_not_equal time.to_i, invoice.updated_at.to_i + end + + def test_touch_touches_immediately_with_a_custom_time + time = Time.now.utc - 25.days + topic = Topic.create!(updated_at: time, created_at: time) + assert_equal time, topic.updated_at + assert_equal time, topic.created_at + + Topic.transaction do + topic.touch_later(:created_at) + time = Time.now.utc - 2.days + topic.touch(time: time) + + assert_equal time.to_i, topic.reload.updated_at.to_i + assert_equal time.to_i, topic.reload.created_at.to_i + end + end + + def test_touch_later_dont_hit_the_db + invoice = Invoice.create! + assert_queries(0) do + invoice.touch_later + end + end +end diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 062bc733f9..2608c84be2 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -34,7 +34,22 @@ class TopicWithUniqEvent < Topic validates :event, uniqueness: true end +class BigIntTest < ActiveRecord::Base + INT_MAX_VALUE = 2147483647 + self.table_name = 'cars' + validates :engines_count, uniqueness: true, inclusion: { in: 0..INT_MAX_VALUE } +end + +class BigIntReverseTest < ActiveRecord::Base + INT_MAX_VALUE = 2147483647 + self.table_name = 'cars' + validates :engines_count, inclusion: { in: 0..INT_MAX_VALUE } + validates :engines_count, uniqueness: true +end + class UniquenessValidationTest < ActiveRecord::TestCase + INT_MAX_VALUE = 2147483647 + fixtures :topics, 'warehouse-things' repair_validations(Topic, Reply) @@ -86,6 +101,16 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert t2.errors[:title] end + def test_validate_uniqueness_when_integer_out_of_range + entry = BigIntTest.create(engines_count: INT_MAX_VALUE + 1) + assert_equal entry.errors[:engines_count], ['is not included in the list'] + end + + def test_validate_uniqueness_when_integer_out_of_range_show_order_does_not_matter + entry = BigIntReverseTest.create(engines_count: INT_MAX_VALUE + 1) + assert_equal entry.errors[:engines_count], ['is not included in the list'] + end + def test_validates_uniqueness_with_newline_chars Topic.validates_uniqueness_of(:title, :case_sensitive => false) diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb index d6fd0c4ab0..52d3290c84 100644 --- a/activerecord/test/schema/mysql2_specific_schema.rb +++ b/activerecord/test/schema/mysql2_specific_schema.rb @@ -24,6 +24,11 @@ ActiveRecord::Schema.define do add_index :key_tests, :pizza, :using => :btree, :name => 'index_key_tests_on_pizza' add_index :key_tests, :snacks, :name => 'index_key_tests_on_snack' + create_table :collation_tests, id: false, force: true do |t| + t.string :string_cs_column, limit: 1, collation: 'utf8_bin' + t.string :string_ci_column, limit: 1, collation: 'utf8_general_ci' + end + ActiveRecord::Base.connection.execute <<-SQL DROP PROCEDURE IF EXISTS ten; SQL @@ -35,15 +40,6 @@ BEGIN END SQL - ActiveRecord::Base.connection.drop_table "collation_tests", if_exists: true - - ActiveRecord::Base.connection.execute <<-SQL -CREATE TABLE collation_tests ( - string_cs_column VARCHAR(1) COLLATE utf8_bin, - string_ci_column VARCHAR(1) COLLATE utf8_general_ci -) CHARACTER SET utf8 COLLATE utf8_general_ci -SQL - ActiveRecord::Base.connection.drop_table "enum_tests", if_exists: true ActiveRecord::Base.connection.execute <<-SQL diff --git a/activerecord/test/schema/mysql_specific_schema.rb b/activerecord/test/schema/mysql_specific_schema.rb index b5378341b5..90f5a60d7b 100644 --- a/activerecord/test/schema/mysql_specific_schema.rb +++ b/activerecord/test/schema/mysql_specific_schema.rb @@ -24,6 +24,11 @@ ActiveRecord::Schema.define do add_index :key_tests, :pizza, :using => :btree, :name => 'index_key_tests_on_pizza' add_index :key_tests, :snacks, :name => 'index_key_tests_on_snack' + create_table :collation_tests, id: false, force: true do |t| + t.string :string_cs_column, limit: 1, collation: 'utf8_bin' + t.string :string_ci_column, limit: 1, collation: 'utf8_general_ci' + end + ActiveRecord::Base.connection.execute <<-SQL DROP PROCEDURE IF EXISTS ten; SQL @@ -46,15 +51,6 @@ BEGIN END SQL - ActiveRecord::Base.connection.drop_table "collation_tests", if_exists: true - - ActiveRecord::Base.connection.execute <<-SQL -CREATE TABLE collation_tests ( - string_cs_column VARCHAR(1) COLLATE utf8_bin, - string_ci_column VARCHAR(1) COLLATE utf8_general_ci -) CHARACTER SET utf8 COLLATE utf8_general_ci -SQL - ActiveRecord::Base.connection.drop_table "enum_tests", if_exists: true ActiveRecord::Base.connection.execute <<-SQL diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 49088527ae..3ad2392365 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,28 @@ +* `ActiveSupport::Callbacks#skip_callback` now raises an `ArgumentError` if + an unrecognized callback is removed. + + *Iain Beeston* + +* Added `ActiveSupport::ArrayInquirer` and `Array#inquiry`. + + Wrapping an array in an `ArrayInquirer` gives a friendlier way to check its + contents: + + variants = ActiveSupport::ArrayInquirer.new([:phone, :tablet]) + + variants.phone? # => true + variants.tablet? # => true + variants.desktop? # => false + + variants.any?(:phone, :tablet) # => true + variants.any?(:phone, :desktop) # => true + variants.any?(:desktop, :watch) # => false + + `Array#inquiry` is a shortcut for wrapping the receiving array in an + `ArrayInquirer`. + + *George Claghorn* + * Deprecate `alias_method_chain` in favour of `Module#prepend` introduced in Ruby 2.0 *Kir Shatrov* diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 290920dbf8..588d6c49f9 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -59,6 +59,7 @@ module ActiveSupport autoload :StringInquirer autoload :TaggedLogging autoload :XmlMini + autoload :ArrayInquirer end autoload :Rescuable @@ -72,6 +73,14 @@ module ActiveSupport end cattr_accessor :test_order # :nodoc: + + def self.halt_callback_chains_on_return_false + Callbacks::CallbackChain.halt_and_display_warning_on_return_false + end + + def self.halt_callback_chains_on_return_false=(value) + Callbacks::CallbackChain.halt_and_display_warning_on_return_false = value + end end autoload :I18n, "active_support/i18n" diff --git a/activesupport/lib/active_support/array_inquirer.rb b/activesupport/lib/active_support/array_inquirer.rb new file mode 100644 index 0000000000..0ae534da00 --- /dev/null +++ b/activesupport/lib/active_support/array_inquirer.rb @@ -0,0 +1,38 @@ +module ActiveSupport + # Wrapping an array in an +ArrayInquirer+ gives a friendlier way to check + # its string-like contents: + # + # variants = ActiveSupport::ArrayInquirer.new([:phone, :tablet]) + # + # variants.phone? # => true + # variants.tablet? # => true + # variants.desktop? # => false + # + # variants.any?(:phone, :tablet) # => true + # variants.any?(:phone, :desktop) # => true + # variants.any?(:desktop, :watch) # => false + class ArrayInquirer < Array + def any?(*candidates, &block) + if candidates.none? + super + else + candidates.any? do |candidate| + include?(candidate) || include?(candidate.to_sym) + end + end + end + + private + def respond_to_missing?(name, include_private = false) + name[-1] == '?' + end + + def method_missing(name, *args) + if name[-1] == '?' + any?(name[0..-2]) + else + super + end + end + end +end diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index d08ecd2f7d..e6a8b84214 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -29,6 +29,7 @@ module ActiveSupport def clear(options = nil) root_dirs = Dir.entries(cache_path).reject {|f| (EXCLUDED_DIRS + [".gitkeep"]).include?(f)} FileUtils.rm_r(root_dirs.collect{|f| File.join(cache_path, f)}) + rescue Errno::ENOENT end # Preemptively iterates through all stored keys and removes the ones which have expired. diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index 37f9494272..814fd288cf 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -5,6 +5,7 @@ require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/kernel/reporting' require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/string/filters' +require 'active_support/deprecation' require 'thread' module ActiveSupport @@ -79,14 +80,10 @@ module ActiveSupport # save # end def run_callbacks(kind, &block) - send "_run_#{kind}_callbacks", &block - end - - private + callbacks = send("_#{kind}_callbacks") - def _run_callbacks(callbacks, &block) if callbacks.empty? - block.call if block + yield if block_given? else runner = callbacks.compile e = Filters::Environment.new(self, false, nil, block) @@ -94,6 +91,8 @@ module ActiveSupport end end + private + # A hook invoked every time a before callback is halted. # This can be overridden in AS::Callback implementors in order # to provide better debugging/logging. @@ -662,10 +661,12 @@ module ActiveSupport # # ===== Options # - # * <tt>:if</tt> - A symbol naming an instance method or a proc; the - # callback will be called only when it returns a +true+ value. - # * <tt>:unless</tt> - A symbol naming an instance method or a proc; the - # callback will be called only when it returns a +false+ value. + # * <tt>:if</tt> - A symbol, a string or an array of symbols and strings, + # each naming an instance method or a proc; the callback will be called + # only when they all return a true value. + # * <tt>:unless</tt> - A symbol, a string or an array of symbols and + # strings, each naming an instance method or a proc; the callback will + # be called only when they all return a false value. # * <tt>:prepend</tt> - If +true+, the callback will be prepended to the # existing chain rather than appended. def set_callback(name, *filter_list, &block) @@ -688,19 +689,27 @@ module ActiveSupport # class Writer < Person # skip_callback :validate, :before, :check_membership, if: -> { self.age > 18 } # end + # + # An <tt>ArgumentError</tt> will be raised if the callback has not + # already been set (unless the <tt>:raise</tt> option is set to <tt>false</tt>). def skip_callback(name, *filter_list, &block) type, filters, options = normalize_callback_params(filter_list, block) + options[:raise] = true unless options.key?(:raise) __update_callbacks(name) do |target, chain| filters.each do |filter| - filter = chain.find {|c| c.matches?(type, filter) } + callback = chain.find {|c| c.matches?(type, filter) } + + if !callback && options[:raise] + raise ArgumentError, "#{type.to_s.capitalize} #{name} callback #{filter.inspect} has not been defined" + end - if filter && options.any? - new_filter = filter.merge_conditional_options(chain, if_option: options[:if], unless_option: options[:unless]) - chain.insert(chain.index(filter), new_filter) + if callback && (options.key?(:if) || options.key?(:unless)) + new_callback = callback.merge_conditional_options(chain, if_option: options[:if], unless_option: options[:unless]) + chain.insert(chain.index(callback), new_callback) end - chain.delete(filter) + chain.delete(callback) end target.set_callbacks name, chain end @@ -797,12 +806,6 @@ module ActiveSupport names.each do |name| class_attribute "_#{name}_callbacks" set_callbacks name, CallbackChain.new(name, options) - - module_eval <<-RUBY, __FILE__, __LINE__ + 1 - def _run_#{name}_callbacks(&block) - _run_callbacks(_#{name}_callbacks, &block) - end - RUBY end end diff --git a/activesupport/lib/active_support/core_ext/array.rb b/activesupport/lib/active_support/core_ext/array.rb index 7d0c1e4c8d..7551551bd7 100644 --- a/activesupport/lib/active_support/core_ext/array.rb +++ b/activesupport/lib/active_support/core_ext/array.rb @@ -4,3 +4,4 @@ require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/array/grouping' require 'active_support/core_ext/array/prepend_and_append' +require 'active_support/core_ext/array/inquiry' diff --git a/activesupport/lib/active_support/core_ext/array/inquiry.rb b/activesupport/lib/active_support/core_ext/array/inquiry.rb new file mode 100644 index 0000000000..de623c466c --- /dev/null +++ b/activesupport/lib/active_support/core_ext/array/inquiry.rb @@ -0,0 +1,15 @@ +class Array + # Wraps the array in an +ArrayInquirer+ object, which gives a friendlier way + # to check its string-like contents. + # + # pets = [:cat, :dog].inquiry + # + # pets.cat? # => true + # pets.ferret? # => false + # + # pets.any?(:cat, :ferret) # => true + # pets.any?(:ferret, :alligator) # => false + def inquiry + ActiveSupport::ArrayInquirer.new(self) + end +end diff --git a/activesupport/lib/active_support/core_ext/module/aliasing.rb b/activesupport/lib/active_support/core_ext/module/aliasing.rb index 25e138264e..a4c40b25ff 100644 --- a/activesupport/lib/active_support/core_ext/module/aliasing.rb +++ b/activesupport/lib/active_support/core_ext/module/aliasing.rb @@ -45,7 +45,7 @@ class Module end # Allows you to make aliases for attributes, which includes - # getter, setter, and query methods. + # getter, setter, and a predicate. # # class Content < ActiveRecord::Base # # has a title attribute diff --git a/activesupport/lib/active_support/core_ext/string/filters.rb b/activesupport/lib/active_support/core_ext/string/filters.rb index 7461d03acc..375ec1aef8 100644 --- a/activesupport/lib/active_support/core_ext/string/filters.rb +++ b/activesupport/lib/active_support/core_ext/string/filters.rb @@ -17,9 +17,8 @@ class String # str.squish! # => "foo bar boo" # str # => "foo bar boo" def squish! - gsub!(/\A[[:space:]]+/, '') - gsub!(/[[:space:]]+\z/, '') gsub!(/[[:space:]]+/, ' ') + strip! self end diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb index 6f1b653639..1ce68ea7c7 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -246,8 +246,10 @@ class Time # Layers additional behavior on Time#<=> so that DateTime and ActiveSupport::TimeWithZone instances # can be chronologically compared with a Time def compare_with_coercion(other) - # we're avoiding Time#to_datetime cause it's expensive - if other.is_a?(Time) + # we're avoiding Time#to_datetime and Time#to_time because they're expensive + if other.class == Time + compare_without_coercion(other) + elsif other.is_a?(Time) compare_without_coercion(other.to_time) else to_datetime <=> other diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index fe8a2ac9ba..a08c655d69 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -73,7 +73,7 @@ module ActiveSupport string = string.sub(/^(?:#{inflections.acronym_regex}(?=\b|[A-Z_])|\w)/) { $&.downcase } end string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" } - string.gsub!(/\//, '::') + string.gsub!('/'.freeze, '::'.freeze) string end @@ -90,7 +90,7 @@ module ActiveSupport # camelize(underscore('SSLError')) # => "SslError" def underscore(camel_cased_word) return camel_cased_word unless camel_cased_word =~ /[A-Z-]|::/ - word = camel_cased_word.to_s.gsub(/::/, '/') + word = camel_cased_word.to_s.gsub('::'.freeze, '/'.freeze) word.gsub!(/(?:(?<=([A-Za-z\d]))|\b)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1 && '_'}#{$2.downcase}" } word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2') word.gsub!(/([a-z\d])([A-Z])/,'\1_\2') diff --git a/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb b/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb index ce03700de1..cd5a2b3cbb 100644 --- a/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb @@ -13,7 +13,7 @@ module ActiveSupport end rounded_number = NumberToRoundedConverter.convert(number, options) - format.gsub(/%n/, rounded_number).gsub(/%u/, options[:unit]) + format.gsub('%n'.freeze, rounded_number).gsub('%u'.freeze, options[:unit]) end private diff --git a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb index 6940beb318..5c6fe2df83 100644 --- a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb @@ -23,7 +23,7 @@ module ActiveSupport unit = determine_unit(units, exponent) rounded_number = NumberToRoundedConverter.convert(number, options) - format.gsub(/%n/, rounded_number).gsub(/%u/, unit).strip + format.gsub('%n'.freeze, rounded_number).gsub('%u'.freeze, unit).strip end private diff --git a/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb index 78d2c9ae6e..ac0d20b454 100644 --- a/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb @@ -20,7 +20,7 @@ module ActiveSupport human_size = number / (base ** exponent) number_to_format = NumberToRoundedConverter.convert(human_size, options) end - conversion_format.gsub(/%n/, number_to_format).gsub(/%u/, unit) + conversion_format.gsub('%n'.freeze, number_to_format).gsub('%u'.freeze, unit) end private diff --git a/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb b/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb index 1af294a03e..4c04d40c19 100644 --- a/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb @@ -5,7 +5,7 @@ module ActiveSupport def convert rounded_number = NumberToRoundedConverter.convert(number, options) - options[:format].gsub(/%n/, rounded_number) + options[:format].gsub('%n'.freeze, rounded_number) end end end diff --git a/activesupport/lib/active_support/railtie.rb b/activesupport/lib/active_support/railtie.rb index ef22433491..cd0fb51009 100644 --- a/activesupport/lib/active_support/railtie.rb +++ b/activesupport/lib/active_support/railtie.rb @@ -13,13 +13,6 @@ module ActiveSupport end end - initializer "active_support.halt_callback_chains_on_return_false", after: :load_config_initializers do |app| - if app.config.active_support.key? :halt_callback_chains_on_return_false - ActiveSupport::Callbacks::CallbackChain.halt_and_display_warning_on_return_false = \ - app.config.active_support.halt_callback_chains_on_return_false - end - end - # Sets the default value for Time.zone # If assigned value cannot be matched to a TimeZone, an exception will be raised. initializer "active_support.initialize_time_zone" do |app| diff --git a/activesupport/test/array_inquirer_test.rb b/activesupport/test/array_inquirer_test.rb new file mode 100644 index 0000000000..b25e5cca86 --- /dev/null +++ b/activesupport/test/array_inquirer_test.rb @@ -0,0 +1,36 @@ +require 'abstract_unit' +require 'active_support/core_ext/array' + +class ArrayInquirerTest < ActiveSupport::TestCase + def setup + @array_inquirer = ActiveSupport::ArrayInquirer.new([:mobile, :tablet]) + end + + def test_individual + assert @array_inquirer.mobile? + assert @array_inquirer.tablet? + assert_not @array_inquirer.desktop? + end + + def test_any + assert @array_inquirer.any?(:mobile, :desktop) + assert @array_inquirer.any?(:watch, :tablet) + assert_not @array_inquirer.any?(:desktop, :watch) + end + + def test_any_with_block + assert @array_inquirer.any? { |v| v == :mobile } + assert_not @array_inquirer.any? { |v| v == :desktop } + end + + def test_respond_to + assert_respond_to @array_inquirer, :development? + end + + def test_inquiry + result = [:mobile, :tablet].inquiry + + assert_instance_of ActiveSupport::ArrayInquirer, result + assert_equal @array_inquirer, result + end +end diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index 4953550c45..527538ed9a 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -684,6 +684,7 @@ class FileStoreTest < ActiveSupport::TestCase def teardown FileUtils.rm_r(cache_dir) + rescue Errno::ENOENT end def cache_dir @@ -703,6 +704,11 @@ class FileStoreTest < ActiveSupport::TestCase assert File.exist?(filepath) end + def test_clear_without_cache_dir + FileUtils.rm_r(cache_dir) + @cache.clear + end + def test_long_keys @cache.write("a"*10000, 1) assert_equal 1, @cache.read("a"*10000) diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb index f6abef8cee..cda9732cae 100644 --- a/activesupport/test/callbacks_test.rb +++ b/activesupport/test/callbacks_test.rb @@ -73,8 +73,8 @@ module CallbacksTest class PersonSkipper < Person skip_callback :save, :before, :before_save_method, :if => :yes - skip_callback :save, :after, :before_save_method, :unless => :yes - skip_callback :save, :after, :before_save_method, :if => :no + skip_callback :save, :after, :after_save_method, :unless => :yes + skip_callback :save, :after, :after_save_method, :if => :no skip_callback :save, :before, :before_save_method, :unless => :no skip_callback :save, :before, CallbackClass , :if => :yes def yes; true; end @@ -1021,7 +1021,7 @@ module CallbacksTest define_callbacks :foo n.times { set_callback :foo, :before, callback } def run; run_callbacks :foo; end - def self.skip(thing); skip_callback :foo, :before, thing; end + def self.skip(*things); skip_callback :foo, :before, *things; end } end @@ -1070,11 +1070,11 @@ module CallbacksTest } end - def test_skip_lambda # removes nothing + def test_skip_lambda # raises error calls = [] callback = ->(o) { calls << o } klass = build_class(callback) - 10.times { klass.skip callback } + assert_raises(ArgumentError) { klass.skip callback } klass.new.run assert_equal 10, calls.length end @@ -1088,11 +1088,29 @@ module CallbacksTest assert_equal 0, calls.length end - def test_skip_eval # removes nothing + def test_skip_string # raises error calls = [] klass = build_class("bar") klass.class_eval { define_method(:bar) { calls << klass } } - klass.skip "bar" + assert_raises(ArgumentError) { klass.skip "bar" } + klass.new.run + assert_equal 1, calls.length + end + + def test_skip_undefined_callback # raises error + calls = [] + klass = build_class(:bar) + klass.class_eval { define_method(:bar) { calls << klass } } + assert_raises(ArgumentError) { klass.skip :qux } + klass.new.run + assert_equal 1, calls.length + end + + def test_skip_without_raise # removes nothing + calls = [] + klass = build_class(:bar) + klass.class_eval { define_method(:bar) { calls << klass } } + klass.skip :qux, raise: false klass.new.run assert_equal 1, calls.length end diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index 702e26859a..4a1d90bfd6 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -160,7 +160,7 @@ class DependenciesTest < ActiveSupport::TestCase def test_ensures_the_expected_constant_is_defined with_autoloading_fixtures do e = assert_raise(LoadError) { Typo } - assert_match %r{Unable to autoload constant Typo, expected .*activesupport/test/autoloading_fixtures/typo.rb to define it}, e.message + assert_match %r{Unable to autoload constant Typo, expected .*/test/autoloading_fixtures/typo.rb to define it}, e.message end end @@ -178,7 +178,7 @@ class DependenciesTest < ActiveSupport::TestCase assert_equal 1, TypO e = assert_raise(LoadError) { Typo } - assert_match %r{Unable to autoload constant Typo, expected .*activesupport/test/autoloading_fixtures/typo.rb to define it}, e.message + assert_match %r{Unable to autoload constant Typo, expected .*/test/autoloading_fixtures/typo.rb to define it}, e.message end end diff --git a/guides/CHANGELOG.md b/guides/CHANGELOG.md index dd5ca4b395..fd177b4238 100644 --- a/guides/CHANGELOG.md +++ b/guides/CHANGELOG.md @@ -1,3 +1,7 @@ +* New section in Configuring: Configuring Active Job + + *Eliot Sykes* + * New section in Active Record Association Basics: Single Table Inheritance *Andrey Nering* diff --git a/guides/rails_guides/levenshtein.rb b/guides/rails_guides/levenshtein.rb index 36183fd321..049f633258 100644 --- a/guides/rails_guides/levenshtein.rb +++ b/guides/rails_guides/levenshtein.rb @@ -14,10 +14,13 @@ module RailsGuides d = (0..m).to_a x = nil - str1.each_char.each_with_index do |char1,i| + # avoid duplicating an enumerable object in the loop + str2_codepoint_enumerable = str2.each_codepoint + + str1.each_codepoint.with_index do |char1, i| e = i+1 - str2.each_char.each_with_index do |char2,j| + str2_codepoint_enumerable.with_index do |char2, j| cost = (char1 == char2) ? 0 : 1 x = [ d[j+1] + 1, # insertion diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index 65dbb070fa..fab0e20aba 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -671,7 +671,7 @@ Filters are methods that are run "before", "after" or "around" a controller acti Filters are inherited, so if you set a filter on `ApplicationController`, it will be run on every controller in your application. -"Before" filters may halt the request cycle. A common "before" filter is one which requires that a user is logged in for an action to be run. You can define the filter method this way: +"before" filters may halt the request cycle. A common "before" filter is one which requires that a user is logged in for an action to be run. You can define the filter method this way: ```ruby class ApplicationController < ActionController::Base @@ -704,9 +704,9 @@ Now, the `LoginsController`'s `new` and `create` actions will work as before wit In addition to "before" filters, you can also run filters after an action has been executed, or both before and after. -"After" filters are similar to "before" filters, but because the action has already been run they have access to the response data that's about to be sent to the client. Obviously, "after" filters cannot stop the action from running. +"after" filters are similar to "before" filters, but because the action has already been run they have access to the response data that's about to be sent to the client. Obviously, "after" filters cannot stop the action from running. -"Around" filters are responsible for running their associated actions by yielding, similar to how Rack middlewares work. +"around" filters are responsible for running their associated actions by yielding, similar to how Rack middlewares work. For example, in a website where changes have an approval workflow an administrator could be able to preview them easily, just apply them within a transaction: diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md index 71f3f8882c..44c02165db 100644 --- a/guides/source/action_view_overview.md +++ b/guides/source/action_view_overview.md @@ -356,7 +356,39 @@ Supposing we use the same `_box` partial from above, this would produce the same View Paths ---------- -TODO... +When rendering the view for a request, the controller needs to resolve where to find each of the directories are located. + +We are able to modify the order these locations are resolved by using `prepend_view_path` and `append_view_path`. + +This allows us to add new paths to the beginning or end of the list used to resolve these paths. + +### Prepend view path + +This can be helpful for example, when we want to prepend a different directory for subdomains. + +We can do this by using: + +```prepend_view_path "app/views/#{request.subdomain}"``` + +Then our list becomes something like: + +``` +[ + ~/rails_app/app/views/<subdomain>, + ~/rails_app/app/views, + # ... +] +``` + +This will put the subdomain path at the beginning of the list. + +### Append view path + +Similarly, we can append paths: + +```append_view_path "app/views/direct"```. + +This will add ```app/views/direct``` and the end of lookup paths for views. Overview of helpers provided by Action View ------------------------------------------- @@ -376,39 +408,13 @@ config.action_controller.asset_host = "assets.example.com" image_tag("rails.png") # => <img src="http://assets.example.com/images/rails.png" alt="Rails" /> ``` -#### register_javascript_expansion - -Register one or more JavaScript files to be included when symbol is passed to javascript_include_tag. This method is typically intended to be called from plugin initialization to register JavaScript files that the plugin installed in `vendor/assets/javascripts`. - -```ruby -ActionView::Helpers::AssetTagHelper.register_javascript_expansion monkey: ["head", "body", "tail"] - -javascript_include_tag :monkey # => - <script src="/assets/head.js"></script> - <script src="/assets/body.js"></script> - <script src="/assets/tail.js"></script> -``` - -#### register_stylesheet_expansion - -Register one or more stylesheet files to be included when symbol is passed to `stylesheet_link_tag`. This method is typically intended to be called from plugin initialization to register stylesheet files that the plugin installed in `vendor/assets/stylesheets`. - -```ruby -ActionView::Helpers::AssetTagHelper.register_stylesheet_expansion monkey: ["head", "body", "tail"] - -stylesheet_link_tag :monkey # => - <link href="/assets/head.css" media="screen" rel="stylesheet" /> - <link href="/assets/body.css" media="screen" rel="stylesheet" /> - <link href="/assets/tail.css" media="screen" rel="stylesheet" /> -``` - #### auto_discovery_link_tag Returns a link tag that browsers and feed readers can use to auto-detect an RSS or Atom feed. ```ruby auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", { title: "RSS Feed" }) # => - <link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://www.example.com/feed" /> + <link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://www.example.com/feed.rss" /> ``` #### image_path @@ -1361,22 +1367,6 @@ date_field_tag "dob" Provides functionality for working with JavaScript in your views. -#### button_to_function - -Returns a button that'll trigger a JavaScript function using the onclick handler. Examples: - -```ruby -button_to_function "Greeting", "alert('Hello world!')" -button_to_function "Delete", "if (confirm('Really?')) do_delete()" -button_to_function "Details" do |page| - page[:details].visual_effect :toggle_slide -end -``` - -#### define_javascript_functions - -Includes the Action Pack JavaScript libraries inside a single `script` tag. - #### escape_javascript Escape carrier returns and single and double quotes for JavaScript segments. @@ -1397,15 +1387,6 @@ alert('All is good') </script> ``` -#### link_to_function - -Returns a link that will trigger a JavaScript function using the onclick handler and return false after the fact. - -```ruby -link_to_function "Greeting", "alert('Hello world!')" -# => <a onclick="alert('Hello world!'); return false;" href="#">Greeting</a> -``` - ### NumberHelper Provides methods for converting numbers into formatted strings. Methods are provided for phone numbers, currency, percentage, precision, positional notation, and file size. diff --git a/guides/source/active_record_migrations.md b/guides/source/active_record_migrations.md index de8bbc4174..7a994cc5de 100644 --- a/guides/source/active_record_migrations.md +++ b/guides/source/active_record_migrations.md @@ -501,7 +501,7 @@ If the helpers provided by Active Record aren't enough you can use the `execute` method to execute arbitrary SQL: ```ruby -Product.connection.execute('UPDATE `products` SET `price`=`free` WHERE 1') +Product.connection.execute("UPDATE products SET price = 'free' WHERE 1=1") ``` For more details and examples of individual methods, check the API documentation. diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index 2820b641f1..de976acd01 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -655,7 +655,7 @@ GROUP BY status Having ------ -SQL uses the `HAVING` clause to specify conditions on the `GROUP BY` fields. You can add the `HAVING` clause to the SQL fired by the `Model.find` by adding the `:having` option to the find. +SQL uses the `HAVING` clause to specify conditions on the `GROUP BY` fields. You can add the `HAVING` clause to the SQL fired by the `Model.find` by adding the `having` method to the find. For example: @@ -1343,7 +1343,7 @@ Client.unscoped { Dynamic Finders --------------- -For every field (also known as an attribute) you define in your table, Active Record provides a finder method. If you have a field called `first_name` on your `Client` model for example, you get `find_by_first_name` for free from Active Record. If you have a `locked` field on the `Client` model, you also get `find_by_locked` and methods. +For every field (also known as an attribute) you define in your table, Active Record provides a finder method. If you have a field called `first_name` on your `Client` model for example, you get `find_by_first_name` for free from Active Record. If you have a `locked` field on the `Client` model, you also get `find_by_locked` method. You can specify an exclamation point (`!`) on the end of the dynamic finders to get them to raise an `ActiveRecord::RecordNotFound` error if they do not return any records, like `Client.find_by_name!("Ryan")` diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md index f3d8e05089..ff60f95a2c 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -475,7 +475,7 @@ The methods `silence_warnings` and `enable_warnings` change the value of `$VERBO silence_warnings { Object.const_set "RAILS_DEFAULT_LOGGER", logger } ``` -Silencing exceptions is also possible with `suppress`. This method receives an arbitrary number of exception classes. If an exception is raised during the execution of the block and is `kind_of?` any of the arguments, `suppress` captures it and returns silently. Otherwise the exception is reraised: +Silencing exceptions is also possible with `suppress`. This method receives an arbitrary number of exception classes. If an exception is raised during the execution of the block and is `kind_of?` any of the arguments, `suppress` captures it and returns silently. Otherwise the exception is not captured: ```ruby # If the user is locked, the increment is lost, no big deal. @@ -3040,53 +3040,6 @@ The method `Range#overlaps?` says whether any two given ranges have non-void int NOTE: Defined in `active_support/core_ext/range/overlaps.rb`. -Extensions to `Proc` --------------------- - -### `bind` - -As you surely know Ruby has an `UnboundMethod` class whose instances are methods that belong to the limbo of methods without a self. The method `Module#instance_method` returns an unbound method for example: - -```ruby -Hash.instance_method(:delete) # => #<UnboundMethod: Hash#delete> -``` - -An unbound method is not callable as is, you need to bind it first to an object with `bind`: - -```ruby -clear = Hash.instance_method(:clear) -clear.bind({a: 1}).call # => {} -``` - -Active Support defines `Proc#bind` with an analogous purpose: - -```ruby -Proc.new { size }.bind([]).call # => 0 -``` - -As you see that's callable and bound to the argument, the return value is indeed a `Method`. - -NOTE: To do so `Proc#bind` actually creates a method under the hood. If you ever see a method with a weird name like `__bind_1256598120_237302` in a stack trace you know now where it comes from. - -Action Pack uses this trick in `rescue_from` for example, which accepts the name of a method and also a proc as callbacks for a given rescued exception. It has to call them in either case, so a bound method is returned by `handler_for_rescue`, thus simplifying the code in the caller: - -```ruby -def handler_for_rescue(exception) - _, rescuer = Array(rescue_handlers).reverse.detect do |klass_name, handler| - ... - end - - case rescuer - when Symbol - method(rescuer) - when Proc - rescuer.bind(self) - end -end -``` - -NOTE: Defined in `active_support/core_ext/proc.rb`. - Extensions to `Date` -------------------- diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md index 352da43b5f..1b14bedfbf 100644 --- a/guides/source/active_support_instrumentation.md +++ b/guides/source/active_support_instrumentation.md @@ -19,7 +19,7 @@ After reading this guide, you will know: Introduction to instrumentation ------------------------------- -The instrumentation API provided by Active Support allows developers to provide hooks which other developers may hook into. There are several of these within the Rails framework, as described below in (TODO: link to section detailing each hook point). With this API, developers can choose to be notified when certain events occur inside their application or another piece of Ruby code. +The instrumentation API provided by Active Support allows developers to provide hooks which other developers may hook into. There are several of these within the [Rails framework](#rails-framework-hooks). With this API, developers can choose to be notified when certain events occur inside their application or another piece of Ruby code. For example, there is a hook provided within Active Record that is called every time Active Record uses an SQL query on a database. This hook could be **subscribed** to, and used to track the number of queries during a certain action. There's another hook around the processing of an action of a controller. This could be used, for instance, to track how long a specific action has taken. diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index ec6017ff73..abac54d22d 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -146,6 +146,17 @@ class CreateSuppliers < ActiveRecord::Migration end ``` +Depending on the use case, you might also need to create a unique index and/or +a foreign key constraint on the supplier column for the accounts table. In this +case, the column definition might look like this: + +```ruby +create_table :accounts do |t| + t.belongs_to :supplier, index: true, unique: true, foreign_key: true + # ... +end +``` + ### The `has_many` Association A `has_many` association indicates a one-to-many connection with another model. You'll often find this association on the "other side" of a `belongs_to` association. This association indicates that each instance of the model has zero or more instances of another model. For example, in an application containing customers and orders, the customer model could be declared like this: @@ -829,6 +840,7 @@ The `belongs_to` association supports these options: * `:counter_cache` * `:dependent` * `:foreign_key` +* `:primary_key` * `:inverse_of` * `:polymorphic` * `:touch` @@ -875,18 +887,26 @@ end With this declaration, Rails will keep the cache value up to date, and then return that value in response to the `size` method. -Although the `:counter_cache` option is specified on the model that includes the `belongs_to` declaration, the actual column must be added to the _associated_ model. In the case above, you would need to add a column named `orders_count` to the `Customer` model. You can override the default column name if you need to: +Although the `:counter_cache` option is specified on the model that includes +the `belongs_to` declaration, the actual column must be added to the +_associated_ (`has_many`) model. In the case above, you would need to add a +column named `orders_count` to the `Customer` model. + +You can override the default column name by specifying a custom column name in +the `counter_cache` declaration instead of `true`. For example, to use +`count_of_orders` instead of `orders_count`: ```ruby class Order < ActiveRecord::Base belongs_to :customer, counter_cache: :count_of_orders end class Customer < ActiveRecord::Base - has_many :orders, counter_cache: :count_of_orders + has_many :orders end ``` -NOTE: You only need to specify the :counter_cache option on the "has_many side" of the association when using a custom name for the counter cache. +NOTE: You only need to specify the :counter_cache option on the `belongs_to` +side of the association. Counter cache columns are added to the containing model's list of read-only attributes through `attr_readonly`. @@ -913,6 +933,26 @@ end TIP: In any case, Rails will not create foreign key columns for you. You need to explicitly define them as part of your migrations. +##### `:primary_key` + +By convention, Rails assumes that the `id` column is used to hold the primary key +of its tables. The `:primary_key` option allows you to specify a different column. + +For example, given we have a `users` table with `guid` as the primary key. If we want a separate `todos` table to hold the foreign key `user_id` in the `guid` column, then we can use `primary_key` to achieve this like so: + +```ruby +class User < ActiveRecord::Base + self.primary_key = 'guid' # primary key is guid and not id +end + +class Todo < ActiveRecord::Base + belongs_to :user, primary_key: 'guid' +end +``` + +When we execute `@user.todos.create` then the `@todo` record will have its +`user_id` value as the `guid` value of `@user`. + ##### `:inverse_of` The `:inverse_of` option specifies the name of the `has_many` or `has_one` association that is the inverse of this association. Does not work in combination with the `:polymorphic` options. diff --git a/guides/source/autoloading_and_reloading_constants.md b/guides/source/autoloading_and_reloading_constants.md index c6149abcba..2b6d7e4044 100644 --- a/guides/source/autoloading_and_reloading_constants.md +++ b/guides/source/autoloading_and_reloading_constants.md @@ -466,9 +466,7 @@ by adding this to `config/application.rb`: config.autoload_paths << "#{Rails.root}/lib" ``` -`config.autoload_paths` is accessible from environment-specific configuration -files, but any changes made to it outside `config/application.rb` don't have any -effect. +`config.autoload_paths` is not changeable from environment-specific configuration files. The value of `autoload_paths` can be inspected. In a just generated application it is (edited): diff --git a/guides/source/command_line.md b/guides/source/command_line.md index b409f20122..3bd84b1ce6 100644 --- a/guides/source/command_line.md +++ b/guides/source/command_line.md @@ -470,7 +470,7 @@ app/models/article.rb: NOTE. When using specific annotations and custom annotations, the annotation name (FIXME, BUG etc) is not displayed in the output lines. -By default, `rake notes` will look in the `app`, `config`, `lib`, `bin` and `test` directories. If you would like to search other directories, you can provide them as a comma separated list in an environment variable `SOURCE_ANNOTATION_DIRECTORIES`. +By default, `rake notes` will look in the `app`, `config`, `db`, `lib` and `test` directories. If you would like to search other directories, you can provide them as a comma separated list in an environment variable `SOURCE_ANNOTATION_DIRECTORIES`. ```bash $ export SOURCE_ANNOTATION_DIRECTORIES='spec,vendor' diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 67285030a9..43ddcf0767 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -309,6 +309,11 @@ All these configuration options are delegated to the `I18n` library. * `config.active_record.belongs_to_required_by_default` is a boolean value and controls whether `belongs_to` association is required by default. +* `config.active_record.warn_on_records_fetched_greater_than` allows setting a + warning threshold for query result size. If the number of records returned + by a query exceeds the threshold, a warning is logged. This can be used to + identify queries which might be causing memory bloat. + The MySQL adapter adds one additional configuration option: * `ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans` controls whether Active Record will consider all `tinyint(1)` columns in a MySQL database to be booleans and is true by default. @@ -528,6 +533,58 @@ There are a few configuration options available in Active Support: * `ActiveSupport::Deprecation.silenced` sets whether or not to display deprecation warnings. +### Configuring Active Job + +`config.active_job` provides the following configuration options: + +* `config.active_job.queue_adapter` sets the adapter for the queueing backend. The default adapter is `:inline` which will perform jobs immediately. For an up-to-date list of built-in adapters see the [ActiveJob::QueueAdapters API documentation](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html). + + ```ruby + # Be sure to have the adapter's gem in your Gemfile + # and follow the adapter's specific installation + # and deployment instructions. + config.active_job.queue_adapter = :sidekiq + ``` + +* `config.active_job.default_queue_name` can be used to change the default queue name. By default this is `"default"`. + + ```ruby + config.active_job.default_queue_name = :medium_priority + ``` + +* `config.active_job.queue_name_prefix` allows you to set an optional, non-blank, queue name prefix for all jobs. By default it is blank and not used. + + The following configuration would queue the given job on the `production_high_priority` queue when run in production: + + ```ruby + config.active_job.queue_name_prefix = Rails.env + ``` + + ```ruby + class GuestsCleanupJob < ActiveJob::Base + queue_as :high_priority + #.... + end + ``` + +* `config.active_job.queue_name_delimiter` has a default value of `'_'`. If `queue_name_prefix` is set, then `queue_name_delimiter` joins the prefix and the non-prefixed queue name. + + The following configuration would queue the provided job on the `video_server.low_priority` queue: + + ```ruby + # prefix must be set for delimiter to be used + config.active_job.queue_name_prefix = 'video_server' + config.active_job.queue_name_delimiter = '.' + ``` + + ```ruby + class EncoderJob < ActiveJob::Base + queue_as :low_priority + #.... + end + ``` + +* `config.active_job.logger` accepts a logger conforming to the interface of Log4r or the default Ruby Logger class, which is then used to log information from Active Job. You can retrieve this logger by calling `logger` on either an Active Job class or an Active Job instance. Set to `nil` to disable logging. ### Configuring a Database @@ -826,15 +883,6 @@ server { Be sure to read the [NGINX documentation](http://nginx.org/en/docs/) for the most up-to-date information. -#### Considerations when deploying to a subdirectory - -Deploying to a subdirectory in production has implications on various parts of -Rails. - -* development environment: -* testing environment: -* serving static assets: -* asset pipeline: Rails Environment Settings -------------------------- @@ -964,6 +1012,10 @@ Below is a comprehensive list of all the initializers found in Rails in the orde * `active_record.set_dispatch_hooks` Resets all reloadable connections to the database if `config.cache_classes` is set to `false`. +* `active_job.logger` Sets `ActiveJob::Base.logger` - if it's not already set - to `Rails.logger` + +* `active_job.set_configs` Sets up Active Job by using the settings in `config.active_job` by `send`'ing the method names as setters to `ActiveJob::Base` and passing the values through. + * `action_mailer.logger` Sets `ActionMailer::Base.logger` - if it's not already set - to `Rails.logger`. * `action_mailer.set_configs` Sets up Action Mailer by using the settings in `config.action_mailer` by `send`'ing the method names as setters to `ActionMailer::Base` and passing the values through. diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md index 018100c316..618b6c3799 100644 --- a/guides/source/contributing_to_ruby_on_rails.md +++ b/guides/source/contributing_to_ruby_on_rails.md @@ -355,9 +355,9 @@ $ RUBYOPT=-W0 bundle exec rake test The CHANGELOG is an important part of every release. It keeps the list of changes for every Rails version. -You should add an entry to the CHANGELOG of the framework that you modified if you're adding or removing a feature, committing a bug fix or adding deprecation notices. Refactorings and documentation changes generally should not go to the CHANGELOG. +You should add an entry **to the top** of the CHANGELOG of the framework that you modified if you're adding or removing a feature, committing a bug fix or adding deprecation notices. Refactorings and documentation changes generally should not go to the CHANGELOG. -A CHANGELOG entry should summarize what was changed and should end with author's name and it should go on top of a CHANGELOG. You can use multiple lines if you need more space and you can attach code examples indented with 4 spaces. If a change is related to a specific issue, you should attach the issue's number. Here is an example CHANGELOG entry: +A CHANGELOG entry should summarize what was changed and should end with the author's name. You can use multiple lines if you need more space and you can attach code examples indented with 4 spaces. If a change is related to a specific issue, you should attach the issue's number. Here is an example CHANGELOG entry: ``` * Summary of a change that briefly describes what was changed. You can use multiple diff --git a/guides/source/engines.md b/guides/source/engines.md index b6775cae4d..bcb0ee7d5d 100644 --- a/guides/source/engines.md +++ b/guides/source/engines.md @@ -402,15 +402,6 @@ Finally, the assets for this resource are generated in two files: `app/assets/stylesheets/blorgh/articles.css`. You'll see how to use these a little later. -By default, the scaffold styling is not applied to the engine because the -engine's layout file, `app/views/layouts/blorgh/application.html.erb`, doesn't -load it. To make the scaffold styling apply, insert this line into the `<head>` -tag of this layout: - -```erb -<%= stylesheet_link_tag "scaffold" %> -``` - You can see what the engine has so far by running `rake db:migrate` at the root of our engine to run the migration generated by the scaffold generator, and then running `rails server` in `test/dummy`. When you open diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index 922ec3922e..db4e81e32e 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -1510,7 +1510,7 @@ comments on articles. We're going to see the same generator that we used before when creating the `Article` model. This time we'll create a `Comment` model to hold -reference of article comments. Run this command in your terminal: +reference to an article. Run this command in your terminal: ```bash $ bin/rails generate model Comment commenter:string body:text article:references diff --git a/guides/source/i18n.md b/guides/source/i18n.md index e8d0a83dd0..27f11ebbee 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -201,7 +201,7 @@ end If your application includes a locale switching menu, you would then have something like this in it: ```ruby -link_to("Deutsch", "#{APP_CONFIG[:deutsch_website_url]}#{request.env['REQUEST_URI']}") +link_to("Deutsch", "#{APP_CONFIG[:deutsch_website_url]}#{request.env['PATH_INFO']}") ``` assuming you would set `APP_CONFIG[:deutsch_website_url]` to some value like `http://www.application.de`. diff --git a/guides/source/initialization.md b/guides/source/initialization.md index 8fbb234698..199545a3b3 100644 --- a/guides/source/initialization.md +++ b/guides/source/initialization.md @@ -34,7 +34,7 @@ Launch! Let's start to boot and initialize the app. A Rails application is usually started by running `rails console` or `rails server`. -### `railties/bin/rails` +### `railties/exe/rails` The `rails` in the command `rails server` is a ruby executable in your load path. This executable contains the following lines: @@ -45,7 +45,7 @@ load Gem.bin_path('railties', 'rails', version) ``` If you try out this command in a Rails console, you would see that this loads -`railties/bin/rails`. A part of the file `railties/bin/rails.rb` has the +`railties/exe/rails`. A part of the file `railties/exe/rails.rb` has the following code: ```ruby @@ -163,7 +163,7 @@ throwing an error message. If the command is valid, a method of the same name is called. ```ruby -COMMAND_WHITELIST = %(plugin generate destroy console server dbconsole application runner new version help) +COMMAND_WHITELIST = %w(plugin generate destroy console server dbconsole application runner new version help) def run_command!(command) command = parse_command(command) diff --git a/guides/source/routing.md b/guides/source/routing.md index 7d0a3efbe7..4ccc50a4d9 100644 --- a/guides/source/routing.md +++ b/guides/source/routing.md @@ -807,6 +807,18 @@ As long as `Sprockets` responds to `call` and returns a `[status, headers, body] NOTE: For the curious, `'articles#index'` actually expands out to `ArticlesController.action(:index)`, which returns a valid Rack application. +If you specify a rack application as the endpoint for a matcher remember that the route will be unchanged in the receiving application. With the following route your rack application should expect the route to be '/admin': + +```ruby +match '/admin', to: AdminApp, via: :all +``` + +If you would prefer to have your rack application receive requests at the root path instead use mount: + +```ruby +mount AdminApp, at: '/admin' +``` + ### Using `root` You can specify what Rails should route `'/'` to with the `root` method: diff --git a/guides/source/security.md b/guides/source/security.md index e486edde31..184af98d65 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -710,7 +710,7 @@ The log files on www.attacker.com will read like this: GET http://www.attacker.com/_app_session=836c1c25278e5b321d6bea4f19cb57e2 ``` -You can mitigate these attacks (in the obvious way) by adding the [httpOnly](http://dev.rubyonrails.org/ticket/8895) flag to cookies, so that document.cookie may not be read by JavaScript. Http only cookies can be used from IE v6.SP1, Firefox v2.0.0.5 and Opera 9.5. Safari is still considering, it ignores the option. But other, older browsers (such as WebTV and IE 5.5 on Mac) can actually cause the page to fail to load. Be warned that cookies [will still be visible using Ajax](http://ha.ckers.org/blog/20070719/firefox-implements-httponly-and-is-vulnerable-to-xmlhttprequest/), though. +You can mitigate these attacks (in the obvious way) by adding the **httpOnly** flag to cookies, so that document.cookie may not be read by JavaScript. Http only cookies can be used from IE v6.SP1, Firefox v2.0.0.5 and Opera 9.5. Safari is still considering, it ignores the option. But other, older browsers (such as WebTV and IE 5.5 on Mac) can actually cause the page to fail to load. Be warned that cookies [will still be visible using Ajax](http://ha.ckers.org/blog/20070719/firefox-implements-httponly-and-is-vulnerable-to-xmlhttprequest/), though. ##### Defacement diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 05c46a9e76..7666601bd7 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -75,6 +75,23 @@ warning by adding the following configuration to your `config/application.rb`: See [#17227](https://github.com/rails/rails/pull/17227) for more details. +### ActiveJob jobs now inherent from ApplicationJob by default + +In Rails 4.2 an ActiveJob inherits from `ActiveJob::Base`. In Rails 5.0 this +behavior has changed to now inherit from `ApplicationJob`. + +When upgrading from Rails 4.2 to Rails 5.0 you need to create an +`application_job.rb` file in `app/jobs/` and add the following content: + +``` +class ApplicationJob < ActiveJob::Base +end +``` + +Then make sure that all your job classes inherit from it. + +See [#19034](https://github.com/rails/rails/pull/19034) for more details. + Upgrading from Rails 4.1 to Rails 4.2 ------------------------------------- diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 0e9ef966ea..da11f337ad 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,20 @@ +* Rename `railties/bin` to `railties/exe` to match the new Bundler executables convention. + + *Islam Wazery* + +* Print `bundle install` output in `rails new` as soon as it's available + + Running `rails new` will now print the output of `bundle install` as + it is available, instead of waiting until all gems finish installing. + + *Max Holder* + +* Respect `pluralize_table_names` when generating fixture file. + + Fixes #19519. + + *Yuji Yaginuma* + * Add a new-line to the end of route method generated code. We need to add a `\n`, because we cannot have two routes diff --git a/railties/bin/rails b/railties/exe/rails index 82c17cabce..82c17cabce 100755 --- a/railties/bin/rails +++ b/railties/exe/rails diff --git a/railties/lib/rails/app_rails_loader.rb b/railties/lib/rails/app_rails_loader.rb index 39d8007333..9a7c6c5f2d 100644 --- a/railties/lib/rails/app_rails_loader.rb +++ b/railties/lib/rails/app_rails_loader.rb @@ -1,4 +1,5 @@ require 'pathname' +require 'rails/version' module Rails module AppRailsLoader @@ -9,7 +10,7 @@ module Rails BUNDLER_WARNING = <<EOS Looks like your app's ./bin/rails is a stub that was generated by Bundler. -In Rails 4, your app's bin/ directory contains executables that are versioned +In Rails #{Rails::VERSION::MAJOR}, your app's bin/ directory contains executables that are versioned like any other source code, rather than stubs that are generated on demand. Here's how to upgrade: diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index e9683d4a95..3e3ef72742 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -511,7 +511,7 @@ module Rails "Read the upgrade documentation to learn more about this new config option." if secrets.secret_token.blank? - raise "Missing `secret_token` and `secret_key_base` for '#{Rails.env}' environment, set these values in `config/secrets.yml`" + raise "Missing `secret_key_base` for '#{Rails.env}' environment, set this value in `config/secrets.yml`" end end end diff --git a/railties/lib/rails/code_statistics.rb b/railties/lib/rails/code_statistics.rb index 583d005d46..fd352dc9b7 100644 --- a/railties/lib/rails/code_statistics.rb +++ b/railties/lib/rails/code_statistics.rb @@ -41,11 +41,9 @@ class CodeStatistics #:nodoc: if File.directory?(path) && (/^\./ !~ file_name) stats.add(calculate_directory_statistics(path, pattern)) + elsif file_name =~ pattern + stats.add_by_file_path(path) end - - next unless file_name =~ pattern - - stats.add_by_file_path(path) end stats diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index 341291f08b..a7da92168d 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -267,10 +267,13 @@ module Rails d = (0..m).to_a x = nil - str1.each_char.each_with_index do |char1,i| + # avoid duplicating an enumerable object in the loop + str2_codepoint_enumerable = str2.each_codepoint + + str1.each_codepoint.with_index do |char1, i| e = i+1 - str2.each_char.each_with_index do |char2,j| + str2_codepoint_enumerable.with_index do |char2, j| cost = (char1 == char2) ? 0 : 1 x = [ d[j+1] + 1, # insertion diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 813f8b21fd..10deeb0ba2 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -315,10 +315,6 @@ module Rails # its own vendored Thor, which could be a different version. Running both # things in the same process is a recipe for a night with paracetamol. # - # We use backticks and #print here instead of vanilla #system because it - # is easier to silence stdout in the existing test suite this way. The - # end-user gets the bundler commands called anyway, so no big deal. - # # We unset temporary bundler variables to load proper bundler and Gemfile. # # Thanks to James Tucker for the Gem tricks involved in this call. @@ -326,8 +322,12 @@ module Rails require 'bundler' Bundler.with_clean_env do - output = `"#{Gem.ruby}" "#{_bundle_command}" #{command}` - print output unless options[:quiet] + full_command = %Q["#{Gem.ruby}" "#{_bundle_command}" #{command}] + if options[:quiet] + system(full_command, out: File::NULL) + else + system(full_command) + end end end diff --git a/railties/lib/rails/generators/named_base.rb b/railties/lib/rails/generators/named_base.rb index 36456e64f5..01a8e2e9b4 100644 --- a/railties/lib/rails/generators/named_base.rb +++ b/railties/lib/rails/generators/named_base.rb @@ -141,6 +141,10 @@ module Rails @plural_file_name ||= file_name.pluralize end + def fixture_file_name + @fixture_file_name ||= (pluralize_table_names? ? plural_file_name : file_name) + end + def route_url @route_url ||= class_path.collect {|dname| "/" + dname }.join + "/" + plural_file_name end diff --git a/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb b/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb new file mode 100644 index 0000000000..a009ace51c --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb @@ -0,0 +1,2 @@ +class ApplicationJob < ActiveJob::Base +end diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/callback_terminator.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/callback_terminator.rb index e63022da91..a70a1b9cde 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/callback_terminator.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/callback_terminator.rb @@ -1,4 +1,4 @@ # Be sure to restart your server when you modify this file. # Do not halt callback chains when a callback returns false. -Rails.application.config.active_support.halt_callback_chains_on_return_false = false +ActiveSupport.halt_callback_chains_on_return_false = false diff --git a/railties/lib/rails/generators/test_unit/model/model_generator.rb b/railties/lib/rails/generators/test_unit/model/model_generator.rb index 2826a3ffa1..086588750e 100644 --- a/railties/lib/rails/generators/test_unit/model/model_generator.rb +++ b/railties/lib/rails/generators/test_unit/model/model_generator.rb @@ -19,7 +19,7 @@ module TestUnit # :nodoc: def create_fixture_file if options[:fixture] && options[:fixture_replacement].nil? - template 'fixtures.yml', File.join('test/fixtures', class_path, "#{plural_file_name}.yml") + template 'fixtures.yml', File.join('test/fixtures', class_path, "#{fixture_file_name}.yml") end end diff --git a/railties/railties.gemspec b/railties/railties.gemspec index 001882fdc6..7cb1f1a49c 100644 --- a/railties/railties.gemspec +++ b/railties/railties.gemspec @@ -15,10 +15,10 @@ Gem::Specification.new do |s| s.email = 'david@loudthinking.com' s.homepage = 'http://www.rubyonrails.org' - s.files = Dir['CHANGELOG.md', 'README.rdoc', 'RDOC_MAIN.rdoc', 'bin/**/*', 'lib/**/{*,.[a-z]*}'] + s.files = Dir['CHANGELOG.md', 'README.rdoc', 'RDOC_MAIN.rdoc', 'exe/**/*', 'lib/**/{*,.[a-z]*}'] s.require_path = 'lib' - s.bindir = 'bin' + s.bindir = 'exe' s.executables = ['rails'] s.rdoc_options << '--exclude' << '.' diff --git a/railties/test/application/asset_debugging_test.rb b/railties/test/application/asset_debugging_test.rb index 9a571fac3a..acd387256c 100644 --- a/railties/test/application/asset_debugging_test.rb +++ b/railties/test/application/asset_debugging_test.rb @@ -57,8 +57,8 @@ module ApplicationTests class ::PostsController < ActionController::Base ; end get '/posts?debug_assets=true' - assert_match(/<script src="\/assets\/application-([0-z]+)\.js\?body=1"><\/script>/, last_response.body) - assert_match(/<script src="\/assets\/xmlhr-([0-z]+)\.js\?body=1"><\/script>/, last_response.body) + assert_match(/<script src="\/assets\/application(\.self)?-([0-z]+)\.js\?body=1"><\/script>/, last_response.body) + assert_match(/<script src="\/assets\/xmlhr(\.self)?-([0-z]+)\.js\?body=1"><\/script>/, last_response.body) end end end diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb index 0a2f283cce..f6b7d4c855 100644 --- a/railties/test/application/assets_test.rb +++ b/railties/test/application/assets_test.rb @@ -204,7 +204,7 @@ module ApplicationTests app_file "app/assets/javascripts/application.js", "alert();" precompile! - manifest = Dir["#{app_path}/public/assets/manifest-*.json"].first + manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first assets = ActiveSupport::JSON.decode(File.read(manifest)) assert_match(/application-([0-z]+)\.js/, assets["assets"]["application.js"]) @@ -217,7 +217,7 @@ module ApplicationTests precompile! - manifest = Dir["#{app_path}/public/x/manifest-*.json"].first + manifest = Dir["#{app_path}/public/x/.sprockets-manifest-*.json"].first assets = ActiveSupport::JSON.decode(File.read(manifest)) assert_match(/application-([0-z]+)\.js/, assets["assets"]["application.js"]) end @@ -229,7 +229,7 @@ module ApplicationTests ENV["RAILS_ENV"] = "production" precompile! - manifest = Dir["#{app_path}/public/assets/manifest-*.json"].first + manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first assets = ActiveSupport::JSON.decode(File.read(manifest)) asset_path = assets["assets"]["application.js"] @@ -261,7 +261,7 @@ module ApplicationTests ENV["RAILS_ENV"] = "production" precompile! - manifest = Dir["#{app_path}/public/assets/manifest-*.json"].first + manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first assets = ActiveSupport::JSON.decode(File.read(manifest)) asset_path = assets["assets"]["application.css"] @@ -291,7 +291,7 @@ module ApplicationTests precompile! - manifest = Dir["#{app_path}/public/assets/manifest-*.json"].first + manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first assets = ActiveSupport::JSON.decode(File.read(manifest)) assert asset_path = assets["assets"].find { |(k, _)| k && k =~ /.png/ }[1] @@ -437,9 +437,9 @@ module ApplicationTests class ::PostsController < ActionController::Base; end get '/posts', {}, {'HTTPS'=>'off'} - assert_match('src="http://example.com/assets/application.js', last_response.body) + assert_match('src="http://example.com/assets/application.self.js', last_response.body) get '/posts', {}, {'HTTPS'=>'on'} - assert_match('src="https://example.com/assets/application.js', last_response.body) + assert_match('src="https://example.com/assets/application.self.js', last_response.body) end test "asset urls should be protocol-relative if no request is in scope" do diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index 2bff21dae5..0648b11813 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -99,7 +99,7 @@ module ApplicationTests end def test_code_statistics_sanity - assert_match "Code LOC: 5 Test LOC: 0 Code to Test Ratio: 1:0.0", + assert_match "Code LOC: 7 Test LOC: 0 Code to Test Ratio: 1:0.0", Dir.chdir(app_path){ `rake stats` } end diff --git a/railties/test/code_statistics_test.rb b/railties/test/code_statistics_test.rb new file mode 100644 index 0000000000..1b1ff80bc1 --- /dev/null +++ b/railties/test/code_statistics_test.rb @@ -0,0 +1,20 @@ +require 'abstract_unit' +require 'rails/code_statistics' + +class CodeStatisticsTest < ActiveSupport::TestCase + def setup + @tmp_path = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'tmp')) + @dir_js = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'tmp', 'lib.js')) + FileUtils.mkdir_p(@dir_js) + end + + def teardown + FileUtils.rm_rf(@tmp_path) + end + + test 'ignores directories that happen to have source files extensions' do + assert_nothing_raised do + @code_statistics = CodeStatistics.new(['tmp dir', @tmp_path]) + end + end +end diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 00a7932267..282e8cc4f9 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -18,6 +18,7 @@ DEFAULT_APP_FILES = %w( app/mailers app/models app/models/concerns + app/jobs app/views/layouts bin/bundle bin/rails @@ -67,6 +68,11 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file("app/assets/javascripts/application.js") end + def test_application_job_file_present + run_generator + assert_file("app/jobs/application_job.rb") + end + def test_invalid_application_name_raises_an_error content = capture(:stderr){ run_generator [File.join(destination_root, "43-things")] } assert_equal "Invalid application name 43-things. Please give a name which does not start with numbers.\n", content diff --git a/railties/test/generators/job_generator_test.rb b/railties/test/generators/job_generator_test.rb index a9e0cea94f..7fd8f2062f 100644 --- a/railties/test/generators/job_generator_test.rb +++ b/railties/test/generators/job_generator_test.rb @@ -7,14 +7,14 @@ class JobGeneratorTest < Rails::Generators::TestCase def test_job_skeleton_is_created run_generator ["refresh_counters"] assert_file "app/jobs/refresh_counters_job.rb" do |job| - assert_match(/class RefreshCountersJob < ActiveJob::Base/, job) + assert_match(/class RefreshCountersJob < ApplicationJob/, job) end end def test_job_queue_param run_generator ["refresh_counters", "--queue", "important"] assert_file "app/jobs/refresh_counters_job.rb" do |job| - assert_match(/class RefreshCountersJob < ActiveJob::Base/, job) + assert_match(/class RefreshCountersJob < ApplicationJob/, job) assert_match(/queue_as :important/, job) end end @@ -22,7 +22,7 @@ class JobGeneratorTest < Rails::Generators::TestCase def test_job_namespace run_generator ["admin/refresh_counters", "--queue", "admin"] assert_file "app/jobs/admin/refresh_counters_job.rb" do |job| - assert_match(/class Admin::RefreshCountersJob < ActiveJob::Base/, job) + assert_match(/class Admin::RefreshCountersJob < ApplicationJob/, job) assert_match(/queue_as :admin/, job) end end diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb index 17a13fbf1f..abd3ff50a4 100644 --- a/railties/test/generators/model_generator_test.rb +++ b/railties/test/generators/model_generator_test.rb @@ -319,6 +319,16 @@ class ModelGeneratorTest < Rails::Generators::TestCase assert_no_file "test/fixtures/accounts.yml" end + def test_fixture_without_pluralization + original_pluralize_table_name = ActiveRecord::Base.pluralize_table_names + ActiveRecord::Base.pluralize_table_names = false + run_generator + assert_generated_fixture("test/fixtures/account.yml", + {"one"=>{"name"=>"MyString", "age"=>1}, "two"=>{"name"=>"MyString", "age"=>1}}) + ensure + ActiveRecord::Base.pluralize_table_names = original_pluralize_table_name + end + def test_check_class_collision content = capture(:stderr){ run_generator ["object"] } assert_match(/The name 'Object' is either already used in your application or reserved/, content) diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 63209559d7..4509797da1 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -322,7 +322,7 @@ Module.new do environment = File.expand_path('../../../../load_paths', __FILE__) require_environment = "-r #{environment}" - `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/railties/bin/rails new #{app_template_path} --skip-gemfile --no-rc` + `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails new #{app_template_path} --skip-gemfile --no-rc` File.open("#{app_template_path}/config/boot.rb", 'w') do |f| f.puts "require '#{environment}'" f.puts "require 'rails/all'" diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb index 6185742cc1..79bd7a8241 100644 --- a/railties/test/railties/engine_test.rb +++ b/railties/test/railties/engine_test.rb @@ -1155,10 +1155,10 @@ YAML assert_equal "App's bar partial", last_response.body.strip get("/assets/foo.js") - assert_equal "// Bukkit's foo js\n;", last_response.body.strip + assert_equal "// Bukkit's foo js", last_response.body.strip get("/assets/bar.js") - assert_equal "// App's bar js\n;", last_response.body.strip + assert_equal "// App's bar js", last_response.body.strip # ensure that railties are not added twice railties = Rails.application.send(:ordered_railties).map(&:class) diff --git a/railties/test/railties/generators_test.rb b/railties/test/railties/generators_test.rb index 7348d70c56..423ece277e 100644 --- a/railties/test/railties/generators_test.rb +++ b/railties/test/railties/generators_test.rb @@ -30,7 +30,7 @@ module RailtiesTests if File.exist?("#{environment}.rb") require_environment = "-r #{environment}" end - `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/railties/bin/rails #{cmd}` + `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails #{cmd}` end def build_engine(is_mountable=false) |