diff options
240 files changed, 2736 insertions, 1628 deletions
diff --git a/.travis.yml b/.travis.yml index e5aaf57f93..8eee7b129b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,8 @@ env: - "GEM=ar:postgresql" matrix: allow_failures: + - rvm: 1.9.3 + env: "GEM=ar:mysql" - rvm: rbx-2 - rvm: jruby - rvm: ruby-head diff --git a/actionmailer/README.rdoc b/actionmailer/README.rdoc index 67b64fe469..ceca912ada 100644 --- a/actionmailer/README.rdoc +++ b/actionmailer/README.rdoc @@ -156,7 +156,11 @@ API documentation is at * http://api.rubyonrails.org -Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here: +Bug reports can be filed for the Ruby on Rails project here: * https://github.com/rails/rails/issues +Feature requests should be discussed on the rails-core mailing list here: + +* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core + diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec index 9b25feaf75..01d97b7213 100644 --- a/actionmailer/actionmailer.gemspec +++ b/actionmailer/actionmailer.gemspec @@ -22,5 +22,5 @@ Gem::Specification.new do |s| s.add_dependency 'actionpack', version s.add_dependency 'actionview', version - s.add_dependency 'mail', '~> 2.5.4' + s.add_dependency 'mail', ['~> 2.5', '>= 2.5.4'] end diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 5852aeaec2..18511edaf0 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -369,8 +369,8 @@ module ActionMailer # This is a symbol and one of <tt>:plain</tt> (will send the password in the clear), <tt>:login</tt> (will # send password Base64 encoded) or <tt>:cram_md5</tt> (combines a Challenge/Response mechanism to exchange # information and a cryptographic Message Digest 5 algorithm to hash important information) - # * <tt>:enable_starttls_auto</tt> - When set to true, detects if STARTTLS is enabled in your SMTP server - # and starts to use it. + # * <tt>:enable_starttls_auto</tt> - Detects if STARTTLS is enabled in your SMTP server and starts + # to use it. Defaults to <tt>true</tt>. # * <tt>:openssl_verify_mode</tt> - When using TLS, you can set how OpenSSL checks the certificate. This is # really useful if you need to validate a self-signed and/or a wildcard certificate. You can use the name # of an OpenSSL verify constant (<tt>'none'</tt>, <tt>'peer'</tt>, <tt>'client_once'</tt>, diff --git a/actionmailer/lib/action_mailer/test_case.rb b/actionmailer/lib/action_mailer/test_case.rb index 207f949fe2..a5442c0316 100644 --- a/actionmailer/lib/action_mailer/test_case.rb +++ b/actionmailer/lib/action_mailer/test_case.rb @@ -20,6 +20,7 @@ module ActionMailer class_attribute :_mailer_class setup :initialize_test_deliveries setup :set_expected_mail + teardown :restore_test_deliveries end module ClassMethods @@ -54,8 +55,15 @@ module ActionMailer protected def initialize_test_deliveries + @old_delivery_method = ActionMailer::Base.delivery_method + @old_perform_deliveries = ActionMailer::Base.perform_deliveries ActionMailer::Base.delivery_method = :test ActionMailer::Base.perform_deliveries = true + end + + def restore_test_deliveries + ActionMailer::Base.delivery_method = @old_delivery_method + ActionMailer::Base.perform_deliveries = @old_perform_deliveries ActionMailer::Base.deliveries.clear end diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb index 93d16f491d..4e5a9c3715 100644 --- a/actionmailer/test/abstract_unit.rb +++ b/actionmailer/test/abstract_unit.rb @@ -23,10 +23,6 @@ ActiveSupport::Deprecation.debug = true # Disable available locale checks to avoid warnings running the test suite. I18n.enforce_available_locales = false -# Bogus template processors -ActionView::Template.register_template_handler :haml, lambda { |template| "Look its HAML!".inspect } -ActionView::Template.register_template_handler :bak, lambda { |template| "Lame backup".inspect } - FIXTURE_LOAD_PATH = File.expand_path('fixtures', File.dirname(__FILE__)) ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH diff --git a/actionmailer/test/i18n_with_controller_test.rb b/actionmailer/test/i18n_with_controller_test.rb index d502d42ffd..d9d588a950 100644 --- a/actionmailer/test/i18n_with_controller_test.rb +++ b/actionmailer/test/i18n_with_controller_test.rb @@ -32,6 +32,10 @@ class ActionMailerI18nWithControllerTest < ActionDispatch::IntegrationTest Routes end + teardown do + I18n.locale = I18n.default_locale + end + def test_send_mail with_translation 'de', email_subject: '[Anmeldung] Willkommen' do get '/test/send_mail' diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 6a50565de5..dfd5ddeedf 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,6 +1,11 @@ -* Deprecate all *_filter callbacks in favor of *_action callbacks. +* Ensure the controller is always notified as soon as the client disconnects + during live streaming, even when the controller is blocked on a write. - *Rafael Mendonça França* + *Nicholas Jakobsen*, *Matthew Draper* + +* Routes specifying 'to:' must be a string that contains a "#" or a rack + application. Use of a symbol should be replaced with `action: symbol`. + Use of a string without a "#" should be replaced with `controller: string`. * Fix URL generation with `:trailing_slash` such that it does not add a trailing slash after `.:format` diff --git a/actionpack/README.rdoc b/actionpack/README.rdoc index 2f6575c3b5..02a24a7412 100644 --- a/actionpack/README.rdoc +++ b/actionpack/README.rdoc @@ -48,6 +48,11 @@ API documentation is at * http://api.rubyonrails.org -Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here: +Bug reports can be filed for the Ruby on Rails project here: * https://github.com/rails/rails/issues + +Feature requests should be discussed on the rails-core mailing list here: + +* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core + diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index 252e297c69..ca5c80cd71 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -1,5 +1,3 @@ -require 'active_support/deprecation' - module AbstractController module Callbacks extend ActiveSupport::Concern @@ -56,11 +54,7 @@ module AbstractController skip_after_action(*names) skip_around_action(*names) end - - def skip_filter(*names) - ActiveSupport::Deprecation.warn("#{callback}_filter is deprecated and will removed in Rails 5. Use #{callback}_action instead.") - skip_action_callback(*names) - end + alias_method :skip_filter, :skip_action_callback # Take callback names and an optional callback proc, normalize them, # then call the block with each callback. This allows us to abstract @@ -175,22 +169,14 @@ module AbstractController set_callback(:process_action, callback, name, options) end end - - define_method "#{callback}_filter" do |*names, &blk| - ActiveSupport::Deprecation.warn("#{callback}_filter is deprecated and will removed in Rails 5. Use #{callback}_action instead.") - send("#{callback}_action", *names, &blk) - end + alias_method :"#{callback}_filter", :"#{callback}_action" define_method "prepend_#{callback}_action" do |*names, &blk| _insert_callbacks(names, blk) do |name, options| set_callback(:process_action, callback, name, options.merge(:prepend => true)) end end - - define_method "prepend_#{callback}_filter" do |*names, &blk| - ActiveSupport::Deprecation.warn("prepend_#{callback}_filter is deprecated and will removed in Rails 5. Use prepend_#{callback}_action instead.") - send("prepend_#{callback}_action", *names, &blk) - end + alias_method :"prepend_#{callback}_filter", :"prepend_#{callback}_action" # Skip a before, after or around callback. See _insert_callbacks # for details on the allowed parameters. @@ -199,18 +185,11 @@ module AbstractController skip_callback(:process_action, callback, name, options) end end - - define_method "skip_#{callback}_filter" do |*names, &blk| - ActiveSupport::Deprecation.warn("skip_#{callback}_filter is deprecated and will removed in Rails 5. Use skip_#{callback}_action instead.") - send("skip_#{callback}_action", *names, &blk) - end + alias_method :"skip_#{callback}_filter", :"skip_#{callback}_action" # *_action is the same as append_*_action - alias_method :"append_#{callback}_action", :"#{callback}_action" # alias_method :append_before_action, :before_action - define_method "append_#{callback}_filter" do |*names, &blk| - ActiveSupport::Deprecation.warn("append_#{callback}_filter is deprecated and will removed in Rails 5. Use append_#{callback}_action instead.") - send("append_#{callback}_action", *names, &blk) - end + alias_method :"append_#{callback}_action", :"#{callback}_action" + alias_method :"append_#{callback}_filter", :"#{callback}_action" end end end diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index 4c0554d27b..67875141cb 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -107,12 +107,25 @@ module ActionController end end + class ClientDisconnected < RuntimeError + end + class Buffer < ActionDispatch::Response::Buffer #:nodoc: include MonitorMixin + # Ignore that the client has disconnected. + # + # If this value is `true`, calling `write` after the client + # disconnects will result in the written content being silently + # discarded. If this value is `false` (the default), a + # ClientDisconnected exception will be raised. + attr_accessor :ignore_disconnect + def initialize(response) @error_callback = lambda { true } @cv = new_cond + @aborted = false + @ignore_disconnect = false super(response, SizedQueue.new(10)) end @@ -123,6 +136,17 @@ module ActionController end super + + unless connected? + @buf.clear + + unless @ignore_disconnect + # Raise ClientDisconnected, which is a RuntimeError (not an + # IOError), because that's more appropriate for something beyond + # the developer's control. + raise ClientDisconnected, "client disconnected" + end + end end def each @@ -133,6 +157,10 @@ module ActionController @response.sent! end + # Write a 'close' event to the buffer; the producer/writing thread + # uses this to notify us that it's finished supplying content. + # + # See also #abort. def close synchronize do super @@ -141,6 +169,26 @@ module ActionController end end + # Inform the producer/writing thread that the client has + # disconnected; the reading thread is no longer interested in + # anything that's being written. + # + # See also #close. + def abort + synchronize do + @aborted = true + @buf.clear + end + end + + # Is the client still connected and waiting for content? + # + # The result of calling `write` when this is `false` is determined + # by `ignore_disconnect`. + def connected? + !@aborted + end + def await_close synchronize do @cv.wait_until { @closed } diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index 136e086d0d..3feb737277 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -72,7 +72,7 @@ module ActionController self.status = _extract_redirect_to_status(options, response_status) self.location = _compute_redirect_to_location(options) - self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.h(location)}\">redirected</a>.</body></html>" + self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(location)}\">redirected</a>.</body></html>" end def _compute_redirect_to_location(options) #:nodoc: diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb index 62d5931b45..04401cad7b 100644 --- a/actionpack/lib/action_controller/metal/streaming.rb +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -183,7 +183,7 @@ module ActionController #:nodoc: # You may also want to configure other parameters like <tt>:tcp_nodelay</tt>. # Please check its documentation for more information: http://unicorn.bogomips.org/Unicorn/Configurator.html#method-i-listen # - # If you are using Unicorn with Nginx, you may need to tweak Nginx. + # If you are using Unicorn with NGINX, you may need to tweak NGINX. # Streaming should work out of the box on Rainbows. # # ==== Passenger diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index d86d49c9dc..c3c3e4c4f1 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -129,6 +129,10 @@ module ActionController # Attribute that keeps track of converted arrays, if any, to avoid double # looping in the common use case permit + mass-assignment. Defined in a # method to instantiate it only if needed. + # + # Testing membership still loops, but it's going to be faster than our own + # loop that converts values. Also, we are not going to build a new array + # object per fetch. def converted_arrays @converted_arrays ||= Set.new end @@ -158,8 +162,8 @@ module ActionController def permit! each_pair do |key, value| value = convert_hashes_to_parameters(key, value) - Array.wrap(value).each do |_| - _.permit! if _.respond_to? :permit! + Array.wrap(value).each do |v| + v.permit! if v.respond_to? :permit! end end diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb index 3e607bbde1..bc5410dc38 100644 --- a/actionpack/lib/action_dispatch/http/headers.rb +++ b/actionpack/lib/action_dispatch/http/headers.rb @@ -6,14 +6,27 @@ module ActionDispatch # headers = ActionDispatch::Http::Headers.new(env) # headers["Content-Type"] # => "text/plain" class Headers - CGI_VARIABLES = %w( - CONTENT_TYPE CONTENT_LENGTH - HTTPS AUTH_TYPE GATEWAY_INTERFACE - PATH_INFO PATH_TRANSLATED QUERY_STRING - REMOTE_ADDR REMOTE_HOST REMOTE_IDENT REMOTE_USER - REQUEST_METHOD SCRIPT_NAME - SERVER_NAME SERVER_PORT SERVER_PROTOCOL SERVER_SOFTWARE - ) + CGI_VARIABLES = Set.new(%W[ + AUTH_TYPE + CONTENT_LENGTH + CONTENT_TYPE + GATEWAY_INTERFACE + HTTPS + PATH_INFO + PATH_TRANSLATED + QUERY_STRING + REMOTE_ADDR + REMOTE_HOST + REMOTE_IDENT + REMOTE_USER + REQUEST_METHOD + SCRIPT_NAME + SERVER_NAME + SERVER_PORT + SERVER_PROTOCOL + SERVER_SOFTWARE + ]).freeze + HTTP_HEADER = /\A[A-Za-z0-9-]+\z/ include Enumerable diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index 1ab11392ce..5f7627cf96 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -15,7 +15,6 @@ module ActionDispatch query_parameters.dup end params.merge!(path_parameters) - params.with_indifferent_access end end alias :params :parameters diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index dfe258e463..4d4b443fb4 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -291,7 +291,7 @@ module ActionDispatch # Override Rack's GET method to support indifferent access def GET - @env["action_dispatch.request.query_parameters"] ||= Utils.deep_munge((normalize_encode_params(super) || {})) + @env["action_dispatch.request.query_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {})) rescue TypeError => e raise ActionController::BadRequest.new(:query, e) end @@ -299,7 +299,7 @@ module ActionDispatch # Override Rack's POST method to support indifferent access def POST - @env["action_dispatch.request.request_parameters"] ||= Utils.deep_munge((normalize_encode_params(super) || {})) + @env["action_dispatch.request.request_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {})) rescue TypeError => e raise ActionController::BadRequest.new(:request, e) end diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index eaea93b730..2fab6be1a5 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -97,6 +97,9 @@ module ActionDispatch # :nodoc: x end + def abort + end + def close @response.commit! @closed = true @@ -207,18 +210,6 @@ module ActionDispatch # :nodoc: end alias_method :status_message, :message - def respond_to?(method, include_private = false) - if method.to_s == 'to_path' - stream.respond_to?(method) - else - super - end - end - - def to_path - stream.to_path - end - # Returns the content of the response as a string. This contains the contents # of any calls to <tt>render</tt>. def body @@ -271,6 +262,17 @@ module ActionDispatch # :nodoc: stream.close if stream.respond_to?(:close) end + def abort + if stream.respond_to?(:abort) + stream.abort + elsif stream.respond_to?(:close) + # `stream.close` should really be reserved for a close from the + # other direction, but we must fall back to it for + # compatibility. + stream.close + end + end + # Turns the Response into a Rack-compatible array of the status, headers, # and body. def to_a @@ -337,6 +339,38 @@ module ActionDispatch # :nodoc: !@sending_file && @charset != false end + class RackBody + def initialize(response) + @response = response + end + + def each(*args, &block) + @response.each(*args, &block) + end + + def close + # Rack "close" maps to Response#abort, and *not* Response#close + # (which is used when the controller's finished writing) + @response.abort + end + + def body + @response.body + end + + def respond_to?(method, include_private = false) + if method.to_s == 'to_path' + @response.stream.respond_to?(method) + else + super + end + end + + def to_path + @response.stream.to_path + end + end + def rack_response(status, header) assign_default_content_type_and_charset!(header) handle_conditional_get! @@ -347,7 +381,7 @@ module ActionDispatch # :nodoc: header.delete CONTENT_TYPE [status, header, []] else - [status, header, Rack::BodyProxy.new(self){}] + [status, header, RackBody.new(self)] end end end diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index 4cba4f5f37..92aba01b95 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -5,18 +5,18 @@ module ActionDispatch module Http module URL IP_HOST_REGEXP = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ - HOST_REGEXP = /(^.*:\/\/)?([^:]+)(?::(\d+$))?/ + HOST_REGEXP = /(^[^:]+:\/\/)?([^:]+)(?::(\d+$))?/ PROTOCOL_REGEXP = /^([^:]+)(:)?(\/\/)?$/ mattr_accessor :tld_length self.tld_length = 1 class << self - def extract_domain(host, tld_length = @@tld_length) + def extract_domain(host, tld_length) host.split('.').last(1 + tld_length).join('.') if named_host?(host) end - def extract_subdomains(host, tld_length = @@tld_length) + def extract_subdomains(host, tld_length) if named_host?(host) parts = host.split('.') parts[0..-(tld_length + 2)] @@ -25,7 +25,7 @@ module ActionDispatch end end - def extract_subdomain(host, tld_length = @@tld_length) + def extract_subdomain(host, tld_length) extract_subdomains(host, tld_length).join('.') end @@ -73,38 +73,38 @@ module ActionDispatch end def build_host_url(options) - if match = options[:host].match(HOST_REGEXP) - options[:protocol] ||= match[1] unless options[:protocol] == false - options[:host] = match[2] - options[:port] = match[3] unless options.key?(:port) + protocol = options[:protocol] + host = options[:host] + port = options[:port] + if match = host.match(HOST_REGEXP) + protocol ||= match[1] unless protocol == false + host = match[2] + port = match[3] unless options.key? :port end - options[:protocol] = normalize_protocol(options) - options[:host] = normalize_host(options) - options[:port] = normalize_port(options) + protocol = normalize_protocol protocol + host = normalize_host(host, options) - result = options[:protocol] + result = protocol.dup if options[:user] && options[:password] result << "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@" end - result << options[:host] - result << ":#{options[:port]}" if options[:port] + result << host + normalize_port(port, protocol) { |normalized_port| + result << ":#{normalized_port}" + } result end def named_host?(host) - host && IP_HOST_REGEXP !~ host + IP_HOST_REGEXP !~ host end - def same_host?(options) - (options[:subdomain] == true || !options.key?(:subdomain)) && options[:domain].nil? - end - - def normalize_protocol(options) - case options[:protocol] + def normalize_protocol(protocol) + case protocol when nil "http://" when false, "//" @@ -112,36 +112,38 @@ module ActionDispatch when PROTOCOL_REGEXP "#{$1}://" else - raise ArgumentError, "Invalid :protocol option: #{options[:protocol].inspect}" + raise ArgumentError, "Invalid :protocol option: #{protocol.inspect}" end end - def normalize_host(options) - return options[:host] if !named_host?(options[:host]) || same_host?(options) + def normalize_host(_host, options) + return _host unless named_host?(_host) tld_length = options[:tld_length] || @@tld_length host = "" - if options[:subdomain] == true || !options.key?(:subdomain) - host << extract_subdomain(options[:host], tld_length).to_param - elsif options[:subdomain].present? - host << options[:subdomain].to_param + subdomain = options[:subdomain] + if subdomain == true || !options.key?(:subdomain) + return _host if options[:domain].nil? + + host << extract_subdomain(_host, tld_length) + elsif subdomain + host << subdomain.to_param end host << "." unless host.empty? - host << (options[:domain] || extract_domain(options[:host], tld_length)) + host << (options[:domain] || extract_domain(_host, tld_length)) host end - def normalize_port(options) - return nil if options[:port].nil? || options[:port] == false + def normalize_port(port, protocol) + return unless port - case options[:protocol] - when "//" - options[:port] + case protocol + when "//" then yield port when "https://" - options[:port].to_i == 443 ? nil : options[:port] + yield port unless port.to_i == 443 else - options[:port].to_i == 80 ? nil : options[:port] + yield port unless port.to_i == 80 end end end diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb index cbb066b092..6a79b4e859 100644 --- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb +++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb @@ -11,7 +11,7 @@ module ActionDispatch # Some Rack servers concatenate repeated headers, like {HTTP RFC 2616}[http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2] # requires. Some Rack servers simply drop preceding headers, and only report # the value that was {given in the last header}[http://andre.arko.net/2011/12/26/repeated-headers-and-ruby-web-servers]. - # If you are behind multiple proxy servers (like Nginx to HAProxy to Unicorn) + # If you are behind multiple proxy servers (like NGINX to HAProxy to Unicorn) # then you should test your Rack server to make sure your data is good. # # IF YOU DON'T USE A PROXY, THIS MAKES YOU VULNERABLE TO IP SPOOFING. @@ -118,7 +118,7 @@ module ActionDispatch # # REMOTE_ADDR will be correct if the request is made directly against the # Ruby process, on e.g. Heroku. When the request is proxied by another - # server like HAProxy or Nginx, the IP address that made the original + # server like HAProxy or NGINX, the IP address that made the original # request will be put in an X-Forwarded-For header. If there are multiple # proxies, that header may contain a list of IPs. Other proxy services # set the Client-Ip header instead, so we check that too. diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 3bc578b379..aac5546aa1 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -7,6 +7,7 @@ require 'active_support/core_ext/module/remove_method' require 'active_support/inflector' require 'action_dispatch/routing/redirection' require 'action_dispatch/routing/endpoint' +require 'active_support/deprecation' module ActionDispatch module Routing @@ -60,32 +61,73 @@ module ActionDispatch end class Mapping #:nodoc: - IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix, :format] ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} - attr_reader :scope, :options, :requirements, :conditions, :defaults - attr_reader :to, :default_controller, :default_action - - def initialize(scope, path, options) - @scope = scope - @requirements, @conditions, @defaults = {}, {}, {} + attr_reader :requirements, :conditions, :defaults + attr_reader :to, :default_controller, :default_action, :as, :anchor + def self.build(scope, path, options) options = scope[:options].merge(options) if scope[:options] - @to = options[:to] - @default_controller = options[:controller] || scope[:controller] - @default_action = options[:action] || scope[:action] - path = normalize_path! path, options[:format] + options.delete :only + options.delete :except + options.delete :shallow_path + options.delete :shallow_prefix + options.delete :shallow + + defaults = (scope[:defaults] || {}).merge options.delete(:defaults) || {} + + new scope, path, defaults, options + end + + def initialize(scope, path, defaults, options) + @requirements, @conditions = {}, {} + @defaults = defaults + + @to = options.delete :to + @default_controller = options.delete(:controller) || scope[:controller] + @default_action = options.delete(:action) || scope[:action] + @as = options.delete :as + @anchor = options.delete :anchor + + formatted = options.delete :format + via = Array(options.delete(:via) { [] }) + options_constraints = options.delete :constraints + + path = normalize_path! path, formatted ast = path_ast path path_params = path_params ast - @options = normalize_options!(options, path_params, ast) - normalize_requirements!(path_params) - normalize_conditions!(path_params, path, ast) - normalize_defaults! + + options = normalize_options!(options, formatted, path_params, ast, scope[:module]) + + + split_constraints(path_params, scope[:constraints]) if scope[:constraints] + constraints = constraints(options, path_params) + + split_constraints path_params, constraints + + @blocks = blocks(options_constraints, scope[:blocks]) + + if options_constraints.is_a?(Hash) + split_constraints path_params, options_constraints + options_constraints.each do |key, default| + if URL_OPTIONS.include?(key) && (String === default || Fixnum === default) + @defaults[key] ||= default + end + end + end + + normalize_format!(formatted) + + @conditions[:path_info] = path + @conditions[:parsed_path_info] = ast + + add_request_method(via, @conditions) + normalize_defaults!(options) end def to_route - [ app, conditions, requirements, defaults, options[:as], options[:anchor] ] + [ app(@blocks), conditions, requirements, defaults, as, anchor ] end private @@ -106,17 +148,17 @@ module ActionDispatch format != false && !path.include?(':format') && !path.end_with?('/') end - def normalize_options!(options, path_params, path_ast) + def normalize_options!(options, formatted, path_params, path_ast, modyoule) # Add a constraint for wildcard route to make it non-greedy and match the # optional format part of the route by default - if options[:format] != false + if formatted != false path_ast.grep(Journey::Nodes::Star) do |node| options[node.name.to_sym] ||= /.+?/ end end if path_params.include?(:controller) - raise ArgumentError, ":controller segment is not allowed within a namespace block" if scope[:module] + raise ArgumentError, ":controller segment is not allowed within a namespace block" if modyoule # Add a default constraint for :controller path segments that matches namespaced # controllers with default routes like :controller/:action/:id(.:format), e.g: @@ -128,23 +170,36 @@ module ActionDispatch if to.respond_to? :call options else - options.merge!(default_controller_and_action(path_params)) + to_endpoint = split_to to + controller = to_endpoint[0] || default_controller + action = to_endpoint[1] || default_action + + controller = add_controller_module(controller, modyoule) + + options.merge! check_controller_and_action(path_params, controller, action) end end - def normalize_requirements!(path_params) - constraints.each do |key, requirement| - next unless path_params.include?(key) || key == :controller - verify_regexp_requirement(requirement) if requirement.is_a?(Regexp) - @requirements[key] = requirement + def split_constraints(path_params, constraints) + constraints.each_pair do |key, requirement| + if path_params.include?(key) || key == :controller + verify_regexp_requirement(requirement) if requirement.is_a?(Regexp) + @requirements[key] = requirement + else + @conditions[key] = requirement + end end + end - if options[:format] == true + def normalize_format!(formatted) + if formatted == true @requirements[:format] ||= /.+/ - elsif Regexp === options[:format] - @requirements[:format] = options[:format] - elsif String === options[:format] - @requirements[:format] = Regexp.compile(options[:format]) + elsif Regexp === formatted + @requirements[:format] = formatted + @defaults[:format] = nil + elsif String === formatted + @requirements[:format] = Regexp.compile(formatted) + @defaults[:format] = formatted end end @@ -158,31 +213,12 @@ module ActionDispatch end end - def normalize_defaults! - @defaults.merge!(scope[:defaults]) if scope[:defaults] - @defaults.merge!(options[:defaults]) if options[:defaults] - - options.each do |key, default| - unless Regexp === default || IGNORE_OPTIONS.include?(key) + def normalize_defaults!(options) + options.each_pair do |key, default| + unless Regexp === default @defaults[key] = default end end - - if options[:constraints].is_a?(Hash) - options[:constraints].each do |key, default| - if URL_OPTIONS.include?(key) && (String === default || Fixnum === default) - @defaults[key] ||= default - end - end - elsif options[:constraints] - verify_callable_constraint(options[:constraints]) - end - - if Regexp === options[:format] - @defaults[:format] = nil - elsif String === options[:format] - @defaults[:format] = options[:format] - end end def verify_callable_constraint(callable_constraint) @@ -191,41 +227,22 @@ module ActionDispatch end end - def normalize_conditions!(path_params, path, ast) - @conditions[:path_info] = path - @conditions[:parsed_path_info] = ast + def add_request_method(via, conditions) + return if via == [:all] - constraints.each do |key, condition| - unless path_params.include?(key) || key == :controller - @conditions[key] = condition - end - end - - required_defaults = [] - options.each do |key, required_default| - unless path_params.include?(key) || IGNORE_OPTIONS.include?(key) || Regexp === required_default - required_defaults << key - end - end - @conditions[:required_defaults] = required_defaults - - via_all = options.delete(:via) if options[:via] == :all - - if !via_all && options[:via].blank? + if via.empty? msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \ "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \ "If you want to expose your action to GET, use `get` in the router:\n" \ " Instead of: match \"controller#action\"\n" \ " Do: get \"controller#action\"" - raise msg + raise ArgumentError, msg end - if via = options[:via] - @conditions[:request_method] = Array(via).map { |m| m.to_s.dasherize.upcase } - end + conditions[:request_method] = via.map { |m| m.to_s.dasherize.upcase } end - def app + def app(blocks) return to if Redirect === to if to.respond_to?(:call) @@ -239,13 +256,7 @@ module ActionDispatch end end - def default_controller_and_action(path_params) - controller, action = get_controller_and_action(default_controller, - default_action, - to, - @scope[:module] - ) - + def check_controller_and_action(path_params, controller, action) hash = check_part(:controller, controller, path_params, {}) do |part| translate_controller(part) { message = "'#{part}' is not a supported controller name. This can lead to potential routing problems." @@ -272,21 +283,30 @@ module ActionDispatch hash end - def get_controller_and_action(controller, action, to, modyoule) + def split_to(to) case to - when Symbol then action = to.to_s - when /#/ then controller, action = to.split('#') - when String then controller = to + when Symbol + ActiveSupport::Deprecation.warn "defining a route where `to` is a symbol is deprecated. Please change \"to: :#{to}\" to \"action: :#{to}\"" + [nil, to.to_s] + when /#/ then to.split('#') + when String + ActiveSupport::Deprecation.warn "defining a route where `to` is a controller without an action is deprecated. Please change \"to: :#{to}\" to \"controller: :#{to}\"" + [to, nil] + else + [] end + end + def add_controller_module(controller, modyoule) if modyoule && !controller.is_a?(Regexp) if controller =~ %r{\A/} - controller = controller[1..-1] + controller[1..-1] else - controller = [modyoule, controller].compact.join("/") + [modyoule, controller].compact.join("/") end + else + controller end - [controller, action] end def translate_controller(controller) @@ -296,24 +316,27 @@ module ActionDispatch yield end - def blocks - if options[:constraints].present? && !options[:constraints].is_a?(Hash) - [options[:constraints]] + def blocks(options_constraints, scope_blocks) + if options_constraints && !options_constraints.is_a?(Hash) + verify_callable_constraint(options_constraints) + [options_constraints] else - scope[:blocks] || [] + scope_blocks || [] end end - def constraints - @constraints ||= {}.tap do |constraints| - constraints.merge!(scope[:constraints]) if scope[:constraints] - - options.except(*IGNORE_OPTIONS).each do |key, option| - constraints[key] = option if Regexp === option + def constraints(options, path_params) + constraints = {} + required_defaults = [] + options.each_pair do |key, option| + if Regexp === option + constraints[key] = option + else + required_defaults << key unless path_params.include?(key) end - - constraints.merge!(options[:constraints]) if options[:constraints].is_a?(Hash) end + @conditions[:required_defaults] = required_defaults + constraints end def path_params(ast) @@ -1445,7 +1468,20 @@ module ActionDispatch if rest.empty? && Hash === path options = path path, to = options.find { |name, _value| name.is_a?(String) } - options[:to] = to + + case to + when Symbol + options[:action] = to + when String + if to =~ /#/ + options[:to] = to + else + options[:controller] = to + end + else + options[:to] = to + end + options.delete(path) paths = [path] else @@ -1515,7 +1551,7 @@ module ActionDispatch options[:as] = name_for_action(options[:as], action) end - mapping = Mapping.new(@scope, URI.parser.escape(path), options) + mapping = Mapping.build(@scope, URI.parser.escape(path), options) app, conditions, requirements, defaults, as, anchor = mapping.to_route @set.add_route(app, conditions, requirements, defaults, as, anchor) end diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb index b1b39a5496..3c1c4fadf6 100644 --- a/actionpack/lib/action_dispatch/routing/redirection.rb +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -37,7 +37,7 @@ module ActionDispatch uri.host ||= req.host uri.port ||= req.port unless req.standard_port? - body = %(<html><body>You are being <a href="#{ERB::Util.h(uri.to_s)}">redirected</a>.</body></html>) + body = %(<html><body>You are being <a href="#{ERB::Util.unwrapped_html_escape(uri.to_s)}">redirected</a>.</body></html>) headers = { 'Location' => uri.to_s, diff --git a/actionpack/test/abstract/callbacks_test.rb b/actionpack/test/abstract/callbacks_test.rb index 07571602e4..8cba049485 100644 --- a/actionpack/test/abstract/callbacks_test.rb +++ b/actionpack/test/abstract/callbacks_test.rb @@ -267,11 +267,9 @@ module AbstractController end class AliasedCallbacks < ControllerWithCallbacks - ActiveSupport::Deprecation.silence do - before_filter :first - after_filter :second - around_filter :aroundz - end + before_filter :first + after_filter :second + around_filter :aroundz def first @text = "Hello world" diff --git a/actionpack/test/controller/assert_select_test.rb b/actionpack/test/controller/assert_select_test.rb index 580604df3a..f07d201563 100644 --- a/actionpack/test/controller/assert_select_test.rb +++ b/actionpack/test/controller/assert_select_test.rb @@ -56,13 +56,16 @@ class AssertSelectTest < ActionController::TestCase def setup super + @old_delivery_method = ActionMailer::Base.delivery_method + @old_perform_deliveries = ActionMailer::Base.perform_deliveries ActionMailer::Base.delivery_method = :test ActionMailer::Base.perform_deliveries = true - ActionMailer::Base.deliveries = [] end def teardown super + ActionMailer::Base.delivery_method = @old_delivery_method + ActionMailer::Base.perform_deliveries = @old_perform_deliveries ActionMailer::Base.deliveries.clear end diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb index 1dca36374a..0500b7c789 100644 --- a/actionpack/test/controller/live_stream_test.rb +++ b/actionpack/test/controller/live_stream_test.rb @@ -202,6 +202,39 @@ module ActionController response.stream.write '' response.stream.write params[:widget][:didnt_check_for_nil] end + + def overfill_buffer_and_die + # Write until the buffer is full. It doesn't expose that + # information directly, so we must hard-code its size: + 10.times do + response.stream.write '.' + end + # .. plus one more, because the #each frees up a slot: + response.stream.write '.' + + latch.release + + # This write will block, and eventually raise + response.stream.write 'x' + + 20.times do + response.stream.write '.' + end + end + + def ignore_client_disconnect + response.stream.ignore_disconnect = true + + response.stream.write '' # commit + + # These writes will be ignored + 15.times do + response.stream.write 'x' + end + + logger.info 'Work complete' + latch.release + end end tests TestController @@ -264,6 +297,62 @@ module ActionController assert t.join(3), 'timeout expired before the thread terminated' end + def test_abort_with_full_buffer + @controller.latch = ActiveSupport::Concurrency::Latch.new + + @request.parameters[:format] = 'plain' + @controller.request = @request + @controller.response = @response + + got_error = ActiveSupport::Concurrency::Latch.new + @response.stream.on_error do + ActionController::Base.logger.warn 'Error while streaming' + got_error.release + end + + t = Thread.new(@response) { |resp| + resp.await_commit + _, _, body = resp.to_a + body.each do |part| + @controller.latch.await + body.close + break + end + } + + capture_log_output do |output| + @controller.process :overfill_buffer_and_die + t.join + got_error.await + assert_match 'Error while streaming', output.rewind && output.read + end + end + + def test_ignore_client_disconnect + @controller.latch = ActiveSupport::Concurrency::Latch.new + + @controller.request = @request + @controller.response = @response + + t = Thread.new(@response) { |resp| + resp.await_commit + _, _, body = resp.to_a + body.each do |part| + body.close + break + end + } + + capture_log_output do |output| + @controller.process :ignore_client_disconnect + t.join + Timeout.timeout(3) do + @controller.latch.await + end + assert_match 'Work complete', output.rewind && output.read + end + end + def test_thread_locals_get_copied @controller.tc = self Thread.current[:originating_thread] = Thread.current.object_id diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb index 33a91d72d9..aa894ffa17 100644 --- a/actionpack/test/controller/parameters/parameters_permit_test.rb +++ b/actionpack/test/controller/parameters/parameters_permit_test.rb @@ -167,9 +167,26 @@ class ParametersPermitTest < ActiveSupport::TestCase end end + # Strong params has an optimization to avoid looping every time you read + # a key whose value is an array and building a new object. We check that + # optimization here. test 'arrays are converted at most once' do params = ActionController::Parameters.new(foo: [{}]) - assert params[:foo].equal?(params[:foo]) + assert_same params[:foo], params[:foo] + end + + # Strong params has an internal cache to avoid duplicated loops in the most + # common usage pattern. See the docs of the method `converted_arrays`. + # + # This test checks that if we push a hash to an array (in-place modification) + # the cache does not get fooled, the hash is still wrapped as strong params, + # and not permitted. + test 'mutated arrays are detected' do + params = ActionController::Parameters.new(users: [{id: 1}]) + + permitted = params.permit(users: [:id]) + permitted[:users] << {injected: 1} + assert_not permitted[:users].last.permitted? end test "fetch doesnt raise ParameterMissing exception if there is a default" do diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 9dc6d77012..721dad4dd9 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -243,6 +243,32 @@ class LegacyRouteSetTests < ActiveSupport::TestCase assert_equal 'clients', get(URI('http://clients.example.org/')) end + def test_scoped_lambda + scope_called = false + rs.draw do + scope '/foo', :constraints => lambda { |req| scope_called = true } do + get '/', :to => lambda { |env| [200, {}, %w{default}] } + end + end + + assert_equal 'default', get(URI('http://www.example.org/foo/')) + assert scope_called, "scope constraint should be called" + end + + def test_scoped_lambda_with_get_lambda + inner_called = false + + rs.draw do + scope '/foo', :constraints => lambda { |req| flunk "should not be called" } do + get '/', :constraints => lambda { |req| inner_called = true }, + :to => lambda { |env| [200, {}, %w{default}] } + end + end + + assert_equal 'default', get(URI('http://www.example.org/foo/')) + assert inner_called, "inner constraint should be called" + end + def test_empty_string_match rs.draw do get '/:username', :constraints => { :username => /[^\/]+/ }, diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb index 2c833aa2e5..73ed33ce2d 100644 --- a/actionpack/test/controller/send_file_test.rb +++ b/actionpack/test/controller/send_file_test.rb @@ -33,17 +33,11 @@ class SendFileTest < ActionController::TestCase include TestFileUtils def setup - @mime_type_defined = defined? Mime::PNG - Mime::Type.register "image/png", :png unless @mime_type_defined @controller = SendFileController.new @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new end - teardown do - Mime::Type.unregister :png unless @mime_type_defined - end - def test_file_nostream @controller.options = { :stream => false } response = nil diff --git a/actionpack/test/dispatch/mapper_test.rb b/actionpack/test/dispatch/mapper_test.rb index bf82e09f39..d8d3209dac 100644 --- a/actionpack/test/dispatch/mapper_test.rb +++ b/actionpack/test/dispatch/mapper_test.rb @@ -38,7 +38,7 @@ module ActionDispatch def test_mapping_requirements options = { :controller => 'foo', :action => 'bar', :via => :get } - m = Mapper::Mapping.new({}, '/store/:name(*rest)', options) + m = Mapper::Mapping.build({}, '/store/:name(*rest)', options) _, _, requirements, _ = m.to_route assert_equal(/.+?/, requirements[:rest]) end diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb index 981cf2426e..d29cc8473e 100644 --- a/actionpack/test/dispatch/mime_type_test.rb +++ b/actionpack/test/dispatch/mime_type_test.rb @@ -1,8 +1,6 @@ require 'abstract_unit' class MimeTypeTest < ActiveSupport::TestCase - Mime::Type.register "image/png", :png unless defined? Mime::PNG - Mime::Type.register "application/pdf", :pdf unless defined? Mime::PDF test "parse single" do Mime::LOOKUP.keys.each do |mime_type| diff --git a/actionpack/test/dispatch/reloader_test.rb b/actionpack/test/dispatch/reloader_test.rb index ce9ccfcee8..62e8197e20 100644 --- a/actionpack/test/dispatch/reloader_test.rb +++ b/actionpack/test/dispatch/reloader_test.rb @@ -3,6 +3,11 @@ require 'abstract_unit' class ReloaderTest < ActiveSupport::TestCase Reloader = ActionDispatch::Reloader + teardown do + Reloader.reset_callbacks :prepare + Reloader.reset_callbacks :cleanup + end + def test_prepare_callbacks a = b = c = nil Reloader.to_prepare { |*args| a = b = c = 1 } diff --git a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb index 2a2f92b5b3..2db3fee6bb 100644 --- a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb @@ -145,7 +145,7 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest test "does not raise EOFError on GET request with multipart content-type" do with_routing do |set| set.draw do - get ':action', to: 'multipart_params_parsing_test/test' + get ':action', controller: 'multipart_params_parsing_test/test' end headers = { "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x" } get "/parse", {}, headers @@ -174,7 +174,7 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest def with_test_routing with_routing do |set| set.draw do - post ':action', :to => 'multipart_params_parsing_test/test' + post ':action', :controller => 'multipart_params_parsing_test/test' end yield end diff --git a/actionpack/test/dispatch/request/query_string_parsing_test.rb b/actionpack/test/dispatch/request/query_string_parsing_test.rb index d82493140f..4e99c26e03 100644 --- a/actionpack/test/dispatch/request/query_string_parsing_test.rb +++ b/actionpack/test/dispatch/request/query_string_parsing_test.rb @@ -105,6 +105,7 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest end test "perform_deep_munge" do + old_perform_deep_munge = ActionDispatch::Request::Utils.perform_deep_munge ActionDispatch::Request::Utils.perform_deep_munge = false begin assert_parses({"action" => nil}, "action") @@ -115,7 +116,7 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest assert_parses({"action" => {"foo" => [{"bar" => nil}]}}, "action[foo][][bar]") assert_parses({"action" => ['1',nil]}, "action[]=1&action[]") ensure - ActionDispatch::Request::Utils.perform_deep_munge = true + ActionDispatch::Request::Utils.perform_deep_munge = old_perform_deep_munge end end diff --git a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb index 9a77454f30..1de05cbf09 100644 --- a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb @@ -130,10 +130,7 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest end test "ambiguous params returns a bad request" do - with_routing do |set| - set.draw do - post ':action', to: ::UrlEncodedParamsParsingTest::TestController - end + with_test_routing do post "/parse", "foo[]=bar&foo[4]=bar" assert_response :bad_request end diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index b48e8ab974..e4950a5d6b 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -805,6 +805,7 @@ class RequestFormat < BaseRequestTest end test "ignore_accept_header" do + old_ignore_accept_header = ActionDispatch::Request.ignore_accept_header ActionDispatch::Request.ignore_accept_header = true begin @@ -834,7 +835,7 @@ class RequestFormat < BaseRequestTest request.expects(:parameters).at_least_once.returns({:format => :json}) assert_equal [ Mime::JSON ], request.formats ensure - ActionDispatch::Request.ignore_accept_header = false + ActionDispatch::Request.ignore_accept_header = old_ignore_accept_header end end end diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index 959a3bc5cd..187b9a2420 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -178,6 +178,7 @@ class ResponseTest < ActiveSupport::TestCase end test "read x_frame_options, x_content_type_options and x_xss_protection" do + original_default_headers = ActionDispatch::Response.default_headers begin ActionDispatch::Response.default_headers = { 'X-Frame-Options' => 'DENY', @@ -193,11 +194,12 @@ class ResponseTest < ActiveSupport::TestCase assert_equal('nosniff', resp.headers['X-Content-Type-Options']) assert_equal('1;', resp.headers['X-XSS-Protection']) ensure - ActionDispatch::Response.default_headers = nil + ActionDispatch::Response.default_headers = original_default_headers end end test "read custom default_header" do + original_default_headers = ActionDispatch::Response.default_headers begin ActionDispatch::Response.default_headers = { 'X-XX-XXXX' => 'Here is my phone number' @@ -209,7 +211,7 @@ class ResponseTest < ActiveSupport::TestCase assert_equal('Here is my phone number', resp.headers['X-XX-XXXX']) ensure - ActionDispatch::Response.default_headers = nil + ActionDispatch::Response.default_headers = original_default_headers end end diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index d6477e19bb..778dbfc74d 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -361,8 +361,8 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest draw do controller(:global) do get 'global/hide_notice' - get 'global/export', :to => :export, :as => :export_request - get '/export/:id/:file', :to => :export, :as => :export_download, :constraints => { :file => /.*/ } + get 'global/export', :action => :export, :as => :export_request + get '/export/:id/:file', :action => :export, :as => :export_download, :constraints => { :file => /.*/ } get 'global/:action' end end @@ -730,8 +730,8 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest draw do resources :replies do member do - put :answer, :to => :mark_as_answer - delete :answer, :to => :unmark_as_answer + put :answer, :action => :mark_as_answer + delete :answer, :action => :unmark_as_answer end end end @@ -1188,7 +1188,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest controller :articles do scope '/articles', :as => 'article' do scope :path => '/:title', :title => /[a-z]+/, :as => :with_title do - get '/:id', :to => :with_id, :as => "" + get '/:id', :action => :with_id, :as => "" end end end @@ -1435,7 +1435,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest def test_scoped_controller_with_namespace_and_action draw do namespace :account do - get ':action/callback', :action => /twitter|github/, :to => "callbacks", :as => :callback + get ':action/callback', :action => /twitter|github/, :controller => "callbacks", :as => :callback end end @@ -1492,7 +1492,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest def test_normalize_namespaced_matches draw do namespace :account do - get 'description', :to => :description, :as => "description" + get 'description', :action => :description, :as => "description" end end @@ -2154,7 +2154,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end resources :invoices do get "outstanding" => "invoices#outstanding", :on => :collection - get "overdue", :to => :overdue, :on => :collection + get "overdue", :action => :overdue, :on => :collection get "print" => "invoices#print", :as => :print, :on => :member post "preview" => "invoices#preview", :as => :preview, :on => :new end @@ -2242,6 +2242,22 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal '/api/1.0/users/first.last.xml', api_user_path(:version => '1.0', :id => 'first.last', :format => :xml) end + def test_match_without_via + assert_raises(ArgumentError) do + draw do + match '/foo/bar', :to => 'files#show' + end + end + end + + def test_match_with_empty_via + assert_raises(ArgumentError) do + draw do + match '/foo/bar', :to => 'files#show', :via => [] + end + end + end + def test_glob_parameter_accepts_regexp draw do get '/:locale/*file.:format', :to => 'files#show', :file => /path\/to\/existing\/file/ @@ -2980,7 +2996,9 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end assert_raise(ArgumentError) do - draw { controller("/feeds") { get '/feeds/:service', :to => :show } } + assert_deprecated do + draw { controller("/feeds") { get '/feeds/:service', :to => :show } } + end end assert_raise(ArgumentError) do @@ -3239,6 +3257,58 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal '/admin/posts/1/comments', admin_post_comments_path('1') end + def test_mix_string_to_controller_action + draw do + get '/projects', controller: 'project_files', + action: 'index', + to: 'comments#index' + end + get '/projects' + assert_equal 'comments#index', @response.body + end + + def test_mix_string_to_controller + draw do + get '/projects', controller: 'project_files', + to: 'comments#index' + end + get '/projects' + assert_equal 'comments#index', @response.body + end + + def test_mix_string_to_action + draw do + get '/projects', action: 'index', + to: 'comments#index' + end + get '/projects' + assert_equal 'comments#index', @response.body + end + + def test_mix_symbol_to_controller_action + assert_deprecated do + draw do + get '/projects', controller: 'project_files', + action: 'index', + to: :show + end + end + get '/projects' + assert_equal 'project_files#show', @response.body + end + + def test_mix_string_to_controller_action_no_hash + assert_deprecated do + draw do + get '/projects', controller: 'project_files', + action: 'index', + to: 'show' + end + end + get '/projects' + assert_equal 'show#index', @response.body + end + def test_shallow_path_and_prefix_are_not_added_to_non_shallow_routes draw do scope shallow_path: 'projects', shallow_prefix: 'project' do @@ -3503,7 +3573,7 @@ class TestNamespaceWithControllerOption < ActionDispatch::IntegrationTest def test_missing_controller ex = assert_raises(ArgumentError) { draw do - get '/foo/bar', :to => :index + get '/foo/bar', :action => :index end } assert_match(/Missing :controller/, ex.message) @@ -3511,8 +3581,10 @@ class TestNamespaceWithControllerOption < ActionDispatch::IntegrationTest def test_missing_action ex = assert_raises(ArgumentError) { - draw do - get '/foo/bar', :to => 'foo' + assert_deprecated do + draw do + get '/foo/bar', :to => 'foo' + end end } assert_match(/Missing :action/, ex.message) @@ -4019,7 +4091,7 @@ class TestInvalidUrls < ActionDispatch::IntegrationTest set.draw do get "/bar/:id", :to => redirect("/foo/show/%{id}") get "/foo/show(/:id)", :to => "test_invalid_urls/foo#show" - get "/foo(/:action(/:id))", :to => "test_invalid_urls/foo" + get "/foo(/:action(/:id))", :controller => "test_invalid_urls/foo" get "/:controller(/:action(/:id))" end diff --git a/actionpack/test/journey/router_test.rb b/actionpack/test/journey/router_test.rb index e092432b01..2e7e8e1bea 100644 --- a/actionpack/test/journey/router_test.rb +++ b/actionpack/test/journey/router_test.rb @@ -213,8 +213,6 @@ module ActionDispatch route_set = Routing::RouteSet.new mapper = Routing::Mapper.new route_set - strexp = Router::Strexp.build("/", {}, ['/', '.', '?'], false) - path = Path::Pattern.new strexp app = lambda { |env| [200, {}, ['success!']] } mapper.get '/weblog', :to => app diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 147e5b47db..d13cc0c7a7 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,13 @@ +* Add `ActionView::Template::Handler.unregister_template_handler`. + + It performs the opposite of `ActionView::Template::Handler.register_template_handler`. + + *Zuhao Wan* + +* Bring `cache_digest` rake tasks up-to-date with the latest API changes + + *Jiri Pospisil* + * Allow custom `:host` option to be passed to `asset_url` helper that overwrites `config.action_controller.asset_host` for particular asset. diff --git a/actionview/README.rdoc b/actionview/README.rdoc index 35f805346c..5bb62c7562 100644 --- a/actionview/README.rdoc +++ b/actionview/README.rdoc @@ -29,6 +29,11 @@ API documentation is at * http://api.rubyonrails.org -Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here: +Bug reports can be filed for the Ruby on Rails project here: * https://github.com/rails/rails/issues + +Feature requests should be discussed on the rails-core mailing list here: + +* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core + diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb index 455ce531ae..8235b30599 100644 --- a/actionview/lib/action_view/base.rb +++ b/actionview/lib/action_view/base.rb @@ -130,8 +130,6 @@ module ActionView #:nodoc: # end # end # end - # - # More builder documentation can be found at http://builder.rubyforge.org. class Base include Helpers, ::ERB::Util, Context diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index f53cce32b0..7333ea999a 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -11,7 +11,7 @@ module ActionView # the assets exist before linking to them: # # image_tag("rails.png") - # # => <img alt="Rails" src="/images/rails.png" /> + # # => <img alt="Rails" src="/assets/rails.png" /> # stylesheet_link_tag("application") # # => <link href="/assets/application.css?body=1" media="screen" rel="stylesheet" /> module AssetTagHelper @@ -20,8 +20,7 @@ module ActionView include AssetUrlHelper include TagHelper - # Returns an HTML script tag for each of the +sources+ provided. If - # you don't specify an extension, <tt>.js</tt> will be appended automatically. + # Returns an HTML script tag for each of the +sources+ provided. # # Sources may be paths to JavaScript files. Relative paths are assumed to be relative # to <tt>assets/javascripts</tt>, full paths are assumed to be relative to the document @@ -34,19 +33,19 @@ module ActionView # last argument. # # When the Asset Pipeline is enabled, you can pass the name of your manifest as - # source and include other JavaScript or CoffeeScript files inside the manifest. + # source, and include other JavaScript or CoffeeScript files inside the manifest. # # javascript_include_tag "xmlhr" - # # => <script src="/javascripts/xmlhr.js?1284139606"></script> + # # => <script src="/assets/xmlhr.js?1284139606"></script> # # javascript_include_tag "template.jst", extname: false - # # => <script src="/javascripts/template.jst?1284139606"></script> + # # => <script src="/assets/template.jst?1284139606"></script> # # javascript_include_tag "xmlhr.js" - # # => <script src="/javascripts/xmlhr.js?1284139606"></script> + # # => <script src="/assets/xmlhr.js?1284139606"></script> # # javascript_include_tag "common.javascript", "/elsewhere/cools" - # # => <script src="/javascripts/common.javascript?1284139606"></script> + # # => <script src="/assets/common.javascript?1284139606"></script> # # <script src="/elsewhere/cools.js?1423139606"></script> # # javascript_include_tag "http://www.example.com/xmlhr" @@ -73,22 +72,22 @@ module ActionView # apply to all media types. # # stylesheet_link_tag "style" - # # => <link href="/stylesheets/style.css" media="screen" rel="stylesheet" /> + # # => <link href="/assets/style.css" media="screen" rel="stylesheet" /> # # stylesheet_link_tag "style.css" - # # => <link href="/stylesheets/style.css" media="screen" rel="stylesheet" /> + # # => <link href="/assets/style.css" media="screen" rel="stylesheet" /> # # stylesheet_link_tag "http://www.example.com/style.css" # # => <link href="http://www.example.com/style.css" media="screen" rel="stylesheet" /> # # stylesheet_link_tag "style", media: "all" - # # => <link href="/stylesheets/style.css" media="all" rel="stylesheet" /> + # # => <link href="/assets/style.css" media="all" rel="stylesheet" /> # # stylesheet_link_tag "style", media: "print" - # # => <link href="/stylesheets/style.css" media="print" rel="stylesheet" /> + # # => <link href="/assets/style.css" media="print" rel="stylesheet" /> # # stylesheet_link_tag "random.styles", "/css/stylish" - # # => <link href="/stylesheets/random.styles" media="screen" rel="stylesheet" /> + # # => <link href="/assets/random.styles" media="screen" rel="stylesheet" /> # # <link href="/css/stylish.css" media="screen" rel="stylesheet" /> def stylesheet_link_tag(*sources) options = sources.extract_options!.stringify_keys @@ -159,17 +158,17 @@ module ActionView # respectively: # # favicon_link_tag - # # => <link href="/images/favicon.ico" rel="shortcut icon" type="image/x-icon" /> + # # => <link href="/assets/favicon.ico" rel="shortcut icon" type="image/x-icon" /> # # favicon_link_tag 'myicon.ico' - # # => <link href="/images/myicon.ico" rel="shortcut icon" type="image/x-icon" /> + # # => <link href="/assets/myicon.ico" rel="shortcut icon" type="image/x-icon" /> # # Mobile Safari looks for a different link tag, pointing to an image that # will be used if you add the page to the home screen of an iOS device. # The following call would generate such a tag: # # favicon_link_tag 'mb-icon.png', rel: 'apple-touch-icon', type: 'image/png' - # # => <link href="/images/mb-icon.png" rel="apple-touch-icon" type="image/png" /> + # # => <link href="/assets/mb-icon.png" rel="apple-touch-icon" type="image/png" /> def favicon_link_tag(source='favicon.ico', options={}) tag('link', { :rel => 'shortcut icon', @@ -265,7 +264,7 @@ module ActionView # video_tag("trailer.ogg", controls: true, autobuffer: true) # # => <video autobuffer="autobuffer" controls="controls" src="/videos/trailer.ogg" ></video> # video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png") - # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/images/screenshot.png"></video> + # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/assets/screenshot.png"></video> # video_tag("/trailers/hd.avi", size: "16x16") # # => <video src="/trailers/hd.avi" width="16" height="16"></video> # video_tag("/trailers/hd.avi", size: "16") diff --git a/actionview/lib/action_view/helpers/capture_helper.rb b/actionview/lib/action_view/helpers/capture_helper.rb index 5afe435459..75d1634b2e 100644 --- a/actionview/lib/action_view/helpers/capture_helper.rb +++ b/actionview/lib/action_view/helpers/capture_helper.rb @@ -202,15 +202,6 @@ module ActionView ensure self.output_buffer = old_buffer end - - # Add the output buffer to the response body and start a new one. - def flush_output_buffer #:nodoc: - if output_buffer && !output_buffer.empty? - response.stream.write output_buffer - self.output_buffer = output_buffer.respond_to?(:clone_empty) ? output_buffer.clone_empty : output_buffer[0, 0] - nil - end - end end end end diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 180c4a62bf..789a413c8d 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -434,7 +434,8 @@ module ActionView output = capture(builder, &block) html_options[:multipart] ||= builder.multipart? - form_tag(options[:url] || {}, html_options) { output } + html_options = html_options_for_form(options[:url] || {}, html_options) + form_tag_with_body(html_options, output) end def apply_form_for_options!(record, object, options) #:nodoc: diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb index 1e818083cc..88b8400644 100644 --- a/actionview/lib/action_view/helpers/form_tag_helper.rb +++ b/actionview/lib/action_view/helpers/form_tag_helper.rb @@ -67,7 +67,7 @@ module ActionView def form_tag(url_for_options = {}, options = {}, &block) html_options = html_options_for_form(url_for_options, options) if block_given? - form_tag_in_block(html_options, &block) + form_tag_with_body(html_options, capture(&block)) else form_tag_html(html_options) end @@ -82,7 +82,7 @@ module ActionView # ==== Options # * <tt>:multiple</tt> - If set to true the selection will allow multiple choices. # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input. - # * <tt>:include_blank</tt> - If set to true, an empty option will be created. + # * <tt>:include_blank</tt> - If set to true, an empty option will be created. If set to a string, the string will be used as the option's content and the value will be empty. # * <tt>:prompt</tt> - Create a prompt option with blank value and the text asking user to select something. # * <tt>:selected</tt> - Provide a default selected value. It should be of the exact type as the provided options. # * Any other key creates standard HTML attributes for the tag. @@ -116,6 +116,9 @@ module ActionView # select_tag "people", options_from_collection_for_select(@people, "id", "name"), include_blank: true # # => <select id="people" name="people"><option value=""></option><option value="1">David</option></select> # + # select_tag "people", options_from_collection_for_select(@people, "id", "name"), include_blank: "All" + # # => <select id="people" name="people"><option value="">All</option><option value="1">David</option></select> + # # select_tag "people", options_from_collection_for_select(@people, "id", "name"), prompt: "Select something" # # => <select id="people" name="people"><option value="">Select something</option><option value="1">David</option></select> # @@ -848,8 +851,7 @@ module ActionView tag(:form, html_options, true) + extra_tags end - def form_tag_in_block(html_options, &block) - content = capture(&block) + def form_tag_with_body(html_options, content) output = form_tag_html(html_options) output << content output.safe_concat("</form>") diff --git a/actionview/lib/action_view/helpers/javascript_helper.rb b/actionview/lib/action_view/helpers/javascript_helper.rb index e475d5b018..629c447f3f 100644 --- a/actionview/lib/action_view/helpers/javascript_helper.rb +++ b/actionview/lib/action_view/helpers/javascript_helper.rb @@ -47,7 +47,13 @@ module ActionView # tag. # # javascript_tag "alert('All is good')", defer: 'defer' - # # => <script defer="defer">alert('All is good')</script> + # + # Returns: + # <script defer="defer"> + # //<![CDATA[ + # alert('All is good') + # //]]> + # </script> # # Instead of passing the content as an argument, you can also use a block # in which case, you pass your +html_options+ as the first parameter. diff --git a/actionview/lib/action_view/helpers/number_helper.rb b/actionview/lib/action_view/helpers/number_helper.rb index 7157a95146..7220bded3c 100644 --- a/actionview/lib/action_view/helpers/number_helper.rb +++ b/actionview/lib/action_view/helpers/number_helper.rb @@ -266,14 +266,8 @@ module ActionView # number_to_human_size(1234567, precision: 2) # => 1.2 MB # number_to_human_size(483989, precision: 2) # => 470 KB # number_to_human_size(1234567, precision: 2, separator: ',') # => 1,2 MB - # - # Non-significant zeros after the fractional separator are - # stripped out by default (set - # <tt>:strip_insignificant_zeros</tt> to +false+ to change - # that): - # - # number_to_human_size(1234567890123, precision: 5) # => "1.1229 TB" - # number_to_human_size(524288000, precision: 5) # => "500 MB" + # number_to_human_size(1234567890123, precision: 5) # => "1.1228 TB" + # number_to_human_size(524288000, precision: 5) # => "500 MB" def number_to_human_size(number, options = {}) delegate_number_helper_method(:number_to_human_size, number, options) end @@ -343,11 +337,15 @@ module ActionView # separator: ',', # significant: false) # => "1,2 Million" # + # number_to_human(500000000, precision: 5) # => "500 Million" + # number_to_human(12345012345, significant: false) # => "12.345 Billion" + # # Non-significant zeros after the decimal separator are stripped # out by default (set <tt>:strip_insignificant_zeros</tt> to # +false+ to change that): - # number_to_human(12345012345, significant_digits: 6) # => "12.345 Billion" - # number_to_human(500000000, precision: 5) # => "500 Million" + # + # number_to_human(12.00001) # => "12" + # number_to_human(12.00001, strip_insignificant_zeros: false) # => "12.0" # # ==== Custom Unit Quantifiers # diff --git a/actionview/lib/action_view/helpers/output_safety_helper.rb b/actionview/lib/action_view/helpers/output_safety_helper.rb index 60a4478c26..e1f40011c0 100644 --- a/actionview/lib/action_view/helpers/output_safety_helper.rb +++ b/actionview/lib/action_view/helpers/output_safety_helper.rb @@ -29,9 +29,9 @@ module ActionView #:nodoc: # # => "<p>foo</p><br /><p>bar</p>" # def safe_join(array, sep=$,) - sep = ERB::Util.html_escape(sep) + sep = ERB::Util.unwrapped_html_escape(sep) - array.map { |i| ERB::Util.html_escape(i) }.join(sep).html_safe + array.map { |i| ERB::Util.unwrapped_html_escape(i) }.join(sep).html_safe end end end diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb index a9f3b0ffbc..9b9ca7d60d 100644 --- a/actionview/lib/action_view/helpers/tag_helper.rb +++ b/actionview/lib/action_view/helpers/tag_helper.rb @@ -139,7 +139,7 @@ module ActionView def content_tag_string(name, content, options, escape = true) tag_options = tag_options(options, escape) if options - content = ERB::Util.h(content) if escape + content = ERB::Util.unwrapped_html_escape(content) if escape "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name.to_sym]}#{content}</#{name}>".html_safe end @@ -174,7 +174,7 @@ module ActionView def tag_option(key, value, escape) value = value.join(" ") if value.is_a?(Array) - value = ERB::Util.h(value) if escape + value = ERB::Util.unwrapped_html_escape(value) if escape %(#{key}="#{value}") end end diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index 894616a449..9a9777317a 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -462,7 +462,7 @@ module ActionView # <strong>Email me:</strong> <span>me@domain.com</span> # </a> def mail_to(email_address, name = nil, html_options = {}, &block) - email_address = ERB::Util.html_escape(email_address) + email_address = ERB::Util.unwrapped_html_escape(email_address) html_options, name = name, nil if block_given? html_options = (html_options || {}).stringify_keys @@ -471,7 +471,7 @@ module ActionView option = html_options.delete(item) || next "#{item}=#{Rack::Utils.escape_path(option)}" }.compact - extras = extras.empty? ? '' : '?' + ERB::Util.html_escape(extras.join('&')) + extras = extras.empty? ? '' : '?' + ERB::Util.unwrapped_html_escape(extras.join('&')) html_options["href"] = "mailto:#{email_address}#{extras}".html_safe diff --git a/actionview/lib/action_view/tasks/dependencies.rake b/actionview/lib/action_view/tasks/dependencies.rake index 1b9426c0e5..b39f7d583b 100644 --- a/actionview/lib/action_view/tasks/dependencies.rake +++ b/actionview/lib/action_view/tasks/dependencies.rake @@ -2,16 +2,20 @@ namespace :cache_digests do desc 'Lookup nested dependencies for TEMPLATE (like messages/show or comments/_comment.html)' task :nested_dependencies => :environment do abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present? - template, format = ENV['TEMPLATE'].split(".") - format ||= :html - puts JSON.pretty_generate ActionView::Digestor.new(template, format, ApplicationController.new.lookup_context).nested_dependencies + puts JSON.pretty_generate ActionView::Digestor.new(name: template_name, finder: finder).nested_dependencies end desc 'Lookup first-level dependencies for TEMPLATE (like messages/show or comments/_comment.html)' task :dependencies => :environment do abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present? - template, format = ENV['TEMPLATE'].split(".") - format ||= :html - puts JSON.pretty_generate ActionView::Digestor.new(template, format, ApplicationController.new.lookup_context).dependencies + puts JSON.pretty_generate ActionView::Digestor.new(name: template_name, finder: finder).dependencies + end + + def template_name + ENV['TEMPLATE'].split('.', 2).first + end + + def finder + ApplicationController.new.lookup_context end end diff --git a/actionview/lib/action_view/template/handlers.rb b/actionview/lib/action_view/template/handlers.rb index d9cddc0040..33bfcb458c 100644 --- a/actionview/lib/action_view/template/handlers.rb +++ b/actionview/lib/action_view/template/handlers.rb @@ -32,6 +32,15 @@ module ActionView #:nodoc: @@template_extensions = nil end + # Opposite to register_template_handler. + def unregister_template_handler(*extensions) + extensions.each do |extension| + handler = @@template_handlers.delete extension.to_sym + @@default_template_handlers = nil if @@default_template_handlers == handler + end + @@template_extensions = nil + end + def template_handler_extensions @@template_handlers.keys.map {|key| key.to_s }.sort end diff --git a/actionview/test/actionpack/controller/layout_test.rb b/actionview/test/actionpack/controller/layout_test.rb index b44f57a23e..bd345fe873 100644 --- a/actionview/test/actionpack/controller/layout_test.rb +++ b/actionview/test/actionpack/controller/layout_test.rb @@ -6,9 +6,6 @@ require 'active_support/core_ext/array/extract_options' # method has access to the view_paths array when looking for a layout to automatically assign. old_load_paths = ActionController::Base.view_paths -ActionView::Template::register_template_handler :mab, - lambda { |template| template.source.inspect } - ActionController::Base.view_paths = [ File.dirname(__FILE__) + '/../../fixtures/actionpack/layout_tests/' ] class LayoutTest < ActionController::Base @@ -17,6 +14,15 @@ class LayoutTest < ActionController::Base self.view_paths = ActionController::Base.view_paths.dup end +module TemplateHandlerHelper + def with_template_handler(*extensions, handler) + ActionView::Template.register_template_handler(*extensions, handler) + yield + ensure + ActionView::Template.unregister_template_handler(*extensions) + end +end + # Restore view_paths to previous value ActionController::Base.view_paths = old_load_paths @@ -39,6 +45,8 @@ class MultipleExtensions < LayoutTest end class LayoutAutoDiscoveryTest < ActionController::TestCase + include TemplateHandlerHelper + def setup super @request.host = "www.nextangle.com" @@ -57,10 +65,12 @@ class LayoutAutoDiscoveryTest < ActionController::TestCase end def test_third_party_template_library_auto_discovers_layout - @controller = ThirdPartyTemplateLibraryController.new - get :hello - assert_response :success - assert_equal 'layouts/third_party_template_library.mab', @response.body + with_template_handler :mab, lambda { |template| template.source.inspect } do + @controller = ThirdPartyTemplateLibraryController.new + get :hello + assert_response :success + assert_equal 'layouts/third_party_template_library.mab', @response.body + end end def test_namespaced_controllers_auto_detect_layouts1 @@ -135,6 +145,7 @@ end class LayoutSetInResponseTest < ActionController::TestCase include ActionView::Template::Handlers + include TemplateHandlerHelper def test_layout_set_when_using_default_layout @controller = DefaultLayoutController.new @@ -191,9 +202,11 @@ class LayoutSetInResponseTest < ActionController::TestCase end def test_layout_set_when_using_render - @controller = SetsLayoutInRenderController.new - get :hello - assert_template :layout => "layouts/third_party_template_library" + with_template_handler :mab, lambda { |template| template.source.inspect } do + @controller = SetsLayoutInRenderController.new + get :hello + assert_template :layout => "layouts/third_party_template_library" + end end def test_layout_is_not_set_when_none_rendered diff --git a/actionview/test/template/capture_helper_test.rb b/actionview/test/template/capture_helper_test.rb index 938f1c3e54..f213da5934 100644 --- a/actionview/test/template/capture_helper_test.rb +++ b/actionview/test/template/capture_helper_test.rb @@ -207,29 +207,6 @@ class CaptureHelperTest < ActionView::TestCase assert_equal "", @av.with_output_buffer {} end - def test_flush_output_buffer_concats_output_buffer_to_response - view = view_with_controller - assert_equal [], view.response.body_parts - - view.output_buffer << 'OMG' - view.flush_output_buffer - assert_equal ['OMG'], view.response.body_parts - assert_equal '', view.output_buffer - - view.output_buffer << 'foobar' - view.flush_output_buffer - assert_equal ['OMG', 'foobar'], view.response.body_parts - assert_equal '', view.output_buffer - end - - def test_flush_output_buffer_preserves_the_encoding_of_the_output_buffer - view = view_with_controller - alt_encoding = alt_encoding(view.output_buffer) - view.output_buffer.force_encoding(alt_encoding) - flush_output_buffer - assert_equal alt_encoding, view.output_buffer.encoding - end - def alt_encoding(output_buffer) output_buffer.encoding == Encoding::US_ASCII ? Encoding::UTF_8 : Encoding::US_ASCII end diff --git a/actionview/test/template/dependency_tracker_test.rb b/actionview/test/template/dependency_tracker_test.rb index df3a0602d1..6c780f2297 100644 --- a/actionview/test/template/dependency_tracker_test.rb +++ b/actionview/test/template/dependency_tracker_test.rb @@ -31,6 +31,7 @@ class DependencyTrackerTest < ActionView::TestCase end def teardown + ActionView::Template.unregister_template_handler :neckbeard tracker.remove_tracker(:neckbeard) end diff --git a/actionview/test/template/number_helper_test.rb b/actionview/test/template/number_helper_test.rb index adb888319d..0495224d04 100644 --- a/actionview/test/template/number_helper_test.rb +++ b/actionview/test/template/number_helper_test.rb @@ -48,6 +48,7 @@ class NumberHelperTest < ActionView::TestCase assert_equal "-111.235", number_with_precision(-111.2346) assert_equal "111.00", number_with_precision(111, precision: 2) assert_equal "0.00100", number_with_precision(0.001, precision: 5) + assert_equal "3.33", number_with_precision(Rational(10, 3), precision: 2) end def test_number_to_human_size diff --git a/actionview/test/template/output_buffer_test.rb b/actionview/test/template/output_buffer_test.rb deleted file mode 100644 index eb0df3d1ab..0000000000 --- a/actionview/test/template/output_buffer_test.rb +++ /dev/null @@ -1,59 +0,0 @@ -require 'abstract_unit' - -class OutputBufferTest < ActionController::TestCase - class TestController < ActionController::Base - def index - render :text => 'foo' - end - end - - tests TestController - - def setup - @vc = @controller.view_context - get :index - assert_equal ['foo'], body_parts - end - - test 'output buffer is nil after rendering' do - assert_nil output_buffer - end - - test 'flushing ignores nil output buffer' do - @controller.view_context.flush_output_buffer - assert_nil output_buffer - assert_equal ['foo'], body_parts - end - - test 'flushing ignores empty output buffer' do - @vc.output_buffer = '' - @vc.flush_output_buffer - assert_equal '', output_buffer - assert_equal ['foo'], body_parts - end - - test 'flushing appends the output buffer to the body parts' do - @vc.output_buffer = 'bar' - @vc.flush_output_buffer - assert_equal '', output_buffer - assert_equal ['foo', 'bar'], body_parts - end - - test 'flushing preserves output buffer encoding' do - original_buffer = ' '.force_encoding(Encoding::EUC_JP) - @vc.output_buffer = original_buffer - @vc.flush_output_buffer - assert_equal ['foo', original_buffer], body_parts - assert_not_equal original_buffer, output_buffer - assert_equal Encoding::EUC_JP, output_buffer.encoding - end - - protected - def output_buffer - @vc.output_buffer - end - - def body_parts - @controller.response.body_parts - end -end diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb index ca508abfb8..240a8e3c32 100644 --- a/actionview/test/template/render_test.rb +++ b/actionview/test/template/render_test.rb @@ -369,23 +369,40 @@ module RenderTestCases def test_render_inline_with_render_from_to_proc ActionView::Template.register_template_handler :ruby_handler, :source.to_proc - assert_equal '3', @view.render(:inline => "(1 + 2).to_s", :type => :ruby_handler) + assert_equal '3', @view.render(inline: "(1 + 2).to_s", type: :ruby_handler) + ensure + ActionView::Template.unregister_template_handler :ruby_handler end def test_render_inline_with_compilable_custom_type ActionView::Template.register_template_handler :foo, CustomHandler - assert_equal 'source: "Hello, World!"', @view.render(:inline => "Hello, World!", :type => :foo) + assert_equal 'source: "Hello, World!"', @view.render(inline: "Hello, World!", type: :foo) + ensure + ActionView::Template.unregister_template_handler :foo end def test_render_inline_with_locals_and_compilable_custom_type ActionView::Template.register_template_handler :foo, CustomHandler - assert_equal 'source: "Hello, <%= name %>!"', @view.render(:inline => "Hello, <%= name %>!", :locals => { :name => "Josh" }, :type => :foo) + assert_equal 'source: "Hello, <%= name %>!"', @view.render(inline: "Hello, <%= name %>!", locals: { name: "Josh" }, type: :foo) + ensure + ActionView::Template.unregister_template_handler :foo end def test_render_knows_about_types_registered_when_extensions_are_checked_earlier_in_initialization ActionView::Template::Handlers.extensions ActionView::Template.register_template_handler :foo, CustomHandler assert ActionView::Template::Handlers.extensions.include?(:foo) + ensure + ActionView::Template.unregister_template_handler :foo + end + + def test_render_does_not_use_unregistered_extension_and_template_handler + ActionView::Template.register_template_handler :foo, CustomHandler + ActionView::Template.unregister_template_handler :foo + assert_not ActionView::Template::Handlers.extensions.include?(:foo) + assert_equal "Hello, World!", @view.render(inline: "Hello, World!", type: :foo) + ensure + ActionView::Template::Handlers.class_variable_get(:@@template_handlers).delete(:foo) end def test_render_ignores_templates_with_malformed_template_handlers @@ -474,7 +491,9 @@ module RenderTestCases def test_render_with_passing_couple_extensions_to_one_register_template_handler_function_call ActionView::Template.register_template_handler :foo1, :foo2, CustomHandler - assert_equal @view.render(:inline => "Hello, World!", :type => :foo1), @view.render(:inline => "Hello, World!", :type => :foo2) + assert_equal @view.render(inline: "Hello, World!", type: :foo1), @view.render(inline: "Hello, World!", type: :foo2) + ensure + ActionView::Template.unregister_template_handler :foo1, :foo2 end def test_render_throws_exception_when_no_extensions_passed_to_register_template_handler_function_call diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index b94558b65c..29cd4cf934 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,3 +1,9 @@ +* Remove deprecated `Validator#setup` without replacement. + + See #10716. + + *Kuldeep Aggarwal* + * Add plural and singular form for length validator's default messages. *Abd ar-Rahman Hamid* diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc index c6500f47e2..f6beff14e1 100644 --- a/activemodel/README.rdoc +++ b/activemodel/README.rdoc @@ -262,6 +262,11 @@ API documentation is at * http://api.rubyonrails.org -Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here: +Bug reports can be filed for the Ruby on Rails project here: * https://github.com/rails/rails/issues + +Feature requests should be discussed on the rails-core mailing list here: + +* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core + diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb index 374265f0d8..9c9b6f4a77 100644 --- a/activemodel/lib/active_model/conversion.rb +++ b/activemodel/lib/active_model/conversion.rb @@ -40,13 +40,15 @@ module ActiveModel self end - # Returns an Enumerable of all key attributes if any is set, regardless if + # Returns an Array of all key attributes if any is set, regardless if # the object is persisted or not. Returns +nil+ if there are no key attributes. # - # class Person < ActiveRecord::Base + # class Person + # include ActiveModel::Conversion + # attr_accessor :id # end # - # person = Person.create + # person = Person.create(id: 1) # person.to_key # => [1] def to_key key = respond_to?(:id) && id @@ -56,10 +58,15 @@ module ActiveModel # Returns a +string+ representing the object's key suitable for use in URLs, # or +nil+ if <tt>persisted?</tt> is +false+. # - # class Person < ActiveRecord::Base + # class Person + # include ActiveModel::Conversion + # attr_accessor :id + # def persisted? + # true + # end # end # - # person = Person.create + # person = Person.create(id: 1) # person.to_param # => "1" def to_param (persisted? && key = to_key) ? key.join('-') : nil diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index bddacc8c45..65cb1e5a88 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -106,7 +106,6 @@ module ActiveModel # Accepts options that will be made available through the +options+ reader. def initialize(options = {}) @options = options.except(:class).freeze - deprecated_setup(options) end # Returns the kind for this validator. @@ -122,21 +121,6 @@ module ActiveModel def validate(record) raise NotImplementedError, "Subclasses must implement a validate(record) method." end - - private - def deprecated_setup(options) # TODO: remove me in 4.2. - return unless respond_to?(:setup) - ActiveSupport::Deprecation.warn "The `Validator#setup` instance method is deprecated and will be removed on Rails 4.2. Do your setup in the constructor instead: - -class MyValidator < ActiveModel::Validator - def initialize(options={}) - super - options[:class].send :attr_accessor, :custom_attribute - end -end -" - setup(options[:class]) - end end # +EachValidator+ is a validator which iterates through the attributes given diff --git a/activemodel/test/cases/conversion_test.rb b/activemodel/test/cases/conversion_test.rb index c5cfbf909d..800cad6d9a 100644 --- a/activemodel/test/cases/conversion_test.rb +++ b/activemodel/test/cases/conversion_test.rb @@ -24,6 +24,10 @@ class ConversionTest < ActiveModel::TestCase assert_equal "1", Contact.new(id: 1).to_param end + test "to_param returns the string joined by '-'" do + assert_equal "abc-xyz", Contact.new(id: ["abc", "xyz"]).to_param + end + test "to_param returns nil if to_key is nil" do klass = Class.new(Contact) do def persisted? diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb index 6a74ee353d..4fee704ef5 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -378,25 +378,4 @@ class ValidationsTest < ActiveModel::TestCase assert topic.invalid? assert duped.valid? end - - # validator test: - def test_setup_is_deprecated_but_still_receives_klass # TODO: remove me in 4.2. - validator_class = Class.new(ActiveModel::Validator) do - def setup(klass) - @old_klass = klass - end - - def validate(*) - @old_klass == Topic or raise "#setup didn't work" - end - end - - assert_deprecated do - Topic.validates_with validator_class - end - - t = Topic.new - t.valid? - end - end diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 9a5d27ef92..efa4abfa4d 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,8 +1,89 @@ * Fixed automatic maintaining test schema to properly handle sql structure - schema format. Fixes #15394. + schema format. + + Fixes #15394. *Wojciech Wnętrzak* +* Pluck now works when selecting columns from different tables with the same + name. + + Fixes #15649 + + *Sean Griffin* + +* Remove `cache_attributes` and friends. All attributes are cached. + + *Sean Griffin* + +* Remove deprecated method `ActiveRecord::Base.quoted_locking_column`. + + *Akshay Vishnoi* + +* `ActiveRecord::FinderMethods.find` with block can handle proc parameter as + `Enumerable#find` does. + + Fixes #15382. + + *James Yang* + +* Make timezone aware attributes work with PostgreSQL array columns. + + Fixes #13402. + + *Kuldeep Aggarwal*, *Sean Griffin* + +* `ActiveRecord::SchemaMigration` has no primary key regardless of the + `primary_key_prefix_type` configuration. + + Fixes #15051. + + *JoseLuis Torres*, *Yves Senn* + +* `rake db:migrate:status` works with legacy migration numbers like `00018_xyz.rb`. + + Fixes #15538. + + *Yves Senn* + +* Baseclass becomes! subclass. + + Before this change, a record which changed its STI type, could not be + updated. + + Fixes #14785. + + *Matthew Draper*, *Earl St Sauver*, *Edo Balvers* + +* Remove deprecated `ActiveRecord::Migrator.proper_table_name`. Use the + `proper_table_name` instance method on `ActiveRecord::Migration` instead. + + *Akshay Vishnoi* + +* Fix regression on eager loading association based on SQL query rather than + existing column. + + Fixes #15480. + + *Lauro Caetano*, *Carlos Antonio da Silva* + +* Return a null column from `column_for_attribute` when no column exists. + + *Sean Griffin* + +* Implemented ActiveRecord::Base#pretty_print to work with PP. + + *Ethan* + +* Preserve type when dumping PostgreSQL point, bit, bit varying and money + columns. + + *Yves Senn* + +* New records remain new after YAML serialization. + + *Sean Griffin* + * PostgreSQL support default values for enum types. Fixes #7814. *Yves Senn* @@ -480,10 +561,6 @@ *Luke Steensen* -* Make possible to change `record_timestamps` inside Callbacks. - - *Tieg Zaharia* - * Fixed error where .persisted? throws SystemStackError for an unsaved model with a custom primary key that didn't save due to validation error. @@ -573,12 +650,11 @@ *arthurnn* -* Passing an Active Record object to `find` is now deprecated. Call `.id` - on the object first. - * Passing an Active Record object to `find` or `exists?` is now deprecated. Call `.id` on the object first. + *Aaron Patterson* + * Only use BINARY for MySQL case sensitive uniqueness check when column has a case insensitive collation. *Ryuta Kamizono* diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc index 969e97913d..f4777919d3 100644 --- a/activerecord/README.rdoc +++ b/activerecord/README.rdoc @@ -208,6 +208,11 @@ API documentation is at: * http://api.rubyonrails.org -Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here: +Bug reports can be filed for the Ruby on Rails project here: * https://github.com/rails/rails/issues + +Feature requests should be discussed on the rails-core mailing list here: + +* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core + diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 45c275a017..e576ec4d40 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -129,10 +129,10 @@ module ActiveRecord # is an instance of the value class. Specifying a custom converter allows the new value to be automatically # converted to an instance of value class if necessary. # - # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that - # should be aggregated using the NetAddr::CIDR value class (http://netaddr.rubyforge.org). The constructor - # for the value class is called +create+ and it expects a CIDR address string as a parameter. New - # values can be assigned to the value object using either another NetAddr::CIDR object, a string + # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that should be + # aggregated using the NetAddr::CIDR value class (http://www.ruby-doc.org/gems/docs/n/netaddr-1.5.0/NetAddr/CIDR.html). + # The constructor for the value class is called +create+ and it expects a CIDR address string as a parameter. + # New values can be assigned to the value object using either another NetAddr::CIDR object, a string # or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet # these requirements: # @@ -244,6 +244,10 @@ module ActiveRecord def writer_method(name, class_name, mapping, allow_nil, converter) define_method("#{name}=") do |part| klass = class_name.constantize + if part.is_a?(Hash) + part = klass.new(*part.values) + end + unless part.is_a?(klass) || converter.nil? || part.nil? part = converter.respond_to?(:call) ? converter.call(part) : klass.send(converter, part) end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 07dfc448e7..6222bfe903 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1577,7 +1577,7 @@ module ActiveRecord scope = nil end - habtm_reflection = ActiveRecord::Reflection::AssociationReflection.new(:has_and_belongs_to_many, name, scope, options, self) + habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(:has_and_belongs_to_many, name, scope, options, self) builder = Builder::HasAndBelongsToMany.new name, self, options diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 4a04303fb8..f1c36cd047 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -179,7 +179,7 @@ module ActiveRecord def creation_attributes attributes = {} - if (reflection.macro == :has_one || reflection.macro == :has_many) && !options[:through] + if (reflection.has_one? || reflection.collection?) && !options[:through] attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key] if reflection.options[:as] diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 572f556999..519d4d8651 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -105,18 +105,9 @@ module ActiveRecord chain.each_with_index do |reflection, i| table, foreign_table = tables.shift, tables.first - if reflection.source_macro == :belongs_to - if reflection.options[:polymorphic] - key = reflection.association_primary_key(assoc_klass) - else - key = reflection.association_primary_key - end - - foreign_key = reflection.foreign_key - else - key = reflection.foreign_key - foreign_key = reflection.active_record_primary_key - end + join_keys = reflection.join_keys(assoc_klass) + key = join_keys.key + foreign_key = join_keys.foreign_key if reflection == chain.last bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key], tracker diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 1edd4fa3aa..81fdd681de 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -92,7 +92,7 @@ module ActiveRecord # has_one associations. def invertible_for?(record) inverse = inverse_reflection_for(record) - inverse && inverse.macro == :has_one + inverse && inverse.has_one? end def target_id diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index f359efd496..a1f4f51664 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -5,7 +5,7 @@ module ActiveRecord::Associations::Builder end def valid_options - valid = super + [:order, :as] + valid = super + [:as] valid += [:through, :source, :source_type] if options[:through] valid end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index c5f7bcae7d..306588ac66 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -57,7 +57,7 @@ module ActiveRecord def ids_writer(ids) pk_column = reflection.primary_key_column ids = Array(ids).reject { |id| id.blank? } - ids.map! { |i| pk_column.type_cast(i) } + ids.map! { |i| pk_column.type_cast_from_user(i) } replace(klass.find(ids).index_by { |r| r.id }.values_at(*ids)) 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 35ad512537..da9b125fd6 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -89,7 +89,7 @@ module ActiveRecord end def through_scope_attributes - scope.where_values_hash(through_association.reflection.name.to_s) + scope.where_values_hash(through_association.reflection.name.to_s).except!(through_association.reflection.foreign_key) end def save_through_record(record) @@ -105,9 +105,9 @@ module ActiveRecord inverse = source_reflection.inverse_of if inverse - if inverse.macro == :has_many + if inverse.collection? record.send(inverse.name) << build_through_record(record) - elsif inverse.macro == :has_one + elsif inverse.has_one? record.send("#{inverse.name}=", build_through_record(record)) end end @@ -116,7 +116,7 @@ module ActiveRecord end def target_reflection_has_associated_record? - !(through_reflection.macro == :belongs_to && owner[through_reflection.foreign_key].blank?) + !(through_reflection.belongs_to? && owner[through_reflection.foreign_key].blank?) end def update_through_counter?(method) @@ -170,7 +170,7 @@ module ActiveRecord klass.decrement_counter counter, records.map(&:id) end - if through_reflection.macro == :has_many && update_through_counter?(method) + if through_reflection.collection? && update_through_counter?(method) update_counter(-count, through_reflection) end @@ -187,7 +187,7 @@ module ActiveRecord records.each do |record| through_records = through_records_for(record) - if through_reflection.macro == :has_many + if through_reflection.collection? through_records.each { |r| through_association.target.delete(r) } else if through_records.include?(through_association.target) diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 01173b68f3..fbb4551b22 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -144,7 +144,7 @@ module ActiveRecord column_aliases = aliases.column_aliases join_root result_set.each { |row_hash| - primary_id = type_caster.type_cast row_hash[primary_key] + primary_id = type_caster.type_cast_from_database row_hash[primary_key] parent = parents[primary_id] ||= join_root.instantiate(row_hash, column_aliases) construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases) } @@ -217,7 +217,7 @@ module ActiveRecord reflection.check_validity! reflection.check_eager_loadable! - if reflection.options[:polymorphic] + if reflection.polymorphic? raise EagerLoadPolymorphicError.new(reflection) end diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 63773bd5e1..33c8619359 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -104,11 +104,11 @@ module ActiveRecord end def association_key_type - @klass.column_types[association_key_name.to_s].type + @klass.column_for_attribute(association_key_name).type end def owner_key_type - @model.column_types[owner_key_name.to_s].type + @model.column_for_attribute(owner_key_name).type end def load_slices(slices) diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index f8a85b8a6f..fcf3b219d4 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -63,14 +63,13 @@ module ActiveRecord # Note: this does not capture all cases, for example it would be crazy to try to # properly support stale-checking for nested associations. def stale_state - if through_reflection.macro == :belongs_to + if through_reflection.belongs_to? owner[through_reflection.foreign_key] && owner[through_reflection.foreign_key].to_s end end def foreign_key_present? - through_reflection.macro == :belongs_to && - !owner[through_reflection.foreign_key].nil? + through_reflection.belongs_to? && !owner[through_reflection.foreign_key].nil? end def ensure_mutable diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index c4cf084a04..40e2918777 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -126,8 +126,8 @@ module ActiveRecord def read_value return if values.values.compact.empty? - @column = object.class.reflect_on_aggregation(name.to_sym) || object.column_for_attribute(name) - klass = column.klass + @column = object.column_for_attribute(name) + klass = column ? column.klass : nil if klass == Time read_time @@ -186,8 +186,7 @@ module ActiveRecord positions = (1..max_position) validate_required_parameters!(positions) - set_values = values.values_at(*positions) - klass.new(*set_values) + values.slice(*positions) end # Checks whether some blank date parameter exists. Note that this is different diff --git a/activerecord/lib/active_record/attribute_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb new file mode 100644 index 0000000000..596161f81d --- /dev/null +++ b/activerecord/lib/active_record/attribute_decorators.rb @@ -0,0 +1,34 @@ +module ActiveRecord + module AttributeDecorators # :nodoc: + extend ActiveSupport::Concern + + included do + class_attribute :attribute_type_decorations, instance_accessor: false # :internal: + self.attribute_type_decorations = Hash.new({}) + end + + module ClassMethods + def decorate_attribute_type(column_name, decorator_name, &block) + clear_caches_calculated_from_columns + column_name = column_name.to_s + + # Create new hashes so we don't modify parent classes + decorations_for_column = attribute_type_decorations[column_name] + new_decorations = decorations_for_column.merge(decorator_name.to_s => block) + self.attribute_type_decorations = attribute_type_decorations.merge(column_name => new_decorations) + end + + private + + def add_user_provided_columns(*) + super.map do |column| + decorations = attribute_type_decorations[column.name].values + decorated_type = decorations.inject(column.cast_type) do |type, block| + block.call(type) + end + column.with_type(decorated_type) + end + end + end + end +end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index fb6473b7c7..e626227e7e 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -18,6 +18,8 @@ module ActiveRecord include TimeZoneConversion include Dirty include Serialization + + delegate :column_for_attribute, to: :class end AttrNames = Module.new { @@ -192,6 +194,26 @@ module ActiveRecord [] end end + + # Returns the column object for the named attribute. + # Returns a +ActiveRecord::ConnectionAdapters::NullColumn+ if the + # named attribute does not exist. + # + # class Person < ActiveRecord::Base + # end + # + # person = Person.new + # person.column_for_attribute(:name) # the result depends on the ConnectionAdapter + # # => #<ActiveRecord::ConnectionAdapters::SQLite3Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...> + # + # person.column_for_attribute(:nothing) + # # => #<ActiveRecord::ConnectionAdapters::NullColumn:0xXXX @name=nil, @sql_type=nil, @cast_type=#<Type::Value>, ...> + def column_for_attribute(name) + name = name.to_s + columns_hash.fetch(name) do + ConnectionAdapters::NullColumn.new(name) + end + end end # If we haven't generated any methods yet, generate them, then @@ -287,11 +309,6 @@ module ActiveRecord } end - # Placeholder so it can be overriden when needed by serialization - def attributes_for_coder # :nodoc: - attributes - end - # Returns an <tt>#inspect</tt>-like string for the value of the # attribute +attr_name+. String attributes are truncated upto 50 # characters, Date and Time attributes are returned in the @@ -344,23 +361,6 @@ module ActiveRecord !value.nil? && !(value.respond_to?(:empty?) && value.empty?) end - # Returns the column object for the named attribute. Returns +nil+ if the - # named attribute not exists. - # - # class Person < ActiveRecord::Base - # end - # - # person = Person.new - # person.column_for_attribute(:name) # the result depends on the ConnectionAdapter - # # => #<ActiveRecord::ConnectionAdapters::SQLite3Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...> - # - # person.column_for_attribute(:nothing) - # # => nil - def column_for_attribute(name) - # FIXME: should this return a null object for columns that don't exist? - self.class.columns_hash[name.to_s] - end - # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, # "2004-12-12" in a date column is cast to a date object, like Date.new(2004, 12, 12)). It raises # <tt>ActiveModel::MissingAttributeError</tt> if the identified attribute is missing. @@ -443,16 +443,16 @@ module ActiveRecord # Filters the primary keys and readonly attributes from the attribute names. def attributes_for_update(attribute_names) - attribute_names.select do |name| - column_for_attribute(name) && !readonly_attribute?(name) + attribute_names.reject do |name| + readonly_attribute?(name) end end # Filters out the primary keys, from the attribute names, when the primary # key is to be generated (e.g. the id attribute has no value). def attributes_for_create(attribute_names) - attribute_names.select do |name| - column_for_attribute(name) && !(pk_attribute?(name) && id.nil?) + attribute_names.reject do |name| + pk_attribute?(name) && id.nil? end end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index ad01b5bf25..be438aba95 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -55,7 +55,7 @@ module ActiveRecord # optimistic locking) won't get written unless they get marked as changed self.class.columns.each do |c| attr, orig_value = c.name, c.default - changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, @raw_attributes[attr]) + changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value) end end @@ -63,19 +63,26 @@ module ActiveRecord def write_attribute(attr, value) attr = attr.to_s - save_changed_attribute(attr, value) + old_value = old_attribute_value(attr) - super(attr, value) + result = super(attr, value) + save_changed_attribute(attr, old_value) + result end - def save_changed_attribute(attr, value) - # The attribute already has an unsaved change. + def save_changed_attribute(attr, old_value) if attribute_changed?(attr) - old = changed_attributes[attr] - changed_attributes.delete(attr) unless _field_changed?(attr, old, value) + changed_attributes.delete(attr) unless _field_changed?(attr, old_value) else - old = clone_attribute_value(:read_attribute, attr) - changed_attributes[attr] = old if _field_changed?(attr, old, value) + changed_attributes[attr] = old_value if _field_changed?(attr, old_value) + end + end + + def old_attribute_value(attr) + if attribute_changed?(attr) + changed_attributes[attr] + else + clone_attribute_value(:read_attribute, attr) end end @@ -93,34 +100,10 @@ module ActiveRecord changed end - def _field_changed?(attr, old, value) - if column = column_for_attribute(attr) - if column.number? && (changes_from_nil_to_empty_string?(column, old, value) || - changes_from_zero_to_string?(old, value)) - value = nil - else - value = column.type_cast(value) - end - end - - old != value - end - - def changes_from_nil_to_empty_string?(column, old, value) - # For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values. - # Hence we don't record it as a change if the value changes from nil to ''. - # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll - # be typecast back to 0 (''.to_i => 0) - column.null && (old.nil? || old == 0) && value.blank? - end - - def changes_from_zero_to_string?(old, value) - # For columns with old 0 and value non-empty string - old == 0 && value.is_a?(String) && value.present? && non_zero?(value) - end - - def non_zero?(value) - value !~ /\A0+(\.0+)?\z/ + def _field_changed?(attr, old_value) + new_value = read_attribute(attr) + raw_value = read_attribute_before_type_cast(attr) + column_for_attribute(attr).changed?(old_value, new_value, raw_value) end end end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 99b95b1fc4..7b7e37884b 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -35,35 +35,22 @@ module ActiveRecord extend ActiveSupport::Concern - ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :time, :date] - - included do - class_attribute :attribute_types_cached_by_default, instance_writer: false - self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT - end - module ClassMethods - # +cache_attributes+ allows you to declare which converted attribute - # values should be cached. Usually caching only pays off for attributes - # with expensive conversion methods, like time related columns (e.g. - # +created_at+, +updated_at+). - def cache_attributes(*attribute_names) - cached_attributes.merge attribute_names.map { |attr| attr.to_s } + [:cache_attributes, :cached_attributes, :cache_attribute?].each do |method_name| + define_method method_name do |*| + cached_attributes_deprecation_warning(method_name) + true + end end - # Returns the attributes which are cached. By default time related columns - # with datatype <tt>:datetime, :time, :date</tt> are cached. - def cached_attributes - @cached_attributes ||= columns.select { |c| cacheable_column?(c) }.map { |col| col.name }.to_set - end + protected - # Returns +true+ if the provided attribute is being cached. - def cache_attribute?(attr_name) - cached_attributes.include?(attr_name) + def cached_attributes_deprecation_warning(method_name) + ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) + Calling `#{method_name}` is no longer necessary. All attributes are cached. + MESSAGE end - protected - if Module.methods_transplantable? def define_method_attribute(name) method = ReaderMethodCache[name] @@ -89,16 +76,6 @@ module ActiveRecord end end end - - private - - def cacheable_column?(column) - if attribute_types_cached_by_default == ATTRIBUTE_TYPES_CACHED_BY_DEFAULT - ! serialized_attributes.include? column.name - else - attribute_types_cached_by_default.include?(column.type) - end - end end # Returns the value of the attribute identified by <tt>attr_name</tt> after @@ -122,11 +99,7 @@ module ActiveRecord return block_given? ? yield(name) : nil } - if self.class.cache_attribute?(name) - @attributes[name] = column.type_cast(value) - else - column.type_cast value - end + @attributes[name] = column.type_cast_from_database(value) } end diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index b247bf8a45..60debb7d18 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -58,11 +58,9 @@ module ActiveRecord Coders::YAMLColumn.new(class_name_or_coder) end - type = columns_hash[attr_name.to_s].cast_type - if type.serialized? - type = type.subtype + decorate_attribute_type(attr_name, :serialize) do |type| + Type::Serialized.new(type, coder) end - property attr_name, Type::Serialized.new(type, coder) # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy # has its own hash of own serialized attributes @@ -76,21 +74,6 @@ module ActiveRecord module Behavior # :nodoc: extend ActiveSupport::Concern - module ClassMethods # :nodoc: - def initialize_attributes(attributes, options = {}) - serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized - super(attributes, options) - - serialized_attributes.each do |key, coder| - if attributes.key?(key) - attributes[key] = Type::Serialized::Attribute.new(coder, attributes[key], serialized) - end - end - - attributes - end - end - def should_record_timestamps? super || (self.record_timestamps && (attributes.keys & self.class.serialized_attributes.keys).present?) end @@ -98,50 +81,6 @@ module ActiveRecord def keys_for_partial_write super | (attributes.keys & self.class.serialized_attributes.keys) end - - def _field_changed?(attr, old, value) - if self.class.serialized_attributes.include?(attr) - old != value - else - super - end - end - - def read_attribute_before_type_cast(attr_name) - if self.class.serialized_attributes.include?(attr_name) - super.unserialized_value - else - super - end - end - - def attributes_before_type_cast - super.dup.tap do |attributes| - self.class.serialized_attributes.each_key do |key| - if attributes.key?(key) - attributes[key] = attributes[key].unserialized_value - end - end - end - end - - def typecasted_attribute_value(name) - if self.class.serialized_attributes.include?(name) - @raw_attributes[name].serialized_value - else - super - end - end - - def attributes_for_coder - attribute_names.each_with_object({}) do |name, attrs| - attrs[name] = if self.class.serialized_attributes.include?(name) - @raw_attributes[name].serialized_value - else - read_attribute(name) - end - end - end end end end diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index 18778698e8..abad949ef4 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -1,16 +1,27 @@ module ActiveRecord module AttributeMethods module TimeZoneConversion - class Type # :nodoc: - delegate :type, :type_cast_for_database, to: :@column + class Type < SimpleDelegator # :nodoc: + def type_cast_from_database(value) + convert_time_to_time_zone(super) + end - def initialize(column) - @column = column + def type_cast_from_user(value) + if value.is_a?(Array) + value.map { |v| type_cast_from_user(v) } + elsif value.respond_to?(:in_time_zone) + value.in_time_zone + end end - def type_cast(value) - value = @column.type_cast(value) - value.acts_like?(:time) ? value.in_time_zone : value + def convert_time_to_time_zone(value) + if value.is_a?(Array) + value.map { |v| convert_time_to_time_zone(v) } + elsif value.acts_like?(:time) + value.in_time_zone + else + value + end end end @@ -25,26 +36,6 @@ module ActiveRecord end module ClassMethods - protected - # Defined for all +datetime+ attributes when +time_zone_aware_attributes+ are enabled. - # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone. - def define_method_attribute=(attr_name) - if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) - method_body, line = <<-EOV, __LINE__ + 1 - def #{attr_name}=(time) - time_with_zone = time.respond_to?(:in_time_zone) ? time.in_time_zone : nil - previous_time = attribute_changed?("#{attr_name}") ? changed_attributes["#{attr_name}"] : read_attribute(:#{attr_name}) - write_attribute(:#{attr_name}, time) - #{attr_name}_will_change! if previous_time != time_with_zone - @attributes["#{attr_name}"] = time_with_zone - end - EOV - generated_attribute_methods.module_eval(method_body, __FILE__, line) - else - super - end - end - private def create_time_zone_conversion_attribute?(name, column) time_zone_aware_attributes && diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index 3b9b9c81e8..b72a6219b0 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -53,11 +53,11 @@ module ActiveRecord # specified +value+. Empty strings for fixnum and float columns are # turned into +nil+. def write_attribute(attr_name, value) - write_attribute_with_type_cast(attr_name, value, :type_cast_for_write) + write_attribute_with_type_cast(attr_name, value, true) end def raw_write_attribute(attr_name, value) - write_attribute_with_type_cast(attr_name, value, :raw_type_cast_for_write) + write_attribute_with_type_cast(attr_name, value, false) end private @@ -66,25 +66,25 @@ module ActiveRecord write_attribute(attribute_name, value) end - def write_attribute_with_type_cast(attr_name, value, type_cast_method) + def write_attribute_with_type_cast(attr_name, value, should_type_cast) attr_name = attr_name.to_s attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key @attributes.delete(attr_name) - column = column_for_attribute(attr_name) + column = type_for_attribute(attr_name) + + unless has_attribute?(attr_name) || self.class.columns_hash.key?(attr_name) + raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'" + end # If we're dealing with a binary column, write the data to the cache # so we don't attempt to typecast multiple times. - if column && column.binary? + if column.binary? @attributes[attr_name] = value + elsif should_type_cast + @attributes[attr_name] = column.type_cast_from_user(value) end - if column - @raw_attributes[attr_name] = column.public_send(type_cast_method, value) - elsif @raw_attributes.has_key?(attr_name) - @raw_attributes[attr_name] = value - else - raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'" - end + @raw_attributes[attr_name] = value end end end diff --git a/activerecord/lib/active_record/properties.rb b/activerecord/lib/active_record/attributes.rb index e70f84808d..9c80121d70 100644 --- a/activerecord/lib/active_record/properties.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -1,16 +1,16 @@ module ActiveRecord - module Properties # :nodoc: + module Attributes # :nodoc: extend ActiveSupport::Concern Type = ActiveRecord::Type included do - class_attribute :user_provided_columns, instance_accessor: false # :internal + class_attribute :user_provided_columns, instance_accessor: false # :internal: self.user_provided_columns = {} end module ClassMethods - # Defines or overrides a property on this model. This allows customization of + # Defines or overrides a attribute on this model. This allows customization of # Active Record's type casting behavior, as well as adding support for user defined # types. # @@ -44,7 +44,7 @@ module ActiveRecord # store_listing.price_in_cents # => BigDecimal.new(10.1) # # class StoreListing < ActiveRecord::Base - # property :price_in_cents, Type::Integer.new + # attribute :price_in_cents, Type::Integer.new # end # # # after @@ -53,7 +53,7 @@ module ActiveRecord # Users may also define their own custom types, as long as they respond to the methods # defined on the value type. The `type_cast` method on your type object will be called # with values both from the database, and from your controllers. See - # `ActiveRecord::Properties::Type::Value` for the expected API. It is recommended that your + # `ActiveRecord::Attributes::Type::Value` for the expected API. It is recommended that your # type objects inherit from an existing type, or the base value type. # # class MoneyType < ActiveRecord::Type::Integer @@ -68,12 +68,12 @@ module ActiveRecord # end # # class StoreListing < ActiveRecord::Base - # property :price_in_cents, MoneyType.new + # attribute :price_in_cents, MoneyType.new # end # # store_listing = StoreListing.new(price_in_cents: '$10.00') # store_listing.price_in_cents # => 1000 - def property(name, cast_type, options = {}) + def attribute(name, cast_type, options = {}) name = name.to_s clear_caches_calculated_from_columns # Assign a new hash to ensure that subclasses do not share a hash @@ -113,6 +113,7 @@ module ActiveRecord @columns_hash = nil @column_types = nil @column_defaults = nil + @raw_column_defaults = nil @column_names = nil @content_columns = nil end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 74e2a8e6b9..b3c3e26c9f 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -187,7 +187,7 @@ module ActiveRecord # Doesn't use after_save as that would save associations added in after_create/after_update twice after_create save_method after_update save_method - elsif reflection.macro == :has_one + elsif reflection.has_one? define_method(save_method) { save_has_one_association(reflection) } unless method_defined?(save_method) # Configures two callbacks instead of a single after_save so that # the model may rely on their execution order relative to its diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 8b0fffcf06..e4d0abb8ef 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -15,11 +15,12 @@ require 'active_support/core_ext/module/introspection' require 'active_support/core_ext/object/duplicable' require 'active_support/core_ext/class/subclasses' require 'arel' +require 'active_record/attribute_decorators' require 'active_record/errors' require 'active_record/log_subscriber' require 'active_record/explain_subscriber' require 'active_record/relation/delegation' -require 'active_record/properties' +require 'active_record/attributes' module ActiveRecord #:nodoc: # = Active Record @@ -310,8 +311,8 @@ module ActiveRecord #:nodoc: include Locking::Optimistic include Locking::Pessimistic include AttributeMethods - include Timestamp include Callbacks + include Timestamp include Associations include ActiveModel::SecurePassword include AutosaveAssociation @@ -322,7 +323,8 @@ module ActiveRecord #:nodoc: include Reflection include Serialization include Store - include Properties + include Attributes + include AttributeDecorators end ActiveSupport.run_load_hooks(:active_record, Base) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index f836e60988..04ae67234f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -18,21 +18,7 @@ module ActiveRecord value = column.type_cast_for_database(value) end - case value - when String, ActiveSupport::Multibyte::Chars - "'#{quote_string(value.to_s)}'" - when true then quoted_true - when false then quoted_false - when nil then "NULL" - # BigDecimals need to be put in a non-normalized form and quoted. - when BigDecimal then value.to_s('F') - when Numeric, ActiveSupport::Duration then value.to_s - when Date, Time then "'#{quoted_date(value)}'" - when Symbol then "'#{quote_string(value.to_s)}'" - when Class then "'#{value.to_s}'" - else - "'#{quote_string(YAML.dump(value))}'" - end + _quote(value) end # Cast a +value+ to a type that the database understands. For example, @@ -52,20 +38,10 @@ module ActiveRecord value = column.type_cast_for_database(value) end - case value - when Symbol, ActiveSupport::Multibyte::Chars - value.to_s - when true then unquoted_true - when false then unquoted_false - # BigDecimals need to be put in a non-normalized form and quoted. - when BigDecimal then value.to_s('F') - when Date, Time then quoted_date(value) - when *types_which_need_no_typecasting - value - else - to_type = column ? " to #{column.type}" : "" - raise TypeError, "can't cast #{value.class}#{to_type}" - end + _type_cast(value) + rescue TypeError + to_type = column ? " to #{column.type}" : "" + raise TypeError, "can't cast #{value.class}#{to_type}" end # Quotes a string, escaping any ' (single quote) and \ (backslash) @@ -129,6 +105,39 @@ module ActiveRecord def types_which_need_no_typecasting [nil, Numeric, String] end + + def _quote(value) + case value + when String, ActiveSupport::Multibyte::Chars, Type::Binary::Data + "'#{quote_string(value.to_s)}'" + when true then quoted_true + when false then quoted_false + when nil then "NULL" + # BigDecimals need to be put in a non-normalized form and quoted. + when BigDecimal then value.to_s('F') + when Numeric, ActiveSupport::Duration then value.to_s + when Date, Time then "'#{quoted_date(value)}'" + when Symbol then "'#{quote_string(value.to_s)}'" + when Class then "'#{value.to_s}'" + else + "'#{quote_string(YAML.dump(value))}'" + end + end + + def _type_cast(value) + case value + when Symbol, ActiveSupport::Multibyte::Chars, Type::Binary::Data + value.to_s + when true then unquoted_true + when false then unquoted_false + # BigDecimals need to be put in a non-normalized form and quoted. + when BigDecimal then value.to_s('F') + when Date, Time then quoted_date(value) + when *types_which_need_no_typecasting + value + else raise TypeError + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 117c0f0969..a9b3e9cfb9 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -102,8 +102,8 @@ module ActiveRecord # * <tt>:index</tt> - # Create an index for the column. Can be either <tt>true</tt> or an options hash. # - # For clarity's sake: the precision is the number of significant digits, - # while the scale is the number of digits that can be stored following + # Note: The precision is the total number of significant digits + # and the scale is the number of digits that can be stored following # the decimal point. For example, the number 123.45 has a precision of 5 # and a scale of 2. A decimal with a precision of 5 and a scale of 2 can # range from -999.99 to 999.99. diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index d3e172927d..5a0efe49c7 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -1,5 +1,3 @@ -require 'ipaddr' - module ActiveRecord module ConnectionAdapters # :nodoc: # The goal of this module is to move Adapter specific column diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index b589cfee09..cc494a7f40 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -14,7 +14,10 @@ module ActiveRecord module ConnectionAdapters # :nodoc: extend ActiveSupport::Autoload - autoload :Column + autoload_at 'active_record/connection_adapters/column' do + autoload :Column + autoload :NullColumn + end autoload :ConnectionSpecification autoload_at 'active_record/connection_adapters/abstract/schema_definitions' do 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 2677b6ee83..200b773172 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -61,16 +61,13 @@ module ActiveRecord @collation = collation @extra = extra super(name, default, cast_type, sql_type, null) + assert_valid_default(default) end - def extract_default(default) - if blob_or_text_column? - if default.blank? - null || strict ? nil : '' - else - raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" - end - elsif missing_default_forged_as_empty_string?(default) + def default + @default ||= if blob_or_text_column? + null || strict ? nil : '' + elsif missing_default_forged_as_empty_string?(@original_default) nil else super @@ -102,6 +99,12 @@ module ActiveRecord def missing_default_forged_as_empty_string?(default) type != :string && !null && default == '' end + + def assert_valid_default(default) + if blob_or_text_column? && default.present? + raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" + end + end end ## @@ -218,10 +221,9 @@ module ActiveRecord # QUOTING ================================================== - def quote(value, column = nil) - if value.kind_of?(String) && column && column.type == :binary - s = value.unpack("H*")[0] - "x'#{s}'" + def _quote(value) # :nodoc: + if value.is_a?(Type::Binary::Data) + "x'#{value.hex}'" else super end diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 4fbc55a07e..72c6990ba5 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -13,11 +13,11 @@ module ActiveRecord ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ end - attr_reader :name, :default, :cast_type, :null, :sql_type, :default_function + attr_reader :name, :cast_type, :null, :sql_type, :default_function delegate :type, :precision, :scale, :limit, :klass, :accessor, - :text?, :number?, :binary?, :serialized?, - :type_cast, :type_cast_for_write, :raw_type_cast_for_write, :type_cast_for_database, + :text?, :number?, :binary?, :serialized?, :changed?, + :type_cast_from_user, :type_cast_from_database, :type_cast_for_database, :type_cast_for_schema, to: :cast_type @@ -35,7 +35,7 @@ module ActiveRecord @cast_type = cast_type @sql_type = sql_type @null = null - @default = extract_default(default) + @original_default = default @default_function = nil end @@ -51,8 +51,21 @@ module ActiveRecord Base.human_attribute_name(@name) end - def extract_default(default) - type_cast(default) + def default + @default ||= type_cast_from_database(@original_default) + end + + def with_type(type) + dup.tap do |clone| + clone.instance_variable_set('@default', nil) + clone.instance_variable_set('@cast_type', type) + end + end + end + + class NullColumn < Column + def initialize(name) + super name, nil, Type::Value.new end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb index f7bad20f00..bb54de05c8 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb @@ -6,15 +6,6 @@ module ActiveRecord "(#{point[0]},#{point[1]})" end - def string_to_bit(value) # :nodoc: - case value - when /^0x/i - value[2..-1].hex.to_s(2) # Hexadecimal notation - else - value # Bit-string notation - end - end - def hstore_to_string(object, array_member = false) # :nodoc: if Hash === object string = object.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(',') @@ -76,32 +67,6 @@ module ActiveRecord end end - def string_to_cidr(string) # :nodoc: - if string.nil? - nil - elsif String === string - begin - IPAddr.new(string) - rescue ArgumentError - nil - end - else - string - end - end - - def cidr_to_string(object) # :nodoc: - if IPAddr === object - "#{object.to_s}/#{object.instance_variable_get(:@mask_addr).to_s(2).count('1')}" - else - object - end - end - - def string_to_array(string, oid) # :nodoc: - parse_pg_array(string).map {|val| type_cast_array(oid, val)} - end - private HstorePair = begin @@ -134,14 +99,6 @@ module ActiveRecord "\"#{value}\"" end end - - def type_cast_array(oid, value) - if ::Array === value - value.map {|item| type_cast_array(oid, item)} - else - oid.type_cast value - end - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb index a579746815..847fd4dded 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -4,6 +4,8 @@ module ActiveRecord module ConnectionAdapters # PostgreSQL-specific extensions to column definitions in a table. class PostgreSQLColumn < Column #:nodoc: + extend PostgreSQL::Cast + attr_accessor :array def initialize(name, default, cast_type, sql_type = nil, null = true, default_function = nil) @@ -17,24 +19,6 @@ module ActiveRecord @default_function = default_function end - - # :stopdoc: - class << self - include PostgreSQL::Cast - - # 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 - end - # :startdoc: end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 2494e19f84..33a98b4fcb 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -2,6 +2,7 @@ require 'active_record/connection_adapters/postgresql/oid/infinity' require 'active_record/connection_adapters/postgresql/oid/array' require 'active_record/connection_adapters/postgresql/oid/bit' +require 'active_record/connection_adapters/postgresql/oid/bit_varying' require 'active_record/connection_adapters/postgresql/oid/bytea' require 'active_record/connection_adapters/postgresql/oid/cidr' require 'active_record/connection_adapters/postgresql/oid/date' 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 0e9dcd8c0c..87817fe8d1 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -10,11 +10,33 @@ module ActiveRecord @subtype = subtype end - def type_cast(value) - if ::String === value - ConnectionAdapters::PostgreSQLColumn.string_to_array value, @subtype + def type_cast_from_database(value) + if value.is_a?(::String) + type_cast_array(parse_pg_array(value)) else - value + super + end + end + + # 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 + + private + + def type_cast_array(value) + if value.is_a?(::Array) + value.map { |item| type_cast_array(item) } + else + @subtype.type_cast_from_database(value) end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb index 9b2d887d07..3073f8ff30 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb @@ -2,10 +2,19 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Bit < Type::String + class Bit < Type::Value + def type + :bit + end + def type_cast(value) if ::String === value - ConnectionAdapters::PostgreSQLColumn.string_to_bit value + case value + when /^0x/i + value[2..-1].hex.to_s(2) # Hexadecimal notation + else + value # Bit-string notation + end else value end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb new file mode 100644 index 0000000000..054af285bb --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class BitVarying < OID::Bit + def type + :bit_varying + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb index 158468fe5b..534961a414 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb @@ -18,8 +18,26 @@ module ActiveRecord end end + def type_cast_for_database(value) + if IPAddr === value + "#{value.to_s}/#{value.instance_variable_get(:@mask_addr).to_s(2).count('1')}" + else + value + end + end + def cast_value(value) - ConnectionAdapters::PostgreSQLColumn.string_to_cidr value + if value.nil? + nil + elsif String === value + begin + IPAddr.new(value) + rescue ArgumentError + nil + end + else + value + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb index 9ccbf71159..34e2276dd1 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb @@ -11,7 +11,8 @@ module ActiveRecord when 'infinity' then ::Float::INFINITY when '-infinity' then -::Float::INFINITY when / BC$/ - super("-" + value.sub(/ BC$/, "")) + astronomical_year = format("%04d", -value[/^\d+/].to_i + 1) + super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year)) else super end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb index 9753d71461..77dd97e140 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb @@ -5,9 +5,8 @@ module ActiveRecord class Float < Type::Float include Infinity - def type_cast(value) + def cast_value(value) case value - when nil then nil when 'Infinity' then ::Float::INFINITY when '-Infinity' then -::Float::INFINITY when 'NaN' then ::Float::NAN diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb index bf680b6624..88de816d4f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb @@ -7,18 +7,16 @@ module ActiveRecord :hstore end - def type_cast_for_write(value) - # roundtrip to ensure uniform uniform types - # TODO: This is not an efficient solution. - cast_value(type_cast_for_database(value)) + def type_cast_from_user(value) + type_cast_from_database(type_cast_for_database(value)) end - def type_cast_for_database(value) - ConnectionAdapters::PostgreSQLColumn.hstore_to_string(value) + def type_cast_from_database(value) + ConnectionAdapters::PostgreSQLColumn.string_to_hstore(value) end - def cast_value(value) - ConnectionAdapters::PostgreSQLColumn.string_to_hstore value + def type_cast_for_database(value) + ConnectionAdapters::PostgreSQLColumn.hstore_to_string(value) end def accessor diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb index 42a5110ffd..b4fed1bcab 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb @@ -7,18 +7,16 @@ module ActiveRecord :json end - def type_cast_for_write(value) - # roundtrip to ensure uniform uniform types - # TODO: This is not an efficient solution. - cast_value(type_cast_for_database(value)) + def type_cast_from_user(value) + type_cast_from_database(type_cast_for_database(value)) end - def type_cast_for_database(value) - ConnectionAdapters::PostgreSQLColumn.json_to_string(value) + def type_cast_from_database(value) + ConnectionAdapters::PostgreSQLColumn.string_to_json(value) end - def cast_value(value) - ConnectionAdapters::PostgreSQLColumn.string_to_json value + def type_cast_for_database(value) + ConnectionAdapters::PostgreSQLColumn.json_to_string(value) end def accessor diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb index 697dceb7c2..d25eb256c2 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb @@ -7,6 +7,10 @@ module ActiveRecord class_attribute :precision + def type + :money + end + def scale 2 end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb index f9531ddee3..9007bfb178 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb @@ -2,7 +2,11 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Point < Type::String + class Point < Type::Value + def type + :point + end + def type_cast(value) if ::String === value if value[0] == '(' && value[-1] == ')' diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb index a0d8a94c74..c289ba8980 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -29,7 +29,7 @@ module ActiveRecord end def type_cast_single(value) - infinity?(value) ? value : @subtype.type_cast(value) + infinity?(value) ? value : @subtype.type_cast_from_database(value) end def cast_value(value) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 7b7144a3a8..3cf40e6cd4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -44,11 +44,6 @@ module ActiveRecord when 'json' then super(PostgreSQLColumn.json_to_string(value), column) else super end - when IPAddr - case sql_type - when 'inet', 'cidr' then super(PostgreSQLColumn.cidr_to_string(value), column) - else super - end when Float if value.infinite? && column.type == :datetime "'#{value.to_s.downcase}'" @@ -66,7 +61,6 @@ module ActiveRecord end when String case sql_type - when 'bytea' then "'#{escape_bytea(value)}'" when 'xml' then "xml '#{quote_string(value)}'" when /^bit/ case value @@ -110,27 +104,12 @@ module ActiveRecord super(value, column) end end - when String - if 'bytea' == column.sql_type - # Return a bind param hash with format as binary. - # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc - # for more information - { value: value, format: 1 } - else - super(value, column) - end when Hash case column.sql_type when 'hstore' then PostgreSQLColumn.hstore_to_string(value, array_member) when 'json' then PostgreSQLColumn.json_to_string(value) else super(value, column) end - when IPAddr - if %w(inet cidr).include? column.sql_type - PostgreSQLColumn.cidr_to_string(value) - else - super(value, column) - end else super(value, column) end @@ -170,8 +149,9 @@ module ActiveRecord result = "#{result}.#{sprintf("%06d", value.usec)}" end - if value.year < 0 - result = result.sub(/^-/, "") + " BC" + if value.year <= 0 + bce_year = format("%04d", -value.year + 1) + result = result.sub(/^-?\d+/, bce_year) + " BC" end result end @@ -184,6 +164,27 @@ module ActiveRecord quote(value, column) end end + + private + + def _quote(value) + if value.is_a?(Type::Binary::Data) + "'#{escape_bytea(value.to_s)}'" + else + super + end + end + + def _type_cast(value) + if value.is_a?(Type::Binary::Data) + # Return a bind param hash with format as binary. + # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc + # for more information + { value: value.to_s, format: 1 } + else + super + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb index bcfd605165..0867e5ef54 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -4,68 +4,84 @@ module ActiveRecord module ColumnMethods def xml(*args) options = args.extract_options! - column(args[0], 'xml', options) + column(args[0], :xml, options) end def tsvector(*args) options = args.extract_options! - column(args[0], 'tsvector', options) + column(args[0], :tsvector, options) end def int4range(name, options = {}) - column(name, 'int4range', options) + column(name, :int4range, options) end def int8range(name, options = {}) - column(name, 'int8range', options) + column(name, :int8range, options) end def tsrange(name, options = {}) - column(name, 'tsrange', options) + column(name, :tsrange, options) end def tstzrange(name, options = {}) - column(name, 'tstzrange', options) + column(name, :tstzrange, options) end def numrange(name, options = {}) - column(name, 'numrange', options) + column(name, :numrange, options) end def daterange(name, options = {}) - column(name, 'daterange', options) + column(name, :daterange, options) end def hstore(name, options = {}) - column(name, 'hstore', options) + column(name, :hstore, options) end def ltree(name, options = {}) - column(name, 'ltree', options) + column(name, :ltree, options) end def inet(name, options = {}) - column(name, 'inet', options) + column(name, :inet, options) end def cidr(name, options = {}) - column(name, 'cidr', options) + column(name, :cidr, options) end def macaddr(name, options = {}) - column(name, 'macaddr', options) + column(name, :macaddr, options) end def uuid(name, options = {}) - column(name, 'uuid', options) + column(name, :uuid, options) end def json(name, options = {}) - column(name, 'json', options) + column(name, :json, options) end def citext(name, options = {}) - column(name, 'citext', options) + column(name, :citext, options) + end + + def point(name, options = {}) + column(name, :point, options) + end + + def bit(name, options) + column(name, :bit, options) + end + + def bit_varying(name, options) + column(name, :bit_varying, options) + end + + def money(name, options) + column(name, :money, options) end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 23c1a8de2f..71b05cdbae 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -103,7 +103,11 @@ module ActiveRecord uuid: { name: "uuid" }, json: { name: "json" }, ltree: { name: "ltree" }, - citext: { name: "citext" } + citext: { name: "citext" }, + point: { name: "point" }, + bit: { name: "bit" }, + bit_varying: { name: "bit varying" }, + money: { name: "money" }, } OID = PostgreSQL::OID #:nodoc: @@ -344,14 +348,14 @@ module ActiveRecord if supports_extensions? res = exec_query "SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled", 'SCHEMA' - res.column_types['enabled'].type_cast res.rows.first.first + res.column_types['enabled'].type_cast_from_database res.rows.first.first end end def extensions if supports_extensions? res = exec_query "SELECT extname from pg_extension", "SCHEMA" - res.rows.map { |r| res.column_types['extname'].type_cast r.first } + res.rows.map { |r| res.column_types['extname'].type_cast_from_database r.first } else super end @@ -432,8 +436,8 @@ module ActiveRecord m.alias_type 'name', 'varchar' m.alias_type 'bpchar', 'varchar' m.register_type 'bool', Type::Boolean.new - m.register_type 'bit', OID::Bit.new - m.alias_type 'varbit', 'bit' + register_class_with_limit m, 'bit', OID::Bit + register_class_with_limit m, 'varbit', OID::BitVarying m.alias_type 'timestamptz', 'timestamp' m.register_type 'date', OID::Date.new m.register_type 'time', OID::Time.new @@ -499,64 +503,16 @@ module ActiveRecord # Extracts the value from a PostgreSQL column default definition. def extract_value_from_default(oid, default) # :nodoc: - # This is a performance optimization for Ruby 1.9.2 in development. - # If the value is nil, we return nil straight away without checking - # the regular expressions. If we check each regular expression, - # Regexp#=== will call NilClass#to_str, which will trigger - # method_missing (defined by whiny nil in ActiveSupport) which - # makes this method very very slow. - return default unless default - - # TODO: The default extraction is related to the cast-type. - # we should probably make a type_map lookup and cast the default- - # expression accordingly - if oid.type == :enum && default =~ /\A'(.*)'::/ - return $1 - end - case default - when /\A'(.*)'::(num|date|tstz|ts|int4|int8)range\z/m - $1 + # Quoted types + when /\A[\(B]?'(.*)'::/m + $1.gsub(/''/, "'") + # Boolean types + when 'true', 'false' + default # Numeric types when /\A\(?(-?\d+(\.\d*)?\)?(::bigint)?)\z/ $1 - # Character types - when /\A\(?'(.*)'::.*\b(?:character varying|bpchar|text)\z/m - $1.gsub(/''/, "'") - # Binary data types - when /\A'(.*)'::bytea\z/m - $1 - # Date/time types - when /\A'(.+)'::(?:time(?:stamp)? with(?:out)? time zone|date)\z/ - $1 - when /\A'(.*)'::interval\z/ - $1 - # Boolean type - when 'true' - true - when 'false' - false - # Geometric types - when /\A'(.*)'::(?:point|line|lseg|box|"?path"?|polygon|circle)\z/ - $1 - # Network address types - when /\A'(.*)'::(?:cidr|inet|macaddr)\z/ - $1 - # Bit string types - when /\AB'(.*)'::"?bit(?: varying)?"?\z/ - $1 - # XML type - when /\A'(.*)'::xml\z/m - $1 - # Arrays - when /\A'(.*)'::"?\D+"?\[\]\z/ - $1 - # Hstore - when /\A'(.*)'::hstore\z/ - $1 - # JSON - when /\A'(.*)'::json\z/ - $1 # Object identifier types when /\A-?\d+\z/ $1 diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index adf893d7e7..e6163771e8 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -219,10 +219,9 @@ module ActiveRecord # QUOTING ================================================== - def quote(value, column = nil) - if value.kind_of?(String) && column && column.type == :binary - s = value.unpack("H*")[0] - "x'#{s}'" + def _quote(value) # :nodoc: + if value.is_a?(Type::Binary::Data) + "x'#{value.hex}'" else super end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index e99e235fe8..7edaf256c7 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -249,10 +249,10 @@ module ActiveRecord # # Instantiates a single new object # User.new(first_name: 'Jamie') def initialize(attributes = nil, options = {}) - defaults = self.class.column_defaults.dup + defaults = self.class.raw_column_defaults.dup defaults.each { |k, v| defaults[k] = v.dup if v.duplicable? } - @raw_attributes = self.class.initialize_attributes(defaults) + @raw_attributes = defaults @column_types_override = nil @column_types = self.class.column_types @@ -278,13 +278,14 @@ module ActiveRecord # post.init_with('attributes' => { 'title' => 'hello world' }) # post.title # => 'hello world' def init_with(coder) - @raw_attributes = self.class.initialize_attributes(coder['attributes']) + @raw_attributes = coder['raw_attributes'] @column_types_override = coder['column_types'] @column_types = self.class.column_types init_internals - @new_record = false + @attributes = coder['attributes'] if coder['attributes'] + @new_record = coder['new_record'] self.class.define_attribute_methods @@ -323,16 +324,16 @@ module ActiveRecord ## def initialize_dup(other) # :nodoc: cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) - self.class.initialize_attributes(cloned_attributes, :serialized => false) @raw_attributes = cloned_attributes @raw_attributes[self.class.primary_key] = nil + @attributes = other.clone_attributes(:read_attribute) + @attributes[self.class.primary_key] = nil run_callbacks(:initialize) unless _initialize_callbacks.empty? @aggregation_cache = {} @association_cache = {} - @attributes = {} @new_record = true @destroyed = false @@ -353,7 +354,10 @@ module ActiveRecord # Post.new.encode_with(coder) # coder # => {"attributes" => {"id" => nil, ... }} def encode_with(coder) - coder['attributes'] = attributes_for_coder + coder['raw_attributes'] = @raw_attributes + coder['attributes'] = @attributes + coder['column_types'] = @column_types_override + coder['new_record'] = new_record? end # Returns true if +comparison_object+ is the same exact object, or +comparison_object+ @@ -432,6 +436,29 @@ module ActiveRecord "#<#{self.class} #{inspection}>" end + # Takes a PP and prettily prints this record to it, allowing you to get a nice result from `pp record` + # when pp is required. + def pretty_print(pp) + pp.object_address_group(self) do + if defined?(@attributes) && @attributes + column_names = self.class.column_names.select { |name| has_attribute?(name) || new_record? } + pp.seplist(column_names, proc { pp.text ',' }) do |column_name| + column_value = read_attribute(column_name) + pp.breakable ' ' + pp.group(1) do + pp.text column_name + pp.text ':' + pp.breakable + pp.pp column_value + end + end + else + pp.breakable ' ' + pp.text 'not initialized' + end + end + end + # Returns a hash of the given methods with their names as keys and returned values as values. def slice(*methods) Hash[methods.map! { |method| [method, public_send(method)] }].with_indifferent_access @@ -520,5 +547,11 @@ module ActiveRecord def init_attributes(attributes, options) assign_attributes(attributes) end + + def thaw + if frozen? + @raw_attributes = @raw_attributes.dup + end + end end end diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index c7ec093824..38c6dcf88d 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -140,17 +140,14 @@ module ActiveRecord @_enum_methods_module ||= begin mod = Module.new do private - def save_changed_attribute(attr_name, value) + def save_changed_attribute(attr_name, old) if (mapping = self.class.defined_enums[attr_name.to_s]) + value = read_attribute(attr_name) if attribute_changed?(attr_name) - old = changed_attributes[attr_name] - if mapping[old] == value changed_attributes.delete(attr_name) end else - old = clone_attribute_value(:read_attribute, attr_name) - if old != value changed_attributes[attr_name] = mapping.key old end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index d40bea5ea7..95fcbbe99a 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -33,7 +33,7 @@ module ActiveRecord # is followed by an indented list of key/value pairs in the "key: value" format. Records are # separated by a blank line for your viewing pleasure. # - # Note that fixtures are unordered. If you want ordered fixtures, use the omap YAML type. + # Note: Fixtures are unordered. If you want ordered fixtures, use the omap YAML type. # See http://yaml.org/type/omap.html # for the specification. You will need ordered fixtures when you have foreign key constraints # on keys in the same table. This is commonly needed for tree structures. Example: @@ -374,8 +374,9 @@ module ActiveRecord # # == Support for YAML defaults # - # You probably already know how to use YAML to set and reuse defaults in - # your <tt>database.yml</tt> file. You can use the same technique in your fixtures: + # You can set and reuse defaults in your fixtures YAML file. + # This is the same technique used in the <tt>database.yml</tt> file + # to specify defaults: # # DEFAULTS: &DEFAULTS # created_on: <%= 3.weeks.ago.to_s(:db) %> @@ -461,13 +462,7 @@ module ActiveRecord @config = config # Remove string values that aren't constants or subclasses of AR - @class_names.delete_if { |k,klass| - unless klass.is_a? Class - klass = klass.safe_constantize - ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name to `set_fixture_class` will be removed in Rails 4.2. Use the class itself instead.") - end - !insert_class(@class_names, k, klass) - } + @class_names.delete_if { |klass_name, klass| !insert_class(@class_names, klass_name, klass) } end def [](fs_name) @@ -573,10 +568,6 @@ module ActiveRecord @config = config @model_class = nil - if class_name.is_a?(String) - ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name to `FixtureSet.new` will be removed in Rails 4.2. Use the class itself instead.") - end - if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any? @model_class = class_name else @@ -656,7 +647,7 @@ module ActiveRecord fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s if association.name.to_s != fk_name && value = row.delete(association.name.to_s) - if association.options[:polymorphic] && value.sub!(/\s*\(([^\)]*)\)\s*$/, "") + if association.polymorphic? && value.sub!(/\s*\(([^\)]*)\)\s*$/, "") # support polymorphic belongs_to as "label (Type)" row[association.foreign_type] = $1 end diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 7fb27ef6e9..4528d8783c 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -66,7 +66,7 @@ module ActiveRecord send(lock_col + '=', previous_lock_value + 1) end - def _update_record(attribute_names = @raw_attributes.keys) #:nodoc: + def _update_record(attribute_names = self.attribute_names) #:nodoc: return super unless locking_enabled? return 0 if attribute_names.empty? @@ -151,12 +151,6 @@ module ActiveRecord @locking_column end - # Quote the column name used for optimistic locking. - def quoted_locking_column - ActiveSupport::Deprecation.warn "ActiveRecord::Base.quoted_locking_column is deprecated and will be removed in Rails 4.2 or later." - connection.quote_column_name(locking_column) - end - # Reset the column used for optimistic locking back to the +lock_version+ default. def reset_locking_column self.locking_column = DEFAULT_LOCKING_COLUMN diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 8fe32bcb6c..481e5c17e4 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -711,7 +711,7 @@ module ActiveRecord if ActiveRecord::Base.timestamped_migrations [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % number].max else - "%.3d" % number + SchemaMigration.normalize_migration_number(number) end end @@ -851,19 +851,6 @@ module ActiveRecord migrations(migrations_paths).last || NullMigration.new end - def proper_table_name(name, options = {}) - ActiveSupport::Deprecation.warn "ActiveRecord::Migrator.proper_table_name is deprecated and will be removed in Rails 4.2. Use the proper_table_name instance method on ActiveRecord::Migration instead" - options = { - table_name_prefix: ActiveRecord::Base.table_name_prefix, - table_name_suffix: ActiveRecord::Base.table_name_suffix - }.merge(options) - if name.respond_to? :table_name - name.table_name - else - "#{options[:table_name_prefix]}#{name}#{options[:table_name_suffix]}" - end - end - def migrations_paths @migrations_paths ||= ['db/migrate'] # just to not break things if someone uses: migration_path = some_string diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index ad6428d8a8..9e1afd32e6 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -51,6 +51,8 @@ module ActiveRecord self.pluralize_table_names = true self.inheritance_column = 'type' + + delegate :type_for_attribute, to: :class end module ClassMethods @@ -218,21 +220,25 @@ module ActiveRecord end def column_types # :nodoc: - @column_types ||= decorate_columns(columns_hash.dup) + @column_types ||= decorate_types(build_types_hash) + end + + def type_for_attribute(attr_name) # :nodoc: + column_types.fetch(attr_name) { Type::Value.new } end - def decorate_columns(columns_hash) # :nodoc: - return if columns_hash.empty? + def decorate_types(types) # :nodoc: + return if types.empty? @time_zone_column_names ||= self.columns_hash.find_all do |name, col| create_time_zone_conversion_attribute?(name, col) end.map!(&:first) @time_zone_column_names.each do |name| - columns_hash[name] = AttributeMethods::TimeZoneConversion::Type.new(columns_hash[name]) + types[name] = AttributeMethods::TimeZoneConversion::Type.new(types[name]) end - columns_hash + types end # Returns a hash where the keys are column names and the values are @@ -241,6 +247,14 @@ module ActiveRecord @column_defaults ||= Hash[columns.map { |c| [c.name, c.default] }] end + # Returns a hash where the keys are the column names and the values + # are the default values suitable for use in `@raw_attriubtes` + def raw_column_defaults # :nodoc: + @raw_column_defauts ||= Hash[column_defaults.map { |name, default| + [name, columns_hash[name].type_cast_for_database(default)] + }] + end + # Returns an array of column names as strings. def column_names @column_names ||= columns.map { |column| column.name } @@ -285,6 +299,7 @@ module ActiveRecord @arel_engine = nil @column_defaults = nil + @raw_column_defauts = nil @column_names = nil @column_types = nil @content_columns = nil @@ -295,13 +310,6 @@ module ActiveRecord @cached_time_zone = nil end - # This is a hook for use by modules that need to do extra stuff to - # attributes when they are initialized. (e.g. attribute - # serialization) - def initialize_attributes(attributes, options = {}) #:nodoc: - attributes - end - private # Guesses the table name, but does not decorate it with prefix and suffix information. @@ -327,6 +335,10 @@ module ActiveRecord base.table_name end end + + def build_types_hash + Hash[columns.map { |column| [column.name, column.cast_type] }] + end end end end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 1242f49e28..8a2a06f2ca 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -516,7 +516,7 @@ module ActiveRecord # Determines if a hash contains a truthy _destroy key. def has_destroy_flag?(hash) - Type::Boolean.new.type_cast(hash['_destroy']) + Type::Boolean.new.type_cast_from_user(hash['_destroy']) end # Determines if a new record should be rejected by checking diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 78ae05073a..3ab8ec4836 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -48,8 +48,11 @@ module ActiveRecord # how this "single-table" inheritance mapping is implemented. def instantiate(attributes, column_types = {}) klass = discriminate_class_for_record(attributes) - column_types = klass.decorate_columns(column_types.dup) - klass.allocate.init_with('attributes' => attributes, 'column_types' => column_types) + klass.allocate.init_with( + 'raw_attributes' => attributes, + 'column_types' => column_types, + 'new_record' => false, + ) end private @@ -490,7 +493,7 @@ module ActiveRecord # Updates the associated record with values matching those of the instance attributes. # Returns the number of affected rows. - def _update_record(attribute_names = @raw_attributes.keys) + def _update_record(attribute_names = self.attribute_names) attributes_values = arel_attributes_with_values_for_update(attribute_names) if attributes_values.empty? 0 @@ -501,7 +504,7 @@ module ActiveRecord # Creates a record with values matching those of the instance attributes # and returns its id. - def _create_record(attribute_names = @raw_attributes.keys) + def _create_record(attribute_names = self.attribute_names) attributes_values = arel_attributes_with_values_for_create(attribute_names) new_id = self.class.unscoped.insert attributes_values diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 1fe54cea3f..39817703cd 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -40,7 +40,7 @@ module ActiveRecord column_types = {} if result_set.respond_to? :column_types - column_types = result_set.column_types.merge(columns_hash) + column_types = result_set.column_types.except(*columns_hash.keys) else ActiveSupport::Deprecation.warn "the object returned from `select_all` must respond to `column_types`" end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index fa25ceaefa..6dca206f2a 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -86,14 +86,15 @@ db_namespace = namespace :db do abort 'Schema migrations table does not exist yet.' end db_list = ActiveRecord::Base.connection.select_values("SELECT version FROM #{ActiveRecord::Migrator.schema_migrations_table_name}") - db_list.map! { |version| "%.3d" % version } + db_list.map! { |version| ActiveRecord::SchemaMigration.normalize_migration_number(version) } file_list = [] ActiveRecord::Migrator.migrations_paths.each do |path| Dir.foreach(path) do |file| # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern if match_data = /^(\d{3,})_(.+)\.rb$/.match(file) - status = db_list.delete(match_data[1]) ? 'up' : 'down' - file_list << [status, match_data[1], match_data[2].humanize] + version = ActiveRecord::SchemaMigration.normalize_migration_number(match_data[1]) + status = db_list.delete(version) ? 'up' : 'down' + file_list << [status, version, match_data[2].humanize] end end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index dd80ec6274..95a3c70c93 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -188,6 +188,23 @@ module ActiveRecord active_record == other_aggregation.active_record end + JoinKeys = Struct.new(:key, :foreign_key) # :nodoc: + + def join_keys(assoc_klass) + if source_macro == :belongs_to + if polymorphic? + reflection_key = association_primary_key(assoc_klass) + else + reflection_key = association_primary_key + end + reflection_foreign_key = foreign_key + else + reflection_key = foreign_key + reflection_foreign_key = active_record_primary_key + end + JoinKeys.new(reflection_key, reflection_foreign_key) + end + private def derive_class_name name.to_s.camelize @@ -228,7 +245,7 @@ module ActiveRecord def initialize(macro, name, scope, options, active_record) super - @collection = [:has_many, :has_and_belongs_to_many].include?(macro) + @collection = macro == :has_many @automatic_inverse_of = nil @type = options[:as] && "#{options[:as]}_type" @foreign_type = options[:foreign_type] || "#{name}_type" @@ -239,7 +256,7 @@ module ActiveRecord def association_scope_cache(conn, owner) key = conn.prepared_statements - if options[:polymorphic] + if polymorphic? key = [key, owner.read_attribute(@foreign_type)] end @association_scope_cache[key] ||= @scope_lock.synchronize { @@ -303,7 +320,7 @@ module ActiveRecord end def check_validity_of_inverse! - unless options[:polymorphic] + unless polymorphic? if has_inverse? && inverse_of.nil? raise InverseOfAssociationNotFoundError.new(self) end @@ -392,7 +409,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi # * you use autosave; <tt>autosave: true</tt> # * the association is a +has_many+ association def validate? - !options[:validate].nil? ? options[:validate] : (options[:autosave] == true || macro == :has_many) + !options[:validate].nil? ? options[:validate] : (options[:autosave] == true || collection?) end # Returns +true+ if +self+ is a +belongs_to+ reflection. @@ -400,10 +417,15 @@ Joining, Preloading and eager loading of these associations is deprecated and wi macro == :belongs_to end + # Returns +true+ if +self+ is a +has_one+ reflection. + def has_one? + macro == :has_one + end + def association_class case macro when :belongs_to - if options[:polymorphic] + if polymorphic? Associations::BelongsToPolymorphicAssociation else Associations::BelongsToAssociation @@ -424,7 +446,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi end def polymorphic? - options.key? :polymorphic + options[:polymorphic] end VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to] @@ -441,7 +463,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi def calculate_constructable(macro, options) case macro when :belongs_to - !options[:polymorphic] + !polymorphic? when :has_one !options[:through] else @@ -538,6 +560,13 @@ Joining, Preloading and eager loading of these associations is deprecated and wi end end + class HasAndBelongsToManyReflection < AssociationReflection #:nodoc: + def initialize(macro, name, scope, options, active_record) + super + @collection = true + end + end + # Holds all the meta-data about a :through association as it was specified # in the Active Record class. class ThroughReflection < AssociationReflection #:nodoc: @@ -723,7 +752,7 @@ directive on your declaration like: raise HasManyThroughAssociationNotFoundError.new(active_record.name, self) end - if through_reflection.options[:polymorphic] + if through_reflection.polymorphic? raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self) end @@ -731,15 +760,15 @@ directive on your declaration like: raise HasManyThroughSourceAssociationNotFoundError.new(self) end - if options[:source_type] && source_reflection.options[:polymorphic].nil? + if options[:source_type] && !source_reflection.polymorphic? raise HasManyThroughAssociationPointlessSourceTypeError.new(active_record.name, self, source_reflection) end - if source_reflection.options[:polymorphic] && options[:source_type].nil? + if source_reflection.polymorphic? && options[:source_type].nil? raise HasManyThroughAssociationPolymorphicSourceError.new(active_record.name, self, source_reflection) end - if macro == :has_one && through_reflection.collection? + if has_one? && through_reflection.collection? raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection) end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index d92ff781ee..cef40be7ce 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -74,7 +74,14 @@ module ActiveRecord def _update_record(values, id, id_was) # :nodoc: substitutes, binds = substitute_values values - um = @klass.unscoped.where(@klass.arel_table[@klass.primary_key].eq(id_was || id)).arel.compile_update(substitutes, @klass.primary_key) + + scope = @klass.unscoped + + if @klass.finder_needs_type_condition? + scope.unscope!(where: @klass.inheritance_column) + end + + um = scope.where(@klass.arel_table[@klass.primary_key].eq(id_was || id)).arel.compile_update(substitutes, @klass.primary_key) @klass.connection.update( um, diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index d155517b18..028e4d80ab 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -176,10 +176,8 @@ module ActiveRecord } end - result = result.map do |attributes| - values = klass.initialize_attributes(attributes).values - - columns.zip(values).map { |column, value| column.type_cast value } + result = result.rows.map do |values| + columns.zip(values).map { |column, value| column.type_cast_from_database value } end columns.one? ? result.map!(&:first) : result end @@ -278,7 +276,7 @@ module ActiveRecord if group_attrs.first.respond_to?(:to_sym) association = @klass._reflect_on_association(group_attrs.first.to_sym) - associated = group_attrs.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations + associated = group_attrs.size == 1 && association && association.belongs_to? # only count belongs_to associations group_fields = Array(associated ? association.foreign_key : group_attrs) else group_fields = group_attrs @@ -379,7 +377,7 @@ module ActiveRecord end def type_cast_using_column(value, column) - column ? column.type_cast(value) : value + column ? column.type_cast_from_database(value) : value end # TODO: refactor to allow non-string `select_values` (eg. Arel nodes). diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 47e90e9021..0c9c761f97 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -65,7 +65,7 @@ module ActiveRecord # # returns an Array of the required fields, available since Rails 3.1. def find(*args) if block_given? - to_a.find { |*block_args| yield(*block_args) } + to_a.find(*args) { |*block_args| yield(*block_args) } else find_with_ids(*args) end diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 293189fa69..ae53f66d7a 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -31,7 +31,7 @@ module ActiveRecord class Result include Enumerable - IDENTITY_TYPE = Class.new { def type_cast(v); v; end }.new # :nodoc: + IDENTITY_TYPE = Type::Value.new # :nodoc: attr_reader :columns, :rows, :column_types diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index a9d164e366..3a004d58c9 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -5,6 +5,9 @@ require 'active_record/base' module ActiveRecord class SchemaMigration < ActiveRecord::Base class << self + def primary_key + nil + end def table_name "#{table_name_prefix}#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}" @@ -36,6 +39,10 @@ module ActiveRecord connection.drop_table(table_name) end end + + def normalize_migration_number(number) + "%.3d" % number.to_i + end end def version diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index 1a766093d0..019fe2218e 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -180,12 +180,12 @@ module ActiveRecord #:nodoc: class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc: def compute_type klass = @serializable.class - type = if klass.serialized_attributes.key?(name) + column = klass.columns_hash[name] || Type::Value.new + + type = if column.serialized? super - elsif klass.columns_hash.key?(name) - klass.columns_hash[name].type else - NilClass + column.type end { :text => :string, diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 6c30ccab72..e2e37e7c00 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -99,9 +99,11 @@ module ActiveRecord end def max_updated_column_timestamp(timestamp_names = timestamp_attributes_for_update) - if (timestamps = timestamp_names.map { |attr| self[attr] }.compact).present? - timestamps.map { |ts| ts.to_time }.max - end + timestamp_names + .map { |attr| self[attr] } + .compact + .map(&:to_time) + .max end def current_time_from_proper_timezone diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index d733063f5a..7e4dc4c895 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -339,7 +339,7 @@ module ActiveRecord # Save the new record state and id of a record so it can be restored later if a transaction fails. def remember_transaction_record_state #:nodoc: - @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key) + @_start_transaction_state[:id] = id unless @_start_transaction_state.include?(:new_record) @_start_transaction_state[:new_record] = @new_record end @@ -347,7 +347,7 @@ module ActiveRecord @_start_transaction_state[:destroyed] = @destroyed end @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 - @_start_transaction_state[:frozen?] = @raw_attributes.frozen? + @_start_transaction_state[:frozen?] = frozen? end # Clear the new record state and id of a record. @@ -367,17 +367,10 @@ module ActiveRecord transaction_level = (@_start_transaction_state[:level] || 0) - 1 if transaction_level < 1 || force restore_state = @_start_transaction_state - was_frozen = restore_state[:frozen?] - @raw_attributes = @raw_attributes.dup if @raw_attributes.frozen? + thaw unless restore_state[:frozen?] @new_record = restore_state[:new_record] @destroyed = restore_state[:destroyed] - if restore_state.has_key?(:id) - write_attribute(self.class.primary_key, restore_state[:id]) - else - @raw_attributes.delete(self.class.primary_key) - @attributes.delete(self.class.primary_key) - end - @raw_attributes.freeze if was_frozen + write_attribute(self.class.primary_key, restore_state[:id]) end end end diff --git a/activerecord/lib/active_record/type/binary.rb b/activerecord/lib/active_record/type/binary.rb index e34b7bb268..3bf29b5026 100644 --- a/activerecord/lib/active_record/type/binary.rb +++ b/activerecord/lib/active_record/type/binary.rb @@ -9,8 +9,31 @@ module ActiveRecord true end - def klass - ::String + def type_cast(value) + if value.is_a?(Data) + value.to_s + else + super + end + end + + def type_cast_for_database(value) + return if value.nil? + Data.new(super) + end + + class Data + def initialize(value) + @value = value + end + + def to_s + @value + end + + def hex + @value.unpack('H*')[0] + end end end end diff --git a/activerecord/lib/active_record/type/decimal.rb b/activerecord/lib/active_record/type/decimal.rb index 6eed005345..a9db51c6ba 100644 --- a/activerecord/lib/active_record/type/decimal.rb +++ b/activerecord/lib/active_record/type/decimal.rb @@ -7,10 +7,6 @@ module ActiveRecord :decimal end - def klass - ::BigDecimal - end - def type_cast_for_schema(value) value.to_s end diff --git a/activerecord/lib/active_record/type/float.rb b/activerecord/lib/active_record/type/float.rb index dc50dae328..42eb44b9a9 100644 --- a/activerecord/lib/active_record/type/float.rb +++ b/activerecord/lib/active_record/type/float.rb @@ -7,10 +7,6 @@ module ActiveRecord :float end - def klass - ::Float - end - alias type_cast_for_database type_cast private diff --git a/activerecord/lib/active_record/type/integer.rb b/activerecord/lib/active_record/type/integer.rb index 1e2147dec9..08477d1303 100644 --- a/activerecord/lib/active_record/type/integer.rb +++ b/activerecord/lib/active_record/type/integer.rb @@ -7,10 +7,6 @@ module ActiveRecord :integer end - def klass - ::Fixnum - end - alias type_cast_for_database type_cast private diff --git a/activerecord/lib/active_record/type/numeric.rb b/activerecord/lib/active_record/type/numeric.rb index 464d631d80..137c9e4c99 100644 --- a/activerecord/lib/active_record/type/numeric.rb +++ b/activerecord/lib/active_record/type/numeric.rb @@ -5,14 +5,38 @@ module ActiveRecord true end - def type_cast_for_write(value) - case value - when true then 1 - when false then 0 - when ::String then value.presence - else super + def type_cast(value) + value = case value + when true then 1 + when false then 0 + when ::String then value.presence + else value + end + super(value) + end + + def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc: + # 0 => 'wibble' should mark as changed so numericality validations run + if nil_or_zero?(old_value) && non_numeric_string?(new_value_before_type_cast) + # nil => '' should not mark as changed + old_value != new_value_before_type_cast.presence + else + super end end + + private + + def non_numeric_string?(value) + # 'wibble'.to_i will give zero, we want to make sure + # that we aren't marking int zero to string zero as + # changed. + value !~ /\A\d+\.?\d*\z/ + end + + def nil_or_zero?(value) + value.nil? || value == 0 + end end end end diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index 4052ac0fa0..e6d84eadc0 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -9,20 +9,23 @@ module ActiveRecord super(subtype) end - def type_cast(value) - if value.respond_to?(:unserialized_value) - value.unserialized_value(super(value.value)) + def type_cast_from_database(value) + if is_default_value?(value) + value else - super + coder.load(super) end end - def type_cast_for_write(value) - Attribute.new(coder, value, :unserialized) + def type_cast_from_user(value) + type_cast_from_database(type_cast_for_database(value)) end - def raw_type_cast_for_write(value) - Attribute.new(coder, value, :serialized) + def type_cast_for_database(value) + return if value.nil? + unless is_default_value?(value) + super coder.dump(value) + end end def serialized? @@ -33,24 +36,21 @@ module ActiveRecord ActiveRecord::Store::IndifferentHashAccessor end - class Attribute < Struct.new(:coder, :value, :state) # :nodoc: - def unserialized_value(v = value) - state == :serialized ? unserialize(v) : value - end + def init_with(coder) + @subtype = coder['subtype'] + @coder = coder['coder'] + __setobj__(@subtype) + end - def serialized_value - state == :unserialized ? serialize : value - end + def encode_with(coder) + coder['subtype'] = @subtype + coder['coder'] = @coder + end - def unserialize(v) - self.state = :unserialized - self.value = coder.load(v) - end + private - def serialize - self.state = :serialized - self.value = coder.dump(value) - end + def is_default_value?(value) + value == coder.load(nil) end end end diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb index b3f6ca7691..3b1554bd5a 100644 --- a/activerecord/lib/active_record/type/string.rb +++ b/activerecord/lib/active_record/type/string.rb @@ -9,10 +9,6 @@ module ActiveRecord true end - def klass - ::String - end - private def cast_value(value) diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb index 9c1e9dc01e..efcdab1c0e 100644 --- a/activerecord/lib/active_record/type/value.rb +++ b/activerecord/lib/active_record/type/value.rb @@ -12,19 +12,20 @@ module ActiveRecord @limit = options[:limit] end - # The simplified that this object represents. Subclasses - # should override this method. + # The simplified type that this object represents. Subclasses + # must override this method. def type; end - # Takes an input from the database, or from attribute setters, - # and casts it to a type appropriate for this object. This method - # should not be overriden by subclasses. Instead, override `cast_value`. - def type_cast(value) - cast_value(value) unless value.nil? + def type_cast_from_database(value) + type_cast(value) + end + + def type_cast_from_user(value) + type_cast(value) end def type_cast_for_database(value) - type_cast_for_write(value) + value end def type_cast_for_schema(value) @@ -48,15 +49,24 @@ module ActiveRecord end def klass # :nodoc: - ::Object end - def type_cast_for_write(value) # :nodoc: - value + # +old_value+ will always be type-cast. + # +new_value+ will come straight from the database + # or from assignment, so it could be anything. Types + # which cannot typecast arbitrary values should override + # this method. + def changed?(old_value, new_value, _new_value_before_type_cast) # :nodoc: + old_value != new_value end - alias_method :raw_type_cast_for_write, :type_cast_for_write # :internal: private + # Takes an input from the database, or from attribute setters, + # and casts it to a type appropriate for this object. This method + # should not be overriden by subclasses. Instead, override `cast_value`. + def type_cast(value) # :api: public + cast_value(value) unless value.nil? + end # Responsible for casting values from external sources to the appropriate # type. Called by `type_cast` for all values except `nil`. diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index b6fccc9b94..2e7b1d7206 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -14,7 +14,6 @@ module ActiveRecord finder_class = find_finder_class_for(record) table = finder_class.arel_table value = map_enum_attribute(finder_class, attribute, value) - value = deserialize_attribute(record, attribute, value) relation = build_relation(finder_class, table, attribute, value) relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.id)) if record.persisted? @@ -86,12 +85,6 @@ module ActiveRecord relation end - def deserialize_attribute(record, attribute, value) - coder = record.class.serialized_attributes[attribute.to_s] - value = coder.dump value if value && coder - value - end - def map_enum_attribute(klass, attribute, value) mapping = klass.defined_enums[attribute.to_s] value = mapping[value] if value && mapping diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb index 1699380eb3..28106d3772 100644 --- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb @@ -105,7 +105,7 @@ module ActiveRecord result = @conn.exec_query('SELECT status FROM ex') - assert_equal 2, result.column_types['status'].type_cast(result.last['status']) + assert_equal 2, result.column_types['status'].type_cast_from_database(result.last['status']) end end diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index 34c2008ab4..66750deb68 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -1,9 +1,9 @@ # encoding: utf-8 require "cases/helper" -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' class PostgresqlArrayTest < ActiveRecord::TestCase + include InTimeZone + class PgArray < ActiveRecord::Base self.table_name = 'pg_arrays' end @@ -14,6 +14,7 @@ class PostgresqlArrayTest < ActiveRecord::TestCase @connection.create_table('pg_arrays') do |t| t.string 'tags', array: true t.integer 'ratings', array: true + t.datetime :datetimes, array: true end end @column = PgArray.columns_hash['tags'] @@ -89,9 +90,9 @@ class PostgresqlArrayTest < ActiveRecord::TestCase end def test_type_cast_array - assert_equal(['1', '2', '3'], @column.type_cast('{1,2,3}')) - assert_equal([], @column.type_cast('{}')) - assert_equal([nil], @column.type_cast('{NULL}')) + assert_equal(['1', '2', '3'], @column.type_cast_from_database('{1,2,3}')) + assert_equal([], @column.type_cast_from_database('{}')) + assert_equal([nil], @column.type_cast_from_database('{NULL}')) end def test_type_cast_integers @@ -197,6 +198,26 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert_equal tags, ar.tags end + def test_datetime_with_timezone_awareness + tz = "Pacific Time (US & Canada)" + + in_time_zone tz do + PgArray.reset_column_information + time_string = Time.current.to_s + time = Time.zone.parse(time_string) + + record = PgArray.new(datetimes: [time_string]) + assert_equal [time], record.datetimes + assert_equal ActiveSupport::TimeZone[tz], record.datetimes.first.time_zone + + record.save! + record.reload + + assert_equal [time], record.datetimes + assert_equal ActiveSupport::TimeZone[tz], record.datetimes.first.time_zone + end + end + private def assert_cycle field, array # test creation diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb new file mode 100644 index 0000000000..3a9397bc26 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +require "cases/helper" +require 'support/connection_helper' +require 'support/schema_dumping_helper' + +class PostgresqlBitStringTest < ActiveRecord::TestCase + include ConnectionHelper + include SchemaDumpingHelper + + class PostgresqlBitString < ActiveRecord::Base; end + + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table('postgresql_bit_strings', :force => true) do |t| + t.bit :a_bit, default: "00000011", limit: 8 + t.bit_varying :a_bit_varying, default: "0011", limit: 4 + end + end + + def teardown + return unless @connection + @connection.execute 'DROP TABLE IF EXISTS postgresql_bit_strings' + end + + def test_bit_string_column + column = PostgresqlBitString.columns_hash["a_bit"] + assert_equal :bit, column.type + assert_equal "bit(8)", column.sql_type + assert_not column.text? + assert_not column.number? + assert_not column.binary? + assert_not column.array + end + + def test_bit_string_varying_column + column = PostgresqlBitString.columns_hash["a_bit_varying"] + assert_equal :bit_varying, column.type + assert_equal "bit varying(4)", column.sql_type + assert_not column.text? + assert_not column.number? + assert_not column.binary? + assert_not column.array + end + + def test_default + column = PostgresqlBitString.columns_hash["a_bit"] + assert_equal "00000011", column.default + assert_equal "00000011", PostgresqlBitString.new.a_bit + + column = PostgresqlBitString.columns_hash["a_bit_varying"] + assert_equal "0011", column.default + assert_equal "0011", PostgresqlBitString.new.a_bit_varying + end + + def test_schema_dumping + output = dump_table_schema("postgresql_bit_strings") + assert_match %r{t\.bit\s+"a_bit",\s+limit: 8,\s+default: "00000011"$}, output + assert_match %r{t\.bit_varying\s+"a_bit_varying",\s+limit: 4,\s+default: "0011"$}, output + end + + def test_assigning_invalid_hex_string_raises_exception + assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit: "FF" } + assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit_varying: "FF" } + end + + def test_roundtrip + PostgresqlBitString.create! a_bit: "00001010", a_bit_varying: "0101" + record = PostgresqlBitString.first + assert_equal "00001010", record.a_bit + assert_equal "0101", record.a_bit_varying + + record.a_bit = "11111111" + record.a_bit_varying = "0xF" + record.save! + + assert record.reload + assert_equal "11111111", record.a_bit + assert_equal "1111", record.a_bit_varying + end +end diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb index fadadfa57c..7872f91943 100644 --- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb @@ -1,8 +1,5 @@ # encoding: utf-8 - require "cases/helper" -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' class PostgresqlByteaTest < ActiveRecord::TestCase class ByteaDataType < ActiveRecord::Base @@ -36,16 +33,16 @@ class PostgresqlByteaTest < ActiveRecord::TestCase data = "\u001F\x8B" assert_equal('UTF-8', data.encoding.name) - assert_equal('ASCII-8BIT', @column.type_cast(data).encoding.name) + assert_equal('ASCII-8BIT', @column.type_cast_from_database(data).encoding.name) end def test_type_cast_binary_value data = "\u001F\x8B".force_encoding("BINARY") - assert_equal(data, @column.type_cast(data)) + assert_equal(data, @column.type_cast_from_database(data)) end def test_type_case_nil - assert_equal(nil, @column.type_cast(nil)) + assert_equal(nil, @column.type_cast_from_database(nil)) end def test_read_value diff --git a/activerecord/test/cases/adapters/postgresql/citext_test.rb b/activerecord/test/cases/adapters/postgresql/citext_test.rb index 8493050726..90e837d426 100644 --- a/activerecord/test/cases/adapters/postgresql/citext_test.rb +++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb @@ -1,8 +1,5 @@ # encoding: utf-8 - require 'cases/helper' -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' if ActiveRecord::Base.connection.supports_extensions? class PostgresqlCitextTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb index ecccbf10e6..0b48fe9af8 100644 --- a/activerecord/test/cases/adapters/postgresql/composite_test.rb +++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- require "cases/helper" require 'support/connection_helper' -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' module PostgresqlCompositeBehavior include ConnectionHelper @@ -92,7 +90,12 @@ class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase end end - def type_cast_for_write(value) + def type_cast_from_user(value) + value + end + + def type_cast_for_database(value) + return if value.nil? "(#{value.city},#{value.street})" end end diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index 5f84c893c0..d26cda46fa 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -121,7 +121,7 @@ module ActiveRecord name = @subscriber.payloads.last[:statement_name] assert name res = @connection.exec_query("EXPLAIN (FORMAT JSON) EXECUTE #{name}(#{bindval})") - plan = res.column_types['QUERY PLAN'].type_cast res.rows.first.first + plan = res.column_types['QUERY PLAN'].type_cast_from_database res.rows.first.first assert_operator plan.length, :>, 0 end diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index 0dad89c67a..a0a34e4b87 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -8,9 +8,6 @@ end class PostgresqlTime < ActiveRecord::Base end -class PostgresqlBitString < ActiveRecord::Base -end - class PostgresqlOid < ActiveRecord::Base end @@ -33,15 +30,12 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase @connection.execute("INSERT INTO postgresql_times (id, time_interval, scaled_time_interval) VALUES (1, '1 year 2 days ago', '3 weeks ago')") @first_time = PostgresqlTime.find(1) - @connection.execute("INSERT INTO postgresql_bit_strings (id, bit_string, bit_string_varying) VALUES (1, B'00010101', X'15')") - @first_bit_string = PostgresqlBitString.find(1) - @connection.execute("INSERT INTO postgresql_oids (id, obj_id) VALUES (1, 1234)") @first_oid = PostgresqlOid.find(1) end teardown do - [PostgresqlNumber, PostgresqlTime, PostgresqlBitString, PostgresqlOid].each(&:delete_all) + [PostgresqlNumber, PostgresqlTime, PostgresqlOid].each(&:delete_all) end def test_data_type_of_number_types @@ -54,11 +48,6 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal :string, @first_time.column_for_attribute(:scaled_time_interval).type end - def test_data_type_of_bit_string_types - assert_equal :string, @first_bit_string.column_for_attribute(:bit_string).type - assert_equal :string, @first_bit_string.column_for_attribute(:bit_string_varying).type - end - def test_data_type_of_oid_types assert_equal :integer, @first_oid.column_for_attribute(:obj_id).type end @@ -76,11 +65,6 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal '-21 days', @first_time.scaled_time_interval end - def test_bit_string_values - assert_equal '00010101', @first_bit_string.bit_string - assert_equal '00010101', @first_bit_string.bit_string_varying - end - def test_oid_values assert_equal 1234, @first_oid.obj_id end @@ -103,23 +87,6 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal '2 years 00:03:00', @first_time.time_interval end - def test_update_bit_string - new_bit_string = '11111111' - new_bit_string_varying = '0xFF' - @first_bit_string.bit_string = new_bit_string - @first_bit_string.bit_string_varying = new_bit_string_varying - assert @first_bit_string.save - assert @first_bit_string.reload - assert_equal new_bit_string, @first_bit_string.bit_string - assert_equal @first_bit_string.bit_string, @first_bit_string.bit_string_varying - end - - def test_invalid_hex_string - new_bit_string = 'FF' - @first_bit_string.bit_string = new_bit_string - assert_raise(ActiveRecord::StatementInvalid) { assert @first_bit_string.save } - end - def test_update_oid new_value = 567890 @first_oid.obj_id = new_value diff --git a/activerecord/test/cases/adapters/postgresql/domain_test.rb b/activerecord/test/cases/adapters/postgresql/domain_test.rb index 5286a847a4..fd7fdecff1 100644 --- a/activerecord/test/cases/adapters/postgresql/domain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/domain_test.rb @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- require "cases/helper" require 'support/connection_helper' -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' class PostgresqlDomainTest < ActiveRecord::TestCase include ConnectionHelper diff --git a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb index 91058f8681..7b99fcdda0 100644 --- a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb +++ b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb @@ -1,6 +1,4 @@ require "cases/helper" -require "active_record/base" -require "active_record/connection_adapters/postgresql_adapter" class PostgresqlExtensionMigrationTest < ActiveRecord::TestCase self.use_transactional_fixtures = false diff --git a/activerecord/test/cases/adapters/postgresql/full_text_test.rb b/activerecord/test/cases/adapters/postgresql/full_text_test.rb index 4442abcbc4..ec646de5e9 100644 --- a/activerecord/test/cases/adapters/postgresql/full_text_test.rb +++ b/activerecord/test/cases/adapters/postgresql/full_text_test.rb @@ -1,8 +1,5 @@ # encoding: utf-8 - require "cases/helper" -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' class PostgresqlFullTextTest < ActiveRecord::TestCase class PostgresqlTsvector < ActiveRecord::Base; end diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb new file mode 100644 index 0000000000..2f106ee664 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +require "cases/helper" +require 'support/connection_helper' +require 'support/schema_dumping_helper' + +class PostgresqlPointTest < ActiveRecord::TestCase + include ConnectionHelper + include SchemaDumpingHelper + + class PostgresqlPoint < ActiveRecord::Base; end + + def setup + @connection = ActiveRecord::Base.connection + @connection.transaction do + @connection.create_table('postgresql_points') do |t| + t.point :x + t.point :y, default: [12.2, 13.3] + t.point :z, default: "(14.4,15.5)" + end + end + end + + teardown do + @connection.execute 'DROP TABLE IF EXISTS postgresql_points' + end + + def test_column + column = PostgresqlPoint.columns_hash["x"] + assert_equal :point, column.type + assert_equal "point", column.sql_type + assert_not column.text? + assert_not column.number? + assert_not column.binary? + assert_not column.array + end + + def test_default + column = PostgresqlPoint.columns_hash["y"] + assert_equal [12.2, 13.3], column.default + assert_equal [12.2, 13.3], PostgresqlPoint.new.y + + column = PostgresqlPoint.columns_hash["z"] + assert_equal [14.4, 15.5], column.default + assert_equal [14.4, 15.5], PostgresqlPoint.new.z + end + + def test_schema_dumping + output = dump_table_schema("postgresql_points") + assert_match %r{t\.point\s+"x"$}, output + assert_match %r{t\.point\s+"y",\s+default: \[12\.2, 13\.3\]$}, output + assert_match %r{t\.point\s+"z",\s+default: \[14\.4, 15\.5\]$}, output + end + + def test_roundtrip + PostgresqlPoint.create! x: [10, 25.2] + record = PostgresqlPoint.first + assert_equal [10, 25.2], record.x + + record.x = [1.1, 2.2] + record.save! + assert record.reload + assert_equal [1.1, 2.2], record.x + end +end diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index 1fef4899be..a25c9cb5e4 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -106,6 +106,7 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase def test_cast_value_on_write x = Hstore.new tags: {"bool" => true, "number" => 5} + assert_equal({"bool" => true, "number" => 5}, x.tags_before_type_cast) assert_equal({"bool" => "true", "number" => "5"}, x.tags) x.save assert_equal({"bool" => "true", "number" => "5"}, x.reload.tags) @@ -117,11 +118,11 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase data = "\"1\"=>\"2\"" hash = @column.class.string_to_hstore data assert_equal({'1' => '2'}, hash) - assert_equal({'1' => '2'}, @column.type_cast(data)) + assert_equal({'1' => '2'}, @column.type_cast_from_database(data)) - assert_equal({}, @column.type_cast("")) - assert_equal({'key'=>nil}, @column.type_cast('key => NULL')) - assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast(%q(c=>"}", "\"a\""=>"b \"a b"))) + assert_equal({}, @column.type_cast_from_database("")) + assert_equal({'key'=>nil}, @column.type_cast_from_database('key => NULL')) + assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast_from_database(%q(c=>"}", "\"a\""=>"b \"a b"))) end def test_with_store_accessors @@ -152,6 +153,16 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase assert_equal "GMT", y.timezone end + def test_yaml_round_trip_with_store_accessors + x = Hstore.new(language: "fr", timezone: "GMT") + assert_equal "fr", x.language + assert_equal "GMT", x.timezone + + y = YAML.load(YAML.dump(x)) + assert_equal "fr", y.language + assert_equal "GMT", y.timezone + end + def test_gen1 assert_equal(%q(" "=>""), @column.class.hstore_to_string({' '=>''})) end @@ -169,31 +180,31 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase end def test_parse1 - assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @column.type_cast('a=>null,b=>NuLl,c=>"NuLl",null=>c')) + assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @column.type_cast_from_database('a=>null,b=>NuLl,c=>"NuLl",null=>c')) end def test_parse2 - assert_equal({" " => " "}, @column.type_cast("\\ =>\\ ")) + assert_equal({" " => " "}, @column.type_cast_from_database("\\ =>\\ ")) end def test_parse3 - assert_equal({"=" => ">"}, @column.type_cast("==>>")) + assert_equal({"=" => ">"}, @column.type_cast_from_database("==>>")) end def test_parse4 - assert_equal({"=a"=>"q=w"}, @column.type_cast('\=a=>q=w')) + assert_equal({"=a"=>"q=w"}, @column.type_cast_from_database('\=a=>q=w')) end def test_parse5 - assert_equal({"=a"=>"q=w"}, @column.type_cast('"=a"=>q\=w')) + assert_equal({"=a"=>"q=w"}, @column.type_cast_from_database('"=a"=>q\=w')) end def test_parse6 - assert_equal({"\"a"=>"q>w"}, @column.type_cast('"\"a"=>q>w')) + assert_equal({"\"a"=>"q>w"}, @column.type_cast_from_database('"\"a"=>q>w')) end def test_parse7 - assert_equal({"\"a"=>"q\"w"}, @column.type_cast('\"a=>q"w')) + assert_equal({"\"a"=>"q\"w"}, @column.type_cast_from_database('\"a=>q"w')) end def test_rewrite @@ -284,6 +295,34 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase Hstore.update_all tags: { } assert_equal({ }, hstore.reload.tags) end + + class TagCollection + def initialize(hash); @hash = hash end + def to_hash; @hash end + def self.load(hash); new(hash) end + def self.dump(object); object.to_hash end + end + + class HstoreWithSerialize < Hstore + serialize :tags, TagCollection + end + + def test_hstore_with_serialized_attributes + HstoreWithSerialize.create! tags: TagCollection.new({"one" => "two"}) + record = HstoreWithSerialize.first + assert_instance_of TagCollection, record.tags + assert_equal({"one" => "two"}, record.tags.to_hash) + record.tags = TagCollection.new("three" => "four") + record.save! + assert_equal({"three" => "four"}, HstoreWithSerialize.first.tags.to_hash) + end + + def test_clone_hstore_with_serialized_attributes + HstoreWithSerialize.create! tags: TagCollection.new({"one" => "two"}) + record = HstoreWithSerialize.first + dupe = record.dup + assert_equal({"one" => "two"}, dupe.tags.to_hash) + end end private diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index 03b546119d..3ee8839823 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -68,6 +68,7 @@ class PostgresqlJSONTest < ActiveRecord::TestCase def test_cast_value_on_write x = JsonDataType.new payload: {"string" => "foo", :symbol => :bar} + assert_equal({"string" => "foo", :symbol => :bar}, x.payload_before_type_cast) assert_equal({"string" => "foo", "symbol" => "bar"}, x.payload) x.save assert_equal({"string" => "foo", "symbol" => "bar"}, x.reload.payload) @@ -79,11 +80,11 @@ class PostgresqlJSONTest < ActiveRecord::TestCase data = "{\"a_key\":\"a_value\"}" hash = column.class.string_to_json data assert_equal({'a_key' => 'a_value'}, hash) - assert_equal({'a_key' => 'a_value'}, column.type_cast(data)) + assert_equal({'a_key' => 'a_value'}, column.type_cast_from_database(data)) - assert_equal({}, column.type_cast("{}")) - assert_equal({'key'=>nil}, column.type_cast('{"key": null}')) - assert_equal({'c'=>'}','"a"'=>'b "a b'}, column.type_cast(%q({"c":"}", "\"a\"":"b \"a b"}))) + assert_equal({}, column.type_cast_from_database("{}")) + assert_equal({'key'=>nil}, column.type_cast_from_database('{"key": null}')) + assert_equal({'c'=>'}','"a"'=>'b "a b'}, column.type_cast_from_database(%q({"c":"}", "\"a\"":"b \"a b"}))) end def test_rewrite @@ -147,6 +148,14 @@ class PostgresqlJSONTest < ActiveRecord::TestCase assert_equal "320×480", y.resolution end + def test_yaml_round_trip_with_store_accessors + x = JsonDataType.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + y = YAML.load(YAML.dump(x)) + assert_equal "320×480", y.resolution + end + def test_update_all json = JsonDataType.create! payload: { "one" => "two" } diff --git a/activerecord/test/cases/adapters/postgresql/ltree_test.rb b/activerecord/test/cases/adapters/postgresql/ltree_test.rb index 718f37a380..ddb7cd658c 100644 --- a/activerecord/test/cases/adapters/postgresql/ltree_test.rb +++ b/activerecord/test/cases/adapters/postgresql/ltree_test.rb @@ -1,7 +1,5 @@ # encoding: utf-8 require "cases/helper" -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' class PostgresqlLtreeTest < ActiveRecord::TestCase class Ltree < ActiveRecord::Base diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb index e109f1682b..bdfeedafab 100644 --- a/activerecord/test/cases/adapters/postgresql/money_test.rb +++ b/activerecord/test/cases/adapters/postgresql/money_test.rb @@ -1,20 +1,28 @@ # encoding: utf-8 - require "cases/helper" -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' +require 'support/schema_dumping_helper' class PostgresqlMoneyTest < ActiveRecord::TestCase + include SchemaDumpingHelper + class PostgresqlMoney < ActiveRecord::Base; end setup do @connection = ActiveRecord::Base.connection @connection.execute("set lc_monetary = 'C'") + @connection.create_table('postgresql_moneys') do |t| + t.column "wealth", "money" + t.column "depth", "money", default: "150.55" + end + end + + teardown do + @connection.execute 'DROP TABLE IF EXISTS postgresql_moneys' end def test_column column = PostgresqlMoney.columns_hash["wealth"] - assert_equal :decimal, column.type + assert_equal :money, column.type assert_equal "money", column.sql_type assert_equal 2, column.scale assert column.number? @@ -23,6 +31,12 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase assert_not column.array end + def test_default + column = PostgresqlMoney.columns_hash["depth"] + assert_equal BigDecimal.new("150.55"), column.default + assert_equal BigDecimal.new("150.55"), PostgresqlMoney.new.depth + end + def test_money_values @connection.execute("INSERT INTO postgresql_moneys (id, wealth) VALUES (1, '567.89'::money)") @connection.execute("INSERT INTO postgresql_moneys (id, wealth) VALUES (2, '-567.89'::money)") @@ -35,10 +49,16 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase def test_money_type_cast column = PostgresqlMoney.columns_hash['wealth'] - assert_equal(12345678.12, column.type_cast("$12,345,678.12")) - assert_equal(12345678.12, column.type_cast("$12.345.678,12")) - assert_equal(-1.15, column.type_cast("-$1.15")) - assert_equal(-2.25, column.type_cast("($2.25)")) + assert_equal(12345678.12, column.type_cast_from_user("$12,345,678.12")) + assert_equal(12345678.12, column.type_cast_from_user("$12.345.678,12")) + assert_equal(-1.15, column.type_cast_from_user("-$1.15")) + assert_equal(-2.25, column.type_cast_from_user("($2.25)")) + end + + def test_schema_dumping + output = dump_table_schema("postgresql_moneys") + assert_match %r{t\.money\s+"wealth",\s+scale: 2$}, output + assert_match %r{t\.money\s+"depth",\s+scale: 2,\s+default: 150.55$}, output end def test_create_and_update_money diff --git a/activerecord/test/cases/adapters/postgresql/network_test.rb b/activerecord/test/cases/adapters/postgresql/network_test.rb index e99af07970..32085cbb17 100644 --- a/activerecord/test/cases/adapters/postgresql/network_test.rb +++ b/activerecord/test/cases/adapters/postgresql/network_test.rb @@ -1,8 +1,5 @@ # encoding: utf-8 - require "cases/helper" -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' class PostgresqlNetworkTest < ActiveRecord::TestCase class PostgresqlNetworkAddress < ActiveRecord::Base diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb index 060b17d071..0f6e39322c 100644 --- a/activerecord/test/cases/adapters/postgresql/range_test.rb +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -1,7 +1,5 @@ require "cases/helper" require 'support/connection_helper' -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' if ActiveRecord::Base.connection.supports_ranges? class PostgresqlRange < ActiveRecord::Base @@ -158,7 +156,7 @@ _SQL assert_equal 0.5..0.7, @first_range.float_range assert_equal 0.5...0.7, @second_range.float_range assert_equal 0.5...Float::INFINITY, @third_range.float_range - assert_equal (-Float::INFINITY...Float::INFINITY), @fourth_range.float_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.float_range) assert_nil @empty_range.float_range end diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb index d4102bf7be..3614b29190 100644 --- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb +++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb @@ -123,6 +123,18 @@ class TimestampTest < ActiveRecord::TestCase assert_equal date, Developer.find_by_name("aaron").updated_at end + def test_bc_timestamp_leap_year + date = Time.utc(-4, 2, 29) + Developer.create!(:name => "taihou", :updated_at => date) + assert_equal date, Developer.find_by_name("taihou").updated_at + end + + def test_bc_timestamp_year_zero + date = Time.utc(0, 4, 7) + Developer.create!(:name => "yahagi", :updated_at => date) + assert_equal date, Developer.find_by_name("yahagi").updated_at + end + private def pg_datetime_precision(table_name, column_name) diff --git a/activerecord/test/cases/adapters/postgresql/xml_test.rb b/activerecord/test/cases/adapters/postgresql/xml_test.rb index c1c85f8c92..48c6eeb62c 100644 --- a/activerecord/test/cases/adapters/postgresql/xml_test.rb +++ b/activerecord/test/cases/adapters/postgresql/xml_test.rb @@ -1,8 +1,5 @@ # encoding: utf-8 - require 'cases/helper' -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' class PostgresqlXMLTest < ActiveRecord::TestCase class XmlDataType < ActiveRecord::Base diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index 811695938e..8700b20dee 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -17,6 +17,20 @@ if ActiveRecord::Base.connection.supports_migrations? ActiveRecord::SchemaMigration.delete_all rescue nil end + def test_has_no_primary_key + old_primary_key_prefix_type = ActiveRecord::Base.primary_key_prefix_type + ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore + assert_nil ActiveRecord::SchemaMigration.primary_key + + ActiveRecord::SchemaMigration.create_table + assert_difference "ActiveRecord::SchemaMigration.count", 1 do + ActiveRecord::SchemaMigration.create version: 12 + end + ensure + ActiveRecord::SchemaMigration.drop_table + ActiveRecord::Base.primary_key_prefix_type = old_primary_key_prefix_type + end + def test_schema_define ActiveRecord::Schema.define(:version => 7) do create_table :fruits do |t| @@ -34,6 +48,7 @@ if ActiveRecord::Base.connection.supports_migrations? def test_schema_define_w_table_name_prefix table_name = ActiveRecord::SchemaMigration.table_name + old_table_name_prefix = ActiveRecord::Base.table_name_prefix ActiveRecord::Base.table_name_prefix = "nep_" ActiveRecord::SchemaMigration.table_name = "nep_#{table_name}" ActiveRecord::Schema.define(:version => 7) do @@ -46,7 +61,7 @@ if ActiveRecord::Base.connection.supports_migrations? end assert_equal 7, ActiveRecord::Migrator::current_version ensure - ActiveRecord::Base.table_name_prefix = "" + ActiveRecord::Base.table_name_prefix = old_table_name_prefix ActiveRecord::SchemaMigration.table_name = table_name end @@ -66,5 +81,12 @@ if ActiveRecord::Base.connection.supports_migrations? end assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" } end + + def test_normalize_version + assert_equal "118", ActiveRecord::SchemaMigration.normalize_migration_number("0000118") + assert_equal "002", ActiveRecord::SchemaMigration.normalize_migration_number("2") + assert_equal "017", ActiveRecord::SchemaMigration.normalize_migration_number("0017") + assert_equal "20131219224947", ActiveRecord::SchemaMigration.normalize_migration_number("20131219224947") + end end end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 4bd4486b41..910067666a 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -1239,6 +1239,10 @@ class EagerAssociationTest < ActiveRecord::TestCase } end + test "including association based on sql condition and no database column" do + assert_equal pets(:parrot), Owner.including_last_pet.first.last_pet + end + test "include instance dependent associations is deprecated" do message = "association scope 'posts_with_signature' is" assert_deprecated message do diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index 8d8201ddae..080c499444 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -872,6 +872,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_redefine_habtm child = SubDeveloper.new("name" => "Aredridel") child.special_projects << SpecialProject.new("name" => "Special Project") - assert_equal true, child.save + assert child.save, 'child object should be saved' end end diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 893030345f..a674a39d65 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -333,7 +333,7 @@ class InverseHasManyTests < ActiveRecord::TestCase def test_parent_instance_should_be_shared_within_create_block_of_new_child man = Man.first - interest = man.interests.build do |i| + interest = man.interests.create do |i| assert i.man.equal?(man), "Man of child should be the same instance as a parent" end assert interest.man.equal?(man), "Man of the child should still be the same instance as a parent" diff --git a/activerecord/test/cases/attribute_decorators_test.rb b/activerecord/test/cases/attribute_decorators_test.rb new file mode 100644 index 0000000000..35393753a2 --- /dev/null +++ b/activerecord/test/cases/attribute_decorators_test.rb @@ -0,0 +1,114 @@ +require 'cases/helper' + +module ActiveRecord + class AttributeDecoratorsTest < ActiveRecord::TestCase + class Model < ActiveRecord::Base + self.table_name = 'attribute_decorators_model' + end + + class StringDecorator < SimpleDelegator + def initialize(delegate, decoration = "decorated!") + @decoration = decoration + super(delegate) + end + + def type_cast_from_user(value) + "#{super} #{@decoration}" + end + + alias type_cast_from_database type_cast_from_user + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table :attribute_decorators_model, force: true do |t| + t.string :a_string + end + end + + teardown do + return unless @connection + @connection.execute 'DROP TABLE IF EXISTS attribute_decorators_model' + Model.attribute_type_decorations.clear + Model.reset_column_information + end + + test "attributes can be decorated" do + model = Model.new(a_string: 'Hello') + assert_equal 'Hello', model.a_string + + Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } + + model = Model.new(a_string: 'Hello') + assert_equal 'Hello decorated!', model.a_string + end + + test "decoration does not eagerly load existing columns" do + assert_no_queries do + Model.reset_column_information + Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } + end + end + + test "undecorated columns are not touched" do + Model.attribute :another_string, Type::String.new, default: 'something or other' + Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } + + assert_equal 'something or other', Model.new.another_string + end + + test "decorators can be chained" do + Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } + Model.decorate_attribute_type(:a_string, :other) { |t| StringDecorator.new(t) } + + model = Model.new(a_string: 'Hello!') + + assert_equal 'Hello! decorated! decorated!', model.a_string + end + + test "decoration of the same type multiple times is idempotent" do + Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } + Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } + + model = Model.new(a_string: 'Hello') + assert_equal 'Hello decorated!', model.a_string + end + + test "decorations occur in order of declaration" do + Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } + Model.decorate_attribute_type(:a_string, :other) do |type| + StringDecorator.new(type, 'decorated again!') + end + + model = Model.new(a_string: 'Hello!') + + assert_equal 'Hello! decorated! decorated again!', model.a_string + end + + test "decorating attributes does not modify parent classes" do + Model.attribute :another_string, Type::String.new, default: 'whatever' + Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } + child_class = Class.new(Model) + child_class.decorate_attribute_type(:another_string, :test) { |t| StringDecorator.new(t) } + child_class.decorate_attribute_type(:a_string, :other) { |t| StringDecorator.new(t) } + + model = Model.new(a_string: 'Hello!') + child = child_class.new(a_string: 'Hello!') + + assert_equal 'Hello! decorated!', model.a_string + assert_equal 'whatever', model.another_string + assert_equal 'Hello! decorated! decorated!', child.a_string + # We are round tripping the default, and we don't undo our decoration + assert_equal 'whatever decorated! decorated!', child.another_string + end + + test "defaults are decorated on the column" do + Model.attribute :a_string, Type::String.new, default: 'whatever' + Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } + + column = Model.columns_hash['a_string'] + + assert_equal 'whatever decorated!', column.default + end + end +end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index da2876170e..a366caf875 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -517,43 +517,17 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - def test_only_time_related_columns_are_meant_to_be_cached_by_default - expected = %w(datetime time date).sort - assert_equal expected, ActiveRecord::Base.attribute_types_cached_by_default.map(&:to_s).sort - end - - def test_declaring_attributes_as_cached_adds_them_to_the_attributes_cached_by_default - default_attributes = Topic.cached_attributes - Topic.cache_attributes :replies_count - expected = default_attributes + ["replies_count"] - assert_equal expected.sort, Topic.cached_attributes.sort - Topic.instance_variable_set "@cached_attributes", nil - end - - def test_cacheable_columns_are_actually_cached - assert_equal cached_columns.sort, Topic.cached_attributes.sort - end - - def test_accessing_cached_attributes_caches_the_converted_values_and_nothing_else - t = topics(:first) - cache = t.instance_variable_get "@attributes" - - assert_not_nil cache - assert cache.empty? + def test_deprecated_cache_attributes + assert_deprecated do + Topic.cache_attributes :replies_count + end - all_columns = Topic.columns.map(&:name) - uncached_columns = all_columns - cached_columns + assert_deprecated do + Topic.cached_attributes + end - all_columns.each do |attr_name| - attribute_gets_cached = Topic.cache_attribute?(attr_name) - val = t.send attr_name unless attr_name == "type" - if attribute_gets_cached - assert cached_columns.include?(attr_name) - assert_equal val, cache[attr_name] - else - assert uncached_columns.include?(attr_name) - assert !cache.include?(attr_name) - end + assert_deprecated do + Topic.cache_attribute? :replies_count end end @@ -870,7 +844,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase end def cached_columns - Topic.columns.map(&:name) - Topic.serialized_attributes.keys + Topic.columns.map(&:name) end def time_related_columns_on_topic diff --git a/activerecord/test/cases/custom_properties_test.rb b/activerecord/test/cases/attributes_test.rb index 9ba1e83df6..79ef0502cb 100644 --- a/activerecord/test/cases/custom_properties_test.rb +++ b/activerecord/test/cases/attributes_test.rb @@ -1,17 +1,17 @@ require 'cases/helper' class OverloadedType < ActiveRecord::Base - property :overloaded_float, Type::Integer.new - property :overloaded_string_with_limit, Type::String.new(limit: 50) - property :non_existent_decimal, Type::Decimal.new - property :string_with_default, Type::String.new, default: 'the overloaded default' + attribute :overloaded_float, Type::Integer.new + attribute :overloaded_string_with_limit, Type::String.new(limit: 50) + attribute :non_existent_decimal, Type::Decimal.new + attribute :string_with_default, Type::String.new, default: 'the overloaded default' end class ChildOfOverloadedType < OverloadedType end class GrandchildOfOverloadedType < ChildOfOverloadedType - property :overloaded_float, Type::Float.new + attribute :overloaded_float, Type::Float.new end class UnoverloadedType < ActiveRecord::Base @@ -54,7 +54,7 @@ module ActiveRecord assert_equal 255, UnoverloadedType.columns_hash['overloaded_string_with_limit'].limit end - def test_nonexistent_property + def test_nonexistent_attribute data = OverloadedType.new(non_existent_decimal: 1) assert_equal BigDecimal.new(1), data.non_existent_decimal @@ -98,7 +98,7 @@ module ActiveRecord assert_not klass.column_names.include?('wibble') assert_equal 5, klass.content_columns.length - klass.property :wibble, Type::Value.new + klass.attribute :wibble, Type::Value.new assert_equal 7, klass.columns.length assert klass.columns_hash.key?('wibble') diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index d65c4b0638..dc8f25b0e7 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -985,9 +985,9 @@ class BasicsTest < ActiveRecord::TestCase class NumericData < ActiveRecord::Base self.table_name = 'numeric_data' - property :world_population, Type::Integer.new - property :my_house_population, Type::Integer.new - property :atoms_in_universe, Type::Integer.new + attribute :world_population, Type::Integer.new + attribute :my_house_population, Type::Integer.new + attribute :atoms_in_universe, Type::Integer.new end def test_big_decimal_conditions @@ -1489,13 +1489,13 @@ class BasicsTest < ActiveRecord::TestCase attrs.delete 'id' typecast = Class.new { - def type_cast value + def type_cast_from_database value "t.lo" end } types = { 'author_name' => typecast.new } - topic = Topic.allocate.init_with 'attributes' => attrs, + topic = Topic.allocate.init_with 'raw_attributes' => attrs, 'column_types' => types assert_equal 't.lo', topic.author_name diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index b9445ee072..222b1505a8 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -16,9 +16,9 @@ Company.has_many :accounts class NumericData < ActiveRecord::Base self.table_name = 'numeric_data' - property :world_population, Type::Integer.new - property :my_house_population, Type::Integer.new - property :atoms_in_universe, Type::Integer.new + attribute :world_population, Type::Integer.new + attribute :my_house_population, Type::Integer.new + attribute :atoms_in_universe, Type::Integer.new end class CalculationsTest < ActiveRecord::TestCase @@ -610,4 +610,11 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal [1,2,3,4,5], taks_relation.pluck(:id) assert_equal [false, true, true, true, true], taks_relation.pluck(:approved) end + + def test_pluck_columns_with_same_name + expected = [["The First Topic", "The Second Topic of the day"], ["The Third Topic of the day", "The Fourth Topic of the day"]] + actual = Topic.joins(:replies) + .pluck('topics.title', 'replies_topics.title') + assert_equal expected, actual + end end diff --git a/activerecord/test/cases/connection_adapters/type_lookup_test.rb b/activerecord/test/cases/connection_adapters/type_lookup_test.rb index 3958c3bfff..d5c1dc1e5d 100644 --- a/activerecord/test/cases/connection_adapters/type_lookup_test.rb +++ b/activerecord/test/cases/connection_adapters/type_lookup_test.rb @@ -85,7 +85,7 @@ module ActiveRecord cast_type = @connection.type_map.lookup(type) assert_equal :decimal, cast_type.type - assert_equal 2, cast_type.type_cast(2.1) + assert_equal 2, cast_type.type_cast_from_user(2.1) end end diff --git a/activerecord/test/cases/core_test.rb b/activerecord/test/cases/core_test.rb index 2a52bf574c..715d92af99 100644 --- a/activerecord/test/cases/core_test.rb +++ b/activerecord/test/cases/core_test.rb @@ -1,6 +1,8 @@ require 'cases/helper' require 'models/person' require 'models/topic' +require 'pp' +require 'active_support/core_ext/string/strip' class NonExistentTable < ActiveRecord::Base; end @@ -30,4 +32,70 @@ class CoreTest < ActiveRecord::TestCase def test_inspect_class_without_table assert_equal "NonExistentTable(Table doesn't exist)", NonExistentTable.inspect end + + def test_pretty_print_new + topic = Topic.new + actual = '' + PP.pp(topic, StringIO.new(actual)) + expected = <<-PRETTY.strip_heredoc + #<Topic:0xXXXXXX + id: nil, + title: nil, + author_name: nil, + author_email_address: "test@test.com", + written_on: nil, + bonus_time: nil, + last_read: nil, + content: nil, + important: nil, + approved: true, + replies_count: 0, + unique_replies_count: 0, + parent_id: nil, + parent_title: nil, + type: nil, + group: nil, + created_at: nil, + updated_at: nil> + PRETTY + assert actual.start_with?(expected.split('XXXXXX').first) + assert actual.end_with?(expected.split('XXXXXX').last) + end + + def test_pretty_print_persisted + topic = topics(:first) + actual = '' + PP.pp(topic, StringIO.new(actual)) + expected = <<-PRETTY.strip_heredoc + #<Topic:0x\\w+ + id: 1, + title: "The First Topic", + author_name: "David", + author_email_address: "david@loudthinking.com", + written_on: 2003-07-16 14:28:11 UTC, + bonus_time: 2000-01-01 14:28:00 UTC, + last_read: Thu, 15 Apr 2004, + content: "Have a nice day", + important: nil, + approved: false, + replies_count: 1, + unique_replies_count: 0, + parent_id: nil, + parent_title: nil, + type: nil, + group: nil, + created_at: [^,]+, + updated_at: [^,>]+> + PRETTY + assert_match(/\A#{expected}\z/, actual) + end + + def test_pretty_print_uninitialized + topic = Topic.allocate + actual = '' + PP.pp(topic, StringIO.new(actual)) + expected = "#<Topic:XXXXXX not initialized>\n" + assert actual.start_with?(expected.split('XXXXXX').first) + assert actual.end_with?(expected.split('XXXXXX').last) + end end diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index f885a8cbc0..92144bc802 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -206,6 +206,11 @@ if current_adapter?(:PostgreSQLAdapter) assert_equal "some text", Default.new.text_col, "Default of text column was not correctly parse after updating default using '::text' since postgreSQL will add parens to the default in db" end + def test_default_containing_quote_and_colons + @connection.execute "ALTER TABLE defaults ALTER COLUMN string_col SET DEFAULT 'foo''::bar'" + assert_equal "foo'::bar", Default.new.string_col + end + teardown do @connection.schema_search_path = @old_search_path Default.reset_column_information diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index df4183c065..87f24e32b2 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -616,6 +616,32 @@ class DirtyTest < ActiveRecord::TestCase end end + test "defaults with type that implements `type_cast_for_database`" do + type = Class.new(ActiveRecord::Type::Value) do + def type_cast(value) + value.to_i + end + + def type_cast_for_database(value) + value.to_s + end + end + + model_class = Class.new(ActiveRecord::Base) do + self.table_name = 'numeric_data' + attribute :foo, type.new, default: 1 + end + + model = model_class.new + assert_not model.foo_changed? + + model = model_class.new(foo: 1) + assert_not model.foo_changed? + + model = model_class.new(foo: '1') + assert_not model.foo_changed? + end + private def with_partial_writes(klass, on = true) old = klass.partial_writes? diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index c0440744e9..bd77c412f6 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -33,6 +33,17 @@ class FinderTest < ActiveRecord::TestCase assert_equal(topics(:first).title, Topic.find(1).title) end + def test_find_with_proc_parameter_and_block + exception = assert_raises(RuntimeError) do + Topic.all.find(-> { raise "should happen" }) { |e| e.title == "non-existing-title" } + end + assert_equal "should happen", exception.message + + assert_nothing_raised(RuntimeError) do + Topic.all.find(-> { raise "should not happen" }) { |e| e.title == topics(:first).title } + end + end + def test_find_passing_active_record_object_is_deprecated assert_deprecated do Topic.find(Topic.last) diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 8bbc0af758..042fdaf0bb 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -84,12 +84,6 @@ class FixturesTest < ActiveRecord::TestCase assert fixtures.detect { |f| f.name == 'collections' }, "no fixtures named 'collections' in #{fixtures.map(&:name).inspect}" end - def test_create_symbol_fixtures_is_deprecated - assert_deprecated do - ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT, :collections, :collections => 'Course') { Course.connection } - end - end - def test_attributes topics = create_fixtures("topics").first assert_equal("The First Topic", topics["first"]["title"]) diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 93fd3b9605..c221430757 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -272,10 +272,6 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert p.treasures.empty? assert RichPerson.connection.select_all("SELECT * FROM peoples_treasures WHERE rich_person_id = 1").empty? end - - def test_quoted_locking_column_is_deprecated - assert_deprecated { ActiveRecord::Base.quoted_locking_column } - end end class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index aa679d4a35..9855835e27 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -13,10 +13,10 @@ require MIGRATIONS_ROOT + "/decimal/1_give_me_big_numbers" class BigNumber < ActiveRecord::Base unless current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter) - property :value_of_e, Type::Integer.new + attribute :value_of_e, Type::Integer.new end - property :world_population, Type::Integer.new - property :my_house_population, Type::Integer.new + attribute :world_population, Type::Integer.new + attribute :my_house_population, Type::Integer.new end class Reminder < ActiveRecord::Base; end @@ -333,47 +333,6 @@ class MigrationTest < ActiveRecord::TestCase Reminder.reset_table_name end - def test_proper_table_name_on_migrator - reminder_class = new_isolated_reminder_class - assert_deprecated do - assert_equal "table", ActiveRecord::Migrator.proper_table_name('table') - end - assert_deprecated do - assert_equal "table", ActiveRecord::Migrator.proper_table_name(:table) - end - assert_deprecated do - assert_equal "reminders", ActiveRecord::Migrator.proper_table_name(reminder_class) - end - reminder_class.reset_table_name - assert_deprecated do - assert_equal reminder_class.table_name, ActiveRecord::Migrator.proper_table_name(reminder_class) - end - - # Use the model's own prefix/suffix if a model is given - ActiveRecord::Base.table_name_prefix = "ARprefix_" - ActiveRecord::Base.table_name_suffix = "_ARsuffix" - reminder_class.table_name_prefix = 'prefix_' - reminder_class.table_name_suffix = '_suffix' - reminder_class.reset_table_name - assert_deprecated do - assert_equal "prefix_reminders_suffix", ActiveRecord::Migrator.proper_table_name(reminder_class) - end - reminder_class.table_name_prefix = '' - reminder_class.table_name_suffix = '' - reminder_class.reset_table_name - - # Use AR::Base's prefix/suffix if string or symbol is given - ActiveRecord::Base.table_name_prefix = "prefix_" - ActiveRecord::Base.table_name_suffix = "_suffix" - reminder_class.reset_table_name - assert_deprecated do - assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name('table') - end - assert_deprecated do - assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name(:table) - end - end - def test_proper_table_name_on_migration reminder_class = new_isolated_reminder_class migration = ActiveRecord::Migration.new diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index bc5ccd0fe9..7f5d0d66d3 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -251,7 +251,7 @@ class PersistenceTest < ActiveRecord::TestCase def test_create_columns_not_equal_attributes topic = Topic.allocate.init_with( - 'attributes' => { + 'raw_attributes' => { 'title' => 'Another New Topic', 'does_not_exist' => 'test' } @@ -302,7 +302,7 @@ class PersistenceTest < ActiveRecord::TestCase topic_reloaded = Topic.allocate topic_reloaded.init_with( - 'attributes' => topic.attributes.merge('does_not_exist' => 'test') + 'raw_attributes' => topic.attributes.merge('does_not_exist' => 'test') ) topic_reloaded.title = 'A New Topic' assert_nothing_raised { topic_reloaded.save } @@ -333,6 +333,15 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal "Reply", topic.type end + def test_update_sti_subclass_type + assert_instance_of Topic, topics(:first) + + reply = topics(:first).becomes!(Reply) + assert_instance_of Reply, reply + reply.save! + assert_instance_of Reply, Reply.find(reply.id) + end + def test_update_after_create klass = Class.new(Topic) do def self.name; 'Topic'; end diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index c719918fd7..f43483d291 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -92,6 +92,7 @@ class PrimaryKeysTest < ActiveRecord::TestCase end def test_primary_key_prefix + old_primary_key_prefix_type = ActiveRecord::Base.primary_key_prefix_type ActiveRecord::Base.primary_key_prefix_type = :table_name Topic.reset_primary_key assert_equal "topicid", Topic.primary_key @@ -103,6 +104,8 @@ class PrimaryKeysTest < ActiveRecord::TestCase ActiveRecord::Base.primary_key_prefix_type = nil Topic.reset_primary_key assert_equal "id", Topic.primary_key + ensure + ActiveRecord::Base.primary_key_prefix_type = old_primary_key_prefix_type end def test_delete_should_quote_pkey diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index e6603f28be..c6e73f78cc 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -80,6 +80,26 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal :integer, @first.column_for_attribute("id").type end + def test_non_existent_columns_return_null_object + column = @first.column_for_attribute("attribute_that_doesnt_exist") + assert_instance_of ActiveRecord::ConnectionAdapters::NullColumn, column + assert_equal "attribute_that_doesnt_exist", column.name + assert_equal nil, column.sql_type + assert_equal nil, column.type + assert_not column.number? + assert_not column.text? + assert_not column.binary? + end + + def test_non_existent_columns_are_identity_types + column = @first.column_for_attribute("attribute_that_doesnt_exist") + object = Object.new + + assert_equal object, column.type_cast_from_database(object) + assert_equal object, column.type_cast_from_user(object) + assert_equal object, column.type_cast_for_database(object) + end + def test_reflection_klass_for_nested_class_name reflection = MacroReflection.new(:company, nil, nil, { :class_name => 'MyApplication::Business::Company' }, ActiveRecord::Base) assert_nothing_raised do diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 9602252b2e..5f02d39e32 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require 'support/schema_dumping_helper' class SchemaDumperTest < ActiveRecord::TestCase setup do @@ -62,7 +63,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|uuid)\s+"/) + if match = column.match(/t\.(?:integer|decimal|float|datetime|timestamp|time|date|text|binary|string|boolean|uuid|point)\s+"/) match[0].length end end @@ -393,7 +394,7 @@ class SchemaDumperTest < ActiveRecord::TestCase output = standard_dump assert_no_match %r{create_table "foo_.+_bar"}, output - assert_no_match %r{create_index "foo_.+_bar"}, output + assert_no_match %r{add_index "foo_.+_bar"}, output assert_no_match %r{create_table "schema_migrations"}, output ensure migration.migrate(:down) @@ -403,3 +404,31 @@ class SchemaDumperTest < ActiveRecord::TestCase end end + +class SchemaDumperDefaultsTest < ActiveRecord::TestCase + include SchemaDumpingHelper + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table :defaults, force: true do |t| + t.string :string_with_default, default: "Hello!" + t.date :date_with_default, default: '2014-06-05' + t.datetime :datetime_with_default, default: "2014-06-05 07:17:04" + t.time :time_with_default, default: "07:17:04" + end + end + + teardown do + return unless @connection + @connection.execute 'DROP TABLE IF EXISTS defaults' + end + + def test_schema_dump_defaults_with_universally_supported_types + output = dump_table_schema('defaults') + + assert_match %r{t\.string\s+"string_with_default",\s+default: "Hello!"}, output + assert_match %r{t\.date\s+"date_with_default",\s+default: '2014-06-05'}, output + assert_match %r{t\.datetime\s+"datetime_with_default",\s+default: '2014-06-05 07:17:04'}, output + assert_match %r{t\.time\s+"time_with_default",\s+default: '2000-01-01 07:17:04'}, output + end +end diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index eaa28148f4..7d1c240638 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -14,6 +14,13 @@ class SerializedAttributeTest < ActiveRecord::TestCase Topic.serialize("content") end + def test_serialize_does_not_eagerly_load_columns + assert_no_queries do + Topic.reset_column_information + Topic.serialize(:content) + end + end + def test_list_of_serialized_attributes assert_equal %w(content), Topic.serialized_attributes.keys end @@ -31,7 +38,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase def test_serialized_attribute_init_with topic = Topic.allocate - topic.init_with('attributes' => { 'content' => '--- foo' }) + topic.init_with('raw_attributes' => { 'content' => '--- foo' }) assert_equal 'foo', topic.content end @@ -59,8 +66,9 @@ class SerializedAttributeTest < ActiveRecord::TestCase def test_serialized_attribute_calling_dup_method Topic.serialize :content, JSON - t = Topic.new(:content => { :foo => :bar }).dup - assert_equal({ :foo => :bar }, t.content_before_type_cast) + orig = Topic.new(content: { foo: :bar }) + clone = orig.dup + assert_equal(orig.content, clone.content) end def test_serialized_attribute_declared_in_subclass @@ -103,8 +111,10 @@ class SerializedAttributeTest < ActiveRecord::TestCase def test_serialized_attribute_should_raise_exception_on_save_with_wrong_type Topic.serialize(:content, Hash) - topic = Topic.new(:content => "string") - assert_raise(ActiveRecord::SerializationTypeMismatch) { topic.save } + assert_raise(ActiveRecord::SerializationTypeMismatch) do + topic = Topic.new(content: 'string') + topic.save + end end def test_should_raise_exception_on_serialized_attribute_with_type_mismatch diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index 6a34c55011..f841b1c983 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -183,20 +183,6 @@ class StoreTest < ActiveRecord::TestCase assert_equal({}, @john.params) end - test "attributes_for_coder should return stored fields already serialized" do - attributes = { - "id" => @john.id, - "name"=> @john.name, - "settings" => "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\ncolor: black\n", - "preferences" => "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nremember_login: true\n", - "json_data" => "{\"height\":\"tall\"}", "json_data_empty"=>"{\"is_a_good_guy\":true}", - "params" => "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess {}\n", - "account_id"=> @john.account_id - } - - assert_equal attributes, @john.attributes_for_coder - end - test "dump, load and dump again a model" do dumped = YAML.dump(@john) loaded = YAML.load(dumped) diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 77ab427be0..0472246f71 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -71,24 +71,6 @@ class TimestampTest < ActiveRecord::TestCase assert_equal @previously_updated_at, @developer.updated_at end - def test_saving_when_callback_sets_record_timestamps_to_false_doesnt_update_its_timestamp - klass = Class.new(Developer) do - before_update :cancel_record_timestamps - def cancel_record_timestamps - self.record_timestamps = false - return true - end - end - - developer = klass.first - previously_updated_at = developer.updated_at - - developer.name = "New Name" - developer.save! - - assert_equal previously_updated_at, developer.updated_at - end - def test_touching_an_attribute_updates_timestamp previously_created_at = @developer.created_at @developer.touch(:created_at) @@ -399,6 +381,19 @@ class TimestampTest < ActiveRecord::TestCase assert_not_equal time, pet.updated_at end + def test_timestamp_column_values_are_present_in_the_callbacks + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'people' + + before_create do + self.born_at = self.created_at + end + end + + person = klass.create first_name: 'David' + assert_not_equal person.born_at, nil + end + def test_timestamp_attributes_for_create toy = Toy.first assert_equal [:created_at, :created_on], toy.send(:timestamp_attributes_for_create) diff --git a/activerecord/test/cases/types_test.rb b/activerecord/test/cases/types_test.rb index 5d5f442d3a..731f8cfba3 100644 --- a/activerecord/test/cases/types_test.rb +++ b/activerecord/test/cases/types_test.rb @@ -6,141 +6,141 @@ module ActiveRecord class TypesTest < ActiveRecord::TestCase def test_type_cast_boolean type = Type::Boolean.new - assert type.type_cast('').nil? - assert type.type_cast(nil).nil? - - assert type.type_cast(true) - assert type.type_cast(1) - assert type.type_cast('1') - assert type.type_cast('t') - assert type.type_cast('T') - assert type.type_cast('true') - assert type.type_cast('TRUE') - assert type.type_cast('on') - assert type.type_cast('ON') + assert type.type_cast_from_user('').nil? + assert type.type_cast_from_user(nil).nil? + + assert type.type_cast_from_user(true) + assert type.type_cast_from_user(1) + assert type.type_cast_from_user('1') + assert type.type_cast_from_user('t') + assert type.type_cast_from_user('T') + assert type.type_cast_from_user('true') + assert type.type_cast_from_user('TRUE') + assert type.type_cast_from_user('on') + assert type.type_cast_from_user('ON') # explicitly check for false vs nil - assert_equal false, type.type_cast(false) - assert_equal false, type.type_cast(0) - assert_equal false, type.type_cast('0') - assert_equal false, type.type_cast('f') - assert_equal false, type.type_cast('F') - assert_equal false, type.type_cast('false') - assert_equal false, type.type_cast('FALSE') - assert_equal false, type.type_cast('off') - assert_equal false, type.type_cast('OFF') - assert_equal false, type.type_cast(' ') - assert_equal false, type.type_cast("\u3000\r\n") - assert_equal false, type.type_cast("\u0000") - assert_equal false, type.type_cast('SOMETHING RANDOM') + assert_equal false, type.type_cast_from_user(false) + assert_equal false, type.type_cast_from_user(0) + assert_equal false, type.type_cast_from_user('0') + assert_equal false, type.type_cast_from_user('f') + assert_equal false, type.type_cast_from_user('F') + assert_equal false, type.type_cast_from_user('false') + assert_equal false, type.type_cast_from_user('FALSE') + assert_equal false, type.type_cast_from_user('off') + assert_equal false, type.type_cast_from_user('OFF') + assert_equal false, type.type_cast_from_user(' ') + assert_equal false, type.type_cast_from_user("\u3000\r\n") + assert_equal false, type.type_cast_from_user("\u0000") + assert_equal false, type.type_cast_from_user('SOMETHING RANDOM') end def test_type_cast_string type = Type::String.new - assert_equal "1", type.type_cast(true) - assert_equal "0", type.type_cast(false) - assert_equal "123", type.type_cast(123) + assert_equal "1", type.type_cast_from_user(true) + assert_equal "0", type.type_cast_from_user(false) + assert_equal "123", type.type_cast_from_user(123) end def test_type_cast_integer type = Type::Integer.new - assert_equal 1, type.type_cast(1) - assert_equal 1, type.type_cast('1') - assert_equal 1, type.type_cast('1ignore') - assert_equal 0, type.type_cast('bad1') - assert_equal 0, type.type_cast('bad') - assert_equal 1, type.type_cast(1.7) - assert_equal 0, type.type_cast(false) - assert_equal 1, type.type_cast(true) - assert_nil type.type_cast(nil) + assert_equal 1, type.type_cast_from_user(1) + assert_equal 1, type.type_cast_from_user('1') + assert_equal 1, type.type_cast_from_user('1ignore') + assert_equal 0, type.type_cast_from_user('bad1') + assert_equal 0, type.type_cast_from_user('bad') + assert_equal 1, type.type_cast_from_user(1.7) + assert_equal 0, type.type_cast_from_user(false) + assert_equal 1, type.type_cast_from_user(true) + assert_nil type.type_cast_from_user(nil) end def test_type_cast_non_integer_to_integer type = Type::Integer.new - assert_nil type.type_cast([1,2]) - assert_nil type.type_cast({1 => 2}) - assert_nil type.type_cast((1..2)) + assert_nil type.type_cast_from_user([1,2]) + assert_nil type.type_cast_from_user({1 => 2}) + assert_nil type.type_cast_from_user((1..2)) end def test_type_cast_activerecord_to_integer type = Type::Integer.new firm = Firm.create(:name => 'Apple') - assert_nil type.type_cast(firm) + assert_nil type.type_cast_from_user(firm) end def test_type_cast_object_without_to_i_to_integer type = Type::Integer.new - assert_nil type.type_cast(Object.new) + assert_nil type.type_cast_from_user(Object.new) end def test_type_cast_nan_and_infinity_to_integer type = Type::Integer.new - assert_nil type.type_cast(Float::NAN) - assert_nil type.type_cast(1.0/0.0) + assert_nil type.type_cast_from_user(Float::NAN) + assert_nil type.type_cast_from_user(1.0/0.0) end def test_type_cast_float type = Type::Float.new - assert_equal 1.0, type.type_cast("1") + assert_equal 1.0, type.type_cast_from_user("1") end def test_type_cast_decimal type = Type::Decimal.new - assert_equal BigDecimal.new("0"), type.type_cast(BigDecimal.new("0")) - assert_equal BigDecimal.new("123"), type.type_cast(123.0) - assert_equal BigDecimal.new("1"), type.type_cast(:"1") + assert_equal BigDecimal.new("0"), type.type_cast_from_user(BigDecimal.new("0")) + assert_equal BigDecimal.new("123"), type.type_cast_from_user(123.0) + assert_equal BigDecimal.new("1"), type.type_cast_from_user(:"1") end def test_type_cast_binary type = Type::Binary.new - assert_equal nil, type.type_cast(nil) - assert_equal "1", type.type_cast("1") - assert_equal 1, type.type_cast(1) + assert_equal nil, type.type_cast_from_user(nil) + assert_equal "1", type.type_cast_from_user("1") + assert_equal 1, type.type_cast_from_user(1) end def test_type_cast_time type = Type::Time.new - assert_equal nil, type.type_cast(nil) - assert_equal nil, type.type_cast('') - assert_equal nil, type.type_cast('ABC') + assert_equal nil, type.type_cast_from_user(nil) + assert_equal nil, type.type_cast_from_user('') + assert_equal nil, type.type_cast_from_user('ABC') time_string = Time.now.utc.strftime("%T") - assert_equal time_string, type.type_cast(time_string).strftime("%T") + assert_equal time_string, type.type_cast_from_user(time_string).strftime("%T") end def test_type_cast_datetime_and_timestamp type = Type::DateTime.new - assert_equal nil, type.type_cast(nil) - assert_equal nil, type.type_cast('') - assert_equal nil, type.type_cast(' ') - assert_equal nil, type.type_cast('ABC') + assert_equal nil, type.type_cast_from_user(nil) + assert_equal nil, type.type_cast_from_user('') + assert_equal nil, type.type_cast_from_user(' ') + assert_equal nil, type.type_cast_from_user('ABC') datetime_string = Time.now.utc.strftime("%FT%T") - assert_equal datetime_string, type.type_cast(datetime_string).strftime("%FT%T") + assert_equal datetime_string, type.type_cast_from_user(datetime_string).strftime("%FT%T") end def test_type_cast_date type = Type::Date.new - assert_equal nil, type.type_cast(nil) - assert_equal nil, type.type_cast('') - assert_equal nil, type.type_cast(' ') - assert_equal nil, type.type_cast('ABC') + assert_equal nil, type.type_cast_from_user(nil) + assert_equal nil, type.type_cast_from_user('') + assert_equal nil, type.type_cast_from_user(' ') + assert_equal nil, type.type_cast_from_user('ABC') date_string = Time.now.utc.strftime("%F") - assert_equal date_string, type.type_cast(date_string).strftime("%F") + assert_equal date_string, type.type_cast_from_user(date_string).strftime("%F") end def test_type_cast_duration_to_integer type = Type::Integer.new - assert_equal 1800, type.type_cast(30.minutes) - assert_equal 7200, type.type_cast(2.hours) + assert_equal 1800, type.type_cast_from_user(30.minutes) + assert_equal 7200, type.type_cast_from_user(2.hours) end def test_string_to_time_with_timezone [:utc, :local].each do |zone| with_timezone_config default: zone do type = Type::DateTime.new - assert_equal Time.utc(2013, 9, 4, 0, 0, 0), type.type_cast("Wed, 04 Sep 2013 03:00:00 EAT") + assert_equal Time.utc(2013, 9, 4, 0, 0, 0), type.type_cast_from_user("Wed, 04 Sep 2013 03:00:00 EAT") end end end @@ -149,7 +149,7 @@ module ActiveRecord def test_binary_encoding type = SQLite3Binary.new utf8_string = "a string".encode(Encoding::UTF_8) - type_cast = type.type_cast(utf8_string) + type_cast = type.type_cast_from_user(utf8_string) assert_equal Encoding::ASCII_8BIT, type_cast.encoding end diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb index 1a690c01a6..c34e7d5a30 100644 --- a/activerecord/test/cases/xml_serialization_test.rb +++ b/activerecord/test/cases/xml_serialization_test.rb @@ -416,8 +416,9 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase def test_should_support_aliased_attributes xml = Author.select("name as firstname").to_xml - array = Hash.from_xml(xml)['authors'] - assert_equal array.size, array.select { |author| author.has_key? 'firstname' }.size + Author.all.each do |author| + assert xml.include?(%(<firstname>#{author.name}</firstname>)), xml + end end def test_array_to_xml_including_has_many_association diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb index 15815d56e4..9f1d110ddb 100644 --- a/activerecord/test/cases/yaml_serialization_test.rb +++ b/activerecord/test/cases/yaml_serialization_test.rb @@ -1,8 +1,10 @@ require 'cases/helper' require 'models/topic' +require 'models/post' +require 'models/author' class YamlSerializationTest < ActiveRecord::TestCase - fixtures :topics + fixtures :topics, :authors, :posts def test_to_yaml_with_time_with_zone_should_not_raise_exception with_timezone_config aware_attributes: true, zone: "Pacific Time (US & Canada)" do @@ -23,13 +25,6 @@ class YamlSerializationTest < ActiveRecord::TestCase assert_equal({:omg=>:lol}, YAML.load(YAML.dump(topic)).content) end - def test_encode_with_coder - topic = Topic.first - coder = {} - topic.encode_with coder - assert_equal({'attributes' => topic.attributes}, coder) - end - def test_psych_roundtrip topic = Topic.first assert topic @@ -47,4 +42,44 @@ class YamlSerializationTest < ActiveRecord::TestCase def test_active_record_relation_serialization [Topic.all].to_yaml end + + def test_raw_types_are_not_changed_on_round_trip + topic = Topic.new(parent_id: "123") + assert_equal "123", topic.parent_id_before_type_cast + assert_equal "123", YAML.load(YAML.dump(topic)).parent_id_before_type_cast + end + + def test_cast_types_are_not_changed_on_round_trip + topic = Topic.new(parent_id: "123") + assert_equal 123, topic.parent_id + assert_equal 123, YAML.load(YAML.dump(topic)).parent_id + end + + def test_new_records_remain_new_after_round_trip + topic = Topic.new + + assert topic.new_record?, "Sanity check that new records are new" + assert YAML.load(YAML.dump(topic)).new_record?, "Record should be new after deserialization" + + topic.save! + + assert_not topic.new_record?, "Saved records are not new" + assert_not YAML.load(YAML.dump(topic)).new_record?, "Saved record should not be new after deserialization" + + topic = Topic.select('title').last + + assert_not topic.new_record?, "Loaded records without ID are not new" + assert_not YAML.load(YAML.dump(topic)).new_record?, "Record should not be new after deserialization" + end + + def test_types_of_virtual_columns_are_not_changed_on_round_trip + author = Author.select('authors.*, count(posts.id) as posts_count') + .joins(:posts) + .group('authors.id') + .first + dumped = YAML.load(YAML.dump(author)) + + assert_equal 5, author.posts_count + assert_equal 5, dumped.posts_count + end end diff --git a/activerecord/test/models/owner.rb b/activerecord/test/models/owner.rb index cf24502d3a..2e3a9a3681 100644 --- a/activerecord/test/models/owner.rb +++ b/activerecord/test/models/owner.rb @@ -3,6 +3,18 @@ class Owner < ActiveRecord::Base has_many :pets, -> { order 'pets.name desc' } has_many :toys, :through => :pets + belongs_to :last_pet, class_name: 'Pet' + scope :including_last_pet, -> { + select(%q[ + owners.*, ( + select p.pet_id from pets p + where p.owner_id = owners.owner_id + order by p.name desc + limit 1 + ) as last_pet_id + ]).includes(:last_pet) + } + after_commit :execute_blocks def blocks diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index 4fcbf4dbd2..e9294a11b9 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -1,6 +1,6 @@ ActiveRecord::Schema.define do - %w(postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings postgresql_uuids postgresql_ltrees + %w(postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_uuids postgresql_ltrees postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent postgresql_json_data_type postgresql_citext).each do |table_name| execute "DROP TABLE IF EXISTS #{quote_table_name table_name}" end @@ -118,13 +118,6 @@ _SQL end execute <<_SQL - CREATE TABLE postgresql_moneys ( - id SERIAL PRIMARY KEY, - wealth MONEY - ); -_SQL - - execute <<_SQL CREATE TABLE postgresql_numbers ( id SERIAL PRIMARY KEY, single REAL, @@ -150,14 +143,6 @@ _SQL _SQL execute <<_SQL - CREATE TABLE postgresql_bit_strings ( - id SERIAL PRIMARY KEY, - bit_string BIT(8), - bit_string_varying BIT VARYING(8) - ); -_SQL - - execute <<_SQL CREATE TABLE postgresql_oids ( id SERIAL PRIMARY KEY, obj_id OID diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 5f459cf682..98fd0b6803 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -529,6 +529,7 @@ ActiveRecord::Schema.define do t.references :best_friend t.references :best_friend_of t.integer :insures, null: false, default: 0 + t.timestamp :born_at t.timestamps end diff --git a/activerecord/test/support/schema_dumping_helper.rb b/activerecord/test/support/schema_dumping_helper.rb new file mode 100644 index 0000000000..2ae8d299e5 --- /dev/null +++ b/activerecord/test/support/schema_dumping_helper.rb @@ -0,0 +1,11 @@ +module SchemaDumpingHelper + def dump_table_schema(table, connection = ActiveRecord::Base.connection) + old_ignore_tables = ActiveRecord::SchemaDumper.ignore_tables + ActiveRecord::SchemaDumper.ignore_tables = connection.tables - [table] + stream = StringIO.new + ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) + stream.string + ensure + ActiveRecord::SchemaDumper.ignore_tables = old_ignore_tables + end +end diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 38ec3c0753..5962dd255c 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,39 @@ +* Fixed `ActiveSupport::Cache::FileStore` exploding with long paths. + *Adam Panzer / Michael Grosser* + +* Fixed `ActiveSupport::TimeWithZone#-` so precision is not unnecessarily lost + when working with objects with a nanosecond component. + + `ActiveSupport::TimeWithZone#-` should return the same result as if we were + using `Time#-`: + + Time.now.end_of_day - Time.now.beginning_of_day #=> 86399.999999999 + + Before: + + Time.zone.now.end_of_day.nsec #=> 999999999 + Time.zone.now.end_of_day - Time.zone.now.beginning_of_day #=> 86400.0 + + After: + + Time.zone.now.end_of_day - Time.zone.now.beginning_of_day + #=> 86399.999999999 + + *Gordon Chan* + +* Fixed precision error in NumberHelper when using Rationals. + + before: + ActiveSupport::NumberHelper.number_to_rounded Rational(1000, 3), precision: 2 + #=> "330.00" + after: + ActiveSupport::NumberHelper.number_to_rounded Rational(1000, 3), precision: 2 + #=> "333.33" + + See #15379. + + *Juanjo Bazán* + * Removed deprecated `Numeric#ago` and friends Replacements: diff --git a/activesupport/README.rdoc b/activesupport/README.rdoc index f3582767c0..a6424a353a 100644 --- a/activesupport/README.rdoc +++ b/activesupport/README.rdoc @@ -30,6 +30,11 @@ API documentation is at: * http://api.rubyonrails.org -Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here: +Bug reports can be filed for the Ruby on Rails project here: * https://github.com/rails/rails/issues + +Feature requests should be discussed on the rails-core mailing list here: + +* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core + diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index 8ed60aebac..d08ecd2f7d 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -14,6 +14,7 @@ module ActiveSupport DIR_FORMATTER = "%03X" FILENAME_MAX_SIZE = 228 # max filename size on file system is 255, minus room for timestamp and random characters appended by Tempfile (used by atomic write) + FILEPATH_MAX_SIZE = 900 # max is 1024, plus some room EXCLUDED_DIRS = ['.', '..'].freeze def initialize(cache_path, options = nil) @@ -117,6 +118,10 @@ module ActiveSupport # Translate a key into a file path. def key_file_path(key) + if key.size > FILEPATH_MAX_SIZE + key = Digest::MD5.hexdigest(key) + end + fname = URI.encode_www_form_component(key) hash = Zlib.adler32(fname) hash, dir_1 = hash.divmod(0x1000) diff --git a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb index 970d6faa1d..28cb3e2a3b 100644 --- a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb +++ b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb @@ -18,6 +18,6 @@ class Hash # # b = { b: 1 } # { a: b }.with_indifferent_access['a'] # calls b.nested_under_indifferent_access - # # => {"b"=>32} + # # => {"b"=>1} alias nested_under_indifferent_access with_indifferent_access end diff --git a/activesupport/lib/active_support/core_ext/string/output_safety.rb b/activesupport/lib/active_support/core_ext/string/output_safety.rb index 2c8995be9a..46cd170c1d 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -19,12 +19,7 @@ class ERB # puts html_escape('is a > 0 & a < 10?') # # => is a > 0 & a < 10? def html_escape(s) - s = s.to_s - if s.html_safe? - s - else - s.gsub(HTML_ESCAPE_REGEXP, HTML_ESCAPE).html_safe - end + unwrapped_html_escape(s).html_safe end # Aliasing twice issues a warning "discarding old...". Remove first to avoid it. @@ -36,6 +31,18 @@ class ERB singleton_class.send(:remove_method, :html_escape) module_function :html_escape + # HTML escapes strings but doesn't wrap them with an ActiveSupport::SafeBuffer. + # This method is not for public consumption! Seriously! + def unwrapped_html_escape(s) # :nodoc: + s = s.to_s + if s.html_safe? + s + else + s.gsub(HTML_ESCAPE_REGEXP, HTML_ESCAPE) + end + end + module_function :unwrapped_html_escape + # A utility method for escaping HTML without affecting existing escaped entities. # # html_escape_once('1 < 2 & 3') @@ -170,13 +177,15 @@ module ActiveSupport #:nodoc: self[0, 0] end - %w[concat prepend].each do |method_name| - define_method method_name do |value| - super(html_escape_interpolated_argument(value)) - end + def concat(value) + super(html_escape_interpolated_argument(value)) end alias << concat + def prepend(value) + super(html_escape_interpolated_argument(value)) + end + def prepend!(value) ActiveSupport::Deprecation.deprecation_warning "ActiveSupport::SafeBuffer#prepend!", :prepend prepend value @@ -231,7 +240,8 @@ module ActiveSupport #:nodoc: private def html_escape_interpolated_argument(arg) - (!html_safe? || arg.html_safe?) ? arg : ERB::Util.h(arg) + (!html_safe? || arg.html_safe?) ? arg : + arg.to_s.gsub(ERB::Util::HTML_ESCAPE_REGEXP, ERB::Util::HTML_ESCAPE) end end end diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index e782cfa2f5..e1eb81b8bc 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -57,7 +57,7 @@ module ActiveSupport def initialize(constructor = {}) if constructor.respond_to?(:to_hash) super() - update(constructor.to_hash) + update(constructor) else super(constructor) end diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb index 7a96c66626..325a3d75dc 100644 --- a/activesupport/lib/active_support/notifications.rb +++ b/activesupport/lib/active_support/notifications.rb @@ -141,6 +141,11 @@ module ActiveSupport # # ActiveSupport::Notifications.unsubscribe(subscriber) # + # You can also unsubscribe by passing the name of the subscriber object. Note + # that this will unsubscribe all subscriptions with the given name: + # + # ActiveSupport::Notifications.unsubscribe("render") + # # == Default Queue # # Notifications ships with a queue implementation that consumes and publishes events @@ -173,8 +178,8 @@ module ActiveSupport unsubscribe(subscriber) end - def unsubscribe(args) - notifier.unsubscribe(args) + def unsubscribe(subscriber_or_name) + notifier.unsubscribe(subscriber_or_name) end def instrumenter diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb index 8f5fa646e8..6bf8c7d5de 100644 --- a/activesupport/lib/active_support/notifications/fanout.rb +++ b/activesupport/lib/active_support/notifications/fanout.rb @@ -25,9 +25,15 @@ module ActiveSupport subscriber end - def unsubscribe(subscriber) + def unsubscribe(subscriber_or_name) synchronize do - @subscribers.reject! { |s| s.matches?(subscriber) } + case subscriber_or_name + when String + @subscribers.reject! { |s| s.matches?(subscriber_or_name) } + else + @subscribers.delete(subscriber_or_name) + end + @listeners_for.clear end end @@ -97,12 +103,11 @@ module ActiveSupport end def subscribed_to?(name) - @pattern === name.to_s + @pattern === name end - def matches?(subscriber_or_name) - self === subscriber_or_name || - @pattern && @pattern === subscriber_or_name + def matches?(name) + @pattern && @pattern === name end end diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb index b169e3af01..5ecda9593a 100644 --- a/activesupport/lib/active_support/number_helper.rb +++ b/activesupport/lib/active_support/number_helper.rb @@ -232,12 +232,8 @@ module ActiveSupport # number_to_human_size(1234567, precision: 2) # => 1.2 MB # number_to_human_size(483989, precision: 2) # => 470 KB # number_to_human_size(1234567, precision: 2, separator: ',') # => 1,2 MB - # - # Non-significant zeros after the fractional separator are stripped out by - # default (set <tt>:strip_insignificant_zeros</tt> to +false+ to change that): - # - # number_to_human_size(1234567890123, precision: 5) # => "1.1229 TB" - # number_to_human_size(524288000, precision: 5) # => "500 MB" + # number_to_human_size(1234567890123, precision: 5) # => "1.1228 TB" + # number_to_human_size(524288000, precision: 5) # => "500 MB" def number_to_human_size(number, options = {}) NumberToHumanSizeConverter.convert(number, options) end @@ -305,12 +301,15 @@ module ActiveSupport # separator: ',', # significant: false) # => "1,2 Million" # + # number_to_human(500000000, precision: 5) # => "500 Million" + # number_to_human(12345012345, significant: false) # => "12.345 Billion" + # # Non-significant zeros after the decimal separator are stripped # out by default (set <tt>:strip_insignificant_zeros</tt> to # +false+ to change that): # - # number_to_human(12345012345, significant_digits: 6) # => "12.345 Billion" - # number_to_human(500000000, precision: 5) # => "500 Million" + # number_to_human(12.00001) # => "12" + # number_to_human(12.00001, strip_insignificant_zeros: false) # => "12.0" # # ==== Custom Unit Quantifiers # diff --git a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb index c45f6cdcfa..01597b288a 100644 --- a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb @@ -12,11 +12,7 @@ module ActiveSupport when Float, String @number = BigDecimal(number.to_s) when Rational - if significant - @number = BigDecimal(number, digit_count(number.to_i) + precision) - else - @number = BigDecimal(number, precision) - end + @number = BigDecimal(number, digit_count(number.to_i) + precision) else @number = number.to_d end diff --git a/activesupport/lib/active_support/testing/declarative.rb b/activesupport/lib/active_support/testing/declarative.rb index e709e6edf5..0bf3643a56 100644 --- a/activesupport/lib/active_support/testing/declarative.rb +++ b/activesupport/lib/active_support/testing/declarative.rb @@ -1,30 +1,6 @@ module ActiveSupport module Testing module Declarative - - def self.extended(klass) #:nodoc: - klass.class_eval do - - unless method_defined?(:describe) - def self.describe(text) - if block_given? - super - else - message = "`describe` without a block is deprecated, please switch to: `def self.name; #{text.inspect}; end`\n" - ActiveSupport::Deprecation.warn message - - class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 - def self.name - "#{text}" - end - RUBY_EVAL - end - end - end - - end - end - unless defined?(Spec) # Helper to define a test method using a String. Under the hood, it replaces # spaces with underscores and defines the test method. diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 3155c0ab0f..4a0ed356b1 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -262,7 +262,7 @@ module ActiveSupport # If we're subtracting a Duration of variable length (i.e., years, months, days), move backwards from #time, # otherwise move backwards #utc, for accuracy when moving across DST boundaries if other.acts_like?(:time) - utc.to_f - other.to_f + to_time - other.to_time elsif duration_of_variable_length?(other) method_missing(:-, other) else diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index 18923f61d1..d55cc5d3b0 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -692,6 +692,11 @@ class FileStoreTest < ActiveSupport::TestCase assert File.exist?(filepath) end + def test_long_keys + @cache.write("a"*10000, 1) + assert_equal 1, @cache.read("a"*10000) + end + def test_key_transformation key = @cache.send(:key_file_path, "views/index?id=1") assert_equal "views/index?id=1", @cache.send(:file_path_key, key) diff --git a/activesupport/test/constantize_test_cases.rb b/activesupport/test/constantize_test_cases.rb index bbeb710a0c..8a9fd4996b 100644 --- a/activesupport/test/constantize_test_cases.rb +++ b/activesupport/test/constantize_test_cases.rb @@ -27,13 +27,24 @@ module ConstantizeTestCases assert_equal Ace::Base::Case, yield("Ace::Base::Case") assert_equal Ace::Base::Case, yield("::Ace::Base::Case") assert_equal Ace::Base::Case::Dice, yield("Ace::Base::Case::Dice") + assert_equal Ace::Base::Case::Dice, yield("Ace::Base::Fase::Dice") assert_equal Ace::Base::Fase::Dice, yield("Ace::Base::Fase::Dice") + assert_equal Ace::Gas::Case, yield("Ace::Gas::Case") assert_equal Ace::Gas::Case::Dice, yield("Ace::Gas::Case::Dice") + assert_equal Ace::Base::Case::Dice, yield("Ace::Gas::Case::Dice") + assert_equal Case::Dice, yield("Case::Dice") + assert_equal AddtlGlobalConstants::Case::Dice, yield("Case::Dice") + assert_equal Object::AddtlGlobalConstants::Case::Dice, yield("Case::Dice") + assert_equal Case::Dice, yield("Object::Case::Dice") + assert_equal AddtlGlobalConstants::Case::Dice, yield("Object::Case::Dice") + assert_equal Object::AddtlGlobalConstants::Case::Dice, yield("Case::Dice") + assert_equal ConstantizeTestCases, yield("ConstantizeTestCases") assert_equal ConstantizeTestCases, yield("::ConstantizeTestCases") + assert_raises(NameError) { yield("UnknownClass") } assert_raises(NameError) { yield("UnknownClass::Ace") } assert_raises(NameError) { yield("UnknownClass::Ace::Base") } diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index cb706d77c2..f18172a5ce 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -70,6 +70,8 @@ class HashExtTest < ActiveSupport::TestCase assert_respond_to h, :to_options! assert_respond_to h, :compact assert_respond_to h, :compact! + assert_respond_to h, :except + assert_respond_to h, :except! end def test_transform_keys @@ -657,6 +659,14 @@ class HashExtTest < ActiveSupport::TestCase assert_equal 1, h['first'] end + def test_to_options_on_indifferent_preserves_works_as_hash_with_dup + h = HashWithIndifferentAccess.new({ a: { b: 'b' } }) + dup = h.dup + + dup[:a][:c] = 'c' + assert_equal 'c', h[:a][:c] + end + def test_indifferent_sub_hashes h = {'user' => {'id' => 5}}.with_indifferent_access ['user', :user].each {|user| [:id, 'id'].each {|id| assert_equal 5, h[user][id], "h[#{user.inspect}][#{id.inspect}] should be 5"}} @@ -919,13 +929,19 @@ class HashExtTest < ActiveSupport::TestCase def test_except_with_more_than_one_argument original = { :a => 'x', :b => 'y', :c => 10 } expected = { :a => 'x' } + assert_equal expected, original.except(:b, :c) + + assert_equal expected, original.except!(:b, :c) + assert_equal expected, original end def test_except_with_original_frozen original = { :a => 'x', :b => 'y' } original.freeze assert_nothing_raised { original.except(:a) } + + assert_raise(RuntimeError) { original.except!(:a) } end def test_except_with_mocha_expectation_on_original diff --git a/activesupport/test/core_ext/blank_test.rb b/activesupport/test/core_ext/object/blank_test.rb index 246bc7fa61..246bc7fa61 100644 --- a/activesupport/test/core_ext/blank_test.rb +++ b/activesupport/test/core_ext/object/blank_test.rb diff --git a/activesupport/test/core_ext/object/inclusion_test.rb b/activesupport/test/core_ext/object/inclusion_test.rb index b054a8dd31..32d512eca3 100644 --- a/activesupport/test/core_ext/object/inclusion_test.rb +++ b/activesupport/test/core_ext/object/inclusion_test.rb @@ -37,11 +37,14 @@ class InTest < ActiveSupport::TestCase end class C < B end + class D + end def test_in_module assert A.in?(B) assert A.in?(C) assert !A.in?(A) + assert !A.in?(D) end def test_no_method_catching @@ -51,5 +54,6 @@ class InTest < ActiveSupport::TestCase def test_presence_in assert_equal "stuff", "stuff".presence_in(%w( lots of stuff )) assert_nil "stuff".presence_in(%w( lots of crap )) + assert_raise(ArgumentError) { 1.presence_in(1) } end end diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index 7fe4d4a6b2..6d779bf3d5 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -246,16 +246,31 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_equal 86_400.0, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2), ActiveSupport::TimeZone['Hawaii'] ) - Time.utc(2000, 1, 1) end + def test_minus_with_time_precision + assert_equal 86_399.999999998, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone['UTC'] ) - Time.utc(2000, 1, 2, 0, 0, 0, Rational(1, 1000)) + assert_equal 86_399.999999998, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone['Hawaii'] ) - Time.utc(2000, 1, 2, 0, 0, 0, Rational(1, 1000)) + end + def test_minus_with_time_with_zone twz1 = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1), ActiveSupport::TimeZone['UTC'] ) twz2 = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2), ActiveSupport::TimeZone['UTC'] ) assert_equal 86_400.0, twz2 - twz1 end + def test_minus_with_time_with_zone_precision + twz1 = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 0, 0, 0, Rational(1, 1000)), ActiveSupport::TimeZone['UTC'] ) + twz2 = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone['UTC'] ) + assert_equal 86_399.999999998, twz2 - twz1 + end + def test_minus_with_datetime assert_equal 86_400.0, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2), ActiveSupport::TimeZone['UTC'] ) - DateTime.civil(2000, 1, 1) end + def test_minus_with_datetime_precision + assert_equal 86_399.999999999, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone['UTC'] ) - DateTime.civil(2000, 1, 1) + end + def test_minus_with_wrapped_datetime assert_equal 86_400.0, ActiveSupport::TimeWithZone.new( DateTime.civil(2000, 1, 2), ActiveSupport::TimeZone['UTC'] ) - Time.utc(2000, 1, 1) assert_equal 86_400.0, ActiveSupport::TimeWithZone.new( DateTime.civil(2000, 1, 2), ActiveSupport::TimeZone['UTC'] ) - DateTime.civil(2000, 1, 1) diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb index a7a0ae02e7..bb51cc68f2 100644 --- a/activesupport/test/number_helper_test.rb +++ b/activesupport/test/number_helper_test.rb @@ -134,6 +134,7 @@ module ActiveSupport assert_equal("111.23460000000000000000", number_helper.number_to_rounded('111.2346', :precision => 20)) assert_equal("111.23460000000000000000", number_helper.number_to_rounded(BigDecimal(111.2346, Float::DIG), :precision => 20)) assert_equal("111.2346" + "0"*96, number_helper.number_to_rounded('111.2346', :precision => 100)) + assert_equal("111.2346", number_helper.number_to_rounded(Rational(1112346, 10000), :precision => 4)) end end @@ -174,6 +175,7 @@ module ActiveSupport assert_equal "9775.0000000000000000", number_helper.number_to_rounded(BigDecimal(9775), :precision => 20, :significant => true ) assert_equal "9775.0000000000000000", number_helper.number_to_rounded("9775", :precision => 20, :significant => true ) assert_equal "9775." + "0"*96, number_helper.number_to_rounded("9775", :precision => 100, :significant => true ) + assert_equal("97.7", number_helper.number_to_rounded(Rational(9772, 100), :precision => 3, :significant => true)) end end diff --git a/guides/code/getting_started/config/environments/production.rb b/guides/code/getting_started/config/environments/production.rb index c8ae858574..60adba903a 100644 --- a/guides/code/getting_started/config/environments/production.rb +++ b/guides/code/getting_started/config/environments/production.rb @@ -16,10 +16,10 @@ Rails.application.configure do # Enable Rack::Cache to put a simple HTTP cache in front of your application # Add `rack-cache` to your Gemfile before enabling this. - # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. + # For large-scale production use, consider using a caching reverse proxy like NGINX, varnish or squid. # config.action_dispatch.rack_cache = true - # Disable Rails's static asset server (Apache or nginx will already do this). + # Disable Rails's static asset server (Apache or NGINX will already do this). config.serve_static_assets = false # Compress JavaScripts and CSS. @@ -36,8 +36,8 @@ Rails.application.configure do config.assets.version = '1.0' # Specifies the header that your server uses for sending files. - # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache - # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true diff --git a/guides/source/2_3_release_notes.md b/guides/source/2_3_release_notes.md index 2302a618b6..52eeb4c2bc 100644 --- a/guides/source/2_3_release_notes.md +++ b/guides/source/2_3_release_notes.md @@ -8,7 +8,7 @@ Rails 2.3 delivers a variety of new and improved features, including pervasive R Application Architecture ------------------------ -There are two major changes in the architecture of Rails applications: complete integration of the [Rack](http://rack.rubyforge.org/) modular web server interface, and renewed support for Rails Engines. +There are two major changes in the architecture of Rails applications: complete integration of the [Rack](http://rack.github.io/) modular web server interface, and renewed support for Rails Engines. ### Rack Integration diff --git a/guides/source/3_0_release_notes.md b/guides/source/3_0_release_notes.md index 2d7c06837b..aec3a383d6 100644 --- a/guides/source/3_0_release_notes.md +++ b/guides/source/3_0_release_notes.md @@ -521,7 +521,7 @@ A large effort was made in Active Support to make it cherry pickable, that is, y These are the main changes in Active Support: * Large clean up of the library removing unused methods throughout. -* Active Support no longer provides vendored versions of [TZInfo](http://tzinfo.rubyforge.org/), [Memcache Client](http://deveiate.org/projects/RMemCache/) and [Builder](http://builder.rubyforge.org/,) these are all included as dependencies and installed via the `bundle install` command. +* Active Support no longer provides vendored versions of TZInfo, Memcache Client and Builder. These are all included as dependencies and installed via the `bundle install` command. * Safe buffers are implemented in `ActiveSupport::SafeBuffer`. * Added `Array.uniq_by` and `Array.uniq_by!`. * Removed `Array#rand` and backported `Array#sample` from Ruby 1.9. diff --git a/guides/source/3_1_release_notes.md b/guides/source/3_1_release_notes.md index 485f8c756b..7626296e7d 100644 --- a/guides/source/3_1_release_notes.md +++ b/guides/source/3_1_release_notes.md @@ -173,7 +173,7 @@ The assets pipeline is powered by [Sprockets](https://github.com/sstephenson/spr ### HTTP Streaming -HTTP Streaming is another change that is new in Rails 3.1. This lets the browser download your stylesheets and JavaScript files while the server is still generating the response. This requires Ruby 1.9.2, is opt-in and requires support from the web server as well, but the popular combo of nginx and unicorn is ready to take advantage of it. +HTTP Streaming is another change that is new in Rails 3.1. This lets the browser download your stylesheets and JavaScript files while the server is still generating the response. This requires Ruby 1.9.2, is opt-in and requires support from the web server as well, but the popular combo of NGINX and Unicorn is ready to take advantage of it. ### Default JS library is now jQuery diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index 1735188f27..3d15319ca4 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -1078,7 +1078,7 @@ Rails keeps a log file for each environment in the `log` folder. These are extre ### Parameters Filtering -You can filter certain request parameters from your log files by appending them to `config.filter_parameters` in the application configuration. These parameters will be marked [FILTERED] in the log. +You can filter out sensitive request parameters from your log files by appending them to `config.filter_parameters` in the application configuration. These parameters will be marked [FILTERED] in the log. ```ruby config.filter_parameters << :password @@ -1086,7 +1086,7 @@ config.filter_parameters << :password ### Redirects Filtering -Sometimes it's desirable to filter out from log files some sensible locations your application is redirecting to. +Sometimes it's desirable to filter out from log files some sensitive locations your application is redirecting to. You can do that by using the `config.filter_redirect` configuration option: ```ruby diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index c67f6188c4..c7117027c0 100644 --- a/guides/source/action_mailer_basics.md +++ b/guides/source/action_mailer_basics.md @@ -17,7 +17,10 @@ After reading this guide, you will know: Introduction ------------ -Action Mailer allows you to send emails from your application using mailer classes and views. Mailers work very similarly to controllers. They inherit from `ActionMailer::Base` and live in `app/mailers`, and they have associated views that appear in `app/views`. +Action Mailer allows you to send emails from your application using mailer classes +and views. Mailers work very similarly to controllers. They inherit from +`ActionMailer::Base` and live in `app/mailers`, and they have associated views +that appear in `app/views`. Sending Emails -------------- @@ -84,8 +87,11 @@ Here is a quick explanation of the items presented in the preceding method. For a full list of all available options, please have a look further down at the Complete List of Action Mailer user-settable attributes section. -* `default Hash` - This is a hash of default values for any email you send from this mailer. In this case we are setting the `:from` header to a value for all messages in this class. This can be overridden on a per-email basis. -* `mail` - The actual email message, we are passing the `:to` and `:subject` headers in. +* `default Hash` - This is a hash of default values for any email you send from +this mailer. In this case we are setting the `:from` header to a value for all +messages in this class. This can be overridden on a per-email basis. +* `mail` - The actual email message, we are passing the `:to` and `:subject` +headers in. Just like controllers, any instance variables we define in the method become available for use in the views. @@ -151,7 +157,7 @@ $ bin/rake db:migrate ``` Now that we have a user model to play with, we will just edit the -`app/controllers/users_controller.rb` make it instruct the UserMailer to deliver +`app/controllers/users_controller.rb` make it instruct the `UserMailer` to deliver an email to the newly created user by editing the create action and inserting a call to `UserMailer.welcome_email` right after the user is successfully saved: @@ -230,9 +236,11 @@ different, encode your content and pass in the encoded content and encoding in a ```ruby encoded_content = SpecialEncode(File.read('/path/to/filename.jpg')) - attachments['filename.jpg'] = {mime_type: 'application/x-gzip', - encoding: 'SpecialEncoding', - content: encoded_content } + attachments['filename.jpg'] = { + mime_type: 'application/x-gzip', + encoding: 'SpecialEncoding', + content: encoded_content + } ``` NOTE: If you specify an encoding, Mail will assume that your content is already @@ -608,7 +616,7 @@ files (environment.rb, production.rb, etc...) | Configuration | Description | |---------------|-------------| |`logger`|Generates information on the mailing run if available. Can be set to `nil` for no logging. Compatible with both Ruby's own `Logger` and `Log4r` loggers.| -|`smtp_settings`|Allows detailed configuration for `:smtp` delivery method:<ul><li>`:address` - Allows you to use a remote mail server. Just change it from its default "localhost" setting.</li><li>`:port` - On the off chance that your mail server doesn't run on port 25, you can change it.</li><li>`:domain` - If you need to specify a HELO domain, you can do it here.</li><li>`:user_name` - If your mail server requires authentication, set the username in this setting.</li><li>`:password` - If your mail server requires authentication, set the password in this setting.</li><li>`:authentication` - If your mail server requires authentication, you need to specify the authentication type here. This is a symbol and one of `:plain`, `:login`, `:cram_md5`.</li><li>`:enable_starttls_auto` - Set this to `false` if there is a problem with your server certificate that you cannot resolve.</li></ul>| +|`smtp_settings`|Allows detailed configuration for `:smtp` delivery method:<ul><li>`:address` - Allows you to use a remote mail server. Just change it from its default `"localhost"` setting.</li><li>`:port` - On the off chance that your mail server doesn't run on port 25, you can change it.</li><li>`:domain` - If you need to specify a HELO domain, you can do it here.</li><li>`:user_name` - If your mail server requires authentication, set the username in this setting.</li><li>`:password` - If your mail server requires authentication, set the password in this setting.</li><li>`:authentication` - If your mail server requires authentication, you need to specify the authentication type here. This is a symbol and one of `:plain`, `:login`, `:cram_md5`.</li><li>`:enable_starttls_auto` - Set this to `false` if there is a problem with your server certificate that you cannot resolve.</li></ul>| |`sendmail_settings`|Allows you to override options for the `:sendmail` delivery method.<ul><li>`:location` - The location of the sendmail executable. Defaults to `/usr/sbin/sendmail`.</li><li>`:arguments` - The command line arguments to be passed to sendmail. Defaults to `-i -t`.</li></ul>| |`raise_delivery_errors`|Whether or not errors should be raised if the email fails to be delivered. This only works if the external email server is configured for immediate delivery.| |`delivery_method`|Defines a delivery method. Possible values are:<ul><li>`:smtp` (default), can be configured by using `config.action_mailer.smtp_settings`.</li><li>`:sendmail`, can be configured by using `config.action_mailer.sendmail_settings`.</li><li>`:file`: save emails to files; can be configured by using `config.action_mailer.file_settings`.</li><li>`:test`: save emails to `ActionMailer::Base.deliveries` array.</li></ul>See [API docs](http://api.rubyonrails.org/classes/ActionMailer/Base.html) for more info.| @@ -617,7 +625,7 @@ files (environment.rb, production.rb, etc...) |`default_options`|Allows you to set default values for the `mail` method options (`:from`, `:reply_to`, etc.).| For a complete writeup of possible configurations see the -[Action Mailer section](configuring.html#configuring-action-mailer) in +[Configuring Action Mailer](configuring.html#configuring-action-mailer) in our Configuring Rails Applications guide. ### Example Action Mailer Configuration @@ -662,6 +670,7 @@ You can find detailed instructions on how to test your mailers in the Intercepting Emails ------------------- + There are situations where you need to edit an email before it's delivered. Fortunately Action Mailer provides hooks to intercept every email. You can register an interceptor to make modifications to mail messages @@ -685,5 +694,5 @@ ActionMailer::Base.register_interceptor(SandboxEmailInterceptor) if Rails.env.st NOTE: The example above uses a custom environment called "staging" for a production like server but for testing purposes. You can read -[Creating Rails environments](./configuring.html#creating-rails-environments) +[Creating Rails environments](configuring.html#creating-rails-environments) for more information about custom Rails environments. diff --git a/guides/source/migrations.md b/guides/source/active_record_migrations.md index 6742c05946..5a550d9e55 100644 --- a/guides/source/migrations.md +++ b/guides/source/active_record_migrations.md @@ -293,16 +293,10 @@ end You can append as many column name/type pairs as you want. -### Supported Type Modifiers +### Passing Modifiers -You can also specify some options just after the field type between curly -braces. You can use the following modifiers: - -* `limit` Sets the maximum size of the `string/text/binary/integer` fields. -* `precision` Defines the precision for the `decimal` fields, representing the total number of digits in the number. -* `scale` Defines the scale for the `decimal` fields, representing the number of digits after the decimal point. -* `polymorphic` Adds a `type` column for `belongs_to` associations. -* `null` Allows or disallows `NULL` values in the column. +Some commonly used [type modifiers](#column-modifiers) can be passed directly on +the command line. They are enclosed by curly braces and follow the field type: For instance, running: @@ -321,6 +315,8 @@ class AddDetailsToProducts < ActiveRecord::Migration end ``` +TIP: Have a look at the generators help output for further details. + Writing a Migration ------------------- @@ -415,6 +411,44 @@ end removes the `description` and `name` columns, creates a `part_number` string column and adds an index on it. Finally it renames the `upccode` column. +### Changing Columns + +Like the `remove_column` and `add_column` Rails provides the `change_column` +migration method. + +```ruby +change_column :products, :part_number, :text +``` + +This changes the column `part_number` on products table to be a `:text` field. + +Besides `change_column`, the `change_column_null` and `change_column_default` +methods are used specifically to change the null and default values of a +column. + +```ruby +change_column_null :products, :name, false +change_column_default :products, :approved, false +``` + +This sets `:name` field on products to a `NOT NULL` column and the default +value of the `:approved` field to false. + +### Column Modifiers + +Column modifiers can be applied when creating or changing a column: + +* `limit` Sets the maximum size of the `string/text/binary/integer` fields. +* `precision` Defines the precision for the `decimal` fields, representing the total number of digits in the number. +* `scale` Defines the scale for the `decimal` fields, representing the number of digits after the decimal point. +* `polymorphic` Adds a `type` column for `belongs_to` associations. +* `null` Allows or disallows `NULL` values in the column. +* `default` Allows to set a default value on the column. NOTE: If using a dynamic value (such as date), the default will only be calculated the first time (e.g. on the date the migration is applied.) +* `index` Adds an index for the column. + +Some adapters may support additional options; see the adapter specific API docs +for further information. + ### When Helpers aren't Enough If the helpers provided by Active Record aren't enough you can use the `execute` diff --git a/guides/source/active_record_postgresql.md b/guides/source/active_record_postgresql.md index dfa488773e..fa135b537c 100644 --- a/guides/source/active_record_postgresql.md +++ b/guides/source/active_record_postgresql.md @@ -51,11 +51,13 @@ Document.create payload: data ```ruby # db/migrate/20140207133952_create_books.rb -create_table :book do |t| +create_table :books do |t| t.string 'title' t.string 'tags', array: true t.integer 'ratings', array: true end +add_index :books, :tags, using: 'gin' +add_index :books, :ratings, using: 'gin' # app/models/book.rb class Book < ActiveRecord::Base diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index ee8cf4ade6..486e7b80ff 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -659,6 +659,23 @@ FROM orders GROUP BY date(created_at) ``` +### Total of grouped items + +To get the total of grouped items on a single query call `count` after the `group`. + +```ruby +Order.group(:status).count +# => { 'awaiting_approval' => 7, 'paid' => 12 } +``` + +The SQL that would be executed would be something like this: + +```sql +SELECT COUNT (*) AS count_all, status AS status +FROM "orders" +GROUP BY status +``` + Having ------ @@ -1143,10 +1160,11 @@ Even though Active Record lets you specify conditions on the eager loaded associ However if you must do this, you may use `where` as you would normally. ```ruby -Article.includes(:comments).where("comments.visible" => true) +Article.includes(:comments).where(comments: { visible: true }) ``` -This would generate a query which contains a `LEFT OUTER JOIN` whereas the `joins` method would generate one using the `INNER JOIN` function instead. +This would generate a query which contains a `LEFT OUTER JOIN` whereas the +`joins` method would generate one using the `INNER JOIN` function instead. ```ruby SELECT "articles"."id" AS t0_r0, ... "comments"."updated_at" AS t1_r5 FROM "articles" LEFT OUTER JOIN "comments" ON "comments"."article_id" = "articles"."id" WHERE (comments.visible = 1) @@ -1154,7 +1172,19 @@ This would generate a query which contains a `LEFT OUTER JOIN` whereas the `join If there was no `where` condition, this would generate the normal set of two queries. -If, in the case of this `includes` query, there were no comments for any articles, all the articles would still be loaded. By using `joins` (an INNER JOIN), the join conditions **must** match, otherwise no records will be returned. +NOTE: Using `where` like this will only work when you pass it a Hash. For +SQL-fragments you need use `references` to force joined tables: + +```ruby +Article.includes(:comments).where("comments.visible = true").references(:comments) +``` + +If, in the case of this `includes` query, there were no comments for any +articles, all the articles would still be loaded. By using `joins` (an INNER +JOIN), the join conditions **must** match, otherwise no records will be +returned. + + Scopes ------ diff --git a/guides/source/api_documentation_guidelines.md b/guides/source/api_documentation_guidelines.md index 2a3bb4e34d..dda9baa8d7 100644 --- a/guides/source/api_documentation_guidelines.md +++ b/guides/source/api_documentation_guidelines.md @@ -13,7 +13,13 @@ After reading this guide, you will know: RDoc ---- -The Rails API documentation is generated with RDoc. Please consult the documentation for help with the [markup](http://rdoc.rubyforge.org/RDoc/Markup.html), and also take into account these [additional directives](http://rdoc.rubyforge.org/RDoc/Parser/Ruby.html). +The [Rails API documentation](http://api.rubyonrails.org) is generated with RDoc. + +```bash + bundle exec rake rdoc +``` + +Resulting HTML files can be found in the ./doc/rdoc directory. Wording ------- diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index 559d3f5e7d..2d1548f252 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -709,7 +709,7 @@ The default matcher for compiling files includes `application.js`, automatically) from `app/assets` folders including your gems: ```ruby -[ Proc.new { |path, fn| fn =~ /app\/assets/ && !%w(.js .css).include?(File.extname(path)) }, +[ Proc.new { |filename, path| path =~ /app\/assets/ && !%w(.js .css).include?(File.extname(filename)) }, /application.(css|js)$/ ] ``` @@ -788,13 +788,15 @@ For Apache: # `mod_expires` to be enabled. <Location /assets/> # Use of ETag is discouraged when Last-Modified is present - Header unset ETag FileETag None + Header unset ETag + FileETag None # RFC says only cache for 1 year - ExpiresActive On ExpiresDefault "access plus 1 year" + ExpiresActive On + ExpiresDefault "access plus 1 year" </Location> ``` -For nginx: +For NGINX: ```nginx location ~ ^/assets/ { @@ -816,7 +818,7 @@ compression ratio, thus reducing the size of the data transfer to the minimum. On the other hand, web servers can be configured to serve compressed content directly from disk, rather than deflating non-compressed files themselves. -Nginx is able to do this automatically enabling `gzip_static`: +NGINX is able to do this automatically enabling `gzip_static`: ```nginx location ~ ^/(assets)/ { @@ -835,7 +837,7 @@ the module compiled. Otherwise, you may need to perform a manual compilation: ./configure --with-http_gzip_static_module ``` -If you're compiling nginx with Phusion Passenger you'll need to pass that option +If you're compiling NGINX with Phusion Passenger you'll need to pass that option when prompted. A robust configuration for Apache is possible but tricky; please Google around. @@ -854,10 +856,12 @@ duplication of work. Local compilation allows you to commit the compiled files into source control, and deploy as normal. -There are two caveats: +There are three caveats: * You must not run the Capistrano deployment task that precompiles assets. -* You must change the following two application configuration settings. +* You must ensure any necessary compressors or minifiers are +available on your development system. +* You must change the following application configuration setting: In `config/environments/development.rb`, place the following line: @@ -871,9 +875,6 @@ development mode, and pass all requests to Sprockets. The prefix is still set to would serve the precompiled assets from `/assets` in development, and you would not see any local changes until you compile assets again. -You will also need to ensure any necessary compressors or minifiers are -available on your development system. - In practice, this will allow you to precompile locally, have those files in your working tree, and commit those files to source control when needed. Development mode will work as expected. @@ -920,7 +921,7 @@ cache forever. This can cause problems. If you use Every cache is different, so evaluate how your CDN handles caching and make sure that it plays nicely with the pipeline. You may find quirks related to your -specific set up, you may not. The defaults nginx uses, for example, should give +specific set up, you may not. The defaults NGINX uses, for example, should give you no problems when used as an HTTP cache. If you want to serve only some assets from your CDN, you can use custom @@ -1024,12 +1025,12 @@ this passes responsibility for serving the file to the web server, which is faster. Have a look at [send_file](http://api.rubyonrails.org/classes/ActionController/DataStreaming.html#method-i-send_file) on how to use this feature. -Apache and nginx support this option, which can be enabled in +Apache and NGINX support this option, which can be enabled in `config/environments/production.rb`: ```ruby -# config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache -# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx +# config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache +# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX ``` WARNING: If you are upgrading an existing application and intend to use this @@ -1039,7 +1040,7 @@ and any other environments you define with production behavior (not TIP: For further details have a look at the docs of your production web server: - [Apache](https://tn123.org/mod_xsendfile/) -- [Nginx](http://wiki.nginx.org/XSendfile) +- [NGINX](http://wiki.nginx.org/XSendfile) Assets Cache Store ------------------ diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md index c652aa6a80..3e39ecdad2 100644 --- a/guides/source/caching_with_rails.md +++ b/guides/source/caching_with_rails.md @@ -28,7 +28,7 @@ config.action_controller.perform_caching = true ### Page Caching -Page caching is a Rails mechanism which allows the request for a generated page to be fulfilled by the webserver (i.e. Apache or nginx), without ever having to go through the Rails stack at all. Obviously, this is super-fast. Unfortunately, it can't be applied to every situation (such as pages that need authentication) and since the webserver is literally just serving a file from the filesystem, cache expiration is an issue that needs to be dealt with. +Page caching is a Rails mechanism which allows the request for a generated page to be fulfilled by the webserver (i.e. Apache or NGINX), without ever having to go through the Rails stack at all. Obviously, this is super-fast. Unfortunately, it can't be applied to every situation (such as pages that need authentication) and since the webserver is literally just serving a file from the filesystem, cache expiration is an issue that needs to be dealt with. INFO: Page Caching has been removed from Rails 4. See the [actionpack-page_caching gem](https://github.com/rails/actionpack-page_caching). See [DHH's key-based cache expiration overview](http://signalvnoise.com/posts/3113-how-key-based-cache-expiration-works) for the newly-preferred method. diff --git a/guides/source/command_line.md b/guides/source/command_line.md index 6efc64c385..7e60f929a1 100644 --- a/guides/source/command_line.md +++ b/guides/source/command_line.md @@ -123,19 +123,18 @@ Usage: rails generate controller NAME [action action] [options] Description: ... - To create a controller within a module, specify the controller name as a - path like 'parent_module/controller_name'. + To create a controller within a module, specify the controller name as a path like 'parent_module/controller_name'. ... Example: - `rails generate controller CreditCard open debit credit close` + `rails generate controller CreditCards open debit credit close` - Credit card controller with URLs like /credit_card/debit. + Credit card controller with URLs like /credit_cards/debit. Controller: app/controllers/credit_card_controller.rb - Test: test/controllers/credit_card_controller_test.rb - Views: app/views/credit_card/debit.html.erb [...] - Helper: app/helpers/credit_card_helper.rb + Test: test/controllers/credit_cards_controller_test.rb + Views: app/views/credit_cards/debit.html.erb [...] + Helper: app/helpers/credit_cards_helper.rb ``` The controller generator is expecting parameters in the form of `generate controller ControllerName action1 action2`. Let's make a `Greetings` controller with an action of **hello**, which will say something nice to us. diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 7a9e1beb23..a0f0738fba 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -120,7 +120,7 @@ numbers. New applications filter out passwords by adding the following `config.f * `secrets.secret_key_base` is used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get `secrets.secret_key_base` initialized to a random key present in `config/secrets.yml`. -* `config.serve_static_assets` configures Rails itself to serve static assets. Defaults to true, but in the production environment is turned off as the server software (e.g. Nginx or Apache) used to run the application should serve static assets instead. Unlike the default setting set this to true when running (absolutely not recommended!) or testing your app in production mode using WEBrick. Otherwise you won't be able use page caching and requests for files that exist regularly under the public directory will anyway hit your Rails app. +* `config.serve_static_assets` configures Rails itself to serve static assets. Defaults to true, but in the production environment is turned off as the server software (e.g. NGINX or Apache) used to run the application should serve static assets instead. Unlike the default setting set this to true when running (absolutely not recommended!) or testing your app in production mode using WEBrick. Otherwise you won't be able use page caching and requests for files that exist regularly under the public directory will anyway hit your Rails app. * `config.session_store` is usually set up in `config/initializers/session_store.rb` and specifies what class to use to store the session. Possible values are `:cookie_store` which is the default, `:mem_cache_store`, and `:disabled`. The last one tells Rails not to deal with sessions. Custom session stores can also be specified: @@ -729,7 +729,7 @@ Rails will now prepend "/app1" when generating links. #### Using Passenger -Passenger makes it easy to run your application in a subdirectory. You can find the relevant configuration in the [passenger manual](http://www.modrails.com/documentation/Users%20guide%20Apache.html#deploying_rails_to_sub_uri). +Passenger makes it easy to run your application in a subdirectory. You can find the relevant configuration in the [Passenger manual](http://www.modrails.com/documentation/Users%20guide%20Apache.html#deploying_rails_to_sub_uri). #### Using a Reverse Proxy @@ -739,11 +739,11 @@ Many modern web servers can be used as a proxy server to balance third-party ele One such application server you can use is [Unicorn](http://unicorn.bogomips.org/) to run behind a reverse proxy. -In this case, you would need to configure the proxy server (nginx, apache, etc) to accept connections from your application server (Unicorn). By default Unicorn will listen for TCP connections on port 8080, but you can change the port or configure it to use sockets instead. +In this case, you would need to configure the proxy server (NGINX, Apache, etc) to accept connections from your application server (Unicorn). By default Unicorn will listen for TCP connections on port 8080, but you can change the port or configure it to use sockets instead. You can find more information in the [Unicorn readme](http://unicorn.bogomips.org/README.html) and understand the [philosophy](http://unicorn.bogomips.org/PHILOSOPHY.html) behind it. -Once you've configured the application server, you must proxy requests to it by configuring your web server appropriately. For example your nginx config may include: +Once you've configured the application server, you must proxy requests to it by configuring your web server appropriately. For example your NGINX config may include: ``` upstream application_server { @@ -769,7 +769,7 @@ server { } ``` -Be sure to read the [nginx documentation](http://nginx.org/en/docs/) for the most up-to-date information. +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 diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml index 5138412312..82e248ee38 100644 --- a/guides/source/documents.yaml +++ b/guides/source/documents.yaml @@ -13,8 +13,8 @@ url: active_record_basics.html description: This guide will get you started with models, persistence to database and the Active Record pattern and library. - - name: Rails Database Migrations - url: migrations.html + name: Active Record Migrations + url: active_record_migrations.html description: This guide covers how you can use Active Record migrations to alter your database in a structured and organized manner. - name: Active Record Validations diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index d9619bbc21..3122a3dacb 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -74,9 +74,8 @@ literally follow along step by step. You can get the complete code [here](https://github.com/rails/docrails/tree/master/guides/code/getting_started). By following along with this guide, you'll create a Rails project called -`blog`, a -(very) simple weblog. Before you can start building the application, you need to -make sure that you have Rails itself installed. +`blog`, a (very) simple weblog. Before you can start building the application, +you need to make sure that you have Rails itself installed. TIP: The examples below use `$` to represent your terminal prompt in a UNIX-like OS, though it may have been customized to appear differently. If you are using Windows, @@ -167,7 +166,7 @@ of the files and folders that Rails created by default: |config/|Configure your application's routes, database, and more. This is covered in more detail in [Configuring Rails Applications](configuring.html).| |config.ru|Rack configuration for Rack based servers used to start the application.| |db/|Contains your current database schema, as well as the database migrations.| -|Gemfile<br>Gemfile.lock|These files allow you to specify what gem dependencies are needed for your Rails application. These files are used by the Bundler gem. For more information about Bundler, see [the Bundler website](http://gembundler.com).| +|Gemfile<br>Gemfile.lock|These files allow you to specify what gem dependencies are needed for your Rails application. These files are used by the Bundler gem. For more information about Bundler, see [the Bundler website](http://bundler.io).| |lib/|Extended modules for your application.| |log/|Application log files.| |public/|The only folder seen by the world as-is. Contains static files and compiled assets.| @@ -623,6 +622,8 @@ method returns an `ActiveSupport::HashWithIndifferentAccess` object, which allows you to access the keys of the hash using either strings or symbols. In this situation, the only parameters that matter are the ones from the form. +TIP: Ensure you have a firm grasp of the `params` method, as you'll use it fairly regularly. Let's consider an example URL: **http://www.example.com/?username=dhh&email=dhh@email.com**. In this URL, `params[:username]` would equal "dhh" and `params[:email]` would equal "dhh@email.com". + If you re-submit the form one more time you'll now no longer get the missing template error. Instead, you'll see something that looks like the following: @@ -739,6 +740,8 @@ database columns. In the first line we do just that (remember that `@article.save` is responsible for saving the model in the database. Finally, we redirect the user to the `show` action, which we'll define later. +TIP: You might be wondering why the `A` in `Article.new` is capitalized above, whereas most other references to articles in this guide have used lowercase. In this context, we are referring to the class named `Article` that is defined in `\models\article.rb`. Class names in Ruby must begin with a capital letter. + TIP: As we'll see later, `@article.save` returns a boolean indicating whether the article was saved or not. diff --git a/guides/source/i18n.md b/guides/source/i18n.md index 0eba3af6e8..8340d6807f 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -860,6 +860,24 @@ en: subject: "Welcome to Rails Guides!" ``` +To send parameters to interpolation use the `default_i18n_subject` method on the mailer. + +```ruby +# user_mailer.rb +class UserMailer < ActionMailer::Base + def welcome(user) + mail(to: user.email, subject: default_i18n_subject(user: user.name)) + end +end +``` + +```yaml +en: + user_mailer: + welcome: + subject: "%{user}, welcome to Rails Guides!" +``` + ### Overview of Other Built-In Methods that Provide I18n Support Rails uses fixed strings and other localizations, such as format strings and other format information in a couple of helpers. Here's a brief overview. diff --git a/guides/source/rails_on_rack.md b/guides/source/rails_on_rack.md index 9053f31b8e..01941fa338 100644 --- a/guides/source/rails_on_rack.md +++ b/guides/source/rails_on_rack.md @@ -18,7 +18,7 @@ Introduction to Rack Rack provides a minimal, modular and adaptable interface for developing web applications in Ruby. By wrapping HTTP requests and responses in the simplest way possible, it unifies and distills the API for web servers, web frameworks, and software in between (the so-called middleware) into a single method call. -- [Rack API Documentation](http://rack.rubyforge.org/doc/) +* [Rack API Documentation](http://rack.github.io/) Explaining Rack is not really in the scope of this guide. In case you are not familiar with Rack's basics, you should check out the [Resources](#resources) section below. diff --git a/guides/source/security.md b/guides/source/security.md index 75d8c8e4c8..7e39986f8b 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -741,7 +741,7 @@ s = sanitize(user_input, tags: tags, attributes: %w(href title)) This allows only the given tags and does a good job, even against all kinds of tricks and malformed tags. -As a second step, _it is good practice to escape all output of the application_, especially when re-displaying user input, which hasn't been input-filtered (as in the search form example earlier on). _Use `escapeHTML()` (or its alias `h()`) method_ to replace the HTML input characters &, ", <, > by their uninterpreted representations in HTML (`&`, `"`, `<`;, and `>`). However, it can easily happen that the programmer forgets to use it, so _it is recommended to use the [SafeErb](http://safe-erb.rubyforge.org/svn/plugins/safe_erb/) plugin_. SafeErb reminds you to escape strings from external sources. +As a second step, _it is good practice to escape all output of the application_, especially when re-displaying user input, which hasn't been input-filtered (as in the search form example earlier on). _Use `escapeHTML()` (or its alias `h()`) method_ to replace the HTML input characters &, ", <, > by their uninterpreted representations in HTML (`&`, `"`, `<`;, and `>`). However, it can easily happen that the programmer forgets to use it, so _it is recommended to use the SafeErb gem. SafeErb reminds you to escape strings from external sources. ##### Obfuscation and Encoding Injection diff --git a/guides/source/testing.md b/guides/source/testing.md index bac4b63c75..c01b2e575a 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -49,7 +49,9 @@ The `test_helper.rb` file holds the default configuration for your tests. ### The Low-Down on Fixtures -For good tests, you'll need to give some thought to setting up test data. In Rails, you can handle this by defining and customizing fixtures. +For good tests, you'll need to give some thought to setting up test data. +In Rails, you can handle this by defining and customizing fixtures. +You can find comprehensive documentation in the [fixture api documentation](http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html). #### What Are Fixtures? @@ -94,6 +96,12 @@ one: category: about ``` +Note: For associations to reference one another by name, you cannot specify the `id:` + attribute on the fixtures. Rails will auto assign a primary key to be consistent between + runs. If you manually specify an `id:` attribute, this behavior will not work. For more + information on this assocation behavior please read the + [fixture api documentation](http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html). + #### ERB'in It Up ERB allows you to embed Ruby code within templates. The YAML fixture format is pre-processed with ERB when Rails loads fixtures. This allows you to use Ruby to help you generate some sample data. For example, the following code generates a thousand users: diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index b328619646..3350b1a4b2 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,7 @@ +* Invalid `bin/rails generate` commands will now show spelling suggestions. + + *Richard Schneeman* + * Add `bin/setup` script to bootstrap an application. *Yves Senn* diff --git a/railties/README.rdoc b/railties/README.rdoc index 6248b5feed..a25658668c 100644 --- a/railties/README.rdoc +++ b/railties/README.rdoc @@ -31,7 +31,11 @@ API documentation is at * http://api.rubyonrails.org -Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here: +Bug reports can be filed for the Ruby on Rails project here: * https://github.com/rails/rails/issues +Feature requests should be discussed on the rails-core mailing list here: + +* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core + diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index 2a0148fe9d..bf2390cb7e 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -156,8 +156,12 @@ module Rails args << "--help" if args.empty? && klass.arguments.any? { |a| a.required? } klass.start(args, config) else - puts "Could not find generator '#{namespace}'. Please choose a generator below." - print_generators + options = sorted_groups.map(&:last).flatten + suggestions = options.sort_by {|suggested| levenshtein_distance(namespace.to_s, suggested) }.first(3) + msg = "Could not find generator '#{namespace}'. " + msg << "Maybe you meant #{ suggestions.map {|s| "'#{s}'"}.join(" or ") }\n" + msg << "Run `rails generate --help` for more options." + puts msg end end @@ -220,31 +224,71 @@ module Rails print_generators end - def self.print_generators + def self.public_namespaces lookup! + subclasses.map { |k| k.namespace } + end - namespaces = subclasses.map{ |k| k.namespace } - namespaces.sort! + def self.print_generators + sorted_groups.each { |b, n| print_list(b, n) } + end + def self.sorted_groups + namespaces = public_namespaces + namespaces.sort! groups = Hash.new { |h,k| h[k] = [] } namespaces.each do |namespace| base = namespace.split(':').first groups[base] << namespace end - # Print Rails defaults first. rails = groups.delete("rails") rails.map! { |n| n.sub(/^rails:/, '') } rails.delete("app") rails.delete("plugin") - print_list("rails", rails) hidden_namespaces.each { |n| groups.delete(n.to_s) } - groups.sort.each { |b, n| print_list(b, n) } + [["rails", rails]] + groups.sort.to_a end protected + # This code is based directly on the Text gem implementation + # Returns a value representing the "cost" of transforming str1 into str2 + def self.levenshtein_distance str1, str2 + s = str1 + t = str2 + n = s.length + m = t.length + max = n/2 + + return m if (0 == n) + return n if (0 == m) + return n if (n - m).abs > max + + d = (0..m).to_a + x = nil + + str1.each_char.each_with_index do |char1,i| + e = i+1 + + str2.each_char.each_with_index do |char2,j| + cost = (char1 == char2) ? 0 : 1 + x = [ + d[j+1] + 1, # insertion + e + 1, # deletion + d[j] + cost # substitution + ].min + d[j] = e + e = x + end + + d[m] = x + end + + return x + end + # Prints a list of generators. def self.print_list(base, namespaces) #:nodoc: namespaces = namespaces.reject do |n| diff --git a/railties/lib/rails/generators/rails/app/templates/bin/setup b/railties/lib/rails/generators/rails/app/templates/bin/setup index 5fcc1a07d6..0e22b3fa5c 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/setup +++ b/railties/lib/rails/generators/rails/app/templates/bin/setup @@ -12,7 +12,7 @@ Dir.chdir APP_ROOT do system "bundle check || bundle install" # puts "\n== Copying sample files ==" - # unless File.exists?("config/database.yml") + # unless File.exist?("config/database.yml") # system "cp config/database.yml.sample config/database.yml" # end diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml index d088dd62bf..187ff01bac 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml @@ -1,7 +1,7 @@ # IBM Dataservers # # Home Page -# http://rubyforge.org/projects/rubyibm/ +# https://github.com/dparnell/ibm_db # # To install the ibm_db gem: # @@ -31,8 +31,6 @@ # Configure Using Gemfile # gem 'ibm_db' # -# For more details on the installation and the connection parameters below, -# please refer to the latest documents at http://rubyforge.org/docman/?group_id=2361 # default: &default adapter: ibm_db diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml index 10ab4c02e2..9aedcc15cb 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml @@ -1,7 +1,7 @@ # Oracle/OCI 8i, 9, 10g # # Requires Ruby/OCI8: -# http://rubyforge.org/projects/ruby-oci8/ +# https://github.com/kubo/ruby-oci8 # # Specify your database using any valid connection syntax, such as a # tnsnames.ora service name, or an SQL connect string of the form: diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt index 9ed71687ea..a2aa7c09db 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt @@ -16,10 +16,10 @@ Rails.application.configure do # Enable Rack::Cache to put a simple HTTP cache in front of your application # Add `rack-cache` to your Gemfile before enabling this. - # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. + # For large-scale production use, consider using a caching reverse proxy like NGINX, varnish or squid. # config.action_dispatch.rack_cache = true - # Disable Rails's static asset server (Apache or nginx will already do this). + # Disable Rails's static asset server (Apache or NGINX will already do this). config.serve_static_assets = false <%- unless options.skip_sprockets? -%> @@ -37,8 +37,8 @@ Rails.application.configure do <%- end -%> # Specifies the header that your server uses for sending files. - # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache - # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb index 8e198d5fe1..b998fef42e 100644 --- a/railties/test/generators/shared_generator_tests.rb +++ b/railties/test/generators/shared_generator_tests.rb @@ -78,9 +78,12 @@ module SharedGeneratorTests end def test_template_raises_an_error_with_invalid_path - content = capture(:stderr){ run_generator([destination_root, "-m", "non/existent/path"]) } - assert_match(/The template \[.*\] could not be loaded/, content) - assert_match(/non\/existent\/path/, content) + quietly do + content = capture(:stderr){ run_generator([destination_root, "-m", "non/existent/path"]) } + + assert_match(/The template \[.*\] could not be loaded/, content) + assert_match(/non\/existent\/path/, content) + end end def test_template_is_executed_when_supplied @@ -89,7 +92,7 @@ module SharedGeneratorTests template.instance_eval "def read; self; end" # Make the string respond to read generator([destination_root], template: path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template) - assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) + quietly { assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) } end def test_template_is_executed_when_supplied_an_https_path @@ -98,7 +101,7 @@ module SharedGeneratorTests template.instance_eval "def read; self; end" # Make the string respond to read generator([destination_root], template: path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template) - assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) + quietly { assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) } end def test_dev_option diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb index 8d6dbf80c2..0b4edafaca 100644 --- a/railties/test/generators_test.rb +++ b/railties/test/generators_test.rb @@ -24,7 +24,13 @@ class GeneratorsTest < Rails::Generators::TestCase name = :unknown output = capture(:stdout){ Rails::Generators.invoke name } assert_match "Could not find generator '#{name}'", output - assert_match "scaffold", output + assert_match "`rails generate --help`", output + end + + def test_generator_suggestions + name = :migrationz + output = capture(:stdout){ Rails::Generators.invoke name } + assert_match "Maybe you meant 'migration'", output end def test_help_when_a_generator_with_required_arguments_is_invoked_without_arguments diff --git a/railties/test/rails_info_test.rb b/railties/test/rails_info_test.rb index 44a5fd1904..4bec302ff8 100644 --- a/railties/test/rails_info_test.rb +++ b/railties/test/rails_info_test.rb @@ -66,16 +66,6 @@ class InfoTest < ActiveSupport::TestCase end protected - def svn_info=(info) - Rails::Info.module_eval do - class << self - def svn_info - info - end - end - end - end - def properties Rails::Info.properties end |