diff options
306 files changed, 4559 insertions, 2707 deletions
diff --git a/.travis.yml b/.travis.yml index f0e9d570f5..a2f47e495c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,12 @@ script: 'ci/travis.rb' before_install: - travis_retry gem install bundler + - "rvm current | grep 'jruby' && export AR_JDBC=true || echo" rvm: - 1.9.3 - 2.0.0 - jruby-head - - rbx-19mode + - rbx-2.1.1 env: - "GEM=railties" - "GEM=ap,am,amo,as,av" @@ -16,7 +17,7 @@ env: matrix: allow_failures: - rvm: jruby-head - - rvm: rbx-19mode + - rvm: rbx-2.1.1 notifications: email: false irc: @@ -12,6 +12,7 @@ gem 'bcrypt-ruby', '~> 3.1.2' gem 'jquery-rails', '~> 2.2.0' gem 'turbolinks' gem 'coffee-rails', '~> 4.0.0' +gem 'arel', github: 'rails/arel' # This needs to be with require false to avoid # it being automatically loaded by sprockets @@ -29,7 +30,7 @@ gem 'dalli', '>= 2.2.1' # Add your own local bundler stuff local_gemfile = File.dirname(__FILE__) + "/.Gemfile" -instance_eval File.read local_gemfile if File.exists? local_gemfile +instance_eval File.read local_gemfile if File.exist? local_gemfile group :test do platforms :mri_19 do @@ -62,14 +63,28 @@ end platforms :jruby do gem 'json' - gem 'activerecord-jdbcsqlite3-adapter', '>= 1.3.0' - - group :db do - gem 'activerecord-jdbcmysql-adapter', '>= 1.3.0' - gem 'activerecord-jdbcpostgresql-adapter', '>= 1.3.0' + if ENV['AR_JDBC'] + gem 'activerecord-jdbcsqlite3-adapter', github: 'jruby/activerecord-jdbc-adapter', branch: 'master' + group :db do + gem 'activerecord-jdbcmysql-adapter', github: 'jruby/activerecord-jdbc-adapter', branch: 'master' + gem 'activerecord-jdbcpostgresql-adapter', github: 'jruby/activerecord-jdbc-adapter', branch: 'master' + end + else + gem 'activerecord-jdbcsqlite3-adapter', '>= 1.3.0' + group :db do + gem 'activerecord-jdbcmysql-adapter', '>= 1.3.0' + gem 'activerecord-jdbcpostgresql-adapter', '>= 1.3.0' + end end end +platforms :rbx do + gem 'psych' + gem 'rubysl-mathn' + gem 'rubysl-matrix' + gem 'rubysl-rexml' +end + # gems that are necessary for ActiveRecord tests with Oracle database if ENV['ORACLE_ENHANCED'] platforms :ruby do @@ -59,7 +59,7 @@ independently outside Rails. Run with `--help` or `-h` for options. -4. Go to http://localhost:3000 and you'll see: "Welcome aboard: You're riding Ruby on Rails!" +4. Using a browser, go to http://localhost:3000 and you'll see: "Welcome aboard: You're riding Ruby on Rails!" 5. Follow the guidelines to start developing your application. You may find the following resources handy: diff --git a/RELEASING_RAILS.rdoc b/RELEASING_RAILS.rdoc index 6f8c79eef2..aafbd25486 100644 --- a/RELEASING_RAILS.rdoc +++ b/RELEASING_RAILS.rdoc @@ -113,7 +113,7 @@ what to do in case anything goes wrong: $ git tag -m'tagging rc release' v3.0.10.rc1 $ git push $ git push --tags - $ for i in $(ls dist); do gem push $i; done + $ for i in $(ls pkg); do gem push $i; done === Send Rails release announcements @@ -36,15 +36,7 @@ task :smoke do end desc "Install gems for all projects." -task :install => :build do - version = File.read("RAILS_VERSION").strip - (PROJECTS - ["railties"]).each do |project| - puts "INSTALLING #{project}" - system("gem install #{project}/pkg/#{project}-#{version}.gem --local --no-ri --no-rdoc") - end - system("gem install railties/pkg/railties-#{version}.gem --local --no-ri --no-rdoc") - system("gem install pkg/rails-#{version}.gem --local --no-ri --no-rdoc") -end +task :install => "all:install" desc "Generate documentation for the Rails framework" if ENV['EDGE'] diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index 1659696bfb..d84b95e6e9 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -1,3 +1,8 @@ +* Instrument the generation of Action Mailer messages. The time it takes to + generate a message is written to the log. + + *Daniel Schierbeck* + * invoke mailer defaults as procs only if they are procs, do not convert with to_proc. That an object is convertible to a proc does not mean it's meant to be always used as a proc. Fixes #11533 diff --git a/actionmailer/README.rdoc b/actionmailer/README.rdoc index 178572706c..96dd0b1a2e 100644 --- a/actionmailer/README.rdoc +++ b/actionmailer/README.rdoc @@ -61,9 +61,7 @@ generated would look like this: Thank you for signing up! -In previous versions of Rails you would call <tt>create_method_name</tt> and -<tt>deliver_method_name</tt>. Rails 3.0 has a much simpler interface - you -simply call the method and optionally call +deliver+ on the return value. +In order to send mails, you simply call the method and then call +deliver+ on the return value. Calling the method returns a Mail Message object: diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index ada86fbc4f..becd4126f4 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -511,11 +511,18 @@ module ActionMailer process(method_name, *args) if method_name end - def process(*args) #:nodoc: - lookup_context.skip_default_locale! + def process(method_name, *args) #:nodoc: + payload = { + :mailer => self.class.name, + :action => method_name + } - super - @_message = NullMail.new unless @_mail_was_called + ActiveSupport::Notifications.instrument("process.action_mailer", payload) do + lookup_context.skip_default_locale! + + super + @_message = NullMail.new unless @_mail_was_called + end end class NullMail #:nodoc: @@ -685,9 +692,9 @@ module ActionMailer content_type = headers[:content_type] # Call all the procs (if any) - class_default = self.class.default - default_values = class_default.merge(class_default) do |k,v| - v.is_a?(Proc) ? instance_eval(&v) : v + default_values = {} + self.class.default.each do |k,v| + default_values[k] = v.is_a?(Proc) ? instance_eval(&v) : v end # Handle defaults diff --git a/actionmailer/lib/action_mailer/log_subscriber.rb b/actionmailer/lib/action_mailer/log_subscriber.rb index 8467d45986..eb6fb11d81 100644 --- a/actionmailer/lib/action_mailer/log_subscriber.rb +++ b/actionmailer/lib/action_mailer/log_subscriber.rb @@ -19,6 +19,13 @@ module ActionMailer debug(event.payload[:mail]) end + # An email was generated. + def process(event) + mailer = event.payload[:mailer] + action = event.payload[:action] + debug("\n#{mailer}##{action}: processed outbound mail in #{event.duration.round(1)}ms") + end + # Use the logger configured for ActionMailer::Base def logger ActionMailer::Base.logger diff --git a/actionmailer/test/log_subscriber_test.rb b/actionmailer/test/log_subscriber_test.rb index 5f52a1bd69..5f0bee88fd 100644 --- a/actionmailer/test/log_subscriber_test.rb +++ b/actionmailer/test/log_subscriber_test.rb @@ -24,10 +24,13 @@ class AMLogSubscriberTest < ActionMailer::TestCase def test_deliver_is_notified BaseMailer.welcome.deliver wait + assert_equal(1, @logger.logged(:info).size) assert_match(/Sent mail to system@test.lindsaar.net/, @logger.logged(:info).first) - assert_equal(1, @logger.logged(:debug).size) - assert_match(/Welcome/, @logger.logged(:debug).first) + + assert_equal(2, @logger.logged(:debug).size) + assert_match(/BaseMailer#welcome: processed outbound mail in [\d.]+ms/, @logger.logged(:debug).first) + assert_match(/Welcome/, @logger.logged(:debug).second) end def test_receive_is_notified @@ -39,4 +42,4 @@ class AMLogSubscriberTest < ActionMailer::TestCase assert_equal(1, @logger.logged(:debug).size) assert_match(/Jamis/, @logger.logged(:debug).first) end -end
\ No newline at end of file +end diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index b0b75f6909..dea80abfcd 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,85 @@ +* Add `session#fetch` method + + fetch behaves similarly to [Hash#fetch](http://www.ruby-doc.org/core-1.9.3/Hash.html#method-i-fetch), + with the exception that the returned value is always saved into the session. + + It returns a value from the hash for the given key. + If the key can’t be found, there are several options: + + * With no other arguments, it will raise an KeyError exception. + * If a default value is given, then that will be returned. + * If the optional code block is specified, then that will be run and its result returned. + + *Damien Mathieu* + +* Don't let strong parameters mutate the given hash via `fetch` + + Create a new instance if the given parameter is a `Hash` instead of + passing it to the `convert_hashes_to_parameters` method since it is + overriding its default value. + + *Brendon Murphy*, *Doug Cole* + +* Add `params` option to `button_to` form helper, which renders the given hash + as hidden form fields. + + *Andy Waite* + +* Make assets helpers work in the controllers like it works in the views. + + Example: + + # config/application.rb + config.asset_host = 'http://mycdn.com' + + ActionController::Base.helpers.asset_path('fallback.png') + # => http://mycdn.com/assets/fallback.png + + Fixes #10051. + + *Tima Maslyuchenko* + +* Respect `SCRIPT_NAME` when using `redirect` with a relative path + + Example: + + # application routes.rb + mount BlogEngine => '/blog' + + # engine routes.rb + get '/admin' => redirect('admin/dashboard') + + This now redirects to the path `/blog/admin/dashboard`, whereas before it would've + generated an invalid url because there would be no slash between the host name and + the path. It also allows redirects to work where the application is deployed to a + subdirectory of a website. + + Fixes #7977. + + *Andrew White* + +* Fixing repond_with working directly on the options hash + This fixes an issue where the respond_with worked directly with the given + options hash, so that if a user relied on it after calling respond_with, + the hash wouldn't be the same. + + Fixes #12029. + + *bluehotdog* + +* Fix `ActionDispatch::RemoteIp::GetIp#calculate_ip` to only check for spoofing + attacks if both `HTTP_CLIENT_IP` and `HTTP_X_FORWARDED_FOR` are set. + + Fixes #10844. + + *Tamir Duberstein* + +* Strong parameters should permit nested number as key. + + Fixes #12293. + + *kennyj* + * Fix regex used to detect URI schemes in `redirect_to` to be consistent with RFC 3986. @@ -10,14 +92,14 @@ * Fix an issue where router can't recognize downcased url encoding path. - Fixes #12269 + Fixes #12269. *kennyj* * Fix custom flash type definition. Misusage of the `_flash_types` class variable caused an error when reloading controllers with custom flash types. - Fixes #12057 + Fixes #12057. *Ricardo de Cillo* @@ -38,21 +120,21 @@ * Fix an issue where :if and :unless controller action procs were being run before checking for the correct action in the :only and :unless options. - Fixes #11799 + Fixes #11799. *Nicholas Jakobsen* * Fix an issue where `assert_dom_equal` and `assert_dom_not_equal` were ignoring the passed failure message argument. - Fixes #11751 + Fixes #11751. *Ryan McGeary* * Allow REMOTE_ADDR, HTTP_HOST and HTTP_USER_AGENT to be overridden from the environment passed into `ActionDispatch::TestRequest.new`. - Fixes #11590 + Fixes #11590. *Andrew White* @@ -67,7 +149,7 @@ * Skip routes pointing to a redirect or mounted application when generating urls using an options hash as they aren't relevant and generate incorrect urls. - Fixes #8018 + Fixes #8018. *Andrew White* @@ -85,7 +167,7 @@ * Fix `ActionDispatch::ParamsParser#parse_formatted_parameters` to rewind body input stream on parsing json params. - Fixes #11345 + Fixes #11345. *Yuri Bol*, *Paul Nikitochkin* @@ -118,7 +200,7 @@ was setting `request.formats` with an array containing a `nil` value, which raised an error when setting the controller formats. - Fixes #10965 + Fixes #10965. *Becker* @@ -127,7 +209,7 @@ no `:to` present in the options hash so should only affect routes using the shorthand syntax (i.e. endpoint is inferred from the path). - Fixes #9856 + Fixes #9856. *Yves Senn*, *Andrew White* diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index db7b56f47e..3b0d094f4f 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -73,7 +73,7 @@ module ActionController # <input type="text" name="post[address]" value="hyacintvej"> # # A request stemming from a form holding these inputs will include <tt>{ "post" => { "name" => "david", "address" => "hyacintvej" } }</tt>. - # If the address input had been named \"post[address][street]", the params would have included + # If the address input had been named <tt>post[address][street]</tt>, the params would have included # <tt>{ "post" => { "address" => { "street" => "hyacintvej" } } }</tt>. There's no limit to the depth of the nesting. # # == Sessions diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index b53ae7f29f..a9c3e438fb 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -73,7 +73,11 @@ module ActionController # Provides a proxy to access helpers methods from outside the view. def helpers - @helper_proxy ||= ActionView::Base.new.extend(_helpers) + @helper_proxy ||= begin + proxy = ActionView::Base.new + proxy.config = config.inheritable_copy + proxy.extend(_helpers) + end end # Overwrite modules_for_helpers to accept :all as argument, which loads diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 66dabd821f..a072fce1a1 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -326,6 +326,7 @@ module ActionController #:nodoc: if collector = retrieve_collector_from_mimes(&block) options = resources.size == 1 ? {} : resources.extract_options! + options = options.clone options[:default_response] = collector.response (options.delete(:responder) || self.class.responder).call(self, resources, options) end diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb index 66ff34a794..b4ba169e8f 100644 --- a/actionpack/lib/action_controller/metal/responder.rb +++ b/actionpack/lib/action_controller/metal/responder.rb @@ -144,7 +144,7 @@ module ActionController #:nodoc: undef_method(:to_json) if method_defined?(:to_json) undef_method(:to_yaml) if method_defined?(:to_yaml) - # Initializes a new responder an invoke the proper format. If the format is + # Initializes a new responder and invokes the proper format. If the format is # not defined, call to_format. # def self.call(*args) diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index b495ab3f0f..b4948d99a8 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -17,7 +17,7 @@ module ActionController def initialize(param) # :nodoc: @param = param - super("param not found: #{param}") + super("param is missing or the value is empty: #{param}") end end @@ -284,7 +284,14 @@ module ActionController # params.fetch(:none, 'Francesco') # => "Francesco" # params.fetch(:none) { 'Francesco' } # => "Francesco" def fetch(key, *args) - convert_hashes_to_parameters(key, super) + value = super + # Don't rely on +convert_hashes_to_parameters+ + # so as to not mutate via a +fetch+ + if value.is_a?(Hash) + value = self.class.new(value) + value.permit! if permitted? + end + value rescue KeyError raise ActionController::ParameterMissing.new(key) end @@ -334,7 +341,7 @@ module ActionController def each_element(object) if object.is_a?(Array) object.map { |el| yield el }.compact - elsif object.is_a?(Hash) && object.keys.all? { |k| k =~ /\A-?\d+\z/ } + elsif fields_for_style?(object) hash = object.class.new object.each { |k,v| hash[k] = yield v } hash @@ -343,6 +350,10 @@ module ActionController end end + def fields_for_style?(object) + object.is_a?(Hash) && object.all? { |k, v| k =~ /\A-?\d+\z/ && v.is_a?(Hash) } + end + def unpermitted_parameters!(params) unpermitted_keys = unpermitted_keys(params) if unpermitted_keys.any? diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index aba8f66118..99b81c898f 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -299,17 +299,24 @@ module ActionDispatch LOCALHOST =~ remote_addr && LOCALHOST =~ remote_ip end - protected + # Extracted into ActionDispatch::Request::Utils.deep_munge, but kept here for backwards compatibility. + def deep_munge(hash) + ActiveSupport::Deprecation.warn( + "This method has been extracted into ActionDispatch::Request::Utils.deep_munge. Please start using that instead." + ) - def parse_query(qs) - Utils.deep_munge(super) + Utils.deep_munge(hash) end - private + protected + def parse_query(qs) + Utils.deep_munge(super) + end - def check_method(name) - HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}") - name - end + private + def check_method(name) + HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}") + name + end end end diff --git a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb index da0cddd93c..971cb3447f 100644 --- a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb +++ b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb @@ -9,8 +9,8 @@ module ActionDispatch attr_reader :memos def initialize - @regexp_states = Hash.new { |h,k| h[k] = {} } - @string_states = Hash.new { |h,k| h[k] = {} } + @regexp_states = {} + @string_states = {} @accepting = {} @memos = Hash.new { |h,k| h[k] = [] } end @@ -111,14 +111,8 @@ module ActionDispatch end def []=(from, to, sym) - case sym - when String - @string_states[from][sym] = to - when Regexp - @regexp_states[from][sym] = to - else - raise ArgumentError, 'unknown symbol: %s' % sym.class - end + to_mappings = states_hash_for(sym)[from] ||= {} + to_mappings[sym] = to end def states @@ -137,18 +131,35 @@ module ActionDispatch private + def states_hash_for(sym) + case sym + when String + @string_states + when Regexp + @regexp_states + else + raise ArgumentError, 'unknown symbol: %s' % sym.class + end + end + def move_regexp(t, a) return [] if t.empty? t.map { |s| - @regexp_states[s].map { |re, v| re === a ? v : nil } + if states = @regexp_states[s] + states.map { |re, v| re === a ? v : nil } + end }.flatten.compact.uniq end def move_string(t, a) return [] if t.empty? - t.map { |s| @string_states[s][a] }.compact + t.map do |s| + if states = @string_states[s] + states[a] + end + end.compact end end end diff --git a/actionpack/lib/action_dispatch/journey/router/utils.rb b/actionpack/lib/action_dispatch/journey/router/utils.rb index 1edf86cd88..d1a004af50 100644 --- a/actionpack/lib/action_dispatch/journey/router/utils.rb +++ b/actionpack/lib/action_dispatch/journey/router/utils.rb @@ -18,7 +18,7 @@ module ActionDispatch path = "/#{path}" path.squeeze!('/') path.sub!(%r{/+\Z}, '') - path.gsub!(/(%[a-f0-9]{2}+)/) { $1.upcase } + path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase } path = '/' if path == '' path end diff --git a/actionpack/lib/action_dispatch/journey/visitors.rb b/actionpack/lib/action_dispatch/journey/visitors.rb index a5b4679fae..9e66cab052 100644 --- a/actionpack/lib/action_dispatch/journey/visitors.rb +++ b/actionpack/lib/action_dispatch/journey/visitors.rb @@ -1,9 +1,12 @@ # encoding: utf-8 + +require 'thread_safe' + module ActionDispatch module Journey # :nodoc: module Visitors # :nodoc: class Visitor # :nodoc: - DISPATCH_CACHE = Hash.new { |h,k| + DISPATCH_CACHE = ThreadSafe::Cache.new { |h,k| h[k] = :"visit_#{k}" } diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index 1de3d14530..37bf9c8c9f 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -96,7 +96,7 @@ module ActionDispatch def source_fragment(path, line) return unless Rails.respond_to?(:root) && Rails.root full_path = Rails.root.join(path) - if File.exists?(full_path) + if File.exist?(full_path) File.open(full_path, "r") do |file| start = [line - 3, 0].max lines = file.each_line.drop(start).take(6) diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb index 8879291dbd..57bc6d5cd0 100644 --- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb +++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb @@ -143,7 +143,7 @@ module ActionDispatch # proxies with incompatible IP header conventions, and there is no way # for us to determine which header is the right one after the fact. # Since we have no idea, we give up and explode. - should_check_ip = @check_ip && client_ips.last + should_check_ip = @check_ip && client_ips.last && forwarded_ips.last if should_check_ip && !forwarded_ips.include?(client_ips.last) # We don't know which came from the proxy, and which from the user raise IpSpoofAttackError, "IP spoofing attack?! " + diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index fcc5bc12c4..1d4f0f89a6 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -29,8 +29,11 @@ module ActionDispatch def call(env) @app.call(env) rescue Exception => exception - raise exception if env['action_dispatch.show_exceptions'] == false - render_exception(env, exception) + if env['action_dispatch.show_exceptions'] == false + raise exception + else + render_exception(env, exception) + end end private diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb index 7bc812fd22..6d911a75f1 100644 --- a/actionpack/lib/action_dispatch/request/session.rb +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -127,6 +127,18 @@ module ActionDispatch @delegate.delete key.to_s end + def fetch(key, default=nil) + if self.key?(key) + self[key] + elsif default + self[key] = default + elsif block_given? + self[key] = yield(key) + else + raise KeyError + end + end + def inspect if loaded? super diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb index 68094f129f..3e54c7e71c 100644 --- a/actionpack/lib/action_dispatch/routing/redirection.rb +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -30,6 +30,10 @@ module ActionDispatch uri.host ||= req.host uri.port ||= req.port unless req.standard_port? + if relative_path?(uri.path) + uri.path = "#{req.script_name}/#{uri.path}" + end + body = %(<html><body>You are being <a href="#{ERB::Util.h(uri.to_s)}">redirected</a>.</body></html>) headers = { @@ -48,6 +52,11 @@ module ActionDispatch def inspect "redirect(#{status})" end + + private + def relative_path?(path) + path && !path.empty? && path[0] != '/' + end end class PathRedirect < Redirect @@ -81,6 +90,11 @@ module ActionDispatch url_options[:path] = (url_options[:path] % escape_path(params)) end + if relative_path?(url_options[:path]) + url_options[:path] = "/#{url_options[:path]}" + url_options[:script_name] = request.script_name + end + ActionDispatch::Http::URL.url_for url_options end @@ -104,6 +118,10 @@ module ActionDispatch # # get 'docs/:article', to: redirect('/wiki/%{article}') # + # Note that if you return a path without a leading slash then the url is prefixed with the + # current SCRIPT_NAME environment variable. This is typically '/' but may be different in + # a mounted engine or where the application is deployed to a subdirectory of a website. + # # Alternatively you can use one of the other syntaxes: # # The block version of redirect allows for the easy encapsulation of any logic associated with diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index 8e19025722..bcebe532bf 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -20,7 +20,7 @@ module ActionDispatch # # <%= link_to('Click here', controller: 'users', # action: 'new', message: 'Welcome!') %> - # # => "/users/new?message=Welcome%21" + # # => <a href="/users/new?message=Welcome%21">Click here</a> # # link_to, and all other functions that require URL generation functionality, # actually use ActionController::UrlFor under the hood. And in particular, diff --git a/actionpack/test/controller/helper_test.rb b/actionpack/test/controller/helper_test.rb index 248c81193e..20f99f19ee 100644 --- a/actionpack/test/controller/helper_test.rb +++ b/actionpack/test/controller/helper_test.rb @@ -201,6 +201,12 @@ class HelperTest < ActiveSupport::TestCase # fun/pdf_helper.rb assert methods.include?(:foobar) end + + def test_helper_proxy_config + AllHelpersController.config.my_var = 'smth' + + assert_equal 'smth', AllHelpersController.helpers.config.my_var + end private def expected_helper_methods diff --git a/actionpack/test/controller/mime/respond_with_test.rb b/actionpack/test/controller/mime/respond_with_test.rb index 76af9e3414..a70592fa1b 100644 --- a/actionpack/test/controller/mime/respond_with_test.rb +++ b/actionpack/test/controller/mime/respond_with_test.rb @@ -65,7 +65,17 @@ class RespondWithController < ActionController::Base respond_with(resource, :responder => responder) end + def respond_with_additional_params + @params = RespondWithController.params + respond_with({:result => resource}, @params) + end + protected + def self.params + { + :foo => 'bar' + } + end def resource Customer.new("david", request.delete? ? nil : 13) @@ -145,6 +155,11 @@ class RespondWithControllerTest < ActionController::TestCase Mime::Type.unregister(:mobile) end + def test_respond_with_shouldnt_modify_original_hash + get :respond_with_additional_params + assert_equal RespondWithController.params, assigns(:params) + end + def test_using_resource @request.accept = "application/xml" get :using_resource diff --git a/actionpack/test/controller/parameters/nested_parameters_test.rb b/actionpack/test/controller/parameters/nested_parameters_test.rb index 91df527dec..3b1257e8d5 100644 --- a/actionpack/test/controller/parameters/nested_parameters_test.rb +++ b/actionpack/test/controller/parameters/nested_parameters_test.rb @@ -169,4 +169,19 @@ class NestedParametersTest < ActiveSupport::TestCase assert_filtered_out permitted[:book][:authors_attributes]['-1'], :age_of_death end + + test "nested number as key" do + params = ActionController::Parameters.new({ + product: { + properties: { + '0' => "prop0", + '1' => "prop1" + } + } + }) + params = params.require(:product).permit(:properties => ["0"]) + assert_not_nil params[:properties]["0"] + assert_nil params[:properties]["1"] + assert_equal "prop0", params[:properties]["0"] + end end diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb index 84e007b5d0..b60c5f058d 100644 --- a/actionpack/test/controller/parameters/parameters_permit_test.rb +++ b/actionpack/test/controller/parameters/parameters_permit_test.rb @@ -147,6 +147,12 @@ class ParametersPermitTest < ActiveSupport::TestCase assert_equal :foo, e.param end + test "fetch with a default value of a hash does not mutate the object" do + params = ActionController::Parameters.new({}) + params.fetch :foo, {} + assert_equal nil, params[:foo] + end + test "fetch doesnt raise ParameterMissing exception if there is a default" do assert_equal "monkey", @params.fetch(:foo, "monkey") assert_equal "monkey", @params.fetch(:foo) { "monkey" } diff --git a/actionpack/test/dispatch/prefix_generation_test.rb b/actionpack/test/dispatch/prefix_generation_test.rb index 113608ecf4..e519fff51e 100644 --- a/actionpack/test/dispatch/prefix_generation_test.rb +++ b/actionpack/test/dispatch/prefix_generation_test.rb @@ -31,6 +31,14 @@ module TestGenerationPrefix get "/polymorphic_path_for_engine", :to => "inside_engine_generating#polymorphic_path_for_engine" get "/conflicting_url", :to => "inside_engine_generating#conflicting" get "/foo", :to => "never#invoked", :as => :named_helper_that_should_be_invoked_only_in_respond_to_test + + get "/relative_path_redirect", :to => redirect("foo") + get "/relative_option_redirect", :to => redirect(:path => "foo") + get "/relative_custom_redirect", :to => redirect { |params, request| "foo" } + + get "/absolute_path_redirect", :to => redirect("/foo") + get "/absolute_option_redirect", :to => redirect(:path => "/foo") + get "/absolute_custom_redirect", :to => redirect { |params, request| "/foo" } end routes @@ -182,6 +190,48 @@ module TestGenerationPrefix assert_equal "engine", last_response.body end + test "[ENGINE] relative path redirect uses SCRIPT_NAME from request" do + get "/awesome/blog/relative_path_redirect" + assert_equal 301, last_response.status + assert_equal "http://example.org/awesome/blog/foo", last_response.headers["Location"] + assert_equal %(<html><body>You are being <a href="http://example.org/awesome/blog/foo">redirected</a>.</body></html>), last_response.body + end + + test "[ENGINE] relative option redirect uses SCRIPT_NAME from request" do + get "/awesome/blog/relative_option_redirect" + assert_equal 301, last_response.status + assert_equal "http://example.org/awesome/blog/foo", last_response.headers["Location"] + assert_equal %(<html><body>You are being <a href="http://example.org/awesome/blog/foo">redirected</a>.</body></html>), last_response.body + end + + test "[ENGINE] relative custom redirect uses SCRIPT_NAME from request" do + get "/awesome/blog/relative_custom_redirect" + assert_equal 301, last_response.status + assert_equal "http://example.org/awesome/blog/foo", last_response.headers["Location"] + assert_equal %(<html><body>You are being <a href="http://example.org/awesome/blog/foo">redirected</a>.</body></html>), last_response.body + end + + test "[ENGINE] absolute path redirect doesn't use SCRIPT_NAME from request" do + get "/awesome/blog/absolute_path_redirect" + assert_equal 301, last_response.status + assert_equal "http://example.org/foo", last_response.headers["Location"] + assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + end + + test "[ENGINE] absolute option redirect doesn't use SCRIPT_NAME from request" do + get "/awesome/blog/absolute_option_redirect" + assert_equal 301, last_response.status + assert_equal "http://example.org/foo", last_response.headers["Location"] + assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + end + + test "[ENGINE] absolute custom redirect doesn't use SCRIPT_NAME from request" do + get "/awesome/blog/absolute_custom_redirect" + assert_equal 301, last_response.status + assert_equal "http://example.org/foo", last_response.headers["Location"] + assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + end + # Inside Application test "[APP] generating engine's route includes prefix" do get "/generate" @@ -281,6 +331,14 @@ module TestGenerationPrefix routes = ActionDispatch::Routing::RouteSet.new routes.draw do get "/posts/:id", :to => "posts#show", :as => :post + + get "/relative_path_redirect", :to => redirect("foo") + get "/relative_option_redirect", :to => redirect(:path => "foo") + get "/relative_custom_redirect", :to => redirect { |params, request| "foo" } + + get "/absolute_path_redirect", :to => redirect("/foo") + get "/absolute_option_redirect", :to => redirect(:path => "/foo") + get "/absolute_custom_redirect", :to => redirect { |params, request| "/foo" } end routes @@ -331,5 +389,47 @@ module TestGenerationPrefix get "/posts/1" assert_equal "/posts/1", last_response.body end + + test "[ENGINE] relative path redirect uses SCRIPT_NAME from request" do + get "/relative_path_redirect" + assert_equal 301, last_response.status + assert_equal "http://example.org/foo", last_response.headers["Location"] + assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + end + + test "[ENGINE] relative option redirect uses SCRIPT_NAME from request" do + get "/relative_option_redirect" + assert_equal 301, last_response.status + assert_equal "http://example.org/foo", last_response.headers["Location"] + assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + end + + test "[ENGINE] relative custom redirect uses SCRIPT_NAME from request" do + get "/relative_custom_redirect" + assert_equal 301, last_response.status + assert_equal "http://example.org/foo", last_response.headers["Location"] + assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + end + + test "[ENGINE] absolute path redirect doesn't use SCRIPT_NAME from request" do + get "/absolute_path_redirect" + assert_equal 301, last_response.status + assert_equal "http://example.org/foo", last_response.headers["Location"] + assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + end + + test "[ENGINE] absolute option redirect doesn't use SCRIPT_NAME from request" do + get "/absolute_option_redirect" + assert_equal 301, last_response.status + assert_equal "http://example.org/foo", last_response.headers["Location"] + assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + end + + test "[ENGINE] absolute custom redirect doesn't use SCRIPT_NAME from request" do + get "/absolute_custom_redirect" + assert_equal 301, last_response.status + assert_equal "http://example.org/foo", last_response.headers["Location"] + assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + end end end diff --git a/actionpack/test/dispatch/request/session_test.rb b/actionpack/test/dispatch/request/session_test.rb index 1517f96fdc..a244d1364c 100644 --- a/actionpack/test/dispatch/request/session_test.rb +++ b/actionpack/test/dispatch/request/session_test.rb @@ -61,6 +61,23 @@ module ActionDispatch assert_equal([], s.values) end + def test_fetch + session = Session.create(store, {}, {}) + + session['one'] = '1' + assert_equal '1', session.fetch(:one) + + assert_equal '2', session.fetch(:two, '2') + assert_equal '2', session.fetch(:two) + + assert_equal 'three', session.fetch(:three) {|el| el.to_s } + assert_equal 'three', session.fetch(:three) + + assert_raise KeyError do + session.fetch(:four) + end + end + private def store Class.new { diff --git a/actionpack/test/fixtures/respond_with/respond_with_additional_params.html.erb b/actionpack/test/fixtures/respond_with/respond_with_additional_params.html.erb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actionpack/test/fixtures/respond_with/respond_with_additional_params.html.erb diff --git a/actionpack/test/journey/router/utils_test.rb b/actionpack/test/journey/router/utils_test.rb index 057dc40cca..93348f4647 100644 --- a/actionpack/test/journey/router/utils_test.rb +++ b/actionpack/test/journey/router/utils_test.rb @@ -15,6 +15,14 @@ module ActionDispatch def test_uri_unescape assert_equal "a/b c+d", Utils.unescape_uri("a%2Fb%20c+d") end + + def test_normalize_path_not_greedy + assert_equal "/foo%20bar%20baz", Utils.normalize_path("/foo%20bar%20baz") + end + + def test_normalize_path_uppercase + assert_equal "/foo%AAbar%AAbaz", Utils.normalize_path("/foo%aabar%aabaz") + end end end end diff --git a/actionpack/test/lib/controller/fake_models.rb b/actionpack/test/lib/controller/fake_models.rb index 38abb16d08..08af187311 100644 --- a/actionpack/test/lib/controller/fake_models.rb +++ b/actionpack/test/lib/controller/fake_models.rb @@ -96,88 +96,6 @@ class Comment attr_accessor :body end -class Tag - extend ActiveModel::Naming - include ActiveModel::Conversion - - attr_reader :id - attr_reader :post_id - def initialize(id = nil, post_id = nil); @id, @post_id = id, post_id end - def to_key; id ? [id] : nil end - def save; @id = 1; @post_id = 1 end - def persisted?; @id.present? end - def to_param; @id; end - def value - @id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}" - end - - attr_accessor :relevances - def relevances_attributes=(attributes); end - -end - -class CommentRelevance - extend ActiveModel::Naming - include ActiveModel::Conversion - - attr_reader :id - attr_reader :comment_id - def initialize(id = nil, comment_id = nil); @id, @comment_id = id, comment_id end - def to_key; id ? [id] : nil end - def save; @id = 1; @comment_id = 1 end - def persisted?; @id.present? end - def to_param; @id; end - def value - @id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}" - end -end - -class Sheep - extend ActiveModel::Naming - include ActiveModel::Conversion - - attr_reader :id - def to_key; id ? [id] : nil end - def save; @id = 1 end - def new_record?; @id.nil? end - def name - @id.nil? ? 'new sheep' : "sheep ##{@id}" - end -end - - -class TagRelevance - extend ActiveModel::Naming - include ActiveModel::Conversion - - attr_reader :id - attr_reader :tag_id - def initialize(id = nil, tag_id = nil); @id, @tag_id = id, tag_id end - def to_key; id ? [id] : nil end - def save; @id = 1; @tag_id = 1 end - def persisted?; @id.present? end - def to_param; @id; end - def value - @id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}" - end -end - -class Author < Comment - attr_accessor :post - def post_attributes=(attributes); end -end - -class HashBackedAuthor < Hash - extend ActiveModel::Naming - include ActiveModel::Conversion - - def persisted?; false; end - - def name - "hash backed author" - end -end - module Blog def self.use_relative_model_naming? true @@ -193,21 +111,8 @@ module Blog end end -class ArelLike - def to_ary - true - end - def each - a = Array.new(2) { |id| Comment.new(id + 1) } - a.each { |i| yield i } - end -end - class RenderJsonTestException < Exception def to_json(options = nil) return { :error => self.class.name, :message => self.to_s }.to_json end end - -class Car < Struct.new(:color) -end diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index a09650c559..4adf1dbd8f 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,8 @@ +* Ensure ActionView::Digestor.cache is correctly cleaned up when + combining recursive templates with ActionView::Resolver.caching = false + + *wyaeld* + * Fix `collection_check_boxes` generated hidden input to use the name attribute provided in the options hash. @@ -32,7 +37,7 @@ *Paul Nikitochkin* -* Only cache template digests if `config.cache_template_loading` id true. +* Only cache template digests if `config.cache_template_loading` is true. *Josh Lauer*, *Justin Ridgewell* diff --git a/actionview/RUNNING_UNIT_TESTS.rdoc b/actionview/RUNNING_UNIT_TESTS.rdoc index a0c35e1810..104b3e288d 100644 --- a/actionview/RUNNING_UNIT_TESTS.rdoc +++ b/actionview/RUNNING_UNIT_TESTS.rdoc @@ -18,7 +18,7 @@ which can be further narrowed down to one test: == Dependency on Active Record and database setup -Test cases in the test/active_record/ directory depend on having +Test cases in the test/activerecord/ directory depend on having activerecord and sqlite installed. If Active Record is not in actionview/../activerecord directory, or the sqlite rubygem is not installed, these tests are skipped. diff --git a/actionview/Rakefile b/actionview/Rakefile index e1cb4b5be0..d56fe9ea76 100644 --- a/actionview/Rakefile +++ b/actionview/Rakefile @@ -11,7 +11,7 @@ task :test => ["test:template", "test:integration:action_pack", "test:integratio namespace :test do task :isolated do - Dir.glob("test/{active_record,template}/**/*_test.rb").all? do |file| + Dir.glob("test/{actionpack,activerecord,template}/**/*_test.rb").all? do |file| sh(Gem.ruby, '-w', '-Ilib:test', file) end or raise "Failures" end diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index af158a630b..5570e2a8dc 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -35,13 +35,13 @@ module ActionView Digestor end + digest = klass.new(name, format, finder, options).digest # Store the actual digest if config.cache_template_loading is true - klass.new(name, format, finder, options).digest.tap do |digest| - @@cache[cache_key] = digest if ActionView::Resolver.caching? - end - rescue Exception - @@cache.delete_pair(cache_key, false) if pre_stored # something went wrong, make sure not to corrupt the @@cache - raise + @@cache[cache_key] = stored_digest = digest if ActionView::Resolver.caching? + digest + ensure + # something went wrong or ActionView::Resolver.caching? is false, make sure not to corrupt the @@cache + @@cache.delete_pair(cache_key, false) if pre_stored && !stored_digest end end diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index a13d0021ea..e2430140c2 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -224,14 +224,14 @@ module ActionView # # ==== Examples # - # image_tag('rails.png') - # # => <img alt="Rails" src="/assets/rails.png" /> + # image_alt('rails.png') + # # => Rails # - # image_tag('hyphenated-file-name.png') - # # => <img alt="Hyphenated file name" src="/assets/hyphenated-file-name.png" /> + # image_alt('hyphenated-file-name.png') + # # => Hyphenated file name # - # image_tag('underscored_file_name.png') - # # => <img alt="Underscored file name" src="/assets/underscored_file_name.png" /> + # image_alt('underscored_file_name.png') + # # => Underscored file name def image_alt(src) File.basename(src, '.*').sub(/-[[:xdigit:]]{32}\z/, '').tr('-_', ' ').capitalize end diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb index 8e1aea50a9..36e4c5725f 100644 --- a/actionview/lib/action_view/helpers/date_helper.rb +++ b/actionview/lib/action_view/helpers/date_helper.rb @@ -50,19 +50,19 @@ module ActionView # distance_of_time_in_words(from_time, from_time + 50.minutes) # => about 1 hour # distance_of_time_in_words(from_time, 50.minutes.from_now) # => about 1 hour # distance_of_time_in_words(from_time, from_time + 15.seconds) # => less than a minute - # distance_of_time_in_words(from_time, from_time + 15.seconds, include_seconds: true) # => less than 20 seconds + # distance_of_time_in_words(from_time, from_time + 15.seconds, include_seconds: true) # => less than 20 seconds # distance_of_time_in_words(from_time, 3.years.from_now) # => about 3 years # distance_of_time_in_words(from_time, from_time + 60.hours) # => 3 days - # distance_of_time_in_words(from_time, from_time + 45.seconds, include_seconds: true) # => less than a minute - # distance_of_time_in_words(from_time, from_time - 45.seconds, include_seconds: true) # => less than a minute + # distance_of_time_in_words(from_time, from_time + 45.seconds, include_seconds: true) # => less than a minute + # distance_of_time_in_words(from_time, from_time - 45.seconds, include_seconds: true) # => less than a minute # distance_of_time_in_words(from_time, 76.seconds.from_now) # => 1 minute # distance_of_time_in_words(from_time, from_time + 1.year + 3.days) # => about 1 year # distance_of_time_in_words(from_time, from_time + 3.years + 6.months) # => over 3 years # distance_of_time_in_words(from_time, from_time + 4.years + 9.days + 30.minutes + 5.seconds) # => about 4 years # # to_time = Time.now + 6.years + 19.days - # distance_of_time_in_words(from_time, to_time, include_seconds: true) # => about 6 years - # distance_of_time_in_words(to_time, from_time, include_seconds: true) # => about 6 years + # distance_of_time_in_words(from_time, to_time, include_seconds: true) # => about 6 years + # distance_of_time_in_words(to_time, from_time, include_seconds: true) # => about 6 years # distance_of_time_in_words(Time.now, Time.now) # => less than a minute def distance_of_time_in_words(from_time, to_time = 0, options = {}) options = { @@ -531,7 +531,7 @@ module ActionView # my_date = Time.now + 2.days # # # Generates a select field for days that defaults to the day for the date in my_date. - # select_day(my_time) + # select_day(my_date) # # # Generates a select field for days that defaults to the number given. # select_day(5) @@ -541,7 +541,7 @@ module ActionView # # # Generates a select field for days that defaults to the day for the date in my_date # # that is named 'due' rather than 'day'. - # select_day(my_time, field_name: 'due') + # select_day(my_date, field_name: 'due') # # # Generates a select field for days with a custom prompt. Use <tt>prompt: true</tt> for a # # generic prompt. diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index ead7871fc5..38d969ed0c 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -1173,7 +1173,7 @@ module ActionView # methods in the +FormHelper+ module. This class, however, allows you to # call methods with the model object you are building the form for. # - # You can create your own custom FormBuilder templates by subclasses this + # You can create your own custom FormBuilder templates by subclassing this # class. For example: # # class MyFormBuilder < ActionView::Helpers::FormBuilder diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb index 732f35643a..11e41ec056 100644 --- a/actionview/lib/action_view/helpers/tag_helper.rb +++ b/actionview/lib/action_view/helpers/tag_helper.rb @@ -114,7 +114,7 @@ module ActionView # cdata_section("hello]]>world") # # => <![CDATA[hello]]]]><![CDATA[>world]]> def cdata_section(content) - splitted = content.gsub(']]>', ']]]]><![CDATA[>') + splitted = content.to_s.gsub(']]>', ']]]]><![CDATA[>') "<![CDATA[#{splitted}]]>".html_safe end diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index 8a4918a8c0..56dd7a4390 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -214,6 +214,7 @@ module ActionView # * <tt>:form</tt> - This hash will be form attributes # * <tt>:form_class</tt> - This controls the class of the form within which the submit button will # be placed + # * <tt>:params</tt> - Hash of parameters to be rendered as hidden fields within the form. # # ==== Data attributes # @@ -288,6 +289,7 @@ module ActionView url = options.is_a?(String) ? options : url_for(options) remote = html_options.delete('remote') + params = html_options.delete('params') method = html_options.delete('method').to_s method_tag = BUTTON_TAG_METHOD_VERBS.include?(method) ? method_tag(method) : ''.html_safe @@ -311,6 +313,11 @@ module ActionView end inner_tags = method_tag.safe_concat(button).safe_concat(request_token_tag) + if params + params.each do |param_name, value| + inner_tags.safe_concat tag(:input, type: "hidden", name: param_name, value: value.to_param) + end + end content_tag('form', content_tag('div', inner_tags), form_options) end @@ -455,7 +462,7 @@ module ActionView html_options, name = name, nil if block_given? html_options = (html_options || {}).stringify_keys - extras = %w{ cc bcc body subject }.map { |item| + extras = %w{ cc bcc body subject }.map! { |item| option = html_options.delete(item) || next "#{item}=#{Rack::Utils.escape_path(option)}" }.compact diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb index e2c50fec47..9b0619f1aa 100644 --- a/actionview/lib/action_view/template.rb +++ b/actionview/lib/action_view/template.rb @@ -142,7 +142,7 @@ module ActionView compile!(view) view.send(method_name, locals, buffer, &block) end - rescue Exception => e + rescue => e handle_render_error(view, e) end @@ -294,7 +294,7 @@ module ActionView begin mod.module_eval(source, identifier, 0) ObjectSpace.define_finalizer(self, Finalizer[method_name, mod]) - rescue Exception => e # errors from template code + rescue => e # errors from template code if logger = (view && view.logger) logger.debug "ERROR: compiling #{method_name} RAISED #{e}" logger.debug "Function body: #{source}" diff --git a/actionview/test/actionpack/abstract/helper_test.rb b/actionview/test/actionpack/abstract/helper_test.rb index 4e05245584..89c4567715 100644 --- a/actionview/test/actionpack/abstract/helper_test.rb +++ b/actionview/test/actionpack/abstract/helper_test.rb @@ -52,7 +52,7 @@ module AbstractController class AbstractInvalidHelpers < AbstractHelpers include ActionController::Helpers - path = File.join(File.expand_path('../../../fixtures', __FILE__), "helpers_missing") + path = File.expand_path('../../../fixtures/helpers_missing', __FILE__) $:.unshift(path) self.helpers_path = path end diff --git a/actionview/test/lib/controller/view_paths_test.rb b/actionview/test/actionpack/controller/view_paths_test.rb index c6e7a523b9..c6e7a523b9 100644 --- a/actionview/test/lib/controller/view_paths_test.rb +++ b/actionview/test/actionpack/controller/view_paths_test.rb diff --git a/actionview/test/lib/controller/fake_controllers.rb b/actionview/test/lib/controller/fake_controllers.rb deleted file mode 100644 index 1a2863b689..0000000000 --- a/actionview/test/lib/controller/fake_controllers.rb +++ /dev/null @@ -1,35 +0,0 @@ -class ContentController < ActionController::Base; end - -module Admin - class AccountsController < ActionController::Base; end - class PostsController < ActionController::Base; end - class StuffController < ActionController::Base; end - class UserController < ActionController::Base; end - class UsersController < ActionController::Base; end -end - -module Api - class UsersController < ActionController::Base; end - class ProductsController < ActionController::Base; end -end - -class AccountController < ActionController::Base; end -class ArchiveController < ActionController::Base; end -class ArticlesController < ActionController::Base; end -class BarController < ActionController::Base; end -class BlogController < ActionController::Base; end -class BooksController < ActionController::Base; end -class CarsController < ActionController::Base; end -class CcController < ActionController::Base; end -class CController < ActionController::Base; end -class FooController < ActionController::Base; end -class GeocodeController < ActionController::Base; end -class NewsController < ActionController::Base; end -class NotesController < ActionController::Base; end -class PagesController < ActionController::Base; end -class PeopleController < ActionController::Base; end -class PostsController < ActionController::Base; end -class SubpathBooksController < ActionController::Base; end -class SymbolsController < ActionController::Base; end -class UserController < ActionController::Base; end -class UsersController < ActionController::Base; end diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb index 0f6b14a57d..779a7fb53c 100644 --- a/actionview/test/template/digestor_test.rb +++ b/actionview/test/template/digestor_test.rb @@ -217,6 +217,31 @@ class TemplateDigestorTest < ActionView::TestCase ActionView::Resolver.caching = resolver_before end + def test_digest_cache_cleanup_with_recursion + first_digest = digest("level/_recursion") + second_digest = digest("level/_recursion") + + assert first_digest + + # If the cache is cleaned up correctly, subsequent digests should return the same + assert_equal first_digest, second_digest + end + + def test_digest_cache_cleanup_with_recursion_and_template_caching_off + resolver_before = ActionView::Resolver.caching + ActionView::Resolver.caching = false + + first_digest = digest("level/_recursion") + second_digest = digest("level/_recursion") + + assert first_digest + + # If the cache is cleaned up correctly, subsequent digests should return the same + assert_equal first_digest, second_digest + ensure + ActionView::Resolver.caching = resolver_before + end + private def assert_logged(message) old_logger = ActionView::Base.logger diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb index 928dfb092d..5a7d11f513 100644 --- a/actionview/test/template/render_test.rb +++ b/actionview/test/template/render_test.rb @@ -381,7 +381,7 @@ module RenderTestCases def test_render_ignores_templates_with_malformed_template_handlers ActiveSupport::Deprecation.silence do %w(malformed malformed.erb malformed.html.erb malformed.en.html.erb).each do |name| - assert File.exists?(File.expand_path("#{FIXTURE_LOAD_PATH}/test/malformed/#{name}~")), "Malformed file (#{name}~) which should be ignored does not exists" + assert File.exist?(File.expand_path("#{FIXTURE_LOAD_PATH}/test/malformed/#{name}~")), "Malformed file (#{name}~) which should be ignored does not exists" assert_raises(ActionView::MissingTemplate) { @view.render(:file => "test/malformed/#{name}") } end end diff --git a/actionview/test/template/tag_helper_test.rb b/actionview/test/template/tag_helper_test.rb index 802da5d566..fb016a52de 100644 --- a/actionview/test/template/tag_helper_test.rb +++ b/actionview/test/template/tag_helper_test.rb @@ -96,6 +96,10 @@ class TagHelperTest < ActionView::TestCase assert_equal "<![CDATA[<hello world>]]>", cdata_section("<hello world>") end + def test_cdata_section_with_string_conversion + assert_equal "<![CDATA[]]>", cdata_section(nil) + end + def test_cdata_section_splitted assert_equal "<![CDATA[hello]]]]><![CDATA[>world]]>", cdata_section("hello]]>world") assert_equal "<![CDATA[hello]]]]><![CDATA[>world]]]]><![CDATA[>again]]>", cdata_section("hello]]>world]]>again") diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb index e3440915a4..deba33510a 100644 --- a/actionview/test/template/url_helper_test.rb +++ b/actionview/test/template/url_helper_test.rb @@ -161,6 +161,13 @@ class UrlHelperTest < ActiveSupport::TestCase ) end + def test_button_to_with_params + assert_dom_equal( + %{<form action="http://www.example.com" class="button_to" method="post"><div><input type="submit" value="Hello" /><input type="hidden" name="foo" value="bar" /><input type="hidden" name="baz" value="quux" /></div></form>}, + button_to("Hello", "http://www.example.com", params: {foo: :bar, baz: "quux"}) + ) + end + def test_link_tag_with_straight_url assert_dom_equal %{<a href="http://www.example.com">Hello</a>}, link_to("Hello", "http://www.example.com") end diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 91b25d5cda..e8602ecbcf 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,3 +1,12 @@ +* Fix `has_secure_password` to honor bcrypt-ruby's cost attribute. + + *T.J. Schuck* + +* Updated the `ActiveModel::Dirty#changed_attributes` method to be indifferent between using + symbols and strings as keys. + + *William Myers* + * Added new API methods `reset_changes` and `changed_applied` to `ActiveModel::Dirty` that control changes state. Previsously you needed to update internal instance variables, but now API methods are available. diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index 51655fe3da..11e755649c 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |s| s.name = 'activemodel' s.version = version s.summary = 'A toolkit for building modeling frameworks (part of Rails).' - s.description = 'A toolkit for building modeling frameworks like Active Record. Rich support for attributes, callbacks, validations, observers, serialization, internationalization, and testing.' + s.description = 'A toolkit for building modeling frameworks like Active Record. Rich support for attributes, callbacks, validations, serialization, internationalization, and testing.' s.required_ruby_version = '>= 1.9.3' diff --git a/activemodel/examples/validations.rb b/activemodel/examples/validations.rb index c94cd17e18..b8e74acd5e 100644 --- a/activemodel/examples/validations.rb +++ b/activemodel/examples/validations.rb @@ -1,3 +1,4 @@ +require File.expand_path('../../../load_paths', __FILE__) require 'active_model' class Person diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index 98e7d84608..c5f1b3f11a 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -145,7 +145,7 @@ module ActiveModel # person.name = 'robert' # person.changed_attributes # => {"name" => "bob"} def changed_attributes - @changed_attributes ||= {} + @changed_attributes ||= ActiveSupport::HashWithIndifferentAccess.new end # Handle <tt>*_changed?</tt> for +method_missing+. diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index bc9edf4a56..198efc5088 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -262,10 +262,10 @@ module ActiveModel # namespaced models regarding whether it's inside isolated engine. # # # For isolated engine: - # ActiveModel::Naming.singular_route_key(Blog::Post) #=> post + # ActiveModel::Naming.singular_route_key(Blog::Post) #=> "post" # # # For shared engine: - # ActiveModel::Naming.singular_route_key(Blog::Post) #=> blog_post + # ActiveModel::Naming.singular_route_key(Blog::Post) #=> "blog_post" def self.singular_route_key(record_or_class) model_name_from_record_or_class(record_or_class).singular_route_key end @@ -274,10 +274,10 @@ module ActiveModel # namespaced models regarding whether it's inside isolated engine. # # # For isolated engine: - # ActiveModel::Naming.route_key(Blog::Post) #=> posts + # ActiveModel::Naming.route_key(Blog::Post) #=> "posts" # # # For shared engine: - # ActiveModel::Naming.route_key(Blog::Post) #=> blog_posts + # ActiveModel::Naming.route_key(Blog::Post) #=> "blog_posts" # # The route key also considers if the noun is uncountable and, in # such cases, automatically appends _index. @@ -289,10 +289,10 @@ module ActiveModel # namespaced models regarding whether it's inside isolated engine. # # # For isolated engine: - # ActiveModel::Naming.param_key(Blog::Post) #=> post + # ActiveModel::Naming.param_key(Blog::Post) #=> "post" # # # For shared engine: - # ActiveModel::Naming.param_key(Blog::Post) #=> blog_post + # ActiveModel::Naming.param_key(Blog::Post) #=> "blog_post" def self.param_key(record_or_class) model_name_from_record_or_class(record_or_class).param_key end diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index 17fafe4be9..7e694b5c50 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -46,7 +46,6 @@ module ActiveModel # This is to avoid ActiveModel (and by extension the entire framework) # being dependent on a binary library. begin - gem 'bcrypt-ruby', '~> 3.1.2' require 'bcrypt' rescue LoadError $stderr.puts "You don't have bcrypt-ruby installed in your application. Please add it to your Gemfile and run bundle install" @@ -103,7 +102,7 @@ module ActiveModel def password=(unencrypted_password) unless unencrypted_password.blank? @password = unencrypted_password - cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine::DEFAULT_COST + cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost) end end diff --git a/activemodel/lib/active_model/validations/clusivity.rb b/activemodel/lib/active_model/validations/clusivity.rb index 1c35cb7c35..fd6cc1edb4 100644 --- a/activemodel/lib/active_model/validations/clusivity.rb +++ b/activemodel/lib/active_model/validations/clusivity.rb @@ -30,12 +30,18 @@ module ActiveModel @delimiter ||= options[:in] || options[:within] end - # In Ruby 1.9 <tt>Range#include?</tt> on non-numeric ranges checks all possible values in the - # range for equality, which is slower but more accurate. <tt>Range#cover?</tt> uses - # the previous logic of comparing a value with the range endpoints, which is fast - # but is only accurate on numeric ranges. + # In Ruby 1.9 <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all + # possible values in the range for equality, which is slower but more accurate. + # <tt>Range#cover?</tt> uses the previous logic of comparing a value with the range + # endpoints, which is fast but is only accurate on Numeric, Time, or DateTime ranges. def inclusion_method(enumerable) - (enumerable.is_a?(Range) && enumerable.first.is_a?(Numeric)) ? :cover? : :include? + return :include? unless enumerable.is_a?(Range) + case enumerable.first + when Numeric, Time, DateTime + :cover? + else + :include? + end end end end diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb index ed34ca8a6e..a90d0b1299 100644 --- a/activemodel/test/cases/dirty_test.rb +++ b/activemodel/test/cases/dirty_test.rb @@ -3,11 +3,12 @@ require "cases/helper" class DirtyTest < ActiveModel::TestCase class DirtyModel include ActiveModel::Dirty - define_attribute_methods :name, :color + define_attribute_methods :name, :color, :size def initialize @name = nil @color = nil + @size = nil end def name @@ -28,6 +29,15 @@ class DirtyTest < ActiveModel::TestCase @color = val end + def size + @size + end + + def size=(val) + attribute_will_change!(:size) unless val == @size + @size = val + end + def save changes_applied end @@ -124,4 +134,9 @@ class DirtyTest < ActiveModel::TestCase assert_equal ["Otto", "Mr. Manfredgensonton"], @model.name_change assert_equal @model.name_was, "Otto" end + + test "using attribute_will_change! with a symbol" do + @model.size = 1 + assert @model.size_changed? + end end diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb index 98e5c747d5..41d0b2263e 100644 --- a/activemodel/test/cases/secure_password_test.rb +++ b/activemodel/test/cases/secure_password_test.rb @@ -82,6 +82,14 @@ class SecurePasswordTest < ActiveModel::TestCase assert_equal BCrypt::Engine::DEFAULT_COST, @user.password_digest.cost end + test "Password digest cost honors bcrypt cost attribute when min_cost is false" do + ActiveModel::SecurePassword.min_cost = false + BCrypt::Engine.cost = 5 + + @user.password = "secret" + assert_equal BCrypt::Engine.cost, @user.password_digest.cost + end + test "Password digest cost can be set to bcrypt min cost to speed up tests" do ActiveModel::SecurePassword.min_cost = true diff --git a/activemodel/test/cases/validations/inclusion_validation_test.rb b/activemodel/test/cases/validations/inclusion_validation_test.rb index 01a373d85d..8b90856869 100644 --- a/activemodel/test/cases/validations/inclusion_validation_test.rb +++ b/activemodel/test/cases/validations/inclusion_validation_test.rb @@ -1,5 +1,6 @@ # encoding: utf-8 require 'cases/helper' +require 'active_support/all' require 'models/topic' require 'models/person' @@ -20,6 +21,27 @@ class InclusionValidationTest < ActiveModel::TestCase assert Topic.new("title" => "bbb", "content" => "abc").valid? end + def test_validates_inclusion_of_time_range + Topic.validates_inclusion_of(:created_at, in: 1.year.ago..Time.now) + assert Topic.new(title: 'aaa', created_at: 2.years.ago).invalid? + assert Topic.new(title: 'aaa', created_at: 3.months.ago).valid? + assert Topic.new(title: 'aaa', created_at: 37.weeks.from_now).invalid? + end + + def test_validates_inclusion_of_date_range + Topic.validates_inclusion_of(:created_at, in: 1.year.until(Date.today)..Date.today) + assert Topic.new(title: 'aaa', created_at: 2.years.until(Date.today)).invalid? + assert Topic.new(title: 'aaa', created_at: 3.months.until(Date.today)).valid? + assert Topic.new(title: 'aaa', created_at: 37.weeks.since(Date.today)).invalid? + end + + def test_validates_inclusion_of_date_time_range + Topic.validates_inclusion_of(:created_at, in: 1.year.until(DateTime.current)..DateTime.current) + assert Topic.new(title: 'aaa', created_at: 2.years.until(DateTime.current)).invalid? + assert Topic.new(title: 'aaa', created_at: 3.months.until(DateTime.current)).valid? + assert Topic.new(title: 'aaa', created_at: 37.weeks.since(DateTime.current)).invalid? + end + def test_validates_inclusion_of Topic.validates_inclusion_of(:title, in: %w( a b c d e f g )) diff --git a/activemodel/test/models/topic.rb b/activemodel/test/models/topic.rb index c9af78f595..1411a093e9 100644 --- a/activemodel/test/models/topic.rb +++ b/activemodel/test/models/topic.rb @@ -6,7 +6,7 @@ class Topic super | [ :message ] end - attr_accessor :title, :author_name, :content, :approved + attr_accessor :title, :author_name, :content, :approved, :created_at attr_accessor :after_validation_performed after_validation :perform_after_validation diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 810a2c668d..0497f6326e 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,223 @@ +* `.unscope` now removes conditions specified in `default_scope`. + + *Jon Leighton* + +* Added ActiveRecord::QueryMethods#rewhere which will overwrite an existing, named where condition. + + Examples: + + Post.where(trashed: true).where(trashed: false) #=> WHERE `trashed` = 1 AND `trashed` = 0 + Post.where(trashed: true).rewhere(trashed: false) #=> WHERE `trashed` = 0 + Post.where(active: true).where(trashed: true).rewhere(trashed: false) #=> WHERE `active` = 1 AND `trashed` = 0 + + *DHH* + +* Extend ActiveRecord::Base#cache_key to take an optional list of timestamp attributes of which the highest will be used. + + Example: + + # last_reviewed_at will be used, if that's more recent than updated_at, or vice versa + Person.find(5).cache_key(:updated_at, :last_reviewed_at) + + *DHH* + +* Added ActiveRecord::Base#enum for declaring enum attributes where the values map to integers in the database, but can be queried by name. + + Example: + + class Conversation < ActiveRecord::Base + enum status: [:active, :archived] + end + + Conversation::STATUS # => { active: 0, archived: 1 } + + # conversation.update! status: 0 + conversation.active! + conversation.active? # => true + conversation.status # => :active + + # conversation.update! status: 1 + conversation.archived! + conversation.archived? # => true + conversation.status # => :archived + + # conversation.update! status: 1 + conversation.status = :archived + + *DHH* + +* ActiveRecord::Base#attribute_for_inspect now truncates long arrays (more than 10 elements) + + *Jan Bernacki* + +* Allow for the name of the schema_migrations table to be configured. + + *Jerad Phelps* + +* Do not add to scope includes values from through associations. + Fixed bug when providing `includes` in through association scope, and fetching targets. + + Example: + + class Vendor < ActiveRecord::Base + has_many :relationships, -> { includes(:user) } + has_many :users, through: :relationships + end + + vendor = Vendor.first + + # Before + + vendor.users.to_a # => Raises exception: not found `:user` for `User` + + # After + + vendor.users.to_a # => No exception is raised + + Fixes #12242, #9517, #10240. + + *Paul Nikitochkin* + +* Type cast json values on write, so that the value is consistent + with reading from the database. + + Example: + + x = JsonDataType.new tags: {"string" => "foo", :symbol => :bar} + + # Before: + x.tags # => {"string" => "foo", :symbol => :bar} + + # After: + x.tags # => {"string" => "foo", "symbol" => "bar"} + + *Severin Schoepke* + +* `ActiveRecord::Store` works together with PG `hstore` columns. + Fixes #12452. + + *Yves Senn* + +* Fix bug where `ActiveRecord::Store` used a global `Hash` to keep track of + all registered `stored_attributes`. Now every subclass of + `ActiveRecord::Base` has it's own `Hash`. + + *Yves Senn* + +* Save `has_one` association when primary key is manually set. + + Fixes #12302. + + *Lauro Caetano* + +* Allow any version of BCrypt when using `has_secure_password`. + + *Mike Perham* + +* Sub-query generated for `Relation` passed as array condition did not take in account + bind values and have invalid syntax. + + Generate sub-query with inline bind values. + + Fixes #12586. + + *Paul Nikitochkin* + +* Fix a bug where rake db:structure:load crashed when the path contained + spaces. + + *Kevin Mook* + +* `ActiveRecord::QueryMethods#unscope` unscopes negative equality + + Allows you to call `#unscope` on a relation with negative equality + operators, i.e. `Arel::Nodes::NotIn` and `Arel::Nodes::NotEqual` that have + been generated through the use of `where.not`. + + *Eric Hankins* + +* Raise an exception when model without primary key calls `.find_with_ids`. + + *Shimpei Makimoto* + +* Make `Relation#empty?` use `exists?` instead of `count`. + + *Szymon Nowak* + +* `rake db:structure:dump` no longer crashes when the port was specified as `Fixnum`. + + *Kenta Okamoto* + +* `NullRelation#pluck` takes a list of columns + + The method signature in `NullRelation` was updated to mimic that in + `Calculations`. + + *Derek Prior* + +* `scope_chain` should not be mutated for other reflections. + + Currently `scope_chain` uses same array for building different + `scope_chain` for different associations. During processing + these arrays are sometimes mutated and because of in-place + mutation the changed `scope_chain` impacts other reflections. + + Fix is to dup the value before adding to the `scope_chain`. + + Fixes #3882. + + *Neeraj Singh* + +* Prevent the inversed association from being reloaded on save. + + Fixes #9499. + + *Dmitry Polushkin* + +* Generate subquery for `Relation` if it passed as array condition for `where` + method. + + Example: + + # Before + Blog.where('id in (?)', Blog.where(id: 1)) + # => SELECT "blogs".* FROM "blogs" WHERE "blogs"."id" = 1 + # => SELECT "blogs".* FROM "blogs" WHERE (id IN (1)) + + # After + Blog.where('id in (?)', Blog.where(id: 1).select(:id)) + # => SELECT "blogs".* FROM "blogs" + # WHERE "blogs"."id" IN (SELECT "blogs"."id" FROM "blogs" WHERE "blogs"."id" = 1) + + Fixes #12415. + + *Paul Nikitochkin* + +* For missed association exception message + which is raised in `ActiveRecord::Associations::Preloader` class + added owner record class name in order to simplify to find problem code. + + *Paul Nikitochkin* + +* `has_and_belongs_to_many` is now transparently implemented in terms of + `has_many :through`. Behavior should remain the same, if not, it is a bug. + +* `create_savepoint`, `rollback_to_savepoint` and `release_savepoint` accept + a savepoint name. + + *Yves Senn* + +* Make `next_migration_number` accessible for third party generators. + + *Yves Senn* + +* Objects instantiated using a null relationship will now retain the + attributes of the where clause. + + Fixes #11676, #11675, #11376. + + *Paul Nikitochkin*, *Peter Brown*, *Nthalk* + * Fixed `ActiveRecord::Associations::CollectionAssociation#find` when using `has_many` association with `:inverse_of` and finding an array of one element, it should return an array of one element too. @@ -11,7 +231,7 @@ * `ActiveRecord::ConnectionAdapters.string_to_time` respects string with timezone (e.g. Wed, 04 Sep 2013 20:30:00 JST). - Fixes: #12278 + Fixes #12278. *kennyj* @@ -42,23 +262,11 @@ *kennyj* -* ActiveRecord::Base#<=> has been removed. Primary keys may not be in order, - or even be numbers, so sorting by id doesn't make sense. Please use `sort_by` - and specify the attribute you wish to sort with. For example, change: - - Post.all.to_a.sort - - to: - - Post.all.to_a.sort_by(&:id) - - *Aaron Patterson* - * Fix: joins association, with defined in the scope block constraints by using several where constraints and at least of them is not `Arel::Nodes::Equality`, generates invalid SQL expression. - Fixes: #11963 + Fixes #11963. *Paul Nikitochkin* @@ -180,6 +388,12 @@ *Yves Senn* +* Fixes bug when using includes combined with select, the select statement was overwritten. + + Fixes #11773. + + *Edo Balvers* + * Load fixtures from linked folders. *Kassio Borges* @@ -231,13 +445,13 @@ to allow the connection adapter to properly determine how to quote the value. This was affecting certain databases that use specific column types. - Fixes: #6763 + Fixes #6763. *Alfred Wong* * rescue from all exceptions in `ConnectionManagement#call` - Fixes #11497 + Fixes #11497. As `ActiveRecord::ConnectionAdapters::ConnectionManagement` middleware does not rescue from Exception (but only from StandardError), the Connection @@ -259,27 +473,27 @@ * Remove extra decrement of transaction deep level. - Fixes: #4566 + Fixes #4566. *Paul Nikitochkin* * Reset @column_defaults when assigning `locking_column`. We had a potential problem. For example: - class Post < ActiveRecord::Base - self.column_defaults # if we call this unintentionally before setting locking_column ... - self.locking_column = 'my_locking_column' - end + class Post < ActiveRecord::Base + self.column_defaults # if we call this unintentionally before setting locking_column ... + self.locking_column = 'my_locking_column' + end - Post.column_defaults["my_locking_column"] - => nil # expected value is 0 ! + Post.column_defaults["my_locking_column"] + => nil # expected value is 0 ! *kennyj* * Remove extra select and update queries on save/touch/destroy ActiveRecord model with belongs to reflection with option `touch: true`. - Fixes: #11288 + Fixes #11288. *Paul Nikitochkin* @@ -367,11 +581,11 @@ Method `delete_all` should not be invoking callbacks and this feature was deprecated in Rails 4.0. This is being removed. `delete_all` will continue to honor the `:dependent` option. However - if `:dependent` value is `:destroy` then the default deletion + if `:dependent` value is `:destroy` then the `:delete_all` deletion strategy for that collection will be applied. User can also force a deletion strategy by passing parameter to - `delete_all`. For example you can do `@post.comments.delete_all(:nullify)` . + `delete_all`. For example you can do `@post.comments.delete_all(:nullify)`. *Neeraj Singh* @@ -519,7 +733,7 @@ *Neeraj Singh* -* Fixture setup does no longer depend on `ActiveRecord::Base.configurations`. +* Fixture setup no longer depends on `ActiveRecord::Base.configurations`. This is relevant when `ENV["DATABASE_URL"]` is used in place of a `database.yml`. *Yves Senn* diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index f19f5ecdf9..7a2c5c8bf2 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -37,6 +37,7 @@ module ActiveRecord autoload :ConnectionHandling autoload :CounterCache autoload :DynamicMatchers + autoload :Enum autoload :Explain autoload :Inheritance autoload :Integration diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 33cbafc6aa..74e2774626 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -73,12 +73,6 @@ module ActiveRecord end end - class HasAndBelongsToManyAssociationForeignKeyNeeded < ActiveRecordError #:nodoc: - def initialize(reflection) - super("Cannot create self referential has_and_belongs_to_many association on '#{reflection.class_name rescue nil}##{reflection.name rescue nil}'. :association_foreign_key cannot be the same as the :foreign_key.") - end - end - class EagerLoadPolymorphicError < ActiveRecordError #:nodoc: def initialize(reflection) super("Can not eagerly load the polymorphic association #{reflection.name.inspect}") @@ -114,7 +108,6 @@ module ActiveRecord autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association' autoload :BelongsToPolymorphicAssociation, 'active_record/associations/belongs_to_polymorphic_association' - autoload :HasAndBelongsToManyAssociation, 'active_record/associations/has_and_belongs_to_many_association' autoload :HasManyAssociation, 'active_record/associations/has_many_association' autoload :HasManyThroughAssociation, 'active_record/associations/has_many_through_association' autoload :HasOneAssociation, 'active_record/associations/has_one_association' @@ -1560,8 +1553,39 @@ module ActiveRecord # has_and_belongs_to_many :categories, join_table: "prods_cats" # has_and_belongs_to_many :categories, -> { readonly } def has_and_belongs_to_many(name, scope = nil, options = {}, &extension) - reflection = Builder::HasAndBelongsToMany.build(self, name, scope, options, &extension) - Reflection.add_reflection self, name, reflection + if scope.is_a?(Hash) + options = scope + scope = nil + end + + builder = Builder::HasAndBelongsToMany.new name, self, options + + join_model = builder.through_model + + middle_reflection = builder.middle_reflection join_model + + Builder::HasMany.define_callbacks self, middle_reflection + Reflection.add_reflection self, middle_reflection.name, middle_reflection + + include Module.new { + class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def destroy_associations + association(:#{middle_reflection.name}).delete_all(:delete_all) + association(:#{name}).reset + super + end + RUBY + } + + hm_options = {} + hm_options[:through] = middle_reflection.name + hm_options[:source] = join_model.right_reflection.name + + [:before_add, :after_add, :before_remove, :after_remove].each do |k| + hm_options[k] = options[k] if options.key? k + end + + has_many name, scope, hm_options, &extension end end end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 67d24b35d1..e6a45487d0 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -13,11 +13,11 @@ module ActiveRecord # BelongsToAssociation # BelongsToPolymorphicAssociation # CollectionAssociation - # HasAndBelongsToManyAssociation # HasManyAssociation # HasManyThroughAssociation + ThroughAssociation class Association #:nodoc: attr_reader :owner, :target, :reflection + attr_accessor :inversed delegate :options, :to => :reflection @@ -43,6 +43,7 @@ module ActiveRecord @loaded = false @target = nil @stale_state = nil + @inversed = false end # Reloads the \target and returns +self+ on success. @@ -60,8 +61,9 @@ module ActiveRecord # Asserts the \target has been loaded setting the \loaded flag to +true+. def loaded! - @loaded = true + @loaded = true @stale_state = stale_state + @inversed = false end # The target is stale if the target no longer points to the record(s) that the @@ -71,7 +73,7 @@ module ActiveRecord # # Note that if the target has not been loaded, it is not considered stale. def stale_target? - loaded? && @stale_state != stale_state + !inversed && loaded? && @stale_state != stale_state end # Sets the target of this association to <tt>\target</tt>, and the \loaded flag to +true+. @@ -105,6 +107,7 @@ module ActiveRecord if record && invertible_for?(record) inverse = record.association(inverse_reflection_for(record).name) inverse.target = owner + inverse.inversed = true end end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 8027acfb83..17f056e764 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -44,18 +44,6 @@ module ActiveRecord chain.each_with_index do |reflection, i| table, foreign_table = tables.shift, tables.first - if reflection.source_macro == :has_and_belongs_to_many - join_table = tables.shift - - scope = scope.joins(join( - join_table, - table[reflection.association_primary_key]. - eq(join_table[reflection.association_foreign_key]) - )) - - table, foreign_table = join_table, tables.first - end - if reflection.source_macro == :belongs_to if reflection.options[:polymorphic] key = reflection.association_primary_key(self.klass) @@ -90,7 +78,8 @@ module ActiveRecord scope = scope.joins(join(foreign_table, constraint)) end - klass = i == 0 ? self.klass : reflection.klass + is_first_chain = i == 0 + klass = is_first_chain ? self.klass : reflection.klass # Exclude the scope of the association itself, because that # was already merged in the #scope method. @@ -101,7 +90,10 @@ module ActiveRecord scope.merge! item.except(:where, :includes, :bind) end - scope.includes! item.includes_values + if is_first_chain + scope.includes! item.includes_values + end + scope.where_values += item.where_values scope.order_values |= item.order_values end diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index 17b74b197e..37ba1c73b1 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -8,7 +8,6 @@ # - HasOneAssociation # - CollectionAssociation # - HasManyAssociation -# - HasAndBelongsToManyAssociation module ActiveRecord::Associations::Builder class Association #:nodoc: @@ -19,9 +18,15 @@ module ActiveRecord::Associations::Builder VALID_OPTIONS = [:class_name, :class, :foreign_key, :validate] - attr_reader :name, :scope, :options - def self.build(model, name, scope, options, &block) + extension = define_extensions model, name, &block + reflection = create_reflection model, name, scope, options, extension + define_accessors model, reflection + define_callbacks model, reflection + reflection + end + + def self.create_reflection(model, name, scope, options, extension = nil) raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol) if scope.is_a?(Hash) @@ -29,47 +34,48 @@ module ActiveRecord::Associations::Builder scope = nil end - builder = new(name, scope, options, &block) - reflection = builder.build(model) - builder.define_accessors model - builder.define_callbacks model, reflection - builder.define_extensions model - reflection - end + validate_options(options) - def initialize(name, scope, options) - @name = name - @scope = scope - @options = options + scope = build_scope(scope, extension) - validate_options + ActiveRecord::Reflection.create(macro, name, scope, options, model) + end + + def self.build_scope(scope, extension) + new_scope = scope if scope && scope.arity == 0 - @scope = proc { instance_exec(&scope) } + new_scope = proc { instance_exec(&scope) } + end + + if extension + new_scope = wrap_scope new_scope, extension end + + new_scope end - def build(model) - ActiveRecord::Reflection.create(macro, name, scope, options, model) + def self.wrap_scope(scope, extension) + scope end - def macro + def self.macro raise NotImplementedError end - def valid_options + def self.valid_options(options) VALID_OPTIONS + Association.extensions.flat_map(&:valid_options) end - def validate_options - options.assert_valid_keys(valid_options) + def self.validate_options(options) + options.assert_valid_keys(valid_options(options)) end - def define_extensions(model) + def self.define_extensions(model, name) end - def define_callbacks(model, reflection) - add_before_destroy_callbacks(model, name) if options[:dependent] + def self.define_callbacks(model, reflection) + add_before_destroy_callbacks(model, reflection) if reflection.options[:dependent] Association.extensions.each do |extension| extension.build model, reflection end @@ -81,14 +87,14 @@ module ActiveRecord::Associations::Builder # end # # Post.first.comments and Post.first.comments= methods are defined by this method... - - def define_accessors(model) + def self.define_accessors(model, reflection) mixin = model.generated_association_methods - define_readers(mixin) - define_writers(mixin) + name = reflection.name + define_readers(mixin, name) + define_writers(mixin, name) end - def define_readers(mixin) + def self.define_readers(mixin, name) mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{name}(*args) association(:#{name}).reader(*args) @@ -96,7 +102,7 @@ module ActiveRecord::Associations::Builder CODE end - def define_writers(mixin) + def self.define_writers(mixin, name) mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{name}=(value) association(:#{name}).writer(value) @@ -104,17 +110,16 @@ module ActiveRecord::Associations::Builder CODE end - def valid_dependent_options + def self.valid_dependent_options raise NotImplementedError end - private - - def add_before_destroy_callbacks(model, name) - unless valid_dependent_options.include? options[:dependent] - raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{options[:dependent]}" + def self.add_before_destroy_callbacks(model, reflection) + unless valid_dependent_options.include? reflection.options[:dependent] + raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{reflection.options[:dependent]}" end + name = reflection.name model.before_destroy lambda { |o| o.association(name).handle_dependency } end end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 4e88b50ec5..aa43c34d86 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -1,50 +1,44 @@ module ActiveRecord::Associations::Builder class BelongsTo < SingularAssociation #:nodoc: - def macro + def self.macro :belongs_to end - def valid_options + def self.valid_options(options) super + [:foreign_type, :polymorphic, :touch] end - def constructable? - !options[:polymorphic] - end - - def valid_dependent_options + def self.valid_dependent_options [:destroy, :delete] end - def define_callbacks(model, reflection) + def self.define_callbacks(model, reflection) super - add_counter_cache_callbacks(model, reflection) if options[:counter_cache] - add_touch_callbacks(model, reflection) if options[:touch] + add_counter_cache_callbacks(model, reflection) if reflection.options[:counter_cache] + add_touch_callbacks(model, reflection) if reflection.options[:touch] end - def define_accessors(mixin) + def self.define_accessors(mixin, reflection) super add_counter_cache_methods mixin end - private - - def add_counter_cache_methods(mixin) + def self.add_counter_cache_methods(mixin) return if mixin.method_defined? :belongs_to_counter_cache_after_create mixin.class_eval do - def belongs_to_counter_cache_after_create(association, reflection) - if record = send(association.name) + def belongs_to_counter_cache_after_create(reflection) + if record = send(reflection.name) cache_column = reflection.counter_cache_column record.class.increment_counter(cache_column, record.id) @_after_create_counter_called = true end end - def belongs_to_counter_cache_before_destroy(association, reflection) + def belongs_to_counter_cache_before_destroy(reflection) foreign_key = reflection.foreign_key.to_sym unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key - record = send association.name + record = send reflection.name if record && !self.destroyed? cache_column = reflection.counter_cache_column record.class.decrement_counter(cache_column, record.id) @@ -52,13 +46,13 @@ module ActiveRecord::Associations::Builder end end - def belongs_to_counter_cache_after_update(association, reflection) + def belongs_to_counter_cache_after_update(reflection) foreign_key = reflection.foreign_key cache_column = reflection.counter_cache_column if (@_after_create_counter_called ||= false) @_after_create_counter_called = false - elsif attribute_changed?(foreign_key) && !new_record? && association.constructable? + elsif attribute_changed?(foreign_key) && !new_record? && reflection.constructable? model = reflection.klass foreign_key_was = attribute_was foreign_key foreign_key = attribute foreign_key @@ -74,20 +68,19 @@ module ActiveRecord::Associations::Builder end end - def add_counter_cache_callbacks(model, reflection) + def self.add_counter_cache_callbacks(model, reflection) cache_column = reflection.counter_cache_column - association = self model.after_create lambda { |record| - record.belongs_to_counter_cache_after_create(association, reflection) + record.belongs_to_counter_cache_after_create(reflection) } model.before_destroy lambda { |record| - record.belongs_to_counter_cache_before_destroy(association, reflection) + record.belongs_to_counter_cache_before_destroy(reflection) } model.after_update lambda { |record| - record.belongs_to_counter_cache_after_update(association, reflection) + record.belongs_to_counter_cache_after_update(reflection) } klass = reflection.class_name.safe_constantize @@ -120,10 +113,10 @@ module ActiveRecord::Associations::Builder end end - def add_touch_callbacks(model, reflection) + def self.add_touch_callbacks(model, reflection) foreign_key = reflection.foreign_key - n = name - touch = options[:touch] + n = reflection.name + touch = reflection.options[:touch] callback = lambda { |record| BelongsTo.touch_record(record, foreign_key, n, touch) diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb index 7bd0687c0b..2ff67f904d 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -1,4 +1,4 @@ -# This class is inherited by the has_many and has_many_and_belongs_to_many association classes +# This class is inherited by the has_many and has_many_and_belongs_to_many association classes require 'active_record/associations' @@ -7,35 +7,29 @@ module ActiveRecord::Associations::Builder CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove] - def valid_options + def self.valid_options(options) super + [:table_name, :before_add, :after_add, :before_remove, :after_remove, :extend] end - attr_reader :block_extension - - def initialize(name, scope, options) + def self.define_callbacks(model, reflection) super - @mod = nil - if block_given? - @mod = Module.new(&Proc.new) - @scope = wrap_scope @scope, @mod - end + name = reflection.name + options = reflection.options + CALLBACKS.each { |callback_name| + define_callback(model, callback_name, name, options) + } end - def define_callbacks(model, reflection) - super - CALLBACKS.each { |callback_name| define_callback(model, callback_name) } - end - - def define_extensions(model) - if @mod + def self.define_extensions(model, name) + if block_given? extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension" - model.parent.const_set(extension_module_name, @mod) + extension = Module.new(&Proc.new) + model.parent.const_set(extension_module_name, extension) end end - def define_callback(model, callback_name) + def self.define_callback(model, callback_name, name, options) full_callback_name = "#{callback_name}_for_#{name}" # TODO : why do i need method_defined? I think its because of the inheritance chain @@ -54,8 +48,7 @@ module ActiveRecord::Associations::Builder end # Defines the setter and getter methods for the collection_singular_ids. - - def define_readers(mixin) + def self.define_readers(mixin, name) super mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 @@ -65,7 +58,7 @@ module ActiveRecord::Associations::Builder CODE end - def define_writers(mixin) + def self.define_writers(mixin, name) super mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 @@ -75,9 +68,7 @@ module ActiveRecord::Associations::Builder CODE end - private - - def wrap_scope(scope, mod) + def self.wrap_scope(scope, mod) if scope proc { |owner| instance_exec(owner, &scope).extending(mod) } else diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index 55ec3bf4f1..1c9c04b044 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -1,24 +1,121 @@ module ActiveRecord::Associations::Builder - class HasAndBelongsToMany < CollectionAssociation #:nodoc: - def macro - :has_and_belongs_to_many + class HasAndBelongsToMany # :nodoc: + class JoinTableResolver + KnownTable = Struct.new :join_table + + class KnownClass + def initialize(lhs_class, rhs_class_name) + @lhs_class = lhs_class + @rhs_class_name = rhs_class_name + @join_table = nil + end + + def join_table + @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_") + end + + private + def klass; @rhs_class_name.constantize; end + end + + def self.build(lhs_class, name, options) + if options[:join_table] + KnownTable.new options[:join_table] + else + class_name = options.fetch(:class_name) { + name.to_s.camelize.singularize + } + KnownClass.new lhs_class, class_name + end + end + end + + attr_reader :lhs_model, :association_name, :options + + def initialize(association_name, lhs_model, options) + @association_name = association_name + @lhs_model = lhs_model + @options = options + end + + def through_model + habtm = JoinTableResolver.build lhs_model, association_name, options + + join_model = Class.new(ActiveRecord::Base) { + class << self; + attr_accessor :class_resolver + attr_accessor :name + attr_accessor :table_name_resolver + attr_accessor :left_reflection + attr_accessor :right_reflection + end + + def self.table_name + table_name_resolver.join_table + end + + def self.compute_type(class_name) + class_resolver.compute_type class_name + end + + def self.add_left_association(name, options) + belongs_to name, options + self.left_reflection = reflect_on_association(name) + end + + def self.add_right_association(name, options) + rhs_name = name.to_s.singularize.to_sym + belongs_to rhs_name, options + self.right_reflection = reflect_on_association(rhs_name) + end + + } + + join_model.name = "HABTM_#{association_name.to_s.camelize}" + join_model.table_name_resolver = habtm + join_model.class_resolver = lhs_model + + join_model.add_left_association :left_side, class: lhs_model + join_model.add_right_association association_name, belongs_to_options(options) + join_model + end + + def middle_reflection(join_model) + middle_name = [lhs_model.name.downcase.pluralize, + association_name].join('_').gsub(/::/, '_').to_sym + middle_options = middle_options join_model + + HasMany.create_reflection(lhs_model, + middle_name, + nil, + middle_options) end - def valid_options - super + [:join_table, :association_foreign_key] + private + + def middle_options(join_model) + middle_options = {} + middle_options[:class] = join_model + middle_options[:source] = join_model.left_reflection.name + if options.key? :foreign_key + middle_options[:foreign_key] = options[:foreign_key] + end + middle_options end - def define_callbacks(model, reflection) - super - name = self.name - model.send(:include, Module.new { - class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def destroy_associations - association(:#{name}).delete_all - super - end - RUBY - }) + def belongs_to_options(options) + rhs_options = {} + + if options.key? :class_name + rhs_options[:foreign_key] = options[:class_name].foreign_key + rhs_options[:class_name] = options[:class_name] + end + + if options.key? :association_foreign_key + rhs_options[:foreign_key] = options[:association_foreign_key] + end + + rhs_options end end end diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index a60cb4769a..227184cd19 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -1,14 +1,14 @@ module ActiveRecord::Associations::Builder class HasMany < CollectionAssociation #:nodoc: - def macro + def self.macro :has_many end - def valid_options + def self.valid_options(options) super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache] end - def valid_dependent_options + def self.valid_dependent_options [:destroy, :delete_all, :nullify, :restrict_with_error, :restrict_with_exception] end end diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index 62d454ce55..064a3c8b51 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -1,27 +1,21 @@ module ActiveRecord::Associations::Builder class HasOne < SingularAssociation #:nodoc: - def macro + def self.macro :has_one end - def valid_options + def self.valid_options(options) valid = super + [:order, :as] valid += [:through, :source, :source_type] if options[:through] valid end - def constructable? - !options[:through] - end - - def valid_dependent_options + def self.valid_dependent_options [:destroy, :delete, :nullify, :restrict_with_error, :restrict_with_exception] end - private - - def add_before_destroy_callbacks(model, name) - super unless options[:through] + def self.add_before_destroy_callbacks(model, reflection) + super unless reflection.options[:through] end end end diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index e81cc99278..66b03c0301 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -1,23 +1,18 @@ -# This class is inherited by the has_one and belongs_to association classes +# This class is inherited by the has_one and belongs_to association classes module ActiveRecord::Associations::Builder class SingularAssociation < Association #:nodoc: - def valid_options + def self.valid_options(options) super + [:remote, :dependent, :counter_cache, :primary_key, :inverse_of] end - def constructable? - true - end - - def define_accessors(model) + def self.define_accessors(model, reflection) super - define_constructors(model.generated_association_methods) if constructable? + define_constructors(model.generated_association_methods, reflection.name) if reflection.constructable? end # Defines the (build|create)_association methods for belongs_to or has_one association - - def define_constructors(mixin) + def self.define_constructors(mixin, name) mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 def build_#{name}(*args, &block) association(:#{name}).build(*args, &block) diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index b9199f62b9..62f23f54f9 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -7,7 +7,6 @@ module ActiveRecord # collections. See the class hierarchy in AssociationProxy. # # CollectionAssociation: - # HasAndBelongsToManyAssociation => has_and_belongs_to_many # HasManyAssociation => has_many # HasManyThroughAssociation + ThroughAssociation => has_many :through # @@ -152,7 +151,7 @@ module ActiveRecord # Removes all records from the association without calling callbacks # on the associated records. It honors the `:dependent` option. However - # if the `:dependent` value is `:destroy` then in that case the default + # if the `:dependent` value is `:destroy` then in that case the `:delete_all` # deletion strategy for the association is applied. # # You can force a particular deletion strategy by passing a parameter. @@ -171,9 +170,7 @@ module ActiveRecord dependent = if dependent.present? dependent elsif options[:dependent] == :destroy - # since delete_all should not invoke callbacks so use the default deletion strategy - # for :destroy - reflection.is_a?(ActiveRecord::Reflection::ThroughReflection) ? :delete_all : :nullify + :delete_all else options[:dependent] end @@ -196,9 +193,7 @@ module ActiveRecord # Count all records using SQL. Construct options and pass them with # scope to the target class's +count+. - def count(column_name = nil, count_options = {}) - column_name, count_options = nil, column_name if column_name.is_a?(Hash) - + def count(column_name = nil) relation = scope if association_scope.distinct_value # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index ea7f768a68..2e70a07962 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -281,7 +281,7 @@ module ActiveRecord # so method calls may be chained. # # class Person < ActiveRecord::Base - # pets :has_many + # has_many :pets # end # # person.pets.size # => 0 @@ -669,8 +669,8 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] - def count(column_name = nil, options = {}) - @association.count(column_name, options) + def count(column_name = nil) + @association.count(column_name) end # Returns the size of the collection. If the collection hasn't been loaded, diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb deleted file mode 100644 index b2e6c708bf..0000000000 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ /dev/null @@ -1,56 +0,0 @@ -module ActiveRecord - # = Active Record Has And Belongs To Many Association - module Associations - class HasAndBelongsToManyAssociation < CollectionAssociation #:nodoc: - attr_reader :join_table - - def initialize(owner, reflection) - @join_table = Arel::Table.new(reflection.join_table) - super - end - - def insert_record(record, validate = true, raise = false) - if record.new_record? - if raise - record.save!(:validate => validate) - else - return unless record.save(:validate => validate) - end - end - - stmt = join_table.compile_insert( - join_table[reflection.foreign_key] => owner.id, - join_table[reflection.association_foreign_key] => record.id - ) - - owner.class.connection.insert stmt - - record - end - - private - - def count_records - load_target.size - end - - def delete_records(records, method) - relation = join_table - condition = relation[reflection.foreign_key].eq(owner.id) - - unless records == :all - condition = condition.and( - relation[reflection.association_foreign_key] - .in(records.map { |x| x.id }.compact) - ) - end - - owner.class.connection.delete(relation.where(condition).compile_delete) - end - - def invertible_for?(record) - false - end - end - end -end diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 5aa17e5fbb..c3ac0680ea 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -1,11 +1,75 @@ module ActiveRecord module Associations class JoinDependency # :nodoc: - autoload :JoinPart, 'active_record/associations/join_dependency/join_part' autoload :JoinBase, 'active_record/associations/join_dependency/join_base' autoload :JoinAssociation, 'active_record/associations/join_dependency/join_association' - attr_reader :join_parts, :reflections, :alias_tracker, :base_klass + class Aliases # :nodoc: + def initialize(tables) + @tables = tables + @alias_cache = tables.each_with_object({}) { |table,h| + h[table.node] = table.columns.each_with_object({}) { |column,i| + i[column.name] = column.alias + } + } + @name_and_alias_cache = tables.each_with_object({}) { |table,h| + h[table.node] = table.columns.map { |column| + [column.name, column.alias] + } + } + end + + def columns + @tables.flat_map { |t| t.column_aliases } + end + + # An array of [column_name, alias] pairs for the table + def column_aliases(node) + @name_and_alias_cache[node] + end + + def column_alias(node, column) + @alias_cache[node][column] + end + + class Table < Struct.new(:node, :columns) + def table + Arel::Nodes::TableAlias.new node.table, node.aliased_table_name + end + + def column_aliases + t = table + columns.map { |column| t[column.name].as Arel.sql column.alias } + end + end + Column = Struct.new(:name, :alias) + end + + attr_reader :alias_tracker, :base_klass, :join_root + + def self.make_tree(associations) + hash = {} + walk_tree associations, hash + hash + end + + def self.walk_tree(associations, hash) + case associations + when Symbol, String + hash[associations.to_sym] ||= {} + when Array + associations.each do |assoc| + walk_tree assoc, hash + end + when Hash + associations.each do |k,v| + cache = hash[k] ||= {} + walk_tree v, cache + end + else + raise ConfigurationError, associations.inspect + end + end # base is the base class on which operation is taking place. # associations is the list of associations which are joined using hash, symbol or array. @@ -29,211 +93,181 @@ module ActiveRecord # joins #=> [] # def initialize(base, associations, joins) - @base_klass = base - @table_joins = joins - @join_parts = [JoinBase.new(base)] - @associations = {} - @reflections = [] @alias_tracker = AliasTracker.new(base.connection, joins) @alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1 - build(associations) + tree = self.class.make_tree associations + @join_root = JoinBase.new base, build(tree, base) + @join_root.children.each { |child| construct_tables! @join_root, child } end - def graft(*associations) - associations.each do |association| - join_associations.detect {|a| association == a} || - build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type) - end - self + def reflections + join_root.drop(1).map!(&:reflection) end - def join_associations - join_parts.drop 1 - end + def join_constraints(outer_joins) + joins = join_root.children.flat_map { |child| + make_inner_joins join_root, child + } - def join_base - join_parts.first + joins.concat outer_joins.flat_map { |oj| + if join_root.match? oj.join_root + walk join_root, oj.join_root + else + oj.join_root.children.flat_map { |child| + make_outer_joins join_root, child + } + end + } end - def join_relation(relation) - join_associations.inject(relation) do |rel,association| - association.join_relation(rel) - end + def aliases + Aliases.new join_root.each_with_index.map { |join_part,i| + columns = join_part.column_names.each_with_index.map { |column_name,j| + Aliases::Column.new column_name, "t#{i}_r#{j}" + } + Aliases::Table.new(join_part, columns) + } end - def columns - join_parts.collect { |join_part| - table = join_part.aliased_table - join_part.column_names_with_alias.collect{ |column_name, aliased_name| - table[column_name].as Arel.sql(aliased_name) + def instantiate(result_set, aliases) + primary_key = aliases.column_alias(join_root, join_root.primary_key) + type_caster = result_set.column_type primary_key + + seen = Hash.new { |h,parent_klass| + h[parent_klass] = Hash.new { |i,parent_id| + i[parent_id] = Hash.new { |j,child_klass| j[child_klass] = {} } } - }.flatten - end + } - def instantiate(rows) - primary_key = join_base.aliased_primary_key - parents = {} + model_cache = Hash.new { |h,klass| h[klass] = {} } + parents = model_cache[join_root] + column_aliases = aliases.column_aliases join_root - records = rows.map { |model| - primary_id = model[primary_key] - parent = parents[primary_id] ||= join_base.instantiate(model) - construct(parent, @associations, join_associations, model) - parent - }.uniq + result_set.each { |row_hash| + primary_id = type_caster.type_cast 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) + } - remove_duplicate_results!(base_klass, records, @associations) - records + parents.values end - protected - - def remove_duplicate_results!(base, records, associations) - case associations - when Symbol, String - reflection = base.reflections[associations] - remove_uniq_by_reflection(reflection, records) - when Array - associations.each do |association| - remove_duplicate_results!(base, records, association) - end - when Hash - associations.each_key do |name| - reflection = base.reflections[name] - remove_uniq_by_reflection(reflection, records) - - parent_records = [] - records.each do |record| - if descendant = record.send(reflection.name) - if reflection.collection? - parent_records.concat descendant.target.uniq - else - parent_records << descendant - end - end - end + private - remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty? - end - end + def make_constraints(parent, child, tables, join_type) + chain = child.reflection.chain + foreign_table = parent.table + foreign_klass = parent.base_klass + child.join_constraints(foreign_table, foreign_klass, child, join_type, tables, child.reflection.scope_chain, chain) end - def cache_joined_association(association) - associations = [] - parent = association.parent - while parent != join_base - associations.unshift(parent.reflection.name) - parent = parent.parent - end - ref = @associations - associations.each do |key| - ref = ref[key] - end - ref[association.reflection.name] ||= {} - end + def make_outer_joins(parent, child) + tables = table_aliases_for(parent, child) + join_type = Arel::OuterJoin + joins = make_constraints parent, child, tables, join_type - def build(associations, parent = join_parts.last, join_type = Arel::InnerJoin) - case associations - when Symbol, String - reflection = parent.reflections[associations.intern] or - raise ConfigurationError, "Association named '#{ associations }' was not found on #{ parent.base_klass.name }; perhaps you misspelled it?" - unless join_association = find_join_association(reflection, parent) - @reflections << reflection - join_association = build_join_association(reflection, parent) - join_association.join_type = join_type - @join_parts << join_association - cache_joined_association(join_association) - end - join_association - when Array - associations.each do |association| - build(association, parent, join_type) - end - when Hash - associations.keys.sort_by { |a| a.to_s }.each do |name| - join_association = build(name, parent, join_type) - build(associations[name], join_association, join_type) - end - else - raise ConfigurationError, associations.inspect - end + joins.concat child.children.flat_map { |c| make_outer_joins(child, c) } end - def find_join_association(name_or_reflection, parent) - if String === name_or_reflection - name_or_reflection = name_or_reflection.to_sym - end + def make_inner_joins(parent, child) + tables = child.tables + join_type = Arel::InnerJoin + joins = make_constraints parent, child, tables, join_type - join_associations.detect { |j| - j.reflection == name_or_reflection && j.parent == parent + joins.concat child.children.flat_map { |c| make_inner_joins(child, c) } + end + + def table_aliases_for(parent, node) + node.reflection.chain.map { |reflection| + alias_tracker.aliased_table_for( + reflection.table_name, + table_alias_for(reflection, parent, reflection != node.reflection) + ) } end - def remove_uniq_by_reflection(reflection, records) - if reflection && reflection.collection? - records.each { |record| record.send(reflection.name).target.uniq! } - end + def construct_tables!(parent, node) + node.tables = table_aliases_for(parent, node) + node.children.each { |child| construct_tables! node, child } end - def build_join_association(reflection, parent) - JoinAssociation.new(reflection, self, parent) + def table_alias_for(reflection, parent, join) + name = "#{reflection.plural_name}_#{parent.table_name}" + name << "_join" if join + name end - def construct(parent, associations, join_parts, row) - case associations - when Symbol, String - name = associations.to_s + def walk(left, right) + intersection, missing = right.children.map { |node1| + [left.children.find { |node2| node1.match? node2 }, node1] + }.partition(&:first) - join_part = join_parts.detect { |j| - j.reflection.name.to_s == name && - j.parent_table_name == parent.class.table_name } + ojs = missing.flat_map { |_,n| make_outer_joins left, n } + intersection.flat_map { |l,r| walk l, r }.concat ojs + end - raise(ConfigurationError, "No such association") unless join_part + def find_reflection(klass, name) + klass.reflect_on_association(name) or + raise ConfigurationError, "Association named '#{ name }' was not found on #{ klass.name }; perhaps you misspelled it?" + end - join_parts.delete(join_part) - construct_association(parent, join_part, row) - when Array - associations.each do |association| - construct(parent, association, join_parts, row) - end - when Hash - associations.sort_by { |k,_| k.to_s }.each do |association_name, assoc| - association = construct(parent, association_name, join_parts, row) - construct(association, assoc, join_parts, row) if association + def build(associations, base_klass) + associations.map do |name, right| + reflection = find_reflection base_klass, name + reflection.check_validity! + + if reflection.options[:polymorphic] + raise EagerLoadPolymorphicError.new(reflection) end - else - raise ConfigurationError, associations.inspect + + JoinAssociation.new reflection, build(right, reflection.klass) end end - def construct_association(record, join_part, row) - return if record.id.to_s != join_part.parent.record_id(row).to_s + def construct(ar_parent, parent, row, rs, seen, model_cache, aliases) + primary_id = ar_parent.id - macro = join_part.reflection.macro - if macro == :has_one - return record.association(join_part.reflection.name).target if record.association_cache.key?(join_part.reflection.name) - association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil? - set_target_and_inverse(join_part, association, record) - else - association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil? - case macro - when :has_many, :has_and_belongs_to_many - other = record.association(join_part.reflection.name) + parent.children.each do |node| + if node.reflection.collection? + other = ar_parent.association(node.reflection.name) other.loaded! - other.target.push(association) if association - other.set_inverse_instance(association) - when :belongs_to - set_target_and_inverse(join_part, association, record) else - raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}" + if ar_parent.association_cache.key?(node.reflection.name) + model = ar_parent.association(node.reflection.name).target + construct(model, node, row, rs, seen, model_cache, aliases) + next + end + end + + key = aliases.column_alias(node, node.primary_key) + id = row[key] + next if id.nil? + + model = seen[parent.base_klass][primary_id][node.base_klass][id] + + if model + construct(model, node, row, rs, seen, model_cache, aliases) + else + model = construct_model(ar_parent, node, row, model_cache, id, aliases) + seen[parent.base_klass][primary_id][node.base_klass][id] = model + construct(model, node, row, rs, seen, model_cache, aliases) end end - association end - def set_target_and_inverse(join_part, association, record) - other = record.association(join_part.reflection.name) - other.target = association - other.set_inverse_instance(association) + def construct_model(record, node, row, model_cache, id, aliases) + model = model_cache[node][id] ||= node.instantiate(row, + aliases.column_aliases(node)) + other = record.association(node.reflection.name) + + if node.reflection.collection? + other.target.push(model) + else + other.target = model + end + + other.set_inverse_instance(model) + model end end end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index 58fc00d811..191d430636 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -1,77 +1,31 @@ +require 'active_record/associations/join_dependency/join_part' + module ActiveRecord module Associations class JoinDependency # :nodoc: class JoinAssociation < JoinPart # :nodoc: - include JoinHelper - # The reflection of the association represented attr_reader :reflection - # The JoinDependency object which this JoinAssociation exists within. This is mainly - # relevant for generating aliases which do not conflict with other joins which are - # part of the query. - attr_reader :join_dependency - - # A JoinBase instance representing the active record we are joining onto. - # (So in Author.has_many :posts, the Author would be that base record.) - attr_reader :parent - - # What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin - attr_accessor :join_type - - # These implement abstract methods from the superclass - attr_reader :aliased_prefix - - attr_reader :tables + attr_accessor :tables - delegate :options, :through_reflection, :source_reflection, :chain, :to => :reflection - delegate :alias_tracker, :to => :join_dependency - - def initialize(reflection, join_dependency, parent = nil) - reflection.check_validity! - - if reflection.options[:polymorphic] - raise EagerLoadPolymorphicError.new(reflection) - end - - super(reflection.klass) + def initialize(reflection, children) + super(reflection.klass, children) @reflection = reflection - @join_dependency = join_dependency - @parent = parent - @join_type = Arel::InnerJoin - @aliased_prefix = "t#{ join_dependency.join_parts.size }" - @tables = construct_tables.reverse - end - - def parent_table_name; parent.table_name; end - alias :alias_suffix :parent_table_name - - def ==(other) - other.class == self.class && - other.reflection == reflection && - other.parent == parent + @tables = nil end - def find_parent_in(other_join_dependency) - other_join_dependency.join_parts.detect do |join_part| - case parent - when JoinBase - parent.base_klass == join_part.base_klass - else - parent == join_part - end - end + def match?(other) + return true if self == other + super && reflection == other.reflection end - def join_constraints + def join_constraints(foreign_table, foreign_klass, node, join_type, tables, scope_chain, chain) joins = [] - tables = @tables.dup - - foreign_table = parent.table - foreign_klass = parent.base_klass + tables = tables.reverse - scope_chain_iter = reflection.scope_chain.reverse_each + scope_chain_iter = scope_chain.reverse_each # The chain starts with the target table, but we want to end with it here (makes # more sense in this context), so we reverse @@ -83,17 +37,6 @@ module ActiveRecord when :belongs_to key = reflection.association_primary_key foreign_key = reflection.foreign_key - when :has_and_belongs_to_many - # Join the join table first... - joins << join( - table, - table[reflection.foreign_key]. - eq(foreign_table[reflection.active_record_primary_key])) - - foreign_table, table = table, tables.shift - - key = reflection.association_primary_key - foreign_key = reflection.association_foreign_key else key = reflection.foreign_key foreign_key = reflection.active_record_primary_key @@ -105,7 +48,7 @@ module ActiveRecord if item.is_a?(Relation) item else - ActiveRecord::Relation.create(klass, table).instance_exec(self, &item) + ActiveRecord::Relation.create(klass, table).instance_exec(node, &item) end end @@ -125,7 +68,7 @@ module ActiveRecord constraint = constraint.and rel.arel.constraints end - joins << join(table, constraint) + joins << table.create_join(table, table.create_on(constraint), join_type) # The current table in this iteration becomes the foreign table in the next foreign_table, foreign_klass = table, klass @@ -162,13 +105,8 @@ module ActiveRecord constraint end - def join_relation(joining_relation) - self.join_type = Arel::OuterJoin - joining_relation.joins(self) - end - def table - tables.last + tables.first end def aliased_table_name diff --git a/activerecord/lib/active_record/associations/join_dependency/join_base.rb b/activerecord/lib/active_record/associations/join_dependency/join_base.rb index a7dacdbfd6..3a26c25737 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_base.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_base.rb @@ -1,18 +1,16 @@ +require 'active_record/associations/join_dependency/join_part' + module ActiveRecord module Associations class JoinDependency # :nodoc: class JoinBase < JoinPart # :nodoc: - def ==(other) - other.class == self.class && - other.base_klass == base_klass - end - - def aliased_prefix - "t0" + def match?(other) + return true if self == other + super && base_klass == other.base_klass end def table - Arel::Table.new(table_name, arel_engine) + base_klass.arel_table end def aliased_table_name diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb index 8024105472..91e1c6a9d7 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb @@ -8,34 +8,36 @@ module ActiveRecord # operations (for example a has_and_belongs_to_many JoinAssociation would result in # two; one for the join table and one for the target table). class JoinPart # :nodoc: + include Enumerable + # The Active Record class which this join part is associated 'about'; for a JoinBase # this is the actual base model, for a JoinAssociation this is the target model of the # association. - attr_reader :base_klass + attr_reader :base_klass, :children - delegate :table_name, :column_names, :primary_key, :reflections, :arel_engine, :to => :base_klass + delegate :table_name, :column_names, :primary_key, :to => :base_klass - def initialize(base_klass) + def initialize(base_klass, children) @base_klass = base_klass - @cached_record = {} @column_names_with_alias = nil + @children = children end - def aliased_table - Arel::Nodes::TableAlias.new table, aliased_table_name + def name + reflection.name end - def ==(other) - raise NotImplementedError + def match?(other) + self.class == other.class end - # An Arel::Table for the active_record - def table - raise NotImplementedError + def each(&block) + yield self + children.each { |child| child.each(&block) } end - # The prefix to be used when aliasing columns in the active_record's table - def aliased_prefix + # An Arel::Table for the active_record + def table raise NotImplementedError end @@ -44,24 +46,7 @@ module ActiveRecord raise NotImplementedError end - # The alias for the primary key of the active_record's table - def aliased_primary_key - "#{aliased_prefix}_r0" - end - - # An array of [column_name, alias] pairs for the table - def column_names_with_alias - unless @column_names_with_alias - @column_names_with_alias = [] - - ([primary_key] + (column_names - [primary_key])).compact.each_with_index do |column_name, i| - @column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"] - end - end - @column_names_with_alias - end - - def extract_record(row) + def extract_record(row, column_names_with_alias) # This code is performance critical as it is called per row. # see: https://github.com/rails/rails/pull/12185 hash = {} @@ -78,12 +63,8 @@ module ActiveRecord hash end - def record_id(row) - row[aliased_primary_key] - end - - def instantiate(row) - @cached_record[record_id(row)] ||= base_klass.instantiate(extract_record(row)) + def instantiate(row, aliases) + base_klass.instantiate(extract_record(row, aliases)) end end end diff --git a/activerecord/lib/active_record/associations/join_helper.rb b/activerecord/lib/active_record/associations/join_helper.rb index 27b70edf1a..f345d16841 100644 --- a/activerecord/lib/active_record/associations/join_helper.rb +++ b/activerecord/lib/active_record/associations/join_helper.rb @@ -10,21 +10,12 @@ module ActiveRecord private def construct_tables - tables = [] - chain.each do |reflection| - tables << alias_tracker.aliased_table_for( + chain.map do |reflection| + alias_tracker.aliased_table_for( table_name_for(reflection), table_alias_for(reflection, reflection != self.reflection) ) - - if reflection.source_macro == :has_and_belongs_to_many - tables << alias_tracker.aliased_table_for( - reflection.source_reflection.join_table, - table_alias_for(reflection, true) - ) - end end - tables end def table_name_for(reflection) diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 127d0e2642..2393667ac8 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -42,7 +42,6 @@ module ActiveRecord autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through' autoload :HasOne, 'active_record/associations/preloader/has_one' autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through' - autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many' autoload :BelongsTo, 'active_record/associations/preloader/belongs_to' end @@ -154,13 +153,13 @@ module ActiveRecord records.group_by do |record| reflection = record.class.reflect_on_association(association) - reflection || raise_config_error(association) + reflection || raise_config_error(record, association) end end - def raise_config_error(association) + def raise_config_error(record, association) raise ActiveRecord::ConfigurationError, - "Association named '#{association}' was not found; " \ + "Association named '#{association}' was not found on #{record.class.name}; " \ "perhaps you misspelled it?" end @@ -205,8 +204,6 @@ module ActiveRecord reflection.options[:through] ? HasManyThrough : HasMany when :has_one reflection.options[:through] ? HasOneThrough : HasOne - when :has_and_belongs_to_many - HasAndBelongsToMany when :belongs_to BelongsTo end diff --git a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb deleted file mode 100644 index b62ca6f681..0000000000 --- a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +++ /dev/null @@ -1,71 +0,0 @@ -module ActiveRecord - module Associations - class Preloader - class HasAndBelongsToMany < CollectionAssociation #:nodoc: - attr_reader :join_table - - def initialize(klass, records, reflection, preload_options) - super - @join_table = Arel::Table.new(reflection.join_table).alias('t0') - end - - # Unlike the other associations, we want to get a raw array of rows so that we can - # access the aliased column on the join table - def records_for(ids) - scope = query_scope ids - klass.connection.select_all(scope.arel, 'SQL', scope.bind_values) - end - - def owner_key_name - reflection.active_record_primary_key - end - - def association_key_name - 'ar_association_key_name' - end - - def association_key - join_table[reflection.foreign_key] - end - - private - - # Once we have used the join table column (in super), we manually instantiate the - # actual records, ensuring that we don't create more than one instances of the same - # record - def load_slices(slices) - identity_map = {} - caster = nil - name = association_key_name - - records_to_keys = slices.flat_map { |slice| - records = records_for(slice) - caster ||= records.column_types.fetch(name, records.identity_type) - records.map! { |row| - record = identity_map[row[klass.primary_key]] ||= klass.instantiate(row) - [record, caster.type_cast(row[name])] - } - } - @preloaded_records = records_to_keys.map(&:first) - - records_to_keys - end - - def build_scope - super.joins(join).select(join_select) - end - - def join_select - association_key.as(Arel.sql(association_key_name)) - end - - def join - condition = table[reflection.association_primary_key].eq( - join_table[reflection.association_foreign_key]) - - table.create_join(join_table, table.create_on(condition)) - end - end - end - end -end diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index ea21836c65..2a8530af62 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -15,7 +15,7 @@ module ActiveRecord through_reflection.name, through_scope) - through_records = owners.map do |owner, h| + through_records = owners.map do |owner| association = owner.association through_reflection.name [owner, Array(association.reader)] @@ -29,29 +29,30 @@ module ActiveRecord source_reflection.name, reflection_scope) + @preloaded_records = preloaders.flat_map(&:preloaded_records) + middle_to_pl = preloaders.each_with_object({}) do |pl,h| pl.owners.each { |middle| h[middle] = pl } end + record_offset = {} + @preloaded_records.each_with_index do |record,i| + record_offset[record] = i + end + through_records.each_with_object({}) { |(lhs,center),records_by_owner| pl_to_middle = center.group_by { |record| middle_to_pl[record] } records_by_owner[lhs] = pl_to_middle.flat_map do |pl, middles| rhs_records = middles.flat_map { |r| - r.send(source_reflection.name) + association = r.association source_reflection.name + + association.reader }.compact - loaded_records = pl.preloaded_records - i = 0 - record_index = loaded_records.each_with_object({}) { |r,indexes| - indexes[r] = i - i += 1 - } - records = rhs_records.sort_by { |rhs| record_index[rhs] } - @preloaded_records.concat rhs_records - records + rhs_records.sort_by { |rhs| record_offset[rhs] } end } end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 208da2cb77..3924eec872 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/enumerable' require 'mutex_m' +require 'thread_safe' module ActiveRecord # = Active Record Attribute Methods @@ -29,23 +30,18 @@ module ActiveRecord } class AttributeMethodCache - include Mutex_m - def initialize - super @module = Module.new - @method_cache = {} + @method_cache = ThreadSafe::Cache.new end def [](name) - synchronize do - @method_cache.fetch(name) { - safe_name = name.unpack('h*').first - temp_method = "__temp__#{safe_name}" - ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name - @module.module_eval method_body(temp_method, safe_name), __FILE__, __LINE__ - @method_cache[name] = @module.instance_method temp_method - } + @method_cache.compute_if_absent(name) do + safe_name = name.unpack('h*').first + temp_method = "__temp__#{safe_name}" + ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name + @module.module_eval method_body(temp_method, safe_name), __FILE__, __LINE__ + @module.instance_method temp_method end end @@ -165,12 +161,9 @@ module ActiveRecord # If we haven't generated any methods yet, generate them, then # see if we've created the method we're looking for. def method_missing(method, *args, &block) # :nodoc: - if self.class.define_attribute_methods - if respond_to_without_attributes?(method) - send(method, *args, &block) - else - super - end + self.class.define_attribute_methods + if respond_to_without_attributes?(method) + send(method, *args, &block) else super end @@ -252,9 +245,10 @@ module ActiveRecord # Returns an <tt>#inspect</tt>-like string for the value of the # attribute +attr_name+. String attributes are truncated upto 50 - # characters, and Date and Time attributes are returned in the - # <tt>:db</tt> format. Other attributes return the value of - # <tt>#inspect</tt> without modification. + # characters, Date and Time attributes are returned in the + # <tt>:db</tt> format, Array attributes are truncated upto 10 values. + # Other attributes return the value of <tt>#inspect</tt> without + # modification. # # person = Person.create!(name: 'David Heinemeier Hansson ' * 3) # @@ -263,6 +257,9 @@ module ActiveRecord # # person.attribute_for_inspect(:created_at) # # => "\"2012-10-22 00:15:07\"" + # + # person.attribute_for_inspect(:tag_ids) + # # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]" def attribute_for_inspect(attr_name) value = read_attribute(attr_name) @@ -270,6 +267,9 @@ module ActiveRecord "#{value[0, 50]}...".inspect elsif value.is_a?(Date) || value.is_a?(Time) %("#{value.to_s(:db)}") + elsif value.is_a?(Array) && value.size > 10 + inspected = value.first(10).inspect + %(#{inspected[0...-1]}, ...]) else value.inspect end diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 1287de0d0d..5701804168 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -66,6 +66,10 @@ module ActiveRecord def type @column.type end + + def accessor + ActiveRecord::Store::IndifferentHashAccessor + end end class Attribute < Struct.new(:coder, :value, :state) # :nodoc: diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index b30d1eb0a6..e9622ca0c1 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -143,71 +143,71 @@ module ActiveRecord module ClassMethods private - def define_non_cyclic_method(name, reflection, &block) - define_method(name) do |*args| - result = true; @_already_called ||= {} - # Loop prevention for validation of associations - unless @_already_called[[name, reflection.name]] - begin - @_already_called[[name, reflection.name]]=true - result = instance_eval(&block) - ensure - @_already_called[[name, reflection.name]]=false + def define_non_cyclic_method(name, &block) + define_method(name) do |*args| + result = true; @_already_called ||= {} + # Loop prevention for validation of associations + unless @_already_called[name] + begin + @_already_called[name]=true + result = instance_eval(&block) + ensure + @_already_called[name]=false + end end - end - result + result + end end - end - # Adds validation and save callbacks for the association as specified by - # the +reflection+. - # - # For performance reasons, we don't check whether to validate at runtime. - # However the validation and callback methods are lazy and those methods - # get created when they are invoked for the very first time. However, - # this can change, for instance, when using nested attributes, which is - # called _after_ the association has been defined. Since we don't want - # the callbacks to get defined multiple times, there are guards that - # check if the save or validation methods have already been defined - # before actually defining them. - def add_autosave_association_callbacks(reflection) - save_method = :"autosave_associated_records_for_#{reflection.name}" - validation_method = :"validate_associated_records_for_#{reflection.name}" - collection = reflection.collection? - - unless method_defined?(save_method) - if collection - before_save :before_save_collection_association - - define_non_cyclic_method(save_method, reflection) { save_collection_association(reflection) } - # 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 - define_method(save_method) { save_has_one_association(reflection) } - # Configures two callbacks instead of a single after_save so that - # the model may rely on their execution order relative to its - # own callbacks. - # - # For example, given that after_creates run before after_saves, if - # we configured instead an after_save there would be no way to fire - # a custom after_create callback after the child association gets - # created. - after_create save_method - after_update save_method - else - define_non_cyclic_method(save_method, reflection) { save_belongs_to_association(reflection) } - before_save save_method + # Adds validation and save callbacks for the association as specified by + # the +reflection+. + # + # For performance reasons, we don't check whether to validate at runtime. + # However the validation and callback methods are lazy and those methods + # get created when they are invoked for the very first time. However, + # this can change, for instance, when using nested attributes, which is + # called _after_ the association has been defined. Since we don't want + # the callbacks to get defined multiple times, there are guards that + # check if the save or validation methods have already been defined + # before actually defining them. + def add_autosave_association_callbacks(reflection) + save_method = :"autosave_associated_records_for_#{reflection.name}" + validation_method = :"validate_associated_records_for_#{reflection.name}" + collection = reflection.collection? + + unless method_defined?(save_method) + if collection + before_save :before_save_collection_association + + define_non_cyclic_method(save_method) { save_collection_association(reflection) } + # 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 + define_method(save_method) { save_has_one_association(reflection) } + # Configures two callbacks instead of a single after_save so that + # the model may rely on their execution order relative to its + # own callbacks. + # + # For example, given that after_creates run before after_saves, if + # we configured instead an after_save there would be no way to fire + # a custom after_create callback after the child association gets + # created. + after_create save_method + after_update save_method + else + define_non_cyclic_method(save_method) { save_belongs_to_association(reflection) } + before_save save_method + end end - end - if reflection.validate? && !method_defined?(validation_method) - method = (collection ? :validate_collection_association : :validate_single_association) - define_non_cyclic_method(validation_method, reflection) { send(method, reflection) } - validate validation_method + if reflection.validate? && !method_defined?(validation_method) + method = (collection ? :validate_collection_association : :validate_single_association) + define_non_cyclic_method(validation_method) { send(method, reflection) } + validate validation_method + end end - end end # Reloads the attributes of the object as usual and clears <tt>marked_for_destruction</tt> flag. @@ -254,173 +254,179 @@ module ActiveRecord private - # Returns the record for an association collection that should be validated - # or saved. If +autosave+ is +false+ only new records will be returned, - # unless the parent is/was a new record itself. - def associated_records_to_validate_or_save(association, new_record, autosave) - if new_record - association && association.target - elsif autosave - association.target.find_all { |record| record.changed_for_autosave? } - else - association.target.find_all { |record| record.new_record? } + # Returns the record for an association collection that should be validated + # or saved. If +autosave+ is +false+ only new records will be returned, + # unless the parent is/was a new record itself. + def associated_records_to_validate_or_save(association, new_record, autosave) + if new_record + association && association.target + elsif autosave + association.target.find_all { |record| record.changed_for_autosave? } + else + association.target.find_all { |record| record.new_record? } + end end - end - # go through nested autosave associations that are loaded in memory (without loading - # any new ones), and return true if is changed for autosave - def nested_records_changed_for_autosave? - self.class.reflect_on_all_autosave_associations.any? do |reflection| - association = association_instance_get(reflection.name) - association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? } + # go through nested autosave associations that are loaded in memory (without loading + # any new ones), and return true if is changed for autosave + def nested_records_changed_for_autosave? + self.class.reflect_on_all_autosave_associations.any? do |reflection| + association = association_instance_get(reflection.name) + association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? } + end end - end - # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is - # turned on for the association. - def validate_single_association(reflection) - association = association_instance_get(reflection.name) - record = association && association.reader - association_valid?(reflection, record) if record - end + # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is + # turned on for the association. + def validate_single_association(reflection) + association = association_instance_get(reflection.name) + record = association && association.reader + association_valid?(reflection, record) if record + end - # Validate the associated records if <tt>:validate</tt> or - # <tt>:autosave</tt> is turned on for the association specified by - # +reflection+. - def validate_collection_association(reflection) - if association = association_instance_get(reflection.name) - if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave]) - records.each { |record| association_valid?(reflection, record) } + # Validate the associated records if <tt>:validate</tt> or + # <tt>:autosave</tt> is turned on for the association specified by + # +reflection+. + def validate_collection_association(reflection) + if association = association_instance_get(reflection.name) + if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave]) + records.each { |record| association_valid?(reflection, record) } + end end end - end - # Returns whether or not the association is valid and applies any errors to - # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt> - # enabled records if they're marked_for_destruction? or destroyed. - def association_valid?(reflection, record) - return true if record.destroyed? || record.marked_for_destruction? - - unless valid = record.valid? - if reflection.options[:autosave] - record.errors.each do |attribute, message| - attribute = "#{reflection.name}.#{attribute}" - errors[attribute] << message - errors[attribute].uniq! + # Returns whether or not the association is valid and applies any errors to + # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt> + # enabled records if they're marked_for_destruction? or destroyed. + def association_valid?(reflection, record) + return true if record.destroyed? || record.marked_for_destruction? + + unless valid = record.valid? + if reflection.options[:autosave] + record.errors.each do |attribute, message| + attribute = "#{reflection.name}.#{attribute}" + errors[attribute] << message + errors[attribute].uniq! + end + else + errors.add(reflection.name) end - else - errors.add(reflection.name) end + valid end - valid - end - # Is used as a before_save callback to check while saving a collection - # association whether or not the parent was a new record before saving. - def before_save_collection_association - @new_record_before_save = new_record? - true - end + # Is used as a before_save callback to check while saving a collection + # association whether or not the parent was a new record before saving. + def before_save_collection_association + @new_record_before_save = new_record? + true + end - # Saves any new associated records, or all loaded autosave associations if - # <tt>:autosave</tt> is enabled on the association. - # - # In addition, it destroys all children that were marked for destruction - # with mark_for_destruction. - # - # This all happens inside a transaction, _if_ the Transactions module is included into - # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. - def save_collection_association(reflection) - if association = association_instance_get(reflection.name) - autosave = reflection.options[:autosave] - - if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave) - - if autosave - records_to_destroy = records.select(&:marked_for_destruction?) - records_to_destroy.each { |record| association.destroy(record) } - records -= records_to_destroy - end + # Saves any new associated records, or all loaded autosave associations if + # <tt>:autosave</tt> is enabled on the association. + # + # In addition, it destroys all children that were marked for destruction + # with mark_for_destruction. + # + # This all happens inside a transaction, _if_ the Transactions module is included into + # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. + def save_collection_association(reflection) + if association = association_instance_get(reflection.name) + autosave = reflection.options[:autosave] + + if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave) + + if autosave + records_to_destroy = records.select(&:marked_for_destruction?) + records_to_destroy.each { |record| association.destroy(record) } + records -= records_to_destroy + end - records.each do |record| - next if record.destroyed? + records.each do |record| + next if record.destroyed? - saved = true + saved = true - if autosave != false && (@new_record_before_save || record.new_record?) - if autosave - saved = association.insert_record(record, false) - else - association.insert_record(record) unless reflection.nested? + if autosave != false && (@new_record_before_save || record.new_record?) + if autosave + saved = association.insert_record(record, false) + else + association.insert_record(record) unless reflection.nested? + end + elsif autosave + saved = record.save(:validate => false) end - elsif autosave - saved = record.save(:validate => false) - end - raise ActiveRecord::Rollback unless saved + raise ActiveRecord::Rollback unless saved + end end - end - # reconstruct the scope now that we know the owner's id - association.reset_scope if association.respond_to?(:reset_scope) + # reconstruct the scope now that we know the owner's id + association.reset_scope if association.respond_to?(:reset_scope) + end end - end - # Saves the associated record if it's new or <tt>:autosave</tt> is enabled - # on the association. - # - # In addition, it will destroy the association if it was marked for - # destruction with mark_for_destruction. - # - # This all happens inside a transaction, _if_ the Transactions module is included into - # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. - def save_has_one_association(reflection) - association = association_instance_get(reflection.name) - record = association && association.load_target - if record && !record.destroyed? - autosave = reflection.options[:autosave] - - if autosave && record.marked_for_destruction? - record.destroy - else - key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id - if autosave != false && (new_record? || record.new_record? || record[reflection.foreign_key] != key || autosave) - unless reflection.through_reflection - record[reflection.foreign_key] = key - end + # Saves the associated record if it's new or <tt>:autosave</tt> is enabled + # on the association. + # + # In addition, it will destroy the association if it was marked for + # destruction with mark_for_destruction. + # + # This all happens inside a transaction, _if_ the Transactions module is included into + # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. + def save_has_one_association(reflection) + association = association_instance_get(reflection.name) + record = association && association.load_target + if record && !record.destroyed? + autosave = reflection.options[:autosave] - saved = record.save(:validate => !autosave) - raise ActiveRecord::Rollback if !saved && autosave - saved + if autosave && record.marked_for_destruction? + record.destroy + else + key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id + if autosave != false && (autosave || new_record? || record_changed?(reflection, record, key)) + + unless reflection.through_reflection + record[reflection.foreign_key] = key + end + + saved = record.save(:validate => !autosave) + raise ActiveRecord::Rollback if !saved && autosave + saved + end end end end - end - # Saves the associated record if it's new or <tt>:autosave</tt> is enabled. - # - # In addition, it will destroy the association if it was marked for destruction. - def save_belongs_to_association(reflection) - association = association_instance_get(reflection.name) - record = association && association.load_target - if record && !record.destroyed? - autosave = reflection.options[:autosave] - - if autosave && record.marked_for_destruction? - self[reflection.foreign_key] = nil - record.destroy - elsif autosave != false - saved = record.save(:validate => !autosave) if record.new_record? || (autosave && record.changed_for_autosave?) - - if association.updated? - association_id = record.send(reflection.options[:primary_key] || :id) - self[reflection.foreign_key] = association_id - association.loaded! - end + # If the record is new or it has changed, returns true. + def record_changed?(reflection, record, key) + record.new_record? || record[reflection.foreign_key] != key || record.attribute_changed?(reflection.foreign_key) + end - saved if autosave + # Saves the associated record if it's new or <tt>:autosave</tt> is enabled. + # + # In addition, it will destroy the association if it was marked for destruction. + def save_belongs_to_association(reflection) + association = association_instance_get(reflection.name) + record = association && association.load_target + if record && !record.destroyed? + autosave = reflection.options[:autosave] + + if autosave && record.marked_for_destruction? + self[reflection.foreign_key] = nil + record.destroy + elsif autosave != false + saved = record.save(:validate => !autosave) if record.new_record? || (autosave && record.changed_for_autosave?) + + if association.updated? + association_id = record.send(reflection.options[:primary_key] || :id) + self[reflection.foreign_key] = association_id + association.loaded! + end + + saved if autosave + end end end - end end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 04e3dd49e7..69a9eabefb 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -291,6 +291,7 @@ module ActiveRecord #:nodoc: extend Translation extend DynamicMatchers extend Explain + extend Enum extend Delegation::DelegateCache include Persistence diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index e4c484d64b..128a9377c1 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -23,11 +23,14 @@ module ActiveRecord # Check out <tt>ActiveRecord::Transactions</tt> for more details about <tt>after_commit</tt> and # <tt>after_rollback</tt>. # + # Additionally, an <tt>after_touch</tt> callback is triggered whenever an + # object is touched. + # # Lastly an <tt>after_find</tt> and <tt>after_initialize</tt> callback is triggered for each object that # is found and instantiated by a finder, with <tt>after_initialize</tt> being triggered after new objects # are instantiated as well. # - # That's a total of twelve callbacks, which gives you immense power to react and prepare for each state in the + # There are nineteen callbacks in total, which give you immense power to react and prepare for each state in the # Active Record life cycle. The sequence for calling <tt>Base#save</tt> for an existing record is similar, # except that each <tt>_create</tt> callback is replaced by the corresponding <tt>_update</tt> callback. # diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index e1f29ea03a..06a2ddb8b7 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -286,6 +286,10 @@ module ActiveRecord # Inserts the given fixture into the table. Overridden in adapters that require # something beyond a simple insert (eg. Oracle). def insert_fixture(fixture, table_name) + execute fixture_sql(fixture, table_name), 'Fixture Insert' + end + + def fixture_sql(fixture, table_name) columns = schema_cache.columns_hash(table_name) key_list = [] @@ -294,7 +298,7 @@ module ActiveRecord quote(value, columns[name]) end - execute "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})", 'Fixture Insert' + "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})" end def empty_insert_statement_value diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index 7aae297cdc..8399232d73 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -9,10 +9,10 @@ module ActiveRecord def dirties_query_cache(base, *method_names) method_names.each do |method_name| base.class_eval <<-end_code, __FILE__, __LINE__ + 1 - def #{method_name}(*) # def update_with_query_dirty(*) - clear_query_cache if @query_cache_enabled # clear_query_cache if @query_cache_enabled - super # super - end # end + def #{method_name}(*) + clear_query_cache if @query_cache_enabled + super + end end_code end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb new file mode 100644 index 0000000000..25c17ce971 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb @@ -0,0 +1,21 @@ +module ActiveRecord + module ConnectionAdapters + module Savepoints #:nodoc: + def supports_savepoints? + true + end + + def create_savepoint(name = current_savepoint_name) + execute("SAVEPOINT #{name}") + end + + def rollback_to_savepoint(name = current_savepoint_name) + execute("ROLLBACK TO SAVEPOINT #{name}") + end + + def release_savepoint(name = current_savepoint_name) + execute("RELEASE SAVEPOINT #{name}") + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index dde45b0ef3..cbe563676b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -33,6 +33,7 @@ module ActiveRecord autoload :Quoting autoload :ConnectionPool autoload :QueryCache + autoload :Savepoints end autoload_at 'active_record/connection_adapters/abstract/transaction' do @@ -395,13 +396,13 @@ module ActiveRecord @transaction.number end - def create_savepoint + def create_savepoint(name = nil) end - def rollback_to_savepoint + def rollback_to_savepoint(name = nil) end - def release_savepoint + def release_savepoint(name = nil) end def case_sensitive_modifier(node) @@ -423,13 +424,14 @@ module ActiveRecord protected - def log(sql, name = "SQL", binds = []) + def log(sql, name = "SQL", binds = [], statement_name = nil) @instrumenter.instrument( "sql.active_record", - :sql => sql, - :name => name, - :connection_id => object_id, - :binds => binds) { yield } + :sql => sql, + :name => name, + :connection_id => object_id, + :statement_name => statement_name, + :binds => binds) { yield } rescue => e message = "#{e.class.name}: #{e.message}: #{sql}" @logger.error message if @logger 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 d502daf230..138ab811dc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -3,6 +3,8 @@ require 'arel/visitors/bind_visitor' module ActiveRecord module ConnectionAdapters class AbstractMysqlAdapter < AbstractAdapter + include Savepoints + class SchemaCreation < AbstractAdapter::SchemaCreation def visit_AddColumn(o) @@ -194,11 +196,6 @@ module ActiveRecord true end - # Returns true, since this connection adapter supports savepoints. - def supports_savepoints? - true - end - def supports_bulk_alter? #:nodoc: true end @@ -340,18 +337,6 @@ module ActiveRecord # Transactions aren't supported end - def create_savepoint - execute("SAVEPOINT #{current_savepoint_name}") - end - - def rollback_to_savepoint - execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}") - end - - def release_savepoint - execute("RELEASE SAVEPOINT #{current_savepoint_name}") - end - # In the simple case, MySQL allows us to place JOINs directly into the UPDATE # query. However, this does not allow for LIMIT, OFFSET and ORDER. To support # these, we must use a subquery. diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 2596c221bc..f2fbd5a8f2 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -13,7 +13,7 @@ module ActiveRecord ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ end - attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale + attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale, :default_function attr_accessor :primary, :coder alias :encoded? :coder @@ -27,16 +27,17 @@ module ActiveRecord # It will be mapped to one of the standard Rails SQL types in the <tt>type</tt> attribute. # +null+ determines if this column allows +NULL+ values. def initialize(name, default, sql_type = nil, null = true) - @name = name - @sql_type = sql_type - @null = null - @limit = extract_limit(sql_type) - @precision = extract_precision(sql_type) - @scale = extract_scale(sql_type) - @type = simplified_type(sql_type) - @default = extract_default(default) - @primary = nil - @coder = nil + @name = name + @sql_type = sql_type + @null = null + @limit = extract_limit(sql_type) + @precision = extract_precision(sql_type) + @scale = extract_scale(sql_type) + @type = simplified_type(sql_type) + @default = extract_default(default) + @default_function = nil + @primary = nil + @coder = nil end # Returns +true+ if the column is either of type string or text. diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 8bad7d0cf5..64fc9e95d8 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -55,7 +55,7 @@ module ActiveRecord begin require path_to_adapter rescue Gem::LoadError => e - raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile." + raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile (and ensure its version is at the minimum required by ActiveRecord)." rescue LoadError => e raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql', 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 41a47183e0..88c9494fc6 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -559,7 +559,7 @@ module ActiveRecord def set_field_encoding field_name field_name.force_encoding(client_encoding) if internal_enc = Encoding.default_internal - field_name = field_name.encoding(internal_enc) + field_name = field_name.encode!(internal_enc) end field_name end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index 86b96a77fb..fa173d13a2 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -134,35 +134,31 @@ module ActiveRecord end def exec_query(sql, name = 'SQL', binds = []) - log(sql, name, binds) do - result = without_prepared_statement?(binds) ? exec_no_cache(sql, binds) : - exec_cache(sql, binds) - - types = {} - fields = result.fields - fields.each_with_index do |fname, i| - ftype = result.ftype i - fmod = result.fmod i - types[fname] = OID::TYPE_MAP.fetch(ftype, fmod) { |oid, mod| - warn "unknown OID: #{fname}(#{oid}) (#{sql})" - OID::Identity.new - } - end - - ret = ActiveRecord::Result.new(fields, result.values, types) - result.clear - return ret + result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) : + exec_cache(sql, name, binds) + + types = {} + fields = result.fields + fields.each_with_index do |fname, i| + ftype = result.ftype i + fmod = result.fmod i + types[fname] = OID::TYPE_MAP.fetch(ftype, fmod) { |oid, mod| + warn "unknown OID: #{fname}(#{oid}) (#{sql})" + OID::Identity.new + } end + + ret = ActiveRecord::Result.new(fields, result.values, types) + result.clear + return ret end def exec_delete(sql, name = 'SQL', binds = []) - log(sql, name, binds) do - result = binds.empty? ? exec_no_cache(sql, binds) : - exec_cache(sql, binds) - affected = result.cmd_tuples - result.clear - affected - end + result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) : + exec_cache(sql, name, binds) + affected = result.cmd_tuples + result.clear + affected end alias :exec_update :exec_delete @@ -218,18 +214,6 @@ module ActiveRecord def rollback_db_transaction execute "ROLLBACK" end - - def create_savepoint - execute("SAVEPOINT #{current_savepoint_name}") - end - - def rollback_to_savepoint - execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}") - end - - def release_savepoint - execute("RELEASE SAVEPOINT #{current_savepoint_name}") - end 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 dab876af14..6c5792954f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -234,6 +234,10 @@ module ActiveRecord ConnectionAdapters::PostgreSQLColumn.string_to_hstore value end + + def accessor + ActiveRecord::Store::StringKeyedHashAccessor + end end class Cidr < Type @@ -245,11 +249,19 @@ module ActiveRecord end class Json < Type + def type_cast_for_write(value) + ConnectionAdapters::PostgreSQLColumn.json_to_string value + end + def type_cast(value) return if value.nil? ConnectionAdapters::PostgreSQLColumn.string_to_json value end + + def accessor + ActiveRecord::Store::StringKeyedHashAccessor + end end class TypeMap diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 3fce8de1ba..5dc70a5ad1 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -46,7 +46,7 @@ module ActiveRecord end # Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>, - # <tt>:encoding</tt>, <tt>:collation</tt>, <tt>:ctype</tt>, + # <tt>:encoding</tt> (defaults to utf8), <tt>:collation</tt>, <tt>:ctype</tt>, # <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses # <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>). # diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index af240fab95..3668aecd4b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -45,12 +45,12 @@ module ActiveRecord module ConnectionAdapters # PostgreSQL-specific extensions to column definitions in a table. class PostgreSQLColumn < Column #:nodoc: - attr_accessor :array, :default_function + attr_accessor :array # Instantiates a new PostgreSQL column definition in a table. def initialize(name, default, oid_type, sql_type = nil, null = true) @oid_type = oid_type default_value = self.class.extract_value_from_default(default) - @default_function = default if !default_value && default && default =~ /.+\(.*\)/ + if sql_type =~ /\[\]$/ @array = true super(name, default_value, sql_type[0..sql_type.length - 3], null) @@ -58,6 +58,8 @@ module ActiveRecord @array = false super(name, default_value, sql_type, null) end + + @default_function = default if has_default_function?(default_value, default) end # :stopdoc: @@ -146,8 +148,16 @@ module ActiveRecord @oid_type.type_cast value end + def accessor + @oid_type.accessor + end + private + def has_default_function?(default_value, default) + !default_value && (%r{\w+\(.*\)} === default) + end + def extract_limit(sql_type) case sql_type when /^bigint/i; 8 @@ -430,6 +440,7 @@ module ActiveRecord include ReferentialIntegrity include SchemaStatements include DatabaseStatements + include Savepoints # Returns 'PostgreSQL' as adapter name for identification purposes. def adapter_name @@ -441,7 +452,7 @@ module ActiveRecord def prepare_column_options(column, types) spec = super spec[:array] = 'true' if column.respond_to?(:array) && column.array - spec[:default] = "\"#{column.default_function}\"" if column.respond_to?(:default_function) && column.default_function + spec[:default] = "\"#{column.default_function}\"" if column.default_function spec end @@ -620,11 +631,6 @@ module ActiveRecord true end - # Returns true, since this connection adapter supports savepoints. - def supports_savepoints? - true - end - # Returns true. def supports_explain? true @@ -771,27 +777,29 @@ module ActiveRecord FEATURE_NOT_SUPPORTED = "0A000" # :nodoc: - def exec_no_cache(sql, binds) - @connection.async_exec(sql) + def exec_no_cache(sql, name, binds) + log(sql, name, binds) { @connection.async_exec(sql) } end - def exec_cache(sql, binds) + def exec_cache(sql, name, binds) stmt_key = prepare_statement(sql) - # Clear the queue - @connection.get_last_result - @connection.send_query_prepared(stmt_key, binds.map { |col, val| - type_cast(val, col) - }) - @connection.block - @connection.get_last_result - rescue PGError => e + log(sql, name, binds, stmt_key) do + @connection.send_query_prepared(stmt_key, binds.map { |col, val| + type_cast(val, col) + }) + @connection.block + @connection.get_last_result + end + rescue ActiveRecord::StatementInvalid => e + pgerror = e.original_exception + # Get the PG code for the failure. Annoyingly, the code for # prepared statements whose return value may have changed is # FEATURE_NOT_SUPPORTED. Check here for more details: # http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573 begin - code = e.result.result_error_field(PGresult::PG_DIAG_SQLSTATE) + code = pgerror.result.result_error_field(PGresult::PG_DIAG_SQLSTATE) rescue raise e end @@ -816,6 +824,8 @@ module ActiveRecord unless @statements.key? sql_key nextkey = @statements.next_key @connection.prepare nextkey, sql + # Clear the queue + @connection.get_last_result @statements[sql_key] = nextkey end @statements[sql_key] diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 136094dcc9..e5ad08b6b0 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -53,6 +53,23 @@ module ActiveRecord # # * <tt>:database</tt> - Path to the database file. class SQLite3Adapter < AbstractAdapter + include Savepoints + + NATIVE_DATABASE_TYPES = { + primary_key: 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL', + string: { name: "varchar", limit: 255 }, + text: { name: "text" }, + integer: { name: "integer" }, + float: { name: "float" }, + decimal: { name: "decimal" }, + datetime: { name: "datetime" }, + timestamp: { name: "datetime" }, + time: { name: "time" }, + date: { name: "date" }, + binary: { name: "blob" }, + boolean: { name: "boolean" } + } + class Version include Comparable @@ -181,11 +198,6 @@ module ActiveRecord true end - # Returns true - def supports_autoincrement? #:nodoc: - true - end - def supports_index_sort_order? true end @@ -198,20 +210,7 @@ module ActiveRecord end def native_database_types #:nodoc: - { - :primary_key => default_primary_key_type, - :string => { :name => "varchar", :limit => 255 }, - :text => { :name => "text" }, - :integer => { :name => "integer" }, - :float => { :name => "float" }, - :decimal => { :name => "decimal" }, - :datetime => { :name => "datetime" }, - :timestamp => { :name => "datetime" }, - :time => { :name => "time" }, - :date => { :name => "date" }, - :binary => { :name => "blob" }, - :boolean => { :name => "boolean" } - } + NATIVE_DATABASE_TYPES end # Returns the current database encoding format as a string, eg: 'UTF-8' @@ -351,18 +350,6 @@ module ActiveRecord exec_query(sql, name).rows end - def create_savepoint - execute("SAVEPOINT #{current_savepoint_name}") - end - - def rollback_to_savepoint - execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}") - end - - def release_savepoint - execute("RELEASE SAVEPOINT #{current_savepoint_name}") - end - def begin_db_transaction #:nodoc: log('begin transaction',nil) { @connection.transaction } end @@ -606,14 +593,6 @@ module ActiveRecord @sqlite_version ||= SQLite3Adapter::Version.new(select_value('select sqlite_version(*)')) end - def default_primary_key_type - if supports_autoincrement? - 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL' - else - 'INTEGER PRIMARY KEY NOT NULL' - end - end - def translate_exception(exception, message) case exception.message when /column(s)? .* (is|are) not unique/ diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 0167ddb5e5..d93c26c130 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -310,6 +310,15 @@ module ActiveRecord @attributes.frozen? end + # Allows sort on objects + def <=>(other_object) + if other_object.is_a?(self.class) + self.to_key <=> other_object.to_key + else + super + end + end + # Returns +true+ if the record is read only. Records loaded through joins with piggy-back # attributes will be marked as read only since they cannot be saved. def readonly? diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb new file mode 100644 index 0000000000..da6bc87950 --- /dev/null +++ b/activerecord/lib/active_record/enum.rb @@ -0,0 +1,64 @@ +module ActiveRecord + # Declare an enum attribute where the values map to integers in the database, but can be queried by name. Example: + # + # class Conversation < ActiveRecord::Base + # enum status: [ :active, :archived ] + # end + # + # Conversation::STATUS # => { active: 0, archived: 1 } + # + # # conversation.update! status: 0 + # conversation.active! + # conversation.active? # => true + # conversation.status # => :active + # + # # conversation.update! status: 1 + # conversation.archived! + # conversation.archived? # => true + # conversation.status # => :archived + # + # # conversation.update! status: 1 + # conversation.status = :archived + # + # You can set the default value from the database declaration, like: + # + # create_table :conversations do |t| + # t.column :status, :integer, default: 0 + # end + # + # Good practice is to let the first declared status be the default. + # + # Finally, it's also possible to explicitly map the relation between attribute and database integer: + # + # class Conversation < ActiveRecord::Base + # enum status: { active: 0, archived: 1 } + # end + module Enum + def enum(definitions) + definitions.each do |name, values| + enum_values = {} + name = name.to_sym + + # def direction=(value) self[:direction] = DIRECTION[value] end + define_method("#{name}=") { |value| self[name] = enum_values[value] } + + # def direction() DIRECTION.key self[:direction] end + define_method(name) { enum_values.key self[name] } + + pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index + pairs.each do |value, i| + enum_values[value] = i + + # scope :incoming, -> { where direction: 0 } + scope value, -> { where name => i } + + # def incoming?() direction == 0 end + define_method("#{value}?") { self[name] == i } + + # def incoming! update! direction: :incoming end + define_method("#{value}!") { update! name => value.to_sym } + end + end + end + end +end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 9a26e5df3f..8601414209 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -504,16 +504,8 @@ module ActiveRecord connection.transaction(:requires_new => true) do fixture_sets.each do |fs| conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection - table_rows = fs.table_rows - - table_rows.keys.each do |table| - conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete' - end - - table_rows.each do |fixture_set_name, rows| - rows.each do |row| - conn.insert_fixture(row, fixture_set_name) - end + fs.fixture_sql(conn).each do |stmt| + conn.execute stmt end end @@ -580,6 +572,16 @@ module ActiveRecord fixtures.size end + def fixture_sql(conn) + table_rows = self.table_rows + + table_rows.keys.map { |table| + "DELETE FROM #{conn.quote_table_name(table)}" + }.concat table_rows.flat_map { |fixture_set_name, rows| + rows.map { |row| conn.fixture_sql(row, fixture_set_name) } + } + end + # Return a hash of rows to be inserted. The key is the table, the value is # a list of rows to insert to that table. def table_rows @@ -639,8 +641,6 @@ module ActiveRecord if association.options[:through] add_join_records(rows, row, HasManyThroughProxy.new(association)) end - when :has_and_belongs_to_many - add_join_records(rows, row, HABTMProxy.new(association)) end end end @@ -674,16 +674,6 @@ module ActiveRecord end end - class HABTMProxy < ReflectionProxy # :nodoc: - def rhs_key - @association.association_foreign_key - end - - def lhs_key - @association.foreign_key - end - end - private def primary_key_name @primary_key_name ||= model_class && model_class.primary_key diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index e826762def..7e1e120288 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -160,7 +160,7 @@ module ActiveRecord end def type_condition(table = arel_table) - sti_column = table[inheritance_column.to_sym] + sti_column = table[inheritance_column] sti_names = ([self] + descendants).map { |model| model.sti_name } sti_column.in(sti_names) diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 2589b2f3da..f88c8e30e6 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -45,10 +45,19 @@ module ActiveRecord # Product.new.cache_key # => "products/new" # Product.find(5).cache_key # => "products/5" (updated_at not available) # Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available) - def cache_key + # + # You can also pass a list of named timestamps, and the newest in the list will be + # used to generate the key: + # + # Person.find(5).cache_key(:updated_at, :last_reviewed_at) + def cache_key(*timestamp_names) case when new_record? "#{self.class.model_name.cache_key}/new" + when timestamp_names.any? + timestamp = max_updated_column_timestamp(timestamp_names) + timestamp = timestamp.utc.to_s(cache_timestamp_format) + "#{self.class.model_name.cache_key}/#{id}-#{timestamp}" when timestamp = max_updated_column_timestamp timestamp = timestamp.utc.to_s(cache_timestamp_format) "#{self.class.model_name.cache_key}/#{id}-#{timestamp}" diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index ddf2afca0c..ff7102d35b 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -3,12 +3,12 @@ module ActiveRecord # Locking::Pessimistic provides support for row-level locking using # SELECT ... FOR UPDATE and other lock types. # - # Pass <tt>lock: true</tt> to <tt>ActiveRecord::Base.find</tt> to obtain an exclusive + # Chain <tt>ActiveRecord::Base#find</tt> to <tt>ActiveRecord::QueryMethods#lock</tt> to obtain an exclusive # lock on the selected rows: # # select * from accounts where id=1 for update - # Account.find(1, lock: true) + # Account.lock.find(1) # - # Pass <tt>lock: 'some locking clause'</tt> to give a database-specific locking clause + # Call <tt>lock('some locking clause')</tt> to use a database-specific locking clause # of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'. Example: # # Account.transaction do diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 0358a36b14..927fbab8f0 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -17,7 +17,7 @@ module ActiveRecord def initialize super - @odd_or_even = false + @odd = false end def render_bind(column, value) @@ -60,17 +60,8 @@ module ActiveRecord debug " #{name} #{sql}#{binds}" end - def identity(event) - return unless logger.debug? - - name = color(event.payload[:name], odd? ? CYAN : MAGENTA, true) - line = odd? ? color(event.payload[:line], nil, true) : event.payload[:line] - - debug " #{name} #{line}" - end - def odd? - @odd_or_even = !@odd_or_even + @odd = !@odd end def logger diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index a1ad4f6255..5224a6b67c 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -120,8 +120,8 @@ module ActiveRecord # a column but keeps the type and content. # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes # the column to a different type using the same parameters as add_column. - # * <tt>remove_column(table_name, column_names)</tt>: Removes the column listed in - # +column_names+ from the table called +table_name+. + # * <tt>remove_column(table_name, column_name, type, options)</tt>: Removes the column + # named +column_name+ from the table called +table_name+. # * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index # with the name of the column. Other options include # <tt>:name</tt>, <tt>:unique</tt> (e.g. @@ -629,7 +629,7 @@ module ActiveRecord def copy(destination, sources, options = {}) copied = [] - FileUtils.mkdir_p(destination) unless File.exists?(destination) + FileUtils.mkdir_p(destination) unless File.exist?(destination) destination_migrations = ActiveRecord::Migrator.migrations(destination) last = destination_migrations.last diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 75c0c1bda8..dc5ff02882 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -34,6 +34,12 @@ module ActiveRecord ## # :singleton-method: + # Accessor for the name of the schema migrations table. By default, the value is "schema_migrations" + class_attribute :schema_migrations_table_name, instance_accessor: false + self.schema_migrations_table_name = "schema_migrations" + + ## + # :singleton-method: # Indicates whether table names should be the pluralized versions of the corresponding class names. # If true, the default table name for a Product class will be +products+. If false, it would just be +product+. # See table_name for the full rules on table/class naming. This is true, by default. diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index d166f0dd66..080b20134d 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -6,7 +6,7 @@ module ActiveRecord @records = [] end - def pluck(_column_name) + def pluck(*column_names) [] end @@ -42,10 +42,6 @@ module ActiveRecord "" end - def where_values_hash - {} - end - def count(*) 0 end @@ -54,7 +50,7 @@ module ActiveRecord 0 end - def calculate(_operation, _column_name, _options = {}) + def calculate(_operation, _column_name) if _operation == :count 0 else diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index bdd00ee259..a73a140ef1 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -335,8 +335,18 @@ module ActiveRecord # Reloads the record from the database. # - # This method modifies the receiver in-place. Attributes are updated, and - # caches busted, in particular the associations cache. + # This method finds record by its primary key (which could be assigned manually) and + # modifies the receiver in-place: + # + # account = Account.new + # # => #<Account id: nil, email: nil> + # account.id = 1 + # account.reload + # # Account Load (1.2ms) SELECT "accounts".* FROM "accounts" WHERE "accounts"."id" = $1 LIMIT 1 [["id", 1]] + # # => #<Account id: 1, email: 'account@example.com'> + # + # Attributes are reloaded from the database, and caches busted, in + # particular the associations cache. # # If the record no longer exists in the database <tt>ActiveRecord::RecordNotFound</tt> # is raised. Otherwise, in addition to the in-place modification the method @@ -391,7 +401,8 @@ module ActiveRecord end # Saves the record with the updated_at/on attributes set to the current time. - # Please note that no validation is performed and no callbacks are executed. + # Please note that no validation is performed and only the +after_touch+ + # callback is executed. # If an attribute name is passed, that attribute is updated along with # updated_at/on attributes. # diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 6bee4f38e7..fd4c973504 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -7,7 +7,7 @@ module ActiveRecord delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all delegate :find_each, :find_in_batches, to: :all delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, - :where, :preload, :eager_load, :includes, :from, :lock, :readonly, + :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :uniq, :distinct, :references, :none, :unscope, to: :all delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all delegate :pluck, :ids, to: :all diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index daccab762f..52b3d3e5e6 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -271,7 +271,7 @@ db_namespace = namespace :db do desc 'Clear a db/schema_cache.dump file.' task :clear => [:environment, :load_config] do filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump") - FileUtils.rm(filename) if File.exists?(filename) + FileUtils.rm(filename) if File.exist?(filename) end end @@ -287,6 +287,7 @@ db_namespace = namespace :db do if ActiveRecord::Base.connection.supports_migrations? File.open(filename, "a") do |f| f.puts ActiveRecord::Base.connection.dump_schema_information + f.print "\n" end end db_namespace['structure:dump'].reenable diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index f47282b7fd..bce7766501 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -12,8 +12,6 @@ module ActiveRecord def self.create(macro, name, scope, options, ar) case macro - when :has_and_belongs_to_many - klass = AssociationReflection when :has_many, :belongs_to, :has_one klass = options[:through] ? ThroughReflection : AssociationReflection when :composed_of @@ -196,10 +194,11 @@ module ActiveRecord def initialize(macro, name, scope, options, active_record) super - @collection = [:has_many, :has_and_belongs_to_many].include?(macro) + @collection = :has_many == macro @automatic_inverse_of = nil @type = options[:as] && "#{options[:as]}_type" @foreign_type = options[:foreign_type] || "#{name}_type" + @constructable = calculate_constructable(macro, options) end # Returns a new, unsaved instance of the associated class. +attributes+ will @@ -208,6 +207,10 @@ module ActiveRecord klass.new(attributes, &block) end + def constructable? # :nodoc: + @constructable + end + def table_name klass.table_name end @@ -251,10 +254,6 @@ module ActiveRecord def check_validity! check_validity_of_inverse! - - if has_and_belongs_to_many? && association_foreign_key == foreign_key - raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(self) - end end def check_validity_of_inverse! @@ -336,10 +335,6 @@ module ActiveRecord macro == :belongs_to end - def has_and_belongs_to_many? - macro == :has_and_belongs_to_many - end - def association_class case macro when :belongs_to @@ -348,8 +343,6 @@ module ActiveRecord else Associations::BelongsToAssociation end - when :has_and_belongs_to_many - Associations::HasAndBelongsToManyAssociation when :has_many if options[:through] Associations::HasManyThroughAssociation @@ -374,11 +367,23 @@ module ActiveRecord protected - def actual_source_reflection # FIXME: this is a horrible name - self - end + def actual_source_reflection # FIXME: this is a horrible name + self + end private + + def calculate_constructable(macro, options) + case macro + when :belongs_to + !options[:polymorphic] + when :has_one + !options[:through] + else + true + end + end + # Attempts to find the inverse association name automatically. # If it cannot find a suitable inverse association name, it returns # nil. @@ -569,7 +574,7 @@ module ActiveRecord # Add to it the scope from this reflection (if any) scope_chain.first << scope if scope - through_scope_chain = through_reflection.scope_chain + through_scope_chain = through_reflection.scope_chain.map(&:dup) if options[:source_type] through_scope_chain.first << @@ -588,7 +593,7 @@ module ActiveRecord # A through association is nested if there would be more than one join table def nested? - chain.length > 2 || through_reflection.has_and_belongs_to_many? + chain.length > 2 end # We want to use the klass from this reflection, rather than just delegate straight to diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index cfaf566ec4..60f2726a6e 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -244,8 +244,7 @@ module ActiveRecord def empty? return @records.empty? if loaded? - c = count(:all) - c.respond_to?(:zero?) ? c.zero? : c.empty? + limit_value == 0 ? true : !exists? end # Returns true if there are any records. @@ -507,8 +506,7 @@ module ActiveRecord visitor = connection.visitor if eager_loading? - join_dependency = construct_join_dependency - relation = construct_relation_for_association_find(join_dependency) + find_with_associations { |rel| relation = rel } end ast = relation.arel.ast diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index fd8496442e..49b01909c6 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -34,7 +34,7 @@ module ActiveRecord # between id 0 and 10,000 and worker 2 handle from 10,000 and beyond # (by setting the +:start+ option on that worker). # - # # Let's process for a batch of 2000 records, skiping the first 2000 rows + # # Let's process for a batch of 2000 records, skipping the first 2000 rows # Person.find_each(start: 2000, batch_size: 2000) do |person| # person.party_all_night! # end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 27c04b0952..2d267183ce 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -19,17 +19,16 @@ module ActiveRecord # # Person.group(:city).count # # => { 'Rome' => 5, 'Paris' => 3 } - def count(column_name = nil, options = {}) - column_name, options = nil, column_name if column_name.is_a?(Hash) - calculate(:count, column_name, options) + def count(column_name = nil) + calculate(:count, column_name) end # Calculates the average value on a given column. Returns +nil+ if there's # no row. See +calculate+ for examples with options. # # Person.average(:age) # => 35.8 - def average(column_name, options = {}) - calculate(:average, column_name, options) + def average(column_name) + calculate(:average, column_name) end # Calculates the minimum value on a given column. The value is returned @@ -37,8 +36,8 @@ module ActiveRecord # +calculate+ for examples with options. # # Person.minimum(:age) # => 7 - def minimum(column_name, options = {}) - calculate(:minimum, column_name, options) + def minimum(column_name) + calculate(:minimum, column_name) end # Calculates the maximum value on a given column. The value is returned @@ -46,8 +45,8 @@ module ActiveRecord # +calculate+ for examples with options. # # Person.maximum(:age) # => 93 - def maximum(column_name, options = {}) - calculate(:maximum, column_name, options) + def maximum(column_name) + calculate(:maximum, column_name) end # Calculates the sum of values on a given column. The value is returned @@ -90,15 +89,15 @@ module ActiveRecord # Person.group(:last_name).having("min(age) > 17").minimum(:age) # # Person.sum("2 * age") - def calculate(operation, column_name, options = {}) + def calculate(operation, column_name) if column_name.is_a?(Symbol) && attribute_alias?(column_name) column_name = attribute_alias(column_name) end if has_include?(column_name) - construct_relation_for_association_calculations.calculate(operation, column_name, options) + construct_relation_for_association_calculations.calculate(operation, column_name) else - perform_calculation(operation, column_name, options) + perform_calculation(operation, column_name) end end @@ -181,7 +180,7 @@ module ActiveRecord eager_loading? || (includes_values.present? && (column_name || references_eager_loaded_tables?)) end - def perform_calculation(operation, column_name, options = {}) + def perform_calculation(operation, column_name) operation = operation.to_s.downcase # If #count is used with #distinct / #uniq it is considered distinct. (eg. relation.distinct.count) diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 0132a02f83..3a02bf90e9 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -201,7 +201,7 @@ module ActiveRecord conditions = conditions.id if Base === conditions return false if !conditions - relation = construct_relation_for_association_find(construct_join_dependency) + relation = apply_join_dependency(self, construct_join_dependency) return false if ActiveRecord::NullRelation === relation relation = relation.except(:select, :order).select(ONE_AS_ONE).limit(1) @@ -242,17 +242,25 @@ module ActiveRecord def find_with_associations join_dependency = construct_join_dependency - relation = construct_relation_for_association_find(join_dependency) - if ActiveRecord::NullRelation === relation - [] + + aliases = join_dependency.aliases + relation = select aliases.columns + relation = apply_join_dependency(relation, join_dependency) + + if block_given? + yield relation else - rows = connection.select_all(relation.arel, 'SQL', relation.bind_values.dup) - join_dependency.instantiate(rows) + if ActiveRecord::NullRelation === relation + [] + else + rows = connection.select_all(relation.arel, 'SQL', relation.bind_values.dup) + join_dependency.instantiate(rows, aliases) + end end end def construct_join_dependency(joins = []) - including = (eager_load_values + includes_values).uniq + including = eager_load_values + includes_values ActiveRecord::Associations::JoinDependency.new(@klass, including, joins) end @@ -260,14 +268,9 @@ module ActiveRecord apply_join_dependency(self, construct_join_dependency(arel.froms.first)) end - def construct_relation_for_association_find(join_dependency) - relation = except(:select).select(join_dependency.columns) - apply_join_dependency(relation, join_dependency) - end - def apply_join_dependency(relation, join_dependency) relation = relation.except(:includes, :eager_load, :preload) - relation = join_dependency.join_relation(relation) + relation = relation.joins join_dependency if using_limitable_reflections?(join_dependency.reflections) relation @@ -297,6 +300,8 @@ module ActiveRecord protected def find_with_ids(*ids) + raise UnknownPrimaryKey.new(@klass) if primary_key.nil? + expects_array = ids.first.kind_of?(Array) return ids.first if expects_array && ids.first.empty? diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index c05632e688..182b9ed89c 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -94,7 +94,7 @@ module ActiveRecord []) relation.joins! rest - @relation = join_dependency.join_relation(relation) + @relation = relation.joins join_dependency end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 9fcb6db726..62c555f6d6 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -341,9 +341,6 @@ module ActiveRecord # User.where(name: "John", active: true).unscope(where: :name) # == User.where(active: true) # - # This method is applied before the default_scope is applied. So the conditions - # specified in default_scope will not be removed. - # # Note that this method is more generalized than ActiveRecord::SpawnMethods#except # because #except will only affect a particular relation's values. It won't wipe # the order, grouping, etc. when that relation is merged. For example: @@ -553,6 +550,18 @@ module ActiveRecord end end + # Allows you to change a previously set where condition for a given attribute, instead of appending to that condition. + # + # Post.where(trashed: true).where(trashed: false) #=> WHERE `trashed` = 1 AND `trashed` = 0 + # Post.where(trashed: true).rewhere(trashed: false) #=> WHERE `trashed` = 0 + # Post.where(active: true).where(trashed: true).rewhere(trashed: false) #=> WHERE `active` = 1 AND `trashed` = 0 + # + # This is short-hand for unscope(where: conditions.keys).where(conditions). Note that unlike reorder, we're only unscoping + # the named conditions -- not the entire where statement. + def rewhere(conditions) + unscope(where: conditions.keys).where(conditions) + end + # Allows to specify a HAVING clause. Note that you can't use HAVING # without also specifying a GROUP clause. # @@ -856,7 +865,7 @@ module ActiveRecord where_values.reject! do |rel| case rel - when Arel::Nodes::In, Arel::Nodes::Equality + when Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual subrelation = (rel.left.kind_of?(Arel::Attributes::Attribute) ? rel.left : rel.right) subrelation.name.to_sym == target_value_sym else @@ -894,6 +903,13 @@ module ActiveRecord def build_where(opts, other = []) case opts when String, Array + #TODO: Remove duplication with: /activerecord/lib/active_record/sanitization.rb:113 + values = Hash === other.first ? other.first.values : other + + values.grep(ActiveRecord::Relation) do |rel| + self.bind_values += rel.bind_values + end + [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] when Hash opts = PredicateBuilder.resolve_column_aliases(klass, opts) @@ -928,7 +944,7 @@ module ActiveRecord :string_join when Hash, Symbol, Array :association_join - when ActiveRecord::Associations::JoinDependency::JoinAssociation + when ActiveRecord::Associations::JoinDependency :stashed_join when Arel::Nodes::Join :join_node @@ -950,10 +966,7 @@ module ActiveRecord join_list ) - join_dependency.graft(*stashed_association_joins) - - joins = join_dependency.join_associations.map!(&:join_constraints) - joins.flatten! + joins = join_dependency.join_constraints stashed_association_joins joins.each { |join| manager.from(join) } diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index d0f1cb5b75..1dc3bf3f12 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -46,6 +46,10 @@ module ActiveRecord IDENTITY_TYPE end + def column_type(name) + @column_types[name] || identity_type + end + def each if block_given? hash_rows.each { |row| yield row } diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 0b87ab9926..cab8fd745a 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -127,7 +127,17 @@ module ActiveRecord raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size) bound = values.dup c = connection - statement.gsub('?') { quote_bound_value(bound.shift, c) } + statement.gsub('?') do + replace_bind_variable(bound.shift, c) + end + end + + def replace_bind_variable(value, c = connection) #:nodoc: + if ActiveRecord::Relation === value + value.to_sql + else + quote_bound_value(value, c) + end end def replace_named_bind_variables(statement, bind_vars) #:nodoc: @@ -135,7 +145,7 @@ module ActiveRecord if $1 == ':' # skip postgresql casts $& # return the whole match elsif bind_vars.include?(match = $2.to_sym) - quote_bound_value(bind_vars[match]) + replace_bind_variable(bind_vars[match]) else raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}" end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 8986d255cd..e055d571ab 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -123,6 +123,7 @@ HEADER tbl.print %Q(, primary_key: "#{pk}") elsif pkcol.sql_type == 'uuid' tbl.print ", id: :uuid" + tbl.print %Q(, default: "#{pkcol.default_function}") if pkcol.default_function end else tbl.print ", id: false" diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index fee19b1096..a9d164e366 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -7,11 +7,11 @@ module ActiveRecord class << self def table_name - "#{table_name_prefix}schema_migrations#{table_name_suffix}" + "#{table_name_prefix}#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}" end def index_name - "#{table_name_prefix}unique_schema_migrations#{table_name_suffix}" + "#{table_name_prefix}unique_#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}" end def table_exists? diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index a610f479f2..b841b977fc 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -86,6 +86,9 @@ module ActiveRecord end end + # assign new store attribute and create new hash to ensure that each class in the hierarchy + # has its own hash of stored attributes. + self.stored_attributes = {} if self.stored_attributes.blank? self.stored_attributes[store_attribute] ||= [] self.stored_attributes[store_attribute] |= keys end @@ -101,26 +104,58 @@ module ActiveRecord protected def read_store_attribute(store_attribute, key) - attribute = initialize_store_attribute(store_attribute) - attribute[key] + accessor = store_accessor_for(store_attribute) + accessor.read(self, store_attribute, key) end def write_store_attribute(store_attribute, key, value) - attribute = initialize_store_attribute(store_attribute) - if value != attribute[key] - send :"#{store_attribute}_will_change!" - attribute[key] = value - end + accessor = store_accessor_for(store_attribute) + accessor.write(self, store_attribute, key, value) end private - def initialize_store_attribute(store_attribute) - attribute = send(store_attribute) - unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess) - attribute = IndifferentCoder.as_indifferent_hash(attribute) - send :"#{store_attribute}=", attribute + def store_accessor_for(store_attribute) + @column_types[store_attribute.to_s].accessor + end + + class HashAccessor + def self.read(object, attribute, key) + prepare(object, attribute) + object.public_send(attribute)[key] + end + + def self.write(object, attribute, key, value) + prepare(object, attribute) + if value != read(object, attribute, key) + object.public_send :"#{attribute}_will_change!" + object.public_send(attribute)[key] = value + end + end + + def self.prepare(object, attribute) + object.public_send :"#{attribute}=", {} unless object.send(attribute) + end + end + + class StringKeyedHashAccessor < HashAccessor + def self.read(object, attribute, key) + super object, attribute, key.to_s + end + + def self.write(object, attribute, key, value) + super object, attribute, key.to_s, value + end + end + + class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor + def self.prepare(object, store_attribute) + attribute = object.send(store_attribute) + unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess) + attribute = IndifferentCoder.as_indifferent_hash(attribute) + object.send :"#{store_attribute}=", attribute + end + attribute end - attribute end class IndifferentCoder # :nodoc: @@ -138,7 +173,7 @@ module ActiveRecord end def load(yaml) - self.class.as_indifferent_hash @coder.load(yaml) + self.class.as_indifferent_hash(@coder.load(yaml)) end def self.as_indifferent_hash(obj) diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index b91bbeb412..be7d496d15 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -146,7 +146,7 @@ module ActiveRecord end def check_schema_file(filename) - unless File.exists?(filename) + unless File.exist?(filename) message = %{#{filename} doesn't exist yet. Run `rake db:migrate` to create it, then try again.} message << %{ If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded.} if defined?(::Rails) Kernel.abort message diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index 50569d2462..c755831e6d 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -134,8 +134,9 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION; args << "--password=#{configuration['password']}" if configuration['password'] args.concat(['--default-character-set', configuration['encoding']]) if configuration['encoding'] configuration.slice('host', 'port', 'socket').each do |k, v| - args.concat([ "--#{k}", v ]) if v + args.concat([ "--#{k}", v.to_s ]) if v end + args end end diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index 4413330fab..3d02ee07d0 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -59,7 +59,7 @@ module ActiveRecord def structure_load(filename) set_psql_env - Kernel.system("psql -q -f #{filename} #{configuration['database']}") + Kernel.system("psql -q -f #{Shellwords.escape(filename)} #{configuration['database']}") end private diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 9253150c4f..e0541b7681 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -98,8 +98,8 @@ module ActiveRecord timestamp_attributes_for_create + timestamp_attributes_for_update end - def max_updated_column_timestamp - if (timestamps = timestamp_attributes_for_update.map { |attr| self[attr] }.compact).present? + 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 end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index dcbf38a89f..45313b5e75 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -220,8 +220,8 @@ module ActiveRecord # after_commit :do_bar, on: :update # after_commit :do_baz, on: :destroy # - # after_commit :do_foo_bar, :on [:create, :update] - # after_commit :do_bar_baz, :on [:update, :destroy] + # after_commit :do_foo_bar, on: [:create, :update] + # after_commit :do_bar_baz, on: [:update, :destroy] # # Note that transactional fixtures do not play well with this feature. Please # use the +test_after_commit+ gem to have these hooks fired in tests. diff --git a/activerecord/lib/rails/generators/active_record.rb b/activerecord/lib/rails/generators/active_record.rb index c8aa37f275..dc29213235 100644 --- a/activerecord/lib/rails/generators/active_record.rb +++ b/activerecord/lib/rails/generators/active_record.rb @@ -1,23 +1,17 @@ require 'rails/generators/named_base' -require 'rails/generators/migration' require 'rails/generators/active_model' +require 'rails/generators/active_record/migration' require 'active_record' module ActiveRecord module Generators # :nodoc: class Base < Rails::Generators::NamedBase # :nodoc: - include Rails::Generators::Migration + include ActiveRecord::Generators::Migration # Set the current directory as base for the inherited generators. def self.base_root File.dirname(__FILE__) end - - # Implement the required interface for Rails::Generators::Migration. - def self.next_migration_number(dirname) - next_migration_number = current_migration_number(dirname) + 1 - ActiveRecord::Migration.next_migration_number(next_migration_number) - end end end end diff --git a/activerecord/lib/rails/generators/active_record/migration.rb b/activerecord/lib/rails/generators/active_record/migration.rb new file mode 100644 index 0000000000..b7418cf42f --- /dev/null +++ b/activerecord/lib/rails/generators/active_record/migration.rb @@ -0,0 +1,18 @@ +require 'rails/generators/migration' + +module ActiveRecord + module Generators # :nodoc: + module Migration + extend ActiveSupport::Concern + include Rails::Generators::Migration + + module ClassMethods + # Implement the required interface for Rails::Generators::Migration. + def next_migration_number(dirname) + next_migration_number = current_migration_number(dirname) + 1 + ActiveRecord::Migration.next_migration_number(next_migration_number) + end + end + end + end +end diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index fedd9f603c..679c515e8c 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -3,14 +3,14 @@ require "cases/helper" class MysqlConnectionTest < ActiveRecord::TestCase def setup super + @subscriber = SQLSubscriber.new + ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber) @connection = ActiveRecord::Base.connection - @connection.extend(LogIntercepter) - @connection.intercepted = true end def teardown - @connection.intercepted = false - @connection.logged = [] + ActiveSupport::Notifications.unsubscribe(@subscriber) + super end def test_no_automatic_reconnection_after_timeout @@ -72,14 +72,14 @@ class MysqlConnectionTest < ActiveRecord::TestCase def test_logs_name_show_variable @connection.show_variable 'foo' - assert_equal "SCHEMA", @connection.logged[0][1] + assert_equal "SCHEMA", @subscriber.logged[0][1] end def test_logs_name_rename_column_sql @connection.execute "CREATE TABLE `bar_baz` (`foo` varchar(255))" - @connection.logged = [] + @subscriber.logged.clear @connection.send(:rename_column_sql, 'bar_baz', 'foo', 'foo2') - assert_equal "SCHEMA", @connection.logged[0][1] + assert_equal "SCHEMA", @subscriber.logged[0][1] ensure @connection.execute "DROP TABLE `bar_baz`" end diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index ecdbefcd03..9536cceb1d 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -113,6 +113,11 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert_equal(PgArray.last.tags, tag_values) end + def test_attribute_for_inspect_for_array_field + record = PgArray.new { |a| a.ratings = (1..11).to_a } + assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]", record.attribute_for_inspect(:ratings)) + end + private def assert_cycle field, array # test creation diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index 6b726ce875..81aa977c59 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -7,14 +7,14 @@ module ActiveRecord def setup super + @subscriber = SQLSubscriber.new + ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber) @connection = ActiveRecord::Base.connection - @connection.extend(LogIntercepter) - @connection.intercepted = true end def teardown - @connection.intercepted = false - @connection.logged = [] + ActiveSupport::Notifications.unsubscribe(@subscriber) + super end def test_encoding @@ -47,38 +47,48 @@ module ActiveRecord def test_tables_logs_name @connection.tables('hello') - assert_equal 'SCHEMA', @connection.logged[0][1] + assert_equal 'SCHEMA', @subscriber.logged[0][1] end def test_indexes_logs_name @connection.indexes('items', 'hello') - assert_equal 'SCHEMA', @connection.logged[0][1] + assert_equal 'SCHEMA', @subscriber.logged[0][1] end def test_table_exists_logs_name @connection.table_exists?('items') - assert_equal 'SCHEMA', @connection.logged[0][1] + assert_equal 'SCHEMA', @subscriber.logged[0][1] end def test_table_alias_length_logs_name @connection.instance_variable_set("@table_alias_length", nil) @connection.table_alias_length - assert_equal 'SCHEMA', @connection.logged[0][1] + assert_equal 'SCHEMA', @subscriber.logged[0][1] end def test_current_database_logs_name @connection.current_database - assert_equal 'SCHEMA', @connection.logged[0][1] + assert_equal 'SCHEMA', @subscriber.logged[0][1] end def test_encoding_logs_name @connection.encoding - assert_equal 'SCHEMA', @connection.logged[0][1] + assert_equal 'SCHEMA', @subscriber.logged[0][1] end def test_schema_names_logs_name @connection.schema_names - assert_equal 'SCHEMA', @connection.logged[0][1] + assert_equal 'SCHEMA', @subscriber.logged[0][1] + end + + def test_statement_key_is_logged + bindval = 1 + @connection.exec_query('SELECT $1::integer', 'SQL', [[nil, bindval]]) + 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 + assert_operator plan.length, :>, 0 end # Must have with_manual_interventions set to true for this diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index 3dbab08a99..c5ff8cb609 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -589,38 +589,28 @@ _SQL end def test_timestamp_with_zone_values_with_rails_time_zone_support - old_tz = ActiveRecord::Base.time_zone_aware_attributes - old_default_tz = ActiveRecord::Base.default_timezone + with_timezone_config default: :utc, aware_attributes: true do + @connection.reconnect! - ActiveRecord::Base.time_zone_aware_attributes = true - ActiveRecord::Base.default_timezone = :utc - - @connection.reconnect! - - @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1) - assert_equal Time.utc(2010,1,1, 11,0,0), @first_timestamp_with_zone.time - assert_instance_of Time, @first_timestamp_with_zone.time + @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1) + assert_equal Time.utc(2010,1,1, 11,0,0), @first_timestamp_with_zone.time + assert_instance_of Time, @first_timestamp_with_zone.time + end ensure - ActiveRecord::Base.default_timezone = old_default_tz - ActiveRecord::Base.time_zone_aware_attributes = old_tz @connection.reconnect! end def test_timestamp_with_zone_values_without_rails_time_zone_support - old_tz = ActiveRecord::Base.time_zone_aware_attributes - old_default_tz = ActiveRecord::Base.default_timezone - - ActiveRecord::Base.time_zone_aware_attributes = false - ActiveRecord::Base.default_timezone = :local - - @connection.reconnect! - - @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1) - assert_equal Time.local(2010,1,1, 11,0,0), @first_timestamp_with_zone.time - assert_instance_of Time, @first_timestamp_with_zone.time + with_timezone_config default: :local, aware_attributes: false do + @connection.reconnect! + # make sure to use a non-UTC time zone + @connection.execute("SET time zone 'America/Jamaica'", 'SCHEMA') + + @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1) + assert_equal Time.utc(2010,1,1, 11,0,0), @first_timestamp_with_zone.time + assert_instance_of Time, @first_timestamp_with_zone.time + end ensure - ActiveRecord::Base.default_timezone = old_default_tz - ActiveRecord::Base.time_zone_aware_attributes = old_tz @connection.reconnect! end end diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index f61f196c71..de724486c2 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -7,6 +7,8 @@ require 'active_record/connection_adapters/postgresql_adapter' class PostgresqlHstoreTest < ActiveRecord::TestCase class Hstore < ActiveRecord::Base self.table_name = 'hstores' + + store_accessor :settings, :language, :timezone end def setup @@ -26,6 +28,7 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase @connection.transaction do @connection.create_table('hstores') do |t| t.hstore 'tags', :default => '' + t.hstore 'settings' end end @column = Hstore.columns.find { |c| c.name == 'tags' } @@ -90,6 +93,24 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast(%q(c=>"}", "\"a\""=>"b \"a b"))) end + def test_with_store_accessors + x = Hstore.new(language: "fr", timezone: "GMT") + assert_equal "fr", x.language + assert_equal "GMT", x.timezone + + x.save! + x = Hstore.first + assert_equal "fr", x.language + assert_equal "GMT", x.timezone + + x.language = "de" + x.save! + + x = Hstore.first + assert_equal "de", x.language + assert_equal "GMT", x.timezone + end + def test_gen1 assert_equal(%q(" "=>""), @column.class.hstore_to_string({' '=>''})) end diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index adac1d3c13..c33c7ef968 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -7,6 +7,8 @@ require 'active_record/connection_adapters/postgresql_adapter' class PostgresqlJSONTest < ActiveRecord::TestCase class JsonDataType < ActiveRecord::Base self.table_name = 'json_data_type' + + store_accessor :settings, :resolution end def setup @@ -15,6 +17,7 @@ class PostgresqlJSONTest < ActiveRecord::TestCase @connection.transaction do @connection.create_table('json_data_type') do |t| t.json 'payload', :default => {} + t.json 'settings' end end rescue ActiveRecord::StatementInvalid @@ -46,6 +49,13 @@ class PostgresqlJSONTest < ActiveRecord::TestCase JsonDataType.reset_column_information end + def test_cast_value_on_write + x = JsonDataType.new payload: {"string" => "foo", :symbol => :bar} + assert_equal({"string" => "foo", "symbol" => "bar"}, x.payload) + x.save + assert_equal({"string" => "foo", "symbol" => "bar"}, x.reload.payload) + end + def test_type_cast_json assert @column @@ -96,4 +106,19 @@ class PostgresqlJSONTest < ActiveRecord::TestCase x.payload = ['v1', {'k2' => 'v2'}, 'v3'] assert x.save! end + + def test_with_store_accessors + x = JsonDataType.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + x.save! + x = JsonDataType.first + assert_equal "320×480", x.resolution + + x.resolution = "640×1136" + x.save! + + x = JsonDataType.first + assert_equal "640×1136", x.resolution + end end diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index 0cd5b420fc..a753a23c09 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -24,7 +24,7 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase @connection.reconnect! @connection.transaction do - @connection.create_table('pg_uuids', id: :uuid) do |t| + @connection.create_table('pg_uuids', id: :uuid, default: 'uuid_generate_v1()') do |t| t.string 'name' t.uuid 'other_uuid', default: 'uuid_generate_v4()' end @@ -60,7 +60,7 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase def test_schema_dumper_for_uuid_primary_key schema = StringIO.new ActiveRecord::SchemaDumper.dump(@connection, schema) - assert_match(/\bcreate_table "pg_uuids", id: :uuid\b/, schema.string) + assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: "uuid_generate_v1\(\)"/, schema.string) assert_match(/t\.uuid "other_uuid", default: "uuid_generate_v4\(\)"/, schema.string) end end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 6ba6518eaa..ce7c869eec 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -21,8 +21,8 @@ module ActiveRecord ) eosql - @conn.extend(LogIntercepter) - @conn.intercepted = true + @subscriber = SQLSubscriber.new + ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber) end def test_valid_column @@ -31,16 +31,16 @@ module ActiveRecord end # sqlite databases should be able to support any type and not - # just the ones mentioned in the native_database_types. - # Therefore test_invalid column should always return true + # just the ones mentioned in the native_database_types. + # Therefore test_invalid column should always return true # even if the type is not valid. def test_invalid_column assert @conn.valid_type?(:foobar) end def teardown - @conn.intercepted = false - @conn.logged = [] + ActiveSupport::Notifications.unsubscribe(@subscriber) + super end def test_column_types @@ -256,7 +256,7 @@ module ActiveRecord def test_tables_logs_name assert_logged [['SCHEMA', []]] do @conn.tables('hello') - assert_not_nil @conn.logged.first.shift + assert_not_nil @subscriber.logged.first.shift end end @@ -268,7 +268,7 @@ module ActiveRecord def test_table_exists_logs_name assert @conn.table_exists?('items') - assert_equal 'SCHEMA', @conn.logged[0][1] + assert_equal 'SCHEMA', @subscriber.logged[0][1] end def test_columns @@ -306,10 +306,10 @@ module ActiveRecord end def test_indexes_logs - assert_difference('@conn.logged.length') do + assert_difference('@subscriber.logged.length') do @conn.indexes('items') end - assert_match(/items/, @conn.logged.last.first) + assert_match(/items/, @subscriber.logged.last.first) end def test_no_indexes @@ -370,7 +370,7 @@ module ActiveRecord def assert_logged logs yield - assert_equal logs, @conn.logged + assert_equal logs, @subscriber.logged end end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 8d3d6962fc..498a4e8144 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -4,6 +4,7 @@ require 'models/tagging' require 'models/tag' require 'models/comment' require 'models/author' +require 'models/essay' require 'models/category' require 'models/company' require 'models/person' @@ -24,7 +25,7 @@ require 'models/categorization' require 'models/sponsor' class EagerAssociationTest < ActiveRecord::TestCase - fixtures :posts, :comments, :authors, :author_addresses, :categories, :categories_posts, + fixtures :posts, :comments, :authors, :essays, :author_addresses, :categories, :categories_posts, :companies, :accounts, :tags, :taggings, :people, :readers, :categorizations, :owners, :pets, :author_favorites, :jobs, :references, :subscribers, :subscriptions, :books, :developers, :projects, :developers_projects, :members, :memberships, :clubs, :sponsors @@ -747,6 +748,8 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_default_scope_as_block + # warm up the habtm cache + EagerDeveloperWithBlockDefaultScope.where(:name => 'David').first.projects developer = EagerDeveloperWithBlockDefaultScope.where(:name => 'David').first projects = Project.order(:id).to_a assert_no_queries do @@ -1136,6 +1139,10 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_deep_including_through_habtm + # warm up habtm cache + posts = Post.all.merge!(:includes => {:categories => :categorizations}, :order => "posts.id").to_a + posts[0].categories[0].categorizations.length + posts = Post.all.merge!(:includes => {:categories => :categorizations}, :order => "posts.id").to_a assert_no_queries { assert_equal 2, posts[0].categories[0].categorizations.length } assert_no_queries { assert_equal 1, posts[0].categories[1].categorizations.length } @@ -1179,4 +1186,12 @@ class EagerAssociationTest < ActiveRecord::TestCase author = Author.includes(:posts).references(:posts).reorder(:name).find_by('posts.title IS NOT NULL') assert_equal authors(:bob), author end + + test "preloading with a polymorphic association and using the existential predicate" do + assert_equal authors(:david), authors(:david).essays.includes(:writer).first.writer + + assert_nothing_raised do + authors(:david).essays.includes(:writer).any? + end + end end diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb index 47dff7d0ea..f8f2832ab1 100644 --- a/activerecord/test/cases/associations/extension_test.rb +++ b/activerecord/test/cases/associations/extension_test.rb @@ -75,7 +75,6 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase private def extend!(model) - builder = ActiveRecord::Associations::Builder::HasMany.new(:association_name, nil, {}) { } - builder.define_extensions(model) + ActiveRecord::Associations::Builder::HasMany.define_extensions(model, :association_name) { } end end 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 f77066e6ab..be928ec8ee 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 @@ -613,7 +613,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase 3, Developer.references(:developers_projects_join).merge( :includes => {:projects => :developers}, - :where => 'developers_projects_join.joined_on IS NOT NULL' + :where => 'projects_developers_projects_join.joined_on IS NOT NULL' ).to_a.size ) end @@ -632,7 +632,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal( 3, Developer.references(:developers_projects_join).merge( - :includes => {:projects => :developers}, :where => 'developers_projects_join.joined_on IS NOT NULL', + :includes => {:projects => :developers}, :where => 'projects_developers_projects_join.joined_on IS NOT NULL', :group => group.join(",") ).to_a.size ) @@ -646,8 +646,8 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_find_scoped_grouped - assert_equal 5, categories(:general).posts_grouped_by_title.size - assert_equal 1, categories(:technology).posts_grouped_by_title.size + assert_equal 5, categories(:general).posts_grouped_by_title.to_a.size + assert_equal 1, categories(:technology).posts_grouped_by_title.to_a.size end def test_find_scoped_grouped_having @@ -718,12 +718,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal project, developer.projects.first end - def test_self_referential_habtm_without_foreign_key_set_should_raise_exception - assert_raise(ActiveRecord::HasAndBelongsToManyAssociationForeignKeyNeeded) { - SelfMember.new.friends - } - end - def test_dynamic_find_should_respect_association_include # SQL error in sort clause if :include is not included # due to Unknown column 'authors.id' diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 064e31f634..dfc8a68e8c 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -80,6 +80,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 'exotic', bulb.name end + def test_build_from_association_should_respect_scope + author = Author.new + + post = author.thinking_posts.build + assert_equal 'So I was thinking', post.title + end + def test_create_from_association_with_nil_values_should_work car = Car.create(:name => 'honda') @@ -94,11 +101,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_do_not_call_callbacks_for_delete_all - bulb_count = Bulb.count car = Car.create(:name => 'honda') car.funky_bulbs.create! assert_nothing_raised { car.reload.funky_bulbs.delete_all } - assert_equal bulb_count + 1, Bulb.count, "bulbs should have been deleted using :nullify strategey" + assert_equal 0, Bulb.count, "bulbs should have been deleted using :delete_all strategey" end def test_building_the_associated_object_with_implicit_sti_base_class @@ -775,13 +781,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_delete_all force_signal37_to_load_all_clients_of_firm - companies(:first_firm).clients_of_firm.create("name" => "Another Client") - clients = companies(:first_firm).clients_of_firm.to_a + companies(:first_firm).dependent_clients_of_firm.create("name" => "Another Client") + clients = companies(:first_firm).dependent_clients_of_firm.to_a assert_equal 2, clients.count - deleted = companies(:first_firm).clients_of_firm.delete_all - assert_equal clients.sort_by(&:id), deleted.sort_by(&:id) - assert_equal 0, companies(:first_firm).clients_of_firm.size - assert_equal 0, companies(:first_firm).clients_of_firm(true).size + + assert_difference "Client.count", -(clients.count) do + companies(:first_firm).dependent_clients_of_firm.delete_all + end end def test_delete_all_with_not_yet_loaded_association_collection @@ -857,7 +863,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 1, firm.dependent_clients_of_firm.size assert_equal 1, Client.find_by_id(client_id).client_of - # :nullify is called on each client + # :delete_all is called on each client since the dependent options is :destroy firm.dependent_clients_of_firm.clear assert_equal 0, firm.dependent_clients_of_firm.size @@ -865,7 +871,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [], Client.destroyed_client_ids[firm.id] # Should be destroyed since the association is dependent. - assert_nil Client.find_by_id(client_id).client_of + assert_nil Client.find_by_id(client_id) end def test_delete_all_with_option_delete_all @@ -1629,6 +1635,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal car.id, bulb.attributes_after_initialize['car_id'] end + def test_attributes_are_set_when_initialized_from_has_many_null_relationship + car = Car.new name: 'honda' + bulb = car.bulbs.where(name: 'headlight').first_or_initialize + assert_equal 'headlight', bulb.name + end + + def test_attributes_are_set_when_initialized_from_polymorphic_has_many_null_relationship + post = Post.new title: 'title', body: 'bar' + tag = Tag.create!(name: 'foo') + + tagging = post.taggings.where(tag: tag).first_or_initialize + + assert_equal tag.id, tagging.tag_id + assert_equal 'Post', tagging.taggable_type + end + def test_replace car = Car.create(:name => 'honda') bulb1 = car.bulbs.create diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 5299e4e17e..c450b1beb5 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -785,7 +785,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase sarah = Person.create!(:first_name => 'Sarah', :primary_contact_id => people(:susan).id, :gender => 'F', :number1_fan_id => 1) john = Person.create!(:first_name => 'John', :primary_contact_id => sarah.id, :gender => 'M', :number1_fan_id => 1) assert_equal sarah.agents, [john] - assert_equal people(:susan).agents.map(&:agents).flatten, people(:susan).agents_of_agents + assert_equal people(:susan).agents.flat_map(&:agents), people(:susan).agents_of_agents end def test_associate_existing_with_nonstandard_primary_key_on_belongs_to @@ -1085,4 +1085,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase readers(:michael_authorless).update(first_post_id: 1) assert_equal [posts(:thinking)], person.reload.first_posts end + + def test_has_many_through_with_includes_in_through_association_scope + assert_not_empty posts(:welcome).author_address_extra_with_address + end end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 9cd4db8dc9..cdd386187b 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -524,4 +524,15 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal 'new name', pirate.ship.reload.name end + def test_has_one_autosave_with_primary_key_manually_set + post = Post.create(id: 1234, title: "Some title", body: 'Some content') + author = Author.new(id: 33, name: 'Hank Moody') + + author.post = post + author.save + author.reload + + assert_not_nil author.post + assert_equal author.post, post + end end diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 2477e60e51..893030345f 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -446,6 +446,19 @@ class InverseHasManyTests < ActiveRecord::TestCase def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.first.secret_interests } end + + def test_child_instance_should_point_to_parent_without_saving + man = Man.new + i = Interest.create(:topic => 'Industrial Revolution Re-enactment') + + man.interests << i + assert_not_nil i.man + + i.man.name = "Charles" + assert_equal i.man.name, man.name + + assert !man.persisted? + end end class InverseBelongsToTests < ActiveRecord::TestCase @@ -590,6 +603,18 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to replaced-parent-owned instance" end + def test_inversed_instance_should_not_be_reloaded_after_stale_state_changed + new_man = Man.new + face = Face.new + new_man.face = face + + old_inversed_man = face.man + new_man.save! + new_inversed_man = face.man + + assert_equal old_inversed_man.object_id, new_inversed_man.object_id + end + def test_should_not_try_to_set_inverse_instances_when_the_inverse_is_a_has_many i = interests(:llama_wrangling) m = i.polymorphic_man diff --git a/activerecord/test/cases/associations/join_dependency_test.rb b/activerecord/test/cases/associations/join_dependency_test.rb deleted file mode 100644 index 08c166dc33..0000000000 --- a/activerecord/test/cases/associations/join_dependency_test.rb +++ /dev/null @@ -1,8 +0,0 @@ -require "cases/helper" -require 'models/edge' - -class JoinDependencyTest < ActiveRecord::TestCase - def test_column_names_with_alias_handles_nil_primary_key - assert_equal Edge.column_names, ActiveRecord::Associations::JoinDependency::JoinBase.new(Edge).column_names_with_alias.map(&:first) - end -end
\ No newline at end of file diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index cf3c07845c..8ef351cda8 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -214,7 +214,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase end def test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection_preload - authors = assert_queries(3) { Author.includes(:post_categories).to_a.sort_by(&:id) } + authors = assert_queries(4) { Author.includes(:post_categories).to_a.sort_by(&:id) } general, cooking = categories(:general), categories(:cooking) assert_no_queries do @@ -242,7 +242,8 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase end def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection_preload - categories = assert_queries(3) { Category.includes(:post_comments).to_a.sort_by(&:id) } + Category.includes(:post_comments).to_a # preheat cache + categories = assert_queries(4) { Category.includes(:post_comments).to_a.sort_by(&:id) } greetings, more = comments(:greetings), comments(:more_greetings) assert_no_queries do @@ -270,7 +271,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase end def test_has_many_through_has_many_with_has_many_through_habtm_source_reflection_preload - authors = assert_queries(5) { Author.includes(:category_post_comments).to_a.sort_by(&:id) } + authors = assert_queries(6) { Author.includes(:category_post_comments).to_a.sort_by(&:id) } greetings, more = comments(:greetings), comments(:more_greetings) assert_no_queries do diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 635278abc1..517d2674a7 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -1440,10 +1440,6 @@ class TestAutosaveAssociationValidationMethodsGeneration < ActiveRecord::TestCas test "should generate validation methods for HABTM associations with :validate => true" do assert_respond_to @pirate, :validate_associated_records_for_parrots end - - test "should not generate validation methods for HABTM associations without :validate => true" do - assert !@pirate.respond_to?(:validate_associated_records_for_non_validated_parrots) - end end class TestAutosaveAssociationWithTouch < ActiveRecord::TestCase diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 15827133a8..2d6979238e 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -78,22 +78,6 @@ end class BasicsTest < ActiveRecord::TestCase fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts - def setup - ActiveRecord::Base.time_zone_aware_attributes = false - ActiveRecord::Base.default_timezone = :local - Time.zone = nil - end - - def test_generated_methods_modules - modules = Computer.ancestors - assert modules.include?(Computer::GeneratedAssociationMethods) - assert_equal(Computer::GeneratedAssociationMethods, Computer.generated_association_methods) - assert(modules.index(Computer.generated_attribute_methods) > modules.index(Computer.generated_association_methods), - "generated_attribute_methods must be higher in inheritance hierarchy than generated_association_methods") - assert_not_equal Computer.generated_association_methods, Post.generated_association_methods - assert(modules.index(Computer.generated_attribute_methods) < modules.index(ActiveRecord::Base.ancestors[1])) - end - def test_column_names_are_escaped conn = ActiveRecord::Base.connection classname = conn.class.name[/[^:]*$/] @@ -234,7 +218,7 @@ class BasicsTest < ActiveRecord::TestCase def test_preserving_time_objects_with_local_time_conversion_to_default_timezone_utc with_env_tz 'America/New_York' do - with_active_record_default_timezone :utc do + with_timezone_config default: :utc do time = Time.local(2000) topic = Topic.create('written_on' => time) saved_time = Topic.find(topic.id).reload.written_on @@ -247,7 +231,7 @@ class BasicsTest < ActiveRecord::TestCase def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_utc with_env_tz 'America/New_York' do - with_active_record_default_timezone :utc do + with_timezone_config default: :utc do Time.use_zone 'Central Time (US & Canada)' do time = Time.zone.local(2000) topic = Topic.create('written_on' => time) @@ -262,18 +246,20 @@ class BasicsTest < ActiveRecord::TestCase def test_preserving_time_objects_with_utc_time_conversion_to_default_timezone_local with_env_tz 'America/New_York' do - time = Time.utc(2000) - topic = Topic.create('written_on' => time) - saved_time = Topic.find(topic.id).reload.written_on - assert_equal time, saved_time - assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "UTC"], time.to_a - assert_equal [0, 0, 19, 31, 12, 1999, 5, 365, false, "EST"], saved_time.to_a + with_timezone_config default: :local do + time = Time.utc(2000) + topic = Topic.create('written_on' => time) + saved_time = Topic.find(topic.id).reload.written_on + assert_equal time, saved_time + assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "UTC"], time.to_a + assert_equal [0, 0, 19, 31, 12, 1999, 5, 365, false, "EST"], saved_time.to_a + end end end def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_local with_env_tz 'America/New_York' do - with_active_record_default_timezone :local do + with_timezone_config default: :local do Time.use_zone 'Central Time (US & Canada)' do time = Time.zone.local(2000) topic = Topic.create('written_on' => time) @@ -493,25 +479,25 @@ class BasicsTest < ActiveRecord::TestCase # Oracle, and Sybase do not have a TIME datatype. unless current_adapter?(:OracleAdapter, :SybaseAdapter) def test_utc_as_time_zone - Topic.default_timezone = :utc - attributes = { "bonus_time" => "5:42:00AM" } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.utc(2000, 1, 1, 5, 42, 0), topic.bonus_time - Topic.default_timezone = :local + with_timezone_config default: :utc do + attributes = { "bonus_time" => "5:42:00AM" } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.utc(2000, 1, 1, 5, 42, 0), topic.bonus_time + end end def test_utc_as_time_zone_and_new - Topic.default_timezone = :utc - attributes = { "bonus_time(1i)"=>"2000", - "bonus_time(2i)"=>"1", - "bonus_time(3i)"=>"1", - "bonus_time(4i)"=>"10", - "bonus_time(5i)"=>"35", - "bonus_time(6i)"=>"50" } - topic = Topic.new(attributes) - assert_equal Time.utc(2000, 1, 1, 10, 35, 50), topic.bonus_time - Topic.default_timezone = :local + with_timezone_config default: :utc do + attributes = { "bonus_time(1i)"=>"2000", + "bonus_time(2i)"=>"1", + "bonus_time(3i)"=>"1", + "bonus_time(4i)"=>"10", + "bonus_time(5i)"=>"35", + "bonus_time(6i)"=>"50" } + topic = Topic.new(attributes) + assert_equal Time.utc(2000, 1, 1, 10, 35, 50), topic.bonus_time + end end end @@ -558,6 +544,19 @@ class BasicsTest < ActiveRecord::TestCase assert_equal [ Topic.find(1) ], [ Topic.find(2).topic ] & [ Topic.find(1) ] end + def test_successful_comparison_of_like_class_records + topic_1 = Topic.create! + topic_2 = Topic.create! + + assert_equal [topic_2, topic_1].sort, [topic_1, topic_2] + end + + def test_failed_comparison_of_unlike_class_records + assert_raises ArgumentError do + [ topics(:first), posts(:welcome) ].sort + end + end + def test_create_without_prepared_statement topic = Topic.connection.unprepared_statement do Topic.create(:title => 'foo') @@ -566,6 +565,15 @@ class BasicsTest < ActiveRecord::TestCase assert_equal topic, Topic.find(topic.id) end + def test_destroy_without_prepared_statement + topic = Topic.create(title: 'foo') + Topic.connection.unprepared_statement do + Topic.find(topic.id).destroy + end + + assert_equal nil, Topic.find_by_id(topic.id) + end + def test_blank_ids one = Subscriber.new(:id => '') two = Subscriber.new(:id => '') @@ -599,10 +607,25 @@ class BasicsTest < ActiveRecord::TestCase end def test_unicode_column_name + Weird.reset_column_information weird = Weird.create(:なまえ => 'たこ焼き仮面') assert_equal 'たこ焼き仮面', weird.なまえ end + def test_respect_internal_encoding + if current_adapter?(:PostgreSQLAdapter) + skip 'pg does not respect internal encoding and always returns utf8' + end + old_default_internal = Encoding.default_internal + silence_warnings { Encoding.default_internal = "EUC-JP" } + + Weird.reset_column_information + + assert_equal ["EUC-JP"], Weird.columns.map {|c| c.name.encoding.name }.uniq + ensure + silence_warnings { Encoding.default_internal = old_default_internal } + end + def test_non_valid_identifier_column_name weird = Weird.create('a$b' => 'value') weird.reload @@ -625,12 +648,14 @@ class BasicsTest < ActiveRecord::TestCase # Oracle, and Sybase do not have a TIME datatype. return true if current_adapter?(:OracleAdapter, :SybaseAdapter) - attributes = { - "bonus_time" => "5:42:00AM" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.local(2000, 1, 1, 5, 42, 0), topic.bonus_time + with_timezone_config default: :local do + attributes = { + "bonus_time" => "5:42:00AM" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.local(2000, 1, 1, 5, 42, 0), topic.bonus_time + end end def test_attributes_on_dummy_time_with_invalid_time @@ -818,19 +843,18 @@ class BasicsTest < ActiveRecord::TestCase # TODO: extend defaults tests to other databases! if current_adapter?(:PostgreSQLAdapter) def test_default - tz = Default.default_timezone - Default.default_timezone = :local - default = Default.new - Default.default_timezone = tz - - # fixed dates / times - assert_equal Date.new(2004, 1, 1), default.fixed_date - assert_equal Time.local(2004, 1,1,0,0,0,0), default.fixed_time - - # char types - assert_equal 'Y', default.char1 - assert_equal 'a varchar field', default.char2 - assert_equal 'a text field', default.char3 + with_timezone_config default: :local do + default = Default.new + + # fixed dates / times + assert_equal Date.new(2004, 1, 1), default.fixed_date + assert_equal Time.local(2004, 1,1,0,0,0,0), default.fixed_time + + # char types + assert_equal 'Y', default.char1 + assert_equal 'a varchar field', default.char2 + assert_equal 'a text field', default.char3 + end end class Geometric < ActiveRecord::Base; end @@ -1339,6 +1363,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_marshal_between_processes + skip "can't marshal between processes when using an in-memory db" if in_memory_db? skip "fork isn't supported" unless Process.respond_to?(:fork) # Define a new model to ensure there are no caches diff --git a/activerecord/test/cases/column_test.rb b/activerecord/test/cases/column_test.rb index 5ab2f18e9d..2a6d8cc2ab 100644 --- a/activerecord/test/cases/column_test.rb +++ b/activerecord/test/cases/column_test.rb @@ -112,13 +112,11 @@ module ActiveRecord end def test_string_to_time_with_timezone - old = ActiveRecord::Base.default_timezone [:utc, :local].each do |zone| - ActiveRecord::Base.default_timezone = zone - assert_equal Time.utc(2013, 9, 4, 0, 0, 0), Column.string_to_time("Wed, 04 Sep 2013 03:00:00 EAT") + with_timezone_config default: zone do + assert_equal Time.utc(2013, 9, 4, 0, 0, 0), Column.string_to_time("Wed, 04 Sep 2013 03:00:00 EAT") + end end - rescue - ActiveRecord::Base.default_timezone = old end end end diff --git a/activerecord/test/cases/date_time_test.rb b/activerecord/test/cases/date_time_test.rb index 427076bd80..c0491bbee5 100644 --- a/activerecord/test/cases/date_time_test.rb +++ b/activerecord/test/cases/date_time_test.rb @@ -5,7 +5,7 @@ require 'models/task' class DateTimeTest < ActiveRecord::TestCase def test_saves_both_date_and_time with_env_tz 'America/New_York' do - with_active_record_default_timezone :utc do + with_timezone_config default: :utc do time_values = [1807, 2, 10, 15, 30, 45] # create DateTime value with local time zone offset local_offset = Rational(Time.local(*time_values).utc_offset, 86400) diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index b277ef0317..9d7f57bf85 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -125,30 +125,30 @@ class DirtyTest < ActiveRecord::TestCase end def test_time_attributes_changes_without_time_zone - target = Class.new(ActiveRecord::Base) - target.table_name = 'pirates' + with_timezone_config aware_attributes: false do + target = Class.new(ActiveRecord::Base) + target.table_name = 'pirates' - target.time_zone_aware_attributes = false + # New record - no changes. + pirate = target.new + assert !pirate.created_on_changed? + assert_nil pirate.created_on_change - # New record - no changes. - pirate = target.new - assert !pirate.created_on_changed? - assert_nil pirate.created_on_change + # Saved - no changes. + pirate.catchphrase = 'arrrr, time zone!!' + pirate.save! + assert !pirate.created_on_changed? + assert_nil pirate.created_on_change - # Saved - no changes. - pirate.catchphrase = 'arrrr, time zone!!' - pirate.save! - assert !pirate.created_on_changed? - assert_nil pirate.created_on_change - - # Change created_on. - old_created_on = pirate.created_on - pirate.created_on = Time.now + 1.day - assert pirate.created_on_changed? - # kind_of does not work because - # ActiveSupport::TimeWithZone.name == 'Time' - assert_instance_of Time, pirate.created_on_was - assert_equal old_created_on, pirate.created_on_was + # Change created_on. + old_created_on = pirate.created_on + pirate.created_on = Time.now + 1.day + assert pirate.created_on_changed? + # kind_of does not work because + # ActiveSupport::TimeWithZone.name == 'Time' + assert_instance_of Time, pirate.created_on_was + assert_equal old_created_on, pirate.created_on_was + end end diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb new file mode 100644 index 0000000000..8eb82ead3c --- /dev/null +++ b/activerecord/test/cases/enum_test.rb @@ -0,0 +1,38 @@ +require 'cases/helper' +require 'models/book' + +class EnumTest < ActiveRecord::TestCase + fixtures :books + + setup do + @book = books(:awdr) + end + + test "query state by predicate" do + assert @book.proposed? + assert_not @book.written? + assert_not @book.published? + + assert @book.unread? + end + + test "query state with symbol" do + assert_equal :proposed, @book.status + assert_equal :unread, @book.read_status + end + + test "find via scope" do + assert_equal @book, Book.proposed.first + assert_equal @book, Book.unread.first + end + + test "update by declaration" do + @book.written! + assert @book.written? + end + + test "update by setter" do + @book.update! status: :written + assert @book.written? + end +end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 1b9ef14ec9..4188b32731 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -11,6 +11,7 @@ require 'models/project' require 'models/developer' require 'models/customer' require 'models/toy' +require 'models/matey' class FinderTest < ActiveRecord::TestCase fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers, :categories, :categorizations @@ -478,7 +479,7 @@ class FinderTest < ActiveRecord::TestCase def test_condition_utc_time_interpolation_with_default_timezone_local with_env_tz 'America/New_York' do - with_active_record_default_timezone :local do + with_timezone_config default: :local do topic = Topic.first assert_equal topic, Topic.all.merge!(:where => ['written_on = ?', topic.written_on.getutc]).first end @@ -487,7 +488,7 @@ class FinderTest < ActiveRecord::TestCase def test_hash_condition_utc_time_interpolation_with_default_timezone_local with_env_tz 'America/New_York' do - with_active_record_default_timezone :local do + with_timezone_config default: :local do topic = Topic.first assert_equal topic, Topic.all.merge!(:where => {:written_on => topic.written_on.getutc}).first end @@ -496,7 +497,7 @@ class FinderTest < ActiveRecord::TestCase def test_condition_local_time_interpolation_with_default_timezone_utc with_env_tz 'America/New_York' do - with_active_record_default_timezone :utc do + with_timezone_config default: :utc do topic = Topic.first assert_equal topic, Topic.all.merge!(:where => ['written_on = ?', topic.written_on.getlocal]).first end @@ -505,7 +506,7 @@ class FinderTest < ActiveRecord::TestCase def test_hash_condition_local_time_interpolation_with_default_timezone_utc with_env_tz 'America/New_York' do - with_active_record_default_timezone :utc do + with_timezone_config default: :utc do topic = Topic.first assert_equal topic, Topic.all.merge!(:where => {:written_on => topic.written_on.getlocal}).first end @@ -860,6 +861,12 @@ class FinderTest < ActiveRecord::TestCase Toy.reset_primary_key end + def test_find_without_primary_key + assert_raises(ActiveRecord::UnknownPrimaryKey) do + Matey.find(1) + end + end + def test_finder_with_offset_string assert_nothing_raised(ActiveRecord::StatementInvalid) { Topic.all.merge!(:offset => "3").to_a } end diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index e61deb1080..bffff07089 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -253,7 +253,8 @@ class FixturesTest < ActiveRecord::TestCase end def test_fixtures_are_set_up_with_database_env_variable - ENV.stubs(:[]).with("DATABASE_URL").returns("sqlite3:///:memory:") + db_url_tmp = ENV['DATABASE_URL'] + ENV['DATABASE_URL'] = "sqlite3:///:memory:" ActiveRecord::Base.stubs(:configurations).returns({}) test_case = Class.new(ActiveRecord::TestCase) do fixtures :accounts @@ -266,6 +267,8 @@ class FixturesTest < ActiveRecord::TestCase result = test_case.new(:test_fixtures).run assert result.passed?, "Expected #{result.name} to pass:\n#{result}" + ensure + ENV['DATABASE_URL'] = db_url_tmp end end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index f96978aff8..34e8f1be0f 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -49,11 +49,58 @@ ensure old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') end -def with_active_record_default_timezone(zone) - old_zone, ActiveRecord::Base.default_timezone = ActiveRecord::Base.default_timezone, zone +def with_timezone_config(cfg) + verify_default_timezone_config + + old_default_zone = ActiveRecord::Base.default_timezone + old_awareness = ActiveRecord::Base.time_zone_aware_attributes + old_zone = Time.zone + + if cfg.has_key?(:default) + ActiveRecord::Base.default_timezone = cfg[:default] + end + if cfg.has_key?(:aware_attributes) + ActiveRecord::Base.time_zone_aware_attributes = cfg[:aware_attributes] + end + if cfg.has_key?(:zone) + Time.zone = cfg[:zone] + end yield ensure - ActiveRecord::Base.default_timezone = old_zone + ActiveRecord::Base.default_timezone = old_default_zone + ActiveRecord::Base.time_zone_aware_attributes = old_awareness + Time.zone = old_zone +end + +# This method makes sure that tests don't leak global state related to time zones. +EXPECTED_ZONE = nil +EXPECTED_DEFAULT_TIMEZONE = :utc +EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES = false +def verify_default_timezone_config + if Time.zone != EXPECTED_ZONE + $stderr.puts <<-MSG +\n#{self.to_s} + Global state `Time.zone` was leaked. + Expected: #{EXPECTED_ZONE} + Got: #{Time.zone} + MSG + end + if ActiveRecord::Base.default_timezone != EXPECTED_DEFAULT_TIMEZONE + $stderr.puts <<-MSG +\n#{self.to_s} + Global state `ActiveRecord::Base.default_timezone` was leaked. + Expected: #{EXPECTED_DEFAULT_TIMEZONE} + Got: #{ActiveRecord::Base.default_timezone} + MSG + end + if ActiveRecord::Base.time_zone_aware_attributes != EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES + $stderr.puts <<-MSG +\n#{self.to_s} + Global state `ActiveRecord::Base.time_zone_aware_attributes` was leaked. + Expected: #{EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES} + Got: #{ActiveRecord::Base.time_zone_aware_attributes} + MSG + end end unless ENV['FIXTURE_DEBUG'] @@ -93,7 +140,7 @@ def load_schema load SCHEMA_ROOT + "/schema.rb" - if File.exists?(adapter_specific_schema_file) + if File.exist?(adapter_specific_schema_file) load adapter_specific_schema_file end ensure @@ -119,21 +166,24 @@ class << Time end end -module LogIntercepter - attr_accessor :logged, :intercepted - def self.extended(base) - base.logged = [] - end - def log(sql, name = 'SQL', binds = [], &block) - if @intercepted - @logged << [sql, name, binds] - yield - else - super(sql, name,binds, &block) - end +class SQLSubscriber + attr_reader :logged + attr_reader :payloads + + def initialize + @logged = [] + @payloads = [] end + + def start(name, id, payload) + @payloads << payload + @logged << [payload[:sql], payload[:name], payload[:binds]] + end + + def finish(name, id, payload); end end + module InTimeZone private diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index a9be132801..73cf99a5d7 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -313,8 +313,12 @@ class InheritanceTest < ActiveRecord::TestCase assert_kind_of SpecialSubscriber, SpecialSubscriber.find("webster132") assert_nothing_raised { s = SpecialSubscriber.new("name" => "And breaaaaathe!"); s.id = 'roger'; s.save } end -end + def test_scope_inherited_properly + assert_nothing_raised { Company.of_first_firm } + assert_nothing_raised { Client.of_first_firm } + end +end class InheritanceComputeTypeTest < ActiveRecord::TestCase fixtures :companies diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb index f5daca2fa8..840865c4cf 100644 --- a/activerecord/test/cases/integration_test.rb +++ b/activerecord/test/cases/integration_test.rb @@ -3,9 +3,10 @@ require 'models/company' require 'models/developer' require 'models/car' require 'models/bulb' +require 'models/owner' class IntegrationTest < ActiveRecord::TestCase - fixtures :companies, :developers + fixtures :companies, :developers, :owners def test_to_param_should_return_string assert_kind_of String, Client.first.to_param @@ -23,17 +24,12 @@ class IntegrationTest < ActiveRecord::TestCase end def test_cache_key_for_existing_record_is_not_timezone_dependent - ActiveRecord::Base.time_zone_aware_attributes = true - - Time.zone = 'UTC' utc_key = Developer.first.cache_key - Time.zone = 'EST' - est_key = Developer.first.cache_key - - assert_equal utc_key, est_key - ensure - Time.zone = 'UTC' + with_timezone_config zone: "EST" do + est_key = Developer.first.cache_key + assert_equal utc_key, est_key + end end def test_cache_key_format_for_existing_record_with_updated_at @@ -86,4 +82,9 @@ class IntegrationTest < ActiveRecord::TestCase dev.touch assert_not_equal key, dev.cache_key end + + def test_named_timestamps_for_cache_key + owner = owners(:blackbeard) + assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:nsec)}", owner.cache_key(:updated_at, :happy_at) + end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 931caff727..acfde2a27a 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -311,14 +311,23 @@ class MigrationTest < ActiveRecord::TestCase end def test_schema_migrations_table_name + original_schema_migrations_table_name = ActiveRecord::Migrator.schema_migrations_table_name + + assert_equal "schema_migrations", ActiveRecord::Migrator.schema_migrations_table_name ActiveRecord::Base.table_name_prefix = "prefix_" ActiveRecord::Base.table_name_suffix = "_suffix" Reminder.reset_table_name assert_equal "prefix_schema_migrations_suffix", ActiveRecord::Migrator.schema_migrations_table_name + ActiveRecord::Base.schema_migrations_table_name = "changed" + Reminder.reset_table_name + assert_equal "prefix_changed_suffix", ActiveRecord::Migrator.schema_migrations_table_name ActiveRecord::Base.table_name_prefix = "" ActiveRecord::Base.table_name_suffix = "" Reminder.reset_table_name - assert_equal "schema_migrations", ActiveRecord::Migrator.schema_migrations_table_name + assert_equal "changed", ActiveRecord::Migrator.schema_migrations_table_name + ensure + ActiveRecord::Base.schema_migrations_table_name = original_schema_migrations_table_name + Reminder.reset_table_name end def test_proper_table_name_on_migrator @@ -701,8 +710,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase @existing_migrations = Dir[@migrations_path + "/*.rb"] copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy"}) - assert File.exists?(@migrations_path + "/4_people_have_hobbies.bukkits.rb") - assert File.exists?(@migrations_path + "/5_people_have_descriptions.bukkits.rb") + assert File.exist?(@migrations_path + "/4_people_have_hobbies.bukkits.rb") + assert File.exist?(@migrations_path + "/5_people_have_descriptions.bukkits.rb") assert_equal [@migrations_path + "/4_people_have_hobbies.bukkits.rb", @migrations_path + "/5_people_have_descriptions.bukkits.rb"], copied.map(&:filename) expected = "# This migration comes from bukkits (originally 1)" @@ -725,10 +734,10 @@ class CopyMigrationsTest < ActiveRecord::TestCase sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy" sources[:omg] = MIGRATIONS_ROOT + "/to_copy2" ActiveRecord::Migration.copy(@migrations_path, sources) - assert File.exists?(@migrations_path + "/4_people_have_hobbies.bukkits.rb") - assert File.exists?(@migrations_path + "/5_people_have_descriptions.bukkits.rb") - assert File.exists?(@migrations_path + "/6_create_articles.omg.rb") - assert File.exists?(@migrations_path + "/7_create_comments.omg.rb") + assert File.exist?(@migrations_path + "/4_people_have_hobbies.bukkits.rb") + assert File.exist?(@migrations_path + "/5_people_have_descriptions.bukkits.rb") + assert File.exist?(@migrations_path + "/6_create_articles.omg.rb") + assert File.exist?(@migrations_path + "/7_create_comments.omg.rb") files_count = Dir[@migrations_path + "/*.rb"].length ActiveRecord::Migration.copy(@migrations_path, sources) @@ -743,8 +752,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) - assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") - assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") + assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") + assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") expected = [@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb", @migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb"] assert_equal expected, copied.map(&:filename) @@ -768,10 +777,10 @@ class CopyMigrationsTest < ActiveRecord::TestCase Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do copied = ActiveRecord::Migration.copy(@migrations_path, sources) - assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") - assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") - assert File.exists?(@migrations_path + "/20100726101012_create_articles.omg.rb") - assert File.exists?(@migrations_path + "/20100726101013_create_comments.omg.rb") + assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") + assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") + assert File.exist?(@migrations_path + "/20100726101012_create_articles.omg.rb") + assert File.exist?(@migrations_path + "/20100726101013_create_comments.omg.rb") assert_equal 4, copied.length files_count = Dir[@migrations_path + "/*.rb"].length @@ -788,8 +797,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase Time.travel_to(Time.utc(2010, 2, 20, 10, 10, 10)) do ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) - assert File.exists?(@migrations_path + "/20100301010102_people_have_hobbies.bukkits.rb") - assert File.exists?(@migrations_path + "/20100301010103_people_have_descriptions.bukkits.rb") + assert File.exist?(@migrations_path + "/20100301010102_people_have_hobbies.bukkits.rb") + assert File.exist?(@migrations_path + "/20100301010103_people_have_descriptions.bukkits.rb") files_count = Dir[@migrations_path + "/*.rb"].length copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) @@ -806,7 +815,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase @existing_migrations = Dir[@migrations_path + "/*.rb"] copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/magic"}) - assert File.exists?(@migrations_path + "/4_currencies_have_symbols.bukkits.rb") + assert File.exist?(@migrations_path + "/4_currencies_have_symbols.bukkits.rb") assert_equal [@migrations_path + "/4_currencies_have_symbols.bukkits.rb"], copied.map(&:filename) expected = "# coding: ISO-8859-15\n# This migration comes from bukkits (originally 1)" @@ -863,8 +872,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) - assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") - assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") + assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") + assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") assert_equal 2, copied.length end ensure @@ -878,8 +887,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) - assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") - assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") + assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") + assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") assert_equal 2, copied.length end ensure diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb index 08b3408665..9124105e6d 100644 --- a/activerecord/test/cases/modules_test.rb +++ b/activerecord/test/cases/modules_test.rb @@ -1,6 +1,7 @@ require "cases/helper" require 'models/company_in_module' require 'models/shop' +require 'models/developer' class ModulesTest < ActiveRecord::TestCase fixtures :accounts, :companies, :projects, :developers, :collections, :products, :variants diff --git a/activerecord/test/cases/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb index ce21760645..b82409bfbe 100644 --- a/activerecord/test/cases/multiparameter_attributes_test.rb +++ b/activerecord/test/cases/multiparameter_attributes_test.rb @@ -5,16 +5,6 @@ require 'models/customer' class MultiParameterAttributeTest < ActiveRecord::TestCase fixtures :topics - def setup - ActiveRecord::Base.time_zone_aware_attributes = false - ActiveRecord::Base.default_timezone = :local - Time.zone = nil - end - - def teardown - ActiveRecord::Base.default_timezone = :utc - end - def test_multiparameter_attributes_on_date attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "24" } topic = Topic.find(1) @@ -86,13 +76,15 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase end def test_multiparameter_attributes_on_time - attributes = { - "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", - "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on + with_timezone_config default: :local do + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on + end end def test_multiparameter_attributes_on_time_with_no_date @@ -152,13 +144,15 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase end def test_multiparameter_attributes_on_time_will_ignore_hour_if_missing - attributes = { - "written_on(1i)" => "2004", "written_on(2i)" => "12", "written_on(3i)" => "12", - "written_on(5i)" => "12", "written_on(6i)" => "02" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.local(2004, 12, 12, 0, 12, 2), topic.written_on + with_timezone_config default: :local do + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "12", "written_on(3i)" => "12", + "written_on(5i)" => "12", "written_on(6i)" => "02" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.local(2004, 12, 12, 0, 12, 2), topic.written_on + end end def test_multiparameter_attributes_on_time_will_ignore_hour_if_blank @@ -180,6 +174,7 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase topic.attributes = attributes assert_nil topic.written_on end + def test_multiparameter_attributes_on_time_with_seconds_will_ignore_date_if_empty attributes = { "written_on(1i)" => "", "written_on(2i)" => "", "written_on(3i)" => "", @@ -191,56 +186,56 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase end def test_multiparameter_attributes_on_time_with_utc - ActiveRecord::Base.default_timezone = :utc - attributes = { - "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", - "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on + with_timezone_config default: :utc do + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on + end end def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes - ActiveRecord::Base.time_zone_aware_attributes = true - ActiveRecord::Base.default_timezone = :utc - Time.zone = ActiveSupport::TimeZone[-28800] - attributes = { - "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", - "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.utc(2004, 6, 24, 23, 24, 0), topic.written_on - assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on.time - assert_equal Time.zone, topic.written_on.time_zone + with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.utc(2004, 6, 24, 23, 24, 0), topic.written_on + assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on.time + assert_equal Time.zone, topic.written_on.time_zone + end end def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes_false - Time.zone = ActiveSupport::TimeZone[-28800] - attributes = { - "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", - "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on - assert_equal false, topic.written_on.respond_to?(:time_zone) + with_timezone_config default: :local, aware_attributes: false, zone: -28800 do + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on + assert_equal false, topic.written_on.respond_to?(:time_zone) + end end def test_multiparameter_attributes_on_time_with_skip_time_zone_conversion_for_attributes - ActiveRecord::Base.time_zone_aware_attributes = true - ActiveRecord::Base.default_timezone = :utc - Time.zone = ActiveSupport::TimeZone[-28800] - Topic.skip_time_zone_conversion_for_attributes = [:written_on] - attributes = { - "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", - "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on - assert_equal false, topic.written_on.respond_to?(:time_zone) + with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do + Topic.skip_time_zone_conversion_for_attributes = [:written_on] + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on + assert_equal false, topic.written_on.respond_to?(:time_zone) + end ensure Topic.skip_time_zone_conversion_for_attributes = [] end @@ -248,30 +243,31 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase # Oracle, and Sybase do not have a TIME datatype. unless current_adapter?(:OracleAdapter, :SybaseAdapter) def test_multiparameter_attributes_on_time_only_column_with_time_zone_aware_attributes_does_not_do_time_zone_conversion - ActiveRecord::Base.time_zone_aware_attributes = true - ActiveRecord::Base.default_timezone = :utc - Time.zone = ActiveSupport::TimeZone[-28800] + with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do + attributes = { + "bonus_time(1i)" => "2000", "bonus_time(2i)" => "1", "bonus_time(3i)" => "1", + "bonus_time(4i)" => "16", "bonus_time(5i)" => "24" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.utc(2000, 1, 1, 16, 24, 0), topic.bonus_time + assert topic.bonus_time.utc? + end + end + end + + def test_multiparameter_attributes_on_time_with_empty_seconds + with_timezone_config default: :local do attributes = { - "bonus_time(1i)" => "2000", "bonus_time(2i)" => "1", "bonus_time(3i)" => "1", - "bonus_time(4i)" => "16", "bonus_time(5i)" => "24" + "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "" } topic = Topic.find(1) topic.attributes = attributes - assert_equal Time.utc(2000, 1, 1, 16, 24, 0), topic.bonus_time - assert topic.bonus_time.utc? + assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on end end - def test_multiparameter_attributes_on_time_with_empty_seconds - attributes = { - "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", - "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on - end - def test_multiparameter_attributes_setting_time_attribute return skip "Oracle does not have TIME data type" if current_adapter? :OracleAdapter diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index 44b2064110..e2439b9a24 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -53,28 +53,28 @@ module ActiveRecord end def test_quoted_time_utc - with_active_record_default_timezone :utc do + with_timezone_config default: :utc do t = Time.now assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t) end end def test_quoted_time_local - with_active_record_default_timezone :local do + with_timezone_config default: :local do t = Time.now assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t) end end def test_quoted_time_crazy - with_active_record_default_timezone :asdfasdf do + with_timezone_config default: :asdfasdf do t = Time.now assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t) end end def test_quoted_datetime_utc - with_active_record_default_timezone :utc do + with_timezone_config default: :utc do t = DateTime.now assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t) end @@ -83,7 +83,7 @@ module ActiveRecord ### # DateTime doesn't define getlocal, so make sure it does nothing def test_quoted_datetime_local - with_active_record_default_timezone :local do + with_timezone_config default: :local do t = DateTime.now assert_equal t.to_s(:db), @quoter.quoted_date(t) end diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 0e5c7df2cc..d7ad5ed29f 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -18,6 +18,11 @@ require 'models/subscription' require 'models/tag' require 'models/sponsor' require 'models/edge' +require 'models/hotel' +require 'models/chef' +require 'models/department' +require 'models/cake_designer' +require 'models/drink_designer' class ReflectionTest < ActiveRecord::TestCase include ActiveRecord::Reflection @@ -227,6 +232,17 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal expected, actual end + def test_scope_chain_does_not_interfere_with_hmt_with_polymorphic_case + @hotel = Hotel.create! + @department = @hotel.departments.create! + @department.chefs.create!(employable: CakeDesigner.create!) + @department.chefs.create!(employable: DrinkDesigner.create!) + + assert_equal 1, @hotel.cake_designers.size + assert_equal 1, @hotel.drink_designers.size + assert_equal 2, @hotel.chefs.size + end + def test_nested? assert !Author.reflect_on_association(:comments).nested? assert Author.reflect_on_association(:tags).nested? diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index 71ade0bcc2..c171c5e14e 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -9,10 +9,11 @@ module ActiveRecord def assert_responds(target, method) assert target.respond_to?(method) assert_nothing_raised do - case target.to_a.method(method).arity - when 0 + method_arity = target.to_a.method(method).arity + + if method_arity.zero? target.send(method) - when -1 + elsif method_arity < 0 if method == :shuffle! target.send(method) else diff --git a/activerecord/test/cases/relation/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb index 92d1e013e8..d44b4dfe3d 100644 --- a/activerecord/test/cases/relation/where_chain_test.rb +++ b/activerecord/test/cases/relation/where_chain_test.rb @@ -76,5 +76,35 @@ module ActiveRecord expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], 'ruby on rails') assert_equal(expected, relation.where_values[1]) end + + def test_rewhere_with_one_condition + relation = Post.where(title: 'hello').where(title: 'world').rewhere(title: 'alone') + + expected = Arel::Nodes::Equality.new(Post.arel_table[@name], 'alone') + assert_equal 1, relation.where_values.size + assert_equal expected, relation.where_values.first + end + + def test_rewhere_with_multiple_overwriting_conditions + relation = Post.where(title: 'hello').where(body: 'world').rewhere(title: 'alone', body: 'again') + + title_expected = Arel::Nodes::Equality.new(Post.arel_table['title'], 'alone') + body_expected = Arel::Nodes::Equality.new(Post.arel_table['body'], 'again') + + assert_equal 2, relation.where_values.size + assert_equal title_expected, relation.where_values.first + assert_equal body_expected, relation.where_values.second + end + + def test_rewhere_with_one_overwriting_condition_and_one_unrelated + relation = Post.where(title: 'hello').where(body: 'world').rewhere(title: 'alone') + + title_expected = Arel::Nodes::Equality.new(Post.arel_table['title'], 'alone') + body_expected = Arel::Nodes::Equality.new(Post.arel_table['body'], 'world') + + assert_equal 2, relation.where_values.size + assert_equal body_expected, relation.where_values.first + assert_equal title_expected, relation.where_values.second + end end end diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index 3e460fa3d6..56e4605ccc 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -24,6 +24,10 @@ module ActiveRecord } end + def test_rewhere_on_root + assert_equal posts(:welcome), Post.rewhere(title: 'Welcome to the weblog').first + end + def test_belongs_to_shallow_where author = Author.new author.id = 1 diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 88a12c61df..c9c7ac04b3 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -42,6 +42,11 @@ class RelationTest < ActiveRecord::TestCase end def test_two_scopes_with_includes_should_not_drop_any_include + # heat habtm cache + car = Car.incl_engines.incl_tyres.first + car.tyres.length + car.engines.length + car = Car.incl_engines.incl_tyres.first assert_no_queries { car.tyres.length } assert_no_queries { car.engines.length } @@ -269,7 +274,7 @@ class RelationTest < ActiveRecord::TestCase def test_none_chained_to_methods_firing_queries_straight_to_db assert_no_queries do - assert_equal [], Developer.none.pluck(:id) # => uses select_all + assert_equal [], Developer.none.pluck(:id, :name) assert_equal 0, Developer.none.delete_all assert_equal 0, Developer.none.update_all(:name => 'David') assert_equal 0, Developer.none.delete(1) @@ -290,7 +295,7 @@ class RelationTest < ActiveRecord::TestCase def test_null_relation_calculations_methods assert_no_queries do assert_equal 0, Developer.none.count - assert_equal 0, Developer.none.calculate(:count, nil, {}) + assert_equal 0, Developer.none.calculate(:count, nil) assert_equal nil, Developer.none.calculate(:average, 'salary') end end @@ -300,6 +305,10 @@ class RelationTest < ActiveRecord::TestCase assert_equal({}, Developer.none.where_values_hash) end + def test_null_relation_where_values_hash + assert_equal({ 'salary' => 100_000 }, Developer.none.where(salary: 100_000).where_values_hash) + end + def test_joins_with_nil_argument assert_nothing_raised { DependentFirm.joins(nil).first } end @@ -477,6 +486,14 @@ class RelationTest < ActiveRecord::TestCase assert_equal Developer.where(name: 'David').map(&:id).sort, developers end + def test_includes_with_select + query = Post.select('comments_count AS ranking').order('ranking').includes(:comments) + .where(comments: { id: 1 }) + + assert_equal ['comments_count AS ranking'], query.select_values + assert_equal 1, query.to_a.size + end + def test_loading_with_one_association posts = Post.preload(:comments) post = posts.find { |p| p.id == 1 } @@ -617,6 +634,36 @@ class RelationTest < ActiveRecord::TestCase relation = Author.where(:id => Author.where(:id => david.id)) assert_equal [david], relation.to_a } + + assert_queries(1) { + relation = Author.where('id in (?)', Author.where(id: david).select(:id)) + assert_equal [david], relation.to_a + } + + assert_queries(1) do + relation = Author.where('id in (:author_ids)', author_ids: Author.where(id: david).select(:id)) + assert_equal [david], relation.to_a + end + end + + def test_find_all_using_where_with_relation_with_bound_values + david = authors(:david) + davids_posts = david.posts.order(:id).to_a + + assert_queries(1) do + relation = Post.where(id: david.posts.select(:id)) + assert_equal davids_posts, relation.order(:id).to_a + end + + assert_queries(1) do + relation = Post.where('id in (?)', david.posts.select(:id)) + assert_equal davids_posts, relation.order(:id).to_a, 'should process Relation as bind variables' + end + + assert_queries(1) do + relation = Post.where('id in (:post_ids)', post_ids: david.posts.select(:id)) + assert_equal davids_posts, relation.order(:id).to_a, 'should process Relation as named bind variables' + end end def test_find_all_using_where_with_relation_and_alternate_primary_key diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb index 082570c55b..766b2ff2ef 100644 --- a/activerecord/test/cases/sanitize_test.rb +++ b/activerecord/test/cases/sanitize_test.rb @@ -1,5 +1,7 @@ require "cases/helper" require 'models/binary' +require 'models/author' +require 'models/post' class SanitizeTest < ActiveRecord::TestCase def setup @@ -9,7 +11,7 @@ class SanitizeTest < ActiveRecord::TestCase quoted_bambi = ActiveRecord::Base.connection.quote("Bambi") quoted_column_name = ActiveRecord::Base.connection.quote_column_name("name") quoted_table_name = ActiveRecord::Base.connection.quote_table_name("adorable_animals") - expected_value = "#{quoted_table_name}.#{quoted_column_name} = #{quoted_bambi}" + expected_value = "#{quoted_table_name}.#{quoted_column_name} = #{quoted_bambi}" assert_equal expected_value, Binary.send(:sanitize_sql_hash, {adorable_animals: {name: 'Bambi'}}) end @@ -31,4 +33,17 @@ class SanitizeTest < ActiveRecord::TestCase assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper"]) assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper".mb_chars]) end + + def test_sanitize_sql_array_handles_relations + david = Author.create!(name: 'David') + david_posts = david.posts.select(:id) + + sub_query_pattern = /\(\bselect\b.*?\bwhere\b.*?\)/i + + select_author_sql = Post.send(:sanitize_sql_array, ['id in (?)', david_posts]) + assert_match(sub_query_pattern, select_author_sql, 'should sanitize `Relation` as subquery for bind variables') + + select_author_sql = Post.send(:sanitize_sql_array, ['id in (:post_ids)', post_ids: david_posts]) + assert_match(sub_query_pattern, select_author_sql, 'should sanitize `Relation` as subquery for named bind variables') + end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 32f86f9c88..1ee8e60924 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -202,6 +202,11 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r(primary_key: "movieid"), match[1], "non-standard primary key not preserved" end + def test_schema_dump_should_use_false_as_default + output = standard_dump + assert_match %r{t\.boolean\s+"has_fun",.+default: false}, output + end + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) def test_schema_dump_should_not_add_default_value_for_mysql_text_field output = standard_dump diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index cd7d91ff85..4fd9d6f52a 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -103,7 +103,7 @@ class DefaultScopingTest < ActiveRecord::TestCase def test_unscope_overrides_default_scope expected = Developer.all.collect { |dev| [dev.name, dev.id] } - received = Developer.order('name ASC, id DESC').unscope(:order).collect { |dev| [dev.name, dev.id] } + received = DeveloperCalledJamis.unscope(:where).collect { |dev| [dev.name, dev.id] } assert_equal expected, received end @@ -122,17 +122,25 @@ class DefaultScopingTest < ActiveRecord::TestCase end def test_unscope_with_where_attributes - expected = Developer.order('salary DESC').collect { |dev| dev.name } - received = DeveloperOrderedBySalary.where(name: 'David').unscope(where: :name).collect { |dev| dev.name } + expected = Developer.order('salary DESC').collect(&:name) + received = DeveloperOrderedBySalary.where(name: 'David').unscope(where: :name).collect(&:name) assert_equal expected, received - expected_2 = Developer.order('salary DESC').collect { |dev| dev.name } - received_2 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope({:where => :name}, :select).collect { |dev| dev.name } + expected_2 = Developer.order('salary DESC').collect(&:name) + received_2 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope({:where => :name}, :select).collect(&:name) assert_equal expected_2, received_2 - expected_3 = Developer.order('salary DESC').collect { |dev| dev.name } - received_3 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope(:select, :where).collect { |dev| dev.name } + expected_3 = Developer.order('salary DESC').collect(&:name) + received_3 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope(:select, :where).collect(&:name) assert_equal expected_3, received_3 + + expected_4 = Developer.order('salary DESC').collect(&:name) + received_4 = DeveloperOrderedBySalary.where.not("name" => "Jamis").unscope(where: :name).collect(&:name) + assert_equal expected_4, received_4 + + expected_5 = Developer.order('salary DESC').collect(&:name) + received_5 = DeveloperOrderedBySalary.where.not("name" => ["Jamis", "David"]).unscope(where: :name).collect(&:name) + assert_equal expected_5, received_5 end def test_unscope_multiple_where_clauses diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index 7fe065ee88..bc67da8d27 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -211,16 +211,15 @@ class SerializedAttributeTest < ActiveRecord::TestCase end def test_serialize_attribute_via_select_method_when_time_zone_available - ActiveRecord::Base.time_zone_aware_attributes = true - Topic.serialize(:content, MyObject) + with_timezone_config aware_attributes: true do + Topic.serialize(:content, MyObject) - myobj = MyObject.new('value1', 'value2') - topic = Topic.create(content: myobj) + myobj = MyObject.new('value1', 'value2') + topic = Topic.create(content: myobj) - assert_equal(myobj, Topic.select(:content).find(topic.id).content) - assert_raise(ActiveModel::MissingAttributeError) { Topic.select(:id).find(topic.id).content } - ensure - ActiveRecord::Base.time_zone_aware_attributes = false + assert_equal(myobj, Topic.select(:content).find(topic.id).content) + assert_raise(ActiveModel::MissingAttributeError) { Topic.select(:id).find(topic.id).content } + end end def test_serialize_attribute_can_be_serialized_in_an_integer_column diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index c2c56abacd..0c9f7ccd55 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -150,4 +150,16 @@ class StoreTest < ActiveRecord::TestCase test "all stored attributes are returned" do assert_equal [:color, :homepage, :favorite_food], Admin::User.stored_attributes[:settings] end + + test "stored_attributes are tracked per class" do + first_model = Class.new(ActiveRecord::Base) do + store_accessor :data, :color + end + second_model = Class.new(ActiveRecord::Base) do + store_accessor :data, :width, :height + end + + assert_equal [:color], first_model.stored_attributes[:data] + assert_equal [:width, :height], second_model.stored_attributes[:data] + end end diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index 816bd62751..bdcf31043a 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -280,6 +280,15 @@ module ActiveRecord assert_match(/Could not dump the database structure/, warnings) end + + def test_structure_dump_with_port_number + filename = "awesome-file.sql" + Kernel.expects(:system).with("mysqldump", "--port", "10000", "--result-file", filename, "--no-data", "test-db").returns(true) + + ActiveRecord::Tasks::DatabaseTasks.structure_dump( + @configuration.merge('port' => 10000), + filename) + end end class MySQLStructureLoadTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index f31896bc7f..6ea225178f 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -206,7 +206,7 @@ module ActiveRecord @connection.expects(:schema_search_path).returns("foo") ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) - assert File.exists?(filename) + assert File.exist?(filename) ensure FileUtils.rm(filename) end @@ -231,6 +231,13 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) end + + def test_structure_load_accepts_path_with_spaces + filename = "awesome file.sql" + Kernel.expects(:system).with("psql -q -f awesome\\ file.sql my-app-db") + + ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) + end end end diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb index 7209c0f14d..da3471adf9 100644 --- a/activerecord/test/cases/tasks/sqlite_rake_test.rb +++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb @@ -159,8 +159,8 @@ module ActiveRecord filename = "awesome-file.sql" ActiveRecord::Tasks::DatabaseTasks.structure_dump @configuration, filename, '/rails/root' - assert File.exists?(dbfile) - assert File.exists?(filename) + assert File.exist?(dbfile) + assert File.exist?(filename) ensure FileUtils.rm_f(filename) FileUtils.rm_f(dbfile) @@ -182,7 +182,7 @@ module ActiveRecord open(filename, 'w') { |f| f.puts("select datetime('now', 'localtime');") } ActiveRecord::Tasks::DatabaseTasks.structure_load @configuration, filename, '/rails/root' - assert File.exists?(dbfile) + assert File.exist?(dbfile) ensure FileUtils.rm_f(filename) FileUtils.rm_f(dbfile) diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index a5cb22aaf6..980981903a 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -375,6 +375,36 @@ class TransactionTest < ActiveRecord::TestCase assert_equal "Three", @three end if Topic.connection.supports_savepoints? + def test_using_named_savepoints + Topic.transaction do + @first.approved = true + @first.save! + Topic.connection.create_savepoint("first") + + @first.approved = false + @first.save! + Topic.connection.rollback_to_savepoint("first") + assert @first.reload.approved? + + @first.approved = false + @first.save! + Topic.connection.release_savepoint("first") + assert_not @first.reload.approved? + end + end if Topic.connection.supports_savepoints? + + def test_releasing_named_savepoints + Topic.transaction do + Topic.connection.create_savepoint("another") + Topic.connection.release_savepoint("another") + + # The savepoint is now gone and we can't remove it again. + assert_raises(ActiveRecord::StatementInvalid) do + Topic.connection.release_savepoint("another") + end + end + end + def test_rollback_when_commit_raises Topic.connection.expects(:begin_db_transaction) Topic.connection.expects(:commit_db_transaction).raises('OH NOES') @@ -391,7 +421,9 @@ class TransactionTest < ActiveRecord::TestCase topic = Topic.new(:title => 'test') topic.freeze e = assert_raise(RuntimeError) { topic.save } - assert_equal "can't modify frozen Hash", e.message + assert_match(/frozen/i, e.message) # Not good enough, but we can't do much + # about it since there is no specific error + # for frozen objects. assert !topic.persisted?, 'not persisted' assert_nil topic.id assert topic.frozen?, 'not frozen' diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb index 68fa15de50..78fa2f935a 100644 --- a/activerecord/test/cases/xml_serialization_test.rb +++ b/activerecord/test/cases/xml_serialization_test.rb @@ -161,21 +161,17 @@ end class DefaultXmlSerializationTimezoneTest < ActiveRecord::TestCase def test_should_serialize_datetime_with_timezone - timezone, Time.zone = Time.zone, "Pacific Time (US & Canada)" - - toy = Toy.create(:name => 'Mickey', :updated_at => Time.utc(2006, 8, 1)) - assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml - ensure - Time.zone = timezone + with_timezone_config zone: "Pacific Time (US & Canada)" do + toy = Toy.create(:name => 'Mickey', :updated_at => Time.utc(2006, 8, 1)) + assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml + end end def test_should_serialize_datetime_with_timezone_reloaded - timezone, Time.zone = Time.zone, "Pacific Time (US & Canada)" - - toy = Toy.create(:name => 'Minnie', :updated_at => Time.utc(2006, 8, 1)).reload - assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml - ensure - Time.zone = timezone + with_timezone_config zone: "Pacific Time (US & Canada)" do + toy = Toy.create(:name => 'Minnie', :updated_at => Time.utc(2006, 8, 1)).reload + assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml + end end end diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb index 302913e095..83a710b1b7 100644 --- a/activerecord/test/cases/yaml_serialization_test.rb +++ b/activerecord/test/cases/yaml_serialization_test.rb @@ -5,16 +5,10 @@ class YamlSerializationTest < ActiveRecord::TestCase fixtures :topics def test_to_yaml_with_time_with_zone_should_not_raise_exception - tz = Time.zone - Time.zone = ActiveSupport::TimeZone["Pacific Time (US & Canada)"] - ActiveRecord::Base.time_zone_aware_attributes = true - - topic = Topic.new(:written_on => DateTime.now) - assert_nothing_raised { topic.to_yaml } - - ensure - Time.zone = tz - ActiveRecord::Base.time_zone_aware_attributes = false + with_timezone_config aware_attributes: true, zone: "Pacific Time (US & Canada)" do + topic = Topic.new(:written_on => DateTime.now) + assert_nothing_raised { topic.to_yaml } + end end def test_roundtrip diff --git a/activerecord/test/fixtures/owners.yml b/activerecord/test/fixtures/owners.yml index 2d21ce433c..3b7b29bb34 100644 --- a/activerecord/test/fixtures/owners.yml +++ b/activerecord/test/fixtures/owners.yml @@ -2,6 +2,7 @@ blackbeard: owner_id: 1 name: blackbeard essay_id: A Modest Proposal + happy_at: '2150-10-10 16:00:00' ashley: owner_id: 2 diff --git a/activerecord/test/fixtures/sponsors.yml b/activerecord/test/fixtures/sponsors.yml index bfc6b238b1..2da541c539 100644 --- a/activerecord/test/fixtures/sponsors.yml +++ b/activerecord/test/fixtures/sponsors.yml @@ -8,5 +8,5 @@ boring_club_sponsor_for_groucho: sponsorable_type: Member crazy_club_sponsor_for_groucho: sponsor_club: crazy_club - sponsorable_id: 2 + sponsorable_id: 3 sponsorable_type: Member diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb index 5458a28cc9..781f67557b 100644 --- a/activerecord/test/models/book.rb +++ b/activerecord/test/models/book.rb @@ -2,8 +2,11 @@ class Book < ActiveRecord::Base has_many :authors has_many :citations, :foreign_key => 'book1_id' - has_many :references, -> { distinct }, :through => :citations, :source => :reference_of + has_many :references, -> { distinct }, through: :citations, source: :reference_of has_many :subscriptions - has_many :subscribers, :through => :subscriptions + has_many :subscribers, through: :subscriptions + + enum status: [:proposed, :written, :published] + enum read_status: {unread: 0, reading: 2, read: 3} end diff --git a/activerecord/test/models/cake_designer.rb b/activerecord/test/models/cake_designer.rb new file mode 100644 index 0000000000..9c57ef573a --- /dev/null +++ b/activerecord/test/models/cake_designer.rb @@ -0,0 +1,3 @@ +class CakeDesigner < ActiveRecord::Base + has_one :chef, as: :employable +end diff --git a/activerecord/test/models/chef.rb b/activerecord/test/models/chef.rb new file mode 100644 index 0000000000..67a4e54f06 --- /dev/null +++ b/activerecord/test/models/chef.rb @@ -0,0 +1,3 @@ +class Chef < ActiveRecord::Base + belongs_to :employable, polymorphic: true +end diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index 1aa77f95bf..0b0b304121 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -11,6 +11,11 @@ class Company < AbstractCompany has_many :contracts has_many :developers, :through => :contracts + scope :of_first_firm, lambda { + joins(:account => :firm). + where('firms.id' => 1) + } + def arbitrary_method "I am Jack's profound disappointment" end diff --git a/activerecord/test/models/department.rb b/activerecord/test/models/department.rb new file mode 100644 index 0000000000..08004a0ed3 --- /dev/null +++ b/activerecord/test/models/department.rb @@ -0,0 +1,4 @@ +class Department < ActiveRecord::Base + has_many :chefs + belongs_to :hotel +end diff --git a/activerecord/test/models/drink_designer.rb b/activerecord/test/models/drink_designer.rb new file mode 100644 index 0000000000..2db968ef11 --- /dev/null +++ b/activerecord/test/models/drink_designer.rb @@ -0,0 +1,3 @@ +class DrinkDesigner < ActiveRecord::Base + has_one :chef, as: :employable +end diff --git a/activerecord/test/models/hotel.rb b/activerecord/test/models/hotel.rb new file mode 100644 index 0000000000..b352cd22f3 --- /dev/null +++ b/activerecord/test/models/hotel.rb @@ -0,0 +1,6 @@ +class Hotel < ActiveRecord::Base + has_many :departments + has_many :chefs, through: :departments + has_many :cake_designers, source_type: 'CakeDesigner', source: :employable, through: :chefs + has_many :drink_designers, source_type: 'DrinkDesigner', source: :employable, through: :chefs +end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 452d54580a..faf539a562 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -65,6 +65,9 @@ class Post < ActiveRecord::Base has_many :author_favorites, :through => :author has_many :author_categorizations, :through => :author, :source => :categorizations has_many :author_addresses, :through => :author + has_many :author_address_extra_with_address, + through: :author_with_address, + source: :author_address_extra has_many :comments_with_interpolated_conditions, ->(p) { where "#{"#{p.aliased_table_name}." rescue ""}body = ?", 'Thank you for the welcome' }, diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 75711673a7..92e2e4d409 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -27,111 +27,113 @@ ActiveRecord::Schema.define do # # # ------------------------------------------------------------------- # - create_table :accounts, :force => true do |t| + create_table :accounts, force: true do |t| t.integer :firm_id t.string :firm_name t.integer :credit_limit end - create_table :admin_accounts, :force => true do |t| + create_table :admin_accounts, force: true do |t| t.string :name end - create_table :admin_users, :force => true do |t| + create_table :admin_users, force: true do |t| t.string :name - t.string :settings, :null => true, :limit => 1024 + t.string :settings, null: true, limit: 1024 # MySQL does not allow default values for blobs. Fake it out with a # big varchar below. - t.string :preferences, :null => true, :default => '', :limit => 1024 - t.string :json_data, :null => true, :limit => 1024 - t.string :json_data_empty, :null => true, :default => "", :limit => 1024 + t.string :preferences, null: true, default: '', limit: 1024 + t.string :json_data, null: true, limit: 1024 + t.string :json_data_empty, null: true, default: "", limit: 1024 t.references :account end - create_table :aircraft, :force => true do |t| + create_table :aircraft, force: true do |t| t.string :name end - create_table :audit_logs, :force => true do |t| - t.column :message, :string, :null=>false - t.column :developer_id, :integer, :null=>false + create_table :audit_logs, force: true do |t| + t.column :message, :string, null: false + t.column :developer_id, :integer, null: false t.integer :unvalidated_developer_id end - create_table :authors, :force => true do |t| - t.string :name, :null => false + create_table :authors, force: true do |t| + t.string :name, null: false t.integer :author_address_id t.integer :author_address_extra_id t.string :organization_id t.string :owned_essay_id end - create_table :author_addresses, :force => true do |t| + create_table :author_addresses, force: true do |t| end - create_table :author_favorites, :force => true do |t| + create_table :author_favorites, force: true do |t| t.column :author_id, :integer t.column :favorite_author_id, :integer end - create_table :auto_id_tests, :force => true, :id => false do |t| + create_table :auto_id_tests, force: true, id: false do |t| t.primary_key :auto_id t.integer :value end - create_table :binaries, :force => true do |t| + create_table :binaries, force: true do |t| t.string :name t.binary :data - t.binary :short_data, :limit => 2048 + t.binary :short_data, limit: 2048 end - create_table :birds, :force => true do |t| + create_table :birds, force: true do |t| t.string :name t.string :color t.integer :pirate_id end - create_table :books, :force => true do |t| + create_table :books, force: true do |t| t.integer :author_id t.column :name, :string + t.column :status, :integer, default: 0 + t.column :read_status, :integer, default: 0 end - create_table :booleans, :force => true do |t| + create_table :booleans, force: true do |t| t.boolean :value - t.boolean :has_fun, :null => false, :default => false + t.boolean :has_fun, null: false, default: false end - create_table :bulbs, :force => true do |t| + create_table :bulbs, force: true do |t| t.integer :car_id t.string :name t.boolean :frickinawesome t.string :color end - create_table "CamelCase", :force => true do |t| + create_table "CamelCase", force: true do |t| t.string :name end - create_table :cars, :force => true do |t| + create_table :cars, force: true do |t| t.string :name t.integer :engines_count t.integer :wheels_count - t.column :lock_version, :integer, :null => false, :default => 0 + t.column :lock_version, :integer, null: false, default: 0 t.timestamps end - create_table :categories, :force => true do |t| - t.string :name, :null => false + create_table :categories, force: true do |t| + t.string :name, null: false t.string :type t.integer :categorizations_count end - create_table :categories_posts, :force => true, :id => false do |t| - t.integer :category_id, :null => false - t.integer :post_id, :null => false + create_table :categories_posts, force: true, id: false do |t| + t.integer :category_id, null: false + t.integer :post_id, null: false end - create_table :categorizations, :force => true do |t| + create_table :categorizations, force: true do |t| t.column :category_id, :integer t.string :named_category_name t.column :post_id, :integer @@ -139,98 +141,98 @@ ActiveRecord::Schema.define do t.column :special, :boolean end - create_table :citations, :force => true do |t| + create_table :citations, force: true do |t| t.column :book1_id, :integer t.column :book2_id, :integer end - create_table :clubs, :force => true do |t| + create_table :clubs, force: true do |t| t.string :name t.integer :category_id end - create_table :collections, :force => true do |t| + create_table :collections, force: true do |t| t.string :name end - create_table :colnametests, :force => true do |t| - t.integer :references, :null => false + create_table :colnametests, force: true do |t| + t.integer :references, null: false end - create_table :comments, :force => true do |t| - t.integer :post_id, :null => false + create_table :comments, force: true do |t| + t.integer :post_id, null: false # use VARCHAR2(4000) instead of CLOB datatype as CLOB data type has many limitations in # Oracle SELECT WHERE clause which causes many unit test failures if current_adapter?(:OracleAdapter) - t.string :body, :null => false, :limit => 4000 + t.string :body, null: false, limit: 4000 else - t.text :body, :null => false + t.text :body, null: false end t.string :type - t.integer :taggings_count, :default => 0 - t.integer :children_count, :default => 0 + t.integer :taggings_count, default: 0 + t.integer :children_count, default: 0 t.integer :parent_id end - create_table :companies, :force => true do |t| + create_table :companies, force: true do |t| t.string :type t.integer :firm_id t.string :firm_name t.string :name t.integer :client_of - t.integer :rating, :default => 1 + t.integer :rating, default: 1 t.integer :account_id - t.string :description, :default => "" + t.string :description, default: "" end - add_index :companies, [:firm_id, :type, :rating], :name => "company_index" - add_index :companies, [:firm_id, :type], :name => "company_partial_index", :where => "rating > 10" - add_index :companies, :name, :name => 'company_name_index', :using => :btree + add_index :companies, [:firm_id, :type, :rating], name: "company_index" + add_index :companies, [:firm_id, :type], name: "company_partial_index", where: "rating > 10" + add_index :companies, :name, name: 'company_name_index', using: :btree - create_table :vegetables, :force => true do |t| + create_table :vegetables, force: true do |t| t.string :name t.integer :seller_id t.string :custom_type end - create_table :computers, :force => true do |t| - t.integer :developer, :null => false - t.integer :extendedWarranty, :null => false + create_table :computers, force: true do |t| + t.integer :developer, null: false + t.integer :extendedWarranty, null: false end - create_table :contracts, :force => true do |t| + create_table :contracts, force: true do |t| t.integer :developer_id t.integer :company_id end - create_table :customers, :force => true do |t| + create_table :customers, force: true do |t| t.string :name - t.integer :balance, :default => 0 + t.integer :balance, default: 0 t.string :address_street t.string :address_city t.string :address_country t.string :gps_location end - create_table :dashboards, :force => true, :id => false do |t| + create_table :dashboards, force: true, id: false do |t| t.string :dashboard_id t.string :name end - create_table :developers, :force => true do |t| + create_table :developers, force: true do |t| t.string :name - t.integer :salary, :default => 70000 + t.integer :salary, default: 70000 t.datetime :created_at t.datetime :updated_at t.datetime :created_on t.datetime :updated_on end - create_table :developers_projects, :force => true, :id => false do |t| - t.integer :developer_id, :null => false - t.integer :project_id, :null => false + create_table :developers_projects, force: true, id: false do |t| + t.integer :developer_id, null: false + t.integer :project_id, null: false t.date :joined_on - t.integer :access_level, :default => 1 + t.integer :access_level, default: 1 end create_table :dog_lovers, force: true do |t| @@ -239,28 +241,28 @@ ActiveRecord::Schema.define do t.integer :dogs_count, default: 0 end - create_table :dogs, :force => true do |t| + create_table :dogs, force: true do |t| t.integer :trainer_id t.integer :breeder_id t.integer :dog_lover_id end - create_table :edges, :force => true, :id => false do |t| - t.column :source_id, :integer, :null => false - t.column :sink_id, :integer, :null => false + create_table :edges, force: true, id: false do |t| + t.column :source_id, :integer, null: false + t.column :sink_id, :integer, null: false end - add_index :edges, [:source_id, :sink_id], :unique => true, :name => 'unique_edge_index' + add_index :edges, [:source_id, :sink_id], unique: true, name: 'unique_edge_index' - create_table :engines, :force => true do |t| + create_table :engines, force: true do |t| t.integer :car_id end - create_table :entrants, :force => true do |t| - t.string :name, :null => false - t.integer :course_id, :null => false + create_table :entrants, force: true do |t| + t.string :name, null: false + t.integer :course_id, null: false end - create_table :essays, :force => true do |t| + create_table :essays, force: true do |t| t.string :name t.string :writer_id t.string :writer_type @@ -268,153 +270,153 @@ ActiveRecord::Schema.define do t.string :author_id end - create_table :events, :force => true do |t| - t.string :title, :limit => 5 + create_table :events, force: true do |t| + t.string :title, limit: 5 end - create_table :eyes, :force => true do |t| + create_table :eyes, force: true do |t| end - create_table :funny_jokes, :force => true do |t| + create_table :funny_jokes, force: true do |t| t.string :name end - create_table :cold_jokes, :force => true do |t| + create_table :cold_jokes, force: true do |t| t.string :name end - create_table :friendships, :force => true do |t| + create_table :friendships, force: true do |t| t.integer :friend_id t.integer :follower_id end - create_table :goofy_string_id, :force => true, :id => false do |t| - t.string :id, :null => false + create_table :goofy_string_id, force: true, id: false do |t| + t.string :id, null: false t.string :info end - create_table :having, :force => true do |t| + create_table :having, force: true do |t| t.string :where end - create_table :guids, :force => true do |t| + create_table :guids, force: true do |t| t.column :key, :string end - create_table :inept_wizards, :force => true do |t| - t.column :name, :string, :null => false - t.column :city, :string, :null => false + create_table :inept_wizards, force: true do |t| + t.column :name, :string, null: false + t.column :city, :string, null: false t.column :type, :string end - create_table :integer_limits, :force => true do |t| + create_table :integer_limits, force: true do |t| t.integer :"c_int_without_limit" (1..8).each do |i| - t.integer :"c_int_#{i}", :limit => i + t.integer :"c_int_#{i}", limit: i end end - create_table :invoices, :force => true do |t| + create_table :invoices, force: true do |t| t.integer :balance t.datetime :updated_at end - create_table :iris, :force => true do |t| + create_table :iris, force: true do |t| t.references :eye t.string :color end - create_table :items, :force => true do |t| + create_table :items, force: true do |t| t.column :name, :string end - create_table :jobs, :force => true do |t| + create_table :jobs, force: true do |t| t.integer :ideal_reference_id end - create_table :keyboards, :force => true, :id => false do |t| + create_table :keyboards, force: true, id: false do |t| t.primary_key :key_number t.string :name end - create_table :legacy_things, :force => true do |t| + create_table :legacy_things, force: true do |t| t.integer :tps_report_number - t.integer :version, :null => false, :default => 0 + t.integer :version, null: false, default: 0 end - create_table :lessons, :force => true do |t| + create_table :lessons, force: true do |t| t.string :name end - create_table :lessons_students, :id => false, :force => true do |t| + create_table :lessons_students, id: false, force: true do |t| t.references :lesson t.references :student end - create_table :lint_models, :force => true + create_table :lint_models, force: true - create_table :line_items, :force => true do |t| + create_table :line_items, force: true do |t| t.integer :invoice_id t.integer :amount end - create_table :lock_without_defaults, :force => true do |t| + create_table :lock_without_defaults, force: true do |t| t.column :lock_version, :integer end - create_table :lock_without_defaults_cust, :force => true do |t| + create_table :lock_without_defaults_cust, force: true do |t| t.column :custom_lock_version, :integer end - create_table :mateys, :id => false, :force => true do |t| + create_table :mateys, id: false, force: true do |t| t.column :pirate_id, :integer t.column :target_id, :integer t.column :weight, :integer end - create_table :members, :force => true do |t| + create_table :members, force: true do |t| t.string :name t.integer :member_type_id end - create_table :member_details, :force => true do |t| + create_table :member_details, force: true do |t| t.integer :member_id t.integer :organization_id t.string :extra_data end - create_table :member_friends, :force => true, :id => false do |t| + create_table :member_friends, force: true, id: false do |t| t.integer :member_id t.integer :friend_id end - create_table :memberships, :force => true do |t| + create_table :memberships, force: true do |t| t.datetime :joined_on t.integer :club_id, :member_id - t.boolean :favourite, :default => false + t.boolean :favourite, default: false t.string :type end - create_table :member_types, :force => true do |t| + create_table :member_types, force: true do |t| t.string :name end - create_table :minivans, :force => true, :id => false do |t| + create_table :minivans, force: true, id: false do |t| t.string :minivan_id t.string :name t.string :speedometer_id t.string :color end - create_table :minimalistics, :force => true do |t| + create_table :minimalistics, force: true do |t| end - create_table :mixed_case_monkeys, :force => true, :id => false do |t| + create_table :mixed_case_monkeys, force: true, id: false do |t| t.primary_key :monkeyID t.integer :fleaCount end - create_table :mixins, :force => true do |t| + create_table :mixins, force: true do |t| t.integer :parent_id t.integer :pos t.datetime :created_at @@ -425,52 +427,52 @@ ActiveRecord::Schema.define do t.string :type end - create_table :movies, :force => true, :id => false do |t| + create_table :movies, force: true, id: false do |t| t.primary_key :movieid t.string :name end - create_table :numeric_data, :force => true do |t| - t.decimal :bank_balance, :precision => 10, :scale => 2 - t.decimal :big_bank_balance, :precision => 15, :scale => 2 - t.decimal :world_population, :precision => 10, :scale => 0 - t.decimal :my_house_population, :precision => 2, :scale => 0 - t.decimal :decimal_number_with_default, :precision => 3, :scale => 2, :default => 2.78 + create_table :numeric_data, force: true do |t| + t.decimal :bank_balance, precision: 10, scale: 2 + t.decimal :big_bank_balance, precision: 15, scale: 2 + t.decimal :world_population, precision: 10, scale: 0 + t.decimal :my_house_population, precision: 2, scale: 0 + t.decimal :decimal_number_with_default, precision: 3, scale: 2, default: 2.78 t.float :temperature # Oracle/SQLServer supports precision up to 38 - if current_adapter?(:OracleAdapter,:SQLServerAdapter) - t.decimal :atoms_in_universe, :precision => 38, :scale => 0 + if current_adapter?(:OracleAdapter, :SQLServerAdapter) + t.decimal :atoms_in_universe, precision: 38, scale: 0 else - t.decimal :atoms_in_universe, :precision => 55, :scale => 0 + t.decimal :atoms_in_universe, precision: 55, scale: 0 end end - create_table :orders, :force => true do |t| + create_table :orders, force: true do |t| t.string :name t.integer :billing_customer_id t.integer :shipping_customer_id end - create_table :organizations, :force => true do |t| + create_table :organizations, force: true do |t| t.string :name end - create_table :owners, :primary_key => :owner_id ,:force => true do |t| + create_table :owners, primary_key: :owner_id, force: true do |t| t.string :name t.column :updated_at, :datetime t.column :happy_at, :datetime t.string :essay_id end - create_table :paint_colors, :force => true do |t| + create_table :paint_colors, force: true do |t| t.integer :non_poly_one_id end - create_table :paint_textures, :force => true do |t| + create_table :paint_textures, force: true do |t| t.integer :non_poly_two_id end - create_table :parrots, :force => true do |t| + create_table :parrots, force: true do |t| t.column :name, :string t.column :color, :string t.column :parrot_sti_class, :string @@ -481,43 +483,43 @@ ActiveRecord::Schema.define do t.column :updated_on, :datetime end - create_table :parrots_pirates, :id => false, :force => true do |t| + create_table :parrots_pirates, id: false, force: true do |t| t.column :parrot_id, :integer t.column :pirate_id, :integer end - create_table :parrots_treasures, :id => false, :force => true do |t| + create_table :parrots_treasures, id: false, force: true do |t| t.column :parrot_id, :integer t.column :treasure_id, :integer end - create_table :people, :force => true do |t| - t.string :first_name, :null => false + create_table :people, force: true do |t| + t.string :first_name, null: false t.references :primary_contact - t.string :gender, :limit => 1 + t.string :gender, limit: 1 t.references :number1_fan - t.integer :lock_version, :null => false, :default => 0 + t.integer :lock_version, null: false, default: 0 t.string :comments - t.integer :followers_count, :default => 0 - t.integer :friends_too_count, :default => 0 + t.integer :followers_count, default: 0 + t.integer :friends_too_count, default: 0 t.references :best_friend t.references :best_friend_of t.integer :insures, null: false, default: 0 t.timestamps end - create_table :peoples_treasures, :id => false, :force => true do |t| + create_table :peoples_treasures, id: false, force: true do |t| t.column :rich_person_id, :integer t.column :treasure_id, :integer end - create_table :pets, :primary_key => :pet_id ,:force => true do |t| + create_table :pets, primary_key: :pet_id, force: true do |t| t.string :name t.integer :owner_id, :integer t.timestamps end - create_table :pirates, :force => true do |t| + create_table :pirates, force: true do |t| t.column :catchphrase, :string t.column :parrot_id, :integer t.integer :non_validated_parrot_id @@ -525,74 +527,74 @@ ActiveRecord::Schema.define do t.column :updated_on, :datetime end - create_table :posts, :force => true do |t| + create_table :posts, force: true do |t| t.integer :author_id - t.string :title, :null => false + t.string :title, null: false # use VARCHAR2(4000) instead of CLOB datatype as CLOB data type has many limitations in # Oracle SELECT WHERE clause which causes many unit test failures if current_adapter?(:OracleAdapter) - t.string :body, :null => false, :limit => 4000 + t.string :body, null: false, limit: 4000 else - t.text :body, :null => false + t.text :body, null: false end t.string :type - t.integer :comments_count, :default => 0 - t.integer :taggings_count, :default => 0 - t.integer :taggings_with_delete_all_count, :default => 0 - t.integer :taggings_with_destroy_count, :default => 0 - t.integer :tags_count, :default => 0 - t.integer :tags_with_destroy_count, :default => 0 - t.integer :tags_with_nullify_count, :default => 0 + t.integer :comments_count, default: 0 + t.integer :taggings_count, default: 0 + t.integer :taggings_with_delete_all_count, default: 0 + t.integer :taggings_with_destroy_count, default: 0 + t.integer :tags_count, default: 0 + t.integer :tags_with_destroy_count, default: 0 + t.integer :tags_with_nullify_count, default: 0 end - create_table :price_estimates, :force => true do |t| + create_table :price_estimates, force: true do |t| t.string :estimate_of_type t.integer :estimate_of_id t.integer :price end - create_table :products, :force => true do |t| + create_table :products, force: true do |t| t.references :collection t.string :name end - create_table :projects, :force => true do |t| + create_table :projects, force: true do |t| t.string :name t.string :type end - create_table :randomly_named_table, :force => true do |t| + create_table :randomly_named_table, force: true do |t| t.string :some_attribute t.integer :another_attribute end - create_table :ratings, :force => true do |t| + create_table :ratings, force: true do |t| t.integer :comment_id t.integer :value end - create_table :readers, :force => true do |t| - t.integer :post_id, :null => false - t.integer :person_id, :null => false - t.boolean :skimmer, :default => false + create_table :readers, force: true do |t| + t.integer :post_id, null: false + t.integer :person_id, null: false + t.boolean :skimmer, default: false t.integer :first_post_id end - create_table :references, :force => true do |t| + create_table :references, force: true do |t| t.integer :person_id t.integer :job_id t.boolean :favourite - t.integer :lock_version, :default => 0 + t.integer :lock_version, default: 0 end - create_table :shape_expressions, :force => true do |t| + create_table :shape_expressions, force: true do |t| t.string :paint_type t.integer :paint_id t.string :shape_type t.integer :shape_id end - create_table :ships, :force => true do |t| + create_table :ships, force: true do |t| t.string :name t.integer :pirate_id t.integer :update_only_pirate_id @@ -602,51 +604,51 @@ ActiveRecord::Schema.define do t.datetime :updated_on end - create_table :ship_parts, :force => true do |t| + create_table :ship_parts, force: true do |t| t.string :name t.integer :ship_id end - create_table :speedometers, :force => true, :id => false do |t| + create_table :speedometers, force: true, id: false do |t| t.string :speedometer_id t.string :name t.string :dashboard_id end - create_table :sponsors, :force => true do |t| + create_table :sponsors, force: true do |t| t.integer :club_id t.integer :sponsorable_id t.string :sponsorable_type end - create_table :string_key_objects, :id => false, :primary_key => :id, :force => true do |t| + create_table :string_key_objects, id: false, primary_key: :id, force: true do |t| t.string :id t.string :name - t.integer :lock_version, :null => false, :default => 0 + t.integer :lock_version, null: false, default: 0 end - create_table :students, :force => true do |t| + create_table :students, force: true do |t| t.string :name end - create_table :subscribers, :force => true, :id => false do |t| - t.string :nick, :null => false + create_table :subscribers, force: true, id: false do |t| + t.string :nick, null: false t.string :name - t.column :books_count, :integer, :null => false, :default => 0 + t.column :books_count, :integer, null: false, default: 0 end - add_index :subscribers, :nick, :unique => true + add_index :subscribers, :nick, unique: true - create_table :subscriptions, :force => true do |t| + create_table :subscriptions, force: true do |t| t.string :subscriber_id t.integer :book_id end - create_table :tags, :force => true do |t| + create_table :tags, force: true do |t| t.column :name, :string - t.column :taggings_count, :integer, :default => 0 + t.column :taggings_count, :integer, default: 0 end - create_table :taggings, :force => true do |t| + create_table :taggings, force: true do |t| t.column :tag_id, :integer t.column :super_tag_id, :integer t.column :taggable_type, :string @@ -654,12 +656,12 @@ ActiveRecord::Schema.define do t.string :comment end - create_table :tasks, :force => true do |t| + create_table :tasks, force: true do |t| t.datetime :starting t.datetime :ending end - create_table :topics, :force => true do |t| + create_table :topics, force: true do |t| t.string :title t.string :author_name t.string :author_email_address @@ -669,15 +671,15 @@ ActiveRecord::Schema.define do # use VARCHAR2(4000) instead of CLOB datatype as CLOB data type has many limitations in # Oracle SELECT WHERE clause which causes many unit test failures if current_adapter?(:OracleAdapter) - t.string :content, :limit => 4000 - t.string :important, :limit => 4000 + t.string :content, limit: 4000 + t.string :important, limit: 4000 else t.text :content t.text :important end - t.boolean :approved, :default => true - t.integer :replies_count, :default => 0 - t.integer :unique_replies_count, :default => 0 + t.boolean :approved, default: true + t.integer :replies_count, default: 0 + t.integer :unique_replies_count, default: 0 t.integer :parent_id t.string :parent_title t.string :type @@ -685,54 +687,54 @@ ActiveRecord::Schema.define do t.timestamps end - create_table :toys, :primary_key => :toy_id ,:force => true do |t| + create_table :toys, primary_key: :toy_id, force: true do |t| t.string :name t.integer :pet_id, :integer t.timestamps end - create_table :traffic_lights, :force => true do |t| + create_table :traffic_lights, force: true do |t| t.string :location t.string :state - t.text :long_state, :null => false + t.text :long_state, null: false t.datetime :created_at t.datetime :updated_at end - create_table :treasures, :force => true do |t| + create_table :treasures, force: true do |t| t.column :name, :string t.column :type, :string t.column :looter_id, :integer t.column :looter_type, :string end - create_table :tyres, :force => true do |t| + create_table :tyres, force: true do |t| t.integer :car_id end - create_table :variants, :force => true do |t| + create_table :variants, force: true do |t| t.references :product t.string :name end - create_table :vertices, :force => true do |t| + create_table :vertices, force: true do |t| t.column :label, :string end - create_table 'warehouse-things', :force => true do |t| + create_table 'warehouse-things', force: true do |t| t.integer :value end [:circles, :squares, :triangles, :non_poly_ones, :non_poly_twos].each do |t| - create_table(t, :force => true) { } + create_table(t, force: true) { } end # NOTE - the following 4 tables are used by models that have :inverse_of options on the associations - create_table :men, :force => true do |t| + create_table :men, force: true do |t| t.string :name end - create_table :faces, :force => true do |t| + create_table :faces, force: true do |t| t.string :description t.integer :man_id t.integer :polymorphic_man_id @@ -741,7 +743,7 @@ ActiveRecord::Schema.define do t.string :horrible_polymorphic_man_type end - create_table :interests, :force => true do |t| + create_table :interests, force: true do |t| t.string :topic t.integer :man_id t.integer :polymorphic_man_id @@ -749,51 +751,67 @@ ActiveRecord::Schema.define do t.integer :zine_id end - create_table :wheels, :force => true do |t| - t.references :wheelable, :polymorphic => true + create_table :wheels, force: true do |t| + t.references :wheelable, polymorphic: true end - create_table :zines, :force => true do |t| + create_table :zines, force: true do |t| t.string :title end - create_table :countries, :force => true, :id => false, :primary_key => 'country_id' do |t| + create_table :countries, force: true, id: false, primary_key: 'country_id' do |t| t.string :country_id t.string :name end - create_table :treaties, :force => true, :id => false, :primary_key => 'treaty_id' do |t| + create_table :treaties, force: true, id: false, primary_key: 'treaty_id' do |t| t.string :treaty_id t.string :name end - create_table :countries_treaties, :force => true, :id => false do |t| - t.string :country_id, :null => false - t.string :treaty_id, :null => false + create_table :countries_treaties, force: true, id: false do |t| + t.string :country_id, null: false + t.string :treaty_id, null: false end - create_table :liquid, :force => true do |t| + create_table :liquid, force: true do |t| t.string :name end - create_table :molecules, :force => true do |t| + create_table :molecules, force: true do |t| t.integer :liquid_id t.string :name end - create_table :electrons, :force => true do |t| + create_table :electrons, force: true do |t| t.integer :molecule_id t.string :name end - create_table :weirds, :force => true do |t| + create_table :weirds, force: true do |t| t.string 'a$b' t.string 'なまえ' t.string 'from' end + create_table :hotels, force: true do |t| + end + create_table :departments, force: true do |t| + t.integer :hotel_id + end + create_table :cake_designers, force: true do |t| + end + create_table :drink_designers, force: true do |t| + end + create_table :chefs, force: true do |t| + t.integer :employable_id + t.string :employable_type + t.integer :department_id + end + + except 'SQLite' do # fk_test_has_fk should be before fk_test_has_pk - create_table :fk_test_has_fk, :force => true do |t| - t.integer :fk_id, :null => false + create_table :fk_test_has_fk, force: true do |t| + t.integer :fk_id, null: false end - create_table :fk_test_has_pk, :force => true do |t| + create_table :fk_test_has_pk, force: true do |t| end execute "ALTER TABLE fk_test_has_fk ADD CONSTRAINT fk_name FOREIGN KEY (#{quote_column_name 'fk_id'}) REFERENCES #{quote_table_name 'fk_test_has_pk'} (#{quote_column_name 'id'})" @@ -802,11 +820,11 @@ ActiveRecord::Schema.define do end end -Course.connection.create_table :courses, :force => true do |t| - t.column :name, :string, :null => false +Course.connection.create_table :courses, force: true do |t| + t.column :name, :string, null: false t.column :college_id, :integer end -College.connection.create_table :colleges, :force => true do |t| - t.column :name, :string, :null => false +College.connection.create_table :colleges, force: true do |t| + t.column :name, :string, null: false end diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb index e9ddeb32cf..b7aff4f47d 100644 --- a/activerecord/test/schema/sqlite_specific_schema.rb +++ b/activerecord/test/schema/sqlite_specific_schema.rb @@ -1,9 +1,6 @@ ActiveRecord::Schema.define do - # For sqlite 3.1.0+, make a table with an autoincrement column - if supports_autoincrement? - create_table :table_with_autoincrement, :force => true do |t| - t.column :name, :string - end + create_table :table_with_autoincrement, :force => true do |t| + t.column :name, :string end execute "DROP TABLE fk_test_has_fk" rescue nil diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index eee7a7a587..e718e79877 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,24 @@ +* Added Numeric#in_milliseconds, like 1.hour.in_milliseconds, so we can feed them to JavaScript functions like getTime(). + + *DHH* + +* Calling ActiveSupport::JSON.decode with unsupported options now raises an error. + + *Godfrey Chan* + +* Support :unless_exist in FileStore + + *Michael Grosser* + +* Fix `slice!` deleting the default value of the hash. + + *Antonio Santos* + +* `require_dependency` accepts objects that respond to `to_path`, in + particular `Pathname` instances. + + *Benjamin Fleischer* + * Disable the ability to iterate over Range of AS::TimeWithZone due to significant performance issues. @@ -17,14 +38,14 @@ You can do this: - class JokeSubscriber < ActiveSupport::Subscriber - # This is much easier to read! - attach_to "active_record" + class JokeSubscriber < ActiveSupport::Subscriber + # This is much easier to read! + attach_to "active_record" - def sql(event) - puts "A rabbi and a priest walk into a bar..." + def sql(event) + puts "A rabbi and a priest walk into a bar..." + end end - end This should make it easier to read and understand these subscribers. diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index c5633d902f..29e2440288 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -12,10 +12,10 @@ require 'active_support/core_ext/string/inflections' module ActiveSupport # See ActiveSupport::Cache::Store for documentation. module Cache - autoload :FileStore, 'active_support/cache/file_store' - autoload :MemoryStore, 'active_support/cache/memory_store' + autoload :FileStore, 'active_support/cache/file_store' + autoload :MemoryStore, 'active_support/cache/memory_store' autoload :MemCacheStore, 'active_support/cache/mem_cache_store' - autoload :NullStore, 'active_support/cache/null_store' + autoload :NullStore, 'active_support/cache/null_store' # These options mean something to all cache implementations. Individual cache # implementations may support additional options. @@ -88,25 +88,24 @@ module ActiveSupport end private + def retrieve_cache_key(key) + case + when key.respond_to?(:cache_key) then key.cache_key + when key.is_a?(Array) then key.map { |element| retrieve_cache_key(element) }.to_param + when key.respond_to?(:to_a) then retrieve_cache_key(key.to_a) + else key.to_param + end.to_s + end - def retrieve_cache_key(key) - case - when key.respond_to?(:cache_key) then key.cache_key - when key.is_a?(Array) then key.map { |element| retrieve_cache_key(element) }.to_param - when key.respond_to?(:to_a) then retrieve_cache_key(key.to_a) - else key.to_param - end.to_s - end - - # Obtains the specified cache store class, given the name of the +store+. - # Raises an error when the store class cannot be found. - def retrieve_store_class(store) - require "active_support/cache/#{store}" - rescue LoadError => e - raise "Could not find cache store adapter for #{store} (#{e})" - else - ActiveSupport::Cache.const_get(store.to_s.camelize) - end + # Obtains the specified cache store class, given the name of the +store+. + # Raises an error when the store class cannot be found. + def retrieve_store_class(store) + require "active_support/cache/#{store}" + rescue LoadError => e + raise "Could not find cache store adapter for #{store} (#{e})" + else + ActiveSupport::Cache.const_get(store.to_s.camelize) + end end # An abstract cache store class. There are multiple cache store @@ -153,7 +152,6 @@ module ActiveSupport # or +write+. To specify the threshold at which to compress values, set the # <tt>:compress_threshold</tt> option. The default threshold is 16K. class Store - cattr_accessor :logger, :instance_writer => true attr_reader :silence, :options @@ -385,6 +383,7 @@ module ActiveSupport # Options are passed to the underlying cache implementation. def write(name, value, options = nil) options = merged_options(options) + instrument(:write, name, options) do entry = Entry.new(value, options) write_entry(namespaced_key(name, options), entry, options) @@ -396,6 +395,7 @@ module ActiveSupport # Options are passed to the underlying cache implementation. def delete(name, options = nil) options = merged_options(options) + instrument(:delete, name) do delete_entry(namespaced_key(name, options), options) end @@ -406,6 +406,7 @@ module ActiveSupport # Options are passed to the underlying cache implementation. def exist?(name, options = nil) options = merged_options(options) + instrument(:exist?, name) do entry = read_entry(namespaced_key(name, options), options) (entry && !entry.expired?) || false @@ -585,6 +586,7 @@ module ActiveSupport result = instrument(:generate, name, options) do |payload| yield(name) end + write(name, result, options) result end @@ -608,6 +610,7 @@ module ActiveSupport else @value = value end + @created_at = Time.now.to_f @expires_in = options[:expires_in] @expires_in = @expires_in.to_f if @expires_in @@ -658,6 +661,7 @@ module ActiveSupport # serialize entries to protect against accidental cache modifications. def dup_value! convert_version_4beta1_entry! if defined?(@v) + if @value && !compressed? && !(@value.is_a?(Numeric) || @value == true || @value == false) if @value.is_a?(String) @value = @value.dup @@ -672,8 +676,10 @@ module ActiveSupport if value && options[:compress] compress_threshold = options[:compress_threshold] || DEFAULT_COMPRESS_LIMIT serialized_value_size = (value.is_a?(String) ? value : Marshal.dump(value)).bytesize + return true if serialized_value_size >= compress_threshold end + false end @@ -696,10 +702,12 @@ module ActiveSupport @value = @v remove_instance_variable(:@v) end + if defined?(@c) @compressed = @c remove_instance_variable(:@c) end + if defined?(@x) && @x @created_at ||= Time.now.to_f @expires_in = @x - @created_at diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index 10d39463ec..5cd6065077 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -97,6 +97,7 @@ module ActiveSupport def write_entry(key, entry, options) file_name = key_file_path(key) + return false if options[:unless_exist] && File.exist?(file_name) ensure_cache_path(File.dirname(file_name)) File.atomic_write(file_name, cache_path) {|f| Marshal.dump(entry, f)} true diff --git a/activesupport/lib/active_support/core_ext/array/access.rb b/activesupport/lib/active_support/core_ext/array/access.rb index 4f1e432b61..67f58bc0fe 100644 --- a/activesupport/lib/active_support/core_ext/array/access.rb +++ b/activesupport/lib/active_support/core_ext/array/access.rb @@ -48,6 +48,8 @@ class Array end # Equal to <tt>self[41]</tt>. Also known as accessing "the reddit". + # + # (1..42).to_a.forty_two # => 42 def forty_two self[41] end diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb index 83038f9da5..f2a221c396 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute.rb @@ -118,7 +118,10 @@ class Class end private - def singleton_class? - ancestors.first != self + + unless respond_to?(:singleton_class?) + def singleton_class? + ancestors.first != self + end end end diff --git a/activesupport/lib/active_support/core_ext/file/atomic.rb b/activesupport/lib/active_support/core_ext/file/atomic.rb index c3e6124a57..0e7e3ba378 100644 --- a/activesupport/lib/active_support/core_ext/file/atomic.rb +++ b/activesupport/lib/active_support/core_ext/file/atomic.rb @@ -23,7 +23,7 @@ class File yield temp_file temp_file.close - if File.exists?(file_name) + if File.exist?(file_name) # Get original file permissions old_stat = stat(file_name) else diff --git a/activesupport/lib/active_support/core_ext/hash/slice.rb b/activesupport/lib/active_support/core_ext/hash/slice.rb index 9fa9b3dac4..8ad600b171 100644 --- a/activesupport/lib/active_support/core_ext/hash/slice.rb +++ b/activesupport/lib/active_support/core_ext/hash/slice.rb @@ -26,6 +26,8 @@ class Hash keys.map! { |key| convert_key(key) } if respond_to?(:convert_key, true) omit = slice(*self.keys - keys) hash = slice(*keys) + hash.default = default + hash.default_proc = default_proc if default_proc replace(hash) omit end diff --git a/activesupport/lib/active_support/core_ext/numeric/time.rb b/activesupport/lib/active_support/core_ext/numeric/time.rb index 87b9a23aef..d97ce938c1 100644 --- a/activesupport/lib/active_support/core_ext/numeric/time.rb +++ b/activesupport/lib/active_support/core_ext/numeric/time.rb @@ -76,4 +76,10 @@ class Numeric # Reads best without arguments: 10.minutes.from_now alias :from_now :since + + # Used with the standard time durations, like 1.hour.in_milliseconds -- + # so we can feed them to JavaScript functions like getTime(). + def in_milliseconds + self * 1000 + end end diff --git a/activesupport/lib/active_support/core_ext/object.rb b/activesupport/lib/active_support/core_ext/object.rb index ec2157221f..f4f9152d6a 100644 --- a/activesupport/lib/active_support/core_ext/object.rb +++ b/activesupport/lib/active_support/core_ext/object.rb @@ -8,7 +8,7 @@ require 'active_support/core_ext/object/inclusion' require 'active_support/core_ext/object/conversions' require 'active_support/core_ext/object/instance_variables' -require 'active_support/core_ext/object/to_json' +require 'active_support/core_ext/object/json' require 'active_support/core_ext/object/to_param' require 'active_support/core_ext/object/to_query' require 'active_support/core_ext/object/with_options' diff --git a/activesupport/lib/active_support/core_ext/object/json.rb b/activesupport/lib/active_support/core_ext/object/json.rb new file mode 100644 index 0000000000..554da1a2aa --- /dev/null +++ b/activesupport/lib/active_support/core_ext/object/json.rb @@ -0,0 +1,218 @@ +# Hack to load json gem first so we can overwrite its to_json. +require 'json' +require 'bigdecimal' +require 'time' + +# The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting +# their default behavior. That said, we need to define the basic to_json method in all of them, +# otherwise they will always use to_json gem implementation, which is backwards incompatible in +# several cases (for instance, the JSON implementation for Hash does not work) with inheritance +# and consequently classes as ActiveSupport::OrderedHash cannot be serialized to json. +[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass].each do |klass| + klass.class_eval do + # Dumps object in JSON (JavaScript Object Notation). See www.json.org for more info. + def to_json(options = nil) + ActiveSupport::JSON.encode(self, options) + end + end +end + +class Object + def as_json(options = nil) #:nodoc: + if respond_to?(:to_hash) + to_hash + else + instance_values + end + end +end + +class Struct #:nodoc: + def as_json(options = nil) + Hash[members.zip(values)] + end +end + +class TrueClass + def as_json(options = nil) #:nodoc: + self + end + + def encode_json(encoder) #:nodoc: + to_s + end +end + +class FalseClass + def as_json(options = nil) #:nodoc: + self + end + + def encode_json(encoder) #:nodoc: + to_s + end +end + +class NilClass + def as_json(options = nil) #:nodoc: + self + end + + def encode_json(encoder) #:nodoc: + 'null' + end +end + +class String + def as_json(options = nil) #:nodoc: + self + end + + def encode_json(encoder) #:nodoc: + encoder.escape(self) + end +end + +class Symbol + def as_json(options = nil) #:nodoc: + to_s + end +end + +class Numeric + def as_json(options = nil) #:nodoc: + self + end + + def encode_json(encoder) #:nodoc: + to_s + end +end + +class Float + # Encoding Infinity or NaN to JSON should return "null". The default returns + # "Infinity" or "NaN" which breaks parsing the JSON. E.g. JSON.parse('[NaN]'). + def as_json(options = nil) #:nodoc: + finite? ? self : nil + end +end + +class BigDecimal + # A BigDecimal would be naturally represented as a JSON number. Most libraries, + # however, parse non-integer JSON numbers directly as floats. Clients using + # those libraries would get in general a wrong number and no way to recover + # other than manually inspecting the string with the JSON code itself. + # + # That's why a JSON string is returned. The JSON literal is not numeric, but + # if the other end knows by contract that the data is supposed to be a + # BigDecimal, it still has the chance to post-process the string and get the + # real value. + # + # Use <tt>ActiveSupport.use_standard_json_big_decimal_format = true</tt> to + # override this behavior. + def as_json(options = nil) #:nodoc: + if finite? + ActiveSupport.encode_big_decimal_as_string ? to_s : self + else + nil + end + end +end + +class Regexp + def as_json(options = nil) #:nodoc: + to_s + end +end + +module Enumerable + def as_json(options = nil) #:nodoc: + to_a.as_json(options) + end +end + +class Range + def as_json(options = nil) #:nodoc: + to_s + end +end + +class Array + def as_json(options = nil) #:nodoc: + # use encoder as a proxy to call as_json on all elements, to protect from circular references + encoder = options && options[:encoder] || ActiveSupport::JSON::Encoding::Encoder.new(options) + map { |v| encoder.as_json(v, options) } + end + + def encode_json(encoder) #:nodoc: + # we assume here that the encoder has already run as_json on self and the elements, so we run encode_json directly + "[#{map { |v| v.encode_json(encoder) } * ','}]" + end +end + +class Hash + def as_json(options = nil) #:nodoc: + # create a subset of the hash by applying :only or :except + subset = if options + if attrs = options[:only] + slice(*Array(attrs)) + elsif attrs = options[:except] + except(*Array(attrs)) + else + self + end + else + self + end + + # use encoder as a proxy to call as_json on all values in the subset, to protect from circular references + encoder = options && options[:encoder] || ActiveSupport::JSON::Encoding::Encoder.new(options) + Hash[subset.map { |k, v| [k.to_s, encoder.as_json(v, options)] }] + end + + def encode_json(encoder) #:nodoc: + # values are encoded with use_options = false, because we don't want hash representations from ActiveModel to be + # processed once again with as_json with options, as this could cause unexpected results (i.e. missing fields); + + # on the other hand, we need to run as_json on the elements, because the model representation may contain fields + # like Time/Date in their original (not jsonified) form, etc. + + "{#{map { |k,v| "#{encoder.encode(k.to_s)}:#{encoder.encode(v, false)}" } * ','}}" + end +end + +class Time + def as_json(options = nil) #:nodoc: + if ActiveSupport.use_standard_json_time_format + xmlschema + else + %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)}) + end + end +end + +class Date + def as_json(options = nil) #:nodoc: + if ActiveSupport.use_standard_json_time_format + strftime("%Y-%m-%d") + else + strftime("%Y/%m/%d") + end + end +end + +class DateTime + def as_json(options = nil) #:nodoc: + if ActiveSupport.use_standard_json_time_format + xmlschema + else + strftime('%Y/%m/%d %H:%M:%S %z') + end + end +end + +class Process::Status + def as_json(options = nil) + { :exitstatus => exitstatus, :pid => pid } + end +end diff --git a/activesupport/lib/active_support/core_ext/object/to_json.rb b/activesupport/lib/active_support/core_ext/object/to_json.rb index 83cc8066e7..3dcae6fc7f 100644 --- a/activesupport/lib/active_support/core_ext/object/to_json.rb +++ b/activesupport/lib/active_support/core_ext/object/to_json.rb @@ -1,27 +1,5 @@ -# Hack to load json gem first so we can overwrite its to_json. -begin - require 'json' -rescue LoadError -end +ActiveSupport::Deprecation.warn 'You have required `active_support/core_ext/object/to_json`. ' \ + 'This file will be removed in Rails 4.2. You should require `active_support/core_ext/object/json` ' \ + 'instead.' -# The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting -# their default behavior. That said, we need to define the basic to_json method in all of them, -# otherwise they will always use to_json gem implementation, which is backwards incompatible in -# several cases (for instance, the JSON implementation for Hash does not work) with inheritance -# and consequently classes as ActiveSupport::OrderedHash cannot be serialized to json. -[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass].each do |klass| - klass.class_eval do - # Dumps object in JSON (JavaScript Object Notation). See www.json.org for more info. - def to_json(options = nil) - ActiveSupport::JSON.encode(self, options) - end - end -end - -module Process - class Status - def as_json(options = nil) - { :exitstatus => exitstatus, :pid => pid } - end - end -end +require 'active_support/core_ext/object/json' diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index db9f5d4baa..19d4ff51d7 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -8,6 +8,7 @@ require 'active_support/core_ext/module/introspection' require 'active_support/core_ext/module/anonymous' require 'active_support/core_ext/module/qualified_const' require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/kernel/reporting' require 'active_support/core_ext/load_error' require 'active_support/core_ext/name_error' require 'active_support/core_ext/string/starts_ends_with' @@ -198,9 +199,19 @@ module ActiveSupport #:nodoc: Dependencies.require_or_load(file_name) end + # Interprets a file using <tt>mechanism</tt> and marks its defined + # constants as autoloaded. <tt>file_name</tt> can be either a string or + # respond to <tt>to_path</tt>. + # + # Use this method in code that absolutely needs a certain constant to be + # defined at that point. A typical use case is to make constant name + # resolution deterministic for constants with the same relative name in + # different namespaces whose evaluation would depend on load order + # otherwise. def require_dependency(file_name, message = "No such file to load -- %s") + file_name = file_name.to_path if file_name.respond_to?(:to_path) unless file_name.is_a?(String) - raise ArgumentError, "the file name must be a String -- you passed #{file_name.inspect}" + raise ArgumentError, "the file name must either be a String or implement #to_path -- you passed #{file_name.inspect}" end Dependencies.depend_on(file_name, message) diff --git a/activesupport/lib/active_support/file_update_checker.rb b/activesupport/lib/active_support/file_update_checker.rb index d6918bede2..78b627c286 100644 --- a/activesupport/lib/active_support/file_update_checker.rb +++ b/activesupport/lib/active_support/file_update_checker.rb @@ -92,7 +92,7 @@ module ActiveSupport def watched @watched || begin - all = @files.select { |f| File.exists?(f) } + all = @files.select { |f| File.exist?(f) } all.concat(Dir[@glob]) if @glob all end diff --git a/activesupport/lib/active_support/json/decoding.rb b/activesupport/lib/active_support/json/decoding.rb index 21de09c1cc..315c76199a 100644 --- a/activesupport/lib/active_support/json/decoding.rb +++ b/activesupport/lib/active_support/json/decoding.rb @@ -14,7 +14,14 @@ module ActiveSupport # ActiveSupport::JSON.decode("{\"team\":\"rails\",\"players\":\"36\"}") # => {"team" => "rails", "players" => "36"} def decode(json, options = {}) - data = ::JSON.parse(json, options.merge(create_additions: false, quirks_mode: true)) + if options.present? + raise ArgumentError, "In Rails 4.1, ActiveSupport::JSON.decode no longer " \ + "accepts an options hash for MultiJSON. MultiJSON reached its end of life " \ + "and has been removed." + end + + data = ::JSON.parse(json, quirks_mode: true) + if ActiveSupport.parse_json_times convert_dates_from(data) else diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index 77b5d8d227..4d73672047 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -1,14 +1,12 @@ #encoding: us-ascii -require 'active_support/core_ext/object/to_json' +require 'active_support/core_ext/object/json' require 'active_support/core_ext/module/delegation' -require 'bigdecimal' require 'active_support/core_ext/big_decimal/conversions' # for #to_s require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/object/instance_variables' -require 'time' require 'active_support/core_ext/time/conversions' require 'active_support/core_ext/date_time/conversions' require 'active_support/core_ext/date/conversions' @@ -148,197 +146,3 @@ module ActiveSupport end end end - -class Object - def as_json(options = nil) #:nodoc: - if respond_to?(:to_hash) - to_hash - else - instance_values - end - end -end - -class Struct #:nodoc: - def as_json(options = nil) - Hash[members.zip(values)] - end -end - -class TrueClass - def as_json(options = nil) #:nodoc: - self - end - - def encode_json(encoder) #:nodoc: - to_s - end -end - -class FalseClass - def as_json(options = nil) #:nodoc: - self - end - - def encode_json(encoder) #:nodoc: - to_s - end -end - -class NilClass - def as_json(options = nil) #:nodoc: - self - end - - def encode_json(encoder) #:nodoc: - 'null' - end -end - -class String - def as_json(options = nil) #:nodoc: - self - end - - def encode_json(encoder) #:nodoc: - encoder.escape(self) - end -end - -class Symbol - def as_json(options = nil) #:nodoc: - to_s - end -end - -class Numeric - def as_json(options = nil) #:nodoc: - self - end - - def encode_json(encoder) #:nodoc: - to_s - end -end - -class Float - # Encoding Infinity or NaN to JSON should return "null". The default returns - # "Infinity" or "NaN" which breaks parsing the JSON. E.g. JSON.parse('[NaN]'). - def as_json(options = nil) #:nodoc: - finite? ? self : nil - end -end - -class BigDecimal - # A BigDecimal would be naturally represented as a JSON number. Most libraries, - # however, parse non-integer JSON numbers directly as floats. Clients using - # those libraries would get in general a wrong number and no way to recover - # other than manually inspecting the string with the JSON code itself. - # - # That's why a JSON string is returned. The JSON literal is not numeric, but - # if the other end knows by contract that the data is supposed to be a - # BigDecimal, it still has the chance to post-process the string and get the - # real value. - # - # Use <tt>ActiveSupport.use_standard_json_big_decimal_format = true</tt> to - # override this behavior. - def as_json(options = nil) #:nodoc: - if finite? - ActiveSupport.encode_big_decimal_as_string ? to_s : self - else - nil - end - end -end - -class Regexp - def as_json(options = nil) #:nodoc: - to_s - end -end - -module Enumerable - def as_json(options = nil) #:nodoc: - to_a.as_json(options) - end -end - -class Range - def as_json(options = nil) #:nodoc: - to_s - end -end - -class Array - def as_json(options = nil) #:nodoc: - # use encoder as a proxy to call as_json on all elements, to protect from circular references - encoder = options && options[:encoder] || ActiveSupport::JSON::Encoding::Encoder.new(options) - map { |v| encoder.as_json(v, options) } - end - - def encode_json(encoder) #:nodoc: - # we assume here that the encoder has already run as_json on self and the elements, so we run encode_json directly - "[#{map { |v| v.encode_json(encoder) } * ','}]" - end -end - -class Hash - def as_json(options = nil) #:nodoc: - # create a subset of the hash by applying :only or :except - subset = if options - if attrs = options[:only] - slice(*Array(attrs)) - elsif attrs = options[:except] - except(*Array(attrs)) - else - self - end - else - self - end - - # use encoder as a proxy to call as_json on all values in the subset, to protect from circular references - encoder = options && options[:encoder] || ActiveSupport::JSON::Encoding::Encoder.new(options) - Hash[subset.map { |k, v| [k.to_s, encoder.as_json(v, options)] }] - end - - def encode_json(encoder) #:nodoc: - # values are encoded with use_options = false, because we don't want hash representations from ActiveModel to be - # processed once again with as_json with options, as this could cause unexpected results (i.e. missing fields); - - # on the other hand, we need to run as_json on the elements, because the model representation may contain fields - # like Time/Date in their original (not jsonified) form, etc. - - "{#{map { |k,v| "#{encoder.encode(k.to_s)}:#{encoder.encode(v, false)}" } * ','}}" - end -end - -class Time - def as_json(options = nil) #:nodoc: - if ActiveSupport.use_standard_json_time_format - xmlschema - else - %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)}) - end - end -end - -class Date - def as_json(options = nil) #:nodoc: - if ActiveSupport.use_standard_json_time_format - strftime("%Y-%m-%d") - else - strftime("%Y/%m/%d") - end - end -end - -class DateTime - def as_json(options = nil) #:nodoc: - if ActiveSupport.use_standard_json_time_format - xmlschema - else - strftime('%Y/%m/%d %H:%M:%S %z') - end - end -end diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb index 99fe03e6d0..8f5fa646e8 100644 --- a/activesupport/lib/active_support/notifications/fanout.rb +++ b/activesupport/lib/active_support/notifications/fanout.rb @@ -107,21 +107,18 @@ module ActiveSupport end class Timed < Evented - def initialize(pattern, delegate) - @timestack = [] - super - end - def publish(name, *args) @delegate.call name, *args end def start(name, id, payload) - @timestack.push Time.now + timestack = Thread.current[:_timestack] ||= [] + timestack.push Time.now end def finish(name, id, payload) - started = @timestack.pop + timestack = Thread.current[:_timestack] + started = timestack.pop @delegate.call(name, started, Time.now, id, payload) end end diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb index c9c0eff2bf..e0151baa36 100644 --- a/activesupport/lib/active_support/number_helper.rb +++ b/activesupport/lib/active_support/number_helper.rb @@ -108,7 +108,7 @@ module ActiveSupport DECIMAL_UNITS = { 0 => :unit, 1 => :ten, 2 => :hundred, 3 => :thousand, 6 => :million, 9 => :billion, 12 => :trillion, 15 => :quadrillion, -1 => :deci, -2 => :centi, -3 => :mili, -6 => :micro, -9 => :nano, -12 => :pico, -15 => :femto } - + INVERTED_DECIMAL_UNITS = DECIMAL_UNITS.invert STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb] # Formats a +number+ into a US phone number (e.g., (555) @@ -561,8 +561,6 @@ module ActiveSupport #for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros) - inverted_du = DECIMAL_UNITS.invert - units = options.delete :units unit_exponents = case units when Hash @@ -573,7 +571,7 @@ module ActiveSupport translate_number_value_with_default("human.decimal_units.units", :locale => options[:locale], :raise => true) else raise ArgumentError, ":units must be a Hash or String translation scope." - end.keys.map{|e_name| inverted_du[e_name] }.sort_by{|e| -e} + end.keys.map!{|e_name| INVERTED_DECIMAL_UNITS[e_name] }.sort_by!{|e| -e} number_exponent = number != 0 ? Math.log10(number.abs).floor : 0 display_exponent = unit_exponents.find{ |e| number_exponent >= e } || 0 diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb index 2f27aff2bd..5e24118d34 100644 --- a/activesupport/lib/active_support/test_case.rb +++ b/activesupport/lib/active_support/test_case.rb @@ -22,7 +22,7 @@ module Minitest # :nodoc: def self.__run reporter, options # :nodoc: # FIXME: MT5's runnables is not ordered. This is needed because - # we have have tests have cross-class order-dependent bugs. + # we have tests with cross-class order-dependent bugs. suites = Runnable.runnables.sort_by { |ts| ts.name.to_s } parallel, serial = suites.partition { |s| s.test_order == :parallel } diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index 16d087ab9d..51007402a1 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -721,6 +721,13 @@ class FileStoreTest < ActiveSupport::TestCase assert @cache.exist?('baz') assert @cache.exist?('quux') end + + def test_write_with_unless_exist + assert_equal true, @cache.write(1, "aaaaaaaaaa") + assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true) + @cache.write(1, nil) + assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true) + end end class MemoryStoreTest < ActiveSupport::TestCase diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index 2d0c56bef5..b059bc3e89 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -781,6 +781,24 @@ class HashExtTest < ActiveSupport::TestCase assert_equal 'bender', slice['login'] end + def test_slice_bang_does_not_override_default + hash = Hash.new(0) + hash.update(a: 1, b: 2) + + hash.slice!(:a) + + assert_equal 0, hash[:c] + end + + def test_slice_bang_does_not_override_default_proc + hash = Hash.new { |h, k| h[k] = [] } + hash.update(a: 1, b: 2) + + hash.slice!(:a) + + assert_equal [], hash[:c] + end + def test_extract original = {:a => 1, :b => 2, :c => 3, :d => 4} expected = {:a => 1, :b => 2} diff --git a/activesupport/test/core_ext/numeric_ext_test.rb b/activesupport/test/core_ext/numeric_ext_test.rb index 1da72eb17d..23bbb8d7d2 100644 --- a/activesupport/test/core_ext/numeric_ext_test.rb +++ b/activesupport/test/core_ext/numeric_ext_test.rb @@ -440,4 +440,8 @@ class NumericExtFormattingTest < ActiveSupport::TestCase assert_equal BigDecimal, BigDecimal("1000010").class assert_equal '1 Million', BigDecimal("1000010").to_s(:human) end + + def test_in_milliseconds + assert_equal 10_000, 10.seconds.in_milliseconds + end end diff --git a/activesupport/test/core_ext/object/json_test.rb b/activesupport/test/core_ext/object/json_test.rb new file mode 100644 index 0000000000..d3d31530df --- /dev/null +++ b/activesupport/test/core_ext/object/json_test.rb @@ -0,0 +1,9 @@ +require 'abstract_unit' + +class JsonTest < ActiveSupport::TestCase + # See activesupport/test/json/encoding_test.rb for JSON encoding tests + + def test_deprecated_require_to_json_rb + assert_deprecated { require 'active_support/core_ext/object/to_json' } + end +end diff --git a/activesupport/test/core_ext/thread_test.rb b/activesupport/test/core_ext/thread_test.rb index 54d2dcd8dd..6a7c6e0604 100644 --- a/activesupport/test/core_ext/thread_test.rb +++ b/activesupport/test/core_ext/thread_test.rb @@ -72,17 +72,4 @@ class ThreadExt < ActiveSupport::TestCase end end - def test_thread_variable_security - rubinius_skip "$SAFE is not supported on Rubinius." - - t = Thread.new { sleep } - - assert_raises(SecurityError) do - Thread.new { $SAFE = 4; t.thread_variable_get(:foo) }.join - end - - assert_raises(SecurityError) do - Thread.new { $SAFE = 4; t.thread_variable_set(:foo, :baz) }.join - end - end end diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index 9b84e75902..2392b71960 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -35,6 +35,17 @@ class DependenciesTest < ActiveSupport::TestCase assert_equal expected.path, e.path end + def test_require_dependency_accepts_an_object_which_implements_to_path + o = Object.new + def o.to_path; 'dependencies/service_one'; end + assert_nothing_raised { + require_dependency o + } + assert defined?(ServiceOne) + ensure + remove_constants(:ServiceOne) + end + def test_tracking_loaded_files require_dependency 'dependencies/service_one' require_dependency 'dependencies/service_two' diff --git a/activesupport/test/descendants_tracker_without_autoloading_test.rb b/activesupport/test/descendants_tracker_without_autoloading_test.rb index 74669aaca1..00b449af51 100644 --- a/activesupport/test/descendants_tracker_without_autoloading_test.rb +++ b/activesupport/test/descendants_tracker_without_autoloading_test.rb @@ -4,4 +4,14 @@ require 'descendants_tracker_test_cases' class DescendantsTrackerWithoutAutoloadingTest < ActiveSupport::TestCase include DescendantsTrackerTestCases + + # Regression test for #8422. https://github.com/rails/rails/issues/8442 + def test_clear_without_autoloaded_singleton_parent + mark_as_autoloaded do + parent_instance = Parent.new + parent_instance.singleton_class.descendants + ActiveSupport::DescendantsTracker.clear + assert !ActiveSupport::DescendantsTracker.class_variable_get(:@@direct_descendants).key?(parent_instance.singleton_class) + end + end end diff --git a/activesupport/test/json/decoding_test.rb b/activesupport/test/json/decoding_test.rb index e9780b36e4..07d7e530ca 100644 --- a/activesupport/test/json/decoding_test.rb +++ b/activesupport/test/json/decoding_test.rb @@ -98,10 +98,8 @@ class TestJSONDecoding < ActiveSupport::TestCase assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%()) } end - def test_cannot_force_json_unmarshalling - encodeded = %q({"json_class":"TestJSONDecoding::Foo"}) - decodeded = {"json_class"=>"TestJSONDecoding::Foo"} - assert_equal decodeded, ActiveSupport::JSON.decode(encodeded, create_additions: true) + def test_cannot_pass_unsupported_options + assert_raise(ArgumentError) { ActiveSupport::JSON.decode("", create_additions: true) } end end diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb index ed1326705c..d549113ff4 100644 --- a/activesupport/test/json/encoding_test.rb +++ b/activesupport/test/json/encoding_test.rb @@ -1,4 +1,5 @@ # encoding: utf-8 +require 'securerandom' require 'abstract_unit' require 'active_support/core_ext/string/inflections' require 'active_support/json' @@ -96,6 +97,13 @@ class TestJSONEncoding < ActiveSupport::TestCase end end + def test_process_status + # There doesn't seem to be a good way to get a handle on a Process::Status object without actually + # creating a child process, hence this to populate $? + system("not_a_real_program_#{SecureRandom.hex}") + assert_equal %({"exitstatus":#{$?.exitstatus},"pid":#{$?.pid}}), ActiveSupport::JSON.encode($?) + end + def test_hash_encoding assert_equal %({\"a\":\"b\"}), ActiveSupport::JSON.encode(:a => :b) assert_equal %({\"a\":1}), ActiveSupport::JSON.encode('a' => 1) diff --git a/activesupport/test/ordered_hash_test.rb b/activesupport/test/ordered_hash_test.rb index c3fe89de4b..0b54026c64 100644 --- a/activesupport/test/ordered_hash_test.rb +++ b/activesupport/test/ordered_hash_test.rb @@ -1,6 +1,6 @@ require 'abstract_unit' require 'active_support/json' -require 'active_support/core_ext/object/to_json' +require 'active_support/core_ext/object/json' require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/array/extract_options' diff --git a/guides/CHANGELOG.md b/guides/CHANGELOG.md index afa695d445..38e407b198 100644 --- a/guides/CHANGELOG.md +++ b/guides/CHANGELOG.md @@ -2,4 +2,8 @@ *Sıtkı Bağdat* +* Added the Rails maintenance policy to the guides. + + *Matias Korhonen* + Please check [4-0-stable](https://github.com/rails/rails/blob/4-0-stable/guides/CHANGELOG.md) for previous changes. diff --git a/guides/bug_report_templates/action_controller_master.rb b/guides/bug_report_templates/action_controller_master.rb index 375ed9bc5d..5d88749118 100644 --- a/guides/bug_report_templates/action_controller_master.rb +++ b/guides/bug_report_templates/action_controller_master.rb @@ -1,4 +1,4 @@ -unless File.exists?('Gemfile') +unless File.exist?('Gemfile') File.write('Gemfile', <<-GEMFILE) source 'https://rubygems.org' gem 'rails', github: 'rails/rails' @@ -48,4 +48,4 @@ class BugTest < Minitest::Test def app Rails.application end -end
\ No newline at end of file +end diff --git a/guides/bug_report_templates/active_record_master.rb b/guides/bug_report_templates/active_record_master.rb index 29dedd88d0..2435444dc9 100644 --- a/guides/bug_report_templates/active_record_master.rb +++ b/guides/bug_report_templates/active_record_master.rb @@ -1,4 +1,4 @@ -unless File.exists?('Gemfile') +unless File.exist?('Gemfile') File.write('Gemfile', <<-GEMFILE) source 'https://rubygems.org' gem 'rails', github: 'rails/rails' diff --git a/guides/code/getting_started/config/boot.rb b/guides/code/getting_started/config/boot.rb index 3596736667..5e5f0c1fac 100644 --- a/guides/code/getting_started/config/boot.rb +++ b/guides/code/getting_started/config/boot.rb @@ -1,4 +1,4 @@ # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) -require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) +require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) diff --git a/guides/rails_guides.rb b/guides/rails_guides.rb index 33975718c9..9d488a8a15 100644 --- a/guides/rails_guides.rb +++ b/guides/rails_guides.rb @@ -5,7 +5,7 @@ $:.unshift pwd def bundler? # Note that rake sets the cwd to the one that contains the Rakefile # being executed. - File.exists?('Gemfile') + File.exist?('Gemfile') end begin diff --git a/guides/rails_guides/generator.rb b/guides/rails_guides/generator.rb index af9c5b8372..aa900454c8 100644 --- a/guides/rails_guides/generator.rb +++ b/guides/rails_guides/generator.rb @@ -184,7 +184,7 @@ module RailsGuides def generate?(source_file, output_file) fin = File.join(source_dir, source_file) fout = output_path_for(output_file) - all || !File.exists?(fout) || File.mtime(fout) < File.mtime(fin) + all || !File.exist?(fout) || File.mtime(fout) < File.mtime(fin) end def generate_guide(guide, output_file) diff --git a/guides/source/_welcome.html.erb b/guides/source/_welcome.html.erb index 0a0a958e30..93c177905c 100644 --- a/guides/source/_welcome.html.erb +++ b/guides/source/_welcome.html.erb @@ -15,7 +15,7 @@ </p> <% end %> <p> - The guides for Rails 3.2.x are available at <a href="http://guides.rubyonrails.org/v3.2.14/">http://guides.rubyonrails.org/v3.2.14/</a>. + The guides for Rails 3.2.x are available at <a href="http://guides.rubyonrails.org/v3.2.15/">http://guides.rubyonrails.org/v3.2.15/</a>. </p> <p> The guides for Rails 2.3.x are available at <a href="http://guides.rubyonrails.org/v2.3.11/">http://guides.rubyonrails.org/v2.3.11/</a>. diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index 8dfecd0190..cd4a1a0792 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -209,7 +209,7 @@ class PeopleController < ActionController::Base # Request reply. def update person = current_account.people.find(params[:id]) - person.update_attributes!(person_params) + person.update!(person_params) redirect_to person end diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index 93a2b89ede..61fd762304 100644 --- a/guides/source/action_mailer_basics.md +++ b/guides/source/action_mailer_basics.md @@ -105,7 +105,7 @@ will be the template used for the email, formatted in HTML: <h1>Welcome to example.com, <%= @user.name %></h1> <p> You have successfully signed up to example.com, - your username is: <%= @user.login %>.<br/> + your username is: <%= @user.login %>.<br> </p> <p> To login to the site, just follow this link: <%= @url %>. diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md index 5cda104138..d19dd11181 100644 --- a/guides/source/action_view_overview.md +++ b/guides/source/action_view_overview.md @@ -68,7 +68,7 @@ Consider the following loop for names: ```html+erb <h1>Names of all the people</h1> <% @people.each do |person| %> - Name: <%= person.name %><br/> + Name: <%= person.name %><br> <% end %> ``` @@ -269,7 +269,7 @@ Rails will render the `_product_ruler` partial (with no data passed to it) betwe ### Layouts -Layouts can be used to render a common view template around the results of Rails controller actions. Typically, every Rails has a couple of overall layouts that most pages are rendered within. For example, a site might have a layout for a logged in user, and a layout for the marketing or sales side of the site. The logged in user layout might include top-level navigation that should be present across many controller actions. The sales layout for a SaaS app might include top-level navigation for things like "Pricing" and "Contact Us." You would expect each layout to have a different look and feel. You can read more details about Layouts in the [Layouts and Rendering in Rails](layouts_and_rendering.html) guide. +Layouts can be used to render a common view template around the results of Rails controller actions. Typically, every Rails application has a couple of overall layouts that most pages are rendered within. For example, a site might have a layout for a logged in user, and a layout for the marketing or sales side of the site. The logged in user layout might include top-level navigation that should be present across many controller actions. The sales layout for a SaaS app might include top-level navigation for things like "Pricing" and "Contact Us." You would expect each layout to have a different look and feel. You can read more details about Layouts in the [Layouts and Rendering in Rails](layouts_and_rendering.html) guide. Partial Layouts --------------- diff --git a/guides/source/active_record_basics.md b/guides/source/active_record_basics.md index 34baae509b..a184f0753d 100644 --- a/guides/source/active_record_basics.md +++ b/guides/source/active_record_basics.md @@ -116,7 +116,7 @@ to Active Record instances: locking](http://api.rubyonrails.org/classes/ActiveRecord/Locking.html) to a model. * `type` - Specifies that the model uses [Single Table - Inheritance](http://api.rubyonrails.org/classes/ActiveRecord/Base.html). + Inheritance](http://api.rubyonrails.org/classes/ActiveRecord/Base.html#label-Single+table+inheritance). * `(association_name)_type` - Stores the type for [polymorphic associations](association_basics.html#polymorphic-associations). * `(table_name)_count` - Used to cache the number of belonging objects on diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index aa2ce99f6d..5cc6ca5798 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -204,7 +204,7 @@ As you start registering new callbacks for your models, they will be queued for The whole callback chain is wrapped in a transaction. If any _before_ callback method returns exactly `false` or raises an exception, the execution chain gets halted and a ROLLBACK is issued; _after_ callbacks can only accomplish that by raising an exception. -WARNING. Raising an arbitrary exception may break code that expects `save` and its friends not to fail like that. The `ActiveRecord::Rollback` exception is thought precisely to tell Active Record a rollback is going on. That one is internally captured but not reraised. +WARNING. Any exception that is not `ActiveRecord::Rollback` will be re-raised by Rails after the callback chain is halted. Raising an exception other than `ActiveRecord::Rollback` may break code that does not expect methods like `save` and `update_attributes` (which normally try to return `true` or `false`) to raise an exception. Relational Callbacks -------------------- diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index faa37efd37..57e8e080f4 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -1189,7 +1189,7 @@ class Post < ActiveRecord::Base end ``` -This may then be called using this: +Call the scope as if it were a class method: ```ruby Post.created_before(Time.zone.now) diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md index 0b2f0a47fa..0df52a655f 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -438,8 +438,6 @@ provide a personalized message or use `presence: true` instead. When `:in` or `:within` have a lower limit of 1, you should either provide a personalized message or call `presence` prior to `length`. -The `size` helper is an alias for `length`. - ### `numericality` This helper validates that your attributes have only numeric values. By @@ -528,7 +526,7 @@ If you validate the presence of an object associated via a `has_one` or Since `false.blank?` is true, if you want to validate the presence of a boolean field you should use `validates :field_name, inclusion: { in: [true, false] }`. -The default error message is _"can't be empty"_. +The default error message is _"can't be blank"_. ### `absence` @@ -783,7 +781,7 @@ end Person.new.valid? # => ActiveModel::StrictValidationFailed: Name can't be blank ``` -There is also an ability to pass custom exception to `:strict` option +There is also an ability to pass custom exception to `:strict` option. ```ruby class Person < ActiveRecord::Base diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md index e6b849e4c9..0370e40012 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -96,12 +96,13 @@ INFO: The predicate for strings uses the Unicode-aware character class `[:space: WARNING: Note that numbers are not mentioned. In particular, 0 and 0.0 are **not** blank. -For example, this method from `ActionDispatch::Session::AbstractStore` uses `blank?` for checking whether a session key is present: +For example, this method from `ActionController::HttpAuthentication::Token::ControllerMethods` uses `blank?` for checking whether a token is present: ```ruby -def ensure_session_key! - if @key.blank? - raise ArgumentError, 'A key is required...' +def authenticate(controller, &login_procedure) + token, options = token_and_options(controller.request) + unless token.blank? + login_procedure.call(token, options) end end ``` @@ -420,11 +421,9 @@ NOTE: Defined in `active_support/core_ext/object/with_options.rb`. ### JSON support -Active Support provides a better implementation of `to_json` than the +json+ gem ordinarily provides for Ruby objects. This is because some classes, like +Hash+ and +OrderedHash+ needs special handling in order to provide a proper JSON representation. +Active Support provides a better implementation of `to_json` than the +json+ gem ordinarily provides for Ruby objects. This is because some classes, like +Hash+, +OrderedHash+, and +Process::Status+ need special handling in order to provide a proper JSON representation. -Active Support also provides an implementation of `as_json` for the <tt>Process::Status</tt> class. - -NOTE: Defined in `active_support/core_ext/object/to_json.rb`. +NOTE: Defined in `active_support/core_ext/object/json.rb`. ### Instance Variables @@ -1999,7 +1998,7 @@ Produce a string representation of a number in human-readable words: 1234567890123456.to_s(:human) # => "1.23 Quadrillion" ``` -NOTE: Defined in `active_support/core_ext/numeric/formatting.rb`. +NOTE: Defined in `active_support/core_ext/numeric/conversions.rb`. Extensions to `Integer` ----------------------- @@ -2444,7 +2443,7 @@ dup[1][2] = 4 array[1][2] == nil # => true ``` -NOTE: Defined in `active_support/core_ext/array/deep_dup.rb`. +NOTE: Defined in `active_support/core_ext/object/deep_dup.rb`. ### Grouping @@ -2670,45 +2669,7 @@ hash[:b][:e] == nil # => true hash[:b][:d] == [3, 4] # => true ``` -NOTE: Defined in `active_support/core_ext/hash/deep_dup.rb`. - -### Diffing - -The method `diff` returns a hash that represents a diff of the receiver and the argument with the following logic: - -* Pairs `key`, `value` that exist in both hashes do not belong to the diff hash. - -* If both hashes have `key`, but with different values, the pair in the receiver wins. - -* The rest is just merged. - -```ruby -{a: 1}.diff(a: 1) -# => {}, first rule - -{a: 1}.diff(a: 2) -# => {:a=>1}, second rule - -{a: 1}.diff(b: 2) -# => {:a=>1, :b=>2}, third rule - -{a: 1, b: 2, c: 3}.diff(b: 1, c: 3, d: 4) -# => {:a=>1, :b=>2, :d=>4}, all rules - -{}.diff({}) # => {} -{a: 1}.diff({}) # => {:a=>1} -{}.diff(a: 1) # => {:a=>1} -``` - -An important property of this diff hash is that you can retrieve the original hash by applying `diff` twice: - -```ruby -hash.diff(hash2).diff(hash2) == hash -``` - -Diffing hashes may be useful for error messages related to expected option hashes for example. - -NOTE: Defined in `active_support/core_ext/hash/diff.rb`. +NOTE: Defined in `active_support/core_ext/object/deep_dup.rb`. ### Working with Keys @@ -3843,13 +3804,13 @@ def default_helper_module! module_path = module_name.underscore helper module_path rescue MissingSourceFile => e - raise e unless e.is_missing? "#{module_path}_helper" + raise e unless e.is_missing? "helpers/#{module_path}_helper" rescue NameError => e raise e unless e.missing_name? "#{module_name}Helper" end ``` -NOTE: Defined in `active_support/core_ext/name_error.rb`. +NOTE: Defined in `actionpack/lib/abstract_controller/helpers.rb`. Extensions to `LoadError` ------------------------- @@ -3872,4 +3833,4 @@ rescue NameError => e end ``` -NOTE: Defined in `active_support/core_ext/load_error.rb`. +NOTE: Defined in `actionpack/lib/abstract_controller/helpers.rb`. diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index 72aff1e0dd..e9d3712a2a 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -405,11 +405,10 @@ JavaScript and stylesheet. * `image-url("rails.png")` becomes `url(/assets/rails.png)` * `image-path("rails.png")` becomes `"/assets/rails.png"`. -The more generic form can also be used but the asset path and class must both be -specified: +The more generic form can also be used: -* `asset-url("rails.png", image)` becomes `url(/assets/rails.png)` -* `asset-path("rails.png", image)` becomes `"/assets/rails.png"` +* `asset-url("rails.png")` becomes `url(/assets/rails.png)` +* `asset-path("rails.png")` becomes `"/assets/rails.png"` #### JavaScript/CoffeeScript and ERB @@ -503,7 +502,11 @@ NOTE. If you want to use multiple Sass files, you should generally use the [Sass rule](http://sass-lang.com/docs/yardoc/file.SASS_REFERENCE.html#import) instead of these Sprockets directives. Using Sprockets directives all Sass files exist within their own scope, making variables or mixins only available within the -document they were defined in. +document they were defined in. You can do file globbing as well using +`@import "*"`, and `@import "**/*"` to add the whole tree equivalent to how +`require_tree` works. Check the [sass-rails +documentation](https://github.com/rails/sass-rails#features) for more info and +important caveats. You can have as many manifest files as you need. For example, the `admin.css` and `admin.js` manifest could contain the JS and CSS files that are used for the @@ -1040,17 +1043,22 @@ Making Your Library or Gem a Pre-Processor As Sprockets uses [Tilt](https://github.com/rtomayko/tilt) as a generic interface to different templating engines, your gem should just implement the Tilt template protocol. Normally, you would subclass `Tilt::Template` and -reimplement `evaluate` method to return final output. Template source is stored -at `@code`. Have a look at +reimplement the `prepare` method, which initializes your template, and the +`evaluate` method, which returns the processed source. The original source is +stored in `data`. Have a look at [`Tilt::Template`](https://github.com/rtomayko/tilt/blob/master/lib/tilt/template.rb) sources to learn more. ```ruby module BangBang class Template < ::Tilt::Template + def prepare + # Do any initialization here + end + # Adds a "!" to original template. def evaluate(scope, locals, &block) - "#{@code}!" + "#{data}!" end end end diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index 91b268d766..c0482f6106 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -718,7 +718,7 @@ The `belongs_to` association creates a one-to-one match with another model. In d #### Methods Added by `belongs_to` -When you declare a `belongs_to` association, the declaring class automatically gains four methods related to the association: +When you declare a `belongs_to` association, the declaring class automatically gains five methods related to the association: * `association(force_reload = false)` * `association=(associate)` @@ -1022,7 +1022,7 @@ The `has_one` association creates a one-to-one match with another model. In data #### Methods Added by `has_one` -When you declare a `has_one` association, the declaring class automatically gains four methods related to the association: +When you declare a `has_one` association, the declaring class automatically gains five methods related to the association: * `association(force_reload = false)` * `association=(associate)` @@ -1140,10 +1140,10 @@ Controls what happens to the associated object when its owner is destroyed: * `:restrict_with_exception` causes an exception to be raised if there is an associated record * `:restrict_with_error` causes an error to be added to the owner if there is an associated object -It's necessary not to set or leave `:nullify` option for those associations -that have `NOT NULL` database constraints. If you don't set `dependent` to -destroy such associations you won't be able to change the associated object -because initial associated object foreign key will be set to unallowed `NULL` +It's necessary not to set or leave `:nullify` option for those associations +that have `NOT NULL` database constraints. If you don't set `dependent` to +destroy such associations you won't be able to change the associated object +because initial associated object foreign key will be set to unallowed `NULL` value. ##### `:foreign_key` @@ -1289,7 +1289,7 @@ The `has_many` association creates a one-to-many relationship with another model #### Methods Added by `has_many` -When you declare a `has_many` association, the declaring class automatically gains 13 methods related to the association: +When you declare a `has_many` association, the declaring class automatically gains 16 methods related to the association: * `collection(force_reload = false)` * `collection<<(object, ...)` @@ -1778,7 +1778,7 @@ The `has_and_belongs_to_many` association creates a many-to-many relationship wi #### Methods Added by `has_and_belongs_to_many` -When you declare a `has_and_belongs_to_many` association, the declaring class automatically gains 13 methods related to the association: +When you declare a `has_and_belongs_to_many` association, the declaring class automatically gains 16 methods related to the association: * `collection(force_reload = false)` * `collection<<(object, ...)` diff --git a/guides/source/command_line.md b/guides/source/command_line.md index ef645c3d2d..1b0b93c3bc 100644 --- a/guides/source/command_line.md +++ b/guides/source/command_line.md @@ -379,13 +379,14 @@ About your application's environment Ruby version 1.9.3 (x86_64-linux) RubyGems version 1.3.6 Rack version 1.3 -Rails version 4.0.0 +Rails version 4.1.0 JavaScript Runtime Node.js (V8) -Active Record version 4.0.0 -Action Pack version 4.0.0 -Action Mailer version 4.0.0 -Active Support version 4.0.0 -Middleware Rack::Sendfile, ActionDispatch::Static, Rack::Lock, #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007ffd131a7c88>, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions, ActionDispatch::RemoteIp, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::Migration::CheckPending, ActiveRecord::ConnectionAdapters::ConnectionManagement, ActiveRecord::QueryCache, ActionDispatch::Cookies, ActionDispatch::Session::EncryptedCookieStore, ActionDispatch::Flash, ActionDispatch::ParamsParser, Rack::Head, Rack::ConditionalGet, Rack::ETag +Active Record version 4.1.0 +Action Pack version 4.1.0 +Action View version 4.1.0 +Action Mailer version 4.1.0 +Active Support version 4.1.0 +Middleware Rack::Sendfile, ActionDispatch::Static, Rack::Lock, #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007ffd131a7c88>, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions, ActionDispatch::RemoteIp, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::Migration::CheckPending, ActiveRecord::ConnectionAdapters::ConnectionManagement, ActiveRecord::QueryCache, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, ActionDispatch::ParamsParser, Rack::Head, Rack::ConditionalGet, Rack::ETag Application root /home/foobar/commandsapp Environment development Database adapter sqlite3 diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 660d730b9a..8ac34c9716 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -261,6 +261,8 @@ config.middleware.delete "Rack::MethodOverride" * `config.active_record.table_name_suffix` lets you set a global string to be appended to table names. If you set this to `_northwest`, then the Customer class will look for `customers_northwest` as its table. The default is an empty string. +* `config.active_record.schema_migrations_table_name` lets you set a string to be used as the name of the schema migrations table. + * `config.active_record.pluralize_table_names` specifies whether Rails will look for singular or plural table names in the database. If set to true (the default), then the Customer class will use the `customers` table. If set to false, then the Customer class will use the `customer` table. * `config.active_record.default_timezone` determines whether to use `Time.local` (if set to `:local`) or `Time.utc` (if set to `:utc`) when pulling dates and times from the database. The default is `:utc` for Rails, although Active Record defaults to `:local` when used outside of Rails. @@ -273,6 +275,12 @@ config.middleware.delete "Rack::MethodOverride" * `config.active_record.cache_timestamp_format` controls the format of the timestamp value in the cache key. Default is `:number`. +* `config.active_record.record_timestamps` is a boolean value which controls whether or not timestamping of `create` and `update` operations on a model occur. The default value is `true`. + +* `config.active_record.partial_writes` is a boolean value and controls whether or not partial writes are used (i.e. whether updates only set attributes that are dirty). Note that when using partial writes, you should also use optimistic locking `config.active_record.lock_optimistically` since concurrent updates may write attributes based on a possibly stale read state. The default value is `true`. + +* `config.active_record.attribute_types_cached_by_default` sets the attribute types that `ActiveRecord::AttributeMethods` will cache by default on reads. The default is `[:datetime, :timestamp, :time, :date]`. + The MySQL adapter adds one additional configuration option: * `ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans` controls whether Active Record will consider all `tinyint(1)` columns in a MySQL database to be booleans and is true by default. @@ -303,7 +311,7 @@ The schema dumper adds one additional configuration option: * `config.action_controller.permit_all_parameters` sets all the parameters for mass assignment to be permitted by default. The default value is `false`. -* `config.action_controller.action_on_unpermitted_params` enables logging or raising an exception if parameters that are not explicitly permitted are found. Set to `:log` or `:raise` to enable. The default value is `:log` in development and test environments, and `false` in all other environments. +* `config.action_controller.action_on_unpermitted_parameters` enables logging or raising an exception if parameters that are not explicitly permitted are found. Set to `:log` or `:raise` to enable. The default value is `:log` in development and test environments, and `false` in all other environments. ### Configuring Action Dispatch @@ -604,7 +612,7 @@ Rails has 5 initialization events which can be hooked into (listed in the order * `before_eager_load`: This is run directly before eager loading occurs, which is the default behavior for the `production` environment and not for the `development` environment. -* `after_initialize`: Run directly after the initialization of the application, but before the application initializers are run. +* `after_initialize`: Run directly after the initialization of the application, after the application initializers in `config/initializers` are run. To define an event for these hooks, use the block syntax within a `Rails::Application`, `Rails::Railtie` or `Rails::Engine` subclass: diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md index b2b08c82c6..a6956eb009 100644 --- a/guides/source/contributing_to_ruby_on_rails.md +++ b/guides/source/contributing_to_ruby_on_rails.md @@ -430,13 +430,18 @@ $ git push origin branch_name ### Issue a Pull Request -Navigate to the Rails repository you just pushed to (e.g. https://github.com/your-user-name/rails) and press "Pull Request" in the upper right hand corner. - -Write your branch name in the branch field (this is filled with "master" by default) and press "Update Commit Range". - -Ensure the changesets you introduced are included in the "Commits" tab. Ensure that the "Files Changed" incorporate all of your changes. - -Fill in some details about your potential patch including a meaningful title. When finished, press "Send pull request". The Rails core team will be notified about your submission. +Navigate to the Rails repository you just pushed to (e.g. +https://github.com/your-user-name/rails) and click on "Pull Requests" seen in +the right panel. On the next page, press "New pull request" in the upper right +hand corner. + +Click on "Edit", if you need to change the branches being compared (it compares +"master" by default) and press "Click to create a pull request for this +comparison". + +Ensure the changesets you introduced are included. Fill in some details about +your potential patch including a meaningful title. When finished, press "Send +pull request". The Rails core team will be notified about your submission. ### Get some Feedback diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml index 1b16f4e516..1bf9ff95e1 100644 --- a/guides/source/documents.yaml +++ b/guides/source/documents.yaml @@ -150,6 +150,13 @@ url: ruby_on_rails_guides_guidelines.html description: This guide documents the Ruby on Rails guides guidelines. - + name: Maintenance Policy + documents: + - + name: Maintenance Policy + url: maintenance_policy.html + description: What versions of Ruby on Rails are currently supported, and when to expect new versions. +- name: Release Notes documents: - diff --git a/guides/source/generators.md b/guides/source/generators.md index e06b13deba..e9c8ef0225 100644 --- a/guides/source/generators.md +++ b/guides/source/generators.md @@ -207,7 +207,7 @@ $ rails generate scaffold User name:string Looking at this output, it's easy to understand how generators work in Rails 3.0 and above. The scaffold generator doesn't actually generate anything, it just invokes others to do the work. This allows us to add/replace/remove any of those invocations. For instance, the scaffold generator invokes the scaffold_controller generator, which invokes erb, test_unit and helper generators. Since each generator has a single responsibility, they are easy to reuse, avoiding code duplication. -Our first customization on the workflow will be to stop generating stylesheets and test fixtures for scaffolds. We can achieve that by changing our configuration to the following: +Our first customization on the workflow will be to stop generating stylesheets, javascripts and test fixtures for scaffolds. We can achieve that by changing our configuration to the following: ```ruby config.generators do |g| @@ -215,10 +215,11 @@ config.generators do |g| g.template_engine :erb g.test_framework :test_unit, fixture: false g.stylesheets false + g.javascripts false end ``` -If we generate another resource with the scaffold generator, we can see that neither stylesheets nor fixtures are created anymore. If you want to customize it further, for example to use DataMapper and RSpec instead of Active Record and TestUnit, it's just a matter of adding their gems to your application and configuring your generators. +If we generate another resource with the scaffold generator, we can see that stylesheets, javascripts and fixtures are not created anymore. If you want to customize it further, for example to use DataMapper and RSpec instead of Active Record and TestUnit, it's just a matter of adding their gems to your application and configuring your generators. To demonstrate this, we are going to create a new helper generator that simply adds some instance variable readers. First, we create a generator within the rails namespace, as this is where rails searches for generators used as hooks: @@ -270,6 +271,7 @@ config.generators do |g| g.template_engine :erb g.test_framework :test_unit, fixture: false g.stylesheets false + g.javascripts false g.helper :my_helper end ``` @@ -334,6 +336,7 @@ config.generators do |g| g.template_engine :erb g.test_framework :test_unit, fixture: false g.stylesheets false + g.javascripts false end ``` @@ -352,6 +355,7 @@ config.generators do |g| g.template_engine :erb g.test_framework :shoulda, fixture: false g.stylesheets false + g.javascripts false # Add a fallback! g.fallbacks[:shoulda] = :test_unit diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index 0a95fbec45..2f322d15da 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -1134,7 +1134,7 @@ appear next to the "Show" link: <tr> <td><%= post.title %></td> <td><%= post.text %></td> - <td><%= link_to 'Show', post %></td> + <td><%= link_to 'Show', post_path(post) %></td> <td><%= link_to 'Edit', edit_post_path(post) %></td> </tr> <% end %> @@ -1490,8 +1490,8 @@ So first, we'll wire up the Post show template </p> <% end %> -<%= link_to 'Edit Post', edit_post_path(@post) %> | -<%= link_to 'Back to Posts', posts_path %> +<%= link_to 'Back', posts_path %> +| <%= link_to 'Edit', edit_post_path(@post) %> ``` This adds a form on the `Post` show page that creates a new comment by diff --git a/guides/source/initialization.md b/guides/source/initialization.md index 91d12b4432..fe6b1ad906 100644 --- a/guides/source/initialization.md +++ b/guides/source/initialization.md @@ -468,14 +468,24 @@ def initialize!(group=:default) #:nodoc: end ``` -As you can see, you can only initialize an app once. This is also where the initializers are run. +As you can see, you can only initialize an app once. The initializers are run through +the `run_initializers` method which is defined in `railties/lib/rails/initializable.rb` -TODO: review this +```ruby +def run_initializers(group=:default, *args) + return if instance_variable_defined?(:@ran) + initializers.tsort_each do |initializer| + initializer.run(*args) if initializer.belongs_to?(group) + end + @ran = true +end +``` -The initializers code itself is tricky. What Rails is doing here is it -traverses all the class ancestors looking for an `initializers` method, -sorting them and running them. For example, the `Engine` class will make -all the engines available by providing the `initializers` method. +The run_initializers code itself is tricky. What Rails is doing here is +traversing all the class ancestors looking for those that respond to an +`initializers` method. It then sorts the ancestors by name, and runs them. +For example, the `Engine` class will make all the engines available by +providing an `initializers` method on them. The `Rails::Application` class, as defined in `railties/lib/rails/application.rb` defines `bootstrap`, `railtie`, and `finisher` initializers. The `bootstrap` initializers diff --git a/guides/source/maintenance_policy.md b/guides/source/maintenance_policy.md new file mode 100644 index 0000000000..93729c6f72 --- /dev/null +++ b/guides/source/maintenance_policy.md @@ -0,0 +1,56 @@ +Maintenance Policy for Ruby on Rails +==================================== + +Support of the Rails framework is divided into four groups: New features, bug +fixes, security issues, and severe security issues. They are handled as +follows, all versions in x.y.z format + +-------------------------------------------------------------------------------- + +New Features +------------ + +New features are only added to the master branch and will not be made available +in point releases. + +Bug Fixes +--------- + +Only the latest release series will receive bug fixes. When enough bugs are +fixed and its deemed worthy to release a new gem, this is the branch it happens +from. + +**Currently included series:** 4.0.z + +Security Issues +--------------- + +The current release series and the next most recent one will receive patches +and new versions in case of a security issue. + +These releases are created by taking the last released version, applying the +security patches, and releasing. Those patches are then applied to the end of +the x-y-stable branch. For example, a theoretical 1.2.3 security release would +be built from 1.2.2, and then added to the end of 1-2-stable. This means that +security releases are easy to upgrade to if you're running the latest version +of Rails. + +**Currently included series:** 4.0.z, 3.2.z + +Severe Security Issues +---------------------- + +For severe security issues we will provide new versions as above, and also the +last major release series will receive patches and new versions. The +classification of the security issue is judged by the core team. + +**Currently included series:** 4.0.z, 3.2.z + +Unsupported Release Series +-------------------------- + +When a release series is no longer supported, it's your own responsibility to +deal with bugs and security issues. We may provide backports of the fixes and +publish them to git, however there will be no new versions released. If you are +not comfortable maintaining your own versions, you should upgrade to a +supported version. diff --git a/guides/source/migrations.md b/guides/source/migrations.md index 0f5379059e..b7283d16cc 100644 --- a/guides/source/migrations.md +++ b/guides/source/migrations.md @@ -184,7 +184,7 @@ class RemovePartNumberFromProducts < ActiveRecord::Migration end ``` -You are not limited to one magically generated column. For example +You are not limited to one magically generated column. For example: ```bash $ rails generate migration AddDetailsToProducts part_number:string price:decimal @@ -227,7 +227,7 @@ or remove from it as you see fit by editing the `db/migrate/YYYYMMDDHHMMSS_add_details_to_products.rb` file. Also, the generator accepts column type as `references`(also available as -`belongs_to`). For instance +`belongs_to`). For instance: ```bash $ rails generate migration AddUserRefToProducts user:references @@ -269,7 +269,7 @@ end The model and scaffold generators will create migrations appropriate for adding a new model. This migration will already contain instructions for creating the relevant table. If you tell Rails what columns you want, then statements for -adding these columns will also be created. For example, running +adding these columns will also be created. For example, running: ```bash $ rails generate model Product name:string description:text @@ -303,7 +303,7 @@ braces. You can use the following modifiers: * `polymorphic` Adds a `type` column for `belongs_to` associations * `null` Allows or disallows `NULL` values in the column. -For instance, running +For instance, running: ```bash $ rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplier:references{polymorphic} @@ -345,7 +345,7 @@ By default, `create_table` will create a primary key called `id`. You can change the name of the primary key with the `:primary_key` option (don't forget to update the corresponding model) or, if you don't want a primary key at all, you can pass the option `id: false`. If you need to pass database specific options -you can place an SQL fragment in the `:options` option. For example, +you can place an SQL fragment in the `:options` option. For example: ```ruby create_table :products, options: "ENGINE=BLACKHOLE" do |t| @@ -359,7 +359,7 @@ will append `ENGINE=BLACKHOLE` to the SQL statement used to create the table ### Creating a Join Table Migration method `create_join_table` creates a HABTM join table. A typical use -would be +would be: ```ruby create_join_table :products, :categories @@ -378,7 +378,7 @@ will create the `product_id` and `category_id` with the `:null` option as `true`. You can pass the option `:table_name` when you want to customize the table -name. For example, +name. For example: ```ruby create_join_table :products, :categories, table_name: :categorization @@ -400,7 +400,7 @@ end A close cousin of `create_table` is `change_table`, used for changing existing tables. It is used in a similar fashion to `create_table` but the object -yielded to the block knows more tricks. For example +yielded to the block knows more tricks. For example: ```ruby change_table :products do |t| @@ -464,7 +464,7 @@ or write the `up` and `down` methods instead of using the `change` method. Complex migrations may require processing that Active Record doesn't know how to reverse. You can use `reversible` to specify what to do when running a -migration what else to do when reverting it. For example, +migration what else to do when reverting it. For example: ```ruby class ExampleMigration < ActiveRecord::Migration @@ -648,7 +648,7 @@ will update your `db/schema.rb` file to match the structure of your database. If you specify a target version, Active Record will run the required migrations (change, up, down) until it has reached the specified version. The version is the numerical prefix on the migration's filename. For example, to migrate -to version 20080906120000 run +to version 20080906120000 run: ```bash $ rake db:migrate VERSION=20080906120000 @@ -665,7 +665,7 @@ down to, but not including, 20080906120000. A common task is to rollback the last migration. For example, if you made a mistake in it and wish to correct it. Rather than tracking down the version -number associated with the previous migration you can run +number associated with the previous migration you can run: ```bash $ rake db:rollback @@ -683,7 +683,7 @@ will revert the last 3 migrations. The `db:migrate:redo` task is a shortcut for doing a rollback and then migrating back up again. As with the `db:rollback` task, you can use the `STEP` parameter -if you need to go more than one version back, for example +if you need to go more than one version back, for example: ```bash $ rake db:migrate:redo STEP=3 @@ -713,7 +713,7 @@ contents of the current `schema.rb` file. If a migration can't be rolled back, If you need to run a specific migration up or down, the `db:migrate:up` and `db:migrate:down` tasks will do that. Just specify the appropriate version and the corresponding migration will have its `change`, `up` or `down` method -invoked, for example, +invoked, for example: ```bash $ rake db:migrate:up VERSION=20080906120000 @@ -755,7 +755,7 @@ Several methods are provided in migrations that allow you to control all this: | say | Takes a message argument and outputs it as is. A second boolean argument can be passed to specify whether to indent or not. | say_with_time | Outputs text along with how long it took to run its block. If the block returns an integer it assumes it is the number of rows affected. -For example, this migration +For example, this migration: ```ruby class CreateProducts < ActiveRecord::Migration @@ -1040,8 +1040,8 @@ this, then you should set the schema format to `:sql`. Instead of using Active Record's schema dumper, the database's structure will be dumped using a tool specific to the database (via the `db:structure:dump` Rake task) into `db/structure.sql`. For example, for PostgreSQL, the `pg_dump` -utility is used. For MySQL, this file will contain the output of `SHOW CREATE -TABLE` for the various tables. +utility is used. For MySQL, this file will contain the output of +`SHOW CREATE TABLE` for the various tables. Loading these schemas is simply a question of executing the SQL statements they contain. By definition, this will create a perfect copy of the database's diff --git a/guides/source/plugins.md b/guides/source/plugins.md index ca55ee0df2..d0aa2e55a2 100644 --- a/guides/source/plugins.md +++ b/guides/source/plugins.md @@ -34,9 +34,15 @@ different rails applications using RubyGems and Bundler if desired. Rails ships with a `rails plugin new` command which creates a - skeleton for developing any kind of Rails extension with the ability - to run integration tests using a dummy Rails application. See usage - and options by asking for help: +skeleton for developing any kind of Rails extension with the ability +to run integration tests using a dummy Rails application. Create your +plugin with the command: + +```bash +$ rails plugin new yaffle +``` + +See usage and options by asking for help: ```bash $ rails plugin --help diff --git a/guides/source/rails_on_rack.md b/guides/source/rails_on_rack.md index 642c70fd9d..d53e0cd2bd 100644 --- a/guides/source/rails_on_rack.md +++ b/guides/source/rails_on_rack.md @@ -225,9 +225,13 @@ config.middleware.delete "Rack::MethodOverride" Much of Action Controller's functionality is implemented as Middlewares. The following list explains the purpose of each of them: + **`Rack::Sendfile`** + +* Sets server specific X-Sendfile header. Configure this via `config.action_dispatch.x_sendfile_header` option. + **`ActionDispatch::Static`** -* Used to serve static assets. Disabled if `config.serve_static_assets` is true. +* Used to serve static assets. Disabled if `config.serve_static_assets` is `false`. **`Rack::Lock`** diff --git a/guides/source/routing.md b/guides/source/routing.md index 37525c48a6..19784823f7 100644 --- a/guides/source/routing.md +++ b/guides/source/routing.md @@ -89,15 +89,15 @@ resources :photos creates seven different routes in your application, all mapping to the `Photos` controller: -| HTTP Verb | Path | Action | Used for | -| --------- | ---------------- | ------- | -------------------------------------------- | -| GET | /photos | index | display a list of all photos | -| GET | /photos/new | new | return an HTML form for creating a new photo | -| POST | /photos | create | create a new photo | -| GET | /photos/:id | show | display a specific photo | -| GET | /photos/:id/edit | edit | return an HTML form for editing a photo | -| PATCH/PUT | /photos/:id | update | update a specific photo | -| DELETE | /photos/:id | destroy | delete a specific photo | +| HTTP Verb | Path | Controller#Action | Used for | +| --------- | ---------------- | ----------------- | -------------------------------------------- | +| GET | /photos | photos#index | display a list of all photos | +| GET | /photos/new | photos#new | return an HTML form for creating a new photo | +| POST | /photos | photos#create | create a new photo | +| GET | /photos/:id | photos#show | display a specific photo | +| GET | /photos/:id/edit | photos#edit | return an HTML form for editing a photo | +| PATCH/PUT | /photos/:id | photos#update | update a specific photo | +| DELETE | /photos/:id | photos#destroy | delete a specific photo | NOTE: Because the router uses the HTTP verb and URL to match inbound requests, four URLs map to seven different actions. @@ -152,14 +152,14 @@ resource :geocoder creates six different routes in your application, all mapping to the `Geocoders` controller: -| HTTP Verb | Path | Action | Used for | -| --------- | -------------- | ------- | --------------------------------------------- | -| GET | /geocoder/new | new | return an HTML form for creating the geocoder | -| POST | /geocoder | create | create the new geocoder | -| GET | /geocoder | show | display the one and only geocoder resource | -| GET | /geocoder/edit | edit | return an HTML form for editing the geocoder | -| PATCH/PUT | /geocoder | update | update the one and only geocoder resource | -| DELETE | /geocoder | destroy | delete the geocoder resource | +| HTTP Verb | Path | Controller#Action | Used for | +| --------- | -------------- | ----------------- | --------------------------------------------- | +| GET | /geocoder/new | geocoders#new | return an HTML form for creating the geocoder | +| POST | /geocoder | geocoders#create | create the new geocoder | +| GET | /geocoder | geocoders#show | display the one and only geocoder resource | +| GET | /geocoder/edit | geocoders#edit | return an HTML form for editing the geocoder | +| PATCH/PUT | /geocoder | geocoders#update | update the one and only geocoder resource | +| DELETE | /geocoder | geocoders#destroy | delete the geocoder resource | NOTE: Because you might want to use the same controller for a singular route (`/account`) and a plural route (`/accounts/45`), singular resources map to plural controllers. So that, for example, `resource :photo` and `resources :photos` creates both singular and plural routes that map to the same controller (`PhotosController`). @@ -189,15 +189,15 @@ end This will create a number of routes for each of the `posts` and `comments` controller. For `Admin::PostsController`, Rails will create: -| HTTP Verb | Path | Action | Used for | -| --------- | --------------------- | ------- | ------------------------- | -| GET | /admin/posts | index | admin_posts_path | -| GET | /admin/posts/new | new | new_admin_post_path | -| POST | /admin/posts | create | admin_posts_path | -| GET | /admin/posts/:id | show | admin_post_path(:id) | -| GET | /admin/posts/:id/edit | edit | edit_admin_post_path(:id) | -| PATCH/PUT | /admin/posts/:id | update | admin_post_path(:id) | -| DELETE | /admin/posts/:id | destroy | admin_post_path(:id) | +| HTTP Verb | Path | Controller#Action | Named Helper | +| --------- | --------------------- | ------------------- | ------------------------- | +| GET | /admin/posts | admin/posts#index | admin_posts_path | +| GET | /admin/posts/new | admin/posts#new | new_admin_post_path | +| POST | /admin/posts | admin/posts#create | admin_posts_path | +| GET | /admin/posts/:id | admin/posts#show | admin_post_path(:id) | +| GET | /admin/posts/:id/edit | admin/posts#edit | edit_admin_post_path(:id) | +| PATCH/PUT | /admin/posts/:id | admin/posts#update | admin_post_path(:id) | +| DELETE | /admin/posts/:id | admin/posts#destroy | admin_post_path(:id) | If you want to route `/posts` (without the prefix `/admin`) to `Admin::PostsController`, you could use: @@ -229,15 +229,15 @@ resources :posts, path: '/admin/posts' In each of these cases, the named routes remain the same as if you did not use `scope`. In the last case, the following paths map to `PostsController`: -| HTTP Verb | Path | Action | Named Helper | -| --------- | --------------------- | ------- | ------------------- | -| GET | /admin/posts | index | posts_path | -| GET | /admin/posts/new | new | new_post_path | -| POST | /admin/posts | create | posts_path | -| GET | /admin/posts/:id | show | post_path(:id) | -| GET | /admin/posts/:id/edit | edit | edit_post_path(:id) | -| PATCH/PUT | /admin/posts/:id | update | post_path(:id) | -| DELETE | /admin/posts/:id | destroy | post_path(:id) | +| HTTP Verb | Path | Controller#Action | Named Helper | +| --------- | --------------------- | ----------------- | ------------------- | +| GET | /admin/posts | posts#index | posts_path | +| GET | /admin/posts/new | posts#new | new_post_path | +| POST | /admin/posts | posts#create | posts_path | +| GET | /admin/posts/:id | posts#show | post_path(:id) | +| GET | /admin/posts/:id/edit | posts#edit | edit_post_path(:id) | +| PATCH/PUT | /admin/posts/:id | posts#update | post_path(:id) | +| DELETE | /admin/posts/:id | posts#destroy | post_path(:id) | ### Nested Resources @@ -263,15 +263,15 @@ end In addition to the routes for magazines, this declaration will also route ads to an `AdsController`. The ad URLs require a magazine: -| HTTP Verb | Path | Action | Used for | -| --------- | ------------------------------------ | ------- | -------------------------------------------------------------------------- | -| GET | /magazines/:magazine_id/ads | index | display a list of all ads for a specific magazine | -| GET | /magazines/:magazine_id/ads/new | new | return an HTML form for creating a new ad belonging to a specific magazine | -| POST | /magazines/:magazine_id/ads | create | create a new ad belonging to a specific magazine | -| GET | /magazines/:magazine_id/ads/:id | show | display a specific ad belonging to a specific magazine | -| GET | /magazines/:magazine_id/ads/:id/edit | edit | return an HTML form for editing an ad belonging to a specific magazine | -| PATCH/PUT | /magazines/:magazine_id/ads/:id | update | update a specific ad belonging to a specific magazine | -| DELETE | /magazines/:magazine_id/ads/:id | destroy | delete a specific ad belonging to a specific magazine | +| HTTP Verb | Path | Controller#Action | Used for | +| --------- | ------------------------------------ | ----------------- | -------------------------------------------------------------------------- | +| GET | /magazines/:magazine_id/ads | ads#index | display a list of all ads for a specific magazine | +| GET | /magazines/:magazine_id/ads/new | ads#new | return an HTML form for creating a new ad belonging to a specific magazine | +| POST | /magazines/:magazine_id/ads | ads#create | create a new ad belonging to a specific magazine | +| GET | /magazines/:magazine_id/ads/:id | ads#show | display a specific ad belonging to a specific magazine | +| GET | /magazines/:magazine_id/ads/:id/edit | ads#edit | return an HTML form for editing an ad belonging to a specific magazine | +| PATCH/PUT | /magazines/:magazine_id/ads/:id | ads#update | update a specific ad belonging to a specific magazine | +| DELETE | /magazines/:magazine_id/ads/:id | ads#destroy | delete a specific ad belonging to a specific magazine | This will also create routing helpers such as `magazine_ads_url` and `edit_magazine_ad_path`. These helpers take an instance of Magazine as the first parameter (`magazine_ads_url(@magazine)`). @@ -350,15 +350,15 @@ end The comments resource here will have the following routes generated for it: -| HTTP Verb | Path | Named Helper | -| --------- | -------------------------------------- | ------------------- | -| GET | /posts/:post_id/comments(.:format) | post_comments | -| POST | /posts/:post_id/comments(.:format) | post_comments | -| GET | /posts/:post_id/comments/new(.:format) | new_post_comment | -| GET | /sekret/comments/:id/edit(.:format) | edit_comment | -| GET | /sekret/comments/:id(.:format) | comment | -| PATCH/PUT | /sekret/comments/:id(.:format) | comment | -| DELETE | /sekret/comments/:id(.:format) | comment | +| HTTP Verb | Path | Controller#Action | Named Helper | +| --------- | -------------------------------------- | ----------------- | ------------------- | +| GET | /posts/:post_id/comments(.:format) | comments#index | post_comments | +| POST | /posts/:post_id/comments(.:format) | comments#create | post_comments | +| GET | /posts/:post_id/comments/new(.:format) | comments#new | new_post_comment | +| GET | /sekret/comments/:id/edit(.:format) | comments#edit | edit_comment | +| GET | /sekret/comments/:id(.:format) | comments#show | comment | +| PATCH/PUT | /sekret/comments/:id(.:format) | comments#update | comment | +| DELETE | /sekret/comments/:id(.:format) | comments#destroy | comment | The `:shallow_prefix` option adds the specified parameter to the named helpers: @@ -372,15 +372,15 @@ end The comments resource here will have the following routes generated for it: -| HTTP Verb | Path | Named Helper | -| --------- | -------------------------------------- | ------------------- | -| GET | /posts/:post_id/comments(.:format) | post_comments | -| POST | /posts/:post_id/comments(.:format) | post_comments | -| GET | /posts/:post_id/comments/new(.:format) | new_post_comment | -| GET | /comments/:id/edit(.:format) | edit_sekret_comment | -| GET | /comments/:id(.:format) | sekret_comment | -| PATCH/PUT | /comments/:id(.:format) | sekret_comment | -| DELETE | /comments/:id(.:format) | sekret_comment | +| HTTP Verb | Path | Controller#Action | Named Helper | +| --------- | -------------------------------------- | ----------------- | ------------------- | +| GET | /posts/:post_id/comments(.:format) | comments#index | post_comments | +| POST | /posts/:post_id/comments(.:format) | comments#create | post_comments | +| GET | /posts/:post_id/comments/new(.:format) | comments#new | new_post_comment | +| GET | /comments/:id/edit(.:format) | comments#edit | edit_sekret_comment | +| GET | /comments/:id(.:format) | comments#show | sekret_comment | +| PATCH/PUT | /comments/:id(.:format) | comments#update | sekret_comment | +| DELETE | /comments/:id(.:format) | comments#destroy | sekret_comment | ### Routing concerns @@ -485,7 +485,10 @@ end This will recognize `/photos/1/preview` with GET, and route to the `preview` action of `PhotosController`, with the resource id value passed in `params[:id]`. It will also create the `preview_photo_url` and `preview_photo_path` helpers. -Within the block of member routes, each route name specifies the HTTP verb that it will recognize. You can use `get`, `patch`, `put`, `post`, or `delete` here. If you don't have multiple `member` routes, you can also pass `:on` to a route, eliminating the block: +Within the block of member routes, each route name specifies the HTTP verb +will be recognized. You can use `get`, `patch`, `put`, `post`, or `delete` here +. If you don't have multiple `member` routes, you can also pass `:on` to a +route, eliminating the block: ```ruby resources :photos do @@ -842,15 +845,15 @@ resources :photos, controller: 'images' will recognize incoming paths beginning with `/photos` but route to the `Images` controller: -| HTTP Verb | Path | Action | Named Helper | -| --------- | ---------------- | ------- | -------------------- | -| GET | /photos | index | photos_path | -| GET | /photos/new | new | new_photo_path | -| POST | /photos | create | photos_path | -| GET | /photos/:id | show | photo_path(:id) | -| GET | /photos/:id/edit | edit | edit_photo_path(:id) | -| PATCH/PUT | /photos/:id | update | photo_path(:id) | -| DELETE | /photos/:id | destroy | photo_path(:id) | +| HTTP Verb | Path | Controller#Action | Named Helper | +| --------- | ---------------- | ----------------- | -------------------- | +| GET | /photos | images#index | photos_path | +| GET | /photos/new | images#new | new_photo_path | +| POST | /photos | images#create | photos_path | +| GET | /photos/:id | images#show | photo_path(:id) | +| GET | /photos/:id/edit | images#edit | edit_photo_path(:id) | +| PATCH/PUT | /photos/:id | images#update | photo_path(:id) | +| DELETE | /photos/:id | images#destroy | photo_path(:id) | NOTE: Use `photos_path`, `new_photo_path`, etc. to generate paths for this resource. @@ -900,15 +903,15 @@ resources :photos, as: 'images' will recognize incoming paths beginning with `/photos` and route the requests to `PhotosController`, but use the value of the :as option to name the helpers. -| HTTP Verb | Path | Action | Named Helper | -| --------- | ---------------- | ------- | -------------------- | -| GET | /photos | index | images_path | -| GET | /photos/new | new | new_image_path | -| POST | /photos | create | images_path | -| GET | /photos/:id | show | image_path(:id) | -| GET | /photos/:id/edit | edit | edit_image_path(:id) | -| PATCH/PUT | /photos/:id | update | image_path(:id) | -| DELETE | /photos/:id | destroy | image_path(:id) | +| HTTP Verb | Path | Controller#Action | Named Helper | +| --------- | ---------------- | ----------------- | -------------------- | +| GET | /photos | photos#index | images_path | +| GET | /photos/new | photos#new | new_image_path | +| POST | /photos | photos#create | images_path | +| GET | /photos/:id | photos#show | image_path(:id) | +| GET | /photos/:id/edit | photos#edit | edit_image_path(:id) | +| PATCH/PUT | /photos/:id | photos#update | image_path(:id) | +| DELETE | /photos/:id | photos#destroy | image_path(:id) | ### Overriding the `new` and `edit` Segments @@ -1005,15 +1008,15 @@ end Rails now creates routes to the `CategoriesController`. -| HTTP Verb | Path | Action | Used for | -| --------- | -------------------------- | ------- | ----------------------- | -| GET | /kategorien | index | categories_path | -| GET | /kategorien/neu | new | new_category_path | -| POST | /kategorien | create | categories_path | -| GET | /kategorien/:id | show | category_path(:id) | -| GET | /kategorien/:id/bearbeiten | edit | edit_category_path(:id) | -| PATCH/PUT | /kategorien/:id | update | category_path(:id) | -| DELETE | /kategorien/:id | destroy | category_path(:id) | +| HTTP Verb | Path | Controller#Action | Named Helper | +| --------- | -------------------------- | ------------------ | ----------------------- | +| GET | /kategorien | categories#index | categories_path | +| GET | /kategorien/neu | categories#new | new_category_path | +| POST | /kategorien | categories#create | categories_path | +| GET | /kategorien/:id | categories#show | category_path(:id) | +| GET | /kategorien/:id/bearbeiten | categories#edit | edit_category_path(:id) | +| PATCH/PUT | /kategorien/:id | categories#update | category_path(:id) | +| DELETE | /kategorien/:id | categories#destroy | category_path(:id) | ### Overriding the Singular Form diff --git a/guides/source/security.md b/guides/source/security.md index 4aba39f55a..d7a41497f8 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -290,7 +290,7 @@ NOTE: _Make sure file uploads don't overwrite important files, and process media Many web applications allow users to upload files. _File names, which the user may choose (partly), should always be filtered_ as an attacker could use a malicious file name to overwrite any file on the server. If you store file uploads at /var/www/uploads, and the user enters a file name like "../../../etc/passwd", it may overwrite an important file. Of course, the Ruby interpreter would need the appropriate permissions to do so - one more reason to run web servers, database servers and other programs as a less privileged Unix user. -When filtering user input file names, _don't try to remove malicious parts_. Think of a situation where the web application removes all "../" in a file name and an attacker uses a string such as "....//" - the result will be "../". It is best to use a whitelist approach, which _checks for the validity of a file name with a set of accepted characters_. This is opposed to a blacklist approach which attempts to remove not allowed characters. In case it isn't a valid file name, reject it (or replace not accepted characters), but don't remove them. Here is the file name sanitizer from the [attachment_fu plugin](https://github.com/technoweenie/attachment_fu/tree/master:) +When filtering user input file names, _don't try to remove malicious parts_. Think of a situation where the web application removes all "../" in a file name and an attacker uses a string such as "....//" - the result will be "../". It is best to use a whitelist approach, which _checks for the validity of a file name with a set of accepted characters_. This is opposed to a blacklist approach which attempts to remove not allowed characters. In case it isn't a valid file name, reject it (or replace not accepted characters), but don't remove them. Here is the file name sanitizer from the [attachment_fu plugin](https://github.com/technoweenie/attachment_fu/tree/master): ```ruby def sanitize_filename(filename) @@ -447,7 +447,7 @@ Here are some ideas how to hide honeypot fields by JavaScript and/or CSS: The most simple negative CAPTCHA is one hidden honeypot field. On the server side, you will check the value of the field: If it contains any text, it must be a bot. Then, you can either ignore the post or return a positive result, but not saving the post to the database. This way the bot will be satisfied and moves on. You can do this with annoying users, too. -You can find more sophisticated negative CAPTCHAs in Ned Batchelder's [blog post](http://nedbatchelder.com/text/stopbots.html:) +You can find more sophisticated negative CAPTCHAs in Ned Batchelder's [blog post](http://nedbatchelder.com/text/stopbots.html): * Include a field with the current UTC time-stamp in it and check it on the server. If it is too far in the past, or if it is in the future, the form is invalid. * Randomize the field names @@ -760,7 +760,7 @@ The following is an excerpt from the [Js.Yamanner@m](http://www.symantec.com/sec The worms exploits a hole in Yahoo's HTML/JavaScript filter, which usually filters all target and onload attributes from tags (because there can be JavaScript). The filter is applied only once, however, so the onload attribute with the worm code stays in place. This is a good example why blacklist filters are never complete and why it is hard to allow HTML/JavaScript in a web application. -Another proof-of-concept webmail worm is Nduja, a cross-domain worm for four Italian webmail services. Find more details on [Rosario Valotta's paper](http://www.xssed.com/article/9/Paper_A_PoC_of_a_cross_webmail_worm_XWW_called_Njuda_connection/). Both webmail worms have the goal to harvest email addresses, something a criminal hacker could make money with. +Another proof-of-concept webmail worm is Nduja, a cross-domain worm for four Italian webmail services. Find more details on [Rosario Valotta's paper](http://www.xssed.com/news/37/Nduja_Connection_A_cross_webmail_worm_XWW/). Both webmail worms have the goal to harvest email addresses, something a criminal hacker could make money with. In December 2006, 34,000 actual user names and passwords were stolen in a [MySpace phishing attack](http://news.netcraft.com/archives/2006/10/27/myspace_accounts_compromised_by_phishers.html). The idea of the attack was to create a profile page named "login_home_index_html", so the URL looked very convincing. Specially-crafted HTML and CSS was used to hide the genuine MySpace content from the page and instead display its own login form. diff --git a/guides/source/testing.md b/guides/source/testing.md index 50115607c9..cf01650b2a 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -359,6 +359,17 @@ Notice the 'E' in the output. It denotes a test with error. NOTE: The execution of each test method stops as soon as any error or an assertion failure is encountered, and the test suite continues with the next method. All test methods are executed in alphabetical order. +When a test fails you are presented with the corresponding backtrace. By default +Rails filters that backtrace and will only print lines relevant to your +application. This eliminates the framwork noise and helps to focus on your +code. However there are situations when you want to see the full +backtrace. simply set the `BACKTRACE` environment variable to enable this +behavior: + +```bash +$ BACKTRACE=1 rake test test/models/post_test.rb +``` + ### What to Include in Your Unit Tests Ideally, you would like to include a test for everything which could possibly break. It's a good practice to have at least one test for each of your validations and at least one test for every method in your model. @@ -534,7 +545,7 @@ instance variable: ```ruby # setting a HTTP Header -@request.headers["Accepts"] = "text/plain, text/html" +@request.headers["Accept"] = "text/plain, text/html" get :index # simulate the request with custom header # setting a CGI variable diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 3daa10ea07..224213268e 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -339,7 +339,7 @@ config.assets.js_compressor = :uglifier ### sass-rails -* `asset_url` with two arguments is deprecated. For example: `asset-url("rails.png", image)` becomes `asset-url("rails.png")` +* `asset-url` with two arguments is deprecated. For example: `asset-url("rails.png", image)` becomes `asset-url("rails.png")` Upgrading from Rails 3.1 to Rails 3.2 ------------------------------------- diff --git a/rails.gemspec b/rails.gemspec index b426faf0e8..d1c199a97a 100644 --- a/rails.gemspec +++ b/rails.gemspec @@ -21,6 +21,7 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', version s.add_dependency 'actionpack', version s.add_dependency 'actionview', version + s.add_dependency 'activemodel', version s.add_dependency 'activerecord', version s.add_dependency 'actionmailer', version s.add_dependency 'railties', version diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index a4babbe8c3..0dddee0555 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,6 +1,34 @@ -* Include `web-console` into newly generated applications' Gemfile. +* `BACKTRACE` environment variable to show unfiltered backtraces for + test failures. - *Genadi Samokovarov* + Example: + + $ BACKTRACE=1 ruby -Itest ... + # or with rake + $ BACKTRACE=1 bin/rake + + *Yves Senn* + +* Removal of all javascript stuff (gems and files) when generating a new + application using the `--skip-javascript` option. + + *Robin Dupret* + +* Make the application name snake cased when it contains spaces + + The application name is used to fill the `database.yml` and + `session_store.rb` files ; previously, if the provided name + contained whitespaces, it led to unexpected names in these files. + + *Robin Dupret* + +* Added `--model-name` option to `ScaffoldControllerGenerator`. + + *yalab* + +* Expose MiddlewareStack#unshift to environment configuration. + + *Ben Pickles* * `rails server` will only extend the logger to output to STDOUT in development environment. diff --git a/railties/bin/rails b/railties/bin/rails index b3026e8a93..82c17cabce 100755 --- a/railties/bin/rails +++ b/railties/bin/rails @@ -1,8 +1,8 @@ #!/usr/bin/env ruby -git_path = File.join(File.expand_path('../../..', __FILE__), '.git') +git_path = File.expand_path('../../../.git', __FILE__) -if File.exists?(git_path) +if File.exist?(git_path) railties_path = File.expand_path('../../lib', __FILE__) $:.unshift(railties_path) end diff --git a/railties/lib/rails/app_rails_loader.rb b/railties/lib/rails/app_rails_loader.rb index 71fcf83dae..1610751844 100644 --- a/railties/lib/rails/app_rails_loader.rb +++ b/railties/lib/rails/app_rails_loader.rb @@ -55,7 +55,7 @@ EOS end def self.find_executable - EXECUTABLES.find { |exe| File.exists?(exe) } + EXECUTABLES.find { |exe| File.exist?(exe) } end end end diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 4faaf5ef1e..d1e88cfafd 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -223,7 +223,7 @@ module Rails # you need to load files in lib/ during the application configuration as well. def add_lib_to_load_path! #:nodoc: path = File.join config.root, 'lib' - if File.exists?(path) && !$LOAD_PATH.include?(path) + if File.exist?(path) && !$LOAD_PATH.include?(path) $LOAD_PATH.unshift(path) end end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 7332444ab9..dd0b9c6d70 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -90,7 +90,7 @@ module Rails # Loads and returns the configuration of the database. def database_configuration yaml = paths["config/database"].first - if File.exists?(yaml) + if File.exist?(yaml) require "erb" YAML.load ERB.new(IO.read(yaml)).result elsif ENV['DATABASE_URL'] diff --git a/railties/lib/rails/cli.rb b/railties/lib/rails/cli.rb index 20313a2608..dd70c272c6 100644 --- a/railties/lib/rails/cli.rb +++ b/railties/lib/rails/cli.rb @@ -1,4 +1,3 @@ -require 'rbconfig' require 'rails/app_rails_loader' # If we are inside a Rails application this method performs an exec and thus diff --git a/railties/lib/rails/commands/application.rb b/railties/lib/rails/commands/application.rb index 678697f09b..c998e6b6a8 100644 --- a/railties/lib/rails/commands/application.rb +++ b/railties/lib/rails/commands/application.rb @@ -13,5 +13,5 @@ module Rails end end -Rails::Generators::AppPreparer.new(ARGV).prepare! -Rails::Generators::AppGenerator.start +args = Rails::Generators::ARGVScrubber.new(ARGV).prepare! +Rails::Generators::AppGenerator.start args diff --git a/railties/lib/rails/commands/commands_tasks.rb b/railties/lib/rails/commands/commands_tasks.rb index 59d2a0793e..de60423784 100644 --- a/railties/lib/rails/commands/commands_tasks.rb +++ b/railties/lib/rails/commands/commands_tasks.rb @@ -139,7 +139,7 @@ EOT # This allows us to run `rails server` from other directories, but still get # the main config.ru and properly set the tmp directory. def set_application_directory! - Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exists?(File.expand_path("config.ru")) + Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exist?(File.expand_path("config.ru")) end def require_application_and_environment! diff --git a/railties/lib/rails/commands/server.rb b/railties/lib/rails/commands/server.rb index 485bd1eb09..4201dac42f 100644 --- a/railties/lib/rails/commands/server.rb +++ b/railties/lib/rails/commands/server.rb @@ -1,6 +1,7 @@ require 'fileutils' require 'optparse' require 'action_dispatch' +require 'rails' module Rails class Server < ::Rack::Server @@ -32,7 +33,7 @@ module Rails opt_parser.parse! args - options[:log_stdout] = options[:daemonize].blank? && options[:environment] == "development" + options[:log_stdout] = options[:daemonize].blank? && (options[:environment] || Rails.env) == "development" options[:server] = args.shift options end diff --git a/railties/lib/rails/configuration.rb b/railties/lib/rails/configuration.rb index c694513960..f5d7dede66 100644 --- a/railties/lib/rails/configuration.rb +++ b/railties/lib/rails/configuration.rb @@ -59,6 +59,10 @@ module Rails @operations << [__method__, args, block] end + def unshift(*args, &block) + @operations << [__method__, args, block] + end + def merge_into(other) #:nodoc: @operations.each do |operation, args, block| other.send(operation, *args, &block) diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index 6b34db3e3f..1c0952cc55 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -1,6 +1,8 @@ activesupport_path = File.expand_path('../../../../activesupport/lib', __FILE__) $:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path) +require 'thor/group' + require 'active_support' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/kernel/singleton_class' @@ -9,8 +11,6 @@ require 'active_support/core_ext/hash/deep_merge' require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/string/inflections' -require 'rails/generators/base' - module Rails module Generators autoload :Actions, 'rails/generators/actions' diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 6f1b7e2218..ecf3551f50 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -5,6 +5,8 @@ require 'rails/version' unless defined?(Rails::VERSION) require 'rbconfig' require 'open-uri' require 'uri' +require 'rails/generators/base' +require 'active_support/core_ext/array/extract_options' module Rails module Generators @@ -76,13 +78,47 @@ module Rails end def initialize(*args) - @original_wd = Dir.pwd + @original_wd = Dir.pwd + @gem_filter = lambda { |gem| true } + @extra_entries = [] super convert_database_option_for_jruby end protected + def gemfile_entry(name, *args) + options = args.extract_options! + version = args.first + github = options[:github] + path = options[:path] + + if github + @extra_entries << GemfileEntry.github(name, github) + elsif path + @extra_entries << GemfileEntry.path(name, path) + else + @extra_entries << GemfileEntry.version(name, version) + end + self + end + + def gemfile_entries + [ rails_gemfile_entry, + database_gemfile_entry, + assets_gemfile_entry, + javascript_gemfile_entry, + jbuilder_gemfile_entry, + sdoc_gemfile_entry, + @extra_entries].flatten.find_all(&@gem_filter) + end + + def add_gem_entry_filter + @gem_filter = lambda { |next_filter,entry| + yield(entry) && next_filter.call(entry) + }.curry[@gem_filter] + end + def builder @builder ||= begin builder_class = get_builder_class @@ -96,21 +132,81 @@ module Rails end def create_root - self.destination_root = File.expand_path(app_path, destination_root) valid_const? empty_directory '.' - set_default_accessors! FileUtils.cd(destination_root) unless options[:pretend] end + class TemplateRecorder < ::BasicObject # :nodoc: + attr_reader :gems + + def initialize(target) + @target = target + # unfortunately, instance eval has access to these ivars + @app_const = target.send :app_const + @app_const_base = target.send :app_const_base + @app_name = target.send :app_name + @commands = [] + @gems = [] + end + + def gemfile_entry(*args) + @target.send :gemfile_entry, *args + end + + def add_gem_entry_filter(*args, &block) + @target.send :add_gem_entry_filter, *args, &block + end + + def method_missing(name, *args, &block) + @commands << [name, args, block] + end + + def respond_to_missing?(method, priv = false) + super || @target.respond_to?(method, priv) + end + + def replay! + @commands.each do |name, args, block| + @target.send name, *args, &block + end + end + end + def apply_rails_template - apply rails_template if rails_template + @recorder = TemplateRecorder.new self + + apply(rails_template, target: @recorder) if rails_template rescue Thor::Error, LoadError, Errno::ENOENT => e raise Error, "The template [#{rails_template}] could not be loaded. Error: #{e}" end + def replay_template + @recorder.replay! if @recorder + end + + def apply(path, config={}) + verbose = config.fetch(:verbose, true) + target = config.fetch(:target, self) + is_uri = path =~ /^https?\:\/\// + path = find_in_source_paths(path) unless is_uri + + say_status :apply, path, verbose + shell.padding += 1 if verbose + + if is_uri + contents = open(path, "Accept" => "application/x-thor-template") {|io| io.read } + else + contents = open(path) {|io| io.read } + end + + target.instance_eval(contents, path) + shell.padding -= 1 if verbose + end + def set_default_accessors! + self.destination_root = File.expand_path(app_path, destination_root) self.rails_template = case options[:template] when /^https?:\/\// options[:template] @@ -122,11 +218,9 @@ module Rails end def database_gemfile_entry - options[:skip_active_record] ? "" : - <<-GEMFILE.strip_heredoc - # Use #{options[:database]} as the database for Active Record - gem '#{gem_for_database}' - GEMFILE + return [] if options[:skip_active_record] + GemfileEntry.version gem_for_database, nil, + "Use #{options[:database]} as the database for Active Record" end def include_all_railties? @@ -137,22 +231,39 @@ module Rails options[value] ? '# ' : '' end + class GemfileEntry < Struct.new(:name, :version, :comment, :options, :commented_out) + def initialize(name, version, comment, options = {}, commented_out = false) + super + end + + def self.github(name, github, comment = nil) + new(name, nil, comment, github: github) + end + + def self.version(name, version, comment = nil) + new(name, version, comment) + end + + def self.path(name, path, comment = nil) + new(name, nil, comment, path: path) + end + + def padding(max_width) + ' ' * (max_width - name.length + 2) + end + end + def rails_gemfile_entry if options.dev? - <<-GEMFILE.strip_heredoc - gem 'rails', path: '#{Rails::Generators::RAILS_DEV_PATH}' - gem 'arel', github: 'rails/arel' - GEMFILE + [GemfileEntry.path('rails', Rails::Generators::RAILS_DEV_PATH), + GemfileEntry.github('arel', 'rails/arel')] elsif options.edge? - <<-GEMFILE.strip_heredoc - gem 'rails', github: 'rails/rails' - gem 'arel', github: 'rails/arel' - GEMFILE + [GemfileEntry.github('rails', 'rails/rails'), + GemfileEntry.github('arel', 'rails/arel')] else - <<-GEMFILE.strip_heredoc - # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' - gem 'rails', '#{Rails::VERSION::STRING}' - GEMFILE + [GemfileEntry.version('rails', + Rails::VERSION::STRING, + "Bundle edge Rails instead: gem 'rails', github: 'rails/rails'")] end end @@ -184,77 +295,67 @@ module Rails end def assets_gemfile_entry - return if options[:skip_sprockets] + return [] if options[:skip_sprockets] - gemfile = if options.dev? || options.edge? - <<-GEMFILE.strip_heredoc - # Use edge version of sprockets-rails - gem 'sprockets-rails', github: 'rails/sprockets-rails' - - # Use SCSS for stylesheets - gem 'sass-rails', github: 'rails/sass-rails' - GEMFILE + gems = [] + if options.dev? || options.edge? + gems << GemfileEntry.github('sprockets-rails', 'rails/sprockets-rails', + 'Use edge version of sprockets-rails') + gems << GemfileEntry.github('sass-rails', 'rails/sass-rails', + 'Use SCSS for stylesheets') else - <<-GEMFILE.strip_heredoc - # Use SCSS for stylesheets - gem 'sass-rails', '~> 4.0.0.rc1' - GEMFILE + gems << GemfileEntry.version('sass-rails', + '~> 4.0.0.rc1', + 'Use SCSS for stylesheets') end - gemfile += <<-GEMFILE.strip_heredoc + gems << GemfileEntry.version('uglifier', + '>= 1.3.0', + 'Use Uglifier as compressor for JavaScript assets') - # Use Uglifier as compressor for JavaScript assets - gem 'uglifier', '>= 1.3.0' - GEMFILE + gems + end - if options[:skip_javascript] - gemfile += <<-GEMFILE - #{coffee_gemfile_entry} - #{javascript_runtime_gemfile_entry} - GEMFILE - end + def jbuilder_gemfile_entry + comment = 'Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder' + GemfileEntry.version('jbuilder', '~> 1.2', comment) + end - gemfile.gsub(/^[ \t]+/, '') + def sdoc_gemfile_entry + comment = 'bundle exec rake doc:rails generates the API under doc/api.' + GemfileEntry.new('sdoc', nil, comment, { group: :doc, require: false }) end def coffee_gemfile_entry + comment = 'Use CoffeeScript for .js.coffee assets and views' if options.dev? || options.edge? - <<-GEMFILE - # Use CoffeeScript for .js.coffee assets and views - gem 'coffee-rails', github: 'rails/coffee-rails' - GEMFILE + GemfileEntry.github 'coffee-rails', 'rails/coffee-rails', comment else - <<-GEMFILE - # Use CoffeeScript for .js.coffee assets and views - gem 'coffee-rails', '~> 4.0.0' - GEMFILE + GemfileEntry.version 'coffee-rails', '~> 4.0.0', comment end end def javascript_gemfile_entry - unless options[:skip_javascript] - <<-GEMFILE.gsub(/^[ \t]+/, '') - #{coffee_gemfile_entry} - #{javascript_runtime_gemfile_entry} - # Use #{options[:javascript]} as the JavaScript library - gem '#{options[:javascript]}-rails' - - # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks - gem 'turbolinks' - GEMFILE + if options[:skip_javascript] + [] + else + gems = [coffee_gemfile_entry, javascript_runtime_gemfile_entry] + gems << GemfileEntry.version("#{options[:javascript]}-rails", nil, + "Use #{options[:javascript]} as the JavaScript library") + + gems << GemfileEntry.version("turbolinks", nil, + "Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks") + gems end end def javascript_runtime_gemfile_entry - runtime = if defined?(JRUBY_VERSION) - "gem 'therubyrhino'" + comment = 'See https://github.com/sstephenson/execjs#readme for more supported runtimes' + if defined?(JRUBY_VERSION) + GemfileEntry.version 'therubyrhino', nil, comment else - "# gem 'therubyracer', platforms: :ruby" + GemfileEntry.new 'therubyracer', nil, comment, { platforms: :ruby }, true end - <<-GEMFILE - # See https://github.com/sstephenson/execjs#readme for more supported runtimes - #{runtime} - GEMFILE end def bundle_command(command) diff --git a/railties/lib/rails/generators/base.rb b/railties/lib/rails/generators/base.rb index 8aec8bc8f9..dc1d4fa181 100644 --- a/railties/lib/rails/generators/base.rb +++ b/railties/lib/rails/generators/base.rb @@ -7,7 +7,7 @@ rescue LoadError exit end -require 'rails/generators/actions' +require 'rails/generators' module Rails module Generators @@ -211,7 +211,7 @@ module Rails return unless base_name && generator_name return unless default_generator_root path = File.join(default_generator_root, 'templates') - path if File.exists?(path) + path if File.exist?(path) end # Returns the base root for a common set of generators. This is used to dynamically @@ -368,12 +368,12 @@ module Rails source_root && File.expand_path("../USAGE", source_root), default_generator_root && File.join(default_generator_root, "USAGE") ] - paths.compact.detect { |path| File.exists? path } + paths.compact.detect { |path| File.exist? path } end def self.default_generator_root path = File.expand_path(File.join(base_name, generator_name), base_root) - path if File.exists?(path) + path if File.exist?(path) end end diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 92c876c835..a2023886cd 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -129,7 +129,9 @@ module Rails end def vendor_javascripts - empty_directory_with_keep_file 'vendor/assets/javascripts' + unless options[:skip_javascript] + empty_directory_with_keep_file 'vendor/assets/javascripts' + end end def vendor_stylesheets @@ -162,6 +164,8 @@ module Rails end end + public_task :set_default_accessors! + public_task :apply_rails_template public_task :create_root def create_root_files @@ -225,7 +229,14 @@ module Rails build(:leftovers) end - public_task :apply_rails_template, :run_bundle + def delete_js_folder_skipping_javascript + if options[:skip_javascript] + remove_dir 'app/assets/javascripts' + end + end + + public_task :run_bundle + public_task :replay_template protected @@ -239,7 +250,7 @@ module Rails end def app_name - @app_name ||= (defined_app_const_base? ? defined_app_name : File.basename(destination_root)).tr(".", "_") + @app_name ||= (defined_app_const_base? ? defined_app_name : File.basename(destination_root)).tr('\\', '').tr(". ", "_") end def defined_app_name @@ -302,58 +313,67 @@ module Rails # # This class should be called before the AppGenerator is required and started # since it configures and mutates ARGV correctly. - class AppPreparer # :nodoc - attr_reader :argv - + class ARGVScrubber # :nodoc def initialize(argv = ARGV) @argv = argv end def prepare! - handle_version_request!(argv.first) - unless handle_invalid_command!(argv.first) - argv.shift - handle_rails_rc! + handle_version_request!(@argv.first) + handle_invalid_command!(@argv.first, @argv) do + handle_rails_rc!(@argv.drop(1)) end end + def self.default_rc_file + File.expand_path('~/.railsrc') + end + private def handle_version_request!(argument) - if ['--version', '-v'].include?(argv.first) + if ['--version', '-v'].include?(argument) require 'rails/version' puts "Rails #{Rails::VERSION::STRING}" exit(0) end end - def handle_invalid_command!(argument) - if argument != "new" - argv[0] = "--help" + def handle_invalid_command!(argument, argv) + if argument == "new" + yield + else + ['--help'] + argv.drop(1) end end - def handle_rails_rc! - unless argv.delete("--no-rc") - insert_railsrc_into_argv!(railsrc) + def handle_rails_rc!(argv) + if argv.find { |arg| arg == '--no-rc' } + argv.reject { |arg| arg == '--no-rc' } + else + railsrc(argv) { |rc_argv, rc| insert_railsrc_into_argv!(rc_argv, rc) } end end - def railsrc + def railsrc(argv) if (customrc = argv.index{ |x| x.include?("--rc=") }) - File.expand_path(argv.delete_at(customrc).gsub(/--rc=/, "")) + fname = File.expand_path(argv[customrc].gsub(/--rc=/, "")) + yield(argv.take(customrc) + argv.drop(customrc + 1), fname) else - File.join(File.expand_path("~"), '.railsrc') + yield argv, self.class.default_rc_file end end - def insert_railsrc_into_argv!(railsrc) - if File.exist?(railsrc) - extra_args_string = File.read(railsrc) - extra_args = extra_args_string.split(/\n+/).map {|l| l.split}.flatten - puts "Using #{extra_args.join(" ")} from #{railsrc}" - argv.insert(1, *extra_args) - end + def read_rc_file(railsrc) + extra_args = File.readlines(railsrc).flat_map(&:split) + puts "Using #{extra_args.join(" ")} from #{railsrc}" + extra_args + end + + def insert_railsrc_into_argv!(argv, railsrc) + return argv unless File.exist?(railsrc) + extra_args = read_rc_file railsrc + argv.take(1) + extra_args + argv.drop(1) end end end diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index edc76e6c34..21dcba7947 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -1,22 +1,19 @@ source 'https://rubygems.org' -<%= rails_gemfile_entry -%> +<% max_width = gemfile_entries.map { |g| g.name.length }.max -%> +<% gemfile_entries.each do |gem| -%> +<% if gem.comment -%> -<%= database_gemfile_entry -%> - -<%= assets_gemfile_entry %> -<%= javascript_gemfile_entry -%> - -# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder -gem 'jbuilder', '~> 1.2' - -# Run `rails console` in the browser. Read more: https://github.com/rails/web-console -gem 'web-console', group: :development - -group :doc do - # bundle exec rake doc:rails generates the API under doc/api. - gem 'sdoc', require: false -end +# <%= gem.comment %> +<% end -%> +<%= gem.commented_out ? '# ' : '' %>gem '<%= gem.name %>'<% if gem.version -%> +, '<%= gem.version %>' +<% elsif gem.options.any? -%> +,<%= gem.padding(max_width) %><%= gem.options.map { |k,v| + "#{k}: #{v.inspect}" }.join(', ') %> +<% else %> +<% end -%> +<% end -%> # Use ActiveModel has_secure_password # gem 'bcrypt-ruby', '~> 3.1.2' @@ -25,7 +22,7 @@ end # gem 'unicorn' # Use Capistrano for deployment -# gem 'capistrano', group: :development +# gem 'capistrano-rails', group: :development <% unless defined?(JRUBY_VERSION) -%> # Use debugger diff --git a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt index c3d1578818..fe71f7122c 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt +++ b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt @@ -4,10 +4,14 @@ <title><%= camelized %></title> <%- if options[:skip_javascript] -%> <%%= stylesheet_link_tag "application", media: "all" %> - <%%= javascript_include_tag "application" %> <%- else -%> + <%- if gemfile_entries.any? { |m| m.name == "turbolinks" } -%> <%%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %> <%%= javascript_include_tag "application", "data-turbolinks-track" => true %> + <%- else -%> + <%%= stylesheet_link_tag "application", media: "all" %> + <%%= javascript_include_tag "application" %> + <%- end -%> <%- end -%> <%%= csrf_meta_tags %> </head> diff --git a/railties/lib/rails/generators/rails/app/templates/config/boot.rb b/railties/lib/rails/generators/rails/app/templates/config/boot.rb index 3596736667..5e5f0c1fac 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/boot.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/boot.rb @@ -1,4 +1,4 @@ # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) -require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) +require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt index cff570a631..b724d468a2 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt @@ -22,8 +22,8 @@ Rails.application.configure do <%- unless options.skip_active_record? -%> # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load - <%- end -%> + <%- end -%> <%- unless options.skip_sprockets? -%> # Debug mode disables concatenation and preprocessing of assets. # This option may cause significant delays in view rendering with a large diff --git a/railties/lib/rails/generators/rails/model/USAGE b/railties/lib/rails/generators/rails/model/USAGE index 145d9ee6e0..833b7beb7f 100644 --- a/railties/lib/rails/generators/rails/model/USAGE +++ b/railties/lib/rails/generators/rails/model/USAGE @@ -60,7 +60,7 @@ Available field types: For decimal, two integers separated by a comma in curly braces will be used for precision and scale: - `rails generate model product price:decimal{10,2}` + `rails generate model product 'price:decimal{10,2}'` You can add a `:uniq` or `:index` suffix for unique or standard indexes respectively: diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb index 97ff6d1b8b..f6f529b80a 100644 --- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb +++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb @@ -184,6 +184,7 @@ task default: :test end end + public_task :set_default_accessors! public_task :create_root def create_root_files @@ -319,7 +320,7 @@ task default: :test @application_definition ||= begin dummy_application_path = File.expand_path("#{dummy_path}/config/application.rb", destination_root) - unless options[:pretend] || !File.exists?(dummy_application_path) + unless options[:pretend] || !File.exist?(dummy_application_path) contents = File.read(dummy_application_path) contents[(contents.index(/module ([\w]+)\n(.*)class Application/m))..-1] end diff --git a/railties/lib/rails/generators/rails/plugin/templates/Gemfile b/railties/lib/rails/generators/rails/plugin/templates/Gemfile index 3f2b78f2fd..d576784415 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/Gemfile +++ b/railties/lib/rails/generators/rails/plugin/templates/Gemfile @@ -23,7 +23,20 @@ end <% if options.dev? || options.edge? -%> # Your gem is dependent on dev or edge Rails. Once you can lock this # dependency down to a specific version, move it to your gemspec. -<%= rails_gemfile_entry -%> +<% max_width = gemfile_entries.map { |g| g.name.length }.max -%> +<% gemfile_entries.each do |gem| -%> +<% if gem.comment -%> + +# <%= gem.comment %> +<% end -%> +<%= gem.commented_out ? '# ' : '' %>gem '<%= gem.name %>'<% if gem.version -%> +, '<%= gem.version %>' +<% elsif gem.options.any? -%> +,<%= gem.padding(max_width) %><%= gem.options.map { |k,v| + "#{k}: #{v.inspect}" }.join(', ') %> +<% else %> +<% end -%> +<% end -%> <% end -%> # To use debugger diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb b/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb index ef360470a3..6266cfc509 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb @@ -1,5 +1,5 @@ # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) -require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) +require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) $LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__) diff --git a/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb b/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb index 4f36b612ae..6bf0a33a5f 100644 --- a/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb +++ b/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb @@ -13,7 +13,7 @@ module Rails argument :attributes, type: :array, default: [], banner: "field:type field:type" def create_controller_files - template "controller.rb", File.join('app/controllers', class_path, "#{controller_file_name}_controller.rb") + template "controller.rb", File.join('app/controllers', controller_class_path, "#{controller_file_name}_controller.rb") end hook_for :template_engine, :test_framework, as: :scaffold diff --git a/railties/lib/rails/generators/resource_helpers.rb b/railties/lib/rails/generators/resource_helpers.rb index 7fd5c00768..a01eb57651 100644 --- a/railties/lib/rails/generators/resource_helpers.rb +++ b/railties/lib/rails/generators/resource_helpers.rb @@ -9,11 +9,19 @@ module Rails def self.included(base) #:nodoc: base.class_option :force_plural, type: :boolean, desc: "Forces the use of a plural ModelName" + base.class_option :model_name, type: :string, desc: "ModelName to be used" end # Set controller variables on initialization. def initialize(*args) #:nodoc: super + if options[:model_name] + controller_name = name + self.name = options[:model_name] + assign_names!(self.name) + else + controller_name = name + end if name == name.pluralize && name.singularize != name.pluralize && !options[:force_plural] unless ResourceHelpers.skip_warn @@ -24,19 +32,26 @@ module Rails assign_names!(name) end - @controller_name = name.pluralize + assign_controller_names!(controller_name.pluralize) end protected - attr_reader :controller_name + attr_reader :controller_name, :controller_file_name def controller_class_path - class_path + if options[:model_name] + @controller_class_path + else + class_path + end end - def controller_file_name - @controller_file_name ||= file_name.pluralize + def assign_controller_names!(name) + @controller_name = name + @controller_class_path = name.include?('/') ? name.split('/') : name.split('::') + @controller_class_path.map! { |m| m.underscore } + @controller_file_name = @controller_class_path.pop end def controller_file_path diff --git a/railties/lib/rails/generators/testing/assertions.rb b/railties/lib/rails/generators/testing/assertions.rb index 6267b2f2ee..2e877f8762 100644 --- a/railties/lib/rails/generators/testing/assertions.rb +++ b/railties/lib/rails/generators/testing/assertions.rb @@ -21,8 +21,8 @@ module Rails # end # end def assert_file(relative, *contents) - absolute = File.expand_path(relative, destination_root) - assert File.exists?(absolute), "Expected file #{relative.inspect} to exist, but does not" + absolute = File.expand_path(relative, destination_root).shellescape + assert File.exist?(absolute), "Expected file #{relative.inspect} to exist, but does not" read = File.read(absolute) if block_given? || !contents.empty? yield read if block_given? @@ -44,7 +44,7 @@ module Rails # assert_no_file "config/random.rb" def assert_no_file(relative) absolute = File.expand_path(relative, destination_root) - assert !File.exists?(absolute), "Expected file #{relative.inspect} to not exist, but does" + assert !File.exist?(absolute), "Expected file #{relative.inspect} to not exist, but does" end alias :assert_no_directory :assert_no_file diff --git a/railties/lib/rails/paths.rb b/railties/lib/rails/paths.rb index de6795eda2..f512aefb23 100644 --- a/railties/lib/rails/paths.rb +++ b/railties/lib/rails/paths.rb @@ -81,34 +81,28 @@ module Rails end def autoload_once - filter_by(:autoload_once?) + filter_by { |p| p.autoload_once? } end def eager_load - filter_by(:eager_load?) + filter_by { |p| p.eager_load? } end def autoload_paths - filter_by(:autoload?) + filter_by { |p| p.autoload? } end def load_paths - filter_by(:load_path?) + filter_by { |p| p.load_path? } end - protected + private - def filter_by(constraint) - all = [] - all_paths.each do |path| - if path.send(constraint) - paths = path.existent - paths -= path.children.map { |p| p.send(constraint) ? [] : p.existent }.flatten - all.concat(paths) - end - end - all.uniq! - all + def filter_by(&block) + all_paths.find_all(&block).flat_map { |path| + paths = path.existent + paths - path.children.map { |p| yield(p) ? [] : p.existent }.flatten + }.uniq end end @@ -130,8 +124,9 @@ module Rails end def children - keys = @root.keys.select { |k| k.include?(@current) } - keys.delete(@current) + keys = @root.keys.find_all { |k| + k.start_with?(@current) && k != @current + } @root.values_at(*keys.sort) end @@ -203,7 +198,7 @@ module Rails # Returns all expanded paths but only if they exist in the filesystem. def existent - expanded.select { |f| File.exists?(f) } + expanded.select { |f| File.exist?(f) } end def existent_directories diff --git a/railties/lib/rails/rack/log_tailer.rb b/railties/lib/rails/rack/log_tailer.rb index 18f22e8089..50d0eb96fc 100644 --- a/railties/lib/rails/rack/log_tailer.rb +++ b/railties/lib/rails/rack/log_tailer.rb @@ -7,7 +7,7 @@ module Rails path = Pathname.new(log || "#{::File.expand_path(Rails.root)}/log/#{Rails.env}.log").cleanpath @cursor = @file = nil - if ::File.exists?(path) + if ::File.exist?(path) @cursor = ::File.size(path) @file = ::File.open(path, 'r') end diff --git a/railties/lib/rails/rack/logger.rb b/railties/lib/rails/rack/logger.rb index ef4cdcb080..3b35798679 100644 --- a/railties/lib/rails/rack/logger.rb +++ b/railties/lib/rails/rack/logger.rb @@ -11,7 +11,6 @@ module Rails def initialize(app, taggers = nil) @app = app @taggers = taggers || [] - @instrumenter = ActiveSupport::Notifications.instrumenter end def call(env) @@ -33,7 +32,8 @@ module Rails logger.debug '' end - @instrumenter.start 'request.action_dispatch', request: request + instrumenter = ActiveSupport::Notifications.instrumenter + instrumenter.start 'request.action_dispatch', request: request logger.info started_request_message(request) resp = @app.call(env) resp[2] = ::Rack::BodyProxy.new(resp[2]) { finish(request) } @@ -70,7 +70,8 @@ module Rails private def finish(request) - @instrumenter.finish 'request.action_dispatch', request: request + instrumenter = ActiveSupport::Notifications.instrumenter + instrumenter.finish 'request.action_dispatch', request: request end def development? diff --git a/railties/lib/rails/tasks/framework.rake b/railties/lib/rails/tasks/framework.rake index bd4e7c33e0..e669315934 100644 --- a/railties/lib/rails/tasks/framework.rake +++ b/railties/lib/rails/tasks/framework.rake @@ -46,7 +46,7 @@ namespace :rails do require 'rails/generators/rails/app/app_generator' gen = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: Rails.root - File.exists?(Rails.root.join("config", "application.rb")) ? + File.exist?(Rails.root.join("config", "application.rb")) ? gen.send(:app_const) : gen.send(:valid_const?) gen end diff --git a/railties/lib/rails/tasks/log.rake b/railties/lib/rails/tasks/log.rake index 6c3f02eb0c..877f175ef3 100644 --- a/railties/lib/rails/tasks/log.rake +++ b/railties/lib/rails/tasks/log.rake @@ -10,7 +10,7 @@ namespace :log do if ENV['LOGS'] ENV['LOGS'].split(',') .map { |file| "log/#{file.strip}.log" } - .select { |file| File.exists?(file) } + .select { |file| File.exist?(file) } else FileList["log/*.log"] end diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb index 46f7466551..be801befc2 100644 --- a/railties/lib/rails/test_help.rb +++ b/railties/lib/rails/test_help.rb @@ -10,7 +10,9 @@ require 'rails/generators/test_case' # Config Rails backtrace in tests. require 'rails/backtrace_cleaner' -MiniTest.backtrace_filter = Rails.backtrace_cleaner +if ENV["BACKTRACE"].nil? + MiniTest.backtrace_filter = Rails.backtrace_cleaner +end if defined?(ActiveRecord::Base) class ActiveSupport::TestCase diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake index 547846c833..38107e77b2 100644 --- a/railties/lib/rails/test_unit/testing.rake +++ b/railties/lib/rails/test_unit/testing.rake @@ -4,7 +4,7 @@ require 'rails/test_unit/sub_test_task' task default: :test -desc 'Runs test:units, test:functionals, test:integration together' +desc 'Runs test:units, test:functionals, test:generators, test:integration together' task :test do Rails::TestTask.test_creator(Rake.application.top_level_tasks).invoke_rake_task end diff --git a/railties/test/app_rails_loader_test.rb b/railties/test/app_rails_loader_test.rb index ceae78ae80..92cb3233d8 100644 --- a/railties/test/app_rails_loader_test.rb +++ b/railties/test/app_rails_loader_test.rb @@ -22,8 +22,8 @@ class AppRailsLoaderTest < ActiveSupport::TestCase exe = "#{script_dir}/rails" test "is not in a Rails application if #{exe} is not found in the current or parent directories" do - File.stubs(:exists?).with('bin/rails').returns(false) - File.stubs(:exists?).with('script/rails').returns(false) + File.stubs(:exist?).with('bin/rails').returns(false) + File.stubs(:exist?).with('script/rails').returns(false) assert !Rails::AppRailsLoader.exec_app_rails end diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb index 035535ce22..b235b51d90 100644 --- a/railties/test/application/assets_test.rb +++ b/railties/test/application/assets_test.rb @@ -37,7 +37,7 @@ module ApplicationTests end def assert_no_file_exists(filename) - assert !File.exists?(filename), "#{filename} does exist" + assert !File.exist?(filename), "#{filename} does exist" end test "assets routes have higher priority" do diff --git a/railties/test/application/middleware/remote_ip_test.rb b/railties/test/application/middleware/remote_ip_test.rb index 91c5807379..946b82eeb3 100644 --- a/railties/test/application/middleware/remote_ip_test.rb +++ b/railties/test/application/middleware/remote_ip_test.rb @@ -33,6 +33,16 @@ module ApplicationTests end end + test "works with both headers individually" do + make_basic_app + assert_nothing_raised(ActionDispatch::RemoteIp::IpSpoofAttackError) do + assert_equal "1.1.1.1", remote_ip("HTTP_X_FORWARDED_FOR" => "1.1.1.1") + end + assert_nothing_raised(ActionDispatch::RemoteIp::IpSpoofAttackError) do + assert_equal "1.1.1.2", remote_ip("HTTP_CLIENT_IP" => "1.1.1.2") + end + end + test "can disable IP spoofing check" do make_basic_app do |app| app.config.action_dispatch.ip_spoofing_check = false diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb index 31a35a09bb..20d1d76d78 100644 --- a/railties/test/application/middleware_test.rb +++ b/railties/test/application/middleware_test.rb @@ -144,6 +144,12 @@ module ApplicationTests assert_equal "Rack::Config", middleware.second end + test 'unshift middleware' do + add_to_config 'config.middleware.unshift Rack::Config' + boot! + assert_equal 'Rack::Config', middleware.first + end + test "Rails.cache does not respond to middleware" do add_to_config "config.cache_store = :memory_store" boot! diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb index 9e711f25bd..4d348c6b4b 100644 --- a/railties/test/application/rake/dbs_test.rb +++ b/railties/test/application/rake/dbs_test.rb @@ -33,12 +33,12 @@ module ApplicationTests Dir.chdir(app_path) do output = `bundle exec rake db:create` assert_equal output, "" - assert File.exists?(expected[:database]) + assert File.exist?(expected[:database]) assert_equal expected[:database], ActiveRecord::Base.connection_config[:database] output = `bundle exec rake db:drop` assert_equal output, "" - assert !File.exists?(expected[:database]) + assert !File.exist?(expected[:database]) end end diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index c1cb1c1eba..2169304aa4 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -227,7 +227,7 @@ module ApplicationTests # ensure we have a schema_migrations table to dump `bundle exec rake db:migrate db:structure:dump DB_STRUCTURE=db/my_structure.sql` end - assert File.exists?(File.join(app_path, 'db', 'my_structure.sql')) + assert File.exist?(File.join(app_path, 'db', 'my_structure.sql')) end def test_rake_dump_structure_should_be_called_twice_when_migrate_redo @@ -248,24 +248,24 @@ module ApplicationTests rails generate model product name:string; bundle exec rake db:migrate db:schema:cache:dump` end - assert File.exists?(File.join(app_path, 'db', 'schema_cache.dump')) + assert File.exist?(File.join(app_path, 'db', 'schema_cache.dump')) end def test_rake_clear_schema_cache Dir.chdir(app_path) do `bundle exec rake db:schema:cache:dump db:schema:cache:clear` end - assert !File.exists?(File.join(app_path, 'db', 'schema_cache.dump')) + assert !File.exist?(File.join(app_path, 'db', 'schema_cache.dump')) end def test_copy_templates Dir.chdir(app_path) do `bundle exec rake rails:templates:copy` %w(controller mailer scaffold).each do |dir| - assert File.exists?(File.join(app_path, 'lib', 'templates', 'erb', dir)) + assert File.exist?(File.join(app_path, 'lib', 'templates', 'erb', dir)) end %w(controller helper scaffold_controller assets).each do |dir| - assert File.exists?(File.join(app_path, 'lib', 'templates', 'rails', dir)) + assert File.exist?(File.join(app_path, 'lib', 'templates', 'rails', dir)) end end end diff --git a/railties/test/application/test_test.rb b/railties/test/application/test_test.rb index c7ad2fba8f..6f2f328588 100644 --- a/railties/test/application/test_test.rb +++ b/railties/test/application/test_test.rb @@ -24,7 +24,7 @@ module ApplicationTests end RUBY - run_test_file 'unit/foo_test.rb' + assert_successful_test_run 'unit/foo_test.rb' end test "integration test" do @@ -49,19 +49,48 @@ module ApplicationTests end RUBY - run_test_file 'integration/posts_test.rb' + assert_successful_test_run 'integration/posts_test.rb' + end + + test "enable full backtraces on test failures" do + app_file 'test/unit/failing_test.rb', <<-RUBY + require 'test_helper' + + class FailingTest < ActiveSupport::TestCase + def test_failure + raise "fail" + end + end + RUBY + + output = run_test_file('unit/failing_test.rb', env: { "BACKTRACE" => "1" }) + assert_match %r{/app/test/unit/failing_test\.rb}, output end private - def run_test_file(name) - result = ruby '-Itest', "#{app_path}/test/#{name}" + def assert_successful_test_run(name) + result = run_test_file(name) assert_equal 0, $?.to_i, result end + def run_test_file(name, options = {}) + ruby '-Itest', "#{app_path}/test/#{name}", options + end + def ruby(*args) + options = args.extract_options! + env = options.fetch(:env, {}) + env["RUBYLIB"] = $:.join(':') + Dir.chdir(app_path) do - `RUBYLIB='#{$:.join(':')}' #{Gem.ruby} #{args.join(' ')}` + `#{env_string(env)} #{Gem.ruby} #{args.join(' ')}` end end + + def env_string(variables) + variables.map do |key, value| + "#{key}='#{value}'" + end.join " " + end end end diff --git a/railties/test/commands/server_test.rb b/railties/test/commands/server_test.rb index 20afca2618..ba688f1e9e 100644 --- a/railties/test/commands/server_test.rb +++ b/railties/test/commands/server_test.rb @@ -27,26 +27,62 @@ class Rails::ServerTest < ActiveSupport::TestCase end def test_environment_with_rails_env - with_rails_env 'production' do - server = Rails::Server.new - assert_equal 'production', server.options[:environment] + with_rack_env nil do + with_rails_env 'production' do + server = Rails::Server.new + assert_equal 'production', server.options[:environment] + end end end def test_environment_with_rack_env - with_rack_env 'production' do - server = Rails::Server.new - assert_equal 'production', server.options[:environment] + with_rails_env nil do + with_rack_env 'production' do + server = Rails::Server.new + assert_equal 'production', server.options[:environment] + end end end def test_log_stdout - args = ["-e", "development"] - options = Rails::Server::Options.new.parse!(args) - assert_equal true, options[:log_stdout] + with_rack_env nil do + with_rails_env nil do + args = [] + options = Rails::Server::Options.new.parse!(args) + assert_equal true, options[:log_stdout] - args = ["-e", "production"] - options = Rails::Server::Options.new.parse!(args) - assert_equal false, options[:log_stdout] + args = ["-e", "development"] + options = Rails::Server::Options.new.parse!(args) + assert_equal true, options[:log_stdout] + + args = ["-e", "production"] + options = Rails::Server::Options.new.parse!(args) + assert_equal false, options[:log_stdout] + + with_rack_env 'development' do + args = [] + options = Rails::Server::Options.new.parse!(args) + assert_equal true, options[:log_stdout] + end + + with_rack_env 'production' do + args = [] + options = Rails::Server::Options.new.parse!(args) + assert_equal false, options[:log_stdout] + end + + with_rails_env 'development' do + args = [] + options = Rails::Server::Options.new.parse!(args) + assert_equal true, options[:log_stdout] + end + + with_rails_env 'production' do + args = [] + options = Rails::Server::Options.new.parse!(args) + assert_equal false, options[:log_stdout] + end + end + end end end diff --git a/railties/test/env_helpers.rb b/railties/test/env_helpers.rb index 6223c85bbf..330fe150ca 100644 --- a/railties/test/env_helpers.rb +++ b/railties/test/env_helpers.rb @@ -1,7 +1,10 @@ +require 'rails' + module EnvHelpers private def with_rails_env(env) + Rails.instance_variable_set :@_env, nil switch_env 'RAILS_ENV', env do switch_env 'RACK_ENV', nil do yield @@ -10,6 +13,7 @@ module EnvHelpers end def with_rack_env(env) + Rails.instance_variable_set :@_env, nil switch_env 'RACK_ENV', env do switch_env 'RAILS_ENV', nil do yield diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 2f0dfc7d3e..43a985615f 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -4,6 +4,7 @@ require 'generators/shared_generator_tests' DEFAULT_APP_FILES = %w( .gitignore + README.rdoc Gemfile Rakefile config.ru @@ -155,6 +156,52 @@ class AppGeneratorTest < Rails::Generators::TestCase end end + def test_add_gemfile_entry + template = Tempfile.open 'my_template' + template.puts 'gemfile_entry "tenderlove"' + template.flush + + run_generator([destination_root, "-m", template.path]) + assert_file "Gemfile", /tenderlove/ + ensure + template.close + template.unlink + end + + def test_add_skip_entry + template = Tempfile.open 'my_template' + template.puts 'add_gem_entry_filter { |gem| gem.name != "jbuilder" }' + template.flush + + run_generator([destination_root, "-m", template.path]) + assert_file "Gemfile" do |contents| + assert_no_match 'jbuilder', contents + end + ensure + template.close + template.unlink + end + + def test_application_html_checks_gems + template = Tempfile.open 'my_template' + template.puts 'add_gem_entry_filter { |gem| gem.name != "turbolinks" }' + template.flush + + run_generator([destination_root, "-m", template.path]) + assert_file "Gemfile" do |contents| + assert_no_match 'turbolinks', contents + end + assert_file "Gemfile" do |contents| + assert_no_match 'turbolinks', contents + end + assert_file "app/views/layouts/application.html.erb" do |contents| + assert_no_match 'turbolinks', contents + end + ensure + template.close + template.unlink + end + def test_config_another_database run_generator([destination_root, "-d", "mysql"]) assert_file "config/database.yml", /mysql/ @@ -254,7 +301,7 @@ class AppGeneratorTest < Rails::Generators::TestCase if defined?(JRUBY_VERSION) assert_gem "therubyrhino" else - assert_file "Gemfile", /# gem\s+["']therubyracer["']+, platforms: :ruby$/ + assert_file "Gemfile", /# gem\s+["']therubyracer["']+, \s+platforms: :ruby$/ end end @@ -298,18 +345,25 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_javascript_is_skipped_if_required run_generator [destination_root, "--skip-javascript"] - assert_file "app/assets/javascripts/application.js" do |contents| - assert_no_match %r{^//=\s+require\s}, contents - end + + assert_no_file "app/assets/javascripts" + assert_no_file "vendor/assets/javascripts" + assert_file "app/views/layouts/application.html.erb" do |contents| assert_match(/stylesheet_link_tag\s+"application", media: "all" %>/, contents) - assert_match(/javascript_include_tag\s+"application" \%>/, contents) + assert_no_match(/javascript_include_tag\s+"application" \%>/, contents) end + assert_file "Gemfile" do |content| - assert_match(/coffee-rails/, content) + assert_no_match(/coffee-rails/, content) end end + def test_inclusion_of_jbuilder + run_generator + assert_file "Gemfile", /gem 'jbuilder'/ + end + def test_inclusion_of_debugger run_generator assert_file "Gemfile", /# gem 'debugger'/ @@ -317,7 +371,7 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_inclusion_of_lazy_loaded_sdoc run_generator - assert_file 'Gemfile', /gem 'sdoc', require: false/ + assert_file 'Gemfile', /gem 'sdoc', \s+group: :doc, require: false/ end def test_template_from_dir_pwd @@ -367,6 +421,16 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_no_match(/run bundle install/, output) end + def test_application_name_with_spaces + path = File.join(destination_root, "foo bar".shellescape) + + # This also applies to MySQL apps but not with SQLite + run_generator [path, "-d", 'postgresql'] + + assert_file "foo bar/config/database.yml", /database: foo_bar_development/ + assert_file "foo bar/config/initializers/session_store.rb", /key: '_foo_bar/ + end + protected def action(*args, &block) diff --git a/railties/test/generators/argv_scrubber_test.rb b/railties/test/generators/argv_scrubber_test.rb new file mode 100644 index 0000000000..a94350cbd7 --- /dev/null +++ b/railties/test/generators/argv_scrubber_test.rb @@ -0,0 +1,136 @@ +require 'active_support/test_case' +require 'active_support/testing/autorun' +require 'rails/generators/rails/app/app_generator' +require 'tempfile' + +module Rails + module Generators + class ARGVScrubberTest < ActiveSupport::TestCase # :nodoc: + # Future people who read this... These tests are just to surround the + # current behavior of the ARGVScrubber, they do not mean that the class + # *must* act this way, I just want to prevent regressions. + + def test_version + ['-v', '--version'].each do |str| + scrubber = ARGVScrubber.new [str] + output = nil + exit_code = nil + scrubber.extend(Module.new { + define_method(:puts) { |str| output = str } + define_method(:exit) { |code| exit_code = code } + }) + scrubber.prepare! + assert_equal "Rails #{Rails::VERSION::STRING}", output + assert_equal 0, exit_code + end + end + + def test_default_help + argv = ['zomg', 'how', 'are', 'you'] + scrubber = ARGVScrubber.new argv + args = scrubber.prepare! + assert_equal ['--help'] + argv.drop(1), args + end + + def test_prepare_returns_args + scrubber = ARGVScrubber.new ['hi mom'] + args = scrubber.prepare! + assert_equal '--help', args.first + end + + def test_no_mutations + scrubber = ARGVScrubber.new ['hi mom'].freeze + args = scrubber.prepare! + assert_equal '--help', args.first + end + + def test_new_command_no_rc + scrubber = Class.new(ARGVScrubber) { + def self.default_rc_file + File.join(Dir.tmpdir, 'whatever') + end + }.new ['new'] + args = scrubber.prepare! + assert_equal [], args + end + + def test_new_homedir_rc + file = Tempfile.new 'myrcfile' + file.puts '--hello-world' + file.flush + + message = nil + scrubber = Class.new(ARGVScrubber) { + define_singleton_method(:default_rc_file) do + file.path + end + define_method(:puts) { |msg| message = msg } + }.new ['new'] + args = scrubber.prepare! + assert_equal ['--hello-world'], args + assert_match 'hello-world', message + assert_match file.path, message + ensure + file.close + file.unlink + end + + def test_rc_whitespace_separated + file = Tempfile.new 'myrcfile' + file.puts '--hello --world' + file.flush + + message = nil + scrubber = Class.new(ARGVScrubber) { + define_method(:puts) { |msg| message = msg } + }.new ['new', "--rc=#{file.path}"] + args = scrubber.prepare! + assert_equal ['--hello', '--world'], args + ensure + file.close + file.unlink + end + + def test_new_rc_option + file = Tempfile.new 'myrcfile' + file.puts '--hello-world' + file.flush + + message = nil + scrubber = Class.new(ARGVScrubber) { + define_method(:puts) { |msg| message = msg } + }.new ['new', "--rc=#{file.path}"] + args = scrubber.prepare! + assert_equal ['--hello-world'], args + assert_match 'hello-world', message + assert_match file.path, message + ensure + file.close + file.unlink + end + + def test_new_rc_option_and_custom_options + file = Tempfile.new 'myrcfile' + file.puts '--hello' + file.puts '--world' + file.flush + + scrubber = Class.new(ARGVScrubber) { + define_method(:puts) { |msg| } + }.new ['new', 'tenderapp', '--love', "--rc=#{file.path}"] + + args = scrubber.prepare! + assert_equal ["tenderapp", "--hello", "--world", "--love"], args + ensure + file.close + file.unlink + end + + def test_no_rc + scrubber = ARGVScrubber.new ['new', '--no-rc'] + args = scrubber.prepare! + assert_equal [], args + end + end + end +end diff --git a/railties/test/generators/generator_test.rb b/railties/test/generators/generator_test.rb new file mode 100644 index 0000000000..94d2c1bf50 --- /dev/null +++ b/railties/test/generators/generator_test.rb @@ -0,0 +1,86 @@ +require 'active_support/test_case' +require 'active_support/testing/autorun' +require 'rails/generators/app_base' +require 'rails/generators/rails/app/app_generator' + +module Rails + module Generators + class GeneratorTest < ActiveSupport::TestCase + def make_builder_class + Class.new(AppBase) do + add_shared_options_for "application" + + # include a module to get around thor's method_added hook + include(Module.new { + def gemfile_entries; super; end + def invoke_all; super; self; end + def add_gem_entry_filter; super; end + def gemfile_entry(*args); super; end + }) + end + end + + def test_construction + klass = make_builder_class + assert klass.start(['new', 'blah']) + end + + def test_add_gem + klass = make_builder_class + generator = klass.start(['new', 'blah']) + generator.gemfile_entry 'tenderlove' + assert_includes generator.gemfile_entries.map(&:name), 'tenderlove' + end + + def test_add_gem_with_version + klass = make_builder_class + generator = klass.start(['new', 'blah']) + generator.gemfile_entry 'tenderlove', '2.0.0' + assert generator.gemfile_entries.find { |gfe| + gfe.name == 'tenderlove' && gfe.version == '2.0.0' + } + end + + def test_add_github_gem + klass = make_builder_class + generator = klass.start(['new', 'blah']) + generator.gemfile_entry 'tenderlove', github: 'hello world' + assert generator.gemfile_entries.find { |gfe| + gfe.name == 'tenderlove' && gfe.options[:github] == 'hello world' + } + end + + def test_add_path_gem + klass = make_builder_class + generator = klass.start(['new', 'blah']) + generator.gemfile_entry 'tenderlove', path: 'hello world' + assert generator.gemfile_entries.find { |gfe| + gfe.name == 'tenderlove' && gfe.options[:path] == 'hello world' + } + end + + def test_filter + klass = make_builder_class + generator = klass.start(['new', 'blah']) + gems = generator.gemfile_entries + generator.add_gem_entry_filter { |gem| + gem.name != gems.first.name + } + assert_equal gems.drop(1), generator.gemfile_entries + end + + def test_two_filters + klass = make_builder_class + generator = klass.start(['new', 'blah']) + gems = generator.gemfile_entries + generator.add_gem_entry_filter { |gem| + gem.name != gems.first.name + } + generator.add_gem_entry_filter { |gem| + gem.name != gems[1].name + } + assert_equal gems.drop(2), generator.gemfile_entries + end + end + end +end diff --git a/railties/test/generators/named_base_test.rb b/railties/test/generators/named_base_test.rb index 2bc2c33a72..ac5cfff229 100644 --- a/railties/test/generators/named_base_test.rb +++ b/railties/test/generators/named_base_test.rb @@ -117,6 +117,25 @@ class NamedBaseTest < Rails::Generators::TestCase assert Rails::Generators.hidden_namespaces.include?('hidden') end + def test_scaffold_plural_names_with_model_name_option + g = generator ['Admin::Foo'], model_name: 'User' + assert_name g, 'user', :singular_name + assert_name g, 'User', :name + assert_name g, 'user', :file_path + assert_name g, 'User', :class_name + assert_name g, 'user', :file_name + assert_name g, 'User', :human_name + assert_name g, 'users', :plural_name + assert_name g, 'user', :i18n_scope + assert_name g, 'users', :table_name + assert_name g, 'Admin::Foos', :controller_name + assert_name g, %w(admin), :controller_class_path + assert_name g, 'Admin::Foos', :controller_class_name + assert_name g, 'admin/foos', :controller_file_path + assert_name g, 'foos', :controller_file_name + assert_name g, 'admin.foos', :controller_i18n_scope + end + protected def assert_name(generator, value, method) diff --git a/railties/test/generators/scaffold_controller_generator_test.rb b/railties/test/generators/scaffold_controller_generator_test.rb index 013cb78252..26e56a162c 100644 --- a/railties/test/generators/scaffold_controller_generator_test.rb +++ b/railties/test/generators/scaffold_controller_generator_test.rb @@ -166,4 +166,13 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase assert_match(/render action: 'new'/, content) end end + + def test_model_name_option + run_generator ["Admin::User", "--model-name=User"] + assert_file "app/controllers/admin/users_controller.rb" do |content| + assert_instance_method :index, content do |m| + assert_match("@users = User.all", m) + end + end + end end diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb index 5130b285a9..eac28badfe 100644 --- a/railties/test/generators_test.rb +++ b/railties/test/generators_test.rb @@ -15,7 +15,7 @@ class GeneratorsTest < Rails::Generators::TestCase end def test_simple_invoke - assert File.exists?(File.join(@path, 'generators', 'model_generator.rb')) + assert File.exist?(File.join(@path, 'generators', 'model_generator.rb')) TestUnit::Generators::ModelGenerator.expects(:start).with(["Account"], {}) Rails::Generators.invoke("test_unit:model", ["Account"]) end @@ -31,7 +31,7 @@ class GeneratorsTest < Rails::Generators::TestCase end def test_should_give_higher_preference_to_rails_generators - assert File.exists?(File.join(@path, 'generators', 'model_generator.rb')) + assert File.exist?(File.join(@path, 'generators', 'model_generator.rb')) Rails::Generators::ModelGenerator.expects(:start).with(["Account"], {}) warnings = capture(:stderr){ Rails::Generators.invoke :model, ["Account"] } assert warnings.empty? diff --git a/railties/test/paths_test.rb b/railties/test/paths_test.rb index 178c505865..ed4559ec6f 100644 --- a/railties/test/paths_test.rb +++ b/railties/test/paths_test.rb @@ -3,7 +3,7 @@ require 'rails/paths' class PathsTest < ActiveSupport::TestCase def setup - File.stubs(:exists?).returns(true) + File.stubs(:exist?).returns(true) @root = Rails::Paths::Root.new("/foo/bar") end diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb index d095535abd..3075ffee0d 100644 --- a/railties/test/railties/engine_test.rb +++ b/railties/test/railties/engine_test.rb @@ -90,8 +90,8 @@ module RailtiesTest Dir.chdir(app_path) do output = `bundle exec rake bukkits:install:migrations` - assert File.exists?("#{app_path}/db/migrate/2_create_users.bukkits.rb") - assert File.exists?("#{app_path}/db/migrate/3_add_last_name_to_users.bukkits.rb") + assert File.exist?("#{app_path}/db/migrate/2_create_users.bukkits.rb") + assert File.exist?("#{app_path}/db/migrate/3_add_last_name_to_users.bukkits.rb") assert_match(/Copied migration 2_create_users.bukkits.rb from bukkits/, output) assert_match(/Copied migration 3_add_last_name_to_users.bukkits.rb from bukkits/, output) assert_match(/NOTE: Migration 3_create_sessions.rb from bukkits has been skipped/, output) @@ -136,7 +136,7 @@ module RailtiesTest Dir.chdir(@plugin.path) do output = `bundle exec rake app:bukkits:install:migrations` - assert File.exists?("#{app_path}/db/migrate/0_add_first_name_to_users.bukkits.rb") + assert File.exist?("#{app_path}/db/migrate/0_add_first_name_to_users.bukkits.rb") assert_match(/Copied migration 0_add_first_name_to_users.bukkits.rb from bukkits/, output) assert_equal 1, Dir["#{app_path}/db/migrate/*.rb"].length end |