diff options
author | aditya-kapoor <aditya.kapoor@vinsol.com> | 2013-12-10 11:44:07 +0530 |
---|---|---|
committer | aditya-kapoor <aditya.kapoor@vinsol.com> | 2013-12-10 11:44:07 +0530 |
commit | 0c55335074ed7baf2f165ada168f726f1f7bba68 (patch) | |
tree | 2a8428d72f6890cb0eb6c5b9b1d40b7b2ce93d8b | |
parent | 6b9b0767bf480d53029afeb3f8e86f66e933fb41 (diff) | |
parent | fe077b50c9ce65c4ac1cc718c34dda45cd24c6fe (diff) | |
download | rails-0c55335074ed7baf2f165ada168f726f1f7bba68.tar.gz rails-0c55335074ed7baf2f165ada168f726f1f7bba68.tar.bz2 rails-0c55335074ed7baf2f165ada168f726f1f7bba68.zip |
Merge branch 'master' of github.com:lifo/docrails
588 files changed, 10534 insertions, 6771 deletions
diff --git a/.travis.yml b/.travis.yml index 7f5b2095e3..1e354a8fb0 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 + - rbx-2.2.1 - jruby-19mode - - rbx-19mode env: - "GEM=railties" - "GEM=ap,am,amo,as,av" @@ -15,8 +16,8 @@ env: - "GEM=ar:postgresql" matrix: allow_failures: + - rvm: rbx-2.2.1 - rvm: jruby-19mode - - rvm: rbx-19mode notifications: email: false irc: @@ -8,10 +8,11 @@ gemspec gem 'mocha', '~> 0.14', require: false gem 'rack-cache', '~> 1.2' -gem 'bcrypt-ruby', '~> 3.1.0' +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 @@ -61,15 +62,27 @@ platforms :ruby do end platforms :jruby do - git 'git://github.com/jruby/activerecord-jdbc-adapter.git' do - gem 'activerecord-jdbcsqlite3-adapter' + gem 'json' + if ENV['AR_JDBC'] + gem 'activerecord-jdbcsqlite3-adapter', github: 'jruby/activerecord-jdbc-adapter', branch: 'master' group :db do - gem 'activerecord-jdbcmysql-adapter' - gem 'activerecord-jdbcpostgresql-adapter' + 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', '~> 2.0' + gem 'rubysl', '~> 2.0' +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..dc8c6bdf74 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -1,7 +1,14 @@ -* 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 +* Instrument the generation of Action Mailer messages. The time it takes to + generate a message is written to the log. - *Alex Tsukernik* + *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. + + *Alex Tsukernik* Please check [4-0-stable](https://github.com/rails/rails/blob/4-0-stable/actionmailer/CHANGELOG.md) for previous changes. diff --git a/actionmailer/README.rdoc b/actionmailer/README.rdoc index a14a6ba18f..c3dcd3c3e4 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 version 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: @@ -76,7 +74,7 @@ Or you can just chain the methods together like: == Setting defaults -It is possible to set default values that will be used in every method in your Action Mailer class. To implement this functionality, you just call the public class method <tt>default</tt> which you get for free from ActionMailer::Base. This method accepts a Hash as the parameter. You can use any of the headers e-mail messages has, like <tt>:from</tt> as the key. You can also pass in a string as the key, like "Content-Type", but Action Mailer does this out of the box for you, so you won't need to worry about that. Finally, it is also possible to pass in a Proc that will get evaluated when it is needed. +It is possible to set default values that will be used in every method in your Action Mailer class. To implement this functionality, you just call the public class method <tt>default</tt> which you get for free from <tt>ActionMailer::Base</tt>. This method accepts a Hash as the parameter. You can use any of the headers e-mail messages has, like <tt>:from</tt> as the key. You can also pass in a string as the key, like "Content-Type", but Action Mailer does this out of the box for you, so you won't need to worry about that. Finally, it is also possible to pass in a Proc that will get evaluated when it is needed. Note that every value you set with this method will get overwritten if you use the same key in your mailer method. diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 9dcd4bf554..501fee55aa 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -373,6 +373,12 @@ module ActionMailer include AbstractController::AssetPaths include AbstractController::Callbacks + PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [:@_action_has_layout] + + def _protected_ivars # :nodoc: + PROTECTED_IVARS + end + helper ActionMailer::MailHelper private_class_method :new #:nodoc: @@ -385,10 +391,6 @@ module ActionMailer parts_order: [ "text/plain", "text/enriched", "text/html" ] }.freeze - def self.default_protected_instance_vars - super.concat [:@_action_has_layout] - end - class << self # Register one or more Observers which will be notified when mail is delivered. def register_observers(*observers) @@ -513,11 +515,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 + } + + ActiveSupport::Notifications.instrument("process.action_mailer", payload) do + lookup_context.skip_default_locale! - super - @_message = NullMail.new unless @_mail_was_called + super + @_message = NullMail.new unless @_mail_was_called + end end class NullMail #:nodoc: @@ -687,9 +696,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/delivery_methods.rb b/actionmailer/lib/action_mailer/delivery_methods.rb index 9a1a27c8ed..aedcd81e52 100644 --- a/actionmailer/lib/action_mailer/delivery_methods.rb +++ b/actionmailer/lib/action_mailer/delivery_methods.rb @@ -64,7 +64,7 @@ module ActionMailer raise "Delivery method cannot be nil" when Symbol if klass = delivery_methods[method] - mail.delivery_method(klass,(send(:"#{method}_settings") || {}).merge!(options || {})) + mail.delivery_method(klass, (send(:"#{method}_settings") || {}).merge(options || {})) else raise "Invalid delivery method #{method.inspect}" end 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/delivery_methods_test.rb b/actionmailer/test/delivery_methods_test.rb index 61a037ea18..20412c7bb2 100644 --- a/actionmailer/test/delivery_methods_test.rb +++ b/actionmailer/test/delivery_methods_test.rb @@ -152,6 +152,9 @@ class MailDeliveryTest < ActiveSupport::TestCase assert_equal "overridden", delivery_method_instance.settings[:user_name] assert_equal "somethingobtuse", delivery_method_instance.settings[:password] assert_equal delivery_method_instance.settings.merge(overridden_options), delivery_method_instance.settings + + # make sure that overriding delivery method options per mail instance doesn't affect the Base setting + assert_equal settings, ActionMailer::Base.smtp_settings end test "non registered delivery methods raises errors" do diff --git a/actionmailer/test/fixtures/base_mailer/email_with_translations.html.erb b/actionmailer/test/fixtures/base_mailer/email_with_translations.html.erb index 30466dd005..d676a6d2da 100644 --- a/actionmailer/test/fixtures/base_mailer/email_with_translations.html.erb +++ b/actionmailer/test/fixtures/base_mailer/email_with_translations.html.erb @@ -1 +1 @@ -<%= t('.greet_user', :name => 'lifo') %>
\ No newline at end of file +<%= t('.greet_user', name: 'lifo') %>
\ No newline at end of file diff --git a/actionmailer/test/fixtures/raw_email10 b/actionmailer/test/fixtures/raw_email10 deleted file mode 100644 index edad5ccff1..0000000000 --- a/actionmailer/test/fixtures/raw_email10 +++ /dev/null @@ -1,20 +0,0 @@ -Return-Path: <xxx@xxxx.xxx> -Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id C1B953B4CB6 for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:05 -0500 -Received: from SMS-GTYxxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id ca for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:04 -0500 -Received: from xxx.xxxx.xxx by SMS-GTYxxx.xxxx.xxx with ESMTP id j4AKR3r23323 for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:03 -0500 -Date: Tue, 10 May 2005 15:27:03 -0500 -From: xxx@xxxx.xxx -Sender: xxx@xxxx.xxx -To: xxxxxxxxxxx@xxxx.xxxx.xxx -Message-Id: <xxx@xxxx.xxx> -X-Original-To: xxxxxxxxxxx@xxxx.xxxx.xxx -Delivered-To: xxx@xxxx.xxx -Importance: normal -Content-Type: text/plain; charset=X-UNKNOWN - -Test test. Hi. Waving. m - ----------------------------------------------------------------- -Sent via Bell Mobility's Text Messaging service. -Envoyé par le service de messagerie texte de Bell Mobilité. ----------------------------------------------------------------- diff --git a/actionmailer/test/fixtures/raw_email12 b/actionmailer/test/fixtures/raw_email12 deleted file mode 100644 index 2cd31720d3..0000000000 --- a/actionmailer/test/fixtures/raw_email12 +++ /dev/null @@ -1,32 +0,0 @@ -Mime-Version: 1.0 (Apple Message framework v730) -Content-Type: multipart/mixed; boundary=Apple-Mail-13-196941151 -Message-Id: <9169D984-4E0B-45EF-82D4-8F5E53AD7012@example.com> -From: foo@example.com -Subject: testing -Date: Mon, 6 Jun 2005 22:21:22 +0200 -To: blah@example.com - - ---Apple-Mail-13-196941151 -Content-Transfer-Encoding: quoted-printable -Content-Type: text/plain; - charset=ISO-8859-1; - delsp=yes; - format=flowed - -This is the first part. - ---Apple-Mail-13-196941151 -Content-Type: image/jpeg -Content-Transfer-Encoding: base64 -Content-Location: Photo25.jpg -Content-ID: <qbFGyPQAS8> -Content-Disposition: inline - -jamisSqGSIb3DQEHAqCAMIjamisxCzAJBgUrDgMCGgUAMIAGCSqGSjamisEHAQAAoIIFSjCCBUYw -ggQujamisQICBD++ukQwDQYJKojamisNAQEFBQAwMTELMAkGA1UEBhMCRjamisAKBgNVBAoTA1RE -QzEUMBIGjamisxMLVERDIE9DRVMgQ0jamisNMDQwMjI5MTE1OTAxWhcNMDYwMjamisIyOTAxWjCB -gDELMAkGA1UEjamisEsxKTAnBgNVBAoTIEjamisuIG9yZ2FuaXNhdG9yaXNrIHRpbjamisRuaW5= - ---Apple-Mail-13-196941151-- - diff --git a/actionmailer/test/fixtures/raw_email13 b/actionmailer/test/fixtures/raw_email13 deleted file mode 100644 index 7d9314e36a..0000000000 --- a/actionmailer/test/fixtures/raw_email13 +++ /dev/null @@ -1,29 +0,0 @@ -Mime-Version: 1.0 (Apple Message framework v730) -Content-Type: multipart/mixed; boundary=Apple-Mail-13-196941151 -Message-Id: <9169D984-4E0B-45EF-82D4-8F5E53AD7012@example.com> -From: foo@example.com -Subject: testing -Date: Mon, 6 Jun 2005 22:21:22 +0200 -To: blah@example.com - - ---Apple-Mail-13-196941151 -Content-Transfer-Encoding: quoted-printable -Content-Type: text/plain; - charset=ISO-8859-1; - delsp=yes; - format=flowed - -This is the first part. - ---Apple-Mail-13-196941151 -Content-Type: text/x-ruby-script; name="hello.rb" -Content-Transfer-Encoding: 7bit -Content-Disposition: attachment; - filename="api.rb" - -puts "Hello, world!" -gets - ---Apple-Mail-13-196941151-- - diff --git a/actionmailer/test/fixtures/raw_email2 b/actionmailer/test/fixtures/raw_email2 deleted file mode 100644 index 9f87bb2a98..0000000000 --- a/actionmailer/test/fixtures/raw_email2 +++ /dev/null @@ -1,114 +0,0 @@ -From xxxxxxxxx.xxxxxxx@gmail.com Sun May 8 19:07:09 2005 -Return-Path: <xxxxxxxxx.xxxxxxx@gmail.com> -X-Original-To: xxxxx@xxxxx.xxxxxxxxx.com -Delivered-To: xxxxx@xxxxx.xxxxxxxxx.com -Received: from localhost (localhost [127.0.0.1]) - by xxxxx.xxxxxxxxx.com (Postfix) with ESMTP id 06C9DA98D - for <xxxxx@xxxxx.xxxxxxxxx.com>; Sun, 8 May 2005 19:09:13 +0000 (GMT) -Received: from xxxxx.xxxxxxxxx.com ([127.0.0.1]) - by localhost (xxxxx.xxxxxxxxx.com [127.0.0.1]) (amavisd-new, port 10024) - with LMTP id 88783-08 for <xxxxx@xxxxx.xxxxxxxxx.com>; - Sun, 8 May 2005 19:09:12 +0000 (GMT) -Received: from xxxxxxx.xxxxxxxxx.com (xxxxxxx.xxxxxxxxx.com [69.36.39.150]) - by xxxxx.xxxxxxxxx.com (Postfix) with ESMTP id 10D8BA960 - for <xxxxx@xxxxxxxxx.org>; Sun, 8 May 2005 19:09:12 +0000 (GMT) -Received: from zproxy.gmail.com (zproxy.gmail.com [64.233.162.199]) - by xxxxxxx.xxxxxxxxx.com (Postfix) with ESMTP id 9EBC4148EAB - for <xxxxx@xxxxxxxxx.com>; Sun, 8 May 2005 14:09:11 -0500 (CDT) -Received: by zproxy.gmail.com with SMTP id 13so1233405nzp - for <xxxxx@xxxxxxxxx.com>; Sun, 08 May 2005 12:09:11 -0700 (PDT) -DomainKey-Signature: a=rsa-sha1; q=dns; c=nofws; - s=beta; d=gmail.com; - h=received:message-id:date:from:reply-to:to:subject:in-reply-to:mime-version:content-type:references; - b=cid1mzGEFa3gtRa06oSrrEYfKca2CTKu9sLMkWxjbvCsWMtp9RGEILjUz0L5RySdH5iO661LyNUoHRFQIa57bylAbXM3g2DTEIIKmuASDG3x3rIQ4sHAKpNxP7Pul+mgTaOKBv+spcH7af++QEJ36gHFXD2O/kx9RePs3JNf/K8= -Received: by 10.36.10.16 with SMTP id 16mr1012493nzj; - Sun, 08 May 2005 12:09:11 -0700 (PDT) -Received: by 10.36.5.10 with HTTP; Sun, 8 May 2005 12:09:11 -0700 (PDT) -Message-ID: <e85734b90505081209eaaa17b@mail.gmail.com> -Date: Sun, 8 May 2005 14:09:11 -0500 -From: xxxxxxxxx xxxxxxx <xxxxxxxxx.xxxxxxx@gmail.com> -Reply-To: xxxxxxxxx xxxxxxx <xxxxxxxxx.xxxxxxx@gmail.com> -To: xxxxx xxxx <xxxxx@xxxxxxxxx.com> -Subject: Fwd: Signed email causes file attachments -In-Reply-To: <F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@mac.com> -Mime-Version: 1.0 -Content-Type: multipart/mixed; - boundary="----=_Part_5028_7368284.1115579351471" -References: <F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@mac.com> - -------=_Part_5028_7368284.1115579351471 -Content-Type: text/plain; charset=ISO-8859-1 -Content-Transfer-Encoding: quoted-printable -Content-Disposition: inline - -We should not include these files or vcards as attachments. - ----------- Forwarded message ---------- -From: xxxxx xxxxxx <xxxxxxxx@xxx.com> -Date: May 8, 2005 1:17 PM -Subject: Signed email causes file attachments -To: xxxxxxx@xxxxxxxxxx.com - - -Hi, - -Just started to use my xxxxxxxx account (to set-up a GTD system, -natch) and noticed that when I send content via email the signature/ -certificate from my email account gets added as a file (e.g. -"smime.p7s"). - -Obviously I can uncheck the signature option in the Mail compose -window but how often will I remember to do that? - -Is there any way these kind of files could be ignored, e.g. via some -sort of exclusions list? - -------=_Part_5028_7368284.1115579351471 -Content-Type: application/pkcs7-signature; name=smime.p7s -Content-Transfer-Encoding: base64 -Content-Disposition: attachment; filename="smime.p7s" - -MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAQAAoIIGFDCCAs0w -ggI2oAMCAQICAw5c+TANBgkqhkiG9w0BAQQFADBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhh -d3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVt -YWlsIElzc3VpbmcgQ0EwHhcNMDUwMzI5MDkzOTEwWhcNMDYwMzI5MDkzOTEwWjBCMR8wHQYDVQQD -ExZUaGF3dGUgRnJlZW1haWwgTWVtYmVyMR8wHQYJKoZIhvcNAQkBFhBzbWhhdW5jaEBtYWMuY29t -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn90dPsYS3LjfMY211OSYrDQLzwNYPlAL -7+/0XA+kdy8/rRnyEHFGwhNCDmg0B6pxC7z3xxJD/8GfCd+IYUUNUQV5m9MkxfP9pTVXZVIYLaBw -o8xS3A0a1LXealcmlEbJibmKkEaoXci3MhryLgpaa+Kk/sH02SNatDO1vS28bPsibZpcc6deFrla -hSYnL+PW54mDTGHIcCN2fbx/Y6qspzqmtKaXrv75NBtuy9cB6KzU4j2xXbTkAwz3pRSghJJaAwdp -+yIivAD3vr0kJE3p+Ez34HMh33EXEpFoWcN+MCEQZD9WnmFViMrvfvMXLGVFQfAAcC060eGFSRJ1 -ZQ9UVQIDAQABoy0wKzAbBgNVHREEFDASgRBzbWhhdW5jaEBtYWMuY29tMAwGA1UdEwEB/wQCMAAw -DQYJKoZIhvcNAQEEBQADgYEAQMrg1n2pXVWteP7BBj+Pk3UfYtbuHb42uHcLJjfjnRlH7AxnSwrd -L3HED205w3Cq8T7tzVxIjRRLO/ljq0GedSCFBky7eYo1PrXhztGHCTSBhsiWdiyLWxKlOxGAwJc/ -lMMnwqLOdrQcoF/YgbjeaUFOQbUh94w9VDNpWZYCZwcwggM/MIICqKADAgECAgENMA0GCSqGSIb3 -DQEBBQUAMIHRMQswCQYDVQQGEwJaQTEVMBMGA1UECBMMV2VzdGVybiBDYXBlMRIwEAYDVQQHEwlD -YXBlIFRvd24xGjAYBgNVBAoTEVRoYXd0ZSBDb25zdWx0aW5nMSgwJgYDVQQLEx9DZXJ0aWZpY2F0 -aW9uIFNlcnZpY2VzIERpdmlzaW9uMSQwIgYDVQQDExtUaGF3dGUgUGVyc29uYWwgRnJlZW1haWwg -Q0ExKzApBgkqhkiG9w0BCQEWHHBlcnNvbmFsLWZyZWVtYWlsQHRoYXd0ZS5jb20wHhcNMDMwNzE3 -MDAwMDAwWhcNMTMwNzE2MjM1OTU5WjBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3RlIENv -bnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVtYWlsIElz -c3VpbmcgQ0EwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMSmPFVzVftOucqZWh5owHUEcJ3f -6f+jHuy9zfVb8hp2vX8MOmHyv1HOAdTlUAow1wJjWiyJFXCO3cnwK4Vaqj9xVsuvPAsH5/EfkTYk -KhPPK9Xzgnc9A74r/rsYPge/QIACZNenprufZdHFKlSFD0gEf6e20TxhBEAeZBlyYLf7AgMBAAGj -gZQwgZEwEgYDVR0TAQH/BAgwBgEB/wIBADBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsLnRo -YXd0ZS5jb20vVGhhd3RlUGVyc29uYWxGcmVlbWFpbENBLmNybDALBgNVHQ8EBAMCAQYwKQYDVR0R -BCIwIKQeMBwxGjAYBgNVBAMTEVByaXZhdGVMYWJlbDItMTM4MA0GCSqGSIb3DQEBBQUAA4GBAEiM -0VCD6gsuzA2jZqxnD3+vrL7CF6FDlpSdf0whuPg2H6otnzYvwPQcUCCTcDz9reFhYsPZOhl+hLGZ -GwDFGguCdJ4lUJRix9sncVcljd2pnDmOjCBPZV+V2vf3h9bGCE6u9uo05RAaWzVNd+NWIXiC3CEZ -Nd4ksdMdRv9dX2VPMYIC5zCCAuMCAQEwaTBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3Rl -IENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVtYWls -IElzc3VpbmcgQ0ECAw5c+TAJBgUrDgMCGgUAoIIBUzAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcB -MBwGCSqGSIb3DQEJBTEPFw0wNTA1MDgxODE3NDZaMCMGCSqGSIb3DQEJBDEWBBQSkG9j6+hB0pKp -fV9tCi/iP59sNTB4BgkrBgEEAYI3EAQxazBpMGIxCzAJBgNVBAYTAlpBMSUwIwYDVQQKExxUaGF3 -dGUgQ29uc3VsdGluZyAoUHR5KSBMdGQuMSwwKgYDVQQDEyNUaGF3dGUgUGVyc29uYWwgRnJlZW1h -aWwgSXNzdWluZyBDQQIDDlz5MHoGCyqGSIb3DQEJEAILMWugaTBiMQswCQYDVQQGEwJaQTElMCMG -A1UEChMcVGhhd3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNv -bmFsIEZyZWVtYWlsIElzc3VpbmcgQ0ECAw5c+TANBgkqhkiG9w0BAQEFAASCAQAm1GeF7dWfMvrW -8yMPjkhE+R8D1DsiCoWSCp+5gAQm7lcK7V3KrZh5howfpI3TmCZUbbaMxOH+7aKRKpFemxoBY5Q8 -rnCkbpg/++/+MI01T69hF/rgMmrGcrv2fIYy8EaARLG0xUVFSZHSP+NQSYz0TTmh4cAESHMzY3JA -nHOoUkuPyl8RXrimY1zn0lceMXlweZRouiPGuPNl1hQKw8P+GhOC5oLlM71UtStnrlk3P9gqX5v7 -Tj7Hx057oVfY8FMevjxGwU3EK5TczHezHbWWgTyum9l2ZQbUQsDJxSniD3BM46C1VcbDLPaotAZ0 -fTYLZizQfm5hcWEbfYVzkSzLAAAAAAAA -------=_Part_5028_7368284.1115579351471-- - diff --git a/actionmailer/test/fixtures/raw_email3 b/actionmailer/test/fixtures/raw_email3 deleted file mode 100644 index 3a0927490a..0000000000 --- a/actionmailer/test/fixtures/raw_email3 +++ /dev/null @@ -1,70 +0,0 @@ -From xxxx@xxxx.com Tue May 10 11:28:07 2005 -Return-Path: <xxxx@xxxx.com> -X-Original-To: xxxx@xxxx.com -Delivered-To: xxxx@xxxx.com -Received: from localhost (localhost [127.0.0.1]) - by xxx.xxxxx.com (Postfix) with ESMTP id 50FD3A96F - for <xxxx@xxxx.com>; Tue, 10 May 2005 17:26:50 +0000 (GMT) -Received: from xxx.xxxxx.com ([127.0.0.1]) - by localhost (xxx.xxxxx.com [127.0.0.1]) (amavisd-new, port 10024) - with LMTP id 70060-03 for <xxxx@xxxx.com>; - Tue, 10 May 2005 17:26:49 +0000 (GMT) -Received: from xxx.xxxxx.com (xxx.xxxxx.com [69.36.39.150]) - by xxx.xxxxx.com (Postfix) with ESMTP id 8B957A94B - for <xxxx@xxxx.com>; Tue, 10 May 2005 17:26:48 +0000 (GMT) -Received: from xxx.xxxxx.com (xxx.xxxxx.com [64.233.184.203]) - by xxx.xxxxx.com (Postfix) with ESMTP id 9972514824C - for <xxxx@xxxx.com>; Tue, 10 May 2005 12:26:40 -0500 (CDT) -Received: by xxx.xxxxx.com with SMTP id 68so1694448wri - for <xxxx@xxxx.com>; Tue, 10 May 2005 10:26:40 -0700 (PDT) -DomainKey-Signature: a=rsa-sha1; q=dns; c=nofws; - s=beta; d=xxxxx.com; - h=received:message-id:date:from:reply-to:to:subject:mime-version:content-type; - b=g8ZO5ttS6GPEMAz9WxrRk9+9IXBUfQIYsZLL6T88+ECbsXqGIgfGtzJJFn6o9CE3/HMrrIGkN5AisxVFTGXWxWci5YA/7PTVWwPOhJff5BRYQDVNgRKqMl/SMttNrrRElsGJjnD1UyQ/5kQmcBxq2PuZI5Zc47u6CILcuoBcM+A= -Received: by 10.54.96.19 with SMTP id t19mr621017wrb; - Tue, 10 May 2005 10:26:39 -0700 (PDT) -Received: by 10.54.110.5 with HTTP; Tue, 10 May 2005 10:26:39 -0700 (PDT) -Message-ID: <xxxx@xxxx.com> -Date: Tue, 10 May 2005 11:26:39 -0600 -From: Test Tester <xxxx@xxxx.com> -Reply-To: Test Tester <xxxx@xxxx.com> -To: xxxx@xxxx.com, xxxx@xxxx.com -Subject: Another PDF -Mime-Version: 1.0 -Content-Type: multipart/mixed; - boundary="----=_Part_2192_32400445.1115745999735" -X-Virus-Scanned: amavisd-new at textdrive.com - -------=_Part_2192_32400445.1115745999735 -Content-Type: text/plain; charset=ISO-8859-1 -Content-Transfer-Encoding: quoted-printable -Content-Disposition: inline - -Just attaching another PDF, here, to see what the message looks like, -and to see if I can figure out what is going wrong here. - -------=_Part_2192_32400445.1115745999735 -Content-Type: application/pdf; name="broken.pdf" -Content-Transfer-Encoding: base64 -Content-Disposition: attachment; filename="broken.pdf" - -JVBERi0xLjQNCiXk9tzfDQoxIDAgb2JqDQo8PCAvTGVuZ3RoIDIgMCBSDQogICAvRmlsdGVyIC9G -bGF0ZURlY29kZQ0KPj4NCnN0cmVhbQ0KeJy9Wt2KJbkNvm/od6jrhZxYln9hWEh2p+8HBvICySaE -ycLuTV4/1ifJ9qnq09NpSBimu76yLUuy/qzqcPz7+em3Ixx/CDc6CsXxs3b5+fvfjr/8cPz6/BRu -rbfAx/n3739/fuJylJ5u5fjX81OuDr4deK4Bz3z/aDP+8fz0yw8g0Ofq7ktr1Mn+u28rvhy/jVeD -QSa+9YNKHP/pxjvDNfVAx/m3MFz54FhvTbaseaxiDoN2LeMVMw+yA7RbHSCDzxZuaYB2E1Yay7QU -x89vz0+tyFDKMlAHK5yqLmnjF+c4RjEiQIUeKwblXMe+AsZjN1J5yGQL5DHpDHksurM81rF6PKab -gK6zAarIDzIiUY23rJsN9iorAE816aIu6lsgAdQFsuhhkHOUFgVjp2GjMqSewITXNQ27jrMeamkg -1rPI3iLWG2CIaSBB+V1245YVRICGbbpYKHc2USFDl6M09acQVQYhlwIrkBNLISvXhGlF1wi5FHCw -wxZkoGNJlVeJCEsqKA+3YAV5AMb6KkeaqEJQmFKKQU8T1pRi2ihE1Y4CDrqoYFFXYjJJOatsyzuI -8SIlykuxKTMibWK8H1PgEvqYgs4GmQSrEjJAalgGirIhik+p4ZQN9E3ETFPAHE1b8pp1l/0Rc1gl -fQs0ABWvyoZZzU8VnPXwVVcO9BEsyjEJaO6eBoZRyKGlrKoYoOygA8BGIzgwN3RQ15ouigG5idZQ -fx2U4Db2CqiLO0WHAZoylGiCAqhniNQjFjQPSkmjwfNTgQ6M1Ih+eWo36wFmjIxDJZiGUBiWsAyR -xX3EekGOizkGI96Ol9zVZTAivikURhRsHh2E3JhWMpSTZCnnonrLhMCodgrNcgo4uyJUJc6qnVss -nrGd1Ptr0YwisCOYyIbUwVjV4xBUNLbguSO2YHujonAMJkMdSI7bIw91Akq2AUlMUWGFTMAOamjU -OvZQCxIkY2pCpMFo/IwLdVLHs6nddwTRrgoVbvLU9eB0G4EMndV0TNoxHbt3JBWwK6hhv3iHfDtF -yokB302IpEBTnWICde4uYc/1khDbSIkQopO6lcqamGBu1OSE3N5IPSsZX00CkSHRiiyx6HQIShsS -HSVNswdVsaOUSAWq9aYhDtGDaoG5a3lBGkYt/lFlBFt1UqrYnzVtUpUQnLiZeouKgf1KhRBViRRk -ExepJCzTwEmFDalIRbLEGtw0gfpESOpIAF/NnpPzcVCG86s0g2DuSyd41uhNGbEgaSrWEXORErbw -------=_Part_2192_32400445.1115745999735-- - diff --git a/actionmailer/test/fixtures/raw_email4 b/actionmailer/test/fixtures/raw_email4 deleted file mode 100644 index 639ad40e49..0000000000 --- a/actionmailer/test/fixtures/raw_email4 +++ /dev/null @@ -1,59 +0,0 @@ -Return-Path: <xxx@xxxx.xxx> -Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id 6AAEE3B4D23 for <xxx@xxxx.xxx>; Sun, 8 May 2005 12:30:23 -0500 -Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id j48HUC213279 for <xxx@xxxx.xxx>; Sun, 8 May 2005 12:30:13 -0500 -Received: from conversion-xxx.xxxx.xxx.net by xxx.xxxx.xxx id <0IG600901LQ64I@xxx.xxxx.xxx> for <xxx@xxxx.xxx>; Sun, 8 May 2005 12:30:12 -0500 -Received: from agw1 by xxx.xxxx.xxx with ESMTP id <0IG600JFYLYCAxxx@xxxx.xxx> for <xxx@xxxx.xxx>; Sun, 8 May 2005 12:30:12 -0500 -Date: Sun, 8 May 2005 12:30:08 -0500 -From: xxx@xxxx.xxx -To: xxx@xxxx.xxx -Message-Id: <7864245.1115573412626.JavaMxxx@xxxx.xxx> -Subject: Filth -Mime-Version: 1.0 -Content-Type: multipart/mixed; boundary=mimepart_427e4cb4ca329_133ae40413c81ef -X-Mms-Priority: 1 -X-Mms-Transaction-Id: 3198421808-0 -X-Mms-Message-Type: 0 -X-Mms-Sender-Visibility: 1 -X-Mms-Read-Reply: 1 -X-Original-To: xxx@xxxx.xxx -X-Mms-Message-Class: 0 -X-Mms-Delivery-Report: 0 -X-Mms-Mms-Version: 16 -Delivered-To: xxx@xxxx.xxx -X-Nokia-Ag-Version: 2.0 - -This is a multi-part message in MIME format. - ---mimepart_427e4cb4ca329_133ae40413c81ef -Content-Type: multipart/mixed; boundary=mimepart_427e4cb4cbd97_133ae40413c8217 - - - ---mimepart_427e4cb4cbd97_133ae40413c8217 -Content-Type: text/plain; charset=utf-8 -Content-Transfer-Encoding: 7bit -Content-Disposition: inline -Content-Location: text.txt - -Some text - ---mimepart_427e4cb4cbd97_133ae40413c8217-- - ---mimepart_427e4cb4ca329_133ae40413c81ef -Content-Type: text/plain; charset=us-ascii -Content-Transfer-Encoding: 7bit - - --- -This Orange Multi Media Message was sent wirefree from an Orange -MMS phone. If you would like to reply, please text or phone the -sender directly by using the phone number listed in the sender's -address. To learn more about Orange's Multi Media Messaging -Service, find us on the Web at xxx.xxxx.xxx.uk/mms - - ---mimepart_427e4cb4ca329_133ae40413c81ef - - ---mimepart_427e4cb4ca329_133ae40413c81ef- - diff --git a/actionmailer/test/fixtures/raw_email5 b/actionmailer/test/fixtures/raw_email5 deleted file mode 100644 index bbe31bcdc5..0000000000 --- a/actionmailer/test/fixtures/raw_email5 +++ /dev/null @@ -1,19 +0,0 @@ -Return-Path: <xxx@xxxx.xxx> -Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id C1B953B4CB6 for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:05 -0500 -Received: from SMS-GTYxxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id ca for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:04 -0500 -Received: from xxx.xxxx.xxx by SMS-GTYxxx.xxxx.xxx with ESMTP id j4AKR3r23323 for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:03 -0500 -Date: Tue, 10 May 2005 15:27:03 -0500 -From: xxx@xxxx.xxx -Sender: xxx@xxxx.xxx -To: xxxxxxxxxxx@xxxx.xxxx.xxx -Message-Id: <xxx@xxxx.xxx> -X-Original-To: xxxxxxxxxxx@xxxx.xxxx.xxx -Delivered-To: xxx@xxxx.xxx -Importance: normal - -Test test. Hi. Waving. m - ----------------------------------------------------------------- -Sent via Bell Mobility's Text Messaging service. -Envoyé par le service de messagerie texte de Bell Mobilité. ----------------------------------------------------------------- diff --git a/actionmailer/test/fixtures/raw_email6 b/actionmailer/test/fixtures/raw_email6 deleted file mode 100644 index 8e37bd7392..0000000000 --- a/actionmailer/test/fixtures/raw_email6 +++ /dev/null @@ -1,20 +0,0 @@ -Return-Path: <xxx@xxxx.xxx> -Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id C1B953B4CB6 for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:05 -0500 -Received: from SMS-GTYxxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id ca for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:04 -0500 -Received: from xxx.xxxx.xxx by SMS-GTYxxx.xxxx.xxx with ESMTP id j4AKR3r23323 for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:03 -0500 -Date: Tue, 10 May 2005 15:27:03 -0500 -From: xxx@xxxx.xxx -Sender: xxx@xxxx.xxx -To: xxxxxxxxxxx@xxxx.xxxx.xxx -Message-Id: <xxx@xxxx.xxx> -X-Original-To: xxxxxxxxxxx@xxxx.xxxx.xxx -Delivered-To: xxx@xxxx.xxx -Importance: normal -Content-Type: text/plain; charset=us-ascii - -Test test. Hi. Waving. m - ----------------------------------------------------------------- -Sent via Bell Mobility's Text Messaging service. -Envoyé par le service de messagerie texte de Bell Mobilité. ----------------------------------------------------------------- diff --git a/actionmailer/test/fixtures/raw_email7 b/actionmailer/test/fixtures/raw_email7 deleted file mode 100644 index da64ada8a5..0000000000 --- a/actionmailer/test/fixtures/raw_email7 +++ /dev/null @@ -1,66 +0,0 @@ -Mime-Version: 1.0 (Apple Message framework v730) -Content-Type: multipart/mixed; boundary=Apple-Mail-13-196941151 -Message-Id: <9169D984-4E0B-45EF-82D4-8F5E53AD7012@example.com> -From: foo@example.com -Subject: testing -Date: Mon, 6 Jun 2005 22:21:22 +0200 -To: blah@example.com - - ---Apple-Mail-13-196941151 -Content-Type: multipart/mixed; - boundary=Apple-Mail-12-196940926 - - ---Apple-Mail-12-196940926 -Content-Transfer-Encoding: quoted-printable -Content-Type: text/plain; - charset=ISO-8859-1; - delsp=yes; - format=flowed - -This is the first part. - ---Apple-Mail-12-196940926 -Content-Transfer-Encoding: 7bit -Content-Type: text/x-ruby-script; - x-unix-mode=0666; - name="test.rb" -Content-Disposition: attachment; - filename=test.rb - -puts "testing, testing" - ---Apple-Mail-12-196940926 -Content-Transfer-Encoding: base64 -Content-Type: application/pdf; - x-unix-mode=0666; - name="test.pdf" -Content-Disposition: inline; - filename=test.pdf - -YmxhaCBibGFoIGJsYWg= - ---Apple-Mail-12-196940926 -Content-Transfer-Encoding: 7bit -Content-Type: text/plain; - charset=US-ASCII; - format=flowed - - - ---Apple-Mail-12-196940926-- - ---Apple-Mail-13-196941151 -Content-Transfer-Encoding: base64 -Content-Type: application/pkcs7-signature; - name=smime.p7s -Content-Disposition: attachment; - filename=smime.p7s - -jamisSqGSIb3DQEHAqCAMIjamisxCzAJBgUrDgMCGgUAMIAGCSqGSjamisEHAQAAoIIFSjCCBUYw -ggQujamisQICBD++ukQwDQYJKojamisNAQEFBQAwMTELMAkGA1UEBhMCRjamisAKBgNVBAoTA1RE -QzEUMBIGjamisxMLVERDIE9DRVMgQ0jamisNMDQwMjI5MTE1OTAxWhcNMDYwMjamisIyOTAxWjCB -gDELMAkGA1UEjamisEsxKTAnBgNVBAoTIEjamisuIG9yZ2FuaXNhdG9yaXNrIHRpbjamisRuaW5= - ---Apple-Mail-13-196941151-- diff --git a/actionmailer/test/fixtures/raw_email8 b/actionmailer/test/fixtures/raw_email8 deleted file mode 100644 index 79996365b3..0000000000 --- a/actionmailer/test/fixtures/raw_email8 +++ /dev/null @@ -1,47 +0,0 @@ -From xxxxxxxxx.xxxxxxx@gmail.com Sun May 8 19:07:09 2005 -Return-Path: <xxxxxxxxx.xxxxxxx@gmail.com> -Message-ID: <e85734b90505081209eaaa17b@mail.gmail.com> -Date: Sun, 8 May 2005 14:09:11 -0500 -From: xxxxxxxxx xxxxxxx <xxxxxxxxx.xxxxxxx@gmail.com> -Reply-To: xxxxxxxxx xxxxxxx <xxxxxxxxx.xxxxxxx@gmail.com> -To: xxxxx xxxx <xxxxx@xxxxxxxxx.com> -Subject: Fwd: Signed email causes file attachments -In-Reply-To: <F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@mac.com> -Mime-Version: 1.0 -Content-Type: multipart/mixed; - boundary="----=_Part_5028_7368284.1115579351471" -References: <F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@mac.com> - -------=_Part_5028_7368284.1115579351471 -Content-Type: text/plain; charset=ISO-8859-1 -Content-Transfer-Encoding: quoted-printable -Content-Disposition: inline - -We should not include these files or vcards as attachments. - ----------- Forwarded message ---------- -From: xxxxx xxxxxx <xxxxxxxx@xxx.com> -Date: May 8, 2005 1:17 PM -Subject: Signed email causes file attachments -To: xxxxxxx@xxxxxxxxxx.com - - -Hi, - -Test attachments oddly encoded with japanese charset. - - -------=_Part_5028_7368284.1115579351471 -Content-Type: application/octet-stream; name*=iso-2022-jp'ja'01%20Quien%20Te%20Dij%8aat.%20Pitbull.mp3 -Content-Transfer-Encoding: base64 -Content-Disposition: attachment - -MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAQAAoIIGFDCCAs0w -ggI2oAMCAQICAw5c+TANBgkqhkiG9w0BAQQFADBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhh -d3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVt -YWlsIElzc3VpbmcgQ0EwHhcNMDUwMzI5MDkzOTEwWhcNMDYwMzI5MDkzOTEwWjBCMR8wHQYDVQQD -ExZUaGF3dGUgRnJlZW1haWwgTWVtYmVyMR8wHQYJKoZIhvcNAQkBFhBzbWhhdW5jaEBtYWMuY29t -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn90dPsYS3LjfMY211OSYrDQLzwNYPlAL -7+/0XA+kdy8/rRnyEHFGwhNCDmg0B6pxC7z3xxJD/8GfCd+IYUUNUQV5m9MkxfP9pTVXZVIYLaBw -------=_Part_5028_7368284.1115579351471-- - diff --git a/actionmailer/test/fixtures/raw_email9 b/actionmailer/test/fixtures/raw_email9 deleted file mode 100644 index 02ea0b05c5..0000000000 --- a/actionmailer/test/fixtures/raw_email9 +++ /dev/null @@ -1,28 +0,0 @@ -Received: from xxx.xxx.xxx ([xxx.xxx.xxx.xxx] verified) - by xxx.com (CommuniGate Pro SMTP 4.2.8) - with SMTP id 2532598 for xxx@xxx.com; Wed, 23 Feb 2005 17:51:49 -0500 -Received-SPF: softfail - receiver=xxx.com; client-ip=xxx.xxx.xxx.xxx; envelope-from=xxx@xxx.xxx -quite Delivered-To: xxx@xxx.xxx -Received: by xxx.xxx.xxx (Wostfix, from userid xxx) - id 0F87F333; Wed, 23 Feb 2005 16:16:17 -0600 -Date: Wed, 23 Feb 2005 18:20:17 -0400 -From: "xxx xxx" <xxx@xxx.xxx> -Message-ID: <4D6AA7EB.6490534@xxx.xxx> -To: xxx@xxx.com -Subject: Stop adware/spyware once and for all. -X-Scanned-By: MIMEDefang 2.11 (www dot roaringpenguin dot com slash mimedefang) - -You are infected with: -Ad Ware and Spy Ware - -Get your free scan and removal download now, -before it gets any worse. - -http://xxx.xxx.info?aid=3D13&?stat=3D4327kdzt - - - - -no more? (you will still be infected) -http://xxx.xxx.info/discon/?xxx@xxx.com diff --git a/actionmailer/test/fixtures/raw_email_quoted_with_0d0a b/actionmailer/test/fixtures/raw_email_quoted_with_0d0a deleted file mode 100644 index 8a2c25a5dd..0000000000 --- a/actionmailer/test/fixtures/raw_email_quoted_with_0d0a +++ /dev/null @@ -1,14 +0,0 @@ -Mime-Version: 1.0 (Apple Message framework v730)
-Message-Id: <9169D984-4E0B-45EF-82D4-8F5E53AD7012@example.com>
-From: foo@example.com
-Subject: testing
-Date: Mon, 6 Jun 2005 22:21:22 +0200
-To: blah@example.com
-Content-Transfer-Encoding: quoted-printable
-Content-Type: text/plain
-
-A fax has arrived from remote ID ''.=0D=0A-----------------------=
--------------------------------------=0D=0ATime: 3/9/2006 3:50:52=
- PM=0D=0AReceived from remote ID: =0D=0AInbound user ID XXXXXXXXXX, r=
-outing code XXXXXXXXX=0D=0AResult: (0/352;0/0) Successful Send=0D=0AP=
-age record: 1 - 1=0D=0AElapsed time: 00:58 on channel 11=0D=0A
diff --git a/actionmailer/test/fixtures/raw_email_with_invalid_characters_in_content_type b/actionmailer/test/fixtures/raw_email_with_invalid_characters_in_content_type deleted file mode 100644 index a8ff7ed4cb..0000000000 --- a/actionmailer/test/fixtures/raw_email_with_invalid_characters_in_content_type +++ /dev/null @@ -1,104 +0,0 @@ -Return-Path: <mikel.other@baci> -Received: from some.isp.com by baci with ESMTP id 632BD5758 for <mikel.lindsaar@baci>; Sun, 21 Oct 2007 19:38:21 +1000 -Date: Sun, 21 Oct 2007 19:38:13 +1000 -From: Mikel Lindsaar <mikel.other@baci> -Reply-To: Mikel Lindsaar <mikel.other@baci> -To: mikel.lindsaar@baci -Message-Id: <009601c813c6$19df3510$0437d30a@mikel091a> -Subject: Testing outlook -Mime-Version: 1.0 -Content-Type: multipart/alternative; boundary=----=_NextPart_000_0093_01C81419.EB75E850 -Delivered-To: mikel.lindsaar@baci -X-Mimeole: Produced By Microsoft MimeOLE V6.00.2900.3138 -X-Msmail-Priority: Normal - -This is a multi-part message in MIME format. - - -------=_NextPart_000_0093_01C81419.EB75E850 -Content-Type: text/plain; charset=iso-8859-1 -Content-Transfer-Encoding: Quoted-printable - -Hello -This is an outlook test - -So there. - -Me. - -------=_NextPart_000_0093_01C81419.EB75E850 -Content-Type: text/html; charset=iso-8859-1 -Content-Transfer-Encoding: Quoted-printable - -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> -<HTML><HEAD> -<META http-equiv=3DContent-Type content=3D"text/html; = -charset=3Diso-8859-1"> -<META content=3D"MSHTML 6.00.6000.16525" name=3DGENERATOR> -<STYLE></STYLE> -</HEAD> -<BODY bgColor=3D#ffffff> -<DIV><FONT face=3DArial size=3D2>Hello</FONT></DIV> -<DIV><FONT face=3DArial size=3D2><STRONG>This is an outlook=20 -test</STRONG></FONT></DIV> -<DIV><FONT face=3DArial size=3D2><STRONG></STRONG></FONT> </DIV> -<DIV><FONT face=3DArial size=3D2><STRONG>So there.</STRONG></FONT></DIV> -<DIV><FONT face=3DArial size=3D2></FONT> </DIV> -<DIV><FONT face=3DArial size=3D2>Me.</FONT></DIV></BODY></HTML> - - -------=_NextPart_000_0093_01C81419.EB75E850-- - - -Return-Path: <mikel.other@baci> -Received: from some.isp.com by baci with ESMTP id 632BD5758 for <mikel.lindsaar@baci>; Sun, 21 Oct 2007 19:38:21 +1000 -Date: Sun, 21 Oct 2007 19:38:13 +1000 -From: Mikel Lindsaar <mikel.other@baci> -Reply-To: Mikel Lindsaar <mikel.other@baci> -To: mikel.lindsaar@baci -Message-Id: <009601c813c6$19df3510$0437d30a@mikel091a> -Subject: Testing outlook -Mime-Version: 1.0 -Content-Type: multipart/alternative; boundary=----=_NextPart_000_0093_01C81419.EB75E850 -Delivered-To: mikel.lindsaar@baci -X-Mimeole: Produced By Microsoft MimeOLE V6.00.2900.3138 -X-Msmail-Priority: Normal - -This is a multi-part message in MIME format. - - -------=_NextPart_000_0093_01C81419.EB75E850 -Content-Type: text/plain; charset=iso-8859-1 -Content-Transfer-Encoding: Quoted-printable - -Hello -This is an outlook test - -So there. - -Me. - -------=_NextPart_000_0093_01C81419.EB75E850 -Content-Type: text/html; charset=iso-8859-1 -Content-Transfer-Encoding: Quoted-printable - -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> -<HTML><HEAD> -<META http-equiv=3DContent-Type content=3D"text/html; = -charset=3Diso-8859-1"> -<META content=3D"MSHTML 6.00.6000.16525" name=3DGENERATOR> -<STYLE></STYLE> -</HEAD> -<BODY bgColor=3D#ffffff> -<DIV><FONT face=3DArial size=3D2>Hello</FONT></DIV> -<DIV><FONT face=3DArial size=3D2><STRONG>This is an outlook=20 -test</STRONG></FONT></DIV> -<DIV><FONT face=3DArial size=3D2><STRONG></STRONG></FONT> </DIV> -<DIV><FONT face=3DArial size=3D2><STRONG>So there.</STRONG></FONT></DIV> -<DIV><FONT face=3DArial size=3D2></FONT> </DIV> -<DIV><FONT face=3DArial size=3D2>Me.</FONT></DIV></BODY></HTML> - - -------=_NextPart_000_0093_01C81419.EB75E850-- - - diff --git a/actionmailer/test/fixtures/raw_email_with_nested_attachment b/actionmailer/test/fixtures/raw_email_with_nested_attachment deleted file mode 100644 index 429c408c5d..0000000000 --- a/actionmailer/test/fixtures/raw_email_with_nested_attachment +++ /dev/null @@ -1,100 +0,0 @@ -From jamis@37signals.com Thu Feb 22 11:20:31 2007 -Mime-Version: 1.0 (Apple Message framework v752.3) -Message-Id: <2CCE0408-10C7-4045-9B16-A1C11C31469B@37signals.com> -Content-Type: multipart/signed; - micalg=sha1; - boundary=Apple-Mail-42-587703407; - protocol="application/pkcs7-signature" -To: Jamis Buck <jamis@jamisbuck.org> -Subject: Testing attachments -From: Jamis Buck <jamis@37signals.com> -Date: Thu, 22 Feb 2007 11:20:31 -0700 - - ---Apple-Mail-42-587703407 -Content-Type: multipart/mixed; - boundary=Apple-Mail-41-587703287 - - ---Apple-Mail-41-587703287 -Content-Transfer-Encoding: 7bit -Content-Type: text/plain; - charset=US-ASCII; - format=flowed - -Here is a test of an attachment via email. - -- Jamis - - ---Apple-Mail-41-587703287 -Content-Transfer-Encoding: base64 -Content-Type: image/png; - x-unix-mode=0644; - name=byo-ror-cover.png -Content-Disposition: inline; - filename=truncated.png - -iVBORw0KGgoAAAANSUhEUgAAAKUAAADXCAYAAAB7wZEQAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz -AAALEgAACxIB0t1+/AAAABd0RVh0Q3JlYXRpb24gVGltZQAxLzI1LzIwMDeD9CJVAAAAGHRFWHRT -b2Z0d2FyZQBBZG9iZSBGaXJld29ya3NPsx9OAAAyBWlUWHRYTUw6Y29tLmFkb2JlLnhtcDw/eHBh -Y2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+Cjx4OnhtcG1l -dGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDQuMS1j -MDIwIDEuMjU1NzE2LCBUdWUgT2N0IDEwIDIwMDYgMjM6MTY6MzQiPgogICA8cmRmOlJERiB4bWxu -czpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAg -ICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp4YXA9Imh0 -dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iPgogICAgICAgICA8eGFwOkNyZWF0b3JUb29sPkFk -b2JlIEZpcmV3b3JrcyBDUzM8L3hhcDpDcmVhdG9yVG9vbD4KICAgICAgICAgPHhhcDpDcmVhdGVE -YXRlPjIwMDctMDEtMjVUMDU6Mjg6MjFaPC94YXA6Q3JlYXRlRGF0ZT4KICAgICAgICAgPHhhcDpN -b2RpZnlEYXRlPjIwMDctMDEtMjVUMDU6Mjg6MjFaPC94YXA6TW9kaWZ5RGF0ZT4KICAgICAgPC9y -ZGY6RGVzY3JpcHRpb24+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAg -ICAgICAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyI+CiAgICAg -ICAgIDxkYzpmb3JtYXQ+aW1hZ2UvcG5nPC9kYzpmb3JtYXQ+CiAgICAgIDwvcmRmOkRlc2NyaXB0 -hhojpmnJMfaYFmSkXWg5PGCmHXVj/c9At0hSK2xGdd8F3muk0VFjb4f5Ue0ksQ8qAcq0delaXhdb -DjKNnF+3B3t9kObZYmk7AZgWYqO9anpR3wpM9sQ5XslB9a+kWyTtNb0fOmudzGHfPFBQDKesyycm -DBL7Cw5bXjIEuci+SSOm/LYnXDZu6iuPEj8lYBb+OU8xx1f9m+e5rhJiYKqjo5vHfiZp+VUkW9xc -Ufd6JHNWc47PkQqb9ie3SLEZB/ZqyAssiqURY+G35iOMZUrHbasHnb80QAPv9FHtAbJIyro7bi5b -ai2TEAKen5+LJNWrglZjm3UbZvt7KryA2J5b5J1jZF8kL6GzvG1Zqx54Y1y7J7n20wMOt9frG2sW -uwGP07kNz3732vf6bfvAvLldfS+9fts2euXY37D+R29FGZdlnhzV4TTFmPJduBP2RbNNua4rTqcT -Qt7Xy1KUB0AHSdP5AZQYvHZg7WD1XvYeMO1A9HhZPqMX5KXbMBrn2efxns/ee21674efxz4Tp/fq -2HZ648dgYaC1i3Vq1IbNPq3PvDTPezY9FaRISjvnzWqdgcWN8EJgjnNq+Z7ktOm9l2Nfth28EZi4 -bG/we5JwxM+Tql47/D/X6b38I8/RyxvxPJrX6zvQbo3h9jyJx+C0ALX327QETHl5eYlaYCT5rPTb -+5/rAq26t3lKIxV/p88hq6ptngdgCzoPjJqndiLfc/6y5A14WeDFGNPct4iUsJBV2bYzLEV7m83s -6Rp63VPhHKC/g/LzaU9qexJRr56043JWinqAtfZqsSm1sjoznthl54dtCqv+uL4nIY+oYWuc3+nH -kGfn8b0HQpvOYLQAZUDanbJs3jQhITZEgdarZK+cO6ySlL13rut5nFaN23s7u3Snz6eRPTkCoc2/ -Vp1zHfZVFpZ87FiMVLV1iqyK5rlzfji2GzjfDsodlD+Weo5UD4h6PwKqzQMqID0tq2VjjFVSMpis -ZLRAs7sePZBZAHI+gIanB8I7MD+femAceeUe2Kxa5jS950kZ1p5eNEdeX1+jFmSpZ+1EdWCsDcne -NPNgUHNw3aYpnzv9PGTX0uo94EtN9qq1rOdxe3kc79T8ukeHJJ8Fnxej6qlylbLLsjQLOy6Xy2a1 -kefs/N+nM7+S7IG5/E5Yc7F003pWErLjbH0O5cGadiMptSB/DZ5U5DI9yeg5MFYyMj8lC/Y7/Xjq -OZlWcnpg9aQfXz2HRq+Wn5xOp6gN8tWq8R44e2pfyzLYemEgprst+XXk2Zj2nXlbsG05BprndTMv -C3QRaXczshhVsHnMgfYn80Y2g5JureA6wBasPeP7LkE/jvZMJAaf/g/U2RelHsisvan5FqweIAHg -Pwc7L68GxvVDAAAAAElFTkSuQmCC - ---Apple-Mail-41-587703287-- - ---Apple-Mail-42-587703407 -Content-Transfer-Encoding: base64 -Content-Type: application/pkcs7-signature; - name=smime.p7s -Content-Disposition: attachment; - filename=smime.p7s - -MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAQAAoIIGJzCCAuAw -ggJJoAMCAQICEFjnFNYXwDEZRWY5EkfzopUwDQYJKoZIhvcNAQEFBQAwYjELMAkGA1UEBhMCWkEx -JTAjBgNVBAoTHFRoYXd0ZSBDb25zdWx0aW5nIChQdHkpIEx0ZC4xLDAqBgNVBAMTI1RoYXd0ZSBQ -ZXJzb25hbCBGcmVlbWFpbCBJc3N1aW5nIENBMB4XDTA2MDkxMjE3MDExMloXDTA3MDkxMjE3MDEx -MlowRTEfMB0GA1UEAxMWVGhhd3RlIEZyZWVtYWlsIE1lbWJlcjEiMCAGCSqGSIb3DQEJARYTamFt -aXNAMzdzaWduYWxzLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAO2A9JeOFIFJ -G6z8pTcAldrZ2nMe+Xb1tNrbHgoVzN/QhHXM4qst2Ml93cmFLjMmwG7P9RJeU4oNx+jTqVoBB7NV -Ne1/o56Do0KhfMZ9iUDQdPLbkZMq4EEpFMdm6PyM3muRKwPhj66iAWe/osCb8DowUK2f66vaRx0Z -Y0MQHIIrXE02Ta4IfAhIfPqBLkZ4WgTYBHN9vMdYea1jF0GO4gqGk1wqwb3yxv2QMYMbwJ6SI+k/ -ZjkSR/OilTCBhwYLKoZIhvcNAQkQAgsxeKB2MGIxCzAJBgNVBAYTAlpBMSUwIwYDVQQKExxUaGF3 -dGUgQ29uc3VsdGluZyAoUHR5KSBMdGQuMSwwKgYDVQQDEyNUaGF3dGUgUGVyc29uYWwgRnJlZW1h -aWwgSXNzdWluZyBDQQIQWOcU1hfAMRlFZjkSR/OilTANBgkqhkiG9w0BAQEFAASCAQCfwQiC3v6/ -yleRDGv3bJ4nQYQ+c3mz3+mn3Xi6uU35n3piwxZZaWRdmLyiXPvU+QReHpSf3l2qsEZM3sdE0XF9 -eRul/+QTFJcDNXOEAxG1zC2Gpz+6c6RrX4Ou12Pwkp+pNrZWTSY/mZgdqcArupOBcZi7qBjoWcy5 -wb54dfvSSjrjmqLbkH/E8ww/6gGQuU/xXpAUZgUrTmQHrNKeIdSh5oDkOxFaFWvnmb8Z/2ixKqW/ -Ux6WqamyvBtTs/5YBEtnpZOk+uVoscYEUBhU+DVJ2OSvTdXSivMtBdXmGTsG22k+P1NGUHi/A7ev -xPaO0uk4V8xyjNlN4HPuGpkrlXwPAAAAAAAA - ---Apple-Mail-42-587703407-- diff --git a/actionmailer/test/fixtures/raw_email_with_partially_quoted_subject b/actionmailer/test/fixtures/raw_email_with_partially_quoted_subject deleted file mode 100644 index e86108da1e..0000000000 --- a/actionmailer/test/fixtures/raw_email_with_partially_quoted_subject +++ /dev/null @@ -1,14 +0,0 @@ -From jamis@37signals.com Mon May 2 16:07:05 2005 -Mime-Version: 1.0 (Apple Message framework v622) -Content-Transfer-Encoding: base64 -Message-Id: <d3b8cf8e49f04480850c28713a1f473e@37signals.com> -Content-Type: text/plain; - charset=EUC-KR; - format=flowed -To: jamis@37signals.com -From: Jamis Buck <jamis@37signals.com> -Subject: Re: Test: =?UTF-8?B?Iua8ouWtlyI=?= mid =?UTF-8?B?Iua8ouWtlyI=?= tail -Date: Mon, 2 May 2005 16:07:05 -0600 - -tOu6zrrQwMcguLbC+bChwfa3ziwgv+y4rrTCIMfPs6q01MC7ILnPvcC0z7TZLg0KDQrBpiDAzLin -wLogSmFtaXPA1LTPtNku diff --git a/actionmailer/test/fixtures/test_mailer/included_subtemplate.text.erb b/actionmailer/test/fixtures/test_mailer/included_subtemplate.text.erb index a93c30ea1a..ae3cfa77e7 100644 --- a/actionmailer/test/fixtures/test_mailer/included_subtemplate.text.erb +++ b/actionmailer/test/fixtures/test_mailer/included_subtemplate.text.erb @@ -1 +1 @@ -Hey Ho, <%= render :partial => "subtemplate" %>
\ No newline at end of file +Hey Ho, <%= render partial: "subtemplate" %>
\ No newline at end of file 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/actionmailer/test/mailers/base_mailer.rb b/actionmailer/test/mailers/base_mailer.rb index 6584bf5195..bd991e209e 100644 --- a/actionmailer/test/mailers/base_mailer.rb +++ b/actionmailer/test/mailers/base_mailer.rb @@ -120,7 +120,7 @@ class BaseMailer < ActionMailer::Base end def with_nil_as_return_value - mail(:template_name => "welcome") + mail(template_name: "welcome") nil end diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 8b78e0f801..638c05619d 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,7 +1,126 @@ -* Introduce `BasicRendering` which is the most basic rendering implementation. It - allows to `render :text` and `render :nothing` without depending on Action View. +* Fix formatting for `rake routes` when a section is shorter than a header. - *Łukasz Strzałkowski* + *Sıtkı Bağdat* + +* Take a hash with options inside array in `#url_for`. + + Example: + + url_for [:new, :admin, :post, { param: 'value' }] + # => http://example.com/admin/posts/new?param=value + + *Andrey Ognevsky* + +* 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. + + *Derek Prior* + +* Fix incorrect `assert_redirected_to` failure message for protocol-relative + URLs. + + *Derek Prior* + +* Fix an issue where router can't recognize downcased url encoding path. + + 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. + + *Ricardo de Cillo* + +* Do not break params filtering on `nil` values. + + Fixes #12149. + + *Vasiliy Ermolovich* * Separate Action View completely from Action Pack. @@ -14,21 +133,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* @@ -43,7 +162,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* @@ -61,7 +180,7 @@ * Fix `ActionDispatch::ParamsParser#parse_formatted_parameters` to rewind body input stream on parsing json params. - Fixes #11345 + Fixes #11345. *Yuri Bol*, *Paul Nikitochkin* @@ -94,7 +213,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* @@ -103,15 +222,17 @@ 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* -* ActionView extracted from ActionPack +* ActionView extracted from ActionPack. *Piotr Sarnacki*, *Łukasz Strzałkowski* -* Fix removing trailing slash for mounted apps #3215 +* Fix removing trailing slash for mounted apps. + + Fixes #3215. *Piotr Sarnacki* diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index b4315c1f57..8a85bf346a 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -21,10 +21,9 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', version - s.add_dependency 'rack', '~> 1.5.2' - s.add_dependency 'rack-test', '~> 0.6.2' + s.add_dependency 'rack', '~> 1.5.2' + s.add_dependency 'rack-test', '~> 0.6.2' - s.add_development_dependency 'actionview', version - s.add_development_dependency 'activemodel', version - s.add_development_dependency 'tzinfo', '~> 0.3.37' + s.add_development_dependency 'actionview', version + s.add_development_dependency 'activemodel', version end diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index a84ed17bd4..af5de815bb 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -114,11 +114,6 @@ module AbstractController end end - # Define some internal variables that should not be propagated to the view. - def self.default_protected_instance_vars - [] - end - abstract! # Calls the action going through the entire action dispatch stack. diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index 41f19fba78..a6e230a088 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -1,5 +1,6 @@ require 'active_support/concern' require 'active_support/core_ext/class/attribute' +require 'set' module AbstractController class DoubleRenderError < Error @@ -13,8 +14,14 @@ module AbstractController module Rendering extend ActiveSupport::Concern - def self.default_protected_instance_vars - super.concat [:@_action_name, :@_response_body, :@_formats, :@_prefixes, :@_config] + # Normalize arguments, options and then delegates render_to_body and + # sticks the result in self.response_body. + # :api: public + def render(*args, &block) + options = _normalize_render(*args, &block) + self.response_body = render_to_body(options) + _process_format(rendered_format) + self.response_body end # Raw rendering of a template to a string. @@ -29,40 +36,49 @@ module AbstractController # overridden in order to still return a string. # :api: plugin def render_to_string(*args, &block) + options = _normalize_render(*args, &block) + render_to_body(options) end - # Raw rendering of a template. - # :api: plugin - def render_to_body(options = {}) - end - - # Normalize arguments, options and then delegates render_to_body and - # sticks the result in self.response_body. + # Performs the actual template rendering. # :api: public - def render(*args, &block) + def render_to_body(options = {}) end - # Return Content-Type of rendered content + # Returns Content-Type of rendered content # :api: public def rendered_format + Mime::TEXT end + DEFAULT_PROTECTED_INSTANCE_VARIABLES = Set.new %w( + @_action_name @_response_body @_formats @_prefixes @_config + @_view_context_class @_view_renderer @_lookup_context + @_routes @_db_runtime + ).map(&:to_sym) + # This method should return a hash with assigns. # You can overwrite this configuration per controller. # :api: public def view_assigns - hash = {} - (instance_variables - self.class.default_protected_instance_vars).each do |name| - hash[name[1..-1]] = instance_variable_get(name) - end - hash + protected_vars = _protected_ivars + variables = instance_variables + + variables.reject! { |s| protected_vars.include? s } + variables.each_with_object({}) { |name, hash| + hash[name.slice(1, name.length)] = instance_variable_get(name) + } end # Normalize args by converting render "foo" to render :action => "foo" and # render "foo/bar" to render :file => "foo/bar". # :api: plugin def _normalize_args(action=nil, options={}) - options + if action.is_a? Hash + action + else + options + end end # Normalize options. @@ -76,5 +92,22 @@ module AbstractController def _process_options(options) options end + + # Process the rendered format. + # :api: private + def _process_format(format) + end + + # Normalize args and options. + # :api: private + def _normalize_render(*args, &block) + options = _normalize_args(*args, &block) + _normalize_options(options) + options + end + + def _protected_ivars # :nodoc: + DEFAULT_PROTECTED_INSTANCE_VARIABLES + end end end diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index d6b1908ccb..417d2efec2 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -13,7 +13,6 @@ module ActionController autoload :Middleware autoload_under "metal" do - autoload :BasicRendering, 'action_controller/metal/rendering' autoload :Compatibility autoload :ConditionalGet autoload :Cookies diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index a00eafc9ed..c84776ab7a 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -3,7 +3,7 @@ require "action_controller/metal/params_wrapper" module ActionController # The <tt>metal</tt> anonymous class was introduced to solve issue with including modules in <tt>ActionController::Base</tt>. - # Modules needes to be included in particluar order. First wee need to have <tt>AbstractController::Rendering</tt> included, + # Modules needs to be included in particluar order. First we need to have <tt>AbstractController::Rendering</tt> included, # next we should include actuall implementation which would be for example <tt>ActionView::Rendering</tt> and after that # <tt>ActionController::Rendering</tt>. This order must be preserved and as we want to have middle module included dynamicaly # <tt>metal</tt> class was introduced. It has <tt>AbstractController::Rendering</tt> included and is parent class of @@ -14,7 +14,6 @@ module ActionController # metal = Class.new(Metal) do include AbstractController::Rendering - include ActionController::BasicRendering end # Action Controllers are the core of a web request in \Rails. They are made up of one or more actions that are executed @@ -74,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 @@ -261,11 +260,17 @@ module ActionController include mod end - def self.default_protected_instance_vars - super.concat [ - :@_status, :@_headers, :@_params, :@_env, :@_response, :@_request, - :@_view_runtime, :@_stream, :@_url_options, :@_action_has_layout - ] + # Define some internal variables that should not be propagated to the view. + PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [ + :@_status, :@_headers, :@_params, :@_env, :@_response, :@_request, + :@_view_runtime, :@_stream, :@_url_options, :@_action_has_layout ] + + def _protected_ivars # :nodoc: + PROTECTED_IVARS + end + + def self.protected_instance_variables + PROTECTED_IVARS end ActiveSupport.run_load_hooks(:action_controller, self) diff --git a/actionpack/lib/action_controller/metal/flash.rb b/actionpack/lib/action_controller/metal/flash.rb index 1d77e331f8..65351284b9 100644 --- a/actionpack/lib/action_controller/metal/flash.rb +++ b/actionpack/lib/action_controller/metal/flash.rb @@ -37,7 +37,7 @@ module ActionController #:nodoc: end helper_method type - _flash_types << type + self._flash_types += [type] end end end diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb index b8afce42c9..a2cb6d1e66 100644 --- a/actionpack/lib/action_controller/metal/force_ssl.rb +++ b/actionpack/lib/action_controller/metal/force_ssl.rb @@ -48,7 +48,7 @@ module ActionController # You can pass any of the following options to affect the redirect status and response # * <tt>status</tt> - Redirect with a custom status (default is 301 Moved Permanently) # * <tt>flash</tt> - Set a flash message when redirecting - # * <tt>alert</tt> - Set a alert message when redirecting + # * <tt>alert</tt> - Set an alert message when redirecting # * <tt>notice</tt> - Set a notice message when redirecting # # ==== Action Options diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index 8237db15ca..43407f5b78 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -1,8 +1,6 @@ module ActionController module Head - extend ActiveSupport::Concern - - # Return a response that has no content (merely headers). The options + # Returns a response that has no content (merely headers). The options # argument is interpreted to be a hash of header names and values. # This allows you to easily return a response that consists only of # significant headers: 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 834d44f045..84ade41036 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 @@ -364,9 +365,7 @@ module ActionController #:nodoc: format = collector.negotiate_format(request) if format - self.content_type ||= format.to_s - lookup_context.formats = [format.to_sym] - lookup_context.rendered_format = lookup_context.formats.first + _process_format(format) collector else raise ActionController::UnknownFormat @@ -397,10 +396,10 @@ module ActionController #:nodoc: # request, with this response then being accessible by calling #response. class Collector include AbstractController::Collector - attr_accessor :order, :format + attr_accessor :format def initialize(mimes) - @order, @responses = [], {} + @responses = {} mimes.each { |mime| send(mime) } end @@ -415,7 +414,6 @@ module ActionController #:nodoc: def custom(mime_type, &block) mime_type = Mime::Type.lookup(mime_type.to_s) unless mime_type.is_a?(Mime::Type) - @order << mime_type @responses[mime_type] ||= block end @@ -424,7 +422,7 @@ module ActionController #:nodoc: end def negotiate_format(request) - @format = request.negotiate_mime(order) + @format = request.negotiate_mime(@responses.keys) end end end diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index e9031f3fac..ab14a61b97 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -71,6 +71,26 @@ module ActionController self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.h(location)}\">redirected</a>.</body></html>" end + def _compute_redirect_to_location(options) #:nodoc: + case options + # The scheme name consist of a letter followed by any combination of + # letters, digits, and the plus ("+"), period ("."), or hyphen ("-") + # characters; and is terminated by a colon (":"). + # See http://tools.ietf.org/html/rfc3986#section-3.1 + # The protocol relative scheme starts with a double slash "//". + when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/i + options + when String + request.protocol + request.host_with_port + options + when :back + request.headers["Referer"] or raise RedirectBackError + when Proc + _compute_redirect_to_location options.call + else + url_for(options) + end.delete("\0\r\n") + end + private def _extract_redirect_to_status(options, response_status) if options.is_a?(Hash) && options.key?(:status) @@ -81,24 +101,5 @@ module ActionController 302 end end - - def _compute_redirect_to_location(options) - case options - # The scheme name consist of a letter followed by any combination of - # letters, digits, and the plus ("+"), period ("."), or hyphen ("-") - # characters; and is terminated by a colon (":"). - # The protocol relative scheme starts with a double slash "//" - when %r{\A(\w[\w+.-]*:|//).*} - options - when String - request.protocol + request.host_with_port + options - when :back - request.headers["Referer"] or raise RedirectBackError - when Proc - _compute_redirect_to_location options.call - else - url_for(options) - end.delete("\0\r\n") - end end end diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index abed6e53cc..62a3844b04 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -8,8 +8,7 @@ module ActionController class MissingRenderer < LoadError def initialize(format) - @format = format - super("No renderer defined for format: #{@format}") + super "No renderer defined for format: #{format}" end end diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index 21224b9c3b..5c48b4ab98 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -1,36 +1,4 @@ module ActionController - # Basic rendering implements the most minimal rendering layer. - # It only supports rendering :text and :nothing. Passing any other option will - # result in `UnsupportedOperationError` exception. For more functionality like - # different formats, layouts etc. you should use `ActionView` gem. - module BasicRendering - extend ActiveSupport::Concern - - # Render text or nothing (empty string) to response_body - # :api: public - def render(*args, &block) - super(*args, &block) - opts = args.first - if opts.has_key?(:text) && opts[:text].present? - self.response_body = opts[:text] - elsif opts.has_key?(:nothing) && opts[:nothing] - self.response_body = " " - else - raise UnsupportedOperationError - end - end - - def rendered_format - Mime::TEXT - end - - class UnsupportedOperationError < StandardError - def initialize - super "Unsupported render operation. BasicRendering supports only :text and :nothing options. For more, you need to include ActionView." - end - end - end - module Rendering extend ActiveSupport::Concern @@ -44,27 +12,31 @@ module ActionController def render(*args) #:nodoc: raise ::AbstractController::DoubleRenderError if self.response_body super - self.content_type ||= rendered_format.to_s - self.response_body end # Overwrite render_to_string because body can now be set to a rack body. def render_to_string(*) - if self.response_body = super + result = super + if result.respond_to?(:each) string = "" - self.response_body.each { |r| string << r } + result.each { |r| string << r } string + else + result end - ensure - self.response_body = nil end - def render_to_body(*) - super || " " + def render_to_body(options = {}) + super || options[:text].presence || ' ' end private + def _process_format(format) + super + self.content_type ||= format.to_s + end + # Normalize arguments by catching blocks and setting them on :update. def _normalize_args(action=nil, options={}, &blk) #:nodoc: options = super diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 573c739da4..bd64b1f812 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -124,6 +124,9 @@ module ActionController #:nodoc: @loaded = true end + # no-op + def destroy; end + def exists? true 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 ae600b1ebe..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? @@ -421,7 +432,7 @@ module ActionController # Slicing filters out non-declared keys. slice(*filter.keys).each do |key, value| - return unless value + next unless value if filter[key] == EMPTY_ARRAY # Declaration { comment_ids: [] }. 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/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index d3696cbb8a..5247e61a23 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -41,7 +41,7 @@ module ActionDispatch # :nodoc: # Get and set headers for this response. attr_accessor :header - + alias_method :headers=, :header= alias_method :headers, :header @@ -181,9 +181,9 @@ module ActionDispatch # :nodoc: end alias_method :status_message, :message - def respond_to?(method) + def respond_to?(method, include_private = false) if method.to_s == 'to_path' - stream.respond_to?(:to_path) + stream.respond_to?(method) else super 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..5a79059ed6 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 @@ -43,9 +43,7 @@ module ActionDispatch move_string(t, a).concat(move_regexp(t, a)) end - def to_json - require 'json' - + def as_json(options = nil) simple_regexp = Hash.new { |h,k| h[k] = {} } @regexp_states.each do |from, hash| @@ -54,11 +52,11 @@ module ActionDispatch end end - JSON.dump({ + { regexp_states: simple_regexp, string_states: @string_states, accepting: @accepting - }) + } end def to_svg @@ -111,14 +109,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 +129,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 462f1a122d..d1a004af50 100644 --- a/actionpack/lib/action_dispatch/journey/router/utils.rb +++ b/actionpack/lib/action_dispatch/journey/router/utils.rb @@ -7,15 +7,18 @@ module ActionDispatch # Normalizes URI path. # # Strips off trailing slash and ensures there is a leading slash. + # Also converts downcase url encoded string to uppercase. # # normalize_path("/foo") # => "/foo" # normalize_path("/foo/") # => "/foo" # normalize_path("foo") # => "/foo" # normalize_path("") # => "/" + # normalize_path("/%ab") # => "/%AB" def self.normalize_path(path) path = "/#{path}" path.squeeze!('/') path.sub!(%r{/+\Z}, '') + path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase } path = '/' if path == '' path end @@ -35,7 +38,7 @@ module ActionDispatch UNSAFE_FRAGMENT = Regexp.new("[^#{safe_fragment}]", false).freeze end - Parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI + Parser = URI::Parser.new def self.escape_path(path) Parser.escape(path.to_s, UriEscape::UNSAFE_SEGMENT) diff --git a/actionpack/lib/action_dispatch/journey/visitors.rb b/actionpack/lib/action_dispatch/journey/visitors.rb index 0a8cb1b4d4..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}" } @@ -84,44 +87,43 @@ module ActionDispatch # Used for formatting urls (url_for) class Formatter < Visitor # :nodoc: - attr_reader :options, :consumed + attr_reader :options def initialize(options) @options = options - @consumed = {} end private - def visit_GROUP(node) - if consumed == options - nil - else - route = visit(node.left) - route.include?("\0") ? nil : route + def visit(node, optional = false) + case node.type + when :LITERAL, :SLASH, :DOT + node.left + when :STAR + visit(node.left) + when :GROUP + visit(node.left, true) + when :CAT + visit_CAT(node, optional) + when :SYMBOL + visit_SYMBOL(node) end end - def terminal(node) - node.left - end - - def binary(node) - [visit(node.left), visit(node.right)].join - end + def visit_CAT(node, optional) + left = visit(node.left, optional) + right = visit(node.right, optional) - def nary(node) - node.children.map { |c| visit(c) }.join + if optional && !(right && left) + "" + else + [left, right].join + end end def visit_SYMBOL(node) - key = node.to_sym - - if value = options[key] - consumed[key] = value + if value = options[node.to_sym] Router::Utils.escape_path(value) - else - "\0" end end end diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb index 852f1cf6f5..baf9d5779e 100644 --- a/actionpack/lib/action_dispatch/middleware/callbacks.rb +++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb @@ -8,14 +8,14 @@ module ActionDispatch class << self delegate :to_prepare, :to_cleanup, :to => "ActionDispatch::Reloader" - end - def self.before(*args, &block) - set_callback(:call, :before, *args, &block) - end + def before(*args, &block) + set_callback(:call, :before, *args, &block) + end - def self.after(*args, &block) - set_callback(:call, :after, *args, &block) + def after(*args, &block) + set_callback(:call, :after, *args, &block) + end end def initialize(app) 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/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index cffb814e1e..120bc54333 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -179,7 +179,8 @@ module ActionDispatch private def draw_section(routes) - name_width, verb_width, path_width = widths(routes) + header_lengths = ['Prefix', 'Verb', 'URI Pattern'].map(&:length) + name_width, verb_width, path_width = widths(routes).zip(header_lengths).map(&:max) routes.map do |r| "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}" diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 288ce3e867..846a6345cb 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -147,14 +147,16 @@ module ActionDispatch @defaults.merge!(options[:defaults]) if options[:defaults] options.each do |key, default| - next if Regexp === default || IGNORE_OPTIONS.include?(key) - @defaults[key] = default + unless Regexp === default || IGNORE_OPTIONS.include?(key) + @defaults[key] = default + end end if options[:constraints].is_a?(Hash) options[:constraints].each do |key, default| - next unless URL_OPTIONS.include?(key) && (String === default || Fixnum === default) - @defaults[key] ||= default + if URL_OPTIONS.include?(key) && (String === default || Fixnum === default) + @defaults[key] ||= default + end end end @@ -166,19 +168,21 @@ module ActionDispatch end def normalize_conditions! - @conditions.merge!(:path_info => path) + @conditions[:path_info] = path constraints.each do |key, condition| - next if segment_keys.include?(key) || key == :controller - @conditions[key] = condition + unless segment_keys.include?(key) || key == :controller + @conditions[key] = condition + end end - @conditions[:required_defaults] = [] + required_defaults = [] options.each do |key, required_default| - next if segment_keys.include?(key) || IGNORE_OPTIONS.include?(key) - next if Regexp === required_default - @conditions[:required_defaults] << key + unless segment_keys.include?(key) || IGNORE_OPTIONS.include?(key) || Regexp === required_default + required_defaults << key + end end + @conditions[:required_defaults] = required_defaults via_all = options.delete(:via) if options[:via] == :all @@ -192,8 +196,7 @@ module ActionDispatch end if via = options[:via] - list = Array(via).map { |m| m.to_s.dasherize.upcase } - @conditions.merge!(:request_method => list) + @conditions[:request_method] = Array(via).map { |m| m.to_s.dasherize.upcase } end end @@ -226,11 +229,13 @@ module ActionDispatch action = action.to_s unless action.is_a?(Regexp) if controller.blank? && segment_keys.exclude?(:controller) - raise ArgumentError, "missing :controller" + message = "Missing :controller key on routes definition, please check your routes." + raise ArgumentError, message end if action.blank? && segment_keys.exclude?(:action) - raise ArgumentError, "missing :action" + message = "Missing :action key on routes definition, please check your routes." + raise ArgumentError, message end if controller.is_a?(String) && controller !~ /\A[a-z_0-9\/]*\z/ @@ -362,8 +367,9 @@ module ActionDispatch # # Yes, controller actions are just rack endpoints # match 'photos/:id', to: PhotosController.action(:show) # - # Because request various HTTP verbs with a single action has security - # implications, is recommendable use HttpHelpers[rdoc-ref:HttpHelpers] + # Because requesting various HTTP verbs with a single action has security + # implications, you must either specify the actions in + # the via options or use one of the HtttpHelpers[rdoc-ref:HttpHelpers] # instead +match+ # # === Options @@ -383,7 +389,7 @@ module ActionDispatch # The namespace for :controller. # # match 'path', to: 'c#a', module: 'sekret', controller: 'posts' - # #=> Sekret::PostsController + # # => Sekret::PostsController # # See <tt>Scoping#namespace</tt> for its scope equivalent. # @@ -432,10 +438,10 @@ module ActionDispatch # # match 'json_only', constraints: { format: 'json' } # - # class Blacklist + # class Whitelist # def matches?(request) request.remote_ip == '1.2.3.4' end # end - # match 'path', to: 'c#a', constraints: Blacklist.new + # match 'path', to: 'c#a', constraints: Whitelist.new # # See <tt>Scoping#constraints</tt> for more examples with its scope # equivalent. @@ -1066,18 +1072,18 @@ module ActionDispatch # a singular resource to map /profile (rather than /profile/:id) to # the show action: # - # resource :geocoder + # resource :profile # # creates six different routes in your application, all mapping to - # the +GeoCoders+ controller (note that the controller is named after + # the +Profiles+ controller (note that the controller is named after # the plural): # - # GET /geocoder/new - # POST /geocoder - # GET /geocoder - # GET /geocoder/edit - # PATCH/PUT /geocoder - # DELETE /geocoder + # GET /profile/new + # POST /profile + # GET /profile + # GET /profile/edit + # PATCH/PUT /profile + # DELETE /profile # # === Options # Takes same options as +resources+. diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index 6d3f8da932..2fb03f2712 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -74,6 +74,19 @@ module ActionDispatch # * <tt>:routing_type</tt> - Allowed values are <tt>:path</tt> or <tt>:url</tt>. # Default is <tt>:url</tt>. # + # Also includes all the options from <tt>url_for</tt>. These include such + # things as <tt>:anchor</tt> or <tt>:trailing_slash</tt>. Example usage + # is given below: + # + # polymorphic_url([blog, post], anchor: 'my_anchor') + # # => "http://example.com/blogs/1/posts/1#my_anchor" + # polymorphic_url([blog, post], anchor: 'my_anchor', script_name: "/my_app") + # # => "http://example.com/my_app/blogs/1/posts/1#my_anchor" + # + # For all of these options, see the documentation for <tt>url_for</tt>. + # + # ==== Functionality + # # # an Article record # polymorphic_url(record) # same as article_url(record) # diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb index d751e04e6a..3e54c7e71c 100644 --- a/actionpack/lib/action_dispatch/routing/redirection.rb +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -17,7 +17,7 @@ module ActionDispatch def call(env) req = Request.new(env) - # If any of the path parameters has a invalid encoding then + # If any of the path parameters has an invalid encoding then # raise since it's likely to trigger errors further on. req.symbolized_path_parameters.each do |key, value| unless value.valid_encoding? @@ -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/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 0e5dc1fc6c..b8abdabca5 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -28,7 +28,7 @@ module ActionDispatch def call(env) params = env[PARAMETERS_KEY] - # If any of the path parameters has a invalid encoding then + # If any of the path parameters has an invalid encoding then # raise since it's likely to trigger errors further on. params.each do |key, value| next unless value.respond_to?(:valid_encoding?) @@ -514,11 +514,12 @@ module ActionDispatch @recall = recall.dup @set = set + normalize_recall! normalize_options! normalize_controller_action_id! use_relative_controller! normalize_controller! - handle_nil_action! + normalize_action! end def controller @@ -537,6 +538,11 @@ module ActionDispatch end end + # Set 'index' as default action for recall + def normalize_recall! + @recall[:action] ||= 'index' + end + def normalize_options! # If an explicit :controller was given, always make :action explicit # too, so that action expiry works as expected for things like @@ -552,8 +558,8 @@ module ActionDispatch options[:controller] = options[:controller].to_s end - if options[:action] - options[:action] = options[:action].to_s + if options.key?(:action) + options[:action] = (options[:action] || 'index').to_s end end @@ -563,8 +569,6 @@ module ActionDispatch # :controller, :action or :id is not found, don't pull any # more keys from the recall. def normalize_controller_action_id! - @recall[:action] ||= 'index' if current_controller - use_recall_for(:controller) or return use_recall_for(:action) or return use_recall_for(:id) @@ -586,13 +590,11 @@ module ActionDispatch @options[:controller] = controller.sub(%r{^/}, '') if controller end - # This handles the case of action: nil being explicitly passed. - # It is identical to action: "index" - def handle_nil_action! - if options.has_key?(:action) && options[:action].nil? - options[:action] = 'index' + # Move 'index' action from options to recall + def normalize_action! + if @options[:action] == 'index' + @recall[:action] = @options.delete(:action) end - recall[:action] = options.delete(:action) if options[:action] == 'index' end # Generates a path from routes, returns [path, params]. diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index 8e19025722..4a0ef40873 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, @@ -155,6 +155,8 @@ module ActionDispatch _routes.url_for(options.symbolize_keys.reverse_merge!(url_options)) when String options + when Array + polymorphic_url(options, options.extract_options!) else polymorphic_url(options) end diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index 44ed0ac1f3..93f9fab9c2 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -67,21 +67,11 @@ module ActionDispatch end def normalize_argument_to_redirection(fragment) - normalized = case fragment - when Regexp - fragment - when %r{^\w[A-Za-z\d+.-]*:.*} - fragment - when String - @request.protocol + @request.host_with_port + fragment - when :back - raise RedirectBackError unless refer = @request.headers["Referer"] - refer - else - @controller.url_for(fragment) - end - - normalized.respond_to?(:delete) ? normalized.delete("\0\r\n") : normalized + if Regexp === fragment + fragment + else + @controller._compute_redirect_to_location(fragment) + end end end end diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 9beb30307b..0c2782e981 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -242,7 +242,7 @@ module ActionDispatch @https = flag end - # Return +true+ if the session is mimicking a secure HTTPS request. + # Returns +true+ if the session is mimicking a secure HTTPS request. # # if session.https? # ... diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb index 22a410db94..ba4cffcd3e 100644 --- a/actionpack/test/controller/action_pack_assertions_test.rb +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -39,6 +39,8 @@ class ActionPackAssertionsController < ActionController::Base def redirect_external() redirect_to "http://www.rubyonrails.org"; end + def redirect_external_protocol_relative() redirect_to "//www.rubyonrails.org"; end + def response404() head '404 AWOL' end def response500() head '500 Sorry' end @@ -258,6 +260,19 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase end end + def test_assert_redirect_failure_message_with_protocol_relative_url + begin + process :redirect_external_protocol_relative + assert_redirected_to "/foo" + rescue ActiveSupport::TestCase::Assertion => ex + assert_no_match( + /#{request.protocol}#{request.host}\/\/www.rubyonrails.org/, + ex.message, + 'protocol relative url was incorrectly normalized' + ) + end + end + def test_template_objects_exist process :assign_this assert !@controller.instance_variable_defined?(:"@hi") @@ -309,6 +324,9 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase process :redirect_external assert_equal 'http://www.rubyonrails.org', @response.redirect_url + + process :redirect_external_protocol_relative + assert_equal '//www.rubyonrails.org', @response.redirect_url end def test_no_redirect_url diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index a67dff5436..57b45b8f7b 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -20,7 +20,7 @@ class FragmentCachingMetalTest < ActionController::TestCase @controller = FragmentCachingMetalTestController.new @controller.perform_caching = true @controller.cache_store = @store - @params = { controller: 'posts', action: 'index'} + @params = { controller: 'posts', action: 'index' } @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new @controller.params = @params @@ -40,7 +40,7 @@ class CachingController < ActionController::Base end class FragmentCachingTestController < CachingController - def some_action; end; + def some_action; end end class FragmentCachingTest < ActionController::TestCase diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb index e58a1aadb8..3b5d7ef446 100644 --- a/actionpack/test/controller/filters_test.rb +++ b/actionpack/test/controller/filters_test.rb @@ -17,7 +17,7 @@ class ActionController::Base def assigns(key = nil) assigns = {} instance_variables.each do |ivar| - next if ActionController::Base.default_protected_instance_vars.include?(ivar) + next if ActionController::Base.protected_instance_variables.include?(ivar) assigns[ivar[1..-1]] = instance_variable_get(ivar) end diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb index 3b874a739a..c64ffef654 100644 --- a/actionpack/test/controller/flash_test.rb +++ b/actionpack/test/controller/flash_test.rb @@ -214,6 +214,18 @@ class FlashTest < ActionController::TestCase get :redirect_with_foo_flash assert_equal "for great justice", @controller.send(:flash)[:foo] end + + class SubclassesTestController < TestController; end + + def test_add_flash_type_to_subclasses + TestController.add_flash_types :foo + assert SubclassesTestController._flash_types.include?(:foo) + end + + def test_do_not_add_flash_type_to_parent_class + SubclassesTestController.add_flash_types :bar + assert_not TestController._flash_types.include?(:bar) + end end class FlashIntegrationTest < ActionDispatch::IntegrationTest 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/http_digest_authentication_test.rb b/actionpack/test/controller/http_digest_authentication_test.rb index 9f1c168209..52a0bc9aa3 100644 --- a/actionpack/test/controller/http_digest_authentication_test.rb +++ b/actionpack/test/controller/http_digest_authentication_test.rb @@ -21,7 +21,7 @@ class HttpDigestAuthenticationTest < ActionController::TestCase def authenticate authenticate_or_request_with_http_digest("SuperSecret") do |username| - # Return the password + # Returns the password USERS[username] end end 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/new_base/render_streaming_test.rb b/actionpack/test/controller/new_base/render_streaming_test.rb index f4bdd3e1d4..2b36a399bb 100644 --- a/actionpack/test/controller/new_base/render_streaming_test.rb +++ b/actionpack/test/controller/new_base/render_streaming_test.rb @@ -92,7 +92,7 @@ module RenderStreaming io.rewind assert_match "(undefined method `invalid!' for nil:NilClass)", io.read ensure - ActionController::Base.logger = _old + ActionView::Base.logger = _old end end diff --git a/actionpack/test/controller/new_base/render_text_test.rb b/actionpack/test/controller/new_base/render_text_test.rb index d6c3926a4d..2a253799f3 100644 --- a/actionpack/test/controller/new_base/render_text_test.rb +++ b/actionpack/test/controller/new_base/render_text_test.rb @@ -1,6 +1,15 @@ require 'abstract_unit' module RenderText + class MinimalController < ActionController::Metal + include AbstractController::Rendering + include ActionController::Rendering + + def index + render text: "Hello World!" + end + end + class SimpleController < ActionController::Base self.view_paths = [ActionView::FixtureResolver.new] @@ -63,6 +72,12 @@ module RenderText end class RenderTextTest < Rack::TestCase + test "rendering text from a minimal controller" do + get "/render_text/minimal/index" + assert_body "Hello World!" + assert_status 200 + end + test "rendering text from an action with default options renders the text with the layout" do with_routing do |set| set.draw { get ':controller', :action => 'index' } 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 437da43d9b..b60c5f058d 100644 --- a/actionpack/test/controller/parameters/parameters_permit_test.rb +++ b/actionpack/test/controller/parameters/parameters_permit_test.rb @@ -107,6 +107,15 @@ class ParametersPermitTest < ActiveSupport::TestCase assert_equal [], permitted[:id] end + test 'do not break params filtering on nil values' do + params = ActionController::Parameters.new(a: 1, b: [1, 2, 3], c: nil) + + permitted = params.permit(:a, c: [], b: []) + assert_equal 1, permitted[:a] + assert_equal [1, 2, 3], permitted[:b] + assert_equal nil, permitted[:c] + end + test 'key to empty array: arrays of permitted scalars pass' do [['foo'], [1], ['foo', 'bar'], [1, 2, 3]].each do |array| params = ActionController::Parameters.new(id: array) @@ -138,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/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index c272e785c2..727db79241 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -78,6 +78,11 @@ class RequestForgeryProtectionControllerUsingNullSession < ActionController::Bas cookies.encrypted[:foo] = 'bar' render :nothing => true end + + def try_to_reset_session + reset_session + render :nothing => true + end end class FreeCookieController < RequestForgeryProtectionControllerUsingResetSession @@ -320,6 +325,11 @@ class RequestForgeryProtectionControllerUsingNullSessionTest < ActionController: post :encrypted assert_response :ok end + + test 'should allow reset_session' do + post :try_to_reset_session + assert_response :ok + end end class RequestForgeryProtectionControllerUsingExceptionTest < ActionController::TestCase diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index f735564305..2c84e95c6e 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -2,6 +2,7 @@ require 'abstract_unit' require 'controller/fake_controllers' require 'active_support/core_ext/object/with_options' +require 'active_support/core_ext/object/json' class MilestonesController < ActionController::Base def index() head :ok end @@ -86,36 +87,36 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_symbols_with_dashes rs.draw do get '/:artist/:song-omg', :to => lambda { |env| - resp = JSON.dump env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] + resp = ActiveSupport::JSON.encode env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] [200, {}, [resp]] } end - hash = JSON.load get(URI('http://example.org/journey/faithfully-omg')) + hash = ActiveSupport::JSON.decode get(URI('http://example.org/journey/faithfully-omg')) assert_equal({"artist"=>"journey", "song"=>"faithfully"}, hash) end def test_id_with_dash rs.draw do get '/journey/:id', :to => lambda { |env| - resp = JSON.dump env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] + resp = ActiveSupport::JSON.encode env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] [200, {}, [resp]] } end - hash = JSON.load get(URI('http://example.org/journey/faithfully-omg')) + hash = ActiveSupport::JSON.decode get(URI('http://example.org/journey/faithfully-omg')) assert_equal({"id"=>"faithfully-omg"}, hash) end def test_dash_with_custom_regexp rs.draw do get '/:artist/:song-omg', :constraints => { :song => /\d+/ }, :to => lambda { |env| - resp = JSON.dump env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] + resp = ActiveSupport::JSON.encode env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] [200, {}, [resp]] } end - hash = JSON.load get(URI('http://example.org/journey/123-omg')) + hash = ActiveSupport::JSON.decode get(URI('http://example.org/journey/123-omg')) assert_equal({"artist"=>"journey", "song"=>"123"}, hash) assert_equal 'Not Found', get(URI('http://example.org/journey/faithfully-omg')) end @@ -123,24 +124,24 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_pre_dash rs.draw do get '/:artist/omg-:song', :to => lambda { |env| - resp = JSON.dump env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] + resp = ActiveSupport::JSON.encode env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] [200, {}, [resp]] } end - hash = JSON.load get(URI('http://example.org/journey/omg-faithfully')) + hash = ActiveSupport::JSON.decode get(URI('http://example.org/journey/omg-faithfully')) assert_equal({"artist"=>"journey", "song"=>"faithfully"}, hash) end def test_pre_dash_with_custom_regexp rs.draw do get '/:artist/omg-:song', :constraints => { :song => /\d+/ }, :to => lambda { |env| - resp = JSON.dump env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] + resp = ActiveSupport::JSON.encode env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] [200, {}, [resp]] } end - hash = JSON.load get(URI('http://example.org/journey/omg-123')) + hash = ActiveSupport::JSON.decode get(URI('http://example.org/journey/omg-123')) assert_equal({"artist"=>"journey", "song"=>"123"}, hash) assert_equal 'Not Found', get(URI('http://example.org/journey/omg-faithfully')) end @@ -160,14 +161,14 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_star_paths_are_greedy_but_not_too_much rs.draw do get "/*path", :to => lambda { |env| - x = JSON.dump env["action_dispatch.request.path_parameters"] + x = ActiveSupport::JSON.encode env["action_dispatch.request.path_parameters"] [200, {}, [x]] } end expected = { "path" => "foo/bar", "format" => "html" } u = URI('http://example.org/foo/bar.html') - assert_equal expected, JSON.parse(get(u)) + assert_equal expected, ActiveSupport::JSON.decode(get(u)) end def test_optional_star_paths_are_greedy @@ -185,7 +186,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_optional_star_paths_are_greedy_but_not_too_much rs.draw do get "/(*filters)", :to => lambda { |env| - x = JSON.dump env["action_dispatch.request.path_parameters"] + x = ActiveSupport::JSON.encode env["action_dispatch.request.path_parameters"] [200, {}, [x]] } end @@ -193,7 +194,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase expected = { "filters" => "ne_27.065938,-80.6092/sw_25.489856,-82", "format" => "542794" } u = URI('http://example.org/ne_27.065938,-80.6092/sw_25.489856,-82.542794') - assert_equal expected, JSON.parse(get(u)) + assert_equal expected, ActiveSupport::JSON.decode(get(u)) end def test_regexp_precidence @@ -1904,6 +1905,10 @@ class RackMountIntegrationTests < ActiveSupport::TestCase assert_equal({:controller => 'news', :action => 'index'}, @routes.recognize_path(URI.parser.escape('こんにちは/世界'), :method => :get)) end + def test_downcased_unicode_path + assert_equal({:controller => 'news', :action => 'index'}, @routes.recognize_path(URI.parser.escape('こんにちは/世界').downcase, :method => :get)) + end + private def sort_extras!(extras) if extras.length == 2 diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index f75c604277..de0476dbde 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -1,5 +1,6 @@ require 'abstract_unit' require 'controller/fake_controllers' +require 'active_support/json/decoding' class TestCaseTest < ActionController::TestCase class TestController < ActionController::Base @@ -622,7 +623,7 @@ XML @request.headers['Referer'] = "http://nohost.com/home" @request.headers['Content-Type'] = "application/rss+xml" get :test_headers - parsed_env = JSON.parse(@response.body) + parsed_env = ActiveSupport::JSON.decode(@response.body) assert_equal "http://nohost.com/home", parsed_env["HTTP_REFERER"] assert_equal "application/rss+xml", parsed_env["CONTENT_TYPE"] end @@ -631,7 +632,7 @@ XML @request.headers['HTTP_REFERER'] = "http://example.com/about" @request.headers['CONTENT_TYPE'] = "application/json" get :test_headers - parsed_env = JSON.parse(@response.body) + parsed_env = ActiveSupport::JSON.decode(@response.body) assert_equal "http://example.com/about", parsed_env["HTTP_REFERER"] assert_equal "application/json", parsed_env["CONTENT_TYPE"] end diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb index 088ad73f2f..d2b4952759 100644 --- a/actionpack/test/controller/url_for_test.rb +++ b/actionpack/test/controller/url_for_test.rb @@ -370,6 +370,24 @@ module AbstractController assert_equal("/c/a?show=false", W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :show => false)) end + def test_url_generation_with_array_and_hash + with_routing do |set| + set.draw do + namespace :admin do + resources :posts + end + end + + kls = Class.new { include set.url_helpers } + kls.default_url_options[:host] = 'www.basecamphq.com' + + controller = kls.new + assert_equal("http://www.basecamphq.com/admin/posts/new?param=value", + controller.send(:url_for, [:new, :admin, :post, { param: 'value' }]) + ) + end + end + private def extract_params(url) url.split('?', 2).last.split('&').sort diff --git a/actionpack/test/controller/webservice_test.rb b/actionpack/test/controller/webservice_test.rb index b2dfd96606..d80b0e2da0 100644 --- a/actionpack/test/controller/webservice_test.rb +++ b/actionpack/test/controller/webservice_test.rb @@ -1,4 +1,5 @@ require 'abstract_unit' +require 'active_support/json/decoding' class WebServiceTest < ActionDispatch::IntegrationTest class TestController < ActionController::Base @@ -54,7 +55,7 @@ class WebServiceTest < ActionDispatch::IntegrationTest def test_register_and_use_json_simple with_test_route_set do - with_params_parsers Mime::JSON => Proc.new { |data| JSON.parse(data)['request'].with_indifferent_access } do + with_params_parsers Mime::JSON => Proc.new { |data| ActiveSupport::JSON.decode(data)['request'].with_indifferent_access } do post "/", '{"request":{"summary":"content...","title":"JSON"}}', 'CONTENT_TYPE' => 'application/json' 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/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index 2fbe7358f9..4501ea095c 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -212,6 +212,11 @@ class ResponseTest < ActiveSupport::TestCase ActionDispatch::Response.default_headers = nil end end + + test "respond_to? accepts include_private" do + assert_not @response.respond_to?(:method_missing) + assert @response.respond_to?(:method_missing, true) + end end class ResponseIntegrationTest < ActionDispatch::IntegrationTest diff --git a/actionpack/test/dispatch/routing/inspector_test.rb b/actionpack/test/dispatch/routing/inspector_test.rb index 4f97d28d2b..c8038bbd7c 100644 --- a/actionpack/test/dispatch/routing/inspector_test.rb +++ b/actionpack/test/dispatch/routing/inspector_test.rb @@ -46,11 +46,11 @@ module ActionDispatch assert_equal [ " Prefix Verb URI Pattern Controller#Action", - "custom_assets GET /custom/assets(.:format) custom_assets#show", - " blog /blog Blog::Engine", + "custom_assets GET /custom/assets(.:format) custom_assets#show", + " blog /blog Blog::Engine", "", "Routes for Blog::Engine:", - "cart GET /cart(.:format) cart#show" + " cart GET /cart(.:format) cart#show" ], output end @@ -61,7 +61,7 @@ module ActionDispatch assert_equal [ "Prefix Verb URI Pattern Controller#Action", - "cart GET /cart(.:format) cart#show" + " cart GET /cart(.:format) cart#show" ], output end @@ -72,7 +72,7 @@ module ActionDispatch assert_equal [ " Prefix Verb URI Pattern Controller#Action", - "custom_assets GET /custom/assets(.:format) custom_assets#show" + "custom_assets GET /custom/assets(.:format) custom_assets#show" ], output end @@ -101,7 +101,7 @@ module ActionDispatch assert_equal [ "Prefix Verb URI Pattern Controller#Action", - "root GET / pages#main" + " root GET / pages#main" ], output end @@ -112,7 +112,7 @@ module ActionDispatch assert_equal [ "Prefix Verb URI Pattern Controller#Action", - " GET /api/:action(.:format) api#:action" + " GET /api/:action(.:format) api#:action" ], output end @@ -123,7 +123,7 @@ module ActionDispatch assert_equal [ "Prefix Verb URI Pattern Controller#Action", - " GET /:controller/:action(.:format) :controller#:action" + " GET /:controller/:action(.:format) :controller#:action" ], output end @@ -134,7 +134,7 @@ module ActionDispatch assert_equal [ "Prefix Verb URI Pattern Controller#Action", - " GET /:controller(/:action(/:id))(.:format) :controller#:action {:id=>/\\d+/}" + " GET /:controller(/:action(/:id))(.:format) :controller#:action {:id=>/\\d+/}" ], output end @@ -145,7 +145,7 @@ module ActionDispatch assert_equal [ "Prefix Verb URI Pattern Controller#Action", - %Q[ GET /photos/:id(.:format) photos#show {:format=>"jpg"}] + %Q[ GET /photos/:id(.:format) photos#show {:format=>"jpg"}] ], output end @@ -156,7 +156,7 @@ module ActionDispatch assert_equal [ "Prefix Verb URI Pattern Controller#Action", - " GET /photos/:id(.:format) photos#show {:id=>/[A-Z]\\d{5}/}" + " GET /photos/:id(.:format) photos#show {:id=>/[A-Z]\\d{5}/}" ], output end @@ -172,7 +172,7 @@ module ActionDispatch assert_equal [ "Prefix Verb URI Pattern Controller#Action", - " GET /foo/:id(.:format) #{RackApp.name} {:id=>/[A-Z]\\d{5}/}" + " GET /foo/:id(.:format) #{RackApp.name} {:id=>/[A-Z]\\d{5}/}" ], output end @@ -191,7 +191,7 @@ module ActionDispatch assert_equal [ "Prefix Verb URI Pattern Controller#Action", - " /foo #{RackApp.name} {:constraint=>( my custom constraint )}" + " /foo #{RackApp.name} {:constraint=>( my custom constraint )}" ], output end @@ -212,9 +212,9 @@ module ActionDispatch assert_equal [ "Prefix Verb URI Pattern Controller#Action", - " foo GET /foo(.:format) redirect(301, /foo/bar) {:subdomain=>\"admin\"}", - " bar GET /bar(.:format) redirect(307, path: /foo/bar)", - "foobar GET /foobar(.:format) redirect(301)" + " foo GET /foo(.:format) redirect(301, /foo/bar) {:subdomain=>\"admin\"}", + " bar GET /bar(.:format) redirect(307, path: /foo/bar)", + "foobar GET /foobar(.:format) redirect(301)" ], output end @@ -241,7 +241,7 @@ module ActionDispatch end assert_equal ["Prefix Verb URI Pattern Controller#Action", - " GET /:controller(/:action) (?-mix:api\\/[^\\/]+)#:action"], output + " GET /:controller(/:action) (?-mix:api\\/[^\\/]+)#:action"], output end end end diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index e4c3ddd3f9..3e9e90a950 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -1102,6 +1102,19 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal 'projects#index', @response.body end + def test_scoped_root_as_name + draw do + scope '(:locale)', :locale => /en|pl/ do + root :to => 'projects#index', :as => 'projects' + end + end + + assert_equal '/en', projects_path(:locale => 'en') + assert_equal '/', projects_path + get '/en' + assert_equal 'projects#index', @response.body + end + def test_scope_with_format_option draw do get "direct/index", as: :no_format_direct, format: false diff --git a/actionpack/test/dispatch/static_test.rb b/actionpack/test/dispatch/static_test.rb index 112f470786..acccbcb2e6 100644 --- a/actionpack/test/dispatch/static_test.rb +++ b/actionpack/test/dispatch/static_test.rb @@ -37,6 +37,10 @@ module StaticTests end def test_served_static_file_with_non_english_filename + if RUBY_ENGINE == 'jruby ' + skip "Stop skipping if following bug gets fixed: " \ + "http://jira.codehaus.org/browse/JRUBY-7192" + end assert_html "means hello in Japanese\n", get("/foo/#{Rack::Utils.escape("こんにちは.html")}") end diff --git a/actionpack/test/fixtures/helpers/abc_helper.rb b/actionpack/test/fixtures/helpers/abc_helper.rb index 7104ff3730..cf2774bb5f 100644 --- a/actionpack/test/fixtures/helpers/abc_helper.rb +++ b/actionpack/test/fixtures/helpers/abc_helper.rb @@ -1,5 +1,3 @@ module AbcHelper def bare_a() end - def bare_b() end - def bare_c() end end diff --git a/actionpack/test/fixtures/helpers/helpery_test_helper.rb b/actionpack/test/fixtures/helpers/helpery_test_helper.rb deleted file mode 100644 index a4f2951efa..0000000000 --- a/actionpack/test/fixtures/helpers/helpery_test_helper.rb +++ /dev/null @@ -1,5 +0,0 @@ -module HelperyTestHelper - def helpery_test - "Default" - end -end 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/gtg/transition_table_test.rb b/actionpack/test/journey/gtg/transition_table_test.rb index 33acba8b65..b968780d8d 100644 --- a/actionpack/test/journey/gtg/transition_table_test.rb +++ b/actionpack/test/journey/gtg/transition_table_test.rb @@ -1,5 +1,5 @@ require 'abstract_unit' -require 'json' +require 'active_support/json/decoding' module ActionDispatch module Journey @@ -13,7 +13,7 @@ module ActionDispatch /articles/:id(.:format) } - json = JSON.load table.to_json + json = ActiveSupport::JSON.decode table.to_json assert json['regexp_states'] assert json['string_states'] assert json['accepting'] 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..b8b51d86c2 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 + def as_json(options = nil) + { :error => self.class.name, :message => self.to_s } end end - -class Car < Struct.new(:color) -end diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 6240dc6ac6..787e6d68be 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,7 +1,60 @@ -* Only cache template digests if `config.cache_template_loading` id true. +* Use `set_backtrace` instead of instance variable `@backtrace` in ActionView exceptions + + *Shimpei Makimoto* + +* Fix `simple_format` escapes own output when passing `sanitize: true` + + *Paul Seidemann* + +* 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. + + *Angel N. Sciortino* + +* Fix some edge cases for AV `select` helper with `:selected` option. + + *Bogdan Gusiev* + +* Ability to pass block to `select` helper + + <%= select(report, "campaign_ids") do %> + <% available_campaigns.each do |c| -%> + <%= content_tag(:option, c.name, value: c.id, data: { tags: c.tags.to_json }) %> + <% end -%> + <% end -%> + + *Bogdan Gusiev* + +* Handle `:namespace` form option in collection labels. + + *Vasiliy Ermolovich* + +* Fix `form_for` when both `namespace` and `as` options are present. + + `as` option no longer overwrites `namespace` option when generating + html id attribute of the form element. + + *Adam Niedzielski* + +* Fix `excerpt` when `:separator` is `nil`. + + *Paul Nikitochkin* + +* Only cache template digests if `config.cache_template_loading` is true. *Josh Lauer*, *Justin Ridgewell* +* Fixed a bug where the lookup details were not being taken into account + when caching the digest of a template - changes to the details now + cause a different cache key to be used. + + *Daniel Schierbeck* + * Added an `extname` hash option for `javascript_include_tag` method. Before: 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/actionview.gemspec b/actionview/actionview.gemspec index 87cb568300..cdac074973 100644 --- a/actionview/actionview.gemspec +++ b/actionview/actionview.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |s| s.name = 'actionview' s.version = version s.summary = 'Rendering framework putting the V in MVC (part of Rails).' - s.description = '' + s.description = 'Simple, battle-tested conventions and helpers for building web pages.' s.required_ruby_version = '>= 1.9.3' diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index 95c3200c81..5570e2a8dc 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -10,7 +10,10 @@ module ActionView class << self def digest(name, format, finder, options = {}) - cache_key = ([name, format] + Array.wrap(options[:dependencies])).join('.') + details_key = finder.details_key.hash + dependencies = Array.wrap(options[:dependencies]) + cache_key = ([name, details_key, format] + dependencies).join('.') + # this is a correctly done double-checked locking idiom # (ThreadSafe::Cache's lookups have volatile semantics) @@cache[cache_key] || @@digest_monitor.synchronize do @@ -32,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..2a44ae5d5c 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -153,14 +153,14 @@ module ActionView # # ==== Examples # - # favicon_link_tag '/myicon.ico' + # favicon_link_tag 'myicon.ico' # # => <link href="/assets/myicon.ico" rel="shortcut icon" type="image/vnd.microsoft.icon" /> # # Mobile Safari looks for a different <link> tag, pointing to an image that # will be used if you add the page to the home screen of an iPod Touch, iPhone, or iPad. # The following call would generate such a tag: # - # favicon_link_tag '/mb-icon.png', rel: 'apple-touch-icon', type: 'image/png' + # favicon_link_tag 'mb-icon.png', rel: 'apple-touch-icon', type: 'image/png' # # => <link href="/assets/mb-icon.png" rel="apple-touch-icon" type="image/png" /> def favicon_link_tag(source='favicon.ico', options={}) tag('link', { @@ -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/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb index 0b957adb91..c830ab23e3 100644 --- a/actionview/lib/action_view/helpers/asset_url_helper.rb +++ b/actionview/lib/action_view/helpers/asset_url_helper.rb @@ -143,9 +143,9 @@ module ActionView "#{source}#{tail}" end - alias_method :path_to_asset, :asset_path # aliased to avoid conflicts with a asset_path named route + alias_method :path_to_asset, :asset_path # aliased to avoid conflicts with an asset_path named route - # Computes the full URL to a asset in the public directory. This + # Computes the full URL to an asset in the public directory. This # will use +asset_path+ internally, so most of their behaviors # will be the same. def asset_url(source, options = {}) diff --git a/actionview/lib/action_view/helpers/atom_feed_helper.rb b/actionview/lib/action_view/helpers/atom_feed_helper.rb index 42b1dd8933..af70a4242a 100644 --- a/actionview/lib/action_view/helpers/atom_feed_helper.rb +++ b/actionview/lib/action_view/helpers/atom_feed_helper.rb @@ -64,7 +64,7 @@ module ActionView # 'xmlns:openSearch' => 'http://a9.com/-/spec/opensearch/1.1/'}) do |feed| # feed.title("My great blog!") # feed.updated((@posts.first.created_at)) - # feed.tag!(openSearch:totalResults, 10) + # feed.tag!('openSearch:totalResults', 10) # # @posts.each do |post| # feed.entry(post) do |entry| diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb index 2a38e5c446..b3af1d4da4 100644 --- a/actionview/lib/action_view/helpers/cache_helper.rb +++ b/actionview/lib/action_view/helpers/cache_helper.rb @@ -4,7 +4,7 @@ module ActionView module CacheHelper # This helper exposes a method for caching fragments of a view # rather than an entire action or page. This technique is useful - # caching pieces like menus, lists of newstopics, static HTML + # caching pieces like menus, lists of new topics, static HTML # fragments, and so on. This method takes a block that contains # the content you wish to cache. # 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 8a4830d887..38d969ed0c 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -442,10 +442,11 @@ module ActionView object = convert_to_model(object) as = options[:as] + namespace = options[:namespace] action, method = object.respond_to?(:persisted?) && object.persisted? ? [:edit, :patch] : [:new, :post] options[:html].reverse_merge!( class: as ? "#{action}_#{as}" : dom_class(object, action), - id: as ? "#{action}_#{as}" : [options[:namespace], dom_id(object, action)].compact.join("_").presence, + id: (as ? [namespace, action, as] : [namespace, dom_id(object, action)]).compact.join("_").presence, method: method ) @@ -1172,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/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb index fcd151ac32..4347983bad 100644 --- a/actionview/lib/action_view/helpers/form_options_helper.rb +++ b/actionview/lib/action_view/helpers/form_options_helper.rb @@ -128,6 +128,15 @@ module ActionView # or <tt>selected: nil</tt> to leave all options unselected. Similarly, you can specify values to be disabled in the option # tags by specifying the <tt>:disabled</tt> option. This can either be a single value or an array of values to be disabled. # + # A block can be passed to +select+ to customize how the options tags will be rendered. This + # is useful when the options tag has complex attributes. + # + # select(report, "campaign_ids") do + # available_campaigns.each do |c| + # content_tag(:option, c.name, value: c.id, data: { tags: c.tags.to_json }) + # end + # end + # # ==== Gotcha # # The HTML specification says when +multiple+ parameter passed to select and all options got deselected @@ -152,8 +161,8 @@ module ActionView # In case if you don't want the helper to generate this hidden field you can specify # <tt>include_hidden: false</tt> option. # - def select(object, method, choices, options = {}, html_options = {}) - Tags::Select.new(object, method, self, choices, options, html_options).render + def select(object, method, choices = nil, options = {}, html_options = {}, &block) + Tags::Select.new(object, method, self, choices, options, html_options, &block).render end # Returns <tt><select></tt> and <tt><option></tt> tags for the collection of existing return values of @@ -766,8 +775,8 @@ module ActionView # <% end %> # # Please refer to the documentation of the base helper for details. - def select(method, choices, options = {}, html_options = {}) - @template.select(@object_name, method, choices, objectify_options(options), @default_options.merge(html_options)) + def select(method, choices = nil, options = {}, html_options = {}, &block) + @template.select(@object_name, method, choices, objectify_options(options), @default_options.merge(html_options), &block) end # Wraps ActionView::Helpers::FormOptionsHelper#collection_select for form builders: diff --git a/actionview/lib/action_view/helpers/number_helper.rb b/actionview/lib/action_view/helpers/number_helper.rb index fda7038a5d..9adc2c1a8f 100644 --- a/actionview/lib/action_view/helpers/number_helper.rb +++ b/actionview/lib/action_view/helpers/number_helper.rb @@ -105,12 +105,7 @@ module ActionView # number_to_currency(1234567890.50, unit: "£", separator: ",", delimiter: "", format: "%n %u") # # => 1234567890,50 £ def number_to_currency(number, options = {}) - return unless number - options = escape_unsafe_delimiters_and_separators(options.symbolize_keys) - - wrap_with_output_safety_handling(number, options.delete(:raise)) { - ActiveSupport::NumberHelper.number_to_currency(number, options) - } + delegate_number_helper_method(:number_to_currency, number, options) end # Formats a +number+ as a percentage string (e.g., 65%). You can @@ -150,12 +145,7 @@ module ActionView # # number_to_percentage("98a", raise: true) # => InvalidNumberError def number_to_percentage(number, options = {}) - return unless number - options = escape_unsafe_delimiters_and_separators(options.symbolize_keys) - - wrap_with_output_safety_handling(number, options.delete(:raise)) { - ActiveSupport::NumberHelper.number_to_percentage(number, options) - } + delegate_number_helper_method(:number_to_percentage, number, options) end # Formats a +number+ with grouped thousands using +delimiter+ @@ -188,11 +178,7 @@ module ActionView # # number_with_delimiter("112a", raise: true) # => raise InvalidNumberError def number_with_delimiter(number, options = {}) - options = escape_unsafe_delimiters_and_separators(options.symbolize_keys) - - wrap_with_output_safety_handling(number, options.delete(:raise)) { - ActiveSupport::NumberHelper.number_to_delimited(number, options) - } + delegate_number_helper_method(:number_to_delimited, number, options) end # Formats a +number+ with the specified level of @@ -237,11 +223,7 @@ module ActionView # number_with_precision(1111.2345, precision: 2, separator: ',', delimiter: '.') # # => 1.111,23 def number_with_precision(number, options = {}) - options = escape_unsafe_delimiters_and_separators(options.symbolize_keys) - - wrap_with_output_safety_handling(number, options.delete(:raise)) { - ActiveSupport::NumberHelper.number_to_rounded(number, options) - } + delegate_number_helper_method(:number_to_rounded, number, options) end # Formats the bytes in +number+ into a more understandable @@ -293,11 +275,7 @@ module ActionView # number_to_human_size(1234567890123, precision: 5) # => "1.1229 TB" # number_to_human_size(524288000, precision: 5) # => "500 MB" def number_to_human_size(number, options = {}) - options = escape_unsafe_delimiters_and_separators(options.symbolize_keys) - - wrap_with_output_safety_handling(number, options.delete(:raise)) { - ActiveSupport::NumberHelper.number_to_human_size(number, options) - } + delegate_number_helper_method(:number_to_human_size, number, options) end # Pretty prints (formats and approximates) a number in a way it @@ -399,15 +377,20 @@ module ActionView # number_to_human(0.34, units: :distance) # => "34 centimeters" # def number_to_human(number, options = {}) + delegate_number_helper_method(:number_to_human, number, options) + end + + private + + def delegate_number_helper_method(method, number, options) + return unless number options = escape_unsafe_delimiters_and_separators(options.symbolize_keys) wrap_with_output_safety_handling(number, options.delete(:raise)) { - ActiveSupport::NumberHelper.number_to_human(number, options) + ActiveSupport::NumberHelper.public_send(method, number, options) } end - private - def escape_unsafe_delimiters_and_separators(options) options[:separator] = ERB::Util.html_escape(options[:separator]) if options[:separator] && !options[:separator].html_safe? options[:delimiter] = ERB::Util.html_escape(options[:delimiter]) if options[:delimiter] && !options[:delimiter].html_safe? diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb index 732f35643a..3528381781 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 @@ -151,7 +151,7 @@ module ActionView attrs << tag_option(key, value, escape) end end - " #{attrs.sort! * ' '}".html_safe unless attrs.empty? + " #{attrs.sort! * ' '}" unless attrs.empty? end def data_tag_option(key, value, escape) diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb index 3fe3f4e9df..8607da301c 100644 --- a/actionview/lib/action_view/helpers/tags/base.rb +++ b/actionview/lib/action_view/helpers/tags/base.rb @@ -119,7 +119,8 @@ module ActionView html_options = html_options.stringify_keys add_default_name_and_id(html_options) options[:include_blank] ||= true unless options[:prompt] || select_not_required?(html_options) - select = content_tag("select", add_options(option_tags, options, value(object)), html_options) + value = options.fetch(:selected) { value(object) } + select = content_tag("select", add_options(option_tags, options, value), html_options) if html_options["multiple"] && options.fetch(:include_hidden, true) tag("input", :disabled => html_options["disabled"], :name => html_options["name"], :type => "hidden", :value => "") + select diff --git a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb index 52006d856b..9b77ebeb1b 100644 --- a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb +++ b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb @@ -27,7 +27,8 @@ module ActionView # Append a hidden field to make sure something will be sent back to the # server if all check boxes are unchecked. - hidden = @template_object.hidden_field_tag("#{tag_name}[]", "", :id => nil) + hidden_name = @html_options[:name] || "#{tag_name}[]" + hidden = @template_object.hidden_field_tag(hidden_name, "", :id => nil) rendered_collection + hidden end diff --git a/actionview/lib/action_view/helpers/tags/collection_helpers.rb b/actionview/lib/action_view/helpers/tags/collection_helpers.rb index 388dcf1f13..787039c82e 100644 --- a/actionview/lib/action_view/helpers/tags/collection_helpers.rb +++ b/actionview/lib/action_view/helpers/tags/collection_helpers.rb @@ -18,7 +18,8 @@ module ActionView end def label(label_html_options={}, &block) - @template_object.label(@object_name, @sanitized_attribute_name, @text, label_html_options, &block) + html_options = label_html_options.merge(@input_html_options) + @template_object.label(@object_name, @sanitized_attribute_name, @text, html_options, &block) end end diff --git a/actionview/lib/action_view/helpers/tags/label.rb b/actionview/lib/action_view/helpers/tags/label.rb index 35d3ba8434..180aa9ac27 100644 --- a/actionview/lib/action_view/helpers/tags/label.rb +++ b/actionview/lib/action_view/helpers/tags/label.rb @@ -30,6 +30,7 @@ module ActionView add_default_name_and_id_for_value(tag_value, name_and_id) options.delete("index") options.delete("namespace") + options.delete("multiple") options["for"] = name_and_id["id"] unless options.key?("for") if block_given? diff --git a/actionview/lib/action_view/helpers/tags/select.rb b/actionview/lib/action_view/helpers/tags/select.rb index d64e2f68ef..00881d9978 100644 --- a/actionview/lib/action_view/helpers/tags/select.rb +++ b/actionview/lib/action_view/helpers/tags/select.rb @@ -3,8 +3,9 @@ module ActionView module Tags # :nodoc: class Select < Base # :nodoc: def initialize(object_name, method_name, template_object, choices, options, html_options) - @choices = choices + @choices = block_given? ? template_object.capture { yield } : choices @choices = @choices.to_a if @choices.is_a?(Range) + @html_options = html_options super(object_name, method_name, template_object, options) diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb index 3fc64fa8a5..b0e4aa3cd3 100644 --- a/actionview/lib/action_view/helpers/text_helper.rb +++ b/actionview/lib/action_view/helpers/text_helper.rb @@ -150,7 +150,7 @@ module ActionView def excerpt(text, phrase, options = {}) return unless text && phrase - separator = options.fetch(:separator, "") + separator = options[:separator] || '' phrase = Regexp.escape(phrase) regex = /#{phrase}/i @@ -171,7 +171,8 @@ module ActionView prefix, first_part = cut_excerpt_part(:first, first_part, separator, options) postfix, second_part = cut_excerpt_part(:second, second_part, separator, options) - prefix + (first_part + separator + phrase + separator + second_part).strip + postfix + affix = [first_part, separator, phrase, separator, second_part].join.strip + [prefix, affix, postfix].join end # Attempts to pluralize the +singular+ word unless +count+ is 1. If @@ -267,7 +268,7 @@ module ActionView content_tag(wrapper_tag, nil, html_options) else paragraphs.map! { |paragraph| - content_tag(wrapper_tag, paragraph, html_options, options[:sanitize]) + content_tag(wrapper_tag, paragraph, html_options, false) }.join("\n\n").html_safe end end diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index 1920a94567..56dd7a4390 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -92,8 +92,9 @@ module ActionView # ==== Data attributes # # * <tt>confirm: 'question?'</tt> - This will allow the unobtrusive JavaScript - # driver to prompt with the question specified. If the user accepts, the link is - # processed normally, otherwise no action is taken. + # driver to prompt with the question specified (in this case, the + # resulting text would be <tt>question?</tt>. If the user accepts, the + # link is processed normally, otherwise no action is taken. # * <tt>:disable_with</tt> - Value of this parameter will be # used as the value for a disabled version of the submit # button when the form is submitted. This feature is provided @@ -213,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 # @@ -287,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 @@ -310,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 @@ -454,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/layouts.rb b/actionview/lib/action_view/layouts.rb index d8de1d95df..ffa67649da 100644 --- a/actionview/lib/action_view/layouts.rb +++ b/actionview/lib/action_view/layouts.rb @@ -221,7 +221,7 @@ module ActionView # This module is mixed in if layout conditions are provided. This means # that if no layout conditions are used, this method is not used module LayoutConditions # :nodoc: - private + private # Determines whether the current action has a layout definition by # checking the action name against the :only and :except conditions @@ -269,15 +269,6 @@ module ActionView _write_layout_method end - # If no layout is supplied, look for a template named the return - # value of this method. - # - # ==== Returns - # * <tt>String</tt> - A template name - def _implied_layout_name # :nodoc: - controller_path - end - # Creates a _layout method to be called by _default_layout . # # If a layout is not explicitly mentioned then look for a layout with the controller's name. @@ -335,6 +326,17 @@ module ActionView private :_layout RUBY end + + private + + # If no layout is supplied, look for a template named the return + # value of this method. + # + # ==== Returns + # * <tt>String</tt> - A template name + def _implied_layout_name # :nodoc: + controller_path + end end def _normalize_options(options) # :nodoc: diff --git a/actionview/lib/action_view/log_subscriber.rb b/actionview/lib/action_view/log_subscriber.rb index 354a136894..6c8d9cb5bf 100644 --- a/actionview/lib/action_view/log_subscriber.rb +++ b/actionview/lib/action_view/log_subscriber.rb @@ -5,7 +5,12 @@ module ActionView # # Provides functionality so that Rails can output logs from Action View. class LogSubscriber < ActiveSupport::LogSubscriber - VIEWS_PATTERN = /^app\/views\//.freeze + VIEWS_PATTERN = /^app\/views\// + + def initialize + @root = nil + super + end def render_template(event) return unless logger.info? @@ -23,8 +28,15 @@ module ActionView protected + EMPTY = '' def from_rails_root(string) - string.sub("#{Rails.root}/", "").sub(VIEWS_PATTERN, "") + string = string.sub(rails_root, EMPTY) + string.sub!(VIEWS_PATTERN, EMPTY) + string + end + + def rails_root + @root ||= "#{Rails.root}/" end end end diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb index 4113448af0..c2783f6377 100644 --- a/actionview/lib/action_view/railtie.rb +++ b/actionview/lib/action_view/railtie.rb @@ -48,5 +48,9 @@ module ActionView ActionMailer::Base.send(:include, ActionView::Layouts) end end + + rake_tasks do + load "action_view/tasks/dependencies.rake" + end end end diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb index 821026268a..36f17f01fd 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -159,7 +159,7 @@ module ActionView # </div> # # If a collection is given, the layout will be rendered once for each item in - # the collection. Just think these two snippets have the same output: + # the collection. For example, these two snippets have the same output: # # <%# app/views/users/_user.html.erb %> # Name: <%= user.name %> diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb index db9c4ef501..82db9e26df 100644 --- a/actionview/lib/action_view/rendering.rb +++ b/actionview/lib/action_view/rendering.rb @@ -77,50 +77,29 @@ module ActionView @_view_renderer ||= ActionView::Renderer.new(lookup_context) end - # Render template to response_body - # :api: public - def render(*args, &block) - options = _normalize_render(*args, &block) - self.response_body = render_to_body(options) - end - - # Raw rendering of a template to a string. - # :api: public - def render_to_string(*args, &block) - options = _normalize_render(*args, &block) - render_to_body(options) - end - - # Raw rendering of a template. - # :api: public def render_to_body(options = {}) _process_options(options) _render_template(options) end - # Find and renders a template based on the options given. - # :api: private - def _render_template(options) #:nodoc: - lookup_context.rendered_format = nil if options[:formats] - view_renderer.render(view_context, options) - end - def rendered_format Mime[lookup_context.rendered_format] end - def default_protected_instance_vars - super.concat([:@_view_context_class, :@_view_renderer, :@_lookup_context]) - end - private - # Normalize args and options. + # Find and renders a template based on the options given. # :api: private - def _normalize_render(*args, &block) - options = _normalize_args(*args, &block) - _normalize_options(options) - options + def _render_template(options) #:nodoc: + lookup_context.rendered_format = nil if options[:formats] + view_renderer.render(view_context, options) + end + + # Assign the rendered format to lookup context. + def _process_format(format) #:nodoc: + super + lookup_context.formats = [format.to_sym] + lookup_context.rendered_format = lookup_context.formats.first end # Normalize args by converting render "foo" to render :action => "foo" and diff --git a/actionview/lib/action_view/routing_url_for.rb b/actionview/lib/action_view/routing_url_for.rb index f10e7e88ba..33be06cbf7 100644 --- a/actionview/lib/action_view/routing_url_for.rb +++ b/actionview/lib/action_view/routing_url_for.rb @@ -83,6 +83,8 @@ module ActionView super when :back _back_url + when Array + polymorphic_path(options, options.extract_options!) else polymorphic_path(options) end diff --git a/actionview/lib/action_view/tasks/dependencies.rake b/actionview/lib/action_view/tasks/dependencies.rake new file mode 100644 index 0000000000..1b9426c0e5 --- /dev/null +++ b/actionview/lib/action_view/tasks/dependencies.rake @@ -0,0 +1,17 @@ +namespace :cache_digests do + desc 'Lookup nested dependencies for TEMPLATE (like messages/show or comments/_comment.html)' + task :nested_dependencies => :environment do + abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present? + template, format = ENV['TEMPLATE'].split(".") + format ||= :html + puts JSON.pretty_generate ActionView::Digestor.new(template, format, ApplicationController.new.lookup_context).nested_dependencies + end + + desc 'Lookup first-level dependencies for TEMPLATE (like messages/show or comments/_comment.html)' + task :dependencies => :environment do + abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present? + template, format = ENV['TEMPLATE'].split(".") + format ||= :html + puts JSON.pretty_generate ActionView::Digestor.new(template, format, ApplicationController.new.lookup_context).dependencies + end +end 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/lib/action_view/template/error.rb b/actionview/lib/action_view/template/error.rb index a89d51221e..7b4b5e13e0 100644 --- a/actionview/lib/action_view/template/error.rb +++ b/actionview/lib/action_view/template/error.rb @@ -56,13 +56,13 @@ module ActionView class Error < ActionViewError #:nodoc: SOURCE_CODE_RADIUS = 3 - attr_reader :original_exception, :backtrace + attr_reader :original_exception def initialize(template, original_exception) super(original_exception.message) @template, @original_exception = template, original_exception @sub_templates = nil - @backtrace = original_exception.backtrace + set_backtrace(original_exception.backtrace) end def file_name diff --git a/actionview/lib/action_view/vendor/html-scanner/html/node.rb b/actionview/lib/action_view/vendor/html-scanner/html/node.rb index 7e7cd4f7b6..27f0f2f6f8 100644 --- a/actionview/lib/action_view/vendor/html-scanner/html/node.rb +++ b/actionview/lib/action_view/vendor/html-scanner/html/node.rb @@ -71,12 +71,12 @@ module HTML #:nodoc: @line, @position = line, pos end - # Return a textual representation of the node. + # Returns a textual representation of the node. def to_s @children.join() end - # Return false (subclasses must override this to provide specific matching + # Returns false (subclasses must override this to provide specific matching # behavior.) +conditions+ may be of any type. def match(conditions) false diff --git a/actionview/lib/action_view/vendor/html-scanner/html/selector.rb b/actionview/lib/action_view/vendor/html-scanner/html/selector.rb index 7f8609c408..dfdd724b9b 100644 --- a/actionview/lib/action_view/vendor/html-scanner/html/selector.rb +++ b/actionview/lib/action_view/vendor/html-scanner/html/selector.rb @@ -488,7 +488,7 @@ module HTML end - # Return the next element after this one. Skips sibling text nodes. + # Returns the next element after this one. Skips sibling text nodes. # # With the +name+ argument, returns the next element with that name, # skipping other sibling elements. diff --git a/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb b/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb index 8ac8d34430..adf4e45930 100644 --- a/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb +++ b/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb @@ -30,7 +30,7 @@ module HTML #:nodoc: @current_line = 1 end - # Return the next token in the sequence, or +nil+ if there are no more tokens in + # Returns the next token in the sequence, or +nil+ if there are no more tokens in # the stream. def next return nil if @scanner.eos? 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/fixtures/helpers/abc_helper.rb b/actionview/test/fixtures/helpers/abc_helper.rb index 7104ff3730..cf2774bb5f 100644 --- a/actionview/test/fixtures/helpers/abc_helper.rb +++ b/actionview/test/fixtures/helpers/abc_helper.rb @@ -1,5 +1,3 @@ module AbcHelper def bare_a() end - def bare_b() end - def bare_c() end end diff --git a/actionview/test/fixtures/helpers/fun/games_helper.rb b/actionview/test/fixtures/helpers/fun/games_helper.rb deleted file mode 100644 index 3b7adce086..0000000000 --- a/actionview/test/fixtures/helpers/fun/games_helper.rb +++ /dev/null @@ -1,5 +0,0 @@ -module Fun - module GamesHelper - def stratego() "Iz guuut!" end - end -end
\ No newline at end of file diff --git a/actionview/test/fixtures/helpers/fun/pdf_helper.rb b/actionview/test/fixtures/helpers/fun/pdf_helper.rb deleted file mode 100644 index 0171be8500..0000000000 --- a/actionview/test/fixtures/helpers/fun/pdf_helper.rb +++ /dev/null @@ -1,5 +0,0 @@ -module Fun - module PdfHelper - def foobar() 'baz' end - end -end diff --git a/actionview/test/fixtures/helpers/just_me_helper.rb b/actionview/test/fixtures/helpers/just_me_helper.rb deleted file mode 100644 index b140a7b9b4..0000000000 --- a/actionview/test/fixtures/helpers/just_me_helper.rb +++ /dev/null @@ -1,3 +0,0 @@ -module JustMeHelper - def me() "mine!" end -end
\ No newline at end of file diff --git a/actionview/test/fixtures/helpers/me_too_helper.rb b/actionview/test/fixtures/helpers/me_too_helper.rb deleted file mode 100644 index ce56042143..0000000000 --- a/actionview/test/fixtures/helpers/me_too_helper.rb +++ /dev/null @@ -1,3 +0,0 @@ -module MeTooHelper - def me() "me too!" end -end
\ No newline at end of file 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 c6608e214a..779a7fb53c 100644 --- a/actionview/test/template/digestor_test.rb +++ b/actionview/test/template/digestor_test.rb @@ -15,6 +15,16 @@ end class FixtureFinder FIXTURES_DIR = "#{File.dirname(__FILE__)}/../fixtures/digestor" + attr_reader :details + + def initialize + @details = {} + end + + def details_key + details.hash + end + def find(logical_name, keys, partial, options) FixtureTemplate.new("digestor/#{partial ? logical_name.gsub(%r|/([^/]+)$|, '/_\1') : logical_name}.#{options[:formats].first}.erb") end @@ -140,6 +150,20 @@ class TemplateDigestorTest < ActionView::TestCase end end + def test_details_are_included_in_cache_key + # Cache the template digest. + old_digest = digest("events/_event") + + # Change the template; the cached digest remains unchanged. + change_template("events/_event") + + # The details are changed, so a new cache key is generated. + finder.details[:foo] = "bar" + + # The cache is busted. + assert_not_equal old_digest, digest("events/_event") + end + def test_extra_whitespace_in_render_partial assert_digest_difference("messages/edit") do change_template("messages/_form") @@ -193,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 @@ -220,7 +269,11 @@ class TemplateDigestorTest < ActionView::TestCase end def digest(template_name, options={}) - ActionView::Digestor.digest(template_name, :html, FixtureFinder.new, options) + ActionView::Digestor.digest(template_name, :html, finder, options) + end + + def finder + @finder ||= FixtureFinder.new end def change_template(template_name) diff --git a/actionview/test/template/form_collections_helper_test.rb b/actionview/test/template/form_collections_helper_test.rb index bc9c21dfd3..d28e4aeb48 100644 --- a/actionview/test/template/form_collections_helper_test.rb +++ b/actionview/test/template/form_collections_helper_test.rb @@ -17,14 +17,14 @@ class FormCollectionsHelperTest < ActionView::TestCase end # COLLECTION RADIO BUTTONS - test 'collection radio accepts a collection and generate inputs from value method' do + test 'collection radio accepts a collection and generates inputs from value method' do with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s assert_select 'input[type=radio][value=true]#user_active_true' assert_select 'input[type=radio][value=false]#user_active_false' end - test 'collection radio accepts a collection and generate inputs from label method' do + test 'collection radio accepts a collection and generates inputs from label method' do with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s assert_select 'label[for=user_active_true]', 'true' @@ -38,7 +38,7 @@ class FormCollectionsHelperTest < ActionView::TestCase assert_select 'label[for=user_active_no]', 'No' end - test 'colection radio should sanitize collection values for labels correctly' do + test 'collection radio should sanitize collection values for labels correctly' do with_collection_radio_buttons :user, :name, ['$0.99', '$1.99'], :to_s, :to_s assert_select 'label[for=user_name_099]', '$0.99' assert_select 'label[for=user_name_199]', '$1.99' @@ -179,6 +179,13 @@ class FormCollectionsHelperTest < ActionView::TestCase assert_select "input[type=hidden][name='user[category_ids][]'][value=]", :count => 1 end + test 'collection check boxes generates a hidden field using the given :name in :html_options' do + collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] + with_collection_check_boxes :user, :category_ids, collection, :id, :name, {}, {name: "user[other_category_ids][]"} + + assert_select "input[type=hidden][name='user[other_category_ids][]'][value=]", :count => 1 + end + test 'collection check boxes accepts a collection and generate a serie of checkboxes with labels for label method' do collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] with_collection_check_boxes :user, :category_ids, collection, :id, :name @@ -194,7 +201,7 @@ class FormCollectionsHelperTest < ActionView::TestCase assert_select 'label[for=user_active_no]', 'No' end - test 'colection check box should sanitize collection values for labels correctly' do + test 'collection check box should sanitize collection values for labels correctly' do with_collection_check_boxes :user, :name, ['$0.99', '$1.99'], :to_s, :to_s assert_select 'label[for=user_name_099]', '$0.99' assert_select 'label[for=user_name_199]', '$1.99' diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb index 8cca43d7ca..3e8a2468ed 100644 --- a/actionview/test/template/form_helper_test.rb +++ b/actionview/test/template/form_helper_test.rb @@ -1282,6 +1282,24 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end + def test_form_with_namespace_and_with_collection_radio_buttons + post = Post.new + def post.active; false; end + + form_for(post, namespace: 'foo') do |f| + concat f.collection_radio_buttons(:active, [true, false], :to_s, :to_s) + end + + expected = whole_form("/posts", "foo_new_post", "new_post") do + "<input id='foo_post_active_true' name='post[active]' type='radio' value='true' />" + + "<label for='foo_post_active_true'>true</label>" + + "<input checked='checked' id='foo_post_active_false' name='post[active]' type='radio' value='false' />" + + "<label for='foo_post_active_false'>false</label>" + end + + assert_dom_equal expected, output_buffer + end + def test_form_for_with_collection_check_boxes post = Post.new def post.tag_ids; [1, 3]; end @@ -1361,6 +1379,24 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end + def test_form_with_namespace_and_with_collection_check_boxes + post = Post.new + def post.tag_ids; [1]; end + collection = [[1, "Tag 1"]] + + form_for(post, namespace: 'foo') do |f| + concat f.collection_check_boxes(:tag_ids, collection, :first, :last) + end + + expected = whole_form("/posts", "foo_new_post", "new_post") do + "<input checked='checked' id='foo_post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" + + "<label for='foo_post_tag_ids_1'>Tag 1</label>" + + "<input name='post[tag_ids][]' type='hidden' value='' />" + end + + assert_dom_equal expected, output_buffer + end + def test_form_for_with_file_field_generate_multipart Post.send :attr_accessor, :file @@ -1681,6 +1717,18 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end + def test_form_for_with_namespace_and_as_option + form_for(@post, namespace: 'namespace', as: 'custom_name') do |f| + concat f.text_field(:title) + end + + expected = whole_form('/posts/123', 'namespace_edit_custom_name', 'edit_custom_name', method: 'patch') do + "<input id='namespace_custom_name_title' name='custom_name[title]' type='text' value='Hello World' />" + end + + assert_dom_equal expected, output_buffer + end + def test_two_form_for_with_namespace form_for(@post, namespace: 'namespace_1') do |f| concat f.label(:title) diff --git a/actionview/test/template/form_options_helper_test.rb b/actionview/test/template/form_options_helper_test.rb index 3ec138b639..50e9d132a7 100644 --- a/actionview/test/template/form_options_helper_test.rb +++ b/actionview/test/template/form_options_helper_test.rb @@ -1,5 +1,4 @@ require 'abstract_unit' -require 'tzinfo' class Map < Hash def category @@ -7,8 +6,6 @@ class Map < Hash end end -TZInfo::Timezone.cattr_reader :loaded_zones - class FormOptionsHelperTest < ActionView::TestCase tests ActionView::Helpers::FormOptionsHelper @@ -22,7 +19,7 @@ class FormOptionsHelperTest < ActionView::TestCase def setup @fake_timezones = %w(A B C D E).map do |id| - tz = TZInfo::Timezone.loaded_zones[id] = stub(:name => id, :to_s => id) + tz = stub(:name => id, :to_s => id) ActiveSupport::TimeZone.stubs(:[]).with(id).returns(tz) tz end @@ -559,6 +556,21 @@ class FormOptionsHelperTest < ActionView::TestCase ) end + def test_select_under_fields_for_with_block + @post = Post.new + + output_buffer = fields_for :post, @post do |f| + concat(f.select(:category) do + concat content_tag(:option, "hello world") + end) + end + + assert_dom_equal( + "<select id=\"post_category\" name=\"post[category]\"><option>hello world</option></select>", + output_buffer + ) + end + def test_select_with_multiple_to_add_hidden_input output_buffer = select(:post, :category, "", {}, :multiple => true) assert_dom_equal( @@ -786,6 +798,22 @@ class FormOptionsHelperTest < ActionView::TestCase ) end + def test_select_not_existing_method_with_selected_value + @post = Post.new + assert_dom_equal( + "<select id=\"post_locale\" name=\"post[locale]\"><option value=\"en\">en</option>\n<option value=\"ru\" selected=\"selected\">ru</option></select>", + select("post", "locale", %w( en ru ), :selected => 'ru') + ) + end + + def test_select_with_prompt_and_selected_value + @post = Post.new + assert_dom_equal( + "<select id=\"post_category\" name=\"post[category]\"><option value=\"one\">one</option>\n<option selected=\"selected\" value=\"two\">two</option></select>", + select("post", "category", %w( one two ), :selected => 'two', :prompt => true) + ) + end + def test_select_with_disabled_array @post = Post.new @post.category = "<mus>" 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/resolver_patterns_test.rb b/actionview/test/template/resolver_patterns_test.rb index 97b1bad055..575eb9bd28 100644 --- a/actionview/test/template/resolver_patterns_test.rb +++ b/actionview/test/template/resolver_patterns_test.rb @@ -20,7 +20,7 @@ class ResolverPatternsTest < ActiveSupport::TestCase assert_equal [:html], templates.first.formats end - def test_should_return_all_templates_when_ambigous_pattern + def test_should_return_all_templates_when_ambiguous_pattern templates = @resolver.find_all("another", "custom_pattern", false, {:locale => [], :formats => [:html], :handlers => [:erb]}) assert_equal 2, templates.size, "expected two templates" assert_equal "Another template!", templates[0].source 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/template_error_test.rb b/actionview/test/template/template_error_test.rb index 91424daeed..3971ec809c 100644 --- a/actionview/test/template/template_error_test.rb +++ b/actionview/test/template/template_error_test.rb @@ -6,6 +6,13 @@ class TemplateErrorTest < ActiveSupport::TestCase assert_equal "original", error.message end + def test_provides_original_backtrace + original_exception = Exception.new + original_exception.set_backtrace(%W[ foo bar baz ]) + error = ActionView::Template::Error.new("test", original_exception) + assert_equal %W[ foo bar baz ], error.backtrace + end + def test_provides_useful_inspect error = ActionView::Template::Error.new("test", Exception.new("original")) assert_equal "#<ActionView::Template::Error: original>", error.inspect diff --git a/actionview/test/template/text_helper_test.rb b/actionview/test/template/text_helper_test.rb index 1b2234f4e2..c624326683 100644 --- a/actionview/test/template/text_helper_test.rb +++ b/actionview/test/template/text_helper_test.rb @@ -42,6 +42,11 @@ class TextHelperTest < ActionView::TestCase assert_equal "<p><b> test with unsafe string </b></p>", simple_format("<b> test with unsafe string </b><script>code!</script>") end + def test_simple_format_should_sanitize_input_when_sanitize_option_is_true + assert_equal '<p><b> test with unsafe string </b></p>', + simple_format('<b> test with unsafe string </b><script>code!</script>', {}, sanitize: true) + end + def test_simple_format_should_not_sanitize_input_when_sanitize_option_is_false assert_equal "<p><b> test with unsafe string </b><script>code!</script></p>", simple_format("<b> test with unsafe string </b><script>code!</script>", {}, :sanitize => false) end @@ -314,6 +319,9 @@ class TextHelperTest < ActionView::TestCase options = { :separator => "\n", :radius => 1 } assert_equal("...very\nvery long\nstring", excerpt("my very\nvery\nvery long\nstring", 'long', options)) + + assert_equal excerpt('This is a beautiful morning', 'a'), + excerpt('This is a beautiful morning', 'a', separator: nil) end def test_word_wrap diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb index d512fa9913..deba33510a 100644 --- a/actionview/test/template/url_helper_test.rb +++ b/actionview/test/template/url_helper_test.rb @@ -1,5 +1,6 @@ # encoding: utf-8 require 'abstract_unit' +require 'minitest/mock' class UrlHelperTest < ActiveSupport::TestCase @@ -160,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 3d3c61ed1c..e8602ecbcf 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,3 +1,18 @@ +* 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. + + *Bogdan Gusiev* + * Fix has_secure_password. `password_confirmation` validations are triggered even if no `password_confirmation` is set. 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 ea5ddf71de..c5f1b3f11a 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -14,13 +14,9 @@ module ActiveModel # track. # * Call <tt>attr_name_will_change!</tt> before each change to the tracked # attribute. - # - # If you wish to also track previous changes on save or update, you need to - # add: - # - # @previously_changed = changes - # - # inside of your save or update method. + # * Call <tt>changes_applied</tt> after the changes are persisted. + # * Call <tt>reset_changes</tt> when you want to reset the changes + # information. # # A minimal implementation could be: # @@ -39,8 +35,12 @@ module ActiveModel # end # # def save - # @previously_changed = changes - # @changed_attributes.clear + # # do persistence work + # changes_applied + # end + # + # def reload! + # reset_changes # end # end # @@ -65,6 +65,12 @@ module ActiveModel # person.changed? # => false # person.name_changed? # => false # + # Reset the changes: + # + # person.previous_changes # => {"name" => ["Uncle Bob", "Bill"]} + # person.reload! + # person.previous_changes # => {} + # # Assigning the same value leaves the attribute unchanged: # # person.name = 'Bill' @@ -129,7 +135,7 @@ module ActiveModel # person.save # person.previous_changes # => {"name" => ["bob", "robert"]} def previous_changes - @previously_changed + @previously_changed ||= {} end # Returns a hash of the attributes with unsaved changes indicating their original @@ -139,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+. @@ -154,6 +160,18 @@ module ActiveModel private + # Removes current changes and makes them accessible through +previous_changes+. + def changes_applied + @previously_changed = changes + @changed_attributes = {} + end + + # Removes all dirty data: current changes and previous changes + def reset_changes + @previously_changed = {} + @changed_attributes = {} + end + # Handle <tt>*_change</tt> for +method_missing+. def attribute_change(attr) [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr) diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index bc9edf4a56..11ebfe6cc0 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 8b9ac97bbb..7e694b5c50 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -20,9 +20,9 @@ module ActiveModel # value to the password_confirmation attribute and the validation # will not be triggered. # - # You need to add bcrypt-ruby (~> 3.1.0) to Gemfile to use #has_secure_password: + # You need to add bcrypt-ruby (~> 3.1.2) to Gemfile to use #has_secure_password: # - # gem 'bcrypt-ruby', '~> 3.1.0' + # gem 'bcrypt-ruby', '~> 3.1.2' # # Example using Active Record (which automatically includes ActiveModel::SecurePassword): # @@ -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.0' 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..bad9e4f9a9 100644 --- a/activemodel/lib/active_model/validations/clusivity.rb +++ b/activemodel/lib/active_model/validations/clusivity.rb @@ -30,12 +30,21 @@ 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? + if enumerable.is_a? Range + case enumerable.first + when Numeric, Time, DateTime + :cover? + else + :include? + end + else + :include? + end end end end diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index be7cae588f..f0fe22438f 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -17,8 +17,8 @@ module ActiveModel raise ArgumentError, "Either :with or :without must be supplied (but not both)" end - check_options_validity(options, :with) - check_options_validity(options, :without) + check_options_validity :with + check_options_validity :without end private @@ -32,21 +32,23 @@ module ActiveModel record.errors.add(attribute, :invalid, options.except(name).merge!(value: value)) end - def regexp_using_multiline_anchors?(regexp) - regexp.source.start_with?("^") || - (regexp.source.end_with?("$") && !regexp.source.end_with?("\\$")) + def check_options_validity(name) + if option = options[name] + if option.is_a?(Regexp) + if options[:multiline] != true && regexp_using_multiline_anchors?(option) + raise ArgumentError, "The provided regular expression is using multiline anchors (^ or $), " \ + "which may present a security risk. Did you mean to use \\A and \\z, or forgot to add the " \ + ":multiline => true option?" + end + elsif !option.respond_to?(:call) + raise ArgumentError, "A regular expression or a proc or lambda must be supplied as :#{name}" + end + end end - def check_options_validity(options, name) - option = options[name] - if option && !option.is_a?(Regexp) && !option.respond_to?(:call) - raise ArgumentError, "A regular expression or a proc or lambda must be supplied as :#{name}" - elsif option && option.is_a?(Regexp) && - regexp_using_multiline_anchors?(option) && options[:multiline] != true - raise ArgumentError, "The provided regular expression is using multiline anchors (^ or $), " \ - "which may present a security risk. Did you mean to use \\A and \\z, or forgot to add the " \ - ":multiline => true option?" - end + def regexp_using_multiline_anchors?(regexp) + source = regexp.source + source.start_with?("^") || (source.end_with?("$") && !source.end_with?("\\$")) end end diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index c6abe45f4a..c8d3236463 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -11,8 +11,9 @@ module ActiveModel def check_validity! keys = CHECKS.keys - [:odd, :even] options.slice(*keys).each do |option, value| - next if value.is_a?(Numeric) || value.is_a?(Proc) || value.is_a?(Symbol) - raise ArgumentError, ":#{option} must be a number, a symbol or a proc" + unless value.is_a?(Numeric) || value.is_a?(Proc) || value.is_a?(Symbol) + raise ArgumentError, ":#{option} must be a number, a symbol or a proc" + end end end @@ -43,11 +44,15 @@ module ActiveModel record.errors.add(attr_name, option, filtered_options(value)) end else - option_value = option_value.call(record) if option_value.is_a?(Proc) - option_value = record.send(option_value) if option_value.is_a?(Symbol) + case option_value + when Proc + option_value = option_value.call(record) + when Symbol + option_value = record.send(option_value) + end unless value.send(CHECKS[option], option_value) - record.errors.add(attr_name, option, filtered_options(value).merge(count: option_value)) + record.errors.add(attr_name, option, filtered_options(value).merge!(count: option_value)) end end end @@ -56,16 +61,9 @@ module ActiveModel protected def parse_raw_value_as_a_number(raw_value) - case raw_value - when /\A0[xX]/ - nil - else - begin - Kernel.Float(raw_value) - rescue ArgumentError, TypeError - nil - end - end + Kernel.Float(raw_value) if raw_value !~ /\A0[xX]/ + rescue ArgumentError, TypeError + nil end def parse_raw_value_as_an_integer(raw_value) @@ -73,7 +71,9 @@ module ActiveModel end def filtered_options(value) - options.except(*RESERVED_OPTIONS).merge!(value: value) + filtered = options.except(*RESERVED_OPTIONS) + filtered[:value] = value + filtered end end diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index 690856aee1..e19b8e0a9e 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -109,7 +109,7 @@ module ActiveModel deprecated_setup(options) end - # Return the kind for this validator. + # Returns the kind for this validator. # # PresenceValidator.new.kind # => :presence # UniquenessValidator.new.kind # => :uniqueness diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb index ba45089cca..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,9 +29,17 @@ 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 - @previously_changed = changes - @changed_attributes.clear + changes_applied end end @@ -125,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/serializers/json_serialization_test.rb b/activemodel/test/cases/serializers/json_serialization_test.rb index e4f5e61e91..bc185c737f 100644 --- a/activemodel/test/cases/serializers/json_serialization_test.rb +++ b/activemodel/test/cases/serializers/json_serialization_test.rb @@ -198,7 +198,7 @@ class JsonSerializationTest < ActiveModel::TestCase assert_no_match %r{"preferences":}, json end - test "custom as_json options should be extendible" do + test "custom as_json options should be extensible" do def @contact.as_json(options = {}); super(options.merge(only: [:name])); end json = @contact.to_json diff --git a/activemodel/test/cases/validations/conditional_validation_test.rb b/activemodel/test/cases/validations/conditional_validation_test.rb index 41a4c33727..5049d6dd61 100644 --- a/activemodel/test/cases/validations/conditional_validation_test.rb +++ b/activemodel/test/cases/validations/conditional_validation_test.rb @@ -23,7 +23,7 @@ class ConditionalValidationTest < ActiveModel::TestCase Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: :condition_is_true) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.valid? - assert t.errors[:title].empty? + assert_empty t.errors[:title] end def test_if_validation_using_method_false @@ -31,7 +31,7 @@ class ConditionalValidationTest < ActiveModel::TestCase Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_true_but_its_not) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.valid? - assert t.errors[:title].empty? + assert_empty t.errors[:title] end def test_unless_validation_using_method_false @@ -57,7 +57,7 @@ class ConditionalValidationTest < ActiveModel::TestCase Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: "a = 1; a == 1") t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.valid? - assert t.errors[:title].empty? + assert_empty t.errors[:title] end def test_if_validation_using_string_false @@ -65,7 +65,7 @@ class ConditionalValidationTest < ActiveModel::TestCase Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: "false") t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.valid? - assert t.errors[:title].empty? + assert_empty t.errors[:title] end def test_unless_validation_using_string_false @@ -93,7 +93,7 @@ class ConditionalValidationTest < ActiveModel::TestCase unless: Proc.new { |r| r.content.size > 4 }) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.valid? - assert t.errors[:title].empty? + assert_empty t.errors[:title] end def test_if_validation_using_block_false @@ -102,7 +102,7 @@ class ConditionalValidationTest < ActiveModel::TestCase if: Proc.new { |r| r.title != "uhohuhoh"}) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.valid? - assert t.errors[:title].empty? + assert_empty t.errors[:title] end def test_unless_validation_using_block_false @@ -124,7 +124,7 @@ class ConditionalValidationTest < ActiveModel::TestCase t = Topic.new assert t.invalid?, "A topic without a title should not be valid" - assert t.errors[:author_name].empty?, "A topic without an 'important' title should not require an author" + assert_empty t.errors[:author_name], "A topic without an 'important' title should not require an author" t.title = "Just a title" assert t.valid?, "A topic with a basic title should be valid" 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 c7928e503b..43651c9b48 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,391 @@ -* Fix PredicateBuilder so polymorhic association keys in `where` clause can - also accept not only `ActiveRecord::Base` direct descendances (decorated +* Fix validation on uniqueness of empty association. + + *Evgeny Li* + +* Make `ActiveRecord::Relation#unscope` affect relations it is merged in to. + + *Jon Leighton* + +* Use strings to represent non-string `order_values`. + + *Yves Senn* + +* Checks to see if the record contains the foreign key to set the inverse automatically. + + *Edo Balvers* + +* Added `ActiveRecord::Base.to_param` for convenient "pretty" URLs derived from a model's attribute or method. + + Example: + + class User < ActiveRecord::Base + to_param :name + end + + user = User.find_by(name: 'Fancy Pants') + user.id # => 123 + user.to_param # => "123-fancy-pants" + + *Javan Makhmali* + +* Added `ActiveRecord::Base.no_touching`, which allows ignoring touch on models. + + Example: + + Post.no_touching do + Post.first.touch + end + + *Sam Stephenson*, *Damien Mathieu* + +* Prevent the counter cache from being decremented twice when destroying + a record on a `has_many :through` association. + + Fixes #11079. + + *Dmitry Dedov* + +* Unify boolean type casting for `MysqlAdapter` and `Mysql2Adapter`. + `type_cast` will return `1` for `true` and `0` for `false`. + + Fixes #11119. + + *Adam Williams*, *Yves Senn* + +* Fix bug where `has_one` associaton record update result in crash, when replaced with itself. + + Fixes #12834. + + *Denis Redozubov*, *Sergio Cambra* + +* Log bind variables after they are type casted. This makes it more + transparent what values are actually sent to the database. + + irb(main):002:0> Event.find("im-no-integer") + # Before: ... WHERE "events"."id" = $1 LIMIT 1 [["id", "im-no-integer"]] + # After: ... WHERE "events"."id" = $1 LIMIT 1 [["id", 0]] + + *Yves Senn* + +* Fix uninitialized constant `TransactionState` error when `Marshall.load` is used on an Active Record result. + + Fixes #12790. + + *Jason Ayre* + +* `.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. + + *arthurnn* + +* Callbacks on has_many should access the in memory parent if a inverse_of is set. + + *arthurnn* + +* `ActiveRecord::ConnectionAdapters.string_to_time` respects + string with timezone (e.g. Wed, 04 Sep 2013 20:30:00 JST). + + Fixes #12278. + + *kennyj* + +* Calling `update_attributes` will now throw an `ArgumentError` whenever it + gets a `nil` argument. More specifically, it will throw an error if the + argument that it gets passed does not respond to to `stringify_keys`. + + Example: + + @my_comment.update_attributes(nil) # => raises ArgumentError + + *John Wang* + +* Deprecate `quoted_locking_column` method, which isn't used anywhere. + + *kennyj* + +* Migration dump UUID default functions to schema.rb. + + Fixes #10751. + + *kennyj* + +* Fixed a bug in `ActiveRecord::Associations::CollectionAssociation#find_by_scan` + when using `has_many` association with `:inverse_of` option and UUID primary key. + + Fixes #10450. + + *kennyj* + +* 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. + + *Paul Nikitochkin* + +* Deprecate the delegation of Array bang methods for associations. + To use them, instead first call `#to_a` on the association to access the + array to be acted on. + + *Ben Woosley* + +* `CollectionAssociation#first`/`#last` (e.g. `has_many`) use a `LIMIT`ed + query to fetch results rather than loading the entire collection. + + *Lann Martin* + +* Make possible to run SQLite rake tasks without the `Rails` constant defined. + + *Damien Mathieu* + +* Allow Relation#from to accept other relations with bind values. + + *Ryan Wallace* + +* Fix inserts with prepared statements disabled. + + Fixes #12023. + + *Rafael Mendonça França* + +* Setting a has_one association on a new record no longer causes an empty + transaction. + + *Dylan Thacker-Smith* + +* Fix `AR::Relation#merge` sometimes failing to preserve `readonly(false)` flag. + + *thedarkone* + +* Re-use `order` argument pre-processing for `reorder`. + + *Paul Nikitochkin* + +* Fix PredicateBuilder so polymorphic association keys in `where` clause can + accept objects other than direct descendants of `ActiveRecord::Base` (decorated models, for example). *Mikhail Dieterle* @@ -78,6 +464,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* @@ -129,13 +521,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 @@ -157,27 +549,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* @@ -265,11 +657,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* @@ -417,7 +809,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/Rakefile b/activerecord/Rakefile index cee1dd5aeb..6f8948f987 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -39,8 +39,10 @@ namespace :test do end namespace :db do - task :create => ['mysql:build_databases', 'postgresql:build_databases'] - task :drop => ['mysql:drop_databases', 'postgresql:drop_databases'] + desc 'Build MySQL and PostgreSQL test databases' + task create: ['mysql:build_databases', 'postgresql:build_databases'] + desc 'Drop MySQL and PostgreSQL test databases' + task drop: ['mysql:drop_databases', 'postgresql:drop_databases'] end %w( mysql mysql2 postgresql sqlite3 sqlite3_mem firebird db2 oracle sybase openbase frontbase jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ).each do |adapter| diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 9986ded904..e1c5b05c36 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -24,5 +24,5 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', version s.add_dependency 'activemodel', version - s.add_dependency 'arel', '~> 4.0.0' + s.add_dependency 'arel', '~> 5.0.0' end diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 9d418dacaf..cbac2ef3c6 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 @@ -44,6 +45,7 @@ module ActiveRecord autoload :Migrator, 'active_record/migration' autoload :ModelSchema autoload :NestedAttributes + autoload :NoTouching autoload :Persistence autoload :QueryCache autoload :Querying @@ -145,10 +147,6 @@ module ActiveRecord autoload :MySQLDatabaseTasks, 'active_record/tasks/mysql_database_tasks' autoload :PostgreSQLDatabaseTasks, 'active_record/tasks/postgresql_database_tasks' - - autoload :FirebirdDatabaseTasks, 'active_record/tasks/firebird_database_tasks' - autoload :SqlserverDatabaseTasks, 'active_record/tasks/sqlserver_database_tasks' - autoload :OracleDatabaseTasks, 'active_record/tasks/oracle_database_tasks' end autoload :TestFixtures, 'active_record/fixtures' diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index d075edc159..0d5313956b 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -224,7 +224,7 @@ module ActiveRecord writer_method(name, class_name, mapping, allow_nil, converter) reflection = ActiveRecord::Reflection.create(:composed_of, part_id, nil, options, self) - Reflection.add_reflection self, part_id, reflection + Reflection.add_aggregate_reflection self, part_id, reflection end private 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..02f45731c9 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 @@ -223,7 +226,12 @@ module ActiveRecord # Returns true if inverse association on the given record needs to be set. # This method is redefined by subclasses. def invertible_for?(record) - inverse_reflection_for(record) + foreign_key_for?(record) && inverse_reflection_for(record) + end + + # Returns true if record contains the foreign_key + def foreign_key_for?(record) + record.attributes.has_key? reflection.foreign_key end # This should be implemented to return the values of the relevant key(s) on the owner, 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/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 8eec4f56af..e1fa5225b5 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -50,8 +50,8 @@ module ActiveRecord # Checks whether record is different to the current target, without loading it def different_target?(record) - if record.nil? - owner[reflection.foreign_key] + if record.nil? + owner[reflection.foreign_key] else record.id != owner[reflection.foreign_key] end diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index 34de1a1f32..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: @@ -17,11 +16,17 @@ module ActiveRecord::Associations::Builder end self.extensions = [] - VALID_OPTIONS = [:class_name, :foreign_key, :validate] - - attr_reader :name, :scope, :options + VALID_OPTIONS = [:class_name, :class, :foreign_key, :validate] 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) - mixin = model.generated_feature_methods - define_readers(mixin) - define_writers(mixin) + def self.define_accessors(model, reflection) + mixin = model.generated_association_methods + 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 d97c0e9afd..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_feature_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 228c500f0a..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 # @@ -80,14 +79,13 @@ module ActiveRecord load_target.find(*args) { |*block_args| yield(*block_args) } else if options[:inverse_of] && loaded? - args = args.flatten - raise RecordNotFound, "Couldn't find #{scope.klass.name} without an ID" if args.blank? - + args_flatten = args.flatten + raise RecordNotFound, "Couldn't find #{scope.klass.name} without an ID" if args_flatten.blank? result = find_by_scan(*args) result_size = Array(result).size - if !result || result_size != args.size - scope.raise_record_not_found_exception!(args, result_size, args.size) + if !result || result_size != args_flatten.size + scope.raise_record_not_found_exception!(args_flatten, result_size, args_flatten.size) else result end @@ -153,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. @@ -172,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 @@ -197,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. @@ -528,21 +522,20 @@ module ActiveRecord # * target already loaded # * owner is new record # * target contains new or changed record(s) - # * the first arg is an integer (which indicates the number of records to be returned) def fetch_first_or_last_using_find?(args) if args.first.is_a?(Hash) true else !(loaded? || owner.new_record? || - target.any? { |record| record.new_record? || record.changed? } || - args.first.kind_of?(Integer)) + target.any? { |record| record.new_record? || record.changed? }) end end def include_in_memory?(record) if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection) - owner.send(reflection.through_reflection.name).any? { |source| + assoc = owner.association(reflection.through_reflection.name) + assoc.reader.any? { |source| target = source.send(reflection.source_reflection.name) target.respond_to?(:include?) ? target.include?(record) : target == record } || target.include?(record) @@ -555,14 +548,14 @@ module ActiveRecord # specified, then #find scans the entire collection. def find_by_scan(*args) expects_array = args.first.kind_of?(Array) - ids = args.flatten.compact.map{ |arg| arg.to_i }.uniq + ids = args.flatten.compact.map{ |arg| arg.to_s }.uniq if ids.size == 1 id = ids.first - record = load_target.detect { |r| id == r.id } + record = load_target.detect { |r| id == r.id.to_s } expects_array ? [ record ] : record else - load_target.select { |r| ids.include?(r.id) } + load_target.select { |r| ids.include?(r.id.to_s) } end end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index ea7f768a68..0b37ecf5b7 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, @@ -787,12 +787,12 @@ module ActiveRecord # has_many :pets # end # - # person.pets.count #=> 1 - # person.pets.many? #=> false + # person.pets.count # => 1 + # person.pets.many? # => false # # person.pets << Pet.new(name: 'Snoopy') - # person.pets.count #=> 2 - # person.pets.many? #=> true + # person.pets.count # => 2 + # person.pets.many? # => true # # You can also pass a block to define criteria. The # behavior is the same, it returns true if the collection 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/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 607ed0da46..0a23109b9b 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -32,6 +32,7 @@ module ActiveRecord def insert_record(record, validate = true, raise = false) set_owner_attributes(record) + set_inverse_instance(record) if raise record.save!(:validate => validate) @@ -125,7 +126,11 @@ module ActiveRecord end def foreign_key_present? - owner.attribute_present?(reflection.association_primary_key) + if reflection.klass.primary_key + owner.attribute_present?(reflection.association_primary_key) + else + false + end end end end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index a74dd1cdab..31b8d27892 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -140,7 +140,21 @@ module ActiveRecord case method when :destroy - count = scope.destroy_all.length + if scope.klass.primary_key + count = scope.destroy_all.length + else + scope.to_a.each do |record| + record.run_callbacks :destroy + end + + arel = scope.arel + + stmt = Arel::DeleteManager.new arel.engine + stmt.from scope.klass.arel_table + stmt.wheres = arel.constraints + + count = scope.klass.connection.delete(stmt, 'SQL', scope.bind_values) + end when :nullify count = scope.update_all(source_reflection.foreign_key => nil) else @@ -149,7 +163,7 @@ module ActiveRecord delete_through_records(records) - if source_reflection.options[:counter_cache] + if source_reflection.options[:counter_cache] && method != :destroy counter = source_reflection.counter_cache_column klass.decrement_counter counter, records.map(&:id) end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 3ab1ea1ff4..944caacab6 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -26,15 +26,19 @@ module ActiveRecord load_target return self.target if !(target || record) - if (target != record) || record.changed? + + assigning_another_record = target != record + if assigning_another_record || record.changed? + save &&= owner.persisted? + transaction_if(save) do - remove_target!(options[:dependent]) if target && !target.destroyed? + remove_target!(options[:dependent]) if target && !target.destroyed? && assigning_another_record if record set_owner_attributes(record) set_inverse_instance(record) - if owner.persisted? && save && !record.save + if save && !record.save nullify_owner_attributes(record) set_owner_attributes(target) if target raise RecordNotSaved, "Failed to save the new associated #{reflection.name}." diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 5aa17e5fbb..3e743c4bfe 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. @@ -19,221 +83,191 @@ module ActiveRecord # end # # If I execute `@physician.patients.to_a` then - # base #=> Physician - # associations #=> [] - # joins #=> [#<Arel::Nodes::InnerJoin: ...] + # base # => Physician + # associations # => [] + # joins # => [#<Arel::Nodes::InnerJoin: ...] # # However if I execute `Physician.joins(:appointments).to_a` then - # base #=> Physician - # associations #=> [:appointments] - # joins #=> [] + # base # => Physician + # associations # => [:appointments] + # 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..84e18684d8 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 @@ -143,11 +86,11 @@ module ActiveRecord # end # # If I execute `Physician.joins(:appointments).to_a` then - # reflection #=> #<ActiveRecord::Reflection::AssociationReflection @macro=:has_many ...> - # table #=> #<Arel::Table @name="appointments" ...> - # key #=> physician_id - # foreign_table #=> #<Arel::Table @name="physicians" ...> - # foreign_key #=> id + # reflection # => #<ActiveRecord::Reflection::AssociationReflection @macro=:has_many ...> + # table # => #<Arel::Table @name="appointments" ...> + # key # => physician_id + # foreign_table # => #<Arel::Table @name="physicians" ...> + # foreign_key # => id # def build_constraint(klass, table, key, foreign_table, foreign_key) constraint = table[key].eq(foreign_table[foreign_key]) @@ -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 b534569063..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,33 +46,25 @@ 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 + 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 = {} - # An array of [column_name, alias] pairs for the table - def column_names_with_alias - unless @column_names_with_alias - @column_names_with_alias = [] + index = 0 + length = column_names_with_alias.length - ([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 + while index < length + column_name, alias_name = column_names_with_alias[index] + hash[column_name] = row[alias_name] + index += 1 end - @column_names_with_alias - end - - def extract_record(row) - Hash[column_names_with_alias.map{|cn, an| [cn, row[an]]}] - end - def record_id(row) - row[aliased_primary_key] + hash 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 2317e34bc0..2393667ac8 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -42,12 +42,9 @@ 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 - attr_reader :records, :associations, :preload_scope, :model - # Eager loads the named associations for the given Active Record record(s). # # In this description, 'association name' shall refer to the name passed @@ -82,38 +79,47 @@ module ActiveRecord # [ :books, :author ] # { author: :avatar } # [ :books, { author: :avatar } ] - def initialize(records, associations, preload_scope = nil) - @records = Array.wrap(records).compact.uniq - @associations = Array.wrap(associations) - @preload_scope = preload_scope || Relation.create(nil, nil) - end - def run - unless records.empty? - associations.each { |association| preload(association) } + NULL_RELATION = Struct.new(:values).new({}) + + def preload(records, associations, preload_scope = nil) + records = Array.wrap(records).compact.uniq + associations = Array.wrap(associations) + preload_scope = preload_scope || NULL_RELATION + + if records.empty? + [] + else + associations.flat_map { |association| + preloaders_on association, records, preload_scope + } end end private - def preload(association) + def preloaders_on(association, records, scope) case association when Hash - preload_hash(association) + preloaders_for_hash(association, records, scope) when Symbol - preload_one(association) + preloaders_for_one(association, records, scope) when String - preload_one(association.to_sym) + preloaders_for_one(association.to_sym, records, scope) else raise ArgumentError, "#{association.inspect} was not recognised for preload" end end - def preload_hash(association) - association.each do |parent, child| - Preloader.new(records, parent, preload_scope).run - Preloader.new(records.map { |record| record.send(parent) }.flatten, child).run - end + def preloaders_for_hash(association, records, scope) + parent, child = association.to_a.first # hash should only be of length 1 + + loaders = preloaders_for_one parent, records, scope + + recs = loaders.flat_map(&:preloaded_records).uniq + loaders.concat Array.wrap(child).flat_map { |assoc| + preloaders_on assoc, recs, scope + } end # Not all records have the same class, so group then preload group on the reflection @@ -123,52 +129,81 @@ module ActiveRecord # Additionally, polymorphic belongs_to associations can have multiple associated # classes, depending on the polymorphic_type field. So we group by the classes as # well. - def preload_one(association) - grouped_records(association).each do |reflection, klasses| - klasses.each do |klass, records| - preloader_for(reflection).new(klass, records, reflection, preload_scope).run + def preloaders_for_one(association, records, scope) + grouped_records(association, records).flat_map do |reflection, klasses| + klasses.map do |rhs_klass, rs| + loader = preloader_for(reflection, rs, rhs_klass).new(rhs_klass, rs, reflection, scope) + loader.run self + loader end end end - def grouped_records(association) - Hash[ - records_by_reflection(association).map do |reflection, records| - [reflection, records.group_by { |record| association_klass(reflection, record) }] - end - ] + def grouped_records(association, records) + reflection_records = records_by_reflection(association, records) + + reflection_records.each_with_object({}) do |(reflection, r_records),h| + h[reflection] = r_records.group_by { |record| + association_klass(reflection, record) + } + end end - def records_by_reflection(association) + def records_by_reflection(association, records) records.group_by do |record| - reflection = record.class.reflections[association] + reflection = record.class.reflect_on_association(association) - unless reflection - raise ActiveRecord::ConfigurationError, "Association named '#{association}' was not found; " \ - "perhaps you misspelled it?" - end - - reflection + reflection || raise_config_error(record, association) end end + def raise_config_error(record, association) + raise ActiveRecord::ConfigurationError, + "Association named '#{association}' was not found on #{record.class.name}; " \ + "perhaps you misspelled it?" + end + def association_klass(reflection, record) if reflection.macro == :belongs_to && reflection.options[:polymorphic] - klass = record.send(reflection.foreign_type) + klass = record.read_attribute(reflection.foreign_type.to_s) klass && klass.constantize else reflection.klass end end - def preloader_for(reflection) + class AlreadyLoaded + attr_reader :owners, :reflection + + def initialize(klass, owners, reflection, preload_scope) + @owners = owners + @reflection = reflection + end + + def run(preloader); end + + def preloaded_records + owners.flat_map { |owner| owner.read_attribute reflection.name } + end + end + + class NullPreloader + def self.new(klass, owners, reflection, preload_scope); self; end + def self.run(preloader); end + end + + def preloader_for(reflection, owners, rhs_klass) + return NullPreloader unless rhs_klass + + if owners.first.association(reflection.name).loaded? + return AlreadyLoaded + end + case reflection.macro when :has_many 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/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 928da71eed..69b65982b3 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -3,6 +3,7 @@ module ActiveRecord class Preloader class Association #:nodoc: attr_reader :owners, :reflection, :preload_scope, :model, :klass + attr_reader :preloaded_records def initialize(klass, owners, reflection, preload_scope) @klass = klass @@ -12,15 +13,14 @@ module ActiveRecord @model = owners.first && owners.first.class @scope = nil @owners_by_key = nil + @preloaded_records = [] end - def run - unless owners.first.association(reflection.name).loaded? - preload - end + def run(preloader) + preload(preloader) end - def preload + def preload(preloader) raise NotImplementedError end @@ -68,36 +68,39 @@ module ActiveRecord private - def associated_records_by_owner + def associated_records_by_owner(preloader) owners_map = owners_by_key owner_keys = owners_map.keys.compact # Each record may have multiple owners, and vice-versa - records_by_owner = Hash[owners.map { |owner| [owner, []] }] + records_by_owner = owners.each_with_object({}) do |owner,h| + h[owner] = [] + end - if klass && owner_keys.any? + if owner_keys.any? # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000) # Make several smaller queries if necessary or make one query if the adapter supports it sliced = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size) - sliced.each { |slice| - records = records_for(slice) - caster = type_caster(records, association_key_name) - records.each do |record| - owner_key = caster.call record[association_key_name] - - owners_map[owner_key].each do |owner| - records_by_owner[owner] << record - end + + records = load_slices sliced + records.each do |record, owner_key| + owners_map[owner_key].each do |owner| + records_by_owner[owner] << record end - } + end end records_by_owner end - IDENTITY = lambda { |value| value } - def type_caster(results, name) - IDENTITY + def load_slices(slices) + @preloaded_records = slices.flat_map { |slice| + records_for(slice) + } + + @preloaded_records.map { |record| + [record, record[association_key_name]] + } end def reflection_scope @@ -116,6 +119,14 @@ module ActiveRecord scope.select! preload_values[:select] || values[:select] || table[Arel.star] scope.includes! preload_values[:includes] || values[:includes] + if preload_values.key? :order + scope.order! preload_values[:order] + else + if values.key? :order + scope.order! values[:order] + end + end + if options[:as] scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name }) end diff --git a/activerecord/lib/active_record/associations/preloader/collection_association.rb b/activerecord/lib/active_record/associations/preloader/collection_association.rb index e6cd35e7a1..5adffcd831 100644 --- a/activerecord/lib/active_record/associations/preloader/collection_association.rb +++ b/activerecord/lib/active_record/associations/preloader/collection_association.rb @@ -9,8 +9,8 @@ module ActiveRecord super.order(preload_scope.values[:order] || reflection_scope.values[:order]) end - def preload - associated_records_by_owner.each do |owner, records| + def preload(preloader) + associated_records_by_owner(preloader).each do |owner, records| association = owner.association(reflection.name) association.loaded! association.target.concat(records) 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 c042a44b21..0000000000 --- a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +++ /dev/null @@ -1,65 +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 associated_records_by_owner - records = {} - super.each_value do |rows| - rows.map! { |row| records[row[klass.primary_key]] ||= klass.instantiate(row) } - end - end - - def type_caster(results, name) - caster = results.column_types.fetch(name, results.identity_type) - lambda { |value| caster.type_cast value } - 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/has_many_through.rb b/activerecord/lib/active_record/associations/preloader/has_many_through.rb index 157b627ad5..7b37b5942d 100644 --- a/activerecord/lib/active_record/associations/preloader/has_many_through.rb +++ b/activerecord/lib/active_record/associations/preloader/has_many_through.rb @@ -4,7 +4,7 @@ module ActiveRecord class HasManyThrough < CollectionAssociation #:nodoc: include ThroughAssociation - def associated_records_by_owner + def associated_records_by_owner(preloader) records_by_owner = super if reflection_scope.distinct_value diff --git a/activerecord/lib/active_record/associations/preloader/singular_association.rb b/activerecord/lib/active_record/associations/preloader/singular_association.rb index 44e804d785..2b5cfda8ce 100644 --- a/activerecord/lib/active_record/associations/preloader/singular_association.rb +++ b/activerecord/lib/active_record/associations/preloader/singular_association.rb @@ -5,8 +5,8 @@ module ActiveRecord private - def preload - associated_records_by_owner.each do |owner, associated_records| + def preload(preloader) + associated_records_by_owner(preloader).each do |owner, associated_records| record = associated_records.first association = owner.association(reflection.name) diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 2c625cec04..2a8530af62 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -2,7 +2,6 @@ module ActiveRecord module Associations class Preloader module ThroughAssociation #:nodoc: - def through_reflection reflection.through_reflection end @@ -11,34 +10,68 @@ module ActiveRecord reflection.source_reflection end - def associated_records_by_owner - through_records = through_records_by_owner + def associated_records_by_owner(preloader) + preloader.preload(owners, + through_reflection.name, + through_scope) + + through_records = owners.map do |owner| + association = owner.association through_reflection.name + + [owner, Array(association.reader)] + end + + reset_association owners, through_reflection.name + + middle_records = through_records.map { |(_,rec)| rec }.flatten + + preloaders = preloader.preload(middle_records, + source_reflection.name, + reflection_scope) - Preloader.new(through_records.values.flatten, source_reflection.name, reflection_scope).run + @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 - through_records.each do |owner, records| - records.map! { |r| r.send(source_reflection.name) }.flatten! - records.compact! + 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| + association = r.association source_reflection.name + + association.reader + }.compact + + rhs_records.sort_by { |rhs| record_offset[rhs] } + end + } end private - def through_records_by_owner - Preloader.new(owners, through_reflection.name, through_scope).run - + def reset_association(owners, association_name) should_reset = (through_scope != through_reflection.klass.unscoped) || (reflection.options[:source_type] && through_reflection.collection?) - owners.each_with_object({}) do |owner, h| - association = owner.association through_reflection.name - h[owner] = Array(association.reader) - - # Dont cache the association - we would only be caching a subset - association.reset if should_reset + # Dont cache the association - we would only be caching a subset + if should_reset + owners.each { |owner| + owner.association(association_name).reset + } end end + def through_scope scope = through_reflection.klass.unscoped diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index 10238555f0..02dc464536 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -42,7 +42,6 @@ module ActiveRecord scope.first.tap { |record| set_inverse_instance(record) } end - # Implemented by subclasses def replace(record) raise NotImplementedError, "Subclasses must implement a replace(record) method" end diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index 4f06955406..30fa2c8ba5 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -12,6 +12,9 @@ module ActiveRecord # of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt> # exception is raised. def assign_attributes(new_attributes) + if !new_attributes.respond_to?(:stringify_keys) + raise ArgumentError, "When assigning attributes, you must pass a hash as an argument." + end return if new_attributes.blank? attributes = new_attributes.stringify_keys 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/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index dc2399643c..19e81abba5 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -19,8 +19,7 @@ module ActiveRecord # Attempts to +save+ the record and clears changed attributes if successful. def save(*) if status = super - @previously_changed = changes - @changed_attributes.clear + changes_applied end status end @@ -28,16 +27,14 @@ module ActiveRecord # Attempts to <tt>save!</tt> the record and clears changed attributes if successful. def save!(*) super.tap do - @previously_changed = changes - @changed_attributes.clear + changes_applied end end # <tt>reload</tt> the record and clears changed attributes. def reload(*) super.tap do - @previously_changed.clear - @changed_attributes.clear + reset_changes end end @@ -48,11 +45,11 @@ module ActiveRecord # The attribute already has an unsaved change. if attribute_changed?(attr) - old = @changed_attributes[attr] - @changed_attributes.delete(attr) unless _field_changed?(attr, old, value) + old = changed_attributes[attr] + changed_attributes.delete(attr) unless _field_changed?(attr, old, value) else old = clone_attribute_value(:read_attribute, attr) - @changed_attributes[attr] = old if _field_changed?(attr, old, value) + changed_attributes[attr] = old if _field_changed?(attr, old, value) end # Carry on. diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 1cf3aba41c..c152a246b5 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -109,13 +109,14 @@ module ActiveRecord # We use #[] first as a perf optimization for non-nil values. See https://gist.github.com/jonleighton/3552829. name = attr_name.to_s @attributes_cache[name] || @attributes_cache.fetch(name) { - column = @columns_hash.fetch(name) { - return @attributes.fetch(name) { - if name == 'id' && self.class.primary_key != name - read_attribute(self.class.primary_key) - end - } - } + column = @column_types_override[name] if @column_types_override + column ||= @column_types[name] + + return @attributes.fetch(name) { + if name == 'id' && self.class.primary_key != name + read_attribute(self.class.primary_key) + end + } unless column value = @attributes.fetch(name) { return block_given? ? yield(name) : nil diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 1287de0d0d..d484659190 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -24,6 +24,9 @@ module ActiveRecord # serialized object must be of that class on retrieval or # <tt>SerializationTypeMismatch</tt> will be raised. # + # A notable side effect of serialized attributes is that the model will + # be updated on every save, even if it is not dirty. + # # ==== Parameters # # * +attr_name+ - The field name that should be serialized. @@ -66,6 +69,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 b06add096f..e05e22ebb0 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -18,6 +18,7 @@ require 'arel' require 'active_record/errors' require 'active_record/log_subscriber' require 'active_record/explain_subscriber' +require 'active_record/relation/delegation' module ActiveRecord #:nodoc: # = Active Record @@ -290,8 +291,11 @@ module ActiveRecord #:nodoc: extend Translation extend DynamicMatchers extend Explain + extend Enum + extend Delegation::DelegateCache include Persistence + include NoTouching include ReadonlyAttributes include ModelSchema include Inheritance 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/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index cfdcae7f63..cebe741daa 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -86,7 +86,7 @@ module ActiveRecord end end - # Return the number of threads currently waiting on this + # Returns the number of threads currently waiting on this # queue. def num_waiting synchronize do 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 32244b1755..223eb552e4 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 @@ -346,7 +350,7 @@ module ActiveRecord protected - # Return a subquery for the given key using the join information. + # Returns a subquery for the given key using the join information. def subquery_for(key, select) subselect = select.clone subselect.projections = [key] @@ -377,7 +381,7 @@ module ActiveRecord def sql_for_insert(sql, pk, id_value, sequence_name, binds) [sql, binds] end - + def last_inserted_id(result) row = result.rows.first row && row.first 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/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb new file mode 100644 index 0000000000..7c330a2f25 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -0,0 +1,83 @@ +module ActiveRecord + module ConnectionAdapters + class AbstractAdapter + class SchemaCreation # :nodoc: + def initialize(conn) + @conn = conn + @cache = {} + end + + def accept(o) + m = @cache[o.class] ||= "visit_#{o.class.name.split('::').last}" + send m, o + end + + def visit_AddColumn(o) + sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale) + sql = "ADD #{quote_column_name(o.name)} #{sql_type}" + add_column_options!(sql, column_options(o)) + end + + private + + def visit_AlterTable(o) + sql = "ALTER TABLE #{quote_table_name(o.name)} " + sql << o.adds.map { |col| visit_AddColumn col }.join(' ') + end + + def visit_ColumnDefinition(o) + sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale) + column_sql = "#{quote_column_name(o.name)} #{sql_type}" + add_column_options!(column_sql, column_options(o)) unless o.primary_key? + column_sql + end + + def visit_TableDefinition(o) + create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE " + create_sql << "#{quote_table_name(o.name)} (" + create_sql << o.columns.map { |c| accept c }.join(', ') + create_sql << ") #{o.options}" + create_sql + end + + def column_options(o) + column_options = {} + column_options[:null] = o.null unless o.null.nil? + column_options[:default] = o.default unless o.default.nil? + column_options[:column] = o + column_options[:first] = o.first + column_options[:after] = o.after + column_options + end + + def quote_column_name(name) + @conn.quote_column_name name + end + + def quote_table_name(name) + @conn.quote_table_name name + end + + def type_to_sql(type, limit, precision, scale) + @conn.type_to_sql type.to_sym, limit, precision, scale + end + + def add_column_options!(sql, options) + sql << " DEFAULT #{@conn.quote(options[:default], options[:column])}" if options_include_default?(options) + # must explicitly check for :null to allow change_column to work on migrations + if options[:null] == false + sql << " NOT NULL" + end + if options[:auto_increment] == true + sql << " AUTO_INCREMENT" + end + sql + end + + def options_include_default?(options) + options.include?(:default) && !(options[:null] == false && options[:default].nil?) + end + 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 ba1cb05d2c..8aa1ce5c04 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -4,6 +4,7 @@ require 'bigdecimal/util' require 'active_support/core_ext/benchmark' require 'active_record/connection_adapters/schema_cache' require 'active_record/connection_adapters/abstract/schema_dumper' +require 'active_record/connection_adapters/abstract/schema_creation' require 'monitor' module ActiveRecord @@ -16,6 +17,7 @@ module ActiveRecord autoload_at 'active_record/connection_adapters/abstract/schema_definitions' do autoload :IndexDefinition autoload :ColumnDefinition + autoload :ChangeColumnDefinition autoload :TableDefinition autoload :Table autoload :AlterTable @@ -33,12 +35,14 @@ module ActiveRecord autoload :Quoting autoload :ConnectionPool autoload :QueryCache + autoload :Savepoints end autoload_at 'active_record/connection_adapters/abstract/transaction' do autoload :ClosedTransaction autoload :RealTransaction autoload :SavepointTransaction + autoload :TransactionState end # Active Record supports multiple database systems. AbstractAdapter and @@ -97,90 +101,13 @@ module ActiveRecord @pool = pool @schema_cache = SchemaCache.new self @visitor = nil + @prepared_statements = false end def valid_type?(type) true end - class SchemaCreation - def initialize(conn) - @conn = conn - @cache = {} - end - - def accept(o) - m = @cache[o.class] ||= "visit_#{o.class.name.split('::').last}" - send m, o - end - - def visit_AddColumn(o) - sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale) - sql = "ADD #{quote_column_name(o.name)} #{sql_type}" - add_column_options!(sql, column_options(o)) - end - - private - - def visit_AlterTable(o) - sql = "ALTER TABLE #{quote_table_name(o.name)} " - sql << o.adds.map { |col| visit_AddColumn col }.join(' ') - end - - def visit_ColumnDefinition(o) - sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale) - column_sql = "#{quote_column_name(o.name)} #{sql_type}" - add_column_options!(column_sql, column_options(o)) unless o.primary_key? - column_sql - end - - def visit_TableDefinition(o) - create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE " - create_sql << "#{quote_table_name(o.name)} (" - create_sql << o.columns.map { |c| accept c }.join(', ') - create_sql << ") #{o.options}" - create_sql - end - - def column_options(o) - column_options = {} - column_options[:null] = o.null unless o.null.nil? - column_options[:default] = o.default unless o.default.nil? - column_options[:column] = o - column_options[:first] = o.first - column_options[:after] = o.after - column_options - end - - def quote_column_name(name) - @conn.quote_column_name name - end - - def quote_table_name(name) - @conn.quote_table_name name - end - - def type_to_sql(type, limit, precision, scale) - @conn.type_to_sql type.to_sym, limit, precision, scale - end - - def add_column_options!(sql, options) - sql << " DEFAULT #{@conn.quote(options[:default], options[:column])}" if options_include_default?(options) - # must explicitly check for :null to allow change_column to work on migrations - if options[:null] == false - sql << " NOT NULL" - end - if options[:auto_increment] == true - sql << " AUTO_INCREMENT" - end - sql - end - - def options_include_default?(options) - options.include?(:default) && !(options[:null] == false && options[:default].nil?) - end - end - def schema_creation SchemaCreation.new self end @@ -208,10 +135,11 @@ module ActiveRecord end def unprepared_statement - old, @visitor = @visitor, unprepared_visitor + old_prepared_statements, @prepared_statements = @prepared_statements, false + old_visitor, @visitor = @visitor, unprepared_visitor yield ensure - @visitor = old + @visitor, @prepared_statements = old_visitor, old_prepared_statements end # Returns the human-readable name of the adapter. Use mixed case - one @@ -393,13 +321,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) @@ -421,13 +349,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 @@ -440,6 +369,10 @@ module ActiveRecord # override in derived class ActiveRecord::StatementInvalid.new(message, exception) end + + def without_prepared_statement?(binds) + !@prepared_statements || binds.empty? + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 4b11ea795c..dcbc3466b2 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) @@ -174,6 +176,7 @@ module ActiveRecord @quoted_column_names, @quoted_table_names = {}, {} if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) + @prepared_statements = true @visitor = Arel::Visitors::MySQL.new self else @visitor = unprepared_visitor @@ -193,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 @@ -208,6 +206,12 @@ module ActiveRecord true end + def type_cast(value, column) + return super unless value == true || value == false + + value ? 1 : 0 + end + # MySQL 4 technically support transaction isolation, but it is affected by a bug # where the transaction level gets persisted for the whole session: # @@ -316,39 +320,19 @@ module ActiveRecord def begin_db_transaction execute "BEGIN" - rescue - # Transactions aren't supported end def begin_isolated_db_transaction(isolation) execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}" begin_db_transaction - rescue - # Transactions aren't supported end def commit_db_transaction #:nodoc: execute "COMMIT" - rescue - # Transactions aren't supported end def rollback_db_transaction #:nodoc: execute "ROLLBACK" - rescue - # 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 diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index fb53090edc..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. @@ -203,11 +204,19 @@ module ActiveRecord end end - def new_time(year, mon, mday, hour, min, sec, microsec) + def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil) # Treat 0000-00-00 00:00:00 as nil. return nil if year.nil? || (year == 0 && mon == 0 && mday == 0) - Time.send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil + if offset + time = Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil + return nil unless time + + time -= offset + Base.default_timezone == :utc ? time : time.getlocal + else + Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil + end end def fast_string_to_date(string) @@ -232,7 +241,7 @@ module ActiveRecord time_hash = Date._parse(string) time_hash[:sec_fraction] = microseconds(time_hash) - new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) + new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)) end end 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/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 92796c996e..e790f731ea 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -229,7 +229,7 @@ module ActiveRecord alias exec_without_stmt exec_query - # Returns an ActiveRecord::Result instance. + # Returns an ActiveRecord::Result instance. def select(sql, name = nil, binds = []) exec_query(sql, name) end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 15b5452850..c4dcba0501 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -160,12 +160,6 @@ module ActiveRecord # QUOTING ================================================== - def type_cast(value, column) - return super unless value == true || value == false - - value ? 1 : 0 - end - def quote_string(string) #:nodoc: @connection.quote(string) end @@ -279,11 +273,7 @@ module ActiveRecord end def exec_query(sql, name = 'SQL', binds = []) - # If the configuration sets prepared_statements:false, binds will - # always be empty, since the bind variables will have been already - # substituted and removed from binds by BindVisitor, so this will - # effectively disable prepared statement usage completely. - if binds.empty? + if without_prepared_statement?(binds) result_set, affected_rows = exec_without_stmt(sql, name) else result_set, affected_rows = exec_stmt(sql, name, binds) @@ -472,15 +462,17 @@ module ActiveRecord def begin_db_transaction #:nodoc: exec_query "BEGIN" - rescue Mysql::Error - # Transactions aren't supported end private def exec_stmt(sql, name, binds) cache = {} - log(sql, name, binds) do + type_casted_binds = binds.map { |col, val| + [col, type_cast(val, col)] + } + + log(sql, name, type_casted_binds) do if binds.empty? stmt = @connection.prepare(sql) else @@ -491,7 +483,7 @@ module ActiveRecord end begin - stmt.execute(*binds.map { |col, val| type_cast(val, col) }) + stmt.execute(*type_casted_binds.map { |_, val| val }) rescue Mysql::Error => e # Older versions of MySQL leave the prepared statement in a bad # place when an error occurs. To support older mysql versions, we @@ -563,7 +555,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/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb index ea44e818e5..bf34f2bdae 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb @@ -67,7 +67,7 @@ module ActiveRecord end end - def array_to_string(value, column, adapter, should_be_quoted = false) + def array_to_string(value, column, adapter) casted_values = value.map do |val| if String === val if val == "NULL" 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 527d13b9b0..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 = binds.empty? ? 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/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index e9daa5d7ff..c1f978a081 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -86,8 +86,11 @@ module ActiveRecord case value when Range - return super(value, column) unless /range$/ =~ column.sql_type - PostgreSQLColumn.range_to_string(value) + if /range$/ =~ column.sql_type + PostgreSQLColumn.range_to_string(value) + else + super(value, column) + end when NilClass if column.array && array_member 'NULL' @@ -101,12 +104,21 @@ module ActiveRecord when 'point' then PostgreSQLColumn.point_to_string(value) when 'json' then PostgreSQLColumn.json_to_string(value) else - return super(value, column) unless column.array - PostgreSQLColumn.array_to_string(value, column, self) + if column.array + PostgreSQLColumn.array_to_string(value, column, self) + else + super(value, column) + end end when String - return super(value, column) unless 'bytea' == column.sql_type - { :value => value, :format => 1 } + if 'bytea' == column.sql_type + # Return a bind param hash with format as binary. + # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc + # for more information + { value: value, format: 1 } + else + super(value, column) + end when Hash case column.sql_type when 'hstore' then PostgreSQLColumn.hstore_to_string(value) @@ -114,8 +126,11 @@ module ActiveRecord else super(value, column) end when IPAddr - return super(value, column) unless ['inet','cidr'].include? column.sql_type - PostgreSQLColumn.cidr_to_string(value) + if %w(inet cidr).include? column.sql_type + PostgreSQLColumn.cidr_to_string(value) + else + super(value, column) + end else super(value, column) end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 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 5b9453a579..adeb57d913 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -49,13 +49,17 @@ module ActiveRecord # 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) + if sql_type =~ /\[\]$/ @array = true - super(name, self.class.extract_value_from_default(default), sql_type[0..sql_type.length - 3], null) + super(name, default_value, sql_type[0..sql_type.length - 3], null) else @array = false - super(name, self.class.extract_value_from_default(default), sql_type, null) + super(name, default_value, sql_type, null) end + + @default_function = default if has_default_function?(default_value, default) end # :stopdoc: @@ -144,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 @@ -428,6 +440,7 @@ module ActiveRecord include ReferentialIntegrity include SchemaStatements include DatabaseStatements + include Savepoints # Returns 'PostgreSQL' as adapter name for identification purposes. def adapter_name @@ -439,6 +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.default_function spec end @@ -531,6 +545,7 @@ module ActiveRecord super(connection, logger) if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) + @prepared_statements = true @visitor = Arel::Visitors::PostgreSQL.new self else @visitor = unprepared_visitor @@ -616,12 +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 end @@ -767,27 +776,30 @@ 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) + type_casted_binds = binds.map { |col, val| + [col, type_cast(val, col)] + } + + log(sql, name, type_casted_binds, stmt_key) do + @connection.send_query_prepared(stmt_key, type_casted_binds.map { |_, val| val }) + @connection.block + @connection.get_last_result + end + rescue ActiveRecord::StatementInvalid => e + pgerror = e.original_exception - # 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 # 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 @@ -812,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 df489a5b1f..2cf1015f2c 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 @@ -113,6 +130,7 @@ module ActiveRecord @config = config if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) + @prepared_statements = true @visitor = Arel::Visitors::SQLite.new self else @visitor = unprepared_visitor @@ -123,14 +141,12 @@ module ActiveRecord 'SQLite' end - # Returns true def supports_ddl_transactions? true end - # Returns true if SQLite version is '3.6.8' or greater, false otherwise. def supports_savepoints? - sqlite_version >= '3.6.8' + true end # Returns true, since this connection adapter supports prepared statement @@ -144,7 +160,6 @@ module ActiveRecord true end - # Returns true. def supports_primary_key? #:nodoc: true end @@ -153,7 +168,6 @@ module ActiveRecord true end - # Returns true def supports_add_column? true end @@ -175,16 +189,6 @@ module ActiveRecord @statements.clear end - # Returns true - def supports_count_distinct? #:nodoc: - true - end - - # Returns true - def supports_autoincrement? #:nodoc: - true - end - def supports_index_sort_order? true end @@ -197,20 +201,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' @@ -218,7 +209,6 @@ module ActiveRecord @connection.encoding.to_s end - # Returns true. def supports_explain? true end @@ -291,10 +281,13 @@ module ActiveRecord end def exec_query(sql, name = nil, binds = []) - log(sql, name, binds) do + type_casted_binds = binds.map { |col, val| + [col, type_cast(val, col)] + } - # Don't cache statements without bind values - if binds.empty? + log(sql, name, type_casted_binds) do + # Don't cache statements if they are not prepared + if without_prepared_statement?(binds) stmt = @connection.prepare(sql) cols = stmt.columns records = stmt.to_a @@ -307,9 +300,7 @@ module ActiveRecord stmt = cache[:stmt] cols = cache[:cols] ||= stmt.columns stmt.reset! - stmt.bind_params binds.map { |col, val| - type_cast(val, col) - } + stmt.bind_params type_casted_binds.map { |_, val| val } end ActiveRecord::Result.new(cols, stmt.to_a) @@ -350,18 +341,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 @@ -605,14 +584,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 2b1e997ef4..96b5686ae0 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -91,12 +91,12 @@ module ActiveRecord def initialize_generated_modules super - generated_feature_methods + generated_association_methods end - def generated_feature_methods - @generated_feature_methods ||= begin - mod = const_set(:GeneratedFeatureMethods, Module.new) + def generated_association_methods + @generated_association_methods ||= begin + mod = const_set(:GeneratedAssociationMethods, Module.new) include mod mod end @@ -109,7 +109,7 @@ module ActiveRecord elsif abstract_class? "#{super}(abstract)" elsif !connected? - "#{super}(no database connection)" + "#{super} (call '#{super}.connection' to establish a connection)" elsif table_exists? attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', ' "#{super}(#{attr_list})" @@ -163,19 +163,22 @@ module ActiveRecord # ==== Example: # # Instantiates a single new object # User.new(first_name: 'Jamie') - def initialize(attributes = nil) + def initialize(attributes = nil, options = {}) defaults = self.class.column_defaults.dup defaults.each { |k, v| defaults[k] = v.dup if v.duplicable? } @attributes = self.class.initialize_attributes(defaults) - @columns_hash = self.class.column_types.dup + @column_types_override = nil + @column_types = self.class.column_types init_internals init_changed_attributes ensure_proper_type populate_with_current_scope_attributes - assign_attributes(attributes) if attributes + # +options+ argument is only needed to make protected_attributes gem easier to hook. + # Remove it when we drop support to this gem. + init_attributes(attributes, options) if attributes yield self if block_given? run_callbacks :initialize unless _initialize_callbacks.empty? @@ -193,7 +196,8 @@ module ActiveRecord # post.title # => 'hello world' def init_with(coder) @attributes = self.class.initialize_attributes(coder['attributes']) - @columns_hash = self.class.column_types.merge(coder['column_types'] || {}) + @column_types_override = coder['column_types'] + @column_types = self.class.column_types init_internals @@ -282,7 +286,7 @@ module ActiveRecord def ==(comparison_object) super || comparison_object.instance_of?(self.class) && - id.present? && + id && comparison_object.id == id end alias :eql? :== @@ -310,6 +314,8 @@ module ActiveRecord def <=>(other_object) if other_object.is_a?(self.class) self.to_key <=> other_object.to_key + else + super end end @@ -416,8 +422,6 @@ module ActiveRecord @aggregation_cache = {} @association_cache = {} @attributes_cache = {} - @previously_changed = {} - @changed_attributes = {} @readonly = false @destroyed = false @marked_for_destruction = false @@ -434,8 +438,14 @@ module ActiveRecord # optimistic locking) won't get written unless they get marked as changed self.class.columns.each do |c| attr, orig_value = c.name, c.default - @changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, @attributes[attr]) + changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, @attributes[attr]) end end + + # This method is needed to make protected_attributes gem easier to hook. + # Remove it when we drop support to this gem. + def init_attributes(attributes, options) + assign_attributes(attributes) + end end end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index e1faadf1ab..3aa5faed87 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -35,7 +35,7 @@ module ActiveRecord stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({ arel_table[counter_name] => object.send(association).count - }) + }, primary_key) connection.update stmt end return true diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb new file mode 100644 index 0000000000..5fcc0382d8 --- /dev/null +++ b/activerecord/lib/active_record/enum.rb @@ -0,0 +1,88 @@ +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.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 + # + # In rare circumstances you might need to access the mapping directly. + # The mappings are exposed through a constant with the attributes name: + # + # Conversation::STATUS # => { "active" => 0, "archived" => 1 } + # + # Use that constant when you need to know the ordinal value of an enum: + # + # Conversation.where("status <> ?", Conversation::STATUS[:archived]) + module Enum + def enum(definitions) + klass = self + definitions.each do |name, values| + # DIRECTION = { } + enum_values = _enum_methods_module.const_set name.to_s.upcase, ActiveSupport::HashWithIndifferentAccess.new + name = name.to_sym + + _enum_methods_module.module_eval do + # def direction=(value) self[:direction] = DIRECTION[value] end + define_method("#{name}=") { |value| + unless enum_values.has_key?(value) + raise ArgumentError, "'#{value}' is not a valid #{name}" + end + 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 } + klass.scope value, -> { klass.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 } + end + end + end + end + + def _enum_methods_module + @_enum_methods_module ||= begin + mod = Module.new + include mod + mod + end + end + end +end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 40263b2a70..55423565e8 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -436,9 +436,47 @@ module ActiveRecord cattr_accessor :all_loaded_fixtures self.all_loaded_fixtures = {} + class ClassCache + def initialize(class_names, config) + @class_names = class_names.stringify_keys + @config = config + + # Remove string values that aren't constants or subclasses of AR + @class_names.delete_if { |k,klass| + unless klass.is_a? Class + klass = klass.safe_constantize + ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name will be removed in Rails 4.2, consider using the class itself instead.") + end + !insert_class(@class_names, k, klass) + } + end + + def [](fs_name) + @class_names.fetch(fs_name) { + klass = default_fixture_model(fs_name, @config).safe_constantize + insert_class(@class_names, fs_name, klass) + } + end + + private + + def insert_class(class_names, name, klass) + # We only want to deal with AR objects. + if klass && klass < ActiveRecord::Base + class_names[name] = klass + else + class_names[name] = nil + end + end + + def default_fixture_model(fs_name, config) + ActiveRecord::FixtureSet.default_fixture_model_name(fs_name, config) + end + end + def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {}, config = ActiveRecord::Base) fixture_set_names = Array(fixture_set_names).map(&:to_s) - class_names = class_names.stringify_keys + class_names = ClassCache.new class_names, config # FIXME: Apparently JK uses this. connection = block_given? ? yield : ActiveRecord::Base.connection @@ -452,10 +490,12 @@ module ActiveRecord fixtures_map = {} fixture_sets = files_to_read.map do |fs_name| + klass = class_names[fs_name] + conn = klass ? klass.connection : connection fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new - connection, + conn, fs_name, - class_names[fs_name] || (default_fixture_model_name(fs_name, config).safe_constantize), + klass, ::File.join(fixtures_directory, fs_name)) end @@ -464,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 @@ -500,10 +532,10 @@ module ActiveRecord attr_reader :table_name, :name, :fixtures, :model_class, :config def initialize(connection, name, class_name, path, config = ActiveRecord::Base) - @fixtures = {} # Ordered hash @name = name @path = path @config = config + @model_class = nil if class_name.is_a?(String) ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name will be removed in Rails 4.2, consider using the class itself instead.") @@ -515,14 +547,13 @@ module ActiveRecord @model_class = class_name.safe_constantize if class_name end - @connection = ( model_class.respond_to?(:connection) ? - model_class.connection : connection ) + @connection = connection @table_name = ( model_class.respond_to?(:table_name) ? model_class.table_name : self.class.default_fixture_table_name(name, config) ) - read_fixture_files + @fixtures = read_fixture_files path, @model_class end def [](x) @@ -541,7 +572,17 @@ module ActiveRecord fixtures.size end - # Return a hash of rows to be inserted. The key is the table, the value is + 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 + + # Returns 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 now = config.default_timezone == :utc ? Time.now.utc : Time.now @@ -556,7 +597,7 @@ module ActiveRecord rows[table_name] = fixtures.map do |label, fixture| row = fixture.to_hash - if model_class && model_class < ActiveRecord::Base + if model_class # fill in timestamp columns if they aren't specified and the model is set to record_timestamps if model_class.record_timestamps timestamp_column_names.each do |c_name| @@ -596,8 +637,10 @@ module ActiveRecord row[fk_name] = ActiveRecord::FixtureSet.identify(value) end - when :has_and_belongs_to_many - handle_habtm(rows, row, association) + when :has_many + if association.options[:through] + add_join_records(rows, row, HasManyThroughProxy.new(association)) + end end end end @@ -607,18 +650,46 @@ module ActiveRecord rows end + class ReflectionProxy # :nodoc: + def initialize(association) + @association = association + end + + def join_table + @association.join_table + end + + def name + @association.name + end + end + + class HasManyThroughProxy < ReflectionProxy # :nodoc: + def rhs_key + @association.foreign_key + end + + def lhs_key + @association.through_reflection.foreign_key + end + end + private def primary_key_name @primary_key_name ||= model_class && model_class.primary_key end - def handle_habtm(rows, row, association) + def add_join_records(rows, row, association) + # This is the case when the join table has no fixtures file if (targets = row.delete(association.name.to_s)) - targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) table_name = association.join_table + lhs_key = association.lhs_key + rhs_key = association.rhs_key + + targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) rows[table_name].concat targets.map { |target| - { association.foreign_key => row[primary_key_name], - association.association_foreign_key => ActiveRecord::FixtureSet.identify(target) } + { lhs_key => row[primary_key_name], + rhs_key => ActiveRecord::FixtureSet.identify(target) } } end end @@ -641,12 +712,12 @@ module ActiveRecord @column_names ||= @connection.columns(@table_name).collect { |c| c.name } end - def read_fixture_files - yaml_files = Dir["#{@path}/{**,*}/*.yml"].select { |f| + def read_fixture_files(path, model_class) + yaml_files = Dir["#{path}/{**,*}/*.yml"].select { |f| ::File.file?(f) - } + [yaml_file_path] + } + [yaml_file_path(path)] - yaml_files.each do |file| + yaml_files.each_with_object({}) do |file, fixtures| FixtureSet::File.open(file) do |fh| fh.each do |fixture_name, row| fixtures[fixture_name] = ActiveRecord::Fixture.new(row, model_class) @@ -655,8 +726,8 @@ module ActiveRecord end end - def yaml_file_path - "#{@path}.yml" + def yaml_file_path(path) + "#{path}.yml" end end 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..27576b1e61 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/string/filters' + module ActiveRecord module Integration extend ActiveSupport::Concern @@ -45,10 +47,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}" @@ -56,5 +67,45 @@ module ActiveRecord "#{self.class.model_name.cache_key}/#{id}" end end + + module ClassMethods + # Defines your model's +to_param+ method to generate "pretty" URLs + # using +method_name+, which can be any attribute or method that + # responds to +to_s+. + # + # class User < ActiveRecord::Base + # to_param :name + # end + # + # user = User.find_by(name: 'Fancy Pants') + # user.id # => 123 + # user_path(user) # => "/users/123-fancy-pants" + # + # Values longer than 20 characters will be truncated. The value + # is truncated word by word. + # + # user = User.find_by(name: 'David HeinemeierHansson') + # user.id # => 125 + # user_path(user) # => "/users/125-david" + # + # Because the generated param begins with the record's +id+, it is + # suitable for passing to +find+. In a controller, for example: + # + # params[:id] # => "123-fancy-pants" + # User.find(params[:id]).id # => 123 + def to_param(method_name = nil) + if method_name.nil? + super() + else + define_method :to_param do + if (default = super()) && (result = send(method_name).to_s).present? + "#{default}-#{result.squish.truncate(20, separator: /\s/, omission: nil).parameterize}" + else + default + end + end + end + end + end end end diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 626fe40103..6f54729b3c 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -84,7 +84,10 @@ module ActiveRecord relation.table[self.class.primary_key].eq(id).and( relation.table[lock_col].eq(self.class.quote_value(previous_lock_value, column_for_attribute(lock_col))) ) - ).arel.compile_update(arel_attributes_with_values_for_update(attribute_names)) + ).arel.compile_update( + arel_attributes_with_values_for_update(attribute_names), + self.class.primary_key + ) affected_rows = self.class.connection.update stmt @@ -150,6 +153,7 @@ module ActiveRecord # Quote the column name used for optimistic locking. def quoted_locking_column + ActiveSupport::Deprecation.warn "ActiveRecord::Base.quoted_locking_column is deprecated and will be removed in Rails 4.2 or later." connection.quote_column_name(locking_column) end 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..654ef21b07 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -17,12 +17,14 @@ module ActiveRecord def initialize super - @odd_or_even = false + @odd = false end def render_bind(column, value) if column if column.binary? + # This specifically deals with the PG adapter that casts bytea columns into a Hash. + value = value[:value] if value.is_a?(Hash) value = "<#{value.bytesize} bytes of binary data>" end @@ -60,17 +62,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..a4247fb6f4 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -32,7 +32,11 @@ module ActiveRecord class PendingMigrationError < ActiveRecordError#:nodoc: def initialize - super("Migrations are pending; run 'bin/rake db:migrate RAILS_ENV=#{::Rails.env}' to resolve this issue.") + if defined?(Rails) + super("Migrations are pending; run 'bin/rake db:migrate RAILS_ENV=#{::Rails.env}' to resolve this issue.") + else + super("Migrations are pending; run 'bin/rake db:migrate' to resolve this issue.") + end end end @@ -120,8 +124,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 +633,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 @@ -892,7 +896,7 @@ module ActiveRecord validate(@migrations) - ActiveRecord::SchemaMigration.create_table + Base.connection.initialize_schema_migrations_table end def current_version 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/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index df28451bb7..9d92e747d4 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -335,7 +335,7 @@ module ActiveRecord # the helper methods defined below. Makes it seem like the nested # associations are just regular associations. def generate_association_writer(association_name, type) - generated_feature_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1 + generated_association_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1 if method_defined?(:#{association_name}_attributes=) remove_method(:#{association_name}_attributes=) end diff --git a/activerecord/lib/active_record/no_touching.rb b/activerecord/lib/active_record/no_touching.rb new file mode 100644 index 0000000000..dbf4564ae5 --- /dev/null +++ b/activerecord/lib/active_record/no_touching.rb @@ -0,0 +1,52 @@ +module ActiveRecord + # = Active Record No Touching + module NoTouching + extend ActiveSupport::Concern + + module ClassMethods + # Lets you selectively disable calls to `touch` for the + # duration of a block. + # + # ==== Examples + # ActiveRecord::Base.no_touching do + # Project.first.touch # does nothing + # Message.first.touch # does nothing + # end + # + # Project.no_touching do + # Project.first.touch # does nothing + # Message.first.touch # works, but does not touch the associated project + # end + # + def no_touching(&block) + NoTouching.apply_to(self, &block) + end + end + + class << self + def apply_to(klass) #:nodoc: + klasses.push(klass) + yield + ensure + klasses.pop + end + + def applied_to?(klass) #:nodoc: + klasses.any? { |k| k >= klass } + end + + private + def klasses + Thread.current[:no_touching_classes] ||= [] + end + end + + def no_touching? + NoTouching.applied_to?(self.class) + end + + def touch(*) + super unless no_touching? + end + end +end 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 582006ea7d..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 @@ -383,14 +393,16 @@ module ActiveRecord end @attributes.update(fresh_object.instance_variable_get('@attributes')) - @columns_hash = fresh_object.instance_variable_get('@columns_hash') - @attributes_cache = {} + @column_types = self.class.column_types + @column_types_override = fresh_object.instance_variable_get('@column_types_override') + @attributes_cache = {} self 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. # @@ -433,7 +445,7 @@ module ActiveRecord changes[self.class.locking_column] = increment_lock if locking_enabled? - @changed_attributes.except!(*changes.keys) + changed_attributes.except!(*changes.keys) primary_key = self.class.primary_key self.class.unscoped.where(primary_key => self[primary_key]).update_all(changes) == 1 end 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/railtie.rb b/activerecord/lib/active_record/railtie.rb index 269f9f975b..eef08aea88 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -46,6 +46,7 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.database_configuration = Rails.application.config.database_configuration ActiveRecord::Tasks::DatabaseTasks.migrations_paths = Rails.application.paths['db/migrate'].to_a ActiveRecord::Tasks::DatabaseTasks.fixtures_path = File.join Rails.root, 'test', 'fixtures' + ActiveRecord::Tasks::DatabaseTasks.root = Rails.root if defined?(ENGINE_PATH) && engine = Rails::Engine.find(ENGINE_PATH) if engine.paths['db/migrate'].existent diff --git a/activerecord/lib/active_record/railties/console_sandbox.rb b/activerecord/lib/active_record/railties/console_sandbox.rb index 1a04950898..604a220303 100644 --- a/activerecord/lib/active_record/railties/console_sandbox.rb +++ b/activerecord/lib/active_record/railties/console_sandbox.rb @@ -1,7 +1,5 @@ ActiveRecord::Base.connection.begin_transaction(joinable: false) at_exit do - if ActiveRecord::Base.connection.transaction_open? - ActiveRecord::Base.connection.rollback_transaction - end + ActiveRecord::Base.connection.rollback_transaction end 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 5245fc130a..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 @@ -24,11 +22,11 @@ module ActiveRecord end def self.add_reflection(ar, name, reflection) - if reflection.class == AggregateReflection - ar.aggregate_reflections = ar.aggregate_reflections.merge(name => reflection) - else - ar.reflections = ar.reflections.merge(name => reflection) - end + ar.reflections = ar.reflections.merge(name => reflection) + end + + def self.add_aggregate_reflection(ar, name, reflection) + ar.aggregate_reflections = ar.aggregate_reflections.merge(name => reflection) end # \Reflection enables to interrogate Active Record classes and objects @@ -121,6 +119,7 @@ module ActiveRecord @scope = scope @options = options @active_record = active_record + @klass = options[:class] @plural_name = active_record.pluralize_table_names ? name.to_s.pluralize : name.to_s end @@ -195,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 @@ -207,6 +207,10 @@ module ActiveRecord klass.new(attributes, &block) end + def constructable? # :nodoc: + @constructable + end + def table_name klass.table_name end @@ -250,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! @@ -335,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 @@ -347,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 @@ -373,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. @@ -568,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 << @@ -587,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 4e86e905ed..745c6cf349 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -7,7 +7,7 @@ module ActiveRecord MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group, :order, :joins, :where, :having, :bind, :references, - :extending] + :extending, :unscope] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering, :reverse_order, :distinct, :create_with, :uniq] @@ -71,7 +71,7 @@ module ActiveRecord def update_record(values, id, id_was) # :nodoc: substitutes, binds = substitute_values values - um = @klass.unscoped.where(@klass.arel_table[@klass.primary_key].eq(id_was || id)).arel.compile_update(substitutes) + um = @klass.unscoped.where(@klass.arel_table[@klass.primary_key].eq(id_was || id)).arel.compile_update(substitutes, @klass.primary_key) @klass.connection.update( um, @@ -244,8 +244,13 @@ module ActiveRecord def empty? return @records.empty? if loaded? - c = count(:all) - c.respond_to?(:zero?) ? c.zero? : c.empty? + if limit_value == 0 + true + else + # FIXME: This count is not compatible with #select('authors.*') or other select narrows + c = count + c.respond_to?(:zero?) ? c.zero? : c.empty? + end end # Returns true if there are any records. @@ -507,8 +512,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 @@ -599,8 +603,9 @@ module ActiveRecord preload = preload_values preload += includes_values unless eager_loading? + preloader = ActiveRecord::Associations::Preloader.new preload.each do |associations| - ActiveRecord::Associations::Preloader.new(@records, associations).run + preloader.preload @records, associations end @records.each { |record| record.readonly! } if readonly_value 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 52a538e5b5..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 @@ -161,8 +160,7 @@ module ActiveRecord result = result.map do |attributes| values = klass.initialize_attributes(attributes).values - iter = columns.each - values.map { |value| iter.next.type_cast value } + columns.zip(values).map { |column, value| column.type_cast value } end columns.one? ? result.map!(&:first) : result end @@ -182,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/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index b6f80ac5c7..1e15bddcdf 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -1,8 +1,34 @@ -require 'thread' -require 'thread_safe' +require 'active_support/concern' +require 'active_support/deprecation' module ActiveRecord module Delegation # :nodoc: + module DelegateCache + def relation_delegate_class(klass) # :nodoc: + @relation_delegate_cache[klass] + end + + def initialize_relation_delegate_cache # :nodoc: + @relation_delegate_cache = cache = {} + [ + ActiveRecord::Relation, + ActiveRecord::Associations::CollectionProxy, + ActiveRecord::AssociationRelation + ].each do |klass| + delegate = Class.new(klass) { + include ClassSpecificRelation + } + const_set klass.name.gsub('::', '_'), delegate + cache[klass] = delegate + end + end + + def inherited(child_class) + child_class.initialize_relation_delegate_cache + super + end + end + extend ActiveSupport::Concern # This module creates compiled delegation methods dynamically at runtime, which makes @@ -58,7 +84,7 @@ module ActiveRecord if @klass.respond_to?(method) self.class.delegate_to_scoped_klass(method) scoping { @klass.send(method, *args, &block) } - elsif Array.method_defined?(method) + elsif array_delegable?(method) self.class.delegate method, :to => :to_a to_a.send(method, *args, &block) elsif arel.respond_to?(method) @@ -71,47 +97,39 @@ module ActiveRecord end module ClassMethods # :nodoc: - @@subclasses = ThreadSafe::Cache.new(:initial_capacity => 2) - def create(klass, *args) relation_class_for(klass).new(klass, *args) end private - # Cache the constants in @@subclasses because looking them up via const_get - # make instantiation significantly slower. + def relation_class_for(klass) - if klass && (klass_name = klass.name) - my_cache = @@subclasses.compute_if_absent(self) { ThreadSafe::Cache.new } - # This hash is keyed by klass.name to avoid memory leaks in development mode - my_cache.compute_if_absent(klass_name) do - # Cache#compute_if_absent guarantees that the block will only executed once for the given klass_name - subclass_name = "#{name.gsub('::', '_')}_#{klass_name.gsub('::', '_')}" - - if const_defined?(subclass_name) - const_get(subclass_name) - else - const_set(subclass_name, Class.new(self) { include ClassSpecificRelation }) - end - end - else - ActiveRecord::Relation - end + klass.relation_delegate_class(self) end end def respond_to?(method, include_private = false) - super || Array.method_defined?(method) || + super || array_delegable?(method) || @klass.respond_to?(method, include_private) || arel.respond_to?(method, include_private) end protected + def array_delegable?(method) + defined = Array.method_defined?(method) + if defined && method.to_s.ends_with?('!') + ActiveSupport::Deprecation.warn( + "Association will no longer delegate #{method} to #to_a as of Rails 4.2. You instead must first call #to_a on the association to expose the array to be acted on." + ) + end + defined + end + def method_missing(method, *args, &block) if @klass.respond_to?(method) scoping { @klass.send(method, *args, &block) } - elsif Array.method_defined?(method) + elsif array_delegable?(method) to_a.send(method, *args, &block) elsif arel.respond_to?(method) arel.send(method, *args, &block) diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 0132a02f83..d91d6367a3 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -6,11 +6,12 @@ module ActiveRecord # If no record can be found for all of the listed ids, then RecordNotFound will be raised. If the primary key # is an integer, find by id coerces its arguments using +to_i+. # - # Person.find(1) # returns the object for ID = 1 - # Person.find("1") # returns the object for ID = 1 - # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6) - # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17) - # Person.find([1]) # returns an array for the object with ID = 1 + # Person.find(1) # returns the object for ID = 1 + # Person.find("1") # returns the object for ID = 1 + # Person.find("31-sarah") # returns the object for ID = 31 + # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6) + # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17) + # Person.find([1]) # returns an array for the object with ID = 1 # Person.where("administrator = 1").order("created_on DESC").find(1) # # <tt>ActiveRecord::RecordNotFound</tt> will be raised if one or more ids are not found. @@ -201,7 +202,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 +243,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 +269,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 +301,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 c08158d38b..182b9ed89c 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -58,7 +58,11 @@ module ActiveRecord def merge normal_values.each do |name| value = values[name] - relation.send("#{name}!", *value) unless value.blank? + # The unless clause is here mostly for performance reasons (since the `send` call might be moderately + # expensive), most of the time the value is going to be `nil` or `.blank?`, the only catch is that + # `false.blank?` returns `true`, so there needs to be an extra check so that explicit `false` values + # don't fall through the cracks. + relation.send("#{name}!", *value) unless value.nil? || (value.blank? && false != value) end merge_multi_values @@ -90,7 +94,7 @@ module ActiveRecord []) relation.joins! rest - @relation = join_dependency.join_relation(relation) + @relation = relation.joins join_dependency end end @@ -107,11 +111,11 @@ module ActiveRecord bind_values = filter_binds(lhs_binds, removed) + rhs_binds conn = relation.klass.connection - bviter = bind_values.each.with_index + bv_index = 0 where_values.map! do |node| if Arel::Nodes::Equality === node && Arel::Nodes::BindParam === node.right - (column, _), i = bviter.next - substitute = conn.substitute_at column, i + substitute = conn.substitute_at(bind_values[bv_index].first, bv_index) + bv_index += 1 Arel::Nodes::Equality.new(node.left, substitute) else node diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 9f2a039d94..cacc787eba 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -289,17 +289,7 @@ module ActiveRecord end def order!(*args) # :nodoc: - args.flatten! - validate_order_args(args) - - references = args.grep(String) - references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact! - references!(references) if references.any? - - # if a symbol is given we prepend the quoted table name - args.map! do |arg| - arg.is_a?(Symbol) ? Arel::Nodes::Ascending.new(klass.arel_table[arg]) : arg - end + preprocess_order_args(args) self.order_values += args self @@ -320,8 +310,7 @@ module ActiveRecord end def reorder!(*args) # :nodoc: - args.flatten! - validate_order_args(args) + preprocess_order_args(args) self.reordering_value = true self.order_values = args @@ -352,16 +341,19 @@ 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. + # This method is similar to <tt>except</tt>, but unlike + # <tt>except</tt>, it persists across merges: + # + # User.order('email').merge(User.except(:order)) + # == User.order('email') # - # 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: + # User.order('email').merge(User.unscope(:order)) + # == User.all # - # Post.comments.except(:order) + # This means it can be used in association definitions: + # + # has_many :comments, -> { unscope where: :trashed } # - # will still have an order if it comes from the default_scope on Comment. def unscope(*args) check_if_method_has_arguments!(:unscope, args) spawn.unscope!(*args) @@ -369,6 +361,7 @@ module ActiveRecord def unscope!(*args) # :nodoc: args.flatten! + self.unscope_values += args args.each do |scope| case scope @@ -564,6 +557,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. # @@ -703,7 +708,7 @@ module ActiveRecord # Specifies table from which the records will be fetched. For example: # # Topic.select('title').from('posts') - # #=> SELECT title FROM posts + # # => SELECT title FROM posts # # Can accept other relation objects. For example: # @@ -867,7 +872,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 @@ -893,19 +898,25 @@ module ActiveRecord end def collapse_wheres(arel, wheres) - equalities = wheres.grep(Arel::Nodes::Equality) - - arel.where(Arel::Nodes::And.new(equalities)) unless equalities.empty? - - (wheres - equalities).each do |where| + predicates = wheres.map do |where| + next where if ::Arel::Nodes::Equality === where where = Arel.sql(where) if String === where - arel.where(Arel::Nodes::Grouping.new(where)) + Arel::Nodes::Grouping.new(where) end + + arel.where(Arel::Nodes::And.new(predicates)) if predicates.present? end 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) @@ -926,6 +937,7 @@ module ActiveRecord case opts when Relation name ||= 'subquery' + self.bind_values = opts.bind_values + self.bind_values opts.arel.as(name.to_s) else opts @@ -939,7 +951,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 @@ -961,10 +973,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) } @@ -993,12 +1002,6 @@ module ActiveRecord s.strip! s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC') end - when Symbol - { o => :desc } - when Hash - o.each_with_object({}) do |(field, dir), memo| - memo[field] = (dir == :asc ? :desc : :asc ) - end else o end @@ -1014,17 +1017,6 @@ module ActiveRecord orders.reject!(&:blank?) orders = reverse_sql_order(orders) if reverse_order_value - orders = orders.flat_map do |order| - case order - when Symbol - table[order].asc - when Hash - order.map { |field, dir| table[field].send(dir) } - else - order - end - end - arel.order(*orders) unless orders.empty? end @@ -1036,6 +1028,29 @@ module ActiveRecord end end + def preprocess_order_args(order_args) + order_args.flatten! + validate_order_args(order_args) + + references = order_args.grep(String) + references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact! + references!(references) if references.any? + + # if a symbol is given we prepend the quoted table name + order_args.map! do |arg| + case arg + when Symbol + table[arg].asc + when Hash + arg.map { |field, dir| + table[field].send(dir) + } + else + arg + end + end.flatten! + end + # Checks to make sure that the arguments are not blank. Note that if some # blank-like object were initially passed into the query method, then this # method will not raise an error. diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 253368ae5b..469451e2f4 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 } @@ -79,9 +83,10 @@ module ActiveRecord end def initialize_copy(other) - @columns = columns.dup - @rows = rows.dup - @hash_rows = nil + @columns = columns.dup + @rows = rows.dup + @column_types = column_types.dup + @hash_rows = nil end private @@ -93,7 +98,21 @@ module ActiveRecord # used as keys in ActiveRecord::Base's @attributes hash columns = @columns.map { |c| c.dup.freeze } @rows.map { |row| - Hash[columns.zip(row)] + # In the past we used Hash[columns.zip(row)] + # though elegant, the verbose way is much more efficient + # both time and memory wise cause it avoids a big array allocation + # this method is called a lot and needs to be micro optimised + hash = {} + + index = 0 + length = columns.length + + while index < length + hash[columns[index]] = row[index] + index += 1 + end + + hash } end end diff --git a/activerecord/lib/active_record/runtime_registry.rb b/activerecord/lib/active_record/runtime_registry.rb index 63e6738622..9d605b826a 100644 --- a/activerecord/lib/active_record/runtime_registry.rb +++ b/activerecord/lib/active_record/runtime_registry.rb @@ -13,5 +13,10 @@ module ActiveRecord extend ActiveSupport::PerThreadRegistry attr_accessor :connection_handler, :sql_runtime, :connection_id + + [:connection_handler, :sql_runtime, :connection_id].each do |val| + class_eval %{ def self.#{val}; instance.#{val}; end }, __FILE__, __LINE__ + class_eval %{ def self.#{val}=(x); instance.#{val}=x; end }, __FILE__, __LINE__ + end end end 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 1181cc739e..e055d571ab 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -17,9 +17,19 @@ module ActiveRecord cattr_accessor :ignore_tables @@ignore_tables = [] - def self.dump(connection=ActiveRecord::Base.connection, stream=STDOUT) - new(connection).dump(stream) - stream + class << self + def dump(connection=ActiveRecord::Base.connection, stream=STDOUT, config = ActiveRecord::Base) + new(connection, generate_options(config)).dump(stream) + stream + end + + private + def generate_options(config) + { + table_name_prefix: config.table_name_prefix, + table_name_suffix: config.table_name_suffix + } + end end def dump(stream) @@ -32,10 +42,11 @@ module ActiveRecord private - def initialize(connection) + def initialize(connection, options = {}) @connection = connection @types = @connection.native_database_types @version = Migrator::current_version rescue nil + @options = options end def header(stream) @@ -112,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" @@ -201,7 +213,7 @@ HEADER end def remove_prefix_and_suffix(table) - table.gsub(/^(#{ActiveRecord::Base.table_name_prefix})(.+)(#{ActiveRecord::Base.table_name_suffix})$/, "\\2") + table.gsub(/^(#{@options[:table_name_prefix]})(.+)(#{@options[:table_name_suffix]})$/, "\\2") end end end 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/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index a5d6aad3f0..01fec31544 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -100,11 +100,7 @@ module ActiveRecord elsif default_scopes.any? evaluate_default_scope do default_scopes.inject(relation) do |default_scope, scope| - if !scope.is_a?(Relation) && scope.respond_to?(:call) - default_scope.merge(unscoped { scope.call }) - else - default_scope.merge(scope) - end + default_scope.merge(unscoped { scope.call }) end end end diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index a610f479f2..186a737561 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -15,6 +15,11 @@ module ActiveRecord # You can set custom coder to encode/decode your serialized attributes to/from different formats. # JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+. # + # NOTE - If you are using PostgreSQL specific columns like +hstore+ or +json+ there is no need for + # the serialization provided by +store+. Simply use +store_accessor+ instead to generate + # the accessor methods. Be aware that these columns use a string keyed hash and do not allow access + # using a symbol. + # # Examples: # # class User < ActiveRecord::Base @@ -86,6 +91,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 +109,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 +178,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 5ff594fdca..be7d496d15 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -23,6 +23,7 @@ module ActiveRecord # * +fixtures_path+: a path to fixtures directory. # * +migrations_paths+: a list of paths to directories with migrations. # * +seed_loader+: an object which will load seeds, it needs to respond to the +load_seed+ method. + # * +root+: a path to the root of the application. # # Example usage of +DatabaseTasks+ outside Rails could look as such: # @@ -37,7 +38,7 @@ module ActiveRecord attr_writer :current_config attr_accessor :database_configuration, :migrations_paths, :seed_loader, :db_dir, - :fixtures_path, :env + :fixtures_path, :env, :root LOCAL_HOSTS = ['127.0.0.1', 'localhost'] @@ -145,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/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb index de8b16627e..5688931db2 100644 --- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb @@ -3,7 +3,7 @@ module ActiveRecord class SQLiteDatabaseTasks # :nodoc: delegate :connection, :establish_connection, to: ActiveRecord::Base - def initialize(configuration, root = Rails.root) + def initialize(configuration, root = ActiveRecord::Tasks::DatabaseTasks.root) @configuration, @root = configuration, root end 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/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index b55af692d6..38f37f5c8a 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -48,7 +48,7 @@ module ActiveRecord def build_relation(klass, table, attribute, value) #:nodoc: if reflection = klass.reflect_on_association(attribute) attribute = reflection.foreign_key - value = value.attributes[reflection.primary_key_column.name] + value = value.attributes[reflection.primary_key_column.name] unless value.nil? end column = klass.columns_hash[attribute.to_s] 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/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index dd355e8d0c..595edc6263 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -195,22 +195,20 @@ module ActiveRecord Klass.remove_connection end - test "transaction state is reset after a reconnect" do - skip "in-memory db doesn't allow reconnect" if in_memory_db? - - @connection.begin_transaction - assert @connection.transaction_open? - @connection.reconnect! - assert !@connection.transaction_open? - end - - test "transaction state is reset after a disconnect" do - skip "in-memory db doesn't allow disconnect" if in_memory_db? + unless in_memory_db? + test "transaction state is reset after a reconnect" do + @connection.begin_transaction + assert @connection.transaction_open? + @connection.reconnect! + assert !@connection.transaction_open? + end - @connection.begin_transaction - assert @connection.transaction_open? - @connection.disconnect! - assert !@connection.transaction_open? + test "transaction state is reset after a disconnect" do + @connection.begin_transaction + assert @connection.transaction_open? + @connection.disconnect! + assert !@connection.transaction_open? + end end end end diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb index 1844a2e0dc..a1b41b6991 100644 --- a/activerecord/test/cases/adapters/mysql/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql/connection_test.rb @@ -16,15 +16,15 @@ class MysqlConnectionTest < ActiveRecord::TestCase end end - def test_connect_with_url - run_without_connection do - ar_config = ARTest.connection_config['arunit'] - - skip "This test doesn't work with custom socket location" if ar_config['socket'] - - url = "mysql://#{ar_config["username"]}@localhost/#{ar_config["database"]}" - Klass.establish_connection(url) - assert_equal ar_config['database'], Klass.connection.current_database + unless ARTest.connection_config['arunit']['socket'] + def test_connect_with_url + run_without_connection do + ar_config = ARTest.connection_config['arunit'] + + url = "mysql://#{ar_config["username"]}@localhost/#{ar_config["database"]}" + Klass.establish_connection(url) + assert_equal ar_config['database'], Klass.connection.current_database + end end end @@ -40,6 +40,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase @connection.update('set @@wait_timeout=1') sleep 2 assert !@connection.active? + + # Repair all fixture connections so other tests won't break. + @fixture_connections.each do |c| + c.verify! + end end def test_successful_reconnection_after_timeout_with_manual_reconnect diff --git a/activerecord/test/cases/adapters/mysql/statement_pool_test.rb b/activerecord/test/cases/adapters/mysql/statement_pool_test.rb index 83de90f179..209a0cf464 100644 --- a/activerecord/test/cases/adapters/mysql/statement_pool_test.rb +++ b/activerecord/test/cases/adapters/mysql/statement_pool_test.rb @@ -3,20 +3,20 @@ require 'cases/helper' module ActiveRecord::ConnectionAdapters class MysqlAdapter class StatementPoolTest < ActiveRecord::TestCase - def test_cache_is_per_pid - return skip('must support fork') unless Process.respond_to?(:fork) + if Process.respond_to?(:fork) + def test_cache_is_per_pid + cache = StatementPool.new nil, 10 + cache['foo'] = 'bar' + assert_equal 'bar', cache['foo'] - cache = StatementPool.new nil, 10 - cache['foo'] = 'bar' - assert_equal 'bar', cache['foo'] + pid = fork { + lookup = cache['foo']; + exit!(!lookup) + } - pid = fork { - lookup = cache['foo']; - exit!(!lookup) - } - - Process.waitpid pid - assert $?.success?, 'process should exit successfully' + Process.waitpid pid + assert $?.success?, 'process should exit successfully' + end end end end diff --git a/activerecord/test/cases/adapters/mysql2/boolean_test.rb b/activerecord/test/cases/adapters/mysql2/boolean_test.rb new file mode 100644 index 0000000000..267aa232d9 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb @@ -0,0 +1,91 @@ +require "cases/helper" + +class Mysql2BooleanTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class BooleanType < ActiveRecord::Base + self.table_name = "mysql_booleans" + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table("mysql_booleans") do |t| + t.boolean "archived" + t.string "published", limit: 1 + end + BooleanType.reset_column_information + + @emulate_booleans = ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans + end + + teardown do + emulate_booleans @emulate_booleans + @connection.drop_table "mysql_booleans" + end + + test "column type with emulated booleans" do + emulate_booleans true + + assert_equal :boolean, boolean_column.type + assert_equal :string, string_column.type + end + + test "column type without emulated booleans" do + emulate_booleans false + + assert_equal :integer, boolean_column.type + assert_equal :string, string_column.type + end + + test "test type casting with emulated booleans" do + emulate_booleans true + + boolean = BooleanType.create!(archived: true, published: true) + attributes = boolean.reload.attributes_before_type_cast + + assert_equal 1, attributes["archived"] + assert_equal "1", attributes["published"] + + assert_equal 1, @connection.type_cast(true, boolean_column) + assert_equal 1, @connection.type_cast(true, string_column) + end + + test "test type casting without emulated booleans" do + emulate_booleans false + + boolean = BooleanType.create!(archived: true, published: true) + attributes = boolean.reload.attributes_before_type_cast + + assert_equal 1, attributes["archived"] + assert_equal "1", attributes["published"] + + assert_equal 1, @connection.type_cast(true, boolean_column) + assert_equal 1, @connection.type_cast(true, string_column) + end + + test "with booleans stored as 1 and 0" do + @connection.execute "INSERT INTO mysql_booleans(archived, published) VALUES(1, '1')" + boolean = BooleanType.first + assert_equal true, boolean.archived + assert_equal "1", boolean.published + end + + test "with booleans stored as t" do + @connection.execute "INSERT INTO mysql_booleans(published) VALUES('t')" + boolean = BooleanType.first + assert_equal "t", boolean.published + end + + def boolean_column + BooleanType.columns.find { |c| c.name == 'archived' } + end + + def string_column + BooleanType.columns.find { |c| c.name == 'published' } + end + + def emulate_booleans(value) + ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans = value + BooleanType.reset_column_information + end +end diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index fedd9f603c..8dc1df1851 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 @@ -18,6 +18,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase @connection.update('set @@wait_timeout=1') sleep 2 assert !@connection.active? + + # Repair all fixture connections so other tests won't break. + @fixture_connections.each do |c| + c.verify! + end end def test_successful_reconnection_after_timeout_with_manual_reconnect @@ -72,14 +77,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..90cca7d3e6 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 @@ -88,33 +98,33 @@ module ActiveRecord # you know the incantation to do that. # To restart PostgreSQL 9.1 on OS X, installed via MacPorts, ... # sudo su postgres -c "pg_ctl restart -D /opt/local/var/db/postgresql91/defaultdb/ -m fast" - def test_reconnection_after_actual_disconnection_with_verify - skip "with_manual_interventions is false in configuration" unless ARTest.config['with_manual_interventions'] - - original_connection_pid = @connection.query('select pg_backend_pid()') + if ARTest.config['with_manual_interventions'] + def test_reconnection_after_actual_disconnection_with_verify + original_connection_pid = @connection.query('select pg_backend_pid()') - # Sanity check. - assert @connection.active? + # Sanity check. + assert @connection.active? - puts 'Kill the connection now (e.g. by restarting the PostgreSQL ' + - 'server with the "-m fast" option) and then press enter.' - $stdin.gets + puts 'Kill the connection now (e.g. by restarting the PostgreSQL ' + + 'server with the "-m fast" option) and then press enter.' + $stdin.gets - @connection.verify! + @connection.verify! - assert @connection.active? + assert @connection.active? - # If we get no exception here, then either we re-connected successfully, or - # we never actually got disconnected. - new_connection_pid = @connection.query('select pg_backend_pid()') + # If we get no exception here, then either we re-connected successfully, or + # we never actually got disconnected. + new_connection_pid = @connection.query('select pg_backend_pid()') - assert_not_equal original_connection_pid, new_connection_pid, - "umm -- looks like you didn't break the connection, because we're still " + - "successfully querying with the same connection pid." + assert_not_equal original_connection_pid, new_connection_pid, + "umm -- looks like you didn't break the connection, because we're still " + + "successfully querying with the same connection pid." - # Repair all fixture connections so other tests won't break. - @fixture_connections.each do |c| - c.verify! + # Repair all fixture connections so other tests won't break. + @fixture_connections.each do |c| + c.verify! + end end end diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index 3dbab08a99..01de202d11 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -184,16 +184,6 @@ _SQL assert_equal :text, @first_array.column_for_attribute(:nicknames).type end - def test_data_type_of_range_types - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - assert_equal :daterange, @first_range.column_for_attribute(:date_range).type - assert_equal :numrange, @first_range.column_for_attribute(:num_range).type - assert_equal :tsrange, @first_range.column_for_attribute(:ts_range).type - assert_equal :tstzrange, @first_range.column_for_attribute(:tstz_range).type - assert_equal :int4range, @first_range.column_for_attribute(:int4_range).type - assert_equal :int8range, @first_range.column_for_attribute(:int8_range).type - end - def test_data_type_of_tsvector_types assert_equal :tsvector, @first_tsvector.column_for_attribute(:text_vector).type end @@ -240,57 +230,185 @@ _SQL assert_equal "'text' 'vector'", @first_tsvector.text_vector end - def test_int4range_values - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - assert_equal 1...11, @first_range.int4_range - assert_equal 2...10, @second_range.int4_range - assert_equal 2...Float::INFINITY, @third_range.int4_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int4_range) - assert_nil @empty_range.int4_range - end + if ActiveRecord::Base.connection.supports_ranges? + def test_data_type_of_range_types + assert_equal :daterange, @first_range.column_for_attribute(:date_range).type + assert_equal :numrange, @first_range.column_for_attribute(:num_range).type + assert_equal :tsrange, @first_range.column_for_attribute(:ts_range).type + assert_equal :tstzrange, @first_range.column_for_attribute(:tstz_range).type + assert_equal :int4range, @first_range.column_for_attribute(:int4_range).type + assert_equal :int8range, @first_range.column_for_attribute(:int8_range).type + end - def test_int8range_values - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - assert_equal 10...101, @first_range.int8_range - assert_equal 11...100, @second_range.int8_range - assert_equal 11...Float::INFINITY, @third_range.int8_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int8_range) - assert_nil @empty_range.int8_range - end + def test_int4range_values + assert_equal 1...11, @first_range.int4_range + assert_equal 2...10, @second_range.int4_range + assert_equal 2...Float::INFINITY, @third_range.int4_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int4_range) + assert_nil @empty_range.int4_range + end - def test_daterange_values - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 5), @first_range.date_range - assert_equal Date.new(2012, 1, 3)...Date.new(2012, 1, 4), @second_range.date_range - assert_equal Date.new(2012, 1, 3)...Float::INFINITY, @third_range.date_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.date_range) - assert_nil @empty_range.date_range - end + def test_int8range_values + assert_equal 10...101, @first_range.int8_range + assert_equal 11...100, @second_range.int8_range + assert_equal 11...Float::INFINITY, @third_range.int8_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int8_range) + assert_nil @empty_range.int8_range + end - def test_numrange_values - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - assert_equal BigDecimal.new('0.1')..BigDecimal.new('0.2'), @first_range.num_range - assert_equal BigDecimal.new('0.1')...BigDecimal.new('0.2'), @second_range.num_range - assert_equal BigDecimal.new('0.1')...BigDecimal.new('Infinity'), @third_range.num_range - assert_equal BigDecimal.new('-Infinity')...BigDecimal.new('Infinity'), @fourth_range.num_range - assert_nil @empty_range.num_range - end + def test_daterange_values + assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 5), @first_range.date_range + assert_equal Date.new(2012, 1, 3)...Date.new(2012, 1, 4), @second_range.date_range + assert_equal Date.new(2012, 1, 3)...Float::INFINITY, @third_range.date_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.date_range) + assert_nil @empty_range.date_range + end - def test_tsrange_values - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - tz = ::ActiveRecord::Base.default_timezone - assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)..Time.send(tz, 2011, 1, 1, 14, 30, 0), @first_range.ts_range - assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 1, 1, 14, 30, 0), @second_range.ts_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.ts_range) - assert_nil @empty_range.ts_range - end + def test_numrange_values + assert_equal BigDecimal.new('0.1')..BigDecimal.new('0.2'), @first_range.num_range + assert_equal BigDecimal.new('0.1')...BigDecimal.new('0.2'), @second_range.num_range + assert_equal BigDecimal.new('0.1')...BigDecimal.new('Infinity'), @third_range.num_range + assert_equal BigDecimal.new('-Infinity')...BigDecimal.new('Infinity'), @fourth_range.num_range + assert_nil @empty_range.num_range + end + + def test_tsrange_values + tz = ::ActiveRecord::Base.default_timezone + assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)..Time.send(tz, 2011, 1, 1, 14, 30, 0), @first_range.ts_range + assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 1, 1, 14, 30, 0), @second_range.ts_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.ts_range) + assert_nil @empty_range.ts_range + end + + def test_tstzrange_values + assert_equal Time.parse('2010-01-01 09:30:00 UTC')..Time.parse('2011-01-01 17:30:00 UTC'), @first_range.tstz_range + assert_equal Time.parse('2010-01-01 09:30:00 UTC')...Time.parse('2011-01-01 17:30:00 UTC'), @second_range.tstz_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.tstz_range) + assert_nil @empty_range.tstz_range + end + + def test_create_tstzrange + tstzrange = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2011-02-02 14:30:00 CDT') + range = PostgresqlRange.new(:tstz_range => tstzrange) + assert range.save + assert range.reload + assert_equal range.tstz_range, tstzrange + assert_equal range.tstz_range, Time.parse('2010-01-01 13:30:00 UTC')...Time.parse('2011-02-02 19:30:00 UTC') + end + + def test_update_tstzrange + new_tstzrange = Time.parse('2010-01-01 14:30:00 CDT')...Time.parse('2011-02-02 14:30:00 CET') + @first_range.tstz_range = new_tstzrange + assert @first_range.save + assert @first_range.reload + assert_equal new_tstzrange, @first_range.tstz_range + @first_range.tstz_range = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2010-01-01 13:30:00 +0000') + assert @first_range.save + assert @first_range.reload + assert_nil @first_range.tstz_range + end + + def test_create_tsrange + tz = ::ActiveRecord::Base.default_timezone + tsrange = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0) + range = PostgresqlRange.new(:ts_range => tsrange) + assert range.save + assert range.reload + assert_equal range.ts_range, tsrange + end + + def test_update_tsrange + tz = ::ActiveRecord::Base.default_timezone + new_tsrange = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0) + @first_range.ts_range = new_tsrange + assert @first_range.save + assert @first_range.reload + assert_equal new_tsrange, @first_range.ts_range + @first_range.ts_range = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0) + assert @first_range.save + assert @first_range.reload + assert_nil @first_range.ts_range + end + + def test_create_numrange + numrange = BigDecimal.new('0.5')...BigDecimal.new('1') + range = PostgresqlRange.new(:num_range => numrange) + assert range.save + assert range.reload + assert_equal range.num_range, numrange + end + + def test_update_numrange + new_numrange = BigDecimal.new('0.5')...BigDecimal.new('1') + @first_range.num_range = new_numrange + assert @first_range.save + assert @first_range.reload + assert_equal new_numrange, @first_range.num_range + @first_range.num_range = BigDecimal.new('0.5')...BigDecimal.new('0.5') + assert @first_range.save + assert @first_range.reload + assert_nil @first_range.num_range + end + + def test_create_daterange + daterange = Range.new(Date.new(2012, 1, 1), Date.new(2013, 1, 1), true) + range = PostgresqlRange.new(:date_range => daterange) + assert range.save + assert range.reload + assert_equal range.date_range, daterange + end + + def test_update_daterange + new_daterange = Date.new(2012, 2, 3)...Date.new(2012, 2, 10) + @first_range.date_range = new_daterange + assert @first_range.save + assert @first_range.reload + assert_equal new_daterange, @first_range.date_range + @first_range.date_range = Date.new(2012, 2, 3)...Date.new(2012, 2, 3) + assert @first_range.save + assert @first_range.reload + assert_nil @first_range.date_range + end + + def test_create_int4range + int4range = Range.new(3, 50, true) + range = PostgresqlRange.new(:int4_range => int4range) + assert range.save + assert range.reload + assert_equal range.int4_range, int4range + end + + def test_update_int4range + new_int4range = 6...10 + @first_range.int4_range = new_int4range + assert @first_range.save + assert @first_range.reload + assert_equal new_int4range, @first_range.int4_range + @first_range.int4_range = 3...3 + assert @first_range.save + assert @first_range.reload + assert_nil @first_range.int4_range + end + + def test_create_int8range + int8range = Range.new(30, 50, true) + range = PostgresqlRange.new(:int8_range => int8range) + assert range.save + assert range.reload + assert_equal range.int8_range, int8range + end - def test_tstzrange_values - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - assert_equal Time.parse('2010-01-01 09:30:00 UTC')..Time.parse('2011-01-01 17:30:00 UTC'), @first_range.tstz_range - assert_equal Time.parse('2010-01-01 09:30:00 UTC')...Time.parse('2011-01-01 17:30:00 UTC'), @second_range.tstz_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.tstz_range) - assert_nil @empty_range.tstz_range + def test_update_int8range + new_int8range = 60000...10000000 + @first_range.int8_range = new_int8range + assert @first_range.save + assert @first_range.reload + assert_equal new_int8range, @first_range.int8_range + @first_range.int8_range = 39999...39999 + assert @first_range.save + assert @first_range.reload + assert_nil @first_range.int8_range + end end def test_money_values @@ -306,141 +424,6 @@ _SQL assert_equal(-2.25, column.type_cast("($2.25)")) end - def test_create_tstzrange - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - tstzrange = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2011-02-02 14:30:00 CDT') - range = PostgresqlRange.new(:tstz_range => tstzrange) - assert range.save - assert range.reload - assert_equal range.tstz_range, tstzrange - assert_equal range.tstz_range, Time.parse('2010-01-01 13:30:00 UTC')...Time.parse('2011-02-02 19:30:00 UTC') - end - - def test_update_tstzrange - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - new_tstzrange = Time.parse('2010-01-01 14:30:00 CDT')...Time.parse('2011-02-02 14:30:00 CET') - @first_range.tstz_range = new_tstzrange - assert @first_range.save - assert @first_range.reload - assert_equal new_tstzrange, @first_range.tstz_range - @first_range.tstz_range = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2010-01-01 13:30:00 +0000') - assert @first_range.save - assert @first_range.reload - assert_nil @first_range.tstz_range - end - - def test_create_tsrange - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - tz = ::ActiveRecord::Base.default_timezone - tsrange = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0) - range = PostgresqlRange.new(:ts_range => tsrange) - assert range.save - assert range.reload - assert_equal range.ts_range, tsrange - end - - def test_update_tsrange - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - tz = ::ActiveRecord::Base.default_timezone - new_tsrange = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0) - @first_range.ts_range = new_tsrange - assert @first_range.save - assert @first_range.reload - assert_equal new_tsrange, @first_range.ts_range - @first_range.ts_range = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0) - assert @first_range.save - assert @first_range.reload - assert_nil @first_range.ts_range - end - - def test_create_numrange - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - numrange = BigDecimal.new('0.5')...BigDecimal.new('1') - range = PostgresqlRange.new(:num_range => numrange) - assert range.save - assert range.reload - assert_equal range.num_range, numrange - end - - def test_update_numrange - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - new_numrange = BigDecimal.new('0.5')...BigDecimal.new('1') - @first_range.num_range = new_numrange - assert @first_range.save - assert @first_range.reload - assert_equal new_numrange, @first_range.num_range - @first_range.num_range = BigDecimal.new('0.5')...BigDecimal.new('0.5') - assert @first_range.save - assert @first_range.reload - assert_nil @first_range.num_range - end - - def test_create_daterange - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - daterange = Range.new(Date.new(2012, 1, 1), Date.new(2013, 1, 1), true) - range = PostgresqlRange.new(:date_range => daterange) - assert range.save - assert range.reload - assert_equal range.date_range, daterange - end - - def test_update_daterange - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - new_daterange = Date.new(2012, 2, 3)...Date.new(2012, 2, 10) - @first_range.date_range = new_daterange - assert @first_range.save - assert @first_range.reload - assert_equal new_daterange, @first_range.date_range - @first_range.date_range = Date.new(2012, 2, 3)...Date.new(2012, 2, 3) - assert @first_range.save - assert @first_range.reload - assert_nil @first_range.date_range - end - - def test_create_int4range - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - int4range = Range.new(3, 50, true) - range = PostgresqlRange.new(:int4_range => int4range) - assert range.save - assert range.reload - assert_equal range.int4_range, int4range - end - - def test_update_int4range - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - new_int4range = 6...10 - @first_range.int4_range = new_int4range - assert @first_range.save - assert @first_range.reload - assert_equal new_int4range, @first_range.int4_range - @first_range.int4_range = 3...3 - assert @first_range.save - assert @first_range.reload - assert_nil @first_range.int4_range - end - - def test_create_int8range - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - int8range = Range.new(30, 50, true) - range = PostgresqlRange.new(:int8_range => int8range) - assert range.save - assert range.reload - assert_equal range.int8_range, int8range - end - - def test_update_int8range - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - new_int8range = 60000...10000000 - @first_range.int8_range = new_int8range - assert @first_range.save - assert @first_range.reload - assert_equal new_int8range, @first_range.int8_range - @first_range.int8_range = 39999...39999 - assert @first_range.save - assert @first_range.reload - assert_nil @first_range.int8_range - end - def test_update_tsvector new_text_vector = "'new' 'text' 'vector'" @first_tsvector.text_vector = new_text_vector @@ -589,38 +572,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 - - ActiveRecord::Base.time_zone_aware_attributes = true - ActiveRecord::Base.default_timezone = :utc - - @connection.reconnect! + with_timezone_config default: :utc, aware_attributes: true do + @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..2845413575 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -7,15 +7,13 @@ 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 @connection = ActiveRecord::Base.connection - unless @connection.supports_extensions? - return skip "do not test on PG without hstore" - end - unless @connection.extension_enabled?('hstore') @connection.enable_extension 'hstore' @connection.commit_db_transaction @@ -26,6 +24,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' } @@ -35,173 +34,193 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase @connection.execute 'drop table if exists hstores' end - def test_hstore_included_in_extensions - assert @connection.respond_to?(:extensions), "connection should have a list of extensions" - assert @connection.extensions.include?('hstore'), "extension list should include hstore" - end + if ActiveRecord::Base.connection.supports_extensions? + def test_hstore_included_in_extensions + assert @connection.respond_to?(:extensions), "connection should have a list of extensions" + assert @connection.extensions.include?('hstore'), "extension list should include hstore" + end - def test_disable_enable_hstore - assert @connection.extension_enabled?('hstore') - @connection.disable_extension 'hstore' - assert_not @connection.extension_enabled?('hstore') - @connection.enable_extension 'hstore' - assert @connection.extension_enabled?('hstore') - ensure - # Restore column(s) dropped by `drop extension hstore cascade;` - load_schema - end + def test_disable_enable_hstore + assert @connection.extension_enabled?('hstore') + @connection.disable_extension 'hstore' + assert_not @connection.extension_enabled?('hstore') + @connection.enable_extension 'hstore' + assert @connection.extension_enabled?('hstore') + ensure + # Restore column(s) dropped by `drop extension hstore cascade;` + load_schema + end - def test_column - assert_equal :hstore, @column.type - end + def test_column + assert_equal :hstore, @column.type + end - def test_change_table_supports_hstore - @connection.transaction do - @connection.change_table('hstores') do |t| - t.hstore 'users', default: '' + def test_change_table_supports_hstore + @connection.transaction do + @connection.change_table('hstores') do |t| + t.hstore 'users', default: '' + end + Hstore.reset_column_information + column = Hstore.columns.find { |c| c.name == 'users' } + assert_equal :hstore, column.type + + raise ActiveRecord::Rollback # reset the schema change end + ensure Hstore.reset_column_information - column = Hstore.columns.find { |c| c.name == 'users' } - assert_equal :hstore, column.type + end - raise ActiveRecord::Rollback # reset the schema change + def test_cast_value_on_write + x = Hstore.new tags: {"bool" => true, "number" => 5} + assert_equal({"bool" => "true", "number" => "5"}, x.tags) + x.save + assert_equal({"bool" => "true", "number" => "5"}, x.reload.tags) end - ensure - Hstore.reset_column_information - end - def test_cast_value_on_write - x = Hstore.new tags: {"bool" => true, "number" => 5} - assert_equal({"bool" => "true", "number" => "5"}, x.tags) - x.save - assert_equal({"bool" => "true", "number" => "5"}, x.reload.tags) - end + def test_type_cast_hstore + assert @column - def test_type_cast_hstore - assert @column + data = "\"1\"=>\"2\"" + hash = @column.class.string_to_hstore data + assert_equal({'1' => '2'}, hash) + assert_equal({'1' => '2'}, @column.type_cast(data)) - data = "\"1\"=>\"2\"" - hash = @column.class.string_to_hstore data - assert_equal({'1' => '2'}, hash) - assert_equal({'1' => '2'}, @column.type_cast(data)) + assert_equal({}, @column.type_cast("")) + assert_equal({'key'=>nil}, @column.type_cast('key => NULL')) + assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast(%q(c=>"}", "\"a\""=>"b \"a b"))) + end - assert_equal({}, @column.type_cast("")) - assert_equal({'key'=>nil}, @column.type_cast('key => NULL')) - assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast(%q(c=>"}", "\"a\""=>"b \"a b"))) - end + def test_with_store_accessors + x = Hstore.new(language: "fr", timezone: "GMT") + assert_equal "fr", x.language + assert_equal "GMT", x.timezone - def test_gen1 - assert_equal(%q(" "=>""), @column.class.hstore_to_string({' '=>''})) - end + x.save! + x = Hstore.first + assert_equal "fr", x.language + assert_equal "GMT", x.timezone - def test_gen2 - assert_equal(%q(","=>""), @column.class.hstore_to_string({','=>''})) - end + x.language = "de" + x.save! - def test_gen3 - assert_equal(%q("="=>""), @column.class.hstore_to_string({'='=>''})) - end + x = Hstore.first + assert_equal "de", x.language + assert_equal "GMT", x.timezone + end - def test_gen4 - assert_equal(%q(">"=>""), @column.class.hstore_to_string({'>'=>''})) - end + def test_gen1 + assert_equal(%q(" "=>""), @column.class.hstore_to_string({' '=>''})) + end - def test_parse1 - assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @column.type_cast('a=>null,b=>NuLl,c=>"NuLl",null=>c')) - end + def test_gen2 + assert_equal(%q(","=>""), @column.class.hstore_to_string({','=>''})) + end - def test_parse2 - assert_equal({" " => " "}, @column.type_cast("\\ =>\\ ")) - end + def test_gen3 + assert_equal(%q("="=>""), @column.class.hstore_to_string({'='=>''})) + end - def test_parse3 - assert_equal({"=" => ">"}, @column.type_cast("==>>")) - end + def test_gen4 + assert_equal(%q(">"=>""), @column.class.hstore_to_string({'>'=>''})) + end - def test_parse4 - assert_equal({"=a"=>"q=w"}, @column.type_cast('\=a=>q=w')) - end + def test_parse1 + assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @column.type_cast('a=>null,b=>NuLl,c=>"NuLl",null=>c')) + end - def test_parse5 - assert_equal({"=a"=>"q=w"}, @column.type_cast('"=a"=>q\=w')) - end + def test_parse2 + assert_equal({" " => " "}, @column.type_cast("\\ =>\\ ")) + end - def test_parse6 - assert_equal({"\"a"=>"q>w"}, @column.type_cast('"\"a"=>q>w')) - end + def test_parse3 + assert_equal({"=" => ">"}, @column.type_cast("==>>")) + end - def test_parse7 - assert_equal({"\"a"=>"q\"w"}, @column.type_cast('\"a=>q"w')) - end + def test_parse4 + assert_equal({"=a"=>"q=w"}, @column.type_cast('\=a=>q=w')) + end - def test_rewrite - @connection.execute "insert into hstores (tags) VALUES ('1=>2')" - x = Hstore.first - x.tags = { '"a\'' => 'b' } - assert x.save! - end + def test_parse5 + assert_equal({"=a"=>"q=w"}, @column.type_cast('"=a"=>q\=w')) + end + def test_parse6 + assert_equal({"\"a"=>"q>w"}, @column.type_cast('"\"a"=>q>w')) + end - def test_select - @connection.execute "insert into hstores (tags) VALUES ('1=>2')" - x = Hstore.first - assert_equal({'1' => '2'}, x.tags) - end + def test_parse7 + assert_equal({"\"a"=>"q\"w"}, @column.type_cast('\"a=>q"w')) + end - def test_select_multikey - @connection.execute "insert into hstores (tags) VALUES ('1=>2,2=>3')" - x = Hstore.first - assert_equal({'1' => '2', '2' => '3'}, x.tags) - end + def test_rewrite + @connection.execute "insert into hstores (tags) VALUES ('1=>2')" + x = Hstore.first + x.tags = { '"a\'' => 'b' } + assert x.save! + end - def test_create - assert_cycle('a' => 'b', '1' => '2') - end + def test_select + @connection.execute "insert into hstores (tags) VALUES ('1=>2')" + x = Hstore.first + assert_equal({'1' => '2'}, x.tags) + end - def test_nil - assert_cycle('a' => nil) - end + def test_select_multikey + @connection.execute "insert into hstores (tags) VALUES ('1=>2,2=>3')" + x = Hstore.first + assert_equal({'1' => '2', '2' => '3'}, x.tags) + end - def test_quotes - assert_cycle('a' => 'b"ar', '1"foo' => '2') - end + def test_create + assert_cycle('a' => 'b', '1' => '2') + end - def test_whitespace - assert_cycle('a b' => 'b ar', '1"foo' => '2') - end + def test_nil + assert_cycle('a' => nil) + end - def test_backslash - assert_cycle('a\\b' => 'b\\ar', '1"foo' => '2') - end + def test_quotes + assert_cycle('a' => 'b"ar', '1"foo' => '2') + end - def test_comma - assert_cycle('a, b' => 'bar', '1"foo' => '2') - end + def test_whitespace + assert_cycle('a b' => 'b ar', '1"foo' => '2') + end - def test_arrow - assert_cycle('a=>b' => 'bar', '1"foo' => '2') - end + def test_backslash + assert_cycle('a\\b' => 'b\\ar', '1"foo' => '2') + end - def test_quoting_special_characters - assert_cycle('ca' => 'cà', 'ac' => 'àc') - end + def test_comma + assert_cycle('a, b' => 'bar', '1"foo' => '2') + end - def test_multiline - assert_cycle("a\nb" => "c\nd") + def test_arrow + assert_cycle('a=>b' => 'bar', '1"foo' => '2') + end + + def test_quoting_special_characters + assert_cycle('ca' => 'cà', 'ac' => 'àc') + end + + def test_multiline + assert_cycle("a\nb" => "c\nd") + end end private - def assert_cycle hash - # test creation - x = Hstore.create!(:tags => hash) - x.reload - assert_equal(hash, x.tags) - - # test updating - x = Hstore.create!(:tags => {}) - x.tags = hash - x.save! - x.reload - assert_equal(hash, x.tags) - end + + def assert_cycle(hash) + # test creation + x = Hstore.create!(:tags => hash) + x.reload + assert_equal(hash, x.tags) + + # test updating + x = Hstore.create!(:tags => {}) + x.tags = hash + x.save! + x.reload + assert_equal(hash, x.tags) + end 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/statement_pool_test.rb b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb index f1c4b85126..1497b0abc7 100644 --- a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb +++ b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb @@ -1,38 +1,40 @@ require 'cases/helper' -module ActiveRecord::ConnectionAdapters - class PostgreSQLAdapter < AbstractAdapter - class InactivePGconn - def query(*args) - raise PGError - end +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter < AbstractAdapter + class InactivePGconn + def query(*args) + raise PGError + end - def status - PGconn::CONNECTION_BAD + def status + PGconn::CONNECTION_BAD + end end - end - class StatementPoolTest < ActiveRecord::TestCase - def test_cache_is_per_pid - return skip('must support fork') unless Process.respond_to?(:fork) + class StatementPoolTest < ActiveRecord::TestCase + if Process.respond_to?(:fork) + def test_cache_is_per_pid + cache = StatementPool.new nil, 10 + cache['foo'] = 'bar' + assert_equal 'bar', cache['foo'] - cache = StatementPool.new nil, 10 - cache['foo'] = 'bar' - assert_equal 'bar', cache['foo'] + pid = fork { + lookup = cache['foo']; + exit!(!lookup) + } - pid = fork { - lookup = cache['foo']; - exit!(!lookup) - } - - Process.waitpid pid - assert $?.success?, 'process should exit successfully' - end + Process.waitpid pid + assert $?.success?, 'process should exit successfully' + end + end - def test_dealloc_does_not_raise_on_inactive_connection - cache = StatementPool.new InactivePGconn.new, 10 - cache['foo'] = 'bar' - assert_nothing_raised { cache.clear } + def test_dealloc_does_not_raise_on_inactive_connection + cache = StatementPool.new InactivePGconn.new, 10 + cache['foo'] = 'bar' + assert_nothing_raised { cache.clear } + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb index dbc69a529c..89210866f0 100644 --- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb +++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb @@ -12,10 +12,6 @@ class TimestampTest < ActiveRecord::TestCase end def test_load_infinity_and_beyond - unless current_adapter?(:PostgreSQLAdapter) - return skip("only tested on postgresql") - end - d = Developer.find_by_sql("select 'infinity'::timestamp as updated_at") assert d.first.updated_at.infinite?, 'timestamp should be infinite' @@ -26,10 +22,6 @@ class TimestampTest < ActiveRecord::TestCase end def test_save_infinity_and_beyond - unless current_adapter?(:PostgreSQLAdapter) - return skip("only tested on postgresql") - end - d = Developer.create!(:name => 'aaron', :updated_at => 1.0 / 0.0) assert_equal(1.0 / 0.0, d.updated_at) @@ -85,9 +77,6 @@ class TimestampTest < ActiveRecord::TestCase end def test_bc_timestamp - unless current_adapter?(:PostgreSQLAdapter) - return skip("only tested on postgresql") - end date = Date.new(0) - 1.second Developer.create!(:name => "aaron", :updated_at => date) assert_equal date, Developer.find_by_name("aaron").updated_at @@ -109,5 +98,4 @@ class TimestampTest < ActiveRecord::TestCase end result && result.send(option) end - end diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index b573d48807..3f5d981444 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -12,10 +12,6 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection - unless @connection.supports_extensions? - return skip "do not test on PG without uuid-ossp" - end - unless @connection.extension_enabled?('uuid-ossp') @connection.enable_extension 'uuid-ossp' @connection.commit_db_transaction @@ -24,7 +20,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 @@ -35,32 +31,35 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase @connection.execute 'drop table if exists pg_uuids' end - def test_id_is_uuid - assert_equal :uuid, UUID.columns_hash['id'].type - assert UUID.primary_key - end + if ActiveRecord::Base.connection.supports_extensions? + def test_id_is_uuid + assert_equal :uuid, UUID.columns_hash['id'].type + assert UUID.primary_key + end - def test_id_has_a_default - u = UUID.create - assert_not_nil u.id - end + def test_id_has_a_default + u = UUID.create + assert_not_nil u.id + end - def test_auto_create_uuid - u = UUID.create - u.reload - assert_not_nil u.other_uuid - end + def test_auto_create_uuid + u = UUID.create + u.reload + assert_not_nil u.other_uuid + end - def test_pk_and_sequence_for_uuid_primary_key - pk, seq = @connection.pk_and_sequence_for('pg_uuids') - assert_equal 'id', pk - assert_equal nil, seq - end + def test_pk_and_sequence_for_uuid_primary_key + pk, seq = @connection.pk_and_sequence_for('pg_uuids') + assert_equal 'id', pk + assert_equal nil, seq + end - 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) + def test_schema_dumper_for_uuid_primary_key + schema = StringIO.new + ActiveRecord::SchemaDumper.dump(@connection, schema) + 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 end @@ -73,6 +72,11 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase @connection = ActiveRecord::Base.connection @connection.reconnect! + unless @connection.extension_enabled?('uuid-ossp') + @connection.enable_extension 'uuid-ossp' + @connection.commit_db_transaction + end + @connection.transaction do @connection.create_table('pg_uuids', id: false) do |t| t.primary_key :id, :uuid, default: nil @@ -85,11 +89,60 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase @connection.execute 'drop table if exists pg_uuids' end - def test_id_allows_default_override_via_nil - col_desc = @connection.execute("SELECT pg_get_expr(d.adbin, d.adrelid) as default + if ActiveRecord::Base.connection.supports_extensions? + def test_id_allows_default_override_via_nil + col_desc = @connection.execute("SELECT pg_get_expr(d.adbin, d.adrelid) as default FROM pg_attribute a LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum WHERE a.attname='id' AND a.attrelid = 'pg_uuids'::regclass").first - assert_nil col_desc["default"] + assert_nil col_desc["default"] + end + end +end + +class PostgresqlUUIDTestInverseOf < ActiveRecord::TestCase + class UuidPost < ActiveRecord::Base + self.table_name = 'pg_uuid_posts' + has_many :uuid_comments, inverse_of: :uuid_post + end + + class UuidComment < ActiveRecord::Base + self.table_name = 'pg_uuid_comments' + belongs_to :uuid_post + end + + def setup + @connection = ActiveRecord::Base.connection + @connection.reconnect! + + unless @connection.extension_enabled?('uuid-ossp') + @connection.enable_extension 'uuid-ossp' + @connection.commit_db_transaction + end + + @connection.transaction do + @connection.create_table('pg_uuid_posts', id: :uuid) do |t| + t.string 'title' + end + @connection.create_table('pg_uuid_comments', id: :uuid) do |t| + t.uuid :uuid_post_id, default: 'uuid_generate_v4()' + t.string 'content' + end + end + end + + def teardown + @connection.transaction do + @connection.execute 'drop table if exists pg_uuid_comments' + @connection.execute 'drop table if exists pg_uuid_posts' + end + end + + if ActiveRecord::Base.connection.supports_extensions? + def test_collection_association_with_uuid + post = UuidPost.create! + comment = post.uuid_comments.create! + assert post.uuid_comments.find(comment.id) + end 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/adapters/sqlite3/sqlite3_create_folder_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb index 5a4fe63580..f545fc2011 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb @@ -12,7 +12,7 @@ module ActiveRecord :adapter => 'sqlite3', :timeout => 100 - assert Dir.exists? dir.join('db') + assert Dir.exist? dir.join('db') assert File.exist? dir.join('db/foo.sqlite3') end end diff --git a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb index 2f04c60a9a..fd0044ac05 100644 --- a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb @@ -3,20 +3,21 @@ require 'cases/helper' module ActiveRecord::ConnectionAdapters class SQLite3Adapter class StatementPoolTest < ActiveRecord::TestCase - def test_cache_is_per_pid - return skip('must support fork') unless Process.respond_to?(:fork) + if Process.respond_to?(:fork) + def test_cache_is_per_pid - cache = StatementPool.new nil, 10 - cache['foo'] = 'bar' - assert_equal 'bar', cache['foo'] + cache = StatementPool.new nil, 10 + cache['foo'] = 'bar' + assert_equal 'bar', cache['foo'] - pid = fork { - lookup = cache['foo']; - exit!(!lookup) - } + pid = fork { + lookup = cache['foo']; + exit!(!lookup) + } - Process.waitpid pid - assert $?.success?, 'process should exit successfully' + Process.waitpid pid + assert $?.success?, 'process should exit successfully' + end end end end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 28bf48f4fd..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 } @@ -1172,8 +1179,19 @@ class EagerAssociationTest < ActiveRecord::TestCase } end - test "works in combination with order(:symbol)" do - author = Author.includes(:posts).references(:posts).order(:name).where('posts.title IS NOT NULL').first + test "works in combination with order(:symbol) and reorder(:symbol)" do + author = Author.includes(:posts).references(:posts).order(:name).find_by('posts.title IS NOT NULL') assert_equal authors(:bob), author + + 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 712a770133..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 @@ -605,16 +605,24 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_join_table_alias + # FIXME: `references` has no impact on the aliases generated for the join + # query. The fact that we pass `:developers_projects_join` to `references` + # and that the SQL string contains `developers_projects_join` is merely a + # coincidence. assert_equal( 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 def test_join_with_group + # FIXME: `references` has no impact on the aliases generated for the join + # query. The fact that we pass `:developers_projects_join` to `references` + # and that the SQL string contains `developers_projects_join` is merely a + # coincidence. group = Developer.columns.inject([]) do |g, c| g << "developers.#{c.name}" g << "developers_projects_2.#{c.name}" @@ -624,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 ) @@ -638,12 +646,12 @@ 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 - assert_equal 2, projects(:active_record).well_payed_salary_groups.size + assert_equal 2, projects(:active_record).well_payed_salary_groups.to_a.size assert projects(:active_record).well_payed_salary_groups.all? { |g| g.salary > 10000 } end @@ -710,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 ce51853bf3..359bcfba5f 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -45,6 +45,24 @@ class HasManyAssociationsTest < ActiveRecord::TestCase Client.destroyed_client_ids.clear end + def test_anonymous_has_many + developer = Class.new(ActiveRecord::Base) { + self.table_name = 'developers' + dev = self + + developer_project = Class.new(ActiveRecord::Base) { + self.table_name = 'developers_projects' + belongs_to :developer, :class => dev + } + has_many :developer_projects, :class => developer_project, :foreign_key => 'developer_id' + } + dev = developer.first + named = Developer.find(dev.id) + assert_operator dev.developer_projects.count, :>, 0 + assert_equal named.projects.map(&:id).sort, + dev.developer_projects.map(&:project_id).sort + end + def test_create_from_association_should_respect_default_scope car = Car.create(:name => 'honda') assert_equal 'honda', car.name @@ -62,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') @@ -76,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 @@ -319,6 +343,18 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find(2, 99) } end + def test_find_ids_and_inverse_of + force_signal37_to_load_all_clients_of_firm + + firm = companies(:first_firm) + client = firm.clients_of_firm.find(3) + assert_kind_of Client, client + + client_ary = firm.clients_of_firm.find([3]) + assert_kind_of Array, client_ary + assert_equal client, client_ary.first + end + def test_find_all firm = Firm.all.merge!(:order => "id").first assert_equal 2, firm.clients.where("#{QUOTED_TYPE} = 'Client'").to_a.length @@ -424,6 +460,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal ['id'], posts(:welcome).comments.select(:id).first.attributes.keys end + def test_select_without_foreign_key + assert_equal companies(:first_firm).accounts.first.credit_limit, companies(:first_firm).accounts.select(:credit_limit).first.credit_limit + end + def test_adding force_signal37_to_load_all_clients_of_firm natural = Client.new("name" => "Natural Company") @@ -500,6 +540,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end + def test_inverse_on_before_validate + firm = companies(:first_firm) + assert_queries(1) do + firm.clients_of_firm << Client.new("name" => "Natural Company") + end + end + def test_new_aliased_to_build company = companies(:first_firm) new_client = assert_no_queries { company.clients_of_firm.new("name" => "Another Client") } @@ -738,13 +785,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 @@ -820,7 +867,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 @@ -828,7 +875,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 @@ -1396,15 +1443,17 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end - def test_calling_first_or_last_with_integer_on_association_should_load_association + def test_calling_first_or_last_with_integer_on_association_should_not_load_association firm = companies(:first_firm) + firm.clients.create(:name => 'Foo') + assert !firm.clients.loaded? - assert_queries 1 do + assert_queries 2 do firm.clients.first(2) firm.clients.last(2) end - assert firm.clients.loaded? + assert !firm.clients.loaded? end def test_calling_many_should_count_instead_of_loading_association @@ -1590,6 +1639,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 @@ -1696,4 +1761,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 1, speedometer.minivans.to_a.size, "Only one association should be present:\n#{speedometer.minivans.to_a}" assert_equal 1, speedometer.reload.minivans.to_a.size end + + test "can unscope the default scope of the associated model" do + car = Car.create! + bulb1 = Bulb.create! name: "defaulty", car: car + bulb2 = Bulb.create! name: "other", car: car + + assert_equal [bulb1], car.bulbs + assert_equal [bulb1, bulb2], car.all_bulbs.sort_by(&:id) + end end 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 c00cae5750..47592f312e 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -29,7 +29,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase fixtures :posts, :readers, :people, :comments, :authors, :categories, :taggings, :tags, :owners, :pets, :toys, :jobs, :references, :companies, :members, :author_addresses, :subscribers, :books, :subscriptions, :developers, :categorizations, :essays, - :categories_posts + :categories_posts, :clubs, :memberships # Dummies to force column loads so query counts are clean. def setup @@ -37,6 +37,128 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase Reader.create :person_id => 0, :post_id => 0 end + def test_preload_sti_rhs_class + developers = Developer.includes(:firms).all.to_a + assert_no_queries do + developers.each { |d| d.firms } + end + end + + def test_preload_sti_middle_relation + club = Club.create!(name: 'Aaron cool banana club') + member1 = Member.create!(name: 'Aaron') + member2 = Member.create!(name: 'Cat') + + SuperMembership.create! club: club, member: member1 + CurrentMembership.create! club: club, member: member2 + + club1 = Club.includes(:members).find_by_id club.id + assert_equal [member1, member2].sort_by(&:id), + club1.members.sort_by(&:id) + end + + def make_model(name) + Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } } + end + + def test_ordered_habtm + person_prime = Class.new(ActiveRecord::Base) do + def self.name; 'Person'; end + + has_many :readers + has_many :posts, -> { order('posts.id DESC') }, :through => :readers + end + posts = person_prime.includes(:posts).first.posts + + assert_operator posts.length, :>, 1 + posts.each_cons(2) do |left,right| + assert_operator left.id, :>, right.id + end + end + + def test_singleton_has_many_through + book = make_model "Book" + subscription = make_model "Subscription" + subscriber = make_model "Subscriber" + + subscriber.primary_key = 'nick' + subscription.belongs_to :book, class: book + subscription.belongs_to :subscriber, class: subscriber + + book.has_many :subscriptions, class: subscription + book.has_many :subscribers, through: :subscriptions, class: subscriber + + anonbook = book.first + namebook = Book.find anonbook.id + + assert_operator anonbook.subscribers.count, :>, 0 + anonbook.subscribers.each do |s| + assert_instance_of subscriber, s + end + assert_equal namebook.subscribers.map(&:id).sort, + anonbook.subscribers.map(&:id).sort + end + + def test_no_pk_join_table_append + lesson, _, student = make_no_pk_hm_t + + sicp = lesson.new(:name => "SICP") + ben = student.new(:name => "Ben Bitdiddle") + sicp.students << ben + assert sicp.save! + end + + def test_no_pk_join_table_delete + lesson, lesson_student, student = make_no_pk_hm_t + + sicp = lesson.new(:name => "SICP") + ben = student.new(:name => "Ben Bitdiddle") + louis = student.new(:name => "Louis Reasoner") + sicp.students << ben + sicp.students << louis + assert sicp.save! + + sicp.students.reload + assert_operator lesson_student.count, :>=, 2 + assert_no_difference('student.count') do + assert_difference('lesson_student.count', -2) do + sicp.students.destroy(*student.all.to_a) + end + end + end + + def test_no_pk_join_model_callbacks + lesson, lesson_student, student = make_no_pk_hm_t + + after_destroy_called = false + lesson_student.after_destroy do + after_destroy_called = true + end + + sicp = lesson.new(:name => "SICP") + ben = student.new(:name => "Ben Bitdiddle") + sicp.students << ben + assert sicp.save! + + sicp.students.reload + sicp.students.destroy(*student.all.to_a) + assert after_destroy_called, "after destroy should be called" + end + + def make_no_pk_hm_t + lesson = make_model 'Lesson' + student = make_model 'Student' + + lesson_student = make_model 'LessonStudent' + lesson_student.table_name = 'lessons_students' + + lesson_student.belongs_to :lesson, :class => lesson + lesson_student.belongs_to :student, :class => student + lesson.has_many :lesson_students, :class => lesson_student + lesson.has_many :students, :through => :lesson_students, :class => student + [lesson, lesson_student, student] + end + def test_pk_is_not_required_for_join post = Post.includes(:scategories).first post2 = Post.includes(:categories).first @@ -392,6 +514,15 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal(post.taggings.count, post.taggings_count) end + def test_update_counter_caches_on_destroy + post = posts(:welcome) + tag = post.tags.create!(name: 'doomed') + + assert_difference 'post.reload.taggings_count', -1 do + tag.tagged_posts.destroy(post) + end + end + def test_replace_association assert_queries(4){posts(:welcome);people(:david);people(:michael); posts(:welcome).people(true)} @@ -663,7 +794,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 @@ -963,4 +1094,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 4fdf9a9643..1f78c73f71 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -505,21 +505,48 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_no_queries { company.account = nil } account = Account.find(2) assert_queries { company.account = account } + + assert_no_queries { Firm.new.account = account } end - def test_has_one_assignment_triggers_save_on_change + def test_has_one_assignment_dont_trigger_save_on_change_of_same_object pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?") ship = pirate.build_ship(name: 'old name') ship.save! ship.name = 'new name' assert ship.changed? + assert_queries(1) do + # One query for updating name, not triggering query for updating pirate_id + pirate.ship = ship + end + + assert_equal 'new name', pirate.ship.reload.name + end + + def test_has_one_assignment_triggers_save_on_change_on_replacing_object + pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?") + ship = pirate.build_ship(name: 'old name') + ship.save! + + new_ship = Ship.create(name: 'new name') assert_queries(2) do # One query for updating name and second query for updating pirate_id - pirate.ship = ship + pirate.ship = new_ship end 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/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index de47a576c6..dffee42e7d 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -41,6 +41,11 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase assert_no_match(/WHERE/i, sql) end + def test_join_association_conditions_support_string_and_arel_expressions + assert_equal 0, Author.joins(:welcome_posts_with_comment).count + assert_equal 1, Author.joins(:welcome_posts_with_comments).count + end + def test_join_conditions_allow_nil_associations authors = Author.includes(:essays).where(:essays => {:id => nil}) assert_equal 2, authors.count @@ -65,7 +70,7 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase end def test_find_with_implicit_inner_joins_does_not_set_associations - authors = Author.joins(:posts).select('authors.*') + authors = Author.joins(:posts).select('authors.*').to_a assert !authors.empty?, "expected authors to be non-empty" assert authors.all? { |a| !a.instance_variable_defined?(:@posts) }, "expected no authors to have the @posts association loaded" 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 9b1abc3b81..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 @@ -371,7 +372,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase prev_default_scope = Club.default_scopes [:includes, :preload, :joins, :eager_load].each do |q| - Club.default_scopes = [Club.send(q, :category)] + Club.default_scopes = [proc { Club.send(q, :category) }] assert_equal categories(:general), members(:groucho).reload.club_category end ensure diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index baba55ea43..9c66ed354e 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -92,7 +92,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase def test_set_attributes_without_hash topic = Topic.new - assert_nothing_raised { topic.attributes = '' } + assert_raise(ArgumentError) { topic.attributes = '' } end def test_integers_as_nil 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 c91cf89f6d..cde188f6c3 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::GeneratedFeatureMethods) - assert_equal(Computer::GeneratedFeatureMethods, Computer.generated_feature_methods) - assert(modules.index(Computer.generated_attribute_methods) > modules.index(Computer.generated_feature_methods), - "generated_attribute_methods must be higher in inheritance hierarchy than generated_feature_methods") - assert_not_equal Computer.generated_feature_methods, Post.generated_feature_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,19 +544,55 @@ class BasicsTest < ActiveRecord::TestCase assert_equal [ Topic.find(1) ], [ Topic.find(2).topic ] & [ Topic.find(1) ] end - def test_comparison + 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') + end + + 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 => '') + assert_equal one, two + end + def test_comparison_with_different_objects topic = Topic.create category = Category.create(:name => "comparison") assert_nil topic <=> category end + def test_comparison_with_different_objects_in_array + topic = Topic.create + assert_raises(ArgumentError) do + [1, topic].sort + end + end + def test_readonly_attributes assert_equal Set.new([ 'title' , 'comments_count' ]), ReadonlyTitlePost.readonly_attributes @@ -585,10 +607,24 @@ class BasicsTest < ActiveRecord::TestCase end def test_unicode_column_name + Weird.reset_column_information weird = Weird.create(:なまえ => 'たこ焼き仮面') assert_equal 'たこ焼き仮面', weird.なまえ end + unless current_adapter?(:PostgreSQLAdapter) + def test_respect_internal_encoding + 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 + end + def test_non_valid_identifier_column_name weird = Weird.create('a$b' => 'value') weird.reload @@ -611,12 +647,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 @@ -804,19 +842,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 @@ -1324,6 +1361,35 @@ class BasicsTest < ActiveRecord::TestCase assert_equal 1, post.comments.length end + if Process.respond_to?(:fork) && !in_memory_db? + def test_marshal_between_processes + # Define a new model to ensure there are no caches + if self.class.const_defined?("Post", false) + flunk "there should be no post constant" + end + + self.class.const_set("Post", Class.new(ActiveRecord::Base) { + has_many :comments + }) + + rd, wr = IO.pipe + + ActiveRecord::Base.connection_handler.clear_all_connections! + + fork do + rd.close + post = Post.new + post.comments.build + wr.write Marshal.dump(post) + wr.close + end + + wr.close + assert Marshal.load rd.read + rd.close + end + end + def test_marshalling_new_record_round_trip_with_associations post = Post.new post.comments.build diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index 03aa9fdb27..291751c435 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -23,46 +23,56 @@ module ActiveRecord @listener = LogListener.new @pk = Topic.columns.find { |c| c.primary } ActiveSupport::Notifications.subscribe('sql.active_record', @listener) - - skip_if_prepared_statement_caching_is_not_supported end def teardown ActiveSupport::Notifications.unsubscribe(@listener) end - def test_binds_are_logged - sub = @connection.substitute_at(@pk, 0) - binds = [[@pk, 1]] - sql = "select * from topics where id = #{sub}" + if ActiveRecord::Base.connection.supports_statement_cache? + def test_binds_are_logged + sub = @connection.substitute_at(@pk, 0) + binds = [[@pk, 1]] + sql = "select * from topics where id = #{sub}" - @connection.exec_query(sql, 'SQL', binds) + @connection.exec_query(sql, 'SQL', binds) - message = @listener.calls.find { |args| args[4][:sql] == sql } - assert_equal binds, message[4][:binds] - end + message = @listener.calls.find { |args| args[4][:sql] == sql } + assert_equal binds, message[4][:binds] + end - def test_find_one_uses_binds - Topic.find(1) - binds = [[@pk, 1]] - message = @listener.calls.find { |args| args[4][:binds] == binds } - assert message, 'expected a message with binds' - end + def test_binds_are_logged_after_type_cast + sub = @connection.substitute_at(@pk, 0) + binds = [[@pk, "3"]] + sql = "select * from topics where id = #{sub}" - def test_logs_bind_vars - pk = Topic.columns.find { |x| x.primary } - - payload = { - :name => 'SQL', - :sql => 'select * from topics where id = ?', - :binds => [[pk, 10]] - } - event = ActiveSupport::Notifications::Event.new( - 'foo', - Time.now, - Time.now, - 123, - payload) + @connection.exec_query(sql, 'SQL', binds) + + message = @listener.calls.find { |args| args[4][:sql] == sql } + assert_equal [[@pk, 3]], message[4][:binds] + end + + def test_find_one_uses_binds + Topic.find(1) + binds = [[@pk, 1]] + message = @listener.calls.find { |args| args[4][:binds] == binds } + assert message, 'expected a message with binds' + end + + def test_logs_bind_vars + pk = Topic.columns.find { |x| x.primary } + + payload = { + :name => 'SQL', + :sql => 'select * from topics where id = ?', + :binds => [[pk, 10]] + } + event = ActiveSupport::Notifications::Event.new( + 'foo', + Time.now, + Time.now, + 123, + payload) logger = Class.new(ActiveRecord::LogSubscriber) { attr_reader :debugs @@ -78,12 +88,7 @@ module ActiveRecord logger.sql event assert_match([[pk.name, 10]].inspect, logger.debugs.first) - end - - private - - def skip_if_prepared_statement_caching_is_not_supported - skip('prepared statement caching is not supported') unless @connection.supports_statement_cache? + end end end end diff --git a/activerecord/test/cases/column_test.rb b/activerecord/test/cases/column_test.rb index 3a4f414ae8..2a6d8cc2ab 100644 --- a/activerecord/test/cases/column_test.rb +++ b/activerecord/test/cases/column_test.rb @@ -110,6 +110,14 @@ module ActiveRecord assert_equal 1800, column.type_cast(30.minutes) assert_equal 7200, column.type_cast(2.hours) end + + def test_string_to_time_with_timezone + [:utc, :local].each do |zone| + 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 + end end end end diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb index df17732fff..00667cc52e 100644 --- a/activerecord/test/cases/connection_management_test.rb +++ b/activerecord/test/cases/connection_management_test.rb @@ -26,25 +26,25 @@ module ActiveRecord assert ActiveRecord::Base.connection_handler.active_connections? end - def test_connection_pool_per_pid - return skip('must support fork') unless Process.respond_to?(:fork) + if Process.respond_to?(:fork) + def test_connection_pool_per_pid + object_id = ActiveRecord::Base.connection.object_id - object_id = ActiveRecord::Base.connection.object_id + rd, wr = IO.pipe - rd, wr = IO.pipe + pid = fork { + rd.close + wr.write Marshal.dump ActiveRecord::Base.connection.object_id + wr.close + exit! + } - pid = fork { - rd.close - wr.write Marshal.dump ActiveRecord::Base.connection.object_id wr.close - exit! - } - - wr.close - Process.waitpid pid - assert_not_equal object_id, Marshal.load(rd.read) - rd.close + Process.waitpid pid + assert_not_equal object_id, Marshal.load(rd.read) + rd.close + end end def test_app_delegation 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/disconnected_test.rb b/activerecord/test/cases/disconnected_test.rb index 1fecfd077e..9e268dad74 100644 --- a/activerecord/test/cases/disconnected_test.rb +++ b/activerecord/test/cases/disconnected_test.rb @@ -7,7 +7,6 @@ class TestDisconnectedAdapter < ActiveRecord::TestCase self.use_transactional_fixtures = false def setup - skip "in-memory database mustn't disconnect" if in_memory_db? @connection = ActiveRecord::Base.connection end @@ -17,11 +16,13 @@ class TestDisconnectedAdapter < ActiveRecord::TestCase ActiveRecord::Base.establish_connection(spec) end - test "can't execute statements while disconnected" do - @connection.execute "SELECT count(*) from products" - @connection.disconnect! - assert_raises(ActiveRecord::StatementInvalid) do + unless in_memory_db? + test "can't execute statements while disconnected" do @connection.execute "SELECT count(*) from products" + @connection.disconnect! + assert_raises(ActiveRecord::StatementInvalid) do + @connection.execute "SELECT count(*) from products" + end end end end diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb index f73e449610..1e6ccecfab 100644 --- a/activerecord/test/cases/dup_test.rb +++ b/activerecord/test/cases/dup_test.rb @@ -126,7 +126,7 @@ module ActiveRecord def test_dup_with_default_scope prev_default_scopes = Topic.default_scopes - Topic.default_scopes = [Topic.where(:approved => true)] + Topic.default_scopes = [proc { Topic.where(:approved => true) }] topic = Topic.new(:approved => false) assert !topic.dup.approved?, "should not be overridden by default scopes" ensure diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb new file mode 100644 index 0000000000..35e8a98156 --- /dev/null +++ b/activerecord/test/cases/enum_test.rb @@ -0,0 +1,66 @@ +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 + + test "enum methods are overwritable" do + assert_equal "do publish work...", @book.published! + assert @book.published? + end + + test "direct assignment" do + @book.status = :written + assert @book.written? + end + + test "assign string value" do + @book.status = "written" + assert @book.written? + end + + test "assign non existing value raises an error" do + e = assert_raises(ArgumentError) do + @book.status = :unknown + end + assert_equal "'unknown' is not a valid status", e.message + end + + test "constant to access the mapping" do + assert_equal 0, Book::STATUS[:proposed] + assert_equal 1, Book::STATUS["written"] + assert_equal 2, Book::STATUS[:published] + end +end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 949e994f8d..735f804184 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 @@ -51,6 +52,7 @@ class FinderTest < ActiveRecord::TestCase assert_equal true, Topic.exists?(heading: "The First Topic") assert_equal true, Topic.exists?(:author_name => "Mary", :approved => true) assert_equal true, Topic.exists?(["parent_id = ?", 1]) + assert_equal true, Topic.exists?(id: [1, 9999]) assert_equal false, Topic.exists?(45) assert_equal false, Topic.exists?(Topic.new) @@ -148,14 +150,14 @@ class FinderTest < ActiveRecord::TestCase end def test_find_by_ids_with_limit_and_offset - assert_equal 2, Entrant.all.merge!(:limit => 2).find([1,3,2]).size - assert_equal 1, Entrant.all.merge!(:limit => 3, :offset => 2).find([1,3,2]).size + assert_equal 2, Entrant.limit(2).find([1,3,2]).size + assert_equal 1, Entrant.limit(3).offset(2).find([1,3,2]).size # Also test an edge case: If you have 11 results, and you set a # limit of 3 and offset of 9, then you should find that there # will be only 2 results, regardless of the limit. devs = Developer.all - last_devs = Developer.all.merge!(:limit => 3, :offset => 9).find devs.map(&:id) + last_devs = Developer.limit(3).offset(9).find devs.map(&:id) assert_equal 2, last_devs.size end @@ -308,7 +310,7 @@ class FinderTest < ActiveRecord::TestCase end def test_find_only_some_columns - topic = Topic.all.merge!(:select => "author_name").find(1) + topic = Topic.select("author_name").find(1) assert_raise(ActiveModel::MissingAttributeError) {topic.title} assert_raise(ActiveModel::MissingAttributeError) {topic.title?} assert_nil topic.read_attribute("title") @@ -320,23 +322,23 @@ class FinderTest < ActiveRecord::TestCase end def test_find_on_array_conditions - assert Topic.all.merge!(:where => ["approved = ?", false]).find(1) - assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => ["approved = ?", true]).find(1) } + assert Topic.where(["approved = ?", false]).find(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.where(["approved = ?", true]).find(1) } end def test_find_on_hash_conditions - assert Topic.all.merge!(:where => { :approved => false }).find(1) - assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :approved => true }).find(1) } + assert Topic.where(approved: false).find(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.where(approved: true).find(1) } end def test_find_on_hash_conditions_with_explicit_table_name - assert Topic.all.merge!(:where => { 'topics.approved' => false }).find(1) - assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { 'topics.approved' => true }).find(1) } + assert Topic.where('topics.approved' => false).find(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.where('topics.approved' => true).find(1) } end def test_find_on_hash_conditions_with_hashed_table_name - assert Topic.all.merge!(:where => {:topics => { :approved => false }}).find(1) - assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => {:topics => { :approved => true }}).find(1) } + assert Topic.where(topics: { approved: false }).find(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.where(topics: { approved: true }).find(1) } end def test_find_with_hash_conditions_on_joined_table @@ -346,7 +348,7 @@ class FinderTest < ActiveRecord::TestCase end def test_find_with_hash_conditions_on_joined_table_and_with_range - firms = DependentFirm.all.merge!(:joins => :account, :where => {:name => 'RailsCore', :accounts => { :credit_limit => 55..60 }}) + firms = DependentFirm.joins(:account).where(name: 'RailsCore', accounts: { credit_limit: 55..60 }) assert_equal 1, firms.size assert_equal companies(:rails_core), firms.first end @@ -364,71 +366,71 @@ class FinderTest < ActiveRecord::TestCase end def test_find_on_hash_conditions_with_range - assert_equal [1,2], Topic.all.merge!(:where => { :id => 1..2 }).to_a.map(&:id).sort - assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :id => 2..3 }).find(1) } + assert_equal [1,2], Topic.where(id: 1..2).to_a.map(&:id).sort + assert_raise(ActiveRecord::RecordNotFound) { Topic.where(id: 2..3).find(1) } end def test_find_on_hash_conditions_with_end_exclusive_range - assert_equal [1,2,3], Topic.all.merge!(:where => { :id => 1..3 }).to_a.map(&:id).sort - assert_equal [1,2], Topic.all.merge!(:where => { :id => 1...3 }).to_a.map(&:id).sort - assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :id => 2...3 }).find(3) } + assert_equal [1,2,3], Topic.where(id: 1..3).to_a.map(&:id).sort + assert_equal [1,2], Topic.where(id: 1...3).to_a.map(&:id).sort + assert_raise(ActiveRecord::RecordNotFound) { Topic.where(id: 2...3).find(3) } end def test_find_on_hash_conditions_with_multiple_ranges - assert_equal [1,2,3], Comment.all.merge!(:where => { :id => 1..3, :post_id => 1..2 }).to_a.map(&:id).sort - assert_equal [1], Comment.all.merge!(:where => { :id => 1..1, :post_id => 1..10 }).to_a.map(&:id).sort + assert_equal [1,2,3], Comment.where(id: 1..3, post_id: 1..2).to_a.map(&:id).sort + assert_equal [1], Comment.where(id: 1..1, post_id: 1..10).to_a.map(&:id).sort end def test_find_on_hash_conditions_with_array_of_integers_and_ranges - assert_equal [1,2,3,5,6,7,8,9], Comment.all.merge!(:where => {:id => [1..2, 3, 5, 6..8, 9]}).to_a.map(&:id).sort + assert_equal [1,2,3,5,6,7,8,9], Comment.where(id: [1..2, 3, 5, 6..8, 9]).to_a.map(&:id).sort end def test_find_on_multiple_hash_conditions - assert Topic.all.merge!(:where => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => false }).find(1) - assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }).find(1) } - assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :author_name => "David", :title => "HHC", :replies_count => 1, :approved => false }).find(1) } - assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }).find(1) } + assert Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: false).find(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: true).find(1) } + assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "HHC", replies_count: 1, approved: false).find(1) } + assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: true).find(1) } end def test_condition_interpolation assert_kind_of Firm, Company.where("name = '%s'", "37signals").first - assert_nil Company.all.merge!(:where => ["name = '%s'", "37signals!"]).first - assert_nil Company.all.merge!(:where => ["name = '%s'", "37signals!' OR 1=1"]).first - assert_kind_of Time, Topic.all.merge!(:where => ["id = %d", 1]).first.written_on + assert_nil Company.where(["name = '%s'", "37signals!"]).first + assert_nil Company.where(["name = '%s'", "37signals!' OR 1=1"]).first + assert_kind_of Time, Topic.where(["id = %d", 1]).first.written_on end def test_condition_array_interpolation - assert_kind_of Firm, Company.all.merge!(:where => ["name = '%s'", "37signals"]).first - assert_nil Company.all.merge!(:where => ["name = '%s'", "37signals!"]).first - assert_nil Company.all.merge!(:where => ["name = '%s'", "37signals!' OR 1=1"]).first - assert_kind_of Time, Topic.all.merge!(:where => ["id = %d", 1]).first.written_on + assert_kind_of Firm, Company.where(["name = '%s'", "37signals"]).first + assert_nil Company.where(["name = '%s'", "37signals!"]).first + assert_nil Company.where(["name = '%s'", "37signals!' OR 1=1"]).first + assert_kind_of Time, Topic.where(["id = %d", 1]).first.written_on end def test_condition_hash_interpolation - assert_kind_of Firm, Company.all.merge!(:where => { :name => "37signals"}).first - assert_nil Company.all.merge!(:where => { :name => "37signals!"}).first - assert_kind_of Time, Topic.all.merge!(:where => {:id => 1}).first.written_on + assert_kind_of Firm, Company.where(name: "37signals").first + assert_nil Company.where(name: "37signals!").first + assert_kind_of Time, Topic.where(id: 1).first.written_on end def test_hash_condition_find_malformed assert_raise(ActiveRecord::StatementInvalid) { - Company.all.merge!(:where => { :id => 2, :dhh => true }).first + Company.where(id: 2, dhh: true).first } end def test_hash_condition_find_with_escaped_characters Company.create("name" => "Ain't noth'n like' \#stuff") - assert Company.all.merge!(:where => { :name => "Ain't noth'n like' \#stuff" }).first + assert Company.where(name: "Ain't noth'n like' \#stuff").first end def test_hash_condition_find_with_array - p1, p2 = Post.all.merge!(:limit => 2, :order => 'id asc').to_a - assert_equal [p1, p2], Post.all.merge!(:where => { :id => [p1, p2] }, :order => 'id asc').to_a - assert_equal [p1, p2], Post.all.merge!(:where => { :id => [p1, p2.id] }, :order => 'id asc').to_a + p1, p2 = Post.limit(2).order('id asc').to_a + assert_equal [p1, p2], Post.where(id: [p1, p2]).order('id asc').to_a + assert_equal [p1, p2], Post.where(id: [p1, p2.id]).order('id asc').to_a end def test_hash_condition_find_with_nil - topic = Topic.all.merge!(:where => { :last_read => nil } ).first + topic = Topic.where(last_read: nil).first assert_not_nil topic assert_nil topic.last_read end @@ -477,61 +479,61 @@ 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 + assert_equal topic, Topic.where(['written_on = ?', topic.written_on.getutc]).first end end end 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 + assert_equal topic, Topic.where(written_on: topic.written_on.getutc).first end end end 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 + assert_equal topic, Topic.where(['written_on = ?', topic.written_on.getlocal]).first end end end 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 + assert_equal topic, Topic.where(written_on: topic.written_on.getlocal).first end end end def test_bind_variables - assert_kind_of Firm, Company.all.merge!(:where => ["name = ?", "37signals"]).first - assert_nil Company.all.merge!(:where => ["name = ?", "37signals!"]).first - assert_nil Company.all.merge!(:where => ["name = ?", "37signals!' OR 1=1"]).first - assert_kind_of Time, Topic.all.merge!(:where => ["id = ?", 1]).first.written_on + assert_kind_of Firm, Company.where(["name = ?", "37signals"]).first + assert_nil Company.where(["name = ?", "37signals!"]).first + assert_nil Company.where(["name = ?", "37signals!' OR 1=1"]).first + assert_kind_of Time, Topic.where(["id = ?", 1]).first.written_on assert_raise(ActiveRecord::PreparedStatementInvalid) { - Company.all.merge!(:where => ["id=? AND name = ?", 2]).first + Company.where(["id=? AND name = ?", 2]).first } assert_raise(ActiveRecord::PreparedStatementInvalid) { - Company.all.merge!(:where => ["id=?", 2, 3, 4]).first + Company.where(["id=?", 2, 3, 4]).first } end def test_bind_variables_with_quotes Company.create("name" => "37signals' go'es agains") - assert Company.all.merge!(:where => ["name = ?", "37signals' go'es agains"]).first + assert Company.where(["name = ?", "37signals' go'es agains"]).first end def test_named_bind_variables_with_quotes Company.create("name" => "37signals' go'es agains") - assert Company.all.merge!(:where => ["name = :name", {:name => "37signals' go'es agains"}]).first + assert Company.where(["name = :name", {name: "37signals' go'es agains"}]).first end def test_bind_arity @@ -549,10 +551,10 @@ class FinderTest < ActiveRecord::TestCase assert_nothing_raised { bind("'+00:00'", :foo => "bar") } - assert_kind_of Firm, Company.all.merge!(:where => ["name = :name", { :name => "37signals" }]).first - assert_nil Company.all.merge!(:where => ["name = :name", { :name => "37signals!" }]).first - assert_nil Company.all.merge!(:where => ["name = :name", { :name => "37signals!' OR 1=1" }]).first - assert_kind_of Time, Topic.all.merge!(:where => ["id = :id", { :id => 1 }]).first.written_on + assert_kind_of Firm, Company.where(["name = :name", { name: "37signals" }]).first + assert_nil Company.where(["name = :name", { name: "37signals!" }]).first + assert_nil Company.where(["name = :name", { name: "37signals!' OR 1=1" }]).first + assert_kind_of Time, Topic.where(["id = :id", { id: 1 }]).first.written_on end class SimpleEnumerable @@ -734,10 +736,9 @@ class FinderTest < ActiveRecord::TestCase end def test_find_all_with_join - developers_on_project_one = Developer.all.merge!( - :joins => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id', - :where => 'project_id=1' - ).to_a + developers_on_project_one = Developer. + joins('LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id'). + where('project_id=1').to_a assert_equal 3, developers_on_project_one.length developer_names = developers_on_project_one.map { |d| d.name } assert developer_names.include?('David') @@ -745,20 +746,17 @@ class FinderTest < ActiveRecord::TestCase end def test_joins_dont_clobber_id - first = Firm.all.merge!( - :joins => 'INNER JOIN companies clients ON clients.firm_id = companies.id', - :where => 'companies.id = 1' - ).first + first = Firm. + joins('INNER JOIN companies clients ON clients.firm_id = companies.id'). + where('companies.id = 1').first assert_equal 1, first.id end def test_joins_with_string_array - person_with_reader_and_post = Post.all.merge!( - :joins => [ - "INNER JOIN categorizations ON categorizations.post_id = posts.id", - "INNER JOIN categories ON categories.id = categorizations.category_id AND categories.type = 'SpecialCategory'" - ] - ) + person_with_reader_and_post = Post. + joins(["INNER JOIN categorizations ON categorizations.post_id = posts.id", + "INNER JOIN categories ON categories.id = categorizations.category_id AND categories.type = 'SpecialCategory'" + ]) assert_equal 1, person_with_reader_and_post.size end @@ -783,9 +781,9 @@ class FinderTest < ActiveRecord::TestCase end def test_find_by_records - p1, p2 = Post.all.merge!(:limit => 2, :order => 'id asc').to_a - assert_equal [p1, p2], Post.all.merge!(:where => ['id in (?)', [p1, p2]], :order => 'id asc') - assert_equal [p1, p2], Post.all.merge!(:where => ['id in (?)', [p1, p2.id]], :order => 'id asc') + p1, p2 = Post.limit(2).order('id asc').to_a + assert_equal [p1, p2], Post.where(['id in (?)', [p1, p2]]).order('id asc') + assert_equal [p1, p2], Post.where(['id in (?)', [p1, p2.id]]).order('id asc') end def test_select_value @@ -812,34 +810,35 @@ class FinderTest < ActiveRecord::TestCase end def test_find_with_order_on_included_associations_with_construct_finder_sql_for_association_limiting_and_is_distinct - assert_equal 2, Post.all.merge!(:includes => { :authors => :author_address }, :order => 'author_addresses.id DESC ', :limit => 2).to_a.size + assert_equal 2, Post.includes(authors: :author_address).order('author_addresses.id DESC ').limit(2).to_a.size - assert_equal 3, Post.all.merge!(:includes => { :author => :author_address, :authors => :author_address}, - :order => 'author_addresses_authors.id DESC ', :limit => 3).to_a.size + assert_equal 3, Post.includes(author: :author_address, authors: :author_address). + order('author_addresses_authors.id DESC ').limit(3).to_a.size end def test_find_with_nil_inside_set_passed_for_one_attribute - client_of = Company.all.merge!( - :where => { - :client_of => [2, 1, nil], - :name => ['37signals', 'Summit', 'Microsoft'] }, - :order => 'client_of DESC' - ).map { |x| x.client_of } + client_of = Company. + where(client_of: [2, 1, nil], + name: ['37signals', 'Summit', 'Microsoft']). + order('client_of DESC'). + map { |x| x.client_of } assert client_of.include?(nil) assert_equal [2, 1].sort, client_of.compact.sort end def test_find_with_nil_inside_set_passed_for_attribute - client_of = Company.all.merge!( - :where => { :client_of => [nil] }, - :order => 'client_of DESC' - ).map { |x| x.client_of } + client_of = Company. + where(client_of: [nil]). + order('client_of DESC'). + map { |x| x.client_of } assert_equal [], client_of.compact end def test_with_limiting_with_custom_select + skip 'broken test' if current_adapter?(:MysqlAdapter) + posts = Post.references(:authors).merge( :includes => :author, :select => ' posts.*, authors.id as "author_id"', :limit => 3, :order => 'posts.id' @@ -859,8 +858,14 @@ 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 } + assert_nothing_raised(ActiveRecord::StatementInvalid) { Topic.offset("3").to_a } end protected diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 4e877aed2e..f3a4887a85 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -84,6 +84,12 @@ class FixturesTest < ActiveRecord::TestCase assert fixtures.detect { |f| f.name == 'collections' }, "no fixtures named 'collections' in #{fixtures.map(&:name).inspect}" end + def test_create_symbol_fixtures_is_deprecated + assert_deprecated do + ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT, :collections, :collections => 'Course') { Course.connection } + end + end + def test_attributes topics = create_fixtures("topics").first assert_equal("The First Topic", topics["first"]["title"]) @@ -247,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 @@ -260,6 +267,43 @@ 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 + +class HasManyThroughFixture < ActiveSupport::TestCase + def make_model(name) + Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } } + end + + def test_has_many_through + pt = make_model "ParrotTreasure" + parrot = make_model "Parrot" + treasure = make_model "Treasure" + + pt.table_name = "parrots_treasures" + pt.belongs_to :parrot, :class => parrot + pt.belongs_to :treasure, :class => treasure + + parrot.has_many :parrot_treasures, :class => pt + parrot.has_many :treasures, :through => :parrot_treasures + + parrots = File.join FIXTURES_ROOT, 'parrots' + + fs = ActiveRecord::FixtureSet.new parrot.connection, "parrots", parrot, parrots + rows = fs.table_rows + assert_equal load_has_and_belongs_to_many['parrots_treasures'], rows['parrots_treasures'] + end + + def load_has_and_belongs_to_many + parrot = make_model "Parrot" + parrot.has_and_belongs_to_many :treasures + + parrots = File.join FIXTURES_ROOT, 'parrots' + + fs = ActiveRecord::FixtureSet.new parrot.connection, "parrots", parrot, parrots + fs.table_rows end end @@ -580,20 +624,24 @@ class FixturesBrokenRollbackTest < ActiveRecord::TestCase end class LoadAllFixturesTest < ActiveRecord::TestCase - self.fixture_path = FIXTURES_ROOT + "/all" - fixtures :all - def test_all_there + self.class.fixture_path = FIXTURES_ROOT + "/all" + self.class.fixtures :all + assert_equal %w(admin/accounts admin/users developers people tasks), fixture_table_names.sort + ensure + ActiveRecord::FixtureSet.reset_cache end end class LoadAllFixturesWithPathnameTest < ActiveRecord::TestCase - self.fixture_path = Pathname.new(FIXTURES_ROOT).join('all') - fixtures :all - def test_all_there + self.class.fixture_path = Pathname.new(FIXTURES_ROOT).join('all') + self.class.fixtures :all + assert_equal %w(admin/accounts admin/users developers people tasks), fixture_table_names.sort + ensure + ActiveRecord::FixtureSet.reset_cache end end diff --git a/activerecord/test/cases/forbidden_attributes_protection_test.rb b/activerecord/test/cases/forbidden_attributes_protection_test.rb index 490b599fb6..981a75faf6 100644 --- a/activerecord/test/cases/forbidden_attributes_protection_test.rb +++ b/activerecord/test/cases/forbidden_attributes_protection_test.rb @@ -61,4 +61,9 @@ class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase assert_equal 'Guille', person.first_name assert_equal 'm', person.gender end + + def test_blank_attributes_should_not_raise + person = Person.new + assert_nil person.assign_attributes(ProtectedParams.new({})) + 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..07ffcef875 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 @@ -22,18 +23,53 @@ class IntegrationTest < ActiveRecord::TestCase assert_equal '1', client.to_param end - def test_cache_key_for_existing_record_is_not_timezone_dependent - ActiveRecord::Base.time_zone_aware_attributes = true + def test_to_param_class_method + firm = Firm.find(4) + assert_equal '4-flamboyant-software', firm.to_param + end - Time.zone = 'UTC' - utc_key = Developer.first.cache_key + def test_to_param_class_method_truncates + firm = Firm.find(4) + firm.name = 'a ' * 100 + assert_equal '4-a-a-a-a-a-a-a-a-a', firm.to_param + end + + def test_to_param_class_method_truncates_edge_case + firm = Firm.find(4) + firm.name = 'David HeinemeierHansson' + assert_equal '4-david', firm.to_param + end + + def test_to_param_class_method_squishes + firm = Firm.find(4) + firm.name = "ab \n" * 100 + assert_equal '4-ab-ab-ab-ab-ab-ab', firm.to_param + end + + def test_to_param_class_method_uses_default_if_blank + firm = Firm.find(4) + firm.name = nil + assert_equal '4', firm.to_param + firm.name = ' ' + assert_equal '4', firm.to_param + end - Time.zone = 'EST' - est_key = Developer.first.cache_key + def test_to_param_class_method_uses_default_if_not_persisted + firm = Firm.new(name: 'Fancy Shirts') + assert_equal nil, firm.to_param + end - assert_equal utc_key, est_key - ensure - Time.zone = 'UTC' + def test_to_param_with_no_arguments + assert_equal 'Firm', Firm.to_param + end + + def test_cache_key_for_existing_record_is_not_timezone_dependent + utc_key = Developer.first.cache_key + + 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 +122,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/invalid_connection_test.rb b/activerecord/test/cases/invalid_connection_test.rb index f2d8f18ec7..f6774d7ef4 100644 --- a/activerecord/test/cases/invalid_connection_test.rb +++ b/activerecord/test/cases/invalid_connection_test.rb @@ -17,6 +17,6 @@ class TestAdapterWithInvalidConnection < ActiveRecord::TestCase end test "inspect on Model class does not raise" do - assert_equal "#{Bird.name}(no database connection)", Bird.inspect + assert_equal "#{Bird.name} (call '#{Bird.name}.connection' to establish a connection)", Bird.inspect end end diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index dfa12cb97c..a16ed963fe 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -272,6 +272,10 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert p.treasures.empty? assert RichPerson.connection.select_all("SELECT * FROM peoples_treasures WHERE rich_person_id = 1").empty? end + + def test_quoted_locking_column_is_deprecated + assert_deprecated { ActiveRecord::Base.quoted_locking_column } + end end class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index 3bdc5a1302..97c0350911 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -119,11 +119,11 @@ class LogSubscriberTest < ActiveRecord::TestCase Thread.new { assert_equal 0, ActiveRecord::LogSubscriber.runtime }.join end - def test_binary_data_is_not_logged - skip if current_adapter?(:Mysql2Adapter) - - Binary.create(data: 'some binary data') - wait - assert_match(/<16 bytes of binary data>/, @logger.logged(:debug).join) + unless current_adapter?(:Mysql2Adapter) + def test_binary_data_is_not_logged + Binary.create(data: 'some binary data') + wait + assert_match(/<16 bytes of binary data>/, @logger.logged(:debug).join) + end end end diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index e37dca856d..e43e256d24 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -74,8 +74,8 @@ module ActiveRecord assert_equal "hello", five.default unless mysql end - def test_add_column_with_array - if current_adapter?(:PostgreSQLAdapter) + if current_adapter?(:PostgreSQLAdapter) + def test_add_column_with_array connection.create_table :testings connection.add_column :testings, :foo, :string, :array => true @@ -83,13 +83,9 @@ module ActiveRecord array_column = columns.detect { |c| c.name == "foo" } assert array_column.array - else - skip "array option only supported in PostgreSQLAdapter" end - end - def test_create_table_with_array_column - if current_adapter?(:PostgreSQLAdapter) + def test_create_table_with_array_column connection.create_table :testings do |t| t.string :foo, :array => true end @@ -98,8 +94,6 @@ module ActiveRecord array_column = columns.detect { |c| c.name == "foo" } assert array_column.array - else - skip "array option only supported in PostgreSQLAdapter" end end @@ -211,20 +205,18 @@ module ActiveRecord connection.create_table table_name end - def test_add_column_not_null_without_default - # Sybase, and SQLite3 will not allow you to add a NOT NULL - # column to a table without a default value. - if current_adapter?(:SybaseAdapter, :SQLite3Adapter) - skip "not supported on #{connection.class}" - end - - connection.create_table :testings do |t| - t.column :foo, :string - end - connection.add_column :testings, :bar, :string, :null => false + # Sybase, and SQLite3 will not allow you to add a NOT NULL + # column to a table without a default value. + unless current_adapter?(:SybaseAdapter, :SQLite3Adapter) + def test_add_column_not_null_without_default + connection.create_table :testings do |t| + t.column :foo, :string + end + connection.add_column :testings, :bar, :string, :null => false - assert_raise(ActiveRecord::StatementInvalid) do - connection.execute "insert into testings (foo, bar) values ('hello', NULL)" + assert_raise(ActiveRecord::StatementInvalid) do + connection.execute "insert into testings (foo, bar) values ('hello', NULL)" + end end end diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index aa606ac8bb..ccf19fb4d0 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -35,12 +35,12 @@ module ActiveRecord assert_no_column TestModel, :last_name end - def test_unabstracted_database_dependent_types - skip "not supported" unless current_adapter?(:MysqlAdapter, :Mysql2Adapter) - - add_column :test_models, :intelligence_quotient, :tinyint - TestModel.reset_column_information - assert_match(/tinyint/, TestModel.columns_hash['intelligence_quotient'].sql_type) + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + def test_unabstracted_database_dependent_types + add_column :test_models, :intelligence_quotient, :tinyint + TestModel.reset_column_information + assert_match(/tinyint/, TestModel.columns_hash['intelligence_quotient'].sql_type) + end end # We specifically do a manual INSERT here, and then test only the SELECT @@ -95,22 +95,22 @@ module ActiveRecord assert_equal 7, wealth_column.scale end - def test_change_column_preserve_other_column_precision_and_scale - skip "only on sqlite3" unless current_adapter?(:SQLite3Adapter) + if current_adapter?(:SQLite3Adapter) + def test_change_column_preserve_other_column_precision_and_scale + connection.add_column 'test_models', 'last_name', :string + connection.add_column 'test_models', 'wealth', :decimal, :precision => 9, :scale => 7 - connection.add_column 'test_models', 'last_name', :string - connection.add_column 'test_models', 'wealth', :decimal, :precision => 9, :scale => 7 - - wealth_column = TestModel.columns_hash['wealth'] - assert_equal 9, wealth_column.precision - assert_equal 7, wealth_column.scale + wealth_column = TestModel.columns_hash['wealth'] + assert_equal 9, wealth_column.precision + assert_equal 7, wealth_column.scale - connection.change_column 'test_models', 'last_name', :string, :null => false - TestModel.reset_column_information + connection.change_column 'test_models', 'last_name', :string, :null => false + TestModel.reset_column_information - wealth_column = TestModel.columns_hash['wealth'] - assert_equal 9, wealth_column.precision - assert_equal 7, wealth_column.scale + wealth_column = TestModel.columns_hash['wealth'] + assert_equal 9, wealth_column.precision + assert_equal 7, wealth_column.scale + end end def test_native_types @@ -163,13 +163,13 @@ module ActiveRecord assert_kind_of BigDecimal, bob.wealth end - def test_out_of_range_limit_should_raise - skip("MySQL and PostgreSQL only") unless current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) - - assert_raise(ActiveRecordError) { add_column :test_models, :integer_too_big, :integer, :limit => 10 } + if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) + def test_out_of_range_limit_should_raise + assert_raise(ActiveRecordError) { add_column :test_models, :integer_too_big, :integer, :limit => 10 } - unless current_adapter?(:PostgreSQLAdapter) - assert_raise(ActiveRecordError) { add_column :test_models, :text_too_big, :integer, :limit => 0xfffffffff } + unless current_adapter?(:PostgreSQLAdapter) + assert_raise(ActiveRecordError) { add_column :test_models, :text_too_big, :integer, :limit => 0xfffffffff } + end end end end diff --git a/activerecord/test/cases/migration/column_positioning_test.rb b/activerecord/test/cases/migration/column_positioning_test.rb index 913d935f7c..87e29e41ba 100644 --- a/activerecord/test/cases/migration/column_positioning_test.rb +++ b/activerecord/test/cases/migration/column_positioning_test.rb @@ -9,10 +9,6 @@ module ActiveRecord def setup super - unless current_adapter?(:MysqlAdapter, :Mysql2Adapter) - skip "not supported on #{connection.class}" - end - @connection = ActiveRecord::Base.connection connection.create_table :testings, :id => false do |t| @@ -28,33 +24,34 @@ module ActiveRecord ActiveRecord::Base.primary_key_prefix_type = nil end - def test_column_positioning - assert_equal %w(first second third), conn.columns(:testings).map {|c| c.name } - end + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + def test_column_positioning + assert_equal %w(first second third), conn.columns(:testings).map {|c| c.name } + end - def test_add_column_with_positioning - conn.add_column :testings, :new_col, :integer - assert_equal %w(first second third new_col), conn.columns(:testings).map {|c| c.name } - end + def test_add_column_with_positioning + conn.add_column :testings, :new_col, :integer + assert_equal %w(first second third new_col), conn.columns(:testings).map {|c| c.name } + end - def test_add_column_with_positioning_first - conn.add_column :testings, :new_col, :integer, :first => true - assert_equal %w(new_col first second third), conn.columns(:testings).map {|c| c.name } - end + def test_add_column_with_positioning_first + conn.add_column :testings, :new_col, :integer, :first => true + assert_equal %w(new_col first second third), conn.columns(:testings).map {|c| c.name } + end - def test_add_column_with_positioning_after - conn.add_column :testings, :new_col, :integer, :after => :first - assert_equal %w(first new_col second third), conn.columns(:testings).map {|c| c.name } - end + def test_add_column_with_positioning_after + conn.add_column :testings, :new_col, :integer, :after => :first + assert_equal %w(first new_col second third), conn.columns(:testings).map {|c| c.name } + end - def test_change_column_with_positioning - conn.change_column :testings, :second, :integer, :first => true - assert_equal %w(second first third), conn.columns(:testings).map {|c| c.name } + def test_change_column_with_positioning + conn.change_column :testings, :second, :integer, :first => true + assert_equal %w(second first third), conn.columns(:testings).map {|c| c.name } - conn.change_column :testings, :second, :integer, :after => :third - assert_equal %w(first third second), conn.columns(:testings).map {|c| c.name } + conn.change_column :testings, :second, :integer, :after => :third + assert_equal %w(first third second), conn.columns(:testings).map {|c| c.name } + end end - end end end diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb index 04521a5f5a..8d1daa0a04 100644 --- a/activerecord/test/cases/migration/index_test.rb +++ b/activerecord/test/cases/migration/index_test.rb @@ -27,32 +27,28 @@ module ActiveRecord ActiveRecord::Base.primary_key_prefix_type = nil end - def test_rename_index - skip "not supported on openbase" if current_adapter?(:OpenBaseAdapter) - - # keep the names short to make Oracle and similar behave - connection.add_index(table_name, [:foo], :name => 'old_idx') - connection.rename_index(table_name, 'old_idx', 'new_idx') - - # if the adapter doesn't support the indexes call, pick defaults that let the test pass - assert_not connection.index_name_exists?(table_name, 'old_idx', false) - assert connection.index_name_exists?(table_name, 'new_idx', true) - end - - def test_double_add_index - skip "not supported on openbase" if current_adapter?(:OpenBaseAdapter) + unless current_adapter?(:OpenBaseAdapter) + def test_rename_index + # keep the names short to make Oracle and similar behave + connection.add_index(table_name, [:foo], :name => 'old_idx') + connection.rename_index(table_name, 'old_idx', 'new_idx') + + # if the adapter doesn't support the indexes call, pick defaults that let the test pass + assert_not connection.index_name_exists?(table_name, 'old_idx', false) + assert connection.index_name_exists?(table_name, 'new_idx', true) + end - connection.add_index(table_name, [:foo], :name => 'some_idx') - assert_raises(ArgumentError) { + def test_double_add_index connection.add_index(table_name, [:foo], :name => 'some_idx') - } - end - - def test_remove_nonexistent_index - skip "not supported on openbase" if current_adapter?(:OpenBaseAdapter) + assert_raises(ArgumentError) { + connection.add_index(table_name, [:foo], :name => 'some_idx') + } + end - # we do this by name, so OpenBase is a wash as noted above - assert_raise(ArgumentError) { connection.remove_index(table_name, "no_such_index") } + def test_remove_nonexistent_index + # we do this by name, so OpenBase is a wash as noted above + assert_raise(ArgumentError) { connection.remove_index(table_name, "no_such_index") } + end end def test_add_index_works_with_long_index_names @@ -189,14 +185,14 @@ module ActiveRecord end end - def test_add_partial_index - skip 'only on pg' unless current_adapter?(:PostgreSQLAdapter) - - connection.add_index("testings", "last_name", :where => "first_name = 'john doe'") - assert connection.index_exists?("testings", "last_name") + if current_adapter?(:PostgreSQLAdapter) + def test_add_partial_index + connection.add_index("testings", "last_name", :where => "first_name = 'john doe'") + assert connection.index_exists?("testings", "last_name") - connection.remove_index("testings", "last_name") - assert !connection.index_exists?("testings", "last_name") + connection.remove_index("testings", "last_name") + assert !connection.index_exists?("testings", "last_name") + end end private diff --git a/activerecord/test/cases/migration/references_index_test.rb b/activerecord/test/cases/migration/references_index_test.rb index 3ff89524fe..19eb7d3c9e 100644 --- a/activerecord/test/cases/migration/references_index_test.rb +++ b/activerecord/test/cases/migration/references_index_test.rb @@ -50,14 +50,14 @@ module ActiveRecord assert connection.index_exists?(table_name, :bar_id, :name => :index_testings_on_bar_id, :unique => true) end - def test_creates_polymorphic_index - return skip "Oracle Adapter does not support foreign keys if :polymorphic => true is used" if current_adapter? :OracleAdapter + unless current_adapter? :OracleAdapter + def test_creates_polymorphic_index + connection.create_table table_name do |t| + t.references :foo, :polymorphic => true, :index => true + end - connection.create_table table_name do |t| - t.references :foo, :polymorphic => true, :index => true + assert connection.index_exists?(table_name, [:foo_id, :foo_type], :name => :index_testings_on_foo_id_and_foo_type) end - - assert connection.index_exists?(table_name, [:foo_id, :foo_type], :name => :index_testings_on_foo_id_and_foo_type) end def test_creates_index_for_existing_table @@ -87,16 +87,16 @@ module ActiveRecord assert_not connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) end - def test_creates_polymorphic_index_for_existing_table - return skip "Oracle Adapter does not support foreign keys if :polymorphic => true is used" if current_adapter? :OracleAdapter - connection.create_table table_name - connection.change_table table_name do |t| - t.references :foo, :polymorphic => true, :index => true - end + unless current_adapter? :OracleAdapter + def test_creates_polymorphic_index_for_existing_table + connection.create_table table_name + connection.change_table table_name do |t| + t.references :foo, :polymorphic => true, :index => true + end - assert connection.index_exists?(table_name, [:foo_id, :foo_type], :name => :index_testings_on_foo_id_and_foo_type) + assert connection.index_exists?(table_name, [:foo_id, :foo_type], :name => :index_testings_on_foo_id_and_foo_type) + end end - end end end diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index 22dbd7c38b..2a7fafc559 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -19,24 +19,24 @@ module ActiveRecord super end - def test_rename_table_for_sqlite_should_work_with_reserved_words - renamed = false - - skip "not supported" unless current_adapter?(:SQLite3Adapter) - - add_column :test_models, :url, :string - connection.rename_table :references, :old_references - connection.rename_table :test_models, :references - - renamed = true - - # Using explicit id in insert for compatibility across all databases - connection.execute "INSERT INTO 'references' (url, created_at, updated_at) VALUES ('http://rubyonrails.com', 0, 0)" - assert_equal 'http://rubyonrails.com', connection.select_value("SELECT url FROM 'references' WHERE id=1") - ensure - return unless renamed - connection.rename_table :references, :test_models - connection.rename_table :old_references, :references + if current_adapter?(:SQLite3Adapter) + def test_rename_table_for_sqlite_should_work_with_reserved_words + renamed = false + + add_column :test_models, :url, :string + connection.rename_table :references, :old_references + connection.rename_table :test_models, :references + + renamed = true + + # Using explicit id in insert for compatibility across all databases + connection.execute "INSERT INTO 'references' (url, created_at, updated_at) VALUES ('http://rubyonrails.com', 0, 0)" + assert_equal 'http://rubyonrails.com', connection.select_value("SELECT url FROM 'references' WHERE id=1") + ensure + return unless renamed + connection.rename_table :references, :test_models + connection.rename_table :old_references, :references + end end def test_rename_table @@ -76,14 +76,14 @@ module ActiveRecord assert_equal ['special_url_idx'], connection.indexes(:octopi).map(&:name) end - def test_rename_table_for_postgresql_should_also_rename_default_sequence - skip 'not supported' unless current_adapter?(:PostgreSQLAdapter) - - rename_table :test_models, :octopi + if current_adapter?(:PostgreSQLAdapter) + def test_rename_table_for_postgresql_should_also_rename_default_sequence + rename_table :test_models, :octopi - pk, seq = connection.pk_and_sequence_for('octopi') + pk, seq = connection.pk_and_sequence_for('octopi') - assert_equal "octopi_#{pk}_seq", seq + assert_equal "octopi_#{pk}_seq", seq + end end end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 931caff727..7c3988859f 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -63,6 +63,7 @@ class MigrationTest < ActiveRecord::TestCase def test_migrator_versions migrations_path = MIGRATIONS_ROOT + "/valid" + old_path = ActiveRecord::Migrator.migrations_paths ActiveRecord::Migrator.migrations_paths = migrations_path ActiveRecord::Migrator.up(migrations_path) @@ -74,6 +75,8 @@ class MigrationTest < ActiveRecord::TestCase assert_equal 0, ActiveRecord::Migrator.current_version assert_equal 3, ActiveRecord::Migrator.last_version assert_equal true, ActiveRecord::Migrator.needs_migration? + ensure + ActiveRecord::Migrator.migrations_paths = old_path end def test_create_table_with_force_true_does_not_drop_nonexisting_table @@ -230,95 +233,94 @@ class MigrationTest < ActiveRecord::TestCase assert migration.went_down, 'have not gone down' end - def test_migrator_one_up_with_exception_and_rollback - unless ActiveRecord::Base.connection.supports_ddl_transactions? - skip "not supported on #{ActiveRecord::Base.connection.class}" - end - - assert_no_column Person, :last_name - - migration = Class.new(ActiveRecord::Migration) { - def version; 100 end - def migrate(x) - add_column "people", "last_name", :string - raise 'Something broke' - end - }.new + if ActiveRecord::Base.connection.supports_ddl_transactions? + def test_migrator_one_up_with_exception_and_rollback + assert_no_column Person, :last_name - migrator = ActiveRecord::Migrator.new(:up, [migration], 100) + migration = Class.new(ActiveRecord::Migration) { + def version; 100 end + def migrate(x) + add_column "people", "last_name", :string + raise 'Something broke' + end + }.new - e = assert_raise(StandardError) { migrator.migrate } + migrator = ActiveRecord::Migrator.new(:up, [migration], 100) - assert_equal "An error has occurred, this and all later migrations canceled:\n\nSomething broke", e.message + e = assert_raise(StandardError) { migrator.migrate } - assert_no_column Person, :last_name, - "On error, the Migrator should revert schema changes but it did not." - end + assert_equal "An error has occurred, this and all later migrations canceled:\n\nSomething broke", e.message - def test_migrator_one_up_with_exception_and_rollback_using_run - unless ActiveRecord::Base.connection.supports_ddl_transactions? - skip "not supported on #{ActiveRecord::Base.connection.class}" + assert_no_column Person, :last_name, + "On error, the Migrator should revert schema changes but it did not." end - assert_no_column Person, :last_name - - migration = Class.new(ActiveRecord::Migration) { - def version; 100 end - def migrate(x) - add_column "people", "last_name", :string - raise 'Something broke' - end - }.new + def test_migrator_one_up_with_exception_and_rollback_using_run + assert_no_column Person, :last_name - migrator = ActiveRecord::Migrator.new(:up, [migration], 100) + migration = Class.new(ActiveRecord::Migration) { + def version; 100 end + def migrate(x) + add_column "people", "last_name", :string + raise 'Something broke' + end + }.new - e = assert_raise(StandardError) { migrator.run } + migrator = ActiveRecord::Migrator.new(:up, [migration], 100) - assert_equal "An error has occurred, this migration was canceled:\n\nSomething broke", e.message + e = assert_raise(StandardError) { migrator.run } - assert_no_column Person, :last_name, - "On error, the Migrator should revert schema changes but it did not." - end + assert_equal "An error has occurred, this migration was canceled:\n\nSomething broke", e.message - def test_migration_without_transaction - unless ActiveRecord::Base.connection.supports_ddl_transactions? - skip "not supported on #{ActiveRecord::Base.connection.class}" + assert_no_column Person, :last_name, + "On error, the Migrator should revert schema changes but it did not." end - assert_no_column Person, :last_name + def test_migration_without_transaction + assert_no_column Person, :last_name - migration = Class.new(ActiveRecord::Migration) { - self.disable_ddl_transaction! + migration = Class.new(ActiveRecord::Migration) { + self.disable_ddl_transaction! - def version; 101 end - def migrate(x) - add_column "people", "last_name", :string - raise 'Something broke' - end - }.new + def version; 101 end + def migrate(x) + add_column "people", "last_name", :string + raise 'Something broke' + end + }.new - migrator = ActiveRecord::Migrator.new(:up, [migration], 101) - e = assert_raise(StandardError) { migrator.migrate } - assert_equal "An error has occurred, all later migrations canceled:\n\nSomething broke", e.message + migrator = ActiveRecord::Migrator.new(:up, [migration], 101) + e = assert_raise(StandardError) { migrator.migrate } + assert_equal "An error has occurred, all later migrations canceled:\n\nSomething broke", e.message - assert_column Person, :last_name, - "without ddl transactions, the Migrator should not rollback on error but it did." - ensure - Person.reset_column_information - if Person.column_names.include?('last_name') - Person.connection.remove_column('people', 'last_name') + assert_column Person, :last_name, + "without ddl transactions, the Migrator should not rollback on error but it did." + ensure + Person.reset_column_information + if Person.column_names.include?('last_name') + Person.connection.remove_column('people', 'last_name') + end end 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 @@ -441,62 +443,62 @@ class MigrationTest < ActiveRecord::TestCase Person.connection.drop_table :binary_testings rescue nil end - def test_create_table_with_custom_sequence_name - skip "not supported" unless current_adapter? :OracleAdapter - - # table name is 29 chars, the standard sequence name will - # be 33 chars and should be shortened - assert_nothing_raised do - begin - Person.connection.create_table :table_with_name_thats_just_ok do |t| - t.column :foo, :string, :null => false + if current_adapter? :OracleAdapter + def test_create_table_with_custom_sequence_name + # table name is 29 chars, the standard sequence name will + # be 33 chars and should be shortened + assert_nothing_raised do + begin + Person.connection.create_table :table_with_name_thats_just_ok do |t| + t.column :foo, :string, :null => false + end + ensure + Person.connection.drop_table :table_with_name_thats_just_ok rescue nil end - ensure - Person.connection.drop_table :table_with_name_thats_just_ok rescue nil end - end - # should be all good w/ a custom sequence name - assert_nothing_raised do - begin - Person.connection.create_table :table_with_name_thats_just_ok, - :sequence_name => 'suitably_short_seq' do |t| - t.column :foo, :string, :null => false - end + # should be all good w/ a custom sequence name + assert_nothing_raised do + begin + Person.connection.create_table :table_with_name_thats_just_ok, + :sequence_name => 'suitably_short_seq' do |t| + t.column :foo, :string, :null => false + end - Person.connection.execute("select suitably_short_seq.nextval from dual") + Person.connection.execute("select suitably_short_seq.nextval from dual") - ensure - Person.connection.drop_table :table_with_name_thats_just_ok, - :sequence_name => 'suitably_short_seq' rescue nil + ensure + Person.connection.drop_table :table_with_name_thats_just_ok, + :sequence_name => 'suitably_short_seq' rescue nil + end end - end - # confirm the custom sequence got dropped - assert_raise(ActiveRecord::StatementInvalid) do - Person.connection.execute("select suitably_short_seq.nextval from dual") + # confirm the custom sequence got dropped + assert_raise(ActiveRecord::StatementInvalid) do + Person.connection.execute("select suitably_short_seq.nextval from dual") + end end end - def test_out_of_range_limit_should_raise - skip("MySQL and PostgreSQL only") unless current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) - - Person.connection.drop_table :test_limits rescue nil - assert_raise(ActiveRecord::ActiveRecordError, "integer limit didn't raise") do - Person.connection.create_table :test_integer_limits, :force => true do |t| - t.column :bigone, :integer, :limit => 10 + if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) + def test_out_of_range_limit_should_raise + Person.connection.drop_table :test_limits rescue nil + assert_raise(ActiveRecord::ActiveRecordError, "integer limit didn't raise") do + Person.connection.create_table :test_integer_limits, :force => true do |t| + t.column :bigone, :integer, :limit => 10 + end end - end - unless current_adapter?(:PostgreSQLAdapter) - assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do - Person.connection.create_table :test_text_limits, :force => true do |t| - t.column :bigtext, :text, :limit => 0xfffffffff + unless current_adapter?(:PostgreSQLAdapter) + assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do + Person.connection.create_table :test_text_limits, :force => true do |t| + t.column :bigtext, :text, :limit => 0xfffffffff + end end end - end - Person.connection.drop_table :test_limits rescue nil + Person.connection.drop_table :test_limits rescue nil + end end protected @@ -701,8 +703,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 +727,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 +745,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 +770,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 +790,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 +808,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 +865,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 +880,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/mixin_test.rb b/activerecord/test/cases/mixin_test.rb index f927c13979..ad0d5cce27 100644 --- a/activerecord/test/cases/mixin_test.rb +++ b/activerecord/test/cases/mixin_test.rb @@ -3,42 +3,11 @@ require "cases/helper" class Mixin < ActiveRecord::Base end -# Let us control what Time.now returns for the TouchTest suite -class Time - @@forced_now_time = nil - cattr_accessor :forced_now_time - - class << self - def now_with_forcing - if @@forced_now_time - @@forced_now_time - else - now_without_forcing - end - end - alias_method_chain :now, :forcing - end -end - - class TouchTest < ActiveRecord::TestCase fixtures :mixins def setup - Time.forced_now_time = Time.now - end - - def teardown - Time.forced_now_time = nil - end - - def test_time_mocking - five_minutes_ago = 5.minutes.ago - Time.forced_now_time = five_minutes_ago - assert_equal five_minutes_ago, Time.now - - Time.forced_now_time = nil - assert_not_equal five_minutes_ago, Time.now + travel_to Time.now end def test_update @@ -68,12 +37,13 @@ class TouchTest < ActiveRecord::TestCase old_updated_at = stamped.updated_at - Time.forced_now_time = 5.minutes.from_now - stamped.lft_will_change! - stamped.save + travel 5.minutes do + stamped.lft_will_change! + stamped.save - assert_equal Time.now, stamped.updated_at - assert_equal old_updated_at, stamped.created_at + assert_equal Time.now, stamped.updated_at + assert_equal old_updated_at, stamped.created_at + end end def test_create_turned_off 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..c70a8f296f 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,36 +243,37 @@ 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 - - topic = Topic.new( "bonus_time(4i)"=> "01", "bonus_time(5i)" => "05" ) - assert_equal 1, topic.bonus_time.hour - assert_equal 5, topic.bonus_time.min + unless current_adapter? :OracleAdapter + def test_multiparameter_attributes_setting_time_attribute + topic = Topic.new( "bonus_time(4i)"=> "01", "bonus_time(5i)" => "05" ) + assert_equal 1, topic.bonus_time.hour + assert_equal 5, topic.bonus_time.min + end end def test_multiparameter_attributes_setting_date_attribute diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 30dc2a34c6..6cd3e2154e 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -419,10 +419,6 @@ class PersistenceTest < ActiveRecord::TestCase assert !Topic.find(1).approved? end - def test_update_attribute_does_not_choke_on_nil - assert Topic.find(1).update(nil) - end - def test_update_attribute_for_readonly_attribute minivan = Minivan.find('m1') assert_raises(ActiveRecord::ActiveRecordError) { minivan.update_attribute(:color, 'black') } @@ -701,6 +697,17 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal topic.title, Topic.find(1234).title end + def test_update_attributes_parameters + topic = Topic.find(1) + assert_nothing_raised do + topic.update_attributes({}) + end + + assert_raises(ArgumentError) do + topic.update_attributes(nil) + end + end + def test_update! Reply.validates_presence_of(:title) reply = Reply.find(2) diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index aa125c70c5..1b915387be 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -185,19 +185,19 @@ end class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase self.use_transactional_fixtures = false - def test_set_primary_key_with_no_connection - return skip("disconnect wipes in-memory db") if in_memory_db? + unless in_memory_db? + def test_set_primary_key_with_no_connection + connection = ActiveRecord::Base.remove_connection - connection = ActiveRecord::Base.remove_connection + model = Class.new(ActiveRecord::Base) + model.primary_key = 'foo' - model = Class.new(ActiveRecord::Base) - model.primary_key = 'foo' + assert_equal 'foo', model.primary_key - assert_equal 'foo', model.primary_key + ActiveRecord::Base.establish_connection(connection) - ActiveRecord::Base.establish_connection(connection) - - assert_equal 'foo', model.primary_key + assert_equal 'foo', model.primary_key + end end end @@ -212,7 +212,6 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter) ensure con.reconnect! end - end end 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 new file mode 100644 index 0000000000..7f99b6841f --- /dev/null +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -0,0 +1,100 @@ +require 'cases/helper' +require 'models/post' +require 'models/comment' + +module ActiveRecord + class DelegationTest < ActiveRecord::TestCase + fixtures :posts + + def assert_responds(target, method) + assert target.respond_to?(method) + assert_nothing_raised do + method_arity = target.to_a.method(method).arity + + if method_arity.zero? + target.send(method) + elsif method_arity < 0 + if method == :shuffle! + target.send(method) + else + target.send(method, 1) + end + else + raise NotImplementedError + end + end + end + end + + class DelegationAssociationTest < DelegationTest + def target + Post.first.comments + end + + [:map, :collect].each do |method| + test "##{method} is delgated" do + assert_responds(target, method) + assert_equal(target.pluck(:body), target.send(method) {|post| post.body }) + end + + test "##{method}! is not delgated" do + assert_deprecated do + assert_responds(target, "#{method}!") + end + end + end + + [:compact!, :flatten!, :reject!, :reverse!, :rotate!, + :shuffle!, :slice!, :sort!, :sort_by!].each do |method| + test "##{method} delegation is deprecated" do + assert_deprecated do + assert_responds(target, method) + end + end + end + + [:select!, :uniq!].each do |method| + test "##{method} is implemented" do + assert_responds(target, method) + end + end + end + + class DelegationRelationTest < DelegationTest + fixtures :comments + + def target + Comment.where(body: 'Normal type') + end + + [:map, :collect].each do |method| + test "##{method} is delgated" do + assert_responds(target, method) + assert_equal(target.pluck(:body), target.send(method) {|post| post.body }) + end + + test "##{method}! is not delgated" do + assert_deprecated do + assert_responds(target, "#{method}!") + end + end + end + + [:compact!, :flatten!, :reject!, :reverse!, :rotate!, + :shuffle!, :slice!, :sort!, :sort_by!].each do |method| + test "##{method} delegation is deprecated" do + assert_deprecated do + assert_responds(target, method) + end + end + end + + [:select!, :uniq!].each do |method| + test "##{method} triggers an immutable error" do + assert_raises ActiveRecord::ImmutableRelation do + assert_responds(target, method) + end + end + end + end +end diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb new file mode 100644 index 0000000000..23500bf5d8 --- /dev/null +++ b/activerecord/test/cases/relation/merging_test.rb @@ -0,0 +1,162 @@ +require 'cases/helper' +require 'models/author' +require 'models/comment' +require 'models/developer' +require 'models/post' +require 'models/project' + +class RelationMergingTest < ActiveRecord::TestCase + fixtures :developers, :comments, :authors, :posts + + def test_relation_merging + devs = Developer.where("salary >= 80000").merge(Developer.limit(2)).merge(Developer.order('id ASC').where("id < 3")) + assert_equal [developers(:david), developers(:jamis)], devs.to_a + + dev_with_count = Developer.limit(1).merge(Developer.order('id DESC')).merge(Developer.select('developers.*')) + assert_equal [developers(:poor_jamis)], dev_with_count.to_a + end + + def test_relation_to_sql + sql = Post.connection.unprepared_statement do + Post.first.comments.to_sql + end + assert_no_match(/\?/, sql) + end + + def test_relation_merging_with_arel_equalities_keeps_last_equality + devs = Developer.where(Developer.arel_table[:salary].eq(80000)).merge( + Developer.where(Developer.arel_table[:salary].eq(9000)) + ) + assert_equal [developers(:poor_jamis)], devs.to_a + end + + def test_relation_merging_with_arel_equalities_keeps_last_equality_with_non_attribute_left_hand + salary_attr = Developer.arel_table[:salary] + devs = Developer.where( + Arel::Nodes::NamedFunction.new('abs', [salary_attr]).eq(80000) + ).merge( + Developer.where( + Arel::Nodes::NamedFunction.new('abs', [salary_attr]).eq(9000) + ) + ) + assert_equal [developers(:poor_jamis)], devs.to_a + end + + def test_relation_merging_with_eager_load + relations = [] + relations << Post.order('comments.id DESC').merge(Post.eager_load(:last_comment)).merge(Post.all) + relations << Post.eager_load(:last_comment).merge(Post.order('comments.id DESC')).merge(Post.all) + + relations.each do |posts| + post = posts.find { |p| p.id == 1 } + assert_equal Post.find(1).last_comment, post.last_comment + end + end + + def test_relation_merging_with_locks + devs = Developer.lock.where("salary >= 80000").order("id DESC").merge(Developer.limit(2)) + assert devs.locked.present? + end + + def test_relation_merging_with_preload + [Post.all.merge(Post.preload(:author)), Post.preload(:author).merge(Post.all)].each do |posts| + assert_queries(2) { assert posts.first.author } + end + end + + def test_relation_merging_with_joins + comments = Comment.joins(:post).where(:body => 'Thank you for the welcome').merge(Post.where(:body => 'Such a lovely day')) + assert_equal 1, comments.count + end + + def test_relation_merging_with_association + assert_queries(2) do # one for loading post, and another one merged query + post = Post.where(:body => 'Such a lovely day').first + comments = Comment.where(:body => 'Thank you for the welcome').merge(post.comments) + assert_equal 1, comments.count + end + end + + test "merge collapses wheres from the LHS only" do + left = Post.where(title: "omg").where(comments_count: 1) + right = Post.where(title: "wtf").where(title: "bbq") + + expected = [left.where_values[1]] + right.where_values + merged = left.merge(right) + + assert_equal expected, merged.where_values + assert !merged.to_sql.include?("omg") + assert merged.to_sql.include?("wtf") + assert merged.to_sql.include?("bbq") + end + + def test_merging_removes_rhs_bind_parameters + left = Post.where(id: Arel::Nodes::BindParam.new('?')) + column = Post.columns_hash['id'] + left.bind_values += [[column, 20]] + right = Post.where(id: 10) + + merged = left.merge(right) + assert_equal [], merged.bind_values + end + + def test_merging_keeps_lhs_bind_parameters + column = Post.columns_hash['id'] + binds = [[column, 20]] + + right = Post.where(id: Arel::Nodes::BindParam.new('?')) + right.bind_values += binds + left = Post.where(id: 10) + + merged = left.merge(right) + assert_equal binds, merged.bind_values + end + + def test_merging_reorders_bind_params + post = Post.first + id_column = Post.columns_hash['id'] + title_column = Post.columns_hash['title'] + + bv = Post.connection.substitute_at id_column, 0 + + right = Post.where(id: bv) + right.bind_values += [[id_column, post.id]] + + left = Post.where(title: bv) + left.bind_values += [[title_column, post.title]] + + merged = left.merge(right) + assert_equal post, merged.first + end +end + +class MergingDifferentRelationsTest < ActiveRecord::TestCase + fixtures :posts, :authors + + test "merging where relations" do + hello_by_bob = Post.where(body: "hello").joins(:author). + merge(Author.where(name: "Bob")).order("posts.id").pluck("posts.id") + + assert_equal [posts(:misc_by_bob).id, + posts(:other_by_bob).id], hello_by_bob + end + + test "merging order relations" do + posts_by_author_name = Post.limit(3).joins(:author). + merge(Author.order(:name)).pluck("authors.name") + + assert_equal ["Bob", "Bob", "David"], posts_by_author_name + + posts_by_author_name = Post.limit(3).joins(:author). + merge(Author.order("name")).pluck("authors.name") + + assert_equal ["Bob", "Bob", "David"], posts_by_author_name + end + + test "merging order relations (using a hash argument)" do + posts_by_author_name = Post.limit(4).joins(:author). + merge(Author.order(name: :desc)).pluck("authors.name") + + assert_equal ["Mary", "Mary", "Mary", "David"], posts_by_author_name + end +end diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb new file mode 100644 index 0000000000..7cb2a19bee --- /dev/null +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -0,0 +1,144 @@ +require 'cases/helper' +require 'models/post' + +module ActiveRecord + class RelationMutationTest < ActiveSupport::TestCase + class FakeKlass < Struct.new(:table_name, :name) + extend ActiveRecord::Delegation::DelegateCache + inherited self + + def connection + Post.connection + end + + def relation_delegate_class(klass) + self.class.relation_delegate_class(klass) + end + end + + def relation + @relation ||= Relation.new FakeKlass.new('posts'), Post.arel_table + end + + (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope]).each do |method| + test "##{method}!" do + assert relation.public_send("#{method}!", :foo).equal?(relation) + assert_equal [:foo], relation.public_send("#{method}_values") + end + end + + test '#order!' do + assert relation.order!('name ASC').equal?(relation) + assert_equal ['name ASC'], relation.order_values + end + + test '#order! with symbol prepends the table name' do + assert relation.order!(:name).equal?(relation) + node = relation.order_values.first + assert node.ascending? + assert_equal :name, node.expr.name + assert_equal "posts", node.expr.relation.name + end + + test '#order! on non-string does not attempt regexp match for references' do + obj = Object.new + obj.expects(:=~).never + assert relation.order!(obj) + assert_equal [obj], relation.order_values + end + + test '#references!' do + assert relation.references!(:foo).equal?(relation) + assert relation.references_values.include?('foo') + end + + test 'extending!' do + mod, mod2 = Module.new, Module.new + + assert relation.extending!(mod).equal?(relation) + assert_equal [mod], relation.extending_values + assert relation.is_a?(mod) + + relation.extending!(mod2) + assert_equal [mod, mod2], relation.extending_values + end + + test 'extending! with empty args' do + relation.extending! + assert_equal [], relation.extending_values + end + + (Relation::SINGLE_VALUE_METHODS - [:from, :lock, :reordering, :reverse_order, :create_with]).each do |method| + test "##{method}!" do + assert relation.public_send("#{method}!", :foo).equal?(relation) + assert_equal :foo, relation.public_send("#{method}_value") + end + end + + test '#from!' do + assert relation.from!('foo').equal?(relation) + assert_equal ['foo', nil], relation.from_value + end + + test '#lock!' do + assert relation.lock!('foo').equal?(relation) + assert_equal 'foo', relation.lock_value + end + + test '#reorder!' do + relation = self.relation.order('foo') + + assert relation.reorder!('bar').equal?(relation) + assert_equal ['bar'], relation.order_values + assert relation.reordering_value + end + + test '#reorder! with symbol prepends the table name' do + assert relation.reorder!(:name).equal?(relation) + node = relation.order_values.first + + assert node.ascending? + assert_equal :name, node.expr.name + assert_equal "posts", node.expr.relation.name + end + + test 'reverse_order!' do + assert relation.reverse_order!.equal?(relation) + assert relation.reverse_order_value + relation.reverse_order! + assert !relation.reverse_order_value + end + + test 'create_with!' do + assert relation.create_with!(foo: 'bar').equal?(relation) + assert_equal({foo: 'bar'}, relation.create_with_value) + end + + test 'test_merge!' do + assert relation.merge!(where: :foo).equal?(relation) + assert_equal [:foo], relation.where_values + end + + test 'merge with a proc' do + assert_equal [:foo], relation.merge(-> { where(:foo) }).where_values + end + + test 'none!' do + assert relation.none!.equal?(relation) + assert_equal [NullRelation], relation.extending_values + assert relation.is_a?(NullRelation) + end + + test 'distinct!' do + relation.distinct! :foo + assert_equal :foo, relation.distinct_value + assert_equal :foo, relation.uniq_value # deprecated access + end + + test 'uniq! was replaced by distinct!' do + relation.uniq! :foo + assert_equal :foo, relation.distinct_value + assert_equal :foo, relation.uniq_value # deprecated access + end + end +end 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/relation_test.rb b/activerecord/test/cases/relation_test.rb index f99801c437..70d113fb39 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -9,6 +9,10 @@ module ActiveRecord fixtures :posts, :comments, :authors class FakeKlass < Struct.new(:table_name, :name) + extend ActiveRecord::Delegation::DelegateCache + + inherited self + def self.connection Post.connection end @@ -76,8 +80,8 @@ module ActiveRecord end def test_table_name_delegates_to_klass - relation = Relation.new FakeKlass.new('foo'), :b - assert_equal 'foo', relation.table_name + relation = Relation.new FakeKlass.new('posts'), :b + assert_equal 'posts', relation.table_name end def test_scope_for_create @@ -177,14 +181,26 @@ module ActiveRecord end test 'merging a hash interpolates conditions' do - klass = stub_everything - klass.stubs(:sanitize_sql).with(['foo = ?', 'bar']).returns('foo = bar') + klass = Class.new(FakeKlass) do + def self.sanitize_sql(args) + raise unless args == ['foo = ?', 'bar'] + 'foo = bar' + end + end relation = Relation.new(klass, :b) relation.merge!(where: ['foo = ?', 'bar']) assert_equal ['foo = bar'], relation.where_values end + def test_merging_readonly_false + relation = Relation.new FakeKlass, :b + readonly_false_relation = relation.readonly(false) + # test merging in both directions + assert_equal false, relation.merge(readonly_false_relation).readonly_value + assert_equal false, readonly_false_relation.merge(relation).readonly_value + end + def test_relation_merging_with_merged_joins_as_symbols special_comments_with_ratings = SpecialComment.joins(:ratings) posts_with_special_comments_with_ratings = Post.group("posts.id").joins(:special_comments).merge(special_comments_with_ratings) @@ -207,132 +223,4 @@ module ActiveRecord end end - - class RelationMutationTest < ActiveSupport::TestCase - class FakeKlass < Struct.new(:table_name, :name) - def arel_table - Post.arel_table - end - - def connection - Post.connection - end - end - - def relation - @relation ||= Relation.new FakeKlass.new('posts'), :b - end - - (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order]).each do |method| - test "##{method}!" do - assert relation.public_send("#{method}!", :foo).equal?(relation) - assert_equal [:foo], relation.public_send("#{method}_values") - end - end - - test "#order!" do - assert relation.order!('name ASC').equal?(relation) - assert_equal ['name ASC'], relation.order_values - end - - test "#order! with symbol prepends the table name" do - assert relation.order!(:name).equal?(relation) - node = relation.order_values.first - assert node.ascending? - assert_equal :name, node.expr.name - assert_equal "posts", node.expr.relation.name - end - - test "#order! on non-string does not attempt regexp match for references" do - obj = Object.new - obj.expects(:=~).never - assert relation.order!(obj) - assert_equal [obj], relation.order_values - end - - test '#references!' do - assert relation.references!(:foo).equal?(relation) - assert relation.references_values.include?('foo') - end - - test 'extending!' do - mod, mod2 = Module.new, Module.new - - assert relation.extending!(mod).equal?(relation) - assert_equal [mod], relation.extending_values - assert relation.is_a?(mod) - - relation.extending!(mod2) - assert_equal [mod, mod2], relation.extending_values - end - - test 'extending! with empty args' do - relation.extending! - assert_equal [], relation.extending_values - end - - (Relation::SINGLE_VALUE_METHODS - [:from, :lock, :reordering, :reverse_order, :create_with]).each do |method| - test "##{method}!" do - assert relation.public_send("#{method}!", :foo).equal?(relation) - assert_equal :foo, relation.public_send("#{method}_value") - end - end - - test '#from!' do - assert relation.from!('foo').equal?(relation) - assert_equal ['foo', nil], relation.from_value - end - - test '#lock!' do - assert relation.lock!('foo').equal?(relation) - assert_equal 'foo', relation.lock_value - end - - test '#reorder!' do - relation = self.relation.order('foo') - - assert relation.reorder!('bar').equal?(relation) - assert_equal ['bar'], relation.order_values - assert relation.reordering_value - end - - test 'reverse_order!' do - assert relation.reverse_order!.equal?(relation) - assert relation.reverse_order_value - relation.reverse_order! - assert !relation.reverse_order_value - end - - test 'create_with!' do - assert relation.create_with!(foo: 'bar').equal?(relation) - assert_equal({foo: 'bar'}, relation.create_with_value) - end - - def test_merge! - assert relation.merge!(where: :foo).equal?(relation) - assert_equal [:foo], relation.where_values - end - - test 'merge with a proc' do - assert_equal [:foo], relation.merge(-> { where(:foo) }).where_values - end - - test 'none!' do - assert relation.none!.equal?(relation) - assert_equal [NullRelation], relation.extending_values - assert relation.is_a?(NullRelation) - end - - test "distinct!" do - relation.distinct! :foo - assert_equal :foo, relation.distinct_value - assert_equal :foo, relation.uniq_value # deprecated access - end - - test "uniq! was replaced by distinct!" do - relation.uniq! :foo - assert_equal :foo, relation.distinct_value - assert_equal :foo, relation.uniq_value # deprecated access - end - end end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index e1a760d240..baa3acf3fb 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 } @@ -139,6 +144,13 @@ class RelationTest < ActiveRecord::TestCase assert_equal relation.to_a, Topic.select('a.*').from(relation, :a).to_a end + def test_finding_with_subquery_with_binds + relation = Post.first.comments + assert_equal relation.to_a, Comment.select('*').from(relation).to_a + assert_equal relation.to_a, Comment.select('subquery.*').from(relation).to_a + assert_equal relation.to_a, Comment.select('a.*').from(relation, :a).to_a + end + def test_finding_with_conditions assert_equal ["David"], Author.where(:name => 'David').map(&:name) assert_equal ['Mary'], Author.where(["name = ?", 'Mary']).map(&:name) @@ -262,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) @@ -283,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 @@ -293,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 @@ -470,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 } @@ -610,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 @@ -720,75 +774,6 @@ class RelationTest < ActiveRecord::TestCase assert_raises(ArgumentError) { Developer.select } end - def test_relation_merging - devs = Developer.where("salary >= 80000").merge(Developer.limit(2)).merge(Developer.order('id ASC').where("id < 3")) - assert_equal [developers(:david), developers(:jamis)], devs.to_a - - dev_with_count = Developer.limit(1).merge(Developer.order('id DESC')).merge(Developer.select('developers.*')) - assert_equal [developers(:poor_jamis)], dev_with_count.to_a - end - - def test_relation_to_sql - sql = Post.connection.unprepared_statement do - Post.first.comments.to_sql - end - assert_no_match(/\?/, sql) - end - - def test_relation_merging_with_arel_equalities_keeps_last_equality - devs = Developer.where(Developer.arel_table[:salary].eq(80000)).merge( - Developer.where(Developer.arel_table[:salary].eq(9000)) - ) - assert_equal [developers(:poor_jamis)], devs.to_a - end - - def test_relation_merging_with_arel_equalities_keeps_last_equality_with_non_attribute_left_hand - salary_attr = Developer.arel_table[:salary] - devs = Developer.where( - Arel::Nodes::NamedFunction.new('abs', [salary_attr]).eq(80000) - ).merge( - Developer.where( - Arel::Nodes::NamedFunction.new('abs', [salary_attr]).eq(9000) - ) - ) - assert_equal [developers(:poor_jamis)], devs.to_a - end - - def test_relation_merging_with_eager_load - relations = [] - relations << Post.order('comments.id DESC').merge(Post.eager_load(:last_comment)).merge(Post.all) - relations << Post.eager_load(:last_comment).merge(Post.order('comments.id DESC')).merge(Post.all) - - relations.each do |posts| - post = posts.find { |p| p.id == 1 } - assert_equal Post.find(1).last_comment, post.last_comment - end - end - - def test_relation_merging_with_locks - devs = Developer.lock.where("salary >= 80000").order("id DESC").merge(Developer.limit(2)) - assert devs.locked.present? - end - - def test_relation_merging_with_preload - [Post.all.merge(Post.preload(:author)), Post.preload(:author).merge(Post.all)].each do |posts| - assert_queries(2) { assert posts.first.author } - end - end - - def test_relation_merging_with_joins - comments = Comment.joins(:post).where(:body => 'Thank you for the welcome').merge(Post.where(:body => 'Such a lovely day')) - assert_equal 1, comments.count - end - - def test_relation_merging_with_association - assert_queries(2) do # one for loading post, and another one merged query - post = Post.where(:body => 'Such a lovely day').first - comments = Comment.where(:body => 'Thank you for the welcome').merge(post.comments) - assert_equal 1, comments.count - end - end - def test_count posts = Post.all @@ -1344,6 +1329,24 @@ class RelationTest < ActiveRecord::TestCase assert_equal [], scope.references_values end + def test_automatically_added_reorder_references + scope = Post.reorder('comments.body') + assert_equal %w(comments), scope.references_values + + scope = Post.reorder('comments.body', 'yaks.body') + assert_equal %w(comments yaks), scope.references_values + + # Don't infer yaks, let's not go down that road again... + scope = Post.reorder('comments.body, yaks.body') + assert_equal %w(comments), scope.references_values + + scope = Post.reorder('comments.body asc') + assert_equal %w(comments), scope.references_values + + scope = Post.reorder('foo(comments.body)') + assert_equal [], scope.references_values + end + def test_presence topics = Topic.all @@ -1525,56 +1528,4 @@ class RelationTest < ActiveRecord::TestCase Array.send(:remove_method, :__omg__) end end - - test "merge collapses wheres from the LHS only" do - left = Post.where(title: "omg").where(comments_count: 1) - right = Post.where(title: "wtf").where(title: "bbq") - - expected = [left.where_values[1]] + right.where_values - merged = left.merge(right) - - assert_equal expected, merged.where_values - assert !merged.to_sql.include?("omg") - assert merged.to_sql.include?("wtf") - assert merged.to_sql.include?("bbq") - end - - def test_merging_removes_rhs_bind_parameters - left = Post.where(id: Arel::Nodes::BindParam.new('?')) - column = Post.columns_hash['id'] - left.bind_values += [[column, 20]] - right = Post.where(id: 10) - - merged = left.merge(right) - assert_equal [], merged.bind_values - end - - def test_merging_keeps_lhs_bind_parameters - column = Post.columns_hash['id'] - binds = [[column, 20]] - - right = Post.where(id: Arel::Nodes::BindParam.new('?')) - right.bind_values += binds - left = Post.where(id: 10) - - merged = left.merge(right) - assert_equal binds, merged.bind_values - end - - def test_merging_reorders_bind_params - post = Post.first - id_column = Post.columns_hash['id'] - title_column = Post.columns_hash['title'] - - bv = Post.connection.substitute_at id_column, 0 - - right = Post.where(id: bv) - right.bind_values += [[id_column, post.id]] - - left = Post.where(title: bv) - left.bind_values += [[title_column, post.title]] - - merged = left.merge(right) - assert_equal post, merged.first - end end 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 a48ae1036f..741827446d 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 @@ -247,19 +252,20 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{t.integer\s+"bigint_default",\s+limit: 8,\s+default: 0}, output end - def test_schema_dump_includes_extensions - connection = ActiveRecord::Base.connection - skip unless connection.supports_extensions? + if ActiveRecord::Base.connection.supports_extensions? + def test_schema_dump_includes_extensions + connection = ActiveRecord::Base.connection - connection.stubs(:extensions).returns(['hstore']) - output = standard_dump - assert_match "# These are extensions that must be enabled", output - assert_match %r{enable_extension "hstore"}, output + connection.stubs(:extensions).returns(['hstore']) + output = standard_dump + assert_match "# These are extensions that must be enabled", output + assert_match %r{enable_extension "hstore"}, output - connection.stubs(:extensions).returns([]) - output = standard_dump - assert_no_match "# These are extensions that must be enabled", output - assert_no_match %r{enable_extension}, output + connection.stubs(:extensions).returns([]) + output = standard_dump + assert_no_match "# These are extensions that must be enabled", output + assert_no_match %r{enable_extension}, output + end end def test_schema_dump_includes_xml_shorthand_definition @@ -299,7 +305,7 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dump_includes_uuid_shorthand_definition output = standard_dump - if %r{create_table "poistgresql_uuids"} =~ output + if %r{create_table "postgresql_uuids"} =~ output assert_match %r{t.uuid "guid"}, output end end diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index cd7d91ff85..11dd32bfb8 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -54,14 +54,14 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal 'Jamis', DeveloperCalledJamis.create!.name end - def test_default_scoping_with_threads - skip "in-memory database mustn't disconnect" if in_memory_db? - - 2.times do - Thread.new { - assert DeveloperOrderedBySalary.all.to_sql.include?('salary DESC') - DeveloperOrderedBySalary.connection.close - }.join + unless in_memory_db? + def test_default_scoping_with_threads + 2.times do + Thread.new { + assert DeveloperOrderedBySalary.all.to_sql.include?('salary DESC') + DeveloperOrderedBySalary.connection.close + }.join + end end end @@ -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 @@ -254,6 +262,12 @@ class DefaultScopingTest < ActiveRecord::TestCase end end + def test_unscope_merging + merged = Developer.where(name: "Jamis").merge(Developer.unscope(:where)) + assert merged.where_values.empty? + assert !merged.where(name: "Jon").where_values.empty? + end + def test_order_in_default_scope_should_not_prevail expected = Developer.all.merge!(order: 'salary desc').to_a.collect { |dev| dev.salary } received = DeveloperOrderedBySalary.all.merge!(order: 'salary').to_a.collect { |dev| dev.salary } @@ -354,23 +368,21 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal 1, DeveloperWithIncludes.where(:audit_logs => { :message => 'foo' }).count end - def test_default_scope_is_threadsafe - if in_memory_db? - skip "in memory db can't share a db between threads" - end - - threads = [] - assert_not_equal 1, ThreadsafeDeveloper.unscoped.count - - threads << Thread.new do - Thread.current[:long_default_scope] = true - assert_equal 1, ThreadsafeDeveloper.all.to_a.count - ThreadsafeDeveloper.connection.close - end - threads << Thread.new do - assert_equal 1, ThreadsafeDeveloper.all.to_a.count - ThreadsafeDeveloper.connection.close + unless in_memory_db? + def test_default_scope_is_threadsafe + threads = [] + assert_not_equal 1, ThreadsafeDeveloper.unscoped.count + + threads << Thread.new do + Thread.current[:long_default_scope] = true + assert_equal 1, ThreadsafeDeveloper.all.to_a.count + ThreadsafeDeveloper.connection.close + end + threads << Thread.new do + assert_equal 1, ThreadsafeDeveloper.all.to_a.count + ThreadsafeDeveloper.connection.close + end + threads.each(&:join) end - threads.each(&:join) end end diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index b49c27bc78..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 @@ -243,10 +242,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase myobj = MyObject.new('value1', 'value2') Topic.create(content: myobj) Topic.create(content: myobj) - - Topic.all.each do |topic| - type = topic.instance_variable_get("@columns_hash")["content"] - assert !type.instance_variable_get("@column").is_a?(ActiveRecord::AttributeMethods::Serialization::Type) - end + type = Topic.column_types["content"] + assert !type.instance_variable_get("@column").is_a?(ActiveRecord::AttributeMethods::Serialization::Type) end end 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..3e3a2828f3 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -65,99 +65,98 @@ module ActiveRecord end end - class MysqlDBCreateAsRootTest < ActiveRecord::TestCase - def setup - unless current_adapter?(:MysqlAdapter) - return skip("only tested on mysql") + if current_adapter?(:MysqlAdapter) + class MysqlDBCreateAsRootTest < ActiveRecord::TestCase + def setup + @connection = stub("Connection", create_database: true) + @error = Mysql::Error.new "Invalid permissions" + @configuration = { + 'adapter' => 'mysql', + 'database' => 'my-app-db', + 'username' => 'pat', + 'password' => 'wossname' + } + + $stdin.stubs(:gets).returns("secret\n") + $stdout.stubs(:print).returns(nil) + @error.stubs(:errno).returns(1045) + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection). + raises(@error). + then.returns(true) end - @connection = stub("Connection", create_database: true) - @error = Mysql::Error.new "Invalid permissions" - @configuration = { - 'adapter' => 'mysql', - 'database' => 'my-app-db', - 'username' => 'pat', - 'password' => 'wossname' - } - - $stdin.stubs(:gets).returns("secret\n") - $stdout.stubs(:print).returns(nil) - @error.stubs(:errno).returns(1045) - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection). - raises(@error). - then.returns(true) - end + if defined?(::Mysql) + def test_root_password_is_requested + assert_permissions_granted_for "pat" + $stdin.expects(:gets).returns("secret\n") - def test_root_password_is_requested - assert_permissions_granted_for "pat" - skip "only if mysql is available" unless defined?(::Mysql) - $stdin.expects(:gets).returns("secret\n") + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + end - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end + def test_connection_established_as_root + assert_permissions_granted_for "pat" + ActiveRecord::Base.expects(:establish_connection).with( + 'adapter' => 'mysql', + 'database' => nil, + 'username' => 'root', + 'password' => 'secret' + ) - def test_connection_established_as_root - assert_permissions_granted_for "pat" - ActiveRecord::Base.expects(:establish_connection).with( - 'adapter' => 'mysql', - 'database' => nil, - 'username' => 'root', - 'password' => 'secret' - ) + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end + def test_database_created_by_root + assert_permissions_granted_for "pat" + @connection.expects(:create_database). + with('my-app-db', :charset => 'utf8', :collation => 'utf8_unicode_ci') - def test_database_created_by_root - assert_permissions_granted_for "pat" - @connection.expects(:create_database). - with('my-app-db', :charset => 'utf8', :collation => 'utf8_unicode_ci') + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end + def test_grant_privileges_for_normal_user + assert_permissions_granted_for "pat" + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end - def test_grant_privileges_for_normal_user - assert_permissions_granted_for "pat" - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end + def test_do_not_grant_privileges_for_root_user + @configuration['username'] = 'root' + @configuration['password'] = '' + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end - def test_do_not_grant_privileges_for_root_user - @configuration['username'] = 'root' - @configuration['password'] = '' - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end + def test_connection_established_as_normal_user + assert_permissions_granted_for "pat" + ActiveRecord::Base.expects(:establish_connection).returns do + ActiveRecord::Base.expects(:establish_connection).with( + 'adapter' => 'mysql', + 'database' => 'my-app-db', + 'username' => 'pat', + 'password' => 'secret' + ) - def test_connection_established_as_normal_user - assert_permissions_granted_for "pat" - ActiveRecord::Base.expects(:establish_connection).returns do - ActiveRecord::Base.expects(:establish_connection).with( - 'adapter' => 'mysql', - 'database' => 'my-app-db', - 'username' => 'pat', - 'password' => 'secret' - ) + raise @error + end - raise @error + ActiveRecord::Tasks::DatabaseTasks.create @configuration end - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end + def test_sends_output_to_stderr_when_other_errors + @error.stubs(:errno).returns(42) - def test_sends_output_to_stderr_when_other_errors - @error.stubs(:errno).returns(42) + $stderr.expects(:puts).at_least_once.returns(nil) - $stderr.expects(:puts).at_least_once.returns(nil) + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end - ActiveRecord::Tasks::DatabaseTasks.create @configuration + private + def assert_permissions_granted_for(db_user) + db_name = @configuration['database'] + db_password = @configuration['password'] + @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON #{db_name}.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;") + end end - - private - def assert_permissions_granted_for(db_user) - db_name = @configuration['database'] - db_password = @configuration['password'] - @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON #{db_name}.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;") - end end class MySQLDBDropTest < ActiveRecord::TestCase @@ -280,6 +279,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/test_case.rb b/activerecord/test/cases/test_case.rb index 8c6d189b0c..4476ce3410 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -77,8 +77,8 @@ module ActiveRecord # FIXME: this needs to be refactored so specific database can add their own # ignored SQL, or better yet, use a different notification for the queries # instead examining the SQL content. - oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im] - mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/] + oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im] + mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i] postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i] sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im] diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index ff1b01556d..2953b2b2be 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -11,6 +11,7 @@ class TimestampTest < ActiveRecord::TestCase def setup @developer = Developer.first + @owner = Owner.first @developer.update_columns(updated_at: Time.now.prev_month) @previously_updated_at = @developer.updated_at end @@ -92,6 +93,53 @@ class TimestampTest < ActiveRecord::TestCase assert_nothing_raised { Car.first.touch } end + def test_touching_a_no_touching_object + Developer.no_touching do + assert @developer.no_touching? + assert !@owner.no_touching? + @developer.touch + end + + assert !@developer.no_touching? + assert !@owner.no_touching? + assert_equal @previously_updated_at, @developer.updated_at + end + + def test_touching_related_objects + @owner = Owner.first + @previously_updated_at = @owner.updated_at + + Owner.no_touching do + @owner.pets.first.touch + end + + assert_equal @previously_updated_at, @owner.updated_at + end + + def test_global_no_touching + ActiveRecord::Base.no_touching do + assert @developer.no_touching? + assert @owner.no_touching? + @developer.touch + end + + assert !@developer.no_touching? + assert !@owner.no_touching? + assert_equal @previously_updated_at, @developer.updated_at + end + + def test_no_touching_threadsafe + Thread.new do + Developer.no_touching do + assert @developer.no_touching? + + sleep(1) + end + end + + assert !@developer.no_touching? + end + def test_saving_a_record_with_a_belongs_to_that_specifies_touching_the_parent_should_update_the_parent_updated_at pet = Pet.first owner = pet.owner @@ -278,31 +326,31 @@ class TimestampTest < ActiveRecord::TestCase def test_timestamp_attributes_for_create toy = Toy.first - assert_equal toy.send(:timestamp_attributes_for_create), [:created_at, :created_on] + assert_equal [:created_at, :created_on], toy.send(:timestamp_attributes_for_create) end def test_timestamp_attributes_for_update toy = Toy.first - assert_equal toy.send(:timestamp_attributes_for_update), [:updated_at, :updated_on] + assert_equal [:updated_at, :updated_on], toy.send(:timestamp_attributes_for_update) end def test_all_timestamp_attributes toy = Toy.first - assert_equal toy.send(:all_timestamp_attributes), [:created_at, :created_on, :updated_at, :updated_on] + assert_equal [:created_at, :created_on, :updated_at, :updated_on], toy.send(:all_timestamp_attributes) end def test_timestamp_attributes_for_create_in_model toy = Toy.first - assert_equal toy.send(:timestamp_attributes_for_create_in_model), [:created_at] + assert_equal [:created_at], toy.send(:timestamp_attributes_for_create_in_model) end def test_timestamp_attributes_for_update_in_model toy = Toy.first - assert_equal toy.send(:timestamp_attributes_for_update_in_model), [:updated_at] + assert_equal [:updated_at], toy.send(:timestamp_attributes_for_update_in_model) end def test_all_timestamp_attributes_in_model toy = Toy.first - assert_equal toy.send(:all_timestamp_attributes_in_model), [:created_at, :updated_at] + assert_equal [:created_at, :updated_at], toy.send(:all_timestamp_attributes_in_model) end end diff --git a/activerecord/test/cases/transaction_isolation_test.rb b/activerecord/test/cases/transaction_isolation_test.rb index 4f1cb99b68..84c16fb109 100644 --- a/activerecord/test/cases/transaction_isolation_test.rb +++ b/activerecord/test/cases/transaction_isolation_test.rb @@ -1,113 +1,105 @@ require 'cases/helper' -class TransactionIsolationUnsupportedTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false +unless ActiveRecord::Base.connection.supports_transaction_isolation? + class TransactionIsolationUnsupportedTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false - class Tag < ActiveRecord::Base - end - - setup do - if ActiveRecord::Base.connection.supports_transaction_isolation? - skip "database supports transaction isolation; test is irrelevant" + class Tag < ActiveRecord::Base end - end - test "setting the isolation level raises an error" do - assert_raises(ActiveRecord::TransactionIsolationError) do - Tag.transaction(isolation: :serializable) { } + test "setting the isolation level raises an error" do + assert_raises(ActiveRecord::TransactionIsolationError) do + Tag.transaction(isolation: :serializable) { } + end end end end -class TransactionIsolationTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false - - class Tag < ActiveRecord::Base - self.table_name = 'tags' - end - - class Tag2 < ActiveRecord::Base - self.table_name = 'tags' - end +if ActiveRecord::Base.connection.supports_transaction_isolation? + class TransactionIsolationTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false - setup do - unless ActiveRecord::Base.connection.supports_transaction_isolation? - skip "database does not support setting transaction isolation" + class Tag < ActiveRecord::Base + self.table_name = 'tags' end - Tag.establish_connection 'arunit' - Tag2.establish_connection 'arunit' - Tag.destroy_all - end - - # It is impossible to properly test read uncommitted. The SQL standard only - # specifies what must not happen at a certain level, not what must happen. At - # the read uncommitted level, there is nothing that must not happen. - test "read uncommitted" do - unless ActiveRecord::Base.connection.transaction_isolation_levels.include?(:read_uncommitted) - skip "database does not support read uncommitted isolation level" + class Tag2 < ActiveRecord::Base + self.table_name = 'tags' end - Tag.transaction(isolation: :read_uncommitted) do - assert_equal 0, Tag.count - Tag2.create - assert_equal 1, Tag.count + + setup do + Tag.establish_connection 'arunit' + Tag2.establish_connection 'arunit' + Tag.destroy_all end - end - # We are testing that a dirty read does not happen - test "read committed" do - Tag.transaction(isolation: :read_committed) do - assert_equal 0, Tag.count + # It is impossible to properly test read uncommitted. The SQL standard only + # specifies what must not happen at a certain level, not what must happen. At + # the read uncommitted level, there is nothing that must not happen. + if ActiveRecord::Base.connection.transaction_isolation_levels.include?(:read_uncommitted) + test "read uncommitted" do + Tag.transaction(isolation: :read_uncommitted) do + assert_equal 0, Tag.count + Tag2.create + assert_equal 1, Tag.count + end + end + end - Tag2.transaction do - Tag2.create + # We are testing that a dirty read does not happen + test "read committed" do + Tag.transaction(isolation: :read_committed) do assert_equal 0, Tag.count + + Tag2.transaction do + Tag2.create + assert_equal 0, Tag.count + end end + + assert_equal 1, Tag.count end - assert_equal 1, Tag.count - end + # We are testing that a nonrepeatable read does not happen + if ActiveRecord::Base.connection.transaction_isolation_levels.include?(:repeatable_read) + test "repeatable read" do + tag = Tag.create(name: 'jon') - # We are testing that a nonrepeatable read does not happen - test "repeatable read" do - unless ActiveRecord::Base.connection.transaction_isolation_levels.include?(:repeatable_read) - skip "database does not support repeatable read isolation level" - end - tag = Tag.create(name: 'jon') + Tag.transaction(isolation: :repeatable_read) do + tag.reload + Tag2.find(tag.id).update(name: 'emily') - Tag.transaction(isolation: :repeatable_read) do - tag.reload - Tag2.find(tag.id).update(name: 'emily') + tag.reload + assert_equal 'jon', tag.name + end - tag.reload - assert_equal 'jon', tag.name + tag.reload + assert_equal 'emily', tag.name + end end - tag.reload - assert_equal 'emily', tag.name - end - - # We are only testing that there are no errors because it's too hard to - # test serializable. Databases behave differently to enforce the serializability - # constraint. - test "serializable" do - Tag.transaction(isolation: :serializable) do - Tag.create + # We are only testing that there are no errors because it's too hard to + # test serializable. Databases behave differently to enforce the serializability + # constraint. + test "serializable" do + Tag.transaction(isolation: :serializable) do + Tag.create + end end - end - test "setting isolation when joining a transaction raises an error" do - Tag.transaction do - assert_raises(ActiveRecord::TransactionIsolationError) do - Tag.transaction(isolation: :serializable) { } + test "setting isolation when joining a transaction raises an error" do + Tag.transaction do + assert_raises(ActiveRecord::TransactionIsolationError) do + Tag.transaction(isolation: :serializable) { } + end end end - end - test "setting isolation when starting a nested transaction raises error" do - Tag.transaction do - assert_raises(ActiveRecord::TransactionIsolationError) do - Tag.transaction(requires_new: true, isolation: :serializable) { } + test "setting isolation when starting a nested transaction raises error" do + Tag.transaction do + assert_raises(ActiveRecord::TransactionIsolationError) do + Tag.transaction(requires_new: true, isolation: :serializable) { } + end end end end diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index a5cb22aaf6..89dab16975 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' @@ -453,6 +485,13 @@ class TransactionTest < ActiveRecord::TestCase raise ActiveRecord::Rollback end end + ensure + begin + Topic.connection.remove_column('topics', 'stuff') + rescue + ensure + Topic.reset_column_information + end end def test_transactions_state_from_rollback @@ -539,23 +578,23 @@ if current_adapter?(:PostgreSQLAdapter) class ConcurrentTransactionTest < TransactionTest # This will cause transactions to overlap and fail unless they are performed on # separate database connections. - def test_transaction_per_thread - skip "in memory db can't share a db between threads" if in_memory_db? - - threads = 3.times.map do - Thread.new do - Topic.transaction do - topic = Topic.find(1) - topic.approved = !topic.approved? - assert topic.save! - topic.approved = !topic.approved? - assert topic.save! + unless in_memory_db? + def test_transaction_per_thread + threads = 3.times.map do + Thread.new do + Topic.transaction do + topic = Topic.find(1) + topic.approved = !topic.approved? + assert topic.save! + topic.approved = !topic.approved? + assert topic.save! + end + Topic.connection.close end - Topic.connection.close end - end - threads.each { |t| t.join } + threads.each { |t| t.join } + end end # Test for dirty reads among simultaneous transactions. diff --git a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb index 32d2bf746f..a73c3bf1af 100644 --- a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb @@ -55,22 +55,30 @@ class I18nGenerateMessageValidationTest < ActiveRecord::TestCase end test "translation for 'taken' can be overridden" do - I18n.backend.store_translations "en", {errors: {attributes: {title: {taken: "Custom taken message" }}}} - assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title') + reset_i18n_load_path do + I18n.backend.store_translations "en", {errors: {attributes: {title: {taken: "Custom taken message" }}}} + assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title') + end end test "translation for 'taken' can be overridden in activerecord scope" do - I18n.backend.store_translations "en", {activerecord: {errors: {messages: {taken: "Custom taken message" }}}} - assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title') + reset_i18n_load_path do + I18n.backend.store_translations "en", {activerecord: {errors: {messages: {taken: "Custom taken message" }}}} + assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title') + end end test "translation for 'taken' can be overridden in activerecord model scope" do - I18n.backend.store_translations "en", {activerecord: {errors: {models: {topic: {taken: "Custom taken message" }}}}} - assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title') + reset_i18n_load_path do + I18n.backend.store_translations "en", {activerecord: {errors: {models: {topic: {taken: "Custom taken message" }}}}} + assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title') + end end test "translation for 'taken' can be overridden in activerecord attributes scope" do - I18n.backend.store_translations "en", {activerecord: {errors: {models: {topic: {attributes: {title: {taken: "Custom taken message" }}}}}}} - assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title') + reset_i18n_load_path do + I18n.backend.store_translations "en", {activerecord: {errors: {models: {topic: {attributes: {title: {taken: "Custom taken message" }}}}}}} + assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title') + end end end diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 2b33f01783..71cb9d7b1e 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -35,6 +35,11 @@ class Employee < ActiveRecord::Base validates_uniqueness_of :nicknames end +class TopicWithUniqEvent < Topic + belongs_to :event, foreign_key: :parent_id + validates :event, uniqueness: true +end + class UniquenessValidationTest < ActiveRecord::TestCase fixtures :topics, 'warehouse-things', :developers @@ -365,15 +370,29 @@ class UniquenessValidationTest < ActiveRecord::TestCase } end - def test_validate_uniqueness_with_array_column - return skip "Uniqueness on arrays has only been tested in PostgreSQL so far." if !current_adapter? :PostgreSQLAdapter + if current_adapter? :PostgreSQLAdapter + def test_validate_uniqueness_with_array_column + e1 = Employee.create("nicknames" => ["john", "johnny"], "commission_by_quarter" => [1000, 1200]) + assert e1.persisted?, "Saving e1" - e1 = Employee.create("nicknames" => ["john", "johnny"], "commission_by_quarter" => [1000, 1200]) - assert e1.persisted?, "Saving e1" + e2 = Employee.create("nicknames" => ["john", "johnny"], "commission_by_quarter" => [2200]) + assert !e2.persisted?, "e2 shouldn't be valid" + assert e2.errors[:nicknames].any?, "Should have errors for nicknames" + assert_equal ["has already been taken"], e2.errors[:nicknames], "Should have uniqueness message for nicknames" + end + end + + def test_validate_uniqueness_on_existing_relation + event = Event.create + assert TopicWithUniqEvent.create(event: event).valid? + + topic = TopicWithUniqEvent.new(event: event) + assert_not topic.valid? + assert_equal ['has already been taken'], topic.errors[:event] + end - e2 = Employee.create("nicknames" => ["john", "johnny"], "commission_by_quarter" => [2200]) - assert !e2.persisted?, "e2 shouldn't be valid" - assert e2.errors[:nicknames].any?, "Should have errors for nicknames" - assert_equal ["has already been taken"], e2.errors[:nicknames], "Should have uniqueness message for nicknames" + def test_validate_uniqueness_on_empty_relation + topic = TopicWithUniqEvent.new + assert topic.valid? end end 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..15815d56e4 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 @@ -49,4 +43,8 @@ class YamlSerializationTest < ActiveRecord::TestCase t = Psych.load Psych.dump topic assert_equal topic.attributes, t.attributes end + + def test_active_record_relation_serialization + [Topic.all].to_yaml + end end 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/author.rb b/activerecord/test/models/author.rb index feb828de31..794d1af43d 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -8,9 +8,7 @@ class Author < ActiveRecord::Base has_many :posts_sorted_by_id_limited, -> { order('posts.id').limit(1) }, :class_name => "Post" has_many :posts_with_categories, -> { includes(:categories) }, :class_name => "Post" has_many :posts_with_comments_and_categories, -> { includes(:comments, :categories).order("posts.id") }, :class_name => "Post" - has_many :posts_containing_the_letter_a, :class_name => "Post" has_many :posts_with_special_categorizations, :class_name => 'PostWithSpecialCategorization' - has_many :posts_with_extension, :class_name => "Post" has_one :post_about_thinking, -> { where("posts.title like '%thinking%'") }, :class_name => 'Post' has_one :post_about_thinking_with_last_comment, -> { where("posts.title like '%thinking%'").includes(:last_comment) }, :class_name => 'Post' has_many :comments, through: :posts do @@ -31,8 +29,14 @@ class Author < ActiveRecord::Base has_many :thinking_posts, -> { where(:title => 'So I was thinking') }, :dependent => :delete_all, :class_name => 'Post' has_many :welcome_posts, -> { where(:title => 'Welcome to the weblog') }, :class_name => 'Post' + has_many :welcome_posts_with_comment, + -> { where(title: 'Welcome to the weblog').where('comments_count = ?', 1) }, + class_name: 'Post' + has_many :welcome_posts_with_comments, + -> { where(title: 'Welcome to the weblog').where(Post.arel_table[:comments_count].gt(0)) }, + class_name: 'Post' + has_many :comments_desc, -> { order('comments.id DESC') }, :through => :posts, :source => :comments - has_many :limited_comments, -> { limit(1) }, :through => :posts, :source => :comments has_many :funky_comments, :through => :posts, :source => :comments has_many :ordered_uniq_comments, -> { distinct.order('comments.id') }, :through => :posts, :source => :comments has_many :ordered_uniq_comments_desc, -> { distinct.order('comments.id DESC') }, :through => :posts, :source => :comments diff --git a/activerecord/test/models/auto_id.rb b/activerecord/test/models/auto_id.rb index d720e2be5e..82c6544bd5 100644 --- a/activerecord/test/models/auto_id.rb +++ b/activerecord/test/models/auto_id.rb @@ -1,4 +1,4 @@ class AutoId < ActiveRecord::Base - def self.table_name () "auto_id_tests" end - def self.primary_key () "auto_id" end + self.table_name = "auto_id_tests" + self.primary_key = "auto_id" end diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb index 5458a28cc9..4cb2c7606b 100644 --- a/activerecord/test/models/book.rb +++ b/activerecord/test/models/book.rb @@ -2,8 +2,16 @@ 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} + + def published! + super + "do publish work..." + end 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/car.rb b/activerecord/test/models/car.rb index a14a9febba..8f3b70a7c6 100644 --- a/activerecord/test/models/car.rb +++ b/activerecord/test/models/car.rb @@ -1,12 +1,10 @@ class Car < ActiveRecord::Base - has_many :bulbs + has_many :all_bulbs, -> { unscope where: :name }, class_name: "Bulb" has_many :funky_bulbs, class_name: 'FunkyBulb', dependent: :destroy has_many :foo_bulbs, -> { where(:name => 'foo') }, :class_name => "Bulb" - has_many :frickinawesome_bulbs, -> { where :frickinawesome => true }, :class_name => "Bulb" has_one :bulb - has_one :frickinawesome_bulb, -> { where :frickinawesome => true }, :class_name => "Bulb" has_many :tyres has_many :engines, :dependent => :destroy 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/citation.rb b/activerecord/test/models/citation.rb index 545aa8110d..3d87eb795c 100644 --- a/activerecord/test/models/citation.rb +++ b/activerecord/test/models/citation.rb @@ -1,6 +1,3 @@ class Citation < ActiveRecord::Base belongs_to :reference_of, :class_name => "Book", :foreign_key => :book2_id - - belongs_to :book1, :class_name => "Book", :foreign_key => :book1_id - belongs_to :book2, :class_name => "Book", :foreign_key => :book2_id end diff --git a/activerecord/test/models/club.rb b/activerecord/test/models/club.rb index 816c5e6937..566e0873f1 100644 --- a/activerecord/test/models/club.rb +++ b/activerecord/test/models/club.rb @@ -2,7 +2,6 @@ class Club < ActiveRecord::Base has_one :membership has_many :memberships, :inverse_of => false has_many :members, :through => :memberships - has_many :current_memberships has_one :sponsor has_one :sponsored_member, :through => :sponsor, :source => :sponsorable, :source_type => "Member" belongs_to :category diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index c5d4ec0833..76411ecb37 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 @@ -35,11 +40,13 @@ module Namespaced end class Firm < Company + to_param :name + has_many :clients, -> { order "id" }, :dependent => :destroy, :before_remove => :log_before_remove, :after_remove => :log_after_remove has_many :unsorted_clients, :class_name => "Client" has_many :unsorted_clients_with_symbol, :class_name => :Client has_many :clients_sorted_desc, -> { order "id DESC" }, :class_name => "Client" - has_many :clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client" + has_many :clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client", :inverse_of => :firm has_many :clients_ordered_by_name, -> { order "name" }, :class_name => "Client" has_many :unvalidated_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :validate => false has_many :dependent_clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client", :dependent => :destroy @@ -49,7 +56,6 @@ class Firm < Company has_many :clients_like_ms, -> { where("name = 'Microsoft'").order("id") }, :class_name => "Client" has_many :clients_like_ms_with_hash_conditions, -> { where(:name => 'Microsoft').order("id") }, :class_name => "Client" has_many :plain_clients, :class_name => 'Client' - has_many :readonly_clients, -> { readonly }, :class_name => 'Client' has_many :clients_using_primary_key, :class_name => 'Client', :primary_key => 'name', :foreign_key => 'firm_name' has_many :clients_using_primary_key_with_delete_all, :class_name => 'Client', @@ -118,6 +124,10 @@ class Client < Company has_many :accounts, :through => :firm, :source => :accounts belongs_to :account + validate do + firm + end + class RaisedOnSave < RuntimeError; end attr_accessor :raise_on_save before_save do @@ -167,7 +177,6 @@ class ExclusivelyDependentFirm < Company has_one :account, :foreign_key => "firm_id", :dependent => :delete has_many :dependent_sanitized_conditional_clients_of_firm, -> { order("id").where("name = 'BigShot Inc.'") }, :foreign_key => "client_of", :class_name => "Client", :dependent => :delete_all has_many :dependent_conditional_clients_of_firm, -> { order("id").where("name = ?", 'BigShot Inc.') }, :foreign_key => "client_of", :class_name => "Client", :dependent => :delete_all - has_many :dependent_hash_conditional_clients_of_firm, -> { order("id").where(:name => 'BigShot Inc.') }, :foreign_key => "client_of", :class_name => "Client", :dependent => :delete_all end class SpecialClient < Client diff --git a/activerecord/test/models/contract.rb b/activerecord/test/models/contract.rb index 2cf5aa7a85..cdf7b267b5 100644 --- a/activerecord/test/models/contract.rb +++ b/activerecord/test/models/contract.rb @@ -1,6 +1,7 @@ class Contract < ActiveRecord::Base belongs_to :company belongs_to :developer + belongs_to :firm, :foreign_key => 'company_id' before_save :hi after_save :bye 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/developer.rb b/activerecord/test/models/developer.rb index c8e2be580e..a26de55758 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -38,6 +38,8 @@ class Developer < ActiveRecord::Base has_and_belongs_to_many :special_projects, :join_table => 'developers_projects', :association_foreign_key => 'project_id' has_many :audit_logs + has_many :contracts + has_many :firms, :through => :contracts, :source => :firm scope :jamises, -> { where(:name => 'Jamis') } 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/member.rb b/activerecord/test/models/member.rb index cc47c7bc18..72095f9236 100644 --- a/activerecord/test/models/member.rb +++ b/activerecord/test/models/member.rb @@ -2,7 +2,6 @@ class Member < ActiveRecord::Base has_one :current_membership has_one :selected_membership has_one :membership - has_many :fellow_members, :through => :club, :source => :members has_one :club, :through => :current_membership has_one :selected_club, :through => :selected_membership, :source => :club has_one :favourite_club, -> { where "memberships.favourite = ?", true }, :through => :membership, :source => :club diff --git a/activerecord/test/models/membership.rb b/activerecord/test/models/membership.rb index bcbb7e42c5..df7167ee93 100644 --- a/activerecord/test/models/membership.rb +++ b/activerecord/test/models/membership.rb @@ -8,6 +8,11 @@ class CurrentMembership < Membership belongs_to :club end +class SuperMembership < Membership + belongs_to :member, -> { order('members.id DESC') } + belongs_to :club +end + class SelectedMembership < Membership def self.default_scope select("'1' as foo") diff --git a/activerecord/test/models/movie.rb b/activerecord/test/models/movie.rb index 6384b4c801..c441be2bef 100644 --- a/activerecord/test/models/movie.rb +++ b/activerecord/test/models/movie.rb @@ -1,5 +1,3 @@ class Movie < ActiveRecord::Base - def self.primary_key - "movieid" - end + self.primary_key = "movieid" end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 25fb2cffe2..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' }, @@ -130,7 +133,6 @@ class Post < ActiveRecord::Base has_many :secure_readers has_many :readers_with_person, -> { includes(:person) }, :class_name => "Reader" has_many :people, :through => :readers - has_many :secure_people, :through => :secure_readers has_many :single_people, :through => :readers has_many :people_with_callbacks, :source=>:person, :through => :readers, :before_add => lambda {|owner, reader| log(:added, :before, reader.first_name) }, diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb index c094b726b4..7f42a4b1f8 100644 --- a/activerecord/test/models/project.rb +++ b/activerecord/test/models/project.rb @@ -1,7 +1,6 @@ class Project < ActiveRecord::Base has_and_belongs_to_many :developers, -> { distinct.order 'developers.name desc, developers.id desc' } has_and_belongs_to_many :readonly_developers, -> { readonly }, :class_name => "Developer" - has_and_belongs_to_many :selected_developers, -> { distinct.select "developers.*" }, :class_name => "Developer" has_and_belongs_to_many :non_unique_developers, -> { order 'developers.name desc, developers.id desc' }, :class_name => 'Developer' has_and_belongs_to_many :limited_developers, -> { limit 1 }, :class_name => "Developer" has_and_belongs_to_many :developers_named_david, -> { where("name = 'David'").distinct }, :class_name => "Developer" diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index 17035bf338..40c8e97fc2 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -34,7 +34,6 @@ class Topic < ActiveRecord::Base has_many :replies, :dependent => :destroy, :foreign_key => "parent_id" has_many :approved_replies, -> { approved }, class_name: 'Reply', foreign_key: "parent_id", counter_cache: 'replies_count' - has_many :replies_with_primary_key, :class_name => "Reply", :dependent => :destroy, :primary_key => "title", :foreign_key => "parent_title" has_many :unique_replies, :dependent => :destroy, :foreign_key => "parent_id" has_many :silly_unique_replies, :dependent => :destroy, :foreign_key => "parent_id" 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 f15297f279..104b5eafa1 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,157 @@ +* Add `ActiveSupport::Testing::TimeHelpers#travel` and `#travel_to`. These methods change current + time to the given time or time difference by stubbing `Time.now` and `Date.today` to return the + time or date after the difference calculation, or the time or date that got passed into the + method respectively. These methods also accept a block, which will return current time back to + its original state at the end of the block. + + Example for `#travel`: + + Time.now # => 2013-11-09 15:34:49 -05:00 + travel 1.day + Time.now # => 2013-11-10 15:34:49 -05:00 + Date.today # => Sun, 10 Nov 2013 + + Example for `#travel_to`: + + Time.now # => 2013-11-09 15:34:49 -05:00 + travel_to Time.new(2004, 11, 24, 01, 04, 44) + Time.now # => 2004-11-24 01:04:44 -05:00 + Date.today # => Wed, 24 Nov 2004 + + Both of these methods also accept a block, which will return the current time back to its + original state at the end of the block: + + Time.now # => 2013-11-09 15:34:49 -05:00 + + travel 1.day do + User.create.created_at # => Sun, 10 Nov 2013 15:34:49 EST -05:00 + end + + travel_to Time.new(2004, 11, 24, 01, 04, 44) do + User.create.created_at # => Wed, 24 Nov 2004 01:04:44 EST -05:00 + end + + Time.now # => 2013-11-09 15:34:49 -05:00 + + This module is included in `ActiveSupport::TestCase` automatically. + + *Prem Sichanugrist*, *DHH* + +* Unify `cattr_*` interface: allow to pass a block to `cattr_reader`. + + Example: + + class A + cattr_reader(:defr) { 'default_reader_value' } + end + A.defr # => 'default_reader_value' + + *Alexey Chernenkov* + +* Improved compatibility with the stdlib JSON gem. + + Previously, calling `::JSON.{generate,dump}` sometimes causes unexpected + failures such as intridea/multi_json#86. + + `::JSON.{generate,dump}` now bypasses the ActiveSupport JSON encoder + completely and yields the same result with or without ActiveSupport. This + means that it will **not** call `as_json` and will ignore any options that + the JSON gem does not natively understand. To invoke ActiveSupport's JSON + encoder instead, use `obj.to_json(options)` or + `ActiveSupport::JSON.encode(obj, options)`. + + *Godfrey Chan* + +* Fix Active Support `Time#to_json` and `DateTime#to_json` to return 3 decimal + places worth of fractional seconds, similar to `TimeWithZone`. + + *Ryan Glover* + +* Removed circular reference protection in JSON encoder, deprecated + `ActiveSupport::JSON::Encoding::CircularReferenceError`. + + *Godfrey Chan*, *Sergio Campamá* + +* Add `capitalize` option to `Inflector.humanize`, so strings can be humanized without being capitalized: + + 'employee_salary'.humanize # => "Employee salary" + 'employee_salary'.humanize(capitalize: false) # => "employee salary" + + *claudiob* + +* Fixed `Object#as_json` and `Struct#as_json` not working properly with options. They now take + the same options as `Hash#as_json`: + + struct = Struct.new(:foo, :bar).new + struct.foo = "hello" + struct.bar = "world" + json = struct.as_json(only: [:foo]) # => {foo: "hello"} + + *Sergio Campamá*, *Godfrey Chan* + +* 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. + + *Bogdan Gusiev* + +* Allow attaching event subscribers to ActiveSupport::Notifications namespaces + before they're defined. Essentially, this means instead of this: + + class JokeSubscriber < ActiveSupport::Subscriber + def sql(event) + puts "A rabbi and a priest walk into a bar..." + end + + # This call needs to happen *after* defining the methods. + attach_to "active_record" + end + + You can do this: + + 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..." + end + end + + This should make it easier to read and understand these subscribers. + + *Daniel Schierbeck* + +* Add `Date#middle_of_day`, `DateTime#middle_of_day` and `Time#middle_of_day` methods. + + Also added `midday`, `noon`, `at_midday`, `at_noon` and `at_middle_of_day` as aliases. + + *Anatoli Makarevich* + +* Fix ActiveSupport::Cache::FileStore#cleanup to no longer rely on missing each_key method. + + *Murray Steele* + * Ensure that autoloaded constants in all-caps nestings are marked as autoloaded. diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index ffc2c2074e..c27c50e47b 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -22,7 +22,7 @@ Gem::Specification.new do |s| s.add_dependency('i18n', '~> 0.6', '>= 0.6.4') s.add_dependency 'json', '~> 1.7' - s.add_dependency 'tzinfo', '~> 0.3.37' + s.add_dependency 'tzinfo', '~> 1.1' s.add_dependency 'minitest', '~> 5.0' s.add_dependency 'thread_safe','~> 0.1' end diff --git a/activesupport/bin/generate_tables b/activesupport/bin/generate_tables index 5fefa429df..f39e89b7d0 100755 --- a/activesupport/bin/generate_tables +++ b/activesupport/bin/generate_tables @@ -28,12 +28,6 @@ module ActiveSupport def initialize @ucd = Unicode::UnicodeDatabase.new - - default = Codepoint.new - default.combining_class = 0 - default.uppercase_mapping = 0 - default.lowercase_mapping = 0 - @ucd.codepoints = Hash.new(default) end def parse_codepoints(line) diff --git a/activesupport/lib/active_support/all.rb b/activesupport/lib/active_support/all.rb index 151008bbaa..f537818300 100644 --- a/activesupport/lib/active_support/all.rb +++ b/activesupport/lib/active_support/all.rb @@ -1,4 +1,3 @@ require 'active_support' -require 'active_support/deprecation' require 'active_support/time' require 'active_support/core_ext' diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 308a1b2cd9..912fd1a20a 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 @@ -234,7 +232,7 @@ module ActiveSupport # bump the cache expiration time by the value set in <tt>:race_condition_ttl</tt>. # Yes, this process is extending the time for a stale value by another few # seconds. Because of extended life of the previous cache, other processes - # will continue to use slightly stale data for a just a big longer. In the + # will continue to use slightly stale data for a just a bit longer. In the # meantime that first process will go ahead and will write into cache the # new value. After that all the processes will start getting new value. # The key is to keep <tt>:race_condition_ttl</tt> small. @@ -363,7 +361,7 @@ module ActiveSupport # # cache.write("bim", "bam") # cache.fetch_multi("bim", "boom") {|key| key * 2 } - # #=> ["bam", "boomboom"] + # # => ["bam", "boomboom"] # def fetch_multi(*names) options = names.extract_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,16 +395,18 @@ 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 end - # Return +true+ if the cache contains an entry for the given key. + # Returns +true+ if the cache contains an entry for the given key. # # 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 472f23c1c5..5cd6065077 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -33,7 +33,8 @@ module ActiveSupport # Premptively iterates through all stored keys and removes the ones which have expired. def cleanup(options = nil) options = merged_options(options) - each_key(options) do |key| + search_dir(cache_path) do |fname| + key = file_path_key(fname) entry = read_entry(key, options) delete_entry(key, options) if entry && entry.expired? end @@ -96,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/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb index fb42c4a41e..cea7eee924 100644 --- a/activesupport/lib/active_support/cache/strategy/local_cache.rb +++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb @@ -23,6 +23,9 @@ module ActiveSupport def set_cache_for(local_cache_key, value) @registry[local_cache_key] = value end + + def self.set_cache_for(l, v); instance.set_cache_for l, v; end + def self.cache_for(l); instance.cache_for l; end end # Simple memory backed cache. This cache is not thread safe and is intended only diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index 5c738572a8..c3aac31323 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -574,7 +574,7 @@ module ActiveSupport # # set_callback :save, :before_meth # - # The callback can specified as a symbol naming an instance method; as a + # The callback can be specified as a symbol naming an instance method; as a # proc, lambda, or block; as a string to be instance evaluated; or as an # object that responds to a certain method determined by the <tt>:scope</tt> # argument to +define_callback+. 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/array/grouping.rb b/activesupport/lib/active_support/core_ext/array/grouping.rb index 37f007c751..3529d57174 100644 --- a/activesupport/lib/active_support/core_ext/array/grouping.rb +++ b/activesupport/lib/active_support/core_ext/array/grouping.rb @@ -53,7 +53,7 @@ class Array # ["4", "5"] # ["6", "7"] def in_groups(number, fill_with = nil) - # size / number gives minor group size; + # size.div number gives minor group size; # size % number gives how many objects need extra accommodation; # each group hold either division or division + 1 items. division = size.div number @@ -83,10 +83,10 @@ class Array # # [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]] # (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]] - def split(value = nil, &block) - if block + def split(value = nil) + if block_given? inject([[]]) do |results, element| - if block.call(element) + if yield(element) results << [] else results.last << element @@ -95,9 +95,9 @@ class Array results end else - results, arr = [[]], self + results, arr = [[]], self.dup until arr.empty? - if (idx = index(value)) + if (idx = arr.index(value)) results.last.concat(arr.shift(idx)) arr.shift results << [] 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/class/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb index 34859617c9..0cf955b889 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb @@ -29,6 +29,16 @@ class Class # end # # Person.new.hair_colors # => NoMethodError + # + # Also, you can pass a block to set up the attribute with a default value. + # + # class Person + # cattr_reader :hair_colors do + # [:brown, :black, :blonde, :red] + # end + # end + # + # Person.hair_colors # => [:brown, :black, :blonde, :red] def cattr_reader(*syms) options = syms.extract_options! syms.each do |sym| @@ -50,6 +60,7 @@ class Class end EOS end + class_variable_set("@@#{sym}", yield) if block_given? end end @@ -162,7 +173,7 @@ class Class # end # end # - # Person.class_variable_get("@@hair_colors") #=> [:brown, :black, :blonde, :red] + # Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red] def cattr_accessor(*syms, &blk) cattr_reader(*syms) cattr_writer(*syms, &blk) 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/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb index 8930376ac8..2684c772ea 100644 --- a/activesupport/lib/active_support/core_ext/hash/conversions.rb +++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb @@ -10,7 +10,7 @@ require 'active_support/core_ext/string/inflections' class Hash # Returns a string containing an XML representation of its receiver: # - # {'foo' => 1, 'bar' => 2}.to_xml + # { foo: 1, bar: 2 }.to_xml # # => # # <?xml version="1.0" encoding="UTF-8"?> # # <hash> @@ -43,7 +43,10 @@ class Hash # end # # { foo: Foo.new }.to_xml(skip_instruct: true) - # # => "<hash><bar>fooing!</bar></hash>" + # # => + # # <hash> + # # <bar>fooing!</bar> + # # </hash> # # * Otherwise, a node with +key+ as tag is created with a string representation of # +value+ as text node. If +value+ is +nil+ an attribute "nil" set to "true" is added. @@ -201,7 +204,7 @@ module ActiveSupport end def become_empty_string?(value) - # {"string" => true} + # { "string" => true } # No tests fail when the second term is removed. value['type'] == 'string' && value['nil'] != 'true' end diff --git a/activesupport/lib/active_support/core_ext/hash/deep_merge.rb b/activesupport/lib/active_support/core_ext/hash/deep_merge.rb index e07db50b77..dc86c92003 100644 --- a/activesupport/lib/active_support/core_ext/hash/deep_merge.rb +++ b/activesupport/lib/active_support/core_ext/hash/deep_merge.rb @@ -1,13 +1,13 @@ class Hash # Returns a new hash with +self+ and +other_hash+ merged recursively. # - # h1 = { x: { y: [4,5,6] }, z: [7,8,9] } - # h2 = { x: { y: [7,8,9] }, z: 'xyz' } + # h1 = { x: { y: [4, 5, 6] }, z: [7, 8, 9] } + # h2 = { x: { y: [7, 8, 9] }, z: 'xyz' } # - # h1.deep_merge(h2) #=> {x: {y: [7, 8, 9]}, z: "xyz"} - # h2.deep_merge(h1) #=> {x: {y: [4, 5, 6]}, z: [7, 8, 9]} + # h1.deep_merge(h2) # => {x: {y: [7, 8, 9]}, z: "xyz"} + # h2.deep_merge(h1) # => {x: {y: [4, 5, 6]}, z: [7, 8, 9]} # h1.deep_merge(h2) { |key, old, new| Array.wrap(old) + Array.wrap(new) } - # #=> {:x=>{:y=>[4, 5, 6, 7, 8, 9]}, :z=>[7, 8, 9, "xyz"]} + # # => {:x=>{:y=>[4, 5, 6, 7, 8, 9]}, :z=>[7, 8, 9, "xyz"]} def deep_merge(other_hash, &block) dup.deep_merge!(other_hash, &block) end diff --git a/activesupport/lib/active_support/core_ext/hash/except.rb b/activesupport/lib/active_support/core_ext/hash/except.rb index d90e996ad4..682d089881 100644 --- a/activesupport/lib/active_support/core_ext/hash/except.rb +++ b/activesupport/lib/active_support/core_ext/hash/except.rb @@ -1,5 +1,5 @@ class Hash - # Return a hash that includes everything but the given keys. This is useful for + # Returns a hash that includes everything but the given keys. This is useful for # limiting a set of parameters to everything but a few known toggles: # # @person.update(params[:person].except(:admin)) diff --git a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb index 981e8436bf..970d6faa1d 100644 --- a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb +++ b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb @@ -18,5 +18,6 @@ class Hash # # b = { b: 1 } # { a: b }.with_indifferent_access['a'] # calls b.nested_under_indifferent_access + # # => {"b"=>32} alias nested_under_indifferent_access with_indifferent_access end diff --git a/activesupport/lib/active_support/core_ext/hash/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb index b4c451ace4..2114202eb0 100644 --- a/activesupport/lib/active_support/core_ext/hash/keys.rb +++ b/activesupport/lib/active_support/core_ext/hash/keys.rb @@ -1,10 +1,10 @@ class Hash - # Return a new hash with all keys converted using the block operation. + # Returns a new hash with all keys converted using the block operation. # # hash = { name: 'Rob', age: '28' } # # hash.transform_keys{ |key| key.to_s.upcase } - # # => { "NAME" => "Rob", "AGE" => "28" } + # # => {"NAME"=>"Rob", "AGE"=>"28"} def transform_keys result = {} each_key do |key| @@ -22,12 +22,12 @@ class Hash self end - # Return a new hash with all keys converted to strings. + # Returns a new hash with all keys converted to strings. # # hash = { name: 'Rob', age: '28' } # # hash.stringify_keys - # #=> { "name" => "Rob", "age" => "28" } + # # => { "name" => "Rob", "age" => "28" } def stringify_keys transform_keys{ |key| key.to_s } end @@ -38,13 +38,13 @@ class Hash transform_keys!{ |key| key.to_s } end - # Return a new hash with all keys converted to symbols, as long as + # Returns a new hash with all keys converted to symbols, as long as # they respond to +to_sym+. # # hash = { 'name' => 'Rob', 'age' => '28' } # # hash.symbolize_keys - # #=> { name: "Rob", age: "28" } + # # => { name: "Rob", age: "28" } def symbolize_keys transform_keys{ |key| key.to_sym rescue key } end @@ -71,14 +71,14 @@ class Hash end end - # Return a new hash with all keys converted by the block operation. + # Returns a new hash with all keys converted by the block operation. # This includes the keys from the root hash and from all # nested hashes. # # hash = { person: { name: 'Rob', age: '28' } } # # hash.deep_transform_keys{ |key| key.to_s.upcase } - # # => { "PERSON" => { "NAME" => "Rob", "AGE" => "28" } } + # # => {"PERSON"=>{"NAME"=>"Rob", "AGE"=>"28"}} def deep_transform_keys(&block) result = {} each do |key, value| @@ -98,14 +98,14 @@ class Hash self end - # Return a new hash with all keys converted to strings. + # Returns a new hash with all keys converted to strings. # This includes the keys from the root hash and from all # nested hashes. # # hash = { person: { name: 'Rob', age: '28' } } # # hash.deep_stringify_keys - # # => { "person" => { "name" => "Rob", "age" => "28" } } + # # => {"person"=>{"name"=>"Rob", "age"=>"28"}} def deep_stringify_keys deep_transform_keys{ |key| key.to_s } end @@ -117,14 +117,14 @@ class Hash deep_transform_keys!{ |key| key.to_s } end - # Return a new hash with all keys converted to symbols, as long as + # Returns a new hash with all keys converted to symbols, as long as # they respond to +to_sym+. This includes the keys from the root hash # and from all nested hashes. # # hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } } # # hash.deep_symbolize_keys - # # => { person: { name: "Rob", age: "28" } } + # # => {:person=>{:name=>"Rob", :age=>"28"}} def deep_symbolize_keys deep_transform_keys{ |key| key.to_sym rescue key } end 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/integer/multiple.rb b/activesupport/lib/active_support/core_ext/integer/multiple.rb index 7c6c2f1ca7..c668c7c2eb 100644 --- a/activesupport/lib/active_support/core_ext/integer/multiple.rb +++ b/activesupport/lib/active_support/core_ext/integer/multiple.rb @@ -1,9 +1,9 @@ class Integer # Check whether the integer is evenly divisible by the argument. # - # 0.multiple_of?(0) #=> true - # 6.multiple_of?(5) #=> false - # 10.multiple_of?(2) #=> true + # 0.multiple_of?(0) # => true + # 6.multiple_of?(5) # => false + # 10.multiple_of?(2) # => true def multiple_of?(number) number != 0 ? self % number == 0 : zero? end diff --git a/activesupport/lib/active_support/core_ext/kernel/reporting.rb b/activesupport/lib/active_support/core_ext/kernel/reporting.rb index 36ab836457..df11737a6b 100644 --- a/activesupport/lib/active_support/core_ext/kernel/reporting.rb +++ b/activesupport/lib/active_support/core_ext/kernel/reporting.rb @@ -60,8 +60,7 @@ module Kernel # puts 'This code gets executed and nothing related to ZeroDivisionError was seen' def suppress(*exception_classes) yield - rescue Exception => e - raise unless exception_classes.any? { |cls| e.kind_of?(cls) } + rescue *exception_classes end # Captures the given stream and returns it: diff --git a/activesupport/lib/active_support/core_ext/module/deprecation.rb b/activesupport/lib/active_support/core_ext/module/deprecation.rb index d873de197f..56d670fbe8 100644 --- a/activesupport/lib/active_support/core_ext/module/deprecation.rb +++ b/activesupport/lib/active_support/core_ext/module/deprecation.rb @@ -1,5 +1,3 @@ -require 'active_support/deprecation/method_wrappers' - class Module # deprecate :foo # deprecate bar: 'message' 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/blank.rb b/activesupport/lib/active_support/core_ext/object/blank.rb index 8a5eb4bc93..f9ff4d9567 100644 --- a/activesupport/lib/active_support/core_ext/object/blank.rb +++ b/activesupport/lib/active_support/core_ext/object/blank.rb @@ -90,7 +90,7 @@ class String # ' '.blank? # => true # ' something here '.blank? # => false def blank? - self !~ /[^[:space:]]/ + self =~ /\A[[:space:]]*\z/ end end diff --git a/activesupport/lib/active_support/core_ext/object/deep_dup.rb b/activesupport/lib/active_support/core_ext/object/deep_dup.rb index 1d639f3af6..2e99f4a1b8 100644 --- a/activesupport/lib/active_support/core_ext/object/deep_dup.rb +++ b/activesupport/lib/active_support/core_ext/object/deep_dup.rb @@ -8,8 +8,8 @@ class Object # dup = object.deep_dup # dup.instance_variable_set(:@a, 1) # - # object.instance_variable_defined?(:@a) #=> false - # dup.instance_variable_defined?(:@a) #=> true + # object.instance_variable_defined?(:@a) # => false + # dup.instance_variable_defined?(:@a) # => true def deep_dup duplicable? ? dup : self end @@ -22,8 +22,8 @@ class Array # dup = array.deep_dup # dup[1][2] = 4 # - # array[1][2] #=> nil - # dup[1][2] #=> 4 + # array[1][2] # => nil + # dup[1][2] # => 4 def deep_dup map { |it| it.deep_dup } end @@ -36,8 +36,8 @@ class Hash # dup = hash.deep_dup # dup[:a][:c] = 'c' # - # hash[:a][:c] #=> nil - # dup[:a][:c] #=> "c" + # hash[:a][:c] # => nil + # dup[:a][:c] # => "c" def deep_dup each_with_object(dup) do |(key, value), hash| hash[key.deep_dup] = value.deep_dup 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..7c11d44f57 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/object/json.rb @@ -0,0 +1,232 @@ +# Hack to load json gem first so we can overwrite its to_json. +require 'json' +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' +require 'active_support/core_ext/module/aliasing' + +# 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. +# +# On the other hand, we should avoid conflict with ::JSON.{generate,dump}(obj). Unfortunately, the +# JSON gem's encoder relies on its own to_json implementation to encode objects. Since it always +# passes a ::JSON::State object as the only argument to to_json, we can detect that and forward the +# calls to the original to_json method. +# +# It should be noted that when using ::JSON.{generate,dump} directly, ActiveSupport's encoder is +# bypassed completely. This means that as_json won't be invoked and the JSON gem will simply +# ignore any options it does not natively understand. This also means that ::JSON.{generate,dump} +# should give exactly the same results with or without active support. +[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass].each do |klass| + klass.class_eval do + def to_json_with_active_support_encoder(options = nil) + if options.is_a?(::JSON::State) + # Called from JSON.{generate,dump}, forward it to JSON gem's to_json + self.to_json_without_active_support_encoder(options) + else + # to_json is being invoked directly, use ActiveSupport's encoder + ActiveSupport::JSON.encode(self, options) + end + end + + alias_method_chain :to_json, :active_support_encoder + end +end + +class Object + def as_json(options = nil) #:nodoc: + if respond_to?(:to_hash) + to_hash.as_json(options) + else + instance_values.as_json(options) + end + end +end + +class Struct #:nodoc: + def as_json(options = nil) + Hash[members.zip(values)].as_json(options) + 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 are not valid JSON. + 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.encode_big_decimal_as_string = 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: + map { |v| options ? v.as_json(options.dup) : v.as_json } + end + + def encode_json(encoder) #:nodoc: + "[#{map { |v| v.as_json.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 + + Hash[subset.map { |k, v| [k.to_s, options ? v.as_json(options.dup) : v.as_json] }] + end + + def encode_json(encoder) #:nodoc: + "{#{map { |k,v| "#{k.as_json.encode_json(encoder)}:#{v.as_json.encode_json(encoder)}" } * ','}}" + end +end + +class Time + def as_json(options = nil) #:nodoc: + if ActiveSupport.use_standard_json_time_format + xmlschema(3) + 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(3) + 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/core_ext/object/try.rb b/activesupport/lib/active_support/core_ext/object/try.rb index 534bbe3c42..48190e1e66 100644 --- a/activesupport/lib/active_support/core_ext/object/try.rb +++ b/activesupport/lib/active_support/core_ext/object/try.rb @@ -47,7 +47,7 @@ class Object end # Same as #try, but will raise a NoMethodError exception if the receiving is not nil and - # does not implemented the tried method. + # does not implement the tried method. def try!(*a, &b) if a.empty? && block_given? yield self diff --git a/activesupport/lib/active_support/core_ext/range.rb b/activesupport/lib/active_support/core_ext/range.rb index 1d8b1ede5a..9368e81235 100644 --- a/activesupport/lib/active_support/core_ext/range.rb +++ b/activesupport/lib/active_support/core_ext/range.rb @@ -1,3 +1,4 @@ require 'active_support/core_ext/range/conversions' require 'active_support/core_ext/range/include_range' require 'active_support/core_ext/range/overlaps' +require 'active_support/core_ext/range/each' diff --git a/activesupport/lib/active_support/core_ext/range/each.rb b/activesupport/lib/active_support/core_ext/range/each.rb new file mode 100644 index 0000000000..d51ea2e944 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/range/each.rb @@ -0,0 +1,24 @@ +require 'active_support/core_ext/module/aliasing' +require 'active_support/core_ext/object/acts_like' + +class Range #:nodoc: + + def each_with_time_with_zone(&block) + ensure_iteration_allowed + each_without_time_with_zone(&block) + end + alias_method_chain :each, :time_with_zone + + def step_with_time_with_zone(n = 1, &block) + ensure_iteration_allowed + step_without_time_with_zone(n, &block) + end + alias_method_chain :step, :time_with_zone + + private + def ensure_iteration_allowed + if first.acts_like?(:time) + raise TypeError, "can't iterate from #{first.class}" + end + end +end diff --git a/activesupport/lib/active_support/core_ext/string/access.rb b/activesupport/lib/active_support/core_ext/string/access.rb index ee3b6d2b3f..d94e1bfca2 100644 --- a/activesupport/lib/active_support/core_ext/string/access.rb +++ b/activesupport/lib/active_support/core_ext/string/access.rb @@ -8,22 +8,22 @@ class String # the beginning of the range is greater than the end of the string. # # str = "hello" - # str.at(0) #=> "h" - # str.at(1..3) #=> "ell" - # str.at(-2) #=> "l" - # str.at(-2..-1) #=> "lo" - # str.at(5) #=> nil - # str.at(5..-1) #=> "" + # str.at(0) # => "h" + # str.at(1..3) # => "ell" + # str.at(-2) # => "l" + # str.at(-2..-1) # => "lo" + # str.at(5) # => nil + # str.at(5..-1) # => "" # # If a Regexp is given, the matching portion of the string is returned. # If a String is given, that given string is returned if it occurs in # the string. In both cases, nil is returned if there is no match. # # str = "hello" - # str.at(/lo/) #=> "lo" - # str.at(/ol/) #=> nil - # str.at("lo") #=> "lo" - # str.at("ol") #=> nil + # str.at(/lo/) # => "lo" + # str.at(/ol/) # => nil + # str.at("lo") # => "lo" + # str.at("ol") # => nil def at(position) self[position] end @@ -32,15 +32,15 @@ class String # If the position is negative, it is counted from the end of the string. # # str = "hello" - # str.from(0) #=> "hello" - # str.from(3) #=> "lo" - # str.from(-2) #=> "lo" + # str.from(0) # => "hello" + # str.from(3) # => "lo" + # str.from(-2) # => "lo" # # You can mix it with +to+ method and do fun things like: # # str = "hello" - # str.from(0).to(-1) #=> "hello" - # str.from(1).to(-2) #=> "ell" + # str.from(0).to(-1) # => "hello" + # str.from(1).to(-2) # => "ell" def from(position) self[position..-1] end @@ -49,15 +49,15 @@ class String # If the position is negative, it is counted from the end of the string. # # str = "hello" - # str.to(0) #=> "h" - # str.to(3) #=> "hell" - # str.to(-2) #=> "hell" + # str.to(0) # => "h" + # str.to(3) # => "hell" + # str.to(-2) # => "hell" # # You can mix it with +from+ method and do fun things like: # # str = "hello" - # str.from(0).to(-1) #=> "hello" - # str.from(1).to(-2) #=> "ell" + # str.from(0).to(-1) # => "hello" + # str.from(1).to(-2) # => "ell" def to(position) self[0, position + 1] end @@ -67,11 +67,11 @@ class String # given limit is greater than or equal to the string length, returns self. # # str = "hello" - # str.first #=> "h" - # str.first(1) #=> "h" - # str.first(2) #=> "he" - # str.first(0) #=> "" - # str.first(6) #=> "hello" + # str.first # => "h" + # str.first(1) # => "h" + # str.first(2) # => "he" + # str.first(0) # => "" + # str.first(6) # => "hello" def first(limit = 1) if limit == 0 '' @@ -87,11 +87,11 @@ class String # the given limit is greater than or equal to the string length, returns self. # # str = "hello" - # str.last #=> "o" - # str.last(1) #=> "o" - # str.last(2) #=> "lo" - # str.last(0) #=> "" - # str.last(6) #=> "hello" + # str.last # => "o" + # str.last(1) # => "o" + # str.last(2) # => "lo" + # str.last(0) # => "" + # str.last(6) # => "hello" def last(limit = 1) if limit == 0 '' diff --git a/activesupport/lib/active_support/core_ext/string/conversions.rb b/activesupport/lib/active_support/core_ext/string/conversions.rb index 6691fc0995..3e0cb8a7ac 100644 --- a/activesupport/lib/active_support/core_ext/string/conversions.rb +++ b/activesupport/lib/active_support/core_ext/string/conversions.rb @@ -36,20 +36,20 @@ class String # Converts a string to a Date value. # - # "1-1-2012".to_date #=> Sun, 01 Jan 2012 - # "01/01/2012".to_date #=> Sun, 01 Jan 2012 - # "2012-12-13".to_date #=> Thu, 13 Dec 2012 - # "12/13/2012".to_date #=> ArgumentError: invalid date + # "1-1-2012".to_date # => Sun, 01 Jan 2012 + # "01/01/2012".to_date # => Sun, 01 Jan 2012 + # "2012-12-13".to_date # => Thu, 13 Dec 2012 + # "12/13/2012".to_date # => ArgumentError: invalid date def to_date ::Date.parse(self, false) unless blank? end # Converts a string to a DateTime value. # - # "1-1-2012".to_datetime #=> Sun, 01 Jan 2012 00:00:00 +0000 - # "01/01/2012 23:59:59".to_datetime #=> Sun, 01 Jan 2012 23:59:59 +0000 - # "2012-12-13 12:50".to_datetime #=> Thu, 13 Dec 2012 12:50:00 +0000 - # "12/13/2012".to_datetime #=> ArgumentError: invalid date + # "1-1-2012".to_datetime # => Sun, 01 Jan 2012 00:00:00 +0000 + # "01/01/2012 23:59:59".to_datetime # => Sun, 01 Jan 2012 23:59:59 +0000 + # "2012-12-13 12:50".to_datetime # => Thu, 13 Dec 2012 12:50:00 +0000 + # "12/13/2012".to_datetime # => ArgumentError: invalid date def to_datetime ::DateTime.parse(self, false) unless blank? end diff --git a/activesupport/lib/active_support/core_ext/string/exclude.rb b/activesupport/lib/active_support/core_ext/string/exclude.rb index 114bcb87f0..0ac684f6ee 100644 --- a/activesupport/lib/active_support/core_ext/string/exclude.rb +++ b/activesupport/lib/active_support/core_ext/string/exclude.rb @@ -2,9 +2,9 @@ class String # The inverse of <tt>String#include?</tt>. Returns true if the string # does not include the other string. # - # "hello".exclude? "lo" #=> false - # "hello".exclude? "ol" #=> true - # "hello".exclude? ?h #=> false + # "hello".exclude? "lo" # => false + # "hello".exclude? "ol" # => true + # "hello".exclude? ?h # => false def exclude?(string) !include?(string) end diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb index 56e8a5f98d..b7b750c77b 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -190,13 +190,19 @@ class String ActiveSupport::Inflector.classify(self) end - # Capitalizes the first word, turns underscores into spaces, and strips '_id'. + # Capitalizes the first word, turns underscores into spaces, and strips a + # trailing '_id' if present. # Like +titleize+, this is meant for creating pretty output. # - # 'employee_salary'.humanize # => "Employee salary" - # 'author_id'.humanize # => "Author" - def humanize - ActiveSupport::Inflector.humanize(self) + # The capitalization of the first word can be turned off by setting the + # optional parameter +capitalize+ to false. + # By default, this parameter is true. + # + # 'employee_salary'.humanize # => "Employee salary" + # 'author_id'.humanize # => "Author" + # 'author_id'.humanize(capitalize: false) # => "author" + def humanize(options = {}) + ActiveSupport::Inflector.humanize(self, options) end # Creates a foreign key name from a class name. diff --git a/activesupport/lib/active_support/core_ext/thread.rb b/activesupport/lib/active_support/core_ext/thread.rb index e80f442973..d5a420301a 100644 --- a/activesupport/lib/active_support/core_ext/thread.rb +++ b/activesupport/lib/active_support/core_ext/thread.rb @@ -39,8 +39,8 @@ class Thread # Thread.current.thread_variable_set(:cat, 'meow') # Thread.current.thread_variable_set("dog", 'woof') # end - # thr.join #=> #<Thread:0x401b3f10 dead> - # thr.thread_variables #=> [:dog, :cat] + # thr.join # => #<Thread:0x401b3f10 dead> + # thr.thread_variables # => [:dog, :cat] # # Note that these are not fiber local variables. Please see Thread#thread_variable_get # for more details. @@ -53,8 +53,8 @@ class Thread # # me = Thread.current # me.thread_variable_set(:oliver, "a") - # me.thread_variable?(:oliver) #=> true - # me.thread_variable?(:stanley) #=> false + # me.thread_variable?(:oliver) # => true + # me.thread_variable?(:stanley) # => false # # Note that these are not fiber local variables. Please see Thread#thread_variable_get # for more details. 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/duration.rb b/activesupport/lib/active_support/duration.rb index 00727ef73c..87b6407038 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -70,12 +70,10 @@ module ActiveSupport alias :until :ago def inspect #:nodoc: - val_for = parts.inject(::Hash.new(0)) { |h,(l,r)| h[l] += r; h } - [:years, :months, :days, :minutes, :seconds]. - select {|unit| val_for[unit].nonzero?}. - tap {|units| units << :seconds if units.empty?}. - map {|unit| [unit, val_for[unit]]}. - map {|unit, val| "#{val} #{val == 1 ? unit.to_s.chop : unit.to_s}"}. + parts. + reduce(::Hash.new(0)) { |h,(l,r)| h[l] += r; h }. + sort_by {|unit, _ | [:years, :months, :days, :minutes, :seconds].index(unit)}. + map {|unit, val| "#{val} #{val == 1 ? unit.to_s.chop : unit.to_s}"}. to_sentence(:locale => :en) end 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/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index 3da99872c0..f690eab604 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -207,7 +207,7 @@ module ActiveSupport # Replaces the contents of this hash with other_hash. # # h = { "a" => 100, "b" => 200 } - # h.replace({ "c" => 300, "d" => 400 }) #=> {"c"=>300, "d"=>400} + # h.replace({ "c" => 300, "d" => 400 }) # => {"c"=>300, "d"=>400} def replace(other_hash) super(self.class.new_from_hash_copying_default(other_hash)) end diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb index c96debb93f..eda0edff28 100644 --- a/activesupport/lib/active_support/inflector/inflections.rb +++ b/activesupport/lib/active_support/inflector/inflections.rb @@ -52,21 +52,21 @@ module ActiveSupport # into a non-delimited single lowercase word when passed to +underscore+. # # acronym 'HTML' - # titleize 'html' #=> 'HTML' - # camelize 'html' #=> 'HTML' - # underscore 'MyHTML' #=> 'my_html' + # titleize 'html' # => 'HTML' + # camelize 'html' # => 'HTML' + # underscore 'MyHTML' # => 'my_html' # # The acronym, however, must occur as a delimited unit and not be part of # another word for conversions to recognize it: # # acronym 'HTTP' - # camelize 'my_http_delimited' #=> 'MyHTTPDelimited' - # camelize 'https' #=> 'Https', not 'HTTPs' - # underscore 'HTTPS' #=> 'http_s', not 'https' + # camelize 'my_http_delimited' # => 'MyHTTPDelimited' + # camelize 'https' # => 'Https', not 'HTTPs' + # underscore 'HTTPS' # => 'http_s', not 'https' # # acronym 'HTTPS' - # camelize 'https' #=> 'HTTPS' - # underscore 'HTTPS' #=> 'https' + # camelize 'https' # => 'HTTPS' + # underscore 'HTTPS' # => 'https' # # Note: Acronyms that are passed to +pluralize+ will no longer be # recognized, since the acronym will not occur as a delimited unit in the @@ -74,25 +74,25 @@ module ActiveSupport # form as an acronym as well: # # acronym 'API' - # camelize(pluralize('api')) #=> 'Apis' + # camelize(pluralize('api')) # => 'Apis' # # acronym 'APIs' - # camelize(pluralize('api')) #=> 'APIs' + # camelize(pluralize('api')) # => 'APIs' # # +acronym+ may be used to specify any word that contains an acronym or # otherwise needs to maintain a non-standard capitalization. The only # restriction is that the word must begin with a capital letter. # # acronym 'RESTful' - # underscore 'RESTful' #=> 'restful' - # underscore 'RESTfulController' #=> 'restful_controller' - # titleize 'RESTfulController' #=> 'RESTful Controller' - # camelize 'restful' #=> 'RESTful' - # camelize 'restful_controller' #=> 'RESTfulController' + # underscore 'RESTful' # => 'restful' + # underscore 'RESTfulController' # => 'restful_controller' + # titleize 'RESTfulController' # => 'RESTful Controller' + # camelize 'restful' # => 'RESTful' + # camelize 'restful_controller' # => 'RESTfulController' # # acronym 'McDonald' - # underscore 'McDonald' #=> 'mcdonald' - # camelize 'mcdonald' #=> 'McDonald' + # underscore 'McDonald' # => 'mcdonald' + # camelize 'mcdonald' # => 'McDonald' def acronym(word) @acronyms[word.downcase] = word @acronym_regex = /#{@acronyms.values.join("|")}/ diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index ffdb7b53c4..0f7ae98a8a 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -98,20 +98,26 @@ module ActiveSupport word end - # Capitalizes the first word and turns underscores into spaces and strips a - # trailing "_id", if any. Like +titleize+, this is meant for creating pretty - # output. - # - # 'employee_salary'.humanize # => "Employee salary" - # 'author_id'.humanize # => "Author" - def humanize(lower_case_and_underscored_word) + # Capitalizes the first word, turns underscores into spaces, and strips a + # trailing '_id' if present. + # Like +titleize+, this is meant for creating pretty output. + # + # The capitalization of the first word can be turned off by setting the + # optional parameter +capitalize+ to false. + # By default, this parameter is true. + # + # humanize('employee_salary') # => "Employee salary" + # humanize('author_id') # => "Author" + # humanize('author_id', capitalize: false) # => "author" + def humanize(lower_case_and_underscored_word, options = {}) result = lower_case_and_underscored_word.to_s.dup inflections.humans.each { |(rule, replacement)| break if result.sub!(rule, replacement) } result.gsub!(/_id$/, "") result.tr!('_', ' ') - result.gsub(/([a-z\d]*)/i) { |match| + result.gsub!(/([a-z\d]*)/i) { |match| "#{inflections.acronyms[match] || match.downcase}" - }.gsub(/^\w/) { $&.upcase } + } + options.fetch(:capitalize, true) ? result.gsub(/^\w/) { $&.upcase } : result end # Capitalizes all the words and replaces some characters in the string to diff --git a/activesupport/lib/active_support/json/decoding.rb b/activesupport/lib/active_support/json/decoding.rb index 30833a4cb1..8b5fc70dee 100644 --- a/activesupport/lib/active_support/json/decoding.rb +++ b/activesupport/lib/active_support/json/decoding.rb @@ -7,14 +7,24 @@ module ActiveSupport mattr_accessor :parse_json_times module JSON + # matches YAML-formatted dates + DATE_REGEX = /^(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[T \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?))$/ + class << self # Parses a JSON string (JavaScript Object Notation) into a hash. # See www.json.org for more info. # # ActiveSupport::JSON.decode("{\"team\":\"rails\",\"players\":\"36\"}") # => {"team" => "rails", "players" => "36"} - def decode(json, proc = nil, options = {}) - data = ::JSON.load(json, proc, options) + def decode(json, options = {}) + 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..0e1c379b5b 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -1,19 +1,8 @@ #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' -require 'set' - module ActiveSupport class << self delegate :use_standard_json_time_format, :use_standard_json_time_format=, @@ -23,9 +12,6 @@ module ActiveSupport end module JSON - # matches YAML-formatted dates - DATE_REGEX = /^(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[T \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?))$/ - # Dumps objects in JSON (JavaScript Object Notation). # See www.json.org for more info. # @@ -36,56 +22,22 @@ module ActiveSupport end module Encoding #:nodoc: - class CircularReferenceError < StandardError; end - class Encoder attr_reader :options def initialize(options = nil) @options = options || {} - @seen = Set.new end - def encode(value, use_options = true) - check_for_circular_references(value) do - jsonified = use_options ? value.as_json(options_for(value)) : value.as_json - jsonified.encode_json(self) - end - end - - # like encode, but only calls as_json, without encoding to string. - def as_json(value, use_options = true) - check_for_circular_references(value) do - use_options ? value.as_json(options_for(value)) : value.as_json - end - end - - def options_for(value) - if value.is_a?(Array) || value.is_a?(Hash) - # hashes and arrays need to get encoder in the options, so that - # they can detect circular references. - options.merge(:encoder => self) - else - options.dup - end + def encode(value) + value.as_json(options.dup).encode_json(self) end def escape(string) Encoding.escape(string) end - - private - def check_for_circular_references(value) - unless @seen.add?(value.__id__) - raise CircularReferenceError, 'object references itself' - end - yield - ensure - @seen.delete(value.__id__) - end end - ESCAPED_CHARS = { "\x00" => '\u0000', "\x01" => '\u0001', "\x02" => '\u0002', "\x03" => '\u0003', "\x04" => '\u0004', "\x05" => '\u0005', @@ -140,6 +92,28 @@ module ActiveSupport json.force_encoding(::Encoding::UTF_8) json end + + # Deprecate CircularReferenceError + def const_missing(name) + if name == :CircularReferenceError + message = "The JSON encoder in Rails 4.1 no longer offers protection from circular references. " \ + "You are seeing this warning because you are rescuing from (or otherwise referencing) " \ + "ActiveSupport::Encoding::CircularReferenceError. In the future, this error will be " \ + "removed from Rails. You should remove these rescue blocks from your code and ensure " \ + "that your data structures are free of circular references so they can be properly " \ + "serialized into JSON.\n\n" \ + "For example, the following Hash contains a circular reference to itself:\n" \ + " h = {}\n" \ + " h['circular'] = h\n" \ + "In this case, calling h.to_json would not work properly." + + ActiveSupport::Deprecation.warn message + + SystemStackError + else + super + end + end end self.use_standard_json_time_format = true @@ -148,197 +122,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/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index a42e7f6542..3c0cf9f137 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -56,11 +56,10 @@ module ActiveSupport #:nodoc: # Forward all undefined methods to the wrapped string. def method_missing(method, *args, &block) + result = @wrapped_string.__send__(method, *args, &block) if method.to_s =~ /!$/ - result = @wrapped_string.__send__(method, *args, &block) self if result else - result = @wrapped_string.__send__(method, *args, &block) result.kind_of?(String) ? chars(result) : result end end diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb index 04e6b71580..1845c6ae38 100644 --- a/activesupport/lib/active_support/multibyte/unicode.rb +++ b/activesupport/lib/active_support/multibyte/unicode.rb @@ -287,6 +287,13 @@ module ActiveSupport class Codepoint attr_accessor :code, :combining_class, :decomp_type, :decomp_mapping, :uppercase_mapping, :lowercase_mapping + # Initializing Codepoint object with default values + def initialize + @combining_class = 0 + @uppercase_mapping = 0 + @lowercase_mapping = 0 + end + def swapcase_mapping uppercase_mapping > 0 ? uppercase_mapping : lowercase_mapping end diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb index b32aa75e59..7a96c66626 100644 --- a/activesupport/lib/active_support/notifications.rb +++ b/activesupport/lib/active_support/notifications.rb @@ -178,7 +178,7 @@ module ActiveSupport end def instrumenter - InstrumentationRegistry.instrumenter_for(notifier) + InstrumentationRegistry.instance.instrumenter_for(notifier) 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 54cff6d394..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) @@ -460,7 +460,7 @@ module ActiveSupport # See <tt>number_to_human_size</tt> if you want to print a file # size. # - # You can also define you own unit-quantifier names if you want + # You can also define your own unit-quantifier names if you want # to use other decimal units (eg.: 1500 becomes "1.5 # kilometers", 0.150 becomes "150 milliliters", etc). You may # define a wide range of unit quantifiers, even fractional ones @@ -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/per_thread_registry.rb b/activesupport/lib/active_support/per_thread_registry.rb index aa682fb36c..a5e7389d16 100644 --- a/activesupport/lib/active_support/per_thread_registry.rb +++ b/activesupport/lib/active_support/per_thread_registry.rb @@ -32,21 +32,19 @@ module ActiveSupport # # If the class has an initializer, it must accept no arguments. module PerThreadRegistry + def instance + Thread.current[name] ||= new + end + protected def method_missing(name, *args, &block) # :nodoc: # Caches the method definition as a singleton method of the receiver. define_singleton_method(name) do |*a, &b| - per_thread_registry_instance.public_send(name, *a, &b) + instance.public_send(name, *a, &b) end send(name, *args, &block) end - - private - - def per_thread_registry_instance - Thread.current[name] ||= new - end end end diff --git a/activesupport/lib/active_support/subscriber.rb b/activesupport/lib/active_support/subscriber.rb index 34c6f900c1..4b9b48539f 100644 --- a/activesupport/lib/active_support/subscriber.rb +++ b/activesupport/lib/active_support/subscriber.rb @@ -31,18 +31,41 @@ module ActiveSupport # Attach the subscriber to a namespace. def attach_to(namespace, subscriber=new, notifier=ActiveSupport::Notifications) + @namespace = namespace + @subscriber = subscriber + @notifier = notifier + subscribers << subscriber + # Add event subscribers for all existing methods on the class. subscriber.public_methods(false).each do |event| - next if %w{ start finish }.include?(event.to_s) + add_event_subscriber(event) + end + end - notifier.subscribe("#{event}.#{namespace}", subscriber) + # Adds event subscribers for all new methods added to the class. + def method_added(event) + # Only public methods are added as subscribers, and only if a notifier + # has been set up. This means that subscribers will only be set up for + # classes that call #attach_to. + if public_method_defined?(event) && notifier + add_event_subscriber(event) end end def subscribers @@subscribers ||= [] end + + protected + + attr_reader :subscriber, :notifier, :namespace + + def add_event_subscriber(event) + return if %w{ start finish }.include?(event.to_s) + + notifier.subscribe("#{event}.#{namespace}", subscriber) + end end def initialize @@ -71,7 +94,7 @@ module ActiveSupport private def event_stack - SubscriberQueueRegistry.get_queue(@queue_key) + SubscriberQueueRegistry.instance.get_queue(@queue_key) end end diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb index 2f27aff2bd..2fb5c04316 100644 --- a/activesupport/lib/active_support/test_case.rb +++ b/activesupport/lib/active_support/test_case.rb @@ -7,6 +7,7 @@ require 'active_support/testing/deprecation' require 'active_support/testing/declarative' require 'active_support/testing/isolation' require 'active_support/testing/constant_lookup' +require 'active_support/testing/time_helpers' require 'active_support/core_ext/kernel/reporting' require 'active_support/deprecation' @@ -15,23 +16,6 @@ begin rescue LoadError end -module Minitest # :nodoc: - class << self - remove_method :__run - end - - 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. - suites = Runnable.runnables.sort_by { |ts| ts.name.to_s } - - parallel, serial = suites.partition { |s| s.test_order == :parallel } - - ParallelEach.new(parallel).map { |suite| suite.run reporter, options } + - serial.map { |suite| suite.run reporter, options } - end -end - module ActiveSupport class TestCase < ::Minitest::Test Assertion = Minitest::Assertion @@ -44,15 +28,14 @@ module ActiveSupport end # FIXME: we have tests that depend on run order, we should fix that and - # remove this method. - def self.test_order # :nodoc: - :sorted - end + # remove this method call. + self.i_suck_and_my_tests_are_order_dependent! include ActiveSupport::Testing::TaggedLogging include ActiveSupport::Testing::SetupAndTeardown include ActiveSupport::Testing::Assertions include ActiveSupport::Testing::Deprecation + include ActiveSupport::Testing::TimeHelpers extend ActiveSupport::Testing::Declarative # test/unit backwards compatibility methods diff --git a/activesupport/lib/active_support/testing/time_helpers.rb b/activesupport/lib/active_support/testing/time_helpers.rb new file mode 100644 index 0000000000..94230e56ba --- /dev/null +++ b/activesupport/lib/active_support/testing/time_helpers.rb @@ -0,0 +1,55 @@ +module ActiveSupport + module Testing + # Containing helpers that helps you test passage of time. + module TimeHelpers + # Change current time to the time in the future or in the past by a given time difference by + # stubbing +Time.now+ and +Date.today+. Note that the stubs are automatically removed + # at the end of each test. + # + # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 + # travel 1.day + # Time.current # => Sun, 10 Nov 2013 15:34:49 EST -05:00 + # Date.current # => Sun, 10 Nov 2013 + # + # This method also accepts a block, which will return the current time back to its original + # state at the end of the block: + # + # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 + # travel 1.day do + # User.create.created_at # => Sun, 10 Nov 2013 15:34:49 EST -05:00 + # end + # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 + def travel(duration, &block) + travel_to Time.now + duration, &block + end + + # Change current time to the given time by stubbing +Time.now+ and +Date.today+ to return the + # time or date passed into this method. Note that the stubs are automatically removed + # at the end of each test. + # + # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 + # travel_to Time.new(2004, 11, 24, 01, 04, 44) + # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00 + # Date.current # => Wed, 24 Nov 2004 + # + # This method also accepts a block, which will return the current time back to its original + # state at the end of the block: + # + # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 + # travel_to Time.new(2004, 11, 24, 01, 04, 44) do + # User.create.created_at # => Wed, 24 Nov 2004 01:04:44 EST -05:00 + # end + # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 + def travel_to(date_or_time, &block) + Time.stubs now: date_or_time.to_time + Date.stubs today: date_or_time.to_date + + if block_given? + block.call + Time.unstub :now + Date.unstub :today + end + end + end + end +end diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 95b9b8e5ae..50db7da9d9 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -146,12 +146,12 @@ module ActiveSupport # to +false+. # # # With ActiveSupport::JSON::Encoding.use_standard_json_time_format = true - # Time.utc(2005,2,1,15,15,10).in_time_zone.to_json - # # => "2005-02-01T15:15:10Z" + # Time.utc(2005,2,1,15,15,10).in_time_zone("Hawaii").to_json + # # => "2005-02-01T05:15:10.000-10:00" # # # With ActiveSupport::JSON::Encoding.use_standard_json_time_format = false - # Time.utc(2005,2,1,15,15,10).in_time_zone.to_json - # # => "2005/02/01 15:15:10 +0000" + # Time.utc(2005,2,1,15,15,10).in_time_zone("Hawaii").to_json + # # => "2005/02/01 05:15:10 -1000" def as_json(options = nil) if ActiveSupport::JSON::Encoding.use_standard_json_time_format xmlschema(3) diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index 3cf82a24b9..a834e526fb 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -346,14 +346,14 @@ module ActiveSupport class << self alias_method :create, :new - # Return a TimeZone instance with the given name, or +nil+ if no + # Returns a TimeZone instance with the given name, or +nil+ if no # such TimeZone instance exists. (This exists to support the use of # this class with the +composed_of+ macro.) def new(name) self[name] end - # Return an array of all TimeZone objects. There are multiple + # Returns an array of all TimeZone objects. There are multiple # TimeZone objects per time zone, in many cases, to make it easier # for users to find their own time zone. def all diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index df570d485a..51007402a1 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -709,6 +709,25 @@ class FileStoreTest < ActiveSupport::TestCase @cache.send(:read_entry, "winston", {}) assert @buffer.string.present? end + + def test_cleanup_removes_all_expired_entries + time = Time.now + @cache.write('foo', 'bar', expires_in: 10) + @cache.write('baz', 'qux') + @cache.write('quux', 'corge', expires_in: 20) + Time.stubs(:now).returns(time + 15) + @cache.cleanup + assert_not @cache.exist?('foo') + 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/array_ext_test.rb b/activesupport/test/core_ext/array_ext_test.rb index 6cd0eb39b7..57722fd52a 100644 --- a/activesupport/test/core_ext/array_ext_test.rb +++ b/activesupport/test/core_ext/array_ext_test.rb @@ -212,18 +212,24 @@ class ArraySplitTests < ActiveSupport::TestCase end def test_split_with_argument - assert_equal [[1, 2], [4, 5]], [1, 2, 3, 4, 5].split(3) - assert_equal [[1, 2, 3, 4, 5]], [1, 2, 3, 4, 5].split(0) + a = [1, 2, 3, 4, 5] + assert_equal [[1, 2], [4, 5]], a.split(3) + assert_equal [[1, 2, 3, 4, 5]], a.split(0) + assert_equal [1, 2, 3, 4, 5], a end def test_split_with_block - assert_equal [[1, 2], [4, 5], [7, 8], [10]], (1..10).to_a.split { |i| i % 3 == 0 } + a = (1..10).to_a + assert_equal [[1, 2], [4, 5], [7, 8], [10]], a.split { |i| i % 3 == 0 } + assert_equal [1, 2, 3, 4, 5, 6, 7, 8, 9 ,10], a end def test_split_with_edge_values - assert_equal [[], [2, 3, 4, 5]], [1, 2, 3, 4, 5].split(1) - assert_equal [[1, 2, 3, 4], []], [1, 2, 3, 4, 5].split(5) - assert_equal [[], [2, 3, 4], []], [1, 2, 3, 4, 5].split { |i| i == 1 || i == 5 } + a = [1, 2, 3, 4, 5] + assert_equal [[], [2, 3, 4, 5]], a.split(1) + assert_equal [[1, 2, 3, 4], []], a.split(5) + assert_equal [[], [2, 3, 4], []], a.split { |i| i == 1 || i == 5 } + assert_equal [1, 2, 3, 4, 5], a end end diff --git a/activesupport/test/core_ext/bigdecimal_test.rb b/activesupport/test/core_ext/bigdecimal_test.rb index a5987044b9..b386e55d6c 100644 --- a/activesupport/test/core_ext/bigdecimal_test.rb +++ b/activesupport/test/core_ext/bigdecimal_test.rb @@ -1,5 +1,4 @@ require 'abstract_unit' -require 'bigdecimal' require 'active_support/core_ext/big_decimal' class BigDecimalTest < ActiveSupport::TestCase diff --git a/activesupport/test/core_ext/class/attribute_accessor_test.rb b/activesupport/test/core_ext/class/attribute_accessor_test.rb index 0d5f39a72b..3bc948f3a6 100644 --- a/activesupport/test/core_ext/class/attribute_accessor_test.rb +++ b/activesupport/test/core_ext/class/attribute_accessor_test.rb @@ -8,6 +8,9 @@ class ClassAttributeAccessorTest < ActiveSupport::TestCase cattr_accessor :bar, :instance_writer => false cattr_reader :shaq, :instance_reader => false cattr_accessor :camp, :instance_accessor => false + cattr_accessor(:defa) { 'default_accessor_value' } + cattr_reader(:defr) { 'default_reader_value' } + cattr_writer(:defw) { 'default_writer_value' } end @object = @class.new end @@ -58,4 +61,10 @@ class ClassAttributeAccessorTest < ActiveSupport::TestCase end assert_equal "invalid class attribute name: 1nvalid", exception.message end + + def test_should_use_default_value_if_block_passed + assert_equal 'default_accessor_value', @class.defa + assert_equal 'default_reader_value', @class.defr + assert_equal 'default_writer_value', @class.class_variable_get('@@defw') + end end diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb index 5e3987265b..ed267cf4b9 100644 --- a/activesupport/test/core_ext/duration_test.rb +++ b/activesupport/test/core_ext/duration_test.rb @@ -27,6 +27,7 @@ class DurationTest < ActiveSupport::TestCase def test_equals assert 1.day == 1.day assert 1.day == 1.day.to_i + assert 1.day.to_i == 1.day assert !(1.day == 'foo') end @@ -37,6 +38,8 @@ class DurationTest < ActiveSupport::TestCase assert_equal '6 months and -2 days', (6.months - 2.days).inspect assert_equal '10 seconds', 10.seconds.inspect assert_equal '10 years, 2 months, and 1 day', (10.years + 2.months + 1.day).inspect + assert_equal '10 years, 2 months, and 1 day', (10.years + 1.month + 1.day + 1.month).inspect + assert_equal '10 years, 2 months, and 1 day', (1.day + 10.years + 2.months).inspect assert_equal '7 days', 1.week.inspect assert_equal '14 days', 1.fortnight.inspect end 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/range_ext_test.rb b/activesupport/test/core_ext/range_ext_test.rb index 2f8723a074..854b0a38bd 100644 --- a/activesupport/test/core_ext/range_ext_test.rb +++ b/activesupport/test/core_ext/range_ext_test.rb @@ -90,4 +90,26 @@ class RangeTest < ActiveSupport::TestCase time_range_2 = Time.utc(2005, 12, 10, 17, 31)..Time.utc(2005, 12, 10, 18, 00) assert !time_range_1.overlaps?(time_range_2) end + + def test_each_on_time_with_zone + twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone['Eastern Time (US & Canada)'] , Time.utc(2006,11,28,10,30)) + assert_raises TypeError do + ((twz - 1.hour)..twz).each {} + end + end + + def test_step_on_time_with_zone + twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone['Eastern Time (US & Canada)'] , Time.utc(2006,11,28,10,30)) + assert_raises TypeError do + ((twz - 1.hour)..twz).step(1) {} + end + end + + def test_include_on_time_with_zone + twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone['Eastern Time (US & Canada)'] , Time.utc(2006,11,28,10,30)) + assert_raises TypeError do + ((twz - 1.hour)..twz).include?(twz) + end + end + end diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index 2537af0394..20e3d4802e 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -11,13 +11,6 @@ require 'active_support/core_ext/string/strip' require 'active_support/core_ext/string/output_safety' require 'active_support/core_ext/string/indent' -module Ace - module Base - class Case - end - end -end - class StringInflectionsTest < ActiveSupport::TestCase include InflectorTestCases include ConstantizeTestCases @@ -162,6 +155,12 @@ class StringInflectionsTest < ActiveSupport::TestCase end end + def test_humanize_without_capitalize + UnderscoreToHumanWithoutCapitalize.each do |underscore, human| + assert_equal(human, underscore.humanize(capitalize: false)) + end + end + def test_ord assert_equal 97, 'a'.ord assert_equal 97, 'abc'.ord @@ -277,7 +276,7 @@ class StringInflectionsTest < ActiveSupport::TestCase def test_truncate_should_not_be_html_safe assert !"Hello World!".truncate(12).html_safe? end - + def test_remove assert_equal "Summer", "Fast Summer".remove(/Fast /) assert_equal "Summer", "Fast Summer".remove!(/Fast /) 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/inflector_test.rb b/activesupport/test/inflector_test.rb index 32739d45a9..6184df481f 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -287,6 +287,12 @@ class InflectorTest < ActiveSupport::TestCase end end + def test_humanize_without_capitalize + UnderscoreToHumanWithoutCapitalize.each do |underscore, human| + assert_equal(human, ActiveSupport::Inflector.humanize(underscore, capitalize: false)) + end + end + def test_humanize_by_rule ActiveSupport::Inflector.inflections do |inflect| inflect.human(/_cnt$/i, '\1_count') diff --git a/activesupport/test/inflector_test_cases.rb b/activesupport/test/inflector_test_cases.rb index cc36d62138..b34a946baf 100644 --- a/activesupport/test/inflector_test_cases.rb +++ b/activesupport/test/inflector_test_cases.rb @@ -212,6 +212,12 @@ module InflectorTestCases "underground" => "Underground" } + UnderscoreToHumanWithoutCapitalize = { + "employee_salary" => "employee salary", + "employee_id" => "employee", + "underground" => "underground" + } + MixtureToTitleCase = { 'active_record' => 'Active Record', 'ActiveRecord' => 'Active Record', diff --git a/activesupport/test/json/decoding_test.rb b/activesupport/test/json/decoding_test.rb index 99c5f2d1ec..07d7e530ca 100644 --- a/activesupport/test/json/decoding_test.rb +++ b/activesupport/test/json/decoding_test.rb @@ -4,6 +4,12 @@ require 'active_support/json' require 'active_support/time' class TestJSONDecoding < ActiveSupport::TestCase + class Foo + def self.json_create(object) + "Foo" + end + end + TESTS = { %q({"returnTo":{"\/categories":"\/"}}) => {"returnTo" => {"/categories" => "/"}}, %q({"return\\"To\\":":{"\/categories":"\/"}}) => {"return\"To\":" => {"/categories" => "/"}}, @@ -52,7 +58,17 @@ class TestJSONDecoding < ActiveSupport::TestCase # tests escaping of "\n" char with Yaml backend %q({"a":"\n"}) => {"a"=>"\n"}, %q({"a":"\u000a"}) => {"a"=>"\n"}, - %q({"a":"Line1\u000aLine2"}) => {"a"=>"Line1\nLine2"} + %q({"a":"Line1\u000aLine2"}) => {"a"=>"Line1\nLine2"}, + # prevent json unmarshalling + %q({"json_class":"TestJSONDecoding::Foo"}) => {"json_class"=>"TestJSONDecoding::Foo"}, + # json "fragments" - these are invalid JSON, but ActionPack relies on this + %q("a string") => "a string", + %q(1.1) => 1.1, + %q(1) => 1, + %q(-1) => -1, + %q(true) => true, + %q(false) => false, + %q(null) => nil } TESTS.each_with_index do |(json, expected), index| @@ -76,7 +92,14 @@ class TestJSONDecoding < ActiveSupport::TestCase end def test_failed_json_decoding + assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%(undefined)) } + assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%({a: 1})) } assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%({: 1})) } + assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%()) } + end + + 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..00f43be6d4 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' @@ -12,7 +13,7 @@ class TestJSONEncoding < ActiveSupport::TestCase class Hashlike def to_hash - { :a => 1 } + { :foo => "hello", :bar => "world" } end end @@ -31,6 +32,25 @@ class TestJSONEncoding < ActiveSupport::TestCase end end + class OptionsTest + def as_json(options = :default) + options + end + end + + class HashWithAsJson < Hash + attr_accessor :as_json_called + + def initialize(*) + super + end + + def as_json(options={}) + @as_json_called = true + super + end + end + TrueTests = [[ true, %(true) ]] FalseTests = [[ false, %(false) ]] NilTests = [[ nil, %(null) ]] @@ -60,7 +80,7 @@ class TestJSONEncoding < ActiveSupport::TestCase [ :"a b", %("a b") ]] ObjectTests = [[ Foo.new(1, 2), %({\"a\":1,\"b\":2}) ]] - HashlikeTests = [[ Hashlike.new, %({\"a\":1}) ]] + HashlikeTests = [[ Hashlike.new, %({\"bar\":\"world\",\"foo\":\"hello\"}) ]] CustomTests = [[ Custom.new, '"custom"' ]] RegexpTests = [[ /^a/, '"(?-mix:^a)"' ], [/^\w{1,2}[a-z]+/ix, '"(?ix-m:^\\\\w{1,2}[a-z]+)"']] @@ -70,8 +90,8 @@ class TestJSONEncoding < ActiveSupport::TestCase DateTimeTests = [[ DateTime.civil(2005,2,1,15,15,10), %("2005/02/01 15:15:10 +0000") ]] StandardDateTests = [[ Date.new(2005,2,1), %("2005-02-01") ]] - StandardTimeTests = [[ Time.utc(2005,2,1,15,15,10), %("2005-02-01T15:15:10Z") ]] - StandardDateTimeTests = [[ DateTime.civil(2005,2,1,15,15,10), %("2005-02-01T15:15:10+00:00") ]] + StandardTimeTests = [[ Time.utc(2005,2,1,15,15,10), %("2005-02-01T15:15:10.000Z") ]] + StandardDateTimeTests = [[ DateTime.civil(2005,2,1,15,15,10), %("2005-02-01T15:15:10.000+00:00") ]] StandardStringTests = [[ 'this is the <string>', %("this is the <string>")]] def sorted_json(json) @@ -96,6 +116,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) @@ -138,19 +165,25 @@ class TestJSONEncoding < ActiveSupport::TestCase def test_exception_raised_when_encoding_circular_reference_in_array a = [1] a << a - assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) } + assert_deprecated do + assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) } + end end def test_exception_raised_when_encoding_circular_reference_in_hash a = { :name => 'foo' } a[:next] = a - assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) } + assert_deprecated do + assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) } + end end def test_exception_raised_when_encoding_circular_reference_in_hash_inside_array a = { :name => 'foo', :sub => [] } a[:sub] << a - assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) } + assert_deprecated do + assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) } + end end def test_hash_key_identifiers_are_always_quoted @@ -170,7 +203,7 @@ class TestJSONEncoding < ActiveSupport::TestCase prev = ActiveSupport.use_standard_json_time_format ActiveSupport.use_standard_json_time_format = true with_env_tz 'US/Eastern' do - assert_equal %("2005-02-01T15:15:10-05:00"), ActiveSupport::JSON.encode(Time.local(2005,2,1,15,15,10)) + assert_equal %("2005-02-01T15:15:10.000-05:00"), ActiveSupport::JSON.encode(Time.local(2005,2,1,15,15,10)) end ensure ActiveSupport.use_standard_json_time_format = prev @@ -196,6 +229,31 @@ class TestJSONEncoding < ActiveSupport::TestCase end end + def test_hash_like_with_options + h = Hashlike.new + json = h.to_json :only => [:foo] + + assert_equal({"foo"=>"hello"}, JSON.parse(json)) + end + + def test_object_to_json_with_options + obj = Object.new + obj.instance_variable_set :@foo, "hello" + obj.instance_variable_set :@bar, "world" + json = obj.to_json :only => ["foo"] + + assert_equal({"foo"=>"hello"}, JSON.parse(json)) + end + + def test_struct_to_json_with_options + struct = Struct.new(:foo, :bar).new + struct.foo = "hello" + struct.bar = "world" + json = struct.to_json :only => [:foo] + + assert_equal({"foo"=>"hello"}, JSON.parse(json)) + end + def test_hash_should_pass_encoding_options_to_children_in_as_json person = { :name => 'John', @@ -277,7 +335,17 @@ class TestJSONEncoding < ActiveSupport::TestCase hash = {"foo" => f, "other_hash" => {"foo" => "other_foo", "test" => "other_test"}} assert_equal({"foo"=>{"foo"=>"hello","bar"=>"world"}, - "other_hash" => {"foo"=>"other_foo","test"=>"other_test"}}, JSON.parse(hash.to_json)) + "other_hash" => {"foo"=>"other_foo","test"=>"other_test"}}, ActiveSupport::JSON.decode(hash.to_json)) + end + + def test_hash_as_json_without_options + json = { foo: OptionsTest.new }.as_json + assert_equal({"foo" => :default}, json) + end + + def test_array_as_json_without_options + json = [ OptionsTest.new ].as_json + assert_equal([:default], json) end def test_struct_encoding @@ -302,13 +370,13 @@ class TestJSONEncoding < ActiveSupport::TestCase assert_equal({"name" => "David", "sub" => { "name" => "David", - "date" => "2010-01-01" }}, JSON.parse(json_custom)) + "date" => "2010-01-01" }}, ActiveSupport::JSON.decode(json_custom)) assert_equal({"name" => "David", "email" => "sample@example.com"}, - JSON.parse(json_strings)) + ActiveSupport::JSON.decode(json_strings)) assert_equal({"name" => "David", "date" => "2010-01-01"}, - JSON.parse(json_string_and_date)) + ActiveSupport::JSON.decode(json_string_and_date)) end def test_opt_out_big_decimal_string_serialization @@ -328,6 +396,38 @@ class TestJSONEncoding < ActiveSupport::TestCase assert_equal false, false.as_json end + def test_json_gem_dump_by_passing_active_support_encoder + h = HashWithAsJson.new + h[:foo] = "hello" + h[:bar] = "world" + + assert_equal %({"foo":"hello","bar":"world"}), JSON.dump(h) + assert_nil h.as_json_called + end + + def test_json_gem_generate_by_passing_active_support_encoder + h = HashWithAsJson.new + h[:foo] = "hello" + h[:bar] = "world" + + assert_equal %({"foo":"hello","bar":"world"}), JSON.generate(h) + assert_nil h.as_json_called + end + + def test_json_gem_pretty_generate_by_passing_active_support_encoder + h = HashWithAsJson.new + h[:foo] = "hello" + h[:bar] = "world" + + assert_equal <<EXPECTED.chomp, JSON.pretty_generate(h) +{ + "foo": "hello", + "bar": "world" +} +EXPECTED + assert_nil h.as_json_called + end + protected def object_keys(json_object) diff --git a/activesupport/test/message_encryptor_test.rb b/activesupport/test/message_encryptor_test.rb index 509c453b5c..203156baa1 100644 --- a/activesupport/test/message_encryptor_test.rb +++ b/activesupport/test/message_encryptor_test.rb @@ -60,7 +60,7 @@ class MessageEncryptorTest < ActiveSupport::TestCase ActiveSupport.use_standard_json_time_format = true encryptor = ActiveSupport::MessageEncryptor.new(SecureRandom.hex(64), SecureRandom.hex(64), :serializer => JSONSerializer.new) message = encryptor.encrypt_and_sign({ :foo => 123, 'bar' => Time.utc(2010) }) - exp = { "foo" => 123, "bar" => "2010-01-01T00:00:00Z" } + exp = { "foo" => 123, "bar" => "2010-01-01T00:00:00.000Z" } assert_equal exp, encryptor.decrypt_and_verify(message) ensure ActiveSupport.use_standard_json_time_format = prev diff --git a/activesupport/test/message_verifier_test.rb b/activesupport/test/message_verifier_test.rb index a8633f7299..f0f261d710 100644 --- a/activesupport/test/message_verifier_test.rb +++ b/activesupport/test/message_verifier_test.rb @@ -49,7 +49,7 @@ class MessageVerifierTest < ActiveSupport::TestCase ActiveSupport.use_standard_json_time_format = true verifier = ActiveSupport::MessageVerifier.new("Hey, I'm a secret!", :serializer => JSONSerializer.new) message = verifier.generate({ :foo => 123, 'bar' => Time.utc(2010) }) - exp = { "foo" => 123, "bar" => "2010-01-01T00:00:00Z" } + exp = { "foo" => 123, "bar" => "2010-01-01T00:00:00.000Z" } assert_equal exp, verifier.verify(message) ensure ActiveSupport.use_standard_json_time_format = prev 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/activesupport/test/subscriber_test.rb b/activesupport/test/subscriber_test.rb new file mode 100644 index 0000000000..253411aa3d --- /dev/null +++ b/activesupport/test/subscriber_test.rb @@ -0,0 +1,40 @@ +require 'abstract_unit' +require 'active_support/subscriber' + +class TestSubscriber < ActiveSupport::Subscriber + attach_to :doodle + + cattr_reader :event + + def self.clear + @@event = nil + end + + def open_party(event) + @@event = event + end + + private + + def private_party(event) + @@event = event + end +end + +class SubscriberTest < ActiveSupport::TestCase + def setup + TestSubscriber.clear + end + + def test_attaches_subscribers + ActiveSupport::Notifications.instrument("open_party.doodle") + + assert_equal "open_party.doodle", TestSubscriber.event.name + end + + def test_does_not_attach_private_methods + ActiveSupport::Notifications.instrument("private_party.doodle") + + assert_nil TestSubscriber.event + end +end diff --git a/activesupport/test/test_test.rb b/activesupport/test/test_test.rb index 0c8cc1f883..5ed2da7e8b 100644 --- a/activesupport/test/test_test.rb +++ b/activesupport/test/test_test.rb @@ -1,4 +1,6 @@ require 'abstract_unit' +require 'active_support/core_ext/date' +require 'active_support/core_ext/numeric/time' class AssertDifferenceTest < ActiveSupport::TestCase def setup @@ -122,7 +124,6 @@ class SetupAndTeardownTest < ActiveSupport::TestCase end end - class SubclassSetupAndTeardownTest < SetupAndTeardownTest setup :bar teardown :bar @@ -143,7 +144,6 @@ class SubclassSetupAndTeardownTest < SetupAndTeardownTest end end - class TestCaseTaggedLoggingTest < ActiveSupport::TestCase def before_setup require 'stringio' @@ -156,3 +156,49 @@ class TestCaseTaggedLoggingTest < ActiveSupport::TestCase assert_match "#{self.class}: #{name}\n", @out.string end end + +class TimeHelperTest < ActiveSupport::TestCase + setup do + Time.stubs now: Time.now + end + + def test_time_helper_travel + expected_time = Time.now + 1.day + travel 1.day + + assert_equal expected_time, Time.now + assert_equal expected_time.to_date, Date.today + end + + def test_time_helper_travel_with_block + expected_time = Time.now + 1.day + + travel 1.day do + assert_equal expected_time, Time.now + assert_equal expected_time.to_date, Date.today + end + + assert_not_equal expected_time, Time.now + assert_not_equal expected_time.to_date, Date.today + end + + def test_time_helper_travel_to + expected_time = Time.new(2004, 11, 24, 01, 04, 44) + travel_to expected_time + + assert_equal expected_time, Time.now + assert_equal Date.new(2004, 11, 24), Date.today + end + + def test_time_helper_travel_to_with_block + expected_time = Time.new(2004, 11, 24, 01, 04, 44) + + travel_to expected_time do + assert_equal expected_time, Time.now + assert_equal Date.new(2004, 11, 24), Date.today + end + + assert_not_equal expected_time, Time.now + assert_not_equal Date.new(2004, 11, 24), Date.today + end +end diff --git a/activesupport/test/transliterate_test.rb b/activesupport/test/transliterate_test.rb index ce91c443e1..e0f85f4e7c 100644 --- a/activesupport/test/transliterate_test.rb +++ b/activesupport/test/transliterate_test.rb @@ -3,7 +3,6 @@ require 'abstract_unit' require 'active_support/inflector/transliterate' class TransliterateTest < ActiveSupport::TestCase - def test_transliterate_should_not_change_ascii_chars (0..127).each do |byte| char = [byte].pack("U") @@ -24,12 +23,13 @@ class TransliterateTest < ActiveSupport::TestCase def test_transliterate_should_work_with_custom_i18n_rules_and_uncomposed_utf8 char = [117, 776].pack("U*") # "ü" as ASCII "u" plus COMBINING DIAERESIS I18n.backend.store_translations(:de, :i18n => {:transliterate => {:rule => {"ü" => "ue"}}}) - I18n.locale = :de + default_locale, I18n.locale = I18n.locale, :de assert_equal "ue", ActiveSupport::Inflector.transliterate(char) + ensure + I18n.locale = default_locale end def test_transliterate_should_allow_a_custom_replacement_char assert_equal "a*b", ActiveSupport::Inflector.transliterate("a索b", "*") end - end diff --git a/activesupport/test/xml_mini_test.rb b/activesupport/test/xml_mini_test.rb index a025279e16..d992028323 100644 --- a/activesupport/test/xml_mini_test.rb +++ b/activesupport/test/xml_mini_test.rb @@ -106,7 +106,11 @@ module XmlMiniTest module Nokogiri end setup do - @xml = ActiveSupport::XmlMini + @xml, @default_backend = ActiveSupport::XmlMini, ActiveSupport::XmlMini.backend + end + + teardown do + ActiveSupport::XmlMini.backend = @default_backend end test "#with_backend should switch backend and then switch back" do @@ -135,7 +139,11 @@ module XmlMiniTest module LibXML end setup do - @xml = ActiveSupport::XmlMini + @xml, @default_backend = ActiveSupport::XmlMini, ActiveSupport::XmlMini.backend + end + + teardown do + ActiveSupport::XmlMini.backend = @default_backend end test "#with_backend should be thread-safe" do diff --git a/guides/CHANGELOG.md b/guides/CHANGELOG.md index afa695d445..4cfc5b1f10 100644 --- a/guides/CHANGELOG.md +++ b/guides/CHANGELOG.md @@ -1,5 +1,13 @@ +* Fixed missing line and shadow on service pages(404, 422, 500). + + *Dmitry Korotkov* + * Removed repetitive th tags. Instead of them added one th tag with a colspan attribute. *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_gem.rb b/guides/bug_report_templates/action_controller_gem.rb index 693bc320b3..89ac28671a 100644 --- a/guides/bug_report_templates/action_controller_gem.rb +++ b/guides/bug_report_templates/action_controller_gem.rb @@ -19,6 +19,8 @@ class TestApp < Rails::Application end class TestController < ActionController::Base + include Rails.application.routes.url_helpers + def index render text: 'Home' end diff --git a/guides/bug_report_templates/action_controller_master.rb b/guides/bug_report_templates/action_controller_master.rb index 375ed9bc5d..d44fd9196a 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' @@ -28,6 +28,8 @@ class TestApp < Rails::Application end class TestController < ActionController::Base + include Rails.application.routes.url_helpers + def index render text: 'Home' end @@ -48,4 +50,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_gem.rb b/guides/bug_report_templates/active_record_gem.rb index 63fbe27aed..a8868a1877 100644 --- a/guides/bug_report_templates/active_record_gem.rb +++ b/guides/bug_report_templates/active_record_gem.rb @@ -25,7 +25,7 @@ class Comment < ActiveRecord::Base belongs_to :post end -class BugTest < Minitest::Test +class BugTest < MiniTest::Unit::TestCase def test_association_stuff post = Post.create! post.comments << Comment.create! 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/Gemfile b/guides/code/getting_started/Gemfile index 3d57012901..eef6180804 100644 --- a/guides/code/getting_started/Gemfile +++ b/guides/code/getting_started/Gemfile @@ -31,7 +31,7 @@ end gem 'jbuilder', '~> 1.2' # To use ActiveModel has_secure_password -# gem 'bcrypt-ruby', '~> 3.1.0' +# gem 'bcrypt-ruby', '~> 3.1.2' # Use unicorn as the app server # gem 'unicorn' 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/code/getting_started/public/404.html b/guides/code/getting_started/public/404.html index 3d287b135d..3265cc8e33 100644 --- a/guides/code/getting_started/public/404.html +++ b/guides/code/getting_started/public/404.html @@ -22,6 +22,7 @@ border-top-right-radius: 9px; background-color: white; padding: 7px 4em 0 4em; + box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); } h1 { @@ -37,6 +38,7 @@ background-color: #F7F7F7; border: 1px solid #CCC; border-right-color: #999; + border-left-color: #999; border-bottom-color: #999; border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; diff --git a/guides/code/getting_started/public/422.html b/guides/code/getting_started/public/422.html index 3b946bf4a4..d823a8fc77 100644 --- a/guides/code/getting_started/public/422.html +++ b/guides/code/getting_started/public/422.html @@ -22,6 +22,7 @@ border-top-right-radius: 9px; background-color: white; padding: 7px 4em 0 4em; + box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); } h1 { @@ -37,6 +38,7 @@ background-color: #F7F7F7; border: 1px solid #CCC; border-right-color: #999; + border-left-color: #999; border-bottom-color: #999; border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; diff --git a/guides/code/getting_started/public/500.html b/guides/code/getting_started/public/500.html index ccc4ad5656..ebf6d4c00c 100644 --- a/guides/code/getting_started/public/500.html +++ b/guides/code/getting_started/public/500.html @@ -22,6 +22,7 @@ border-top-right-radius: 9px; background-color: white; padding: 7px 4em 0 4em; + box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); } h1 { @@ -37,6 +38,7 @@ background-color: #F7F7F7; border: 1px solid #CCC; border-right-color: #999; + border-left-color: #999; border-bottom-color: #999; border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; 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/2_2_release_notes.md b/guides/source/2_2_release_notes.md index 7db4cf07e7..c11d1240c4 100644 --- a/guides/source/2_2_release_notes.md +++ b/guides/source/2_2_release_notes.md @@ -327,7 +327,7 @@ Other features of memoization include `unmemoize`, `unmemoize_all`, and `memoize The `each_with_object` method provides an alternative to `inject`, using a method backported from Ruby 1.9. It iterates over a collection, passing the current element and the memo into the block. ```ruby -%w(foo bar).each_with_object({}) { |str, hsh| hsh[str] = str.upcase } #=> {'foo' => 'FOO', 'bar' => 'BAR'} +%w(foo bar).each_with_object({}) { |str, hsh| hsh[str] = str.upcase } # => {'foo' => 'FOO', 'bar' => 'BAR'} ``` Lead Contributor: [Adam Keys](http://therealadam.com/) diff --git a/guides/source/2_3_release_notes.md b/guides/source/2_3_release_notes.md index c2002fb8fa..8c633fa169 100644 --- a/guides/source/2_3_release_notes.md +++ b/guides/source/2_3_release_notes.md @@ -604,7 +604,7 @@ Deprecated A few pieces of older code are deprecated in this release: * If you're one of the (fairly rare) Rails developers who deploys in a fashion that depends on the inspector, reaper, and spawner scripts, you'll need to know that those scripts are no longer included in core Rails. If you need them, you'll be able to pick up copies via the [irs_process_scripts](http://github.com/rails/irs_process_scripts/tree) plugin. -* `render_component` goes from "deprecated" to "nonexistent" in Rails 2.3. If you still need it, you can install the [render_component plugin](http://github.com/rails/render_component/tree/master.) +* `render_component` goes from "deprecated" to "nonexistent" in Rails 2.3. If you still need it, you can install the [render_component plugin](http://github.com/rails/render_component/tree/master). * Support for Rails components has been removed. * If you were one of the people who got used to running `script/performance/request` to look at performance based on integration tests, you need to learn a new trick: that script has been removed from core Rails now. There's a new request_profiler plugin that you can install to get the exact same functionality back. * `ActionController::Base#session_enabled?` is deprecated because sessions are lazy-loaded now. diff --git a/guides/source/3_0_release_notes.md b/guides/source/3_0_release_notes.md index d398cd680c..cf9d694de7 100644 --- a/guides/source/3_0_release_notes.md +++ b/guides/source/3_0_release_notes.md @@ -73,8 +73,6 @@ You can see an example of how that works at [Rails Upgrade is now an Official Pl Aside from Rails Upgrade tool, if you need more help, there are people on IRC and [rubyonrails-talk](http://groups.google.com/group/rubyonrails-talk) that are probably doing the same thing, possibly hitting the same issues. Be sure to blog your own experiences when upgrading so others can benefit from your knowledge! -More information - [The Path to Rails 3: Approaching the upgrade](http://omgbloglol.com/post/353978923/the-path-to-rails-3-approaching-the-upgrade) - Creating a Rails 3.0 application -------------------------------- diff --git a/guides/source/3_2_release_notes.md b/guides/source/3_2_release_notes.md index dc4d942671..a9484cf97a 100644 --- a/guides/source/3_2_release_notes.md +++ b/guides/source/3_2_release_notes.md @@ -21,7 +21,7 @@ If you're upgrading an existing application, it's a great idea to have good test Rails 3.2 requires Ruby 1.8.7 or higher. Support for all of the previous Ruby versions has been dropped officially and you should upgrade as early as possible. Rails 3.2 is also compatible with Ruby 1.9.2. -TIP: Note that Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails. Ruby Enterprise Edition has these fixed since the release of 1.8.7-2010.02. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults, so if you want to use 1.9.x, jump on to 1.9.2 or 1.9.3 for smooth sailing. +TIP: Note that Ruby 1.8.7 p248 and p249 have marshalling bugs that crash Rails. Ruby Enterprise Edition has these fixed since the release of 1.8.7-2010.02. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults, so if you want to use 1.9.x, jump on to 1.9.2 or 1.9.3 for smooth sailing. ### What to update in your apps @@ -137,7 +137,7 @@ Railties * Update `Rails::Rack::Logger` middleware to apply any tags set in `config.log_tags` to `ActiveSupport::TaggedLogging`. This makes it easy to tag log lines with debug information like subdomain and request id -- both very helpful in debugging multi-user production applications. -* Default options to `rails new` can be set in `~/.railsrc`. You can specify extra command-line arguments to be used every time 'rails new' runs in the `.railsrc` configuration file in your home directory. +* Default options to `rails new` can be set in `~/.railsrc`. You can specify extra command-line arguments to be used every time `rails new` runs in the `.railsrc` configuration file in your home directory. * Add an alias `d` for `destroy`. This works for engines too. @@ -185,9 +185,9 @@ Action Pack end ``` - Rails will use 'layouts/single_car' when a request comes in :show action, and use 'layouts/application' (or 'layouts/cars', if exists) when a request comes in for any other actions. + Rails will use `layouts/single_car` when a request comes in `:show` action, and use `layouts/application` (or `layouts/cars`, if exists) when a request comes in for any other actions. -* form\_for is changed to use "#{action}\_#{as}" as the css class and id if `:as` option is provided. Earlier versions used "#{as}\_#{action}". +* `form\_for` is changed to use `#{action}\_#{as}` as the css class and id if `:as` option is provided. Earlier versions used `#{as}\_#{action}`. * `ActionController::ParamsWrapper` on Active Record models now only wrap `attr_accessible` attributes if they were set. If not, only the attributes returned by the class method `attribute_names` will be wrapped. This fixes the wrapping of nested attributes by adding them to `attr_accessible`. @@ -219,7 +219,7 @@ Action Pack * MIME type entries for PDF, ZIP and other formats were added. -* Allow fresh_when/stale? to take a record instead of an options hash. +* Allow `fresh_when/stale?` to take a record instead of an options hash. * Changed log level of warning for missing CSRF token from `:debug` to `:warn`. @@ -227,7 +227,7 @@ Action Pack #### Deprecations -* Deprecated implied layout lookup in controllers whose parent had a explicit layout set: +* Deprecated implied layout lookup in controllers whose parent had an explicit layout set: ```ruby class ApplicationController @@ -254,7 +254,7 @@ Action Pack * Added `ActionDispatch::RequestId` middleware that'll make a unique X-Request-Id header available to the response and enables the `ActionDispatch::Request#uuid` method. This makes it easy to trace requests from end-to-end in the stack and to identify individual requests in mixed logs like Syslog. -* The `ShowExceptions` middleware now accepts a exceptions application that is responsible to render an exception when the application fails. The application is invoked with a copy of the exception in `env["action_dispatch.exception"]` and with the `PATH_INFO` rewritten to the status code. +* The `ShowExceptions` middleware now accepts an exceptions application that is responsible to render an exception when the application fails. The application is invoked with a copy of the exception in `env["action_dispatch.exception"]` and with the `PATH_INFO` rewritten to the status code. * Allow rescue responses to be configured through a railtie as in `config.action_dispatch.rescue_responses`. @@ -375,7 +375,7 @@ Active Record * Support index sort order in SQLite, MySQL and PostgreSQL adapters. -* Allow the `:class_name` option for associations to take a symbol in addition to a string. This is to avoid confusing newbies, and to be consistent with the fact that other options like :foreign_key already allow a symbol or a string. +* Allow the `:class_name` option for associations to take a symbol in addition to a string. This is to avoid confusing newbies, and to be consistent with the fact that other options like `:foreign_key` already allow a symbol or a string. ```ruby has_many :clients, :class_name => :Client # Note that the symbol need to be capitalized diff --git a/guides/source/4_0_release_notes.md b/guides/source/4_0_release_notes.md index ff4d30b4c0..c0eb77c1e7 100644 --- a/guides/source/4_0_release_notes.md +++ b/guides/source/4_0_release_notes.md @@ -116,7 +116,7 @@ Documentation Railties -------- -Please refer to the [Changelog](https://github.com/rails/rails/blob/master/railties/CHANGELOG.md) for detailed changes. +Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/railties/CHANGELOG.md) for detailed changes. ### Notable changes @@ -139,7 +139,7 @@ Please refer to the [Changelog](https://github.com/rails/rails/blob/master/railt Action Mailer ------------- -Please refer to the [Changelog](https://github.com/rails/rails/blob/master/actionmailer/CHANGELOG.md) for detailed changes. +Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/actionmailer/CHANGELOG.md) for detailed changes. ### Notable changes @@ -148,7 +148,7 @@ Please refer to the [Changelog](https://github.com/rails/rails/blob/master/actio Active Model ------------ -Please refer to the [Changelog](https://github.com/rails/rails/blob/master/activemodel/CHANGELOG.md) for detailed changes. +Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/activemodel/CHANGELOG.md) for detailed changes. ### Notable changes @@ -161,19 +161,19 @@ Please refer to the [Changelog](https://github.com/rails/rails/blob/master/activ Active Support -------------- -Please refer to the [Changelog](https://github.com/rails/rails/blob/master/activesupport/CHANGELOG.md) for detailed changes. +Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/activesupport/CHANGELOG.md) for detailed changes. ### Notable changes -* Replace deprecated `memcache-client` gem with `dalli` in ActiveSupport::Cache::MemCacheStore. +* Replace deprecated `memcache-client` gem with `dalli` in `ActiveSupport::Cache::MemCacheStore`. -* Optimize ActiveSupport::Cache::Entry to reduce memory and processing overhead. +* Optimize `ActiveSupport::Cache::Entry` to reduce memory and processing overhead. * Inflections can now be defined per locale. `singularize` and `pluralize` accept locale as an extra argument. * `Object#try` will now return nil instead of raise a NoMethodError if the receiving object does not implement the method, but you can still get the old behavior by using the new `Object#try!`. -* `String#to_date` now raises `Argument Error: invalid date` instead of `NoMethodError: undefined method 'div' for nil:NilClass` +* `String#to_date` now raises `ArgumentError: invalid date` instead of `NoMethodError: undefined method 'div' for nil:NilClass` when given an invalid date. It is now the same as `Date.parse`, and it accepts more invalid dates than 3.x, such as: ``` @@ -190,20 +190,20 @@ Please refer to the [Changelog](https://github.com/rails/rails/blob/master/activ * Deprecate `ActiveSupport::TestCase#pending` method, use `skip` from MiniTest instead. -* ActiveSupport::Benchmarkable#silence has been deprecated due to its lack of thread safety. It will be removed without replacement in Rails 4.1. +* `ActiveSupport::Benchmarkable#silence` has been deprecated due to its lack of thread safety. It will be removed without replacement in Rails 4.1. * `ActiveSupport::JSON::Variable` is deprecated. Define your own `#as_json` and `#encode_json` methods for custom JSON string literals. -* Deprecates the compatibility method Module#local_constant_names, use Module#local_constants instead (which returns symbols). +* Deprecates the compatibility method `Module#local_constant_names`, use `Module#local_constants` instead (which returns symbols). -* BufferedLogger is deprecated. Use ActiveSupport::Logger, or the logger from Ruby standard library. +* `BufferedLogger` is deprecated. Use `ActiveSupport::Logger`, or the logger from Ruby standard library. * Deprecate `assert_present` and `assert_blank` in favor of `assert object.blank?` and `assert object.present?` Action Pack ----------- -Please refer to the [Changelog](https://github.com/rails/rails/blob/master/actionpack/CHANGELOG.md) for detailed changes. +Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/actionpack/CHANGELOG.md) for detailed changes. ### Notable changes @@ -215,7 +215,7 @@ Please refer to the [Changelog](https://github.com/rails/rails/blob/master/actio Active Record ------------- -Please refer to the [Changelog](https://github.com/rails/rails/blob/master/activerecord/CHANGELOG.md) for detailed changes. +Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/activerecord/CHANGELOG.md) for detailed changes. ### Notable changes @@ -266,9 +266,9 @@ Please refer to the [Changelog](https://github.com/rails/rails/blob/master/activ * `find_all_by_...` can be rewritten using `where(...)`. * `find_last_by_...` can be rewritten using `where(...).last`. * `scoped_by_...` can be rewritten using `where(...)`. - * `find_or_initialize_by_...` can be rewritten using `where(...).first_or_initialize`. - * `find_or_create_by_...` can be rewritten using `find_or_create_by(...)` or `where(...).first_or_create`. - * `find_or_create_by_...!` can be rewritten using `find_or_create_by!(...)` or `where(...).first_or_create!`. + * `find_or_initialize_by_...` can be rewritten using `find_or_initialize_by(...)`. + * `find_or_create_by_...` can be rewritten using `find_or_create_by(...)`. + * `find_or_create_by_...!` can be rewritten using `find_or_create_by!(...)`. Credits ------- diff --git a/guides/source/_welcome.html.erb b/guides/source/_welcome.html.erb index 9210c40c17..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.13/">http://guides.rubyonrails.org/v3.2.13/</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 8dcd544a52..de9ead78a6 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 @@ -328,9 +328,7 @@ the job done: ```ruby def product_params - params.require(:product).permit(:name).tap do |whitelisted| - whitelisted[:data] = params[:product][:data] - end + params.require(:product).permit(:name, data: params[:product][:data].try(:keys)) end ``` @@ -350,7 +348,7 @@ For most stores, this ID is used to look up the session data on the server, e.g. The CookieStore can store around 4kB of data - much less than the others - but this is usually enough. Storing large amounts of data in the session is discouraged no matter which session store your application uses. You should especially avoid storing complex objects (anything other than basic Ruby objects, the most common example being model instances) in the session, as the server might not be able to reassemble them between requests, which will result in an error. -If your user sessions don't store critical data or don't need to be around for long periods (for instance if you just use the flash for messaging), you can consider using ActionDispatch::Session::CacheStore. This will store sessions using the cache implementation you have configured for your application. The advantage of this is that you can use your existing cache infrastructure for storing sessions without requiring any additional setup or administration. The downside, of course, is that the sessions will be ephemeral and could disappear at any time. +If your user sessions don't store critical data or don't need to be around for long periods (for instance if you just use the flash for messaging), you can consider using `ActionDispatch::Session::CacheStore`. This will store sessions using the cache implementation you have configured for your application. The advantage of this is that you can use your existing cache infrastructure for storing sessions without requiring any additional setup or administration. The downside, of course, is that the sessions will be ephemeral and could disappear at any time. Read more about session storage in the [Security Guide](security.html). @@ -808,11 +806,11 @@ class AdminsController < ApplicationController private - def authenticate - authenticate_or_request_with_http_digest do |username| - USERS[username] + def authenticate + authenticate_or_request_with_http_digest do |username| + USERS[username] + end end - end end ``` @@ -839,13 +837,13 @@ class ClientsController < ApplicationController private - def generate_pdf(client) - Prawn::Document.new do - text client.name, align: :center - text "Address: #{client.address}" - text "Email: #{client.email}" - end.render - end + def generate_pdf(client) + Prawn::Document.new do + text client.name, align: :center + text "Address: #{client.address}" + text "Email: #{client.email}" + end.render + end end ``` @@ -1048,9 +1046,9 @@ class ApplicationController < ActionController::Base private - def record_not_found - render text: "404 Not Found", status: 404 - end + def record_not_found + render text: "404 Not Found", status: 404 + end end ``` @@ -1062,10 +1060,10 @@ class ApplicationController < ActionController::Base private - def user_not_authorized - flash[:error] = "You don't have access to this section." - redirect_to :back - end + def user_not_authorized + flash[:error] = "You don't have access to this section." + redirect_to :back + end end class ClientsController < ApplicationController @@ -1079,10 +1077,10 @@ class ClientsController < ApplicationController private - # If the user is not authorized, just throw the exception. - def check_authorization - raise User::NotAuthorized unless current_user.admin? - end + # If the user is not authorized, just throw the exception. + def check_authorization + raise User::NotAuthorized unless current_user.admin? + end end ``` diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index bf34799eb3..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 %>. @@ -569,25 +569,25 @@ class UserMailer < ActionMailer::Base private - def set_delivery_options - # You have access to the mail instance, - # @business and @user instance variables here - if @business && @business.has_smtp_settings? - mail.delivery_method.settings.merge!(@business.smtp_settings) + def set_delivery_options + # You have access to the mail instance, + # @business and @user instance variables here + if @business && @business.has_smtp_settings? + mail.delivery_method.settings.merge!(@business.smtp_settings) + end end - end - def prevent_delivery_to_guests - if @user && @user.guest? - mail.perform_deliveries = false + def prevent_delivery_to_guests + if @user && @user.guest? + mail.perform_deliveries = false + end end - end - def set_business_headers - if @business - headers["X-SMTPAPI-CATEGORY"] = @business.code + def set_business_headers + if @business + headers["X-SMTPAPI-CATEGORY"] = @business.code + end end - end end ``` 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_model_basics.md b/guides/source/active_model_basics.md index 1d87646e49..0019d08328 100644 --- a/guides/source/active_model_basics.md +++ b/guides/source/active_model_basics.md @@ -120,8 +120,8 @@ class Person end def save - @previously_changed = changes # do save work... + changes_applied end end ``` diff --git a/guides/source/active_record_basics.md b/guides/source/active_record_basics.md index a80c3b6c8e..a184f0753d 100644 --- a/guides/source/active_record_basics.md +++ b/guides/source/active_record_basics.md @@ -48,10 +48,10 @@ overall database access code. Active Record gives us several mechanisms, the most important being the ability to: -* Represent models and their data -* Represent associations between these models -* Represent inheritance hierarchies through related models -* Validate models before they get persisted to the database +* Represent models and their data. +* Represent associations between these models. +* Represent inheritance hierarchies through related models. +* Validate models before they get persisted to the database. * Perform database operations in an object-oriented fashion. Convention over Configuration in Active Record @@ -78,15 +78,15 @@ of two or more words, the model class name should follow the Ruby conventions, using the CamelCase form, while the table name must contain the words separated by underscores. Examples: -* Database Table - Plural with underscores separating words (e.g., `book_clubs`) +* Database Table - Plural with underscores separating words (e.g., `book_clubs`). * Model Class - Singular with the first letter of each word capitalized (e.g., -`BookClub`) +`BookClub`). | Model / Class | Table / Schema | | ------------- | -------------- | | `Post` | `posts` | | `LineItem` | `line_items` | -| `Deer` | `deer` | +| `Deer` | `deers` | | `Mouse` | `mice` | | `Person` | `people` | @@ -101,7 +101,7 @@ depending on the purpose of these columns. fields that Active Record will look for when you create associations between your models. * **Primary keys** - By default, Active Record will use an integer column named - `id` as the table's primary key. When using [Rails + `id` as the table's primary key. When using [Active Record Migrations](migrations.html) to create your tables, this column will be automatically created. @@ -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 @@ -368,6 +368,6 @@ Rails keeps track of which files have been committed to the database and provides rollback features. To actually create the table, you'd run `rake db:migrate` and to roll it back, `rake db:rollback`. -Note that the above code is database-agnostic: it will run in MySQL, postgresql, -Oracle and others. You can learn more about migrations in the [Active Record -Migrations guide](migrations.html) +Note that the above code is database-agnostic: it will run in MySQL, +PostgreSQL, Oracle and others. You can learn more about migrations in the +[Active Record Migrations guide](migrations.html). diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 95eb84dd1f..ac5e8ffc0c 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -35,11 +35,11 @@ class User < ActiveRecord::Base before_validation :ensure_login_has_a_value protected - def ensure_login_has_a_value - if login.nil? - self.login = email unless email.blank? + def ensure_login_has_a_value + if login.nil? + self.login = email unless email.blank? + end end - end end ``` @@ -65,13 +65,13 @@ class User < ActiveRecord::Base after_validation :set_location, on: [ :create, :update ] protected - def normalize_name - self.name = self.name.downcase.titleize - end + def normalize_name + self.name = self.name.downcase.titleize + end - def set_location - self.location = LocationService.query(self) - end + def set_location + self.location = LocationService.query(self) + end end ``` @@ -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 -------------------- @@ -290,7 +290,7 @@ Here's an example where we create a class with an `after_destroy` callback for a ```ruby class PictureFileCallbacks def after_destroy(picture_file) - if File.exists?(picture_file.filepath) + if File.exist?(picture_file.filepath) File.delete(picture_file.filepath) end end @@ -310,7 +310,7 @@ Note that we needed to instantiate a new `PictureFileCallbacks` object, since we ```ruby class PictureFileCallbacks def self.after_destroy(picture_file) - if File.exists?(picture_file.filepath) + if File.exist?(picture_file.filepath) File.delete(picture_file.filepath) end end diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index ba0dc6d9eb..bdfcfd92ce 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -473,7 +473,7 @@ In the case of a belongs_to relationship, an association key can be used to spec ```ruby Post.where(author: author) -Author.joins(:posts).where(posts: {author: author}) +Author.joins(:posts).where(posts: { author: author }) ``` NOTE: The values cannot be symbols. For example, you cannot do `Client.where(status: :active)`. @@ -685,9 +685,9 @@ This will return single order objects for each day, but only those that are orde Overriding Conditions --------------------- -### `except` +### `unscope` -You can specify certain conditions to be excepted by using the `except` method. For example: +You can specify certain conditions to be removed using the `unscope` method. For example: ```ruby Post.where('id > 10').limit(20).order('id asc').except(:order) @@ -698,30 +698,24 @@ The SQL that would be executed: ```sql SELECT * FROM posts WHERE id > 10 LIMIT 20 -# Original query without `except` +# Original query without `unscope` SELECT * FROM posts WHERE id > 10 ORDER BY id asc LIMIT 20 ``` -### `unscope` - -The `except` method does not work when the relation is merged. For example: - -```ruby -Post.comments.except(:order) -``` - -will still have an order if the order comes from a default scope on Comment. In order to remove all ordering, even from relations which are merged in, use unscope as follows: +You can additionally unscope specific where clauses. For example: ```ruby -Post.order('id DESC').limit(20).unscope(:order) = Post.limit(20) -Post.order('id DESC').limit(20).unscope(:order, :limit) = Post.all +Post.where(id: 10, trashed: false).unscope(where: :id) +# => SELECT "posts".* FROM "posts" WHERE trashed = 0 ``` -You can additionally unscope specific where clauses. For example: +A relation which has used `unscope` will affect any relation it is +merged in to: ```ruby -Post.where(id: 10).limit(1).unscope({ where: :id }, :limit).order('id DESC') = Post.order('id DESC') +Post.order('id asc').merge(Post.unscope(:order)) +# => SELECT "posts".* FROM "posts" ``` ### `only` @@ -943,7 +937,7 @@ WARNING: This method only works with `INNER JOIN`. Active Record lets you use the names of the [associations](association_basics.html) defined on the model as a shortcut for specifying `JOIN` clause for those associations when using the `joins` method. -For example, consider the following `Category`, `Post`, `Comments` and `Guest` models: +For example, consider the following `Category`, `Post`, `Comment`, `Guest` and `Tag` models: ```ruby class Category < ActiveRecord::Base @@ -1022,7 +1016,7 @@ Or, in English: "return all posts that have a comment made by a guest." #### Joining Nested Associations (Multiple Level) ```ruby -Category.joins(posts: [{comments: :guest}, :tags]) +Category.joins(posts: [{ comments: :guest }, :tags]) ``` This produces: @@ -1048,7 +1042,7 @@ An alternative and cleaner syntax is to nest the hash conditions: ```ruby time_range = (Time.now.midnight - 1.day)..Time.now.midnight -Client.joins(:orders).where(orders: {created_at: time_range}) +Client.joins(:orders).where(orders: { created_at: time_range }) ``` This will find all clients who have orders that were created yesterday, again using a `BETWEEN` SQL expression. @@ -1109,7 +1103,7 @@ This loads all the posts and the associated category and comments for each post. #### Nested Associations Hash ```ruby -Category.includes(posts: [{comments: :guest}, :tags]).find(1) +Category.includes(posts: [{ comments: :guest }, :tags]).find(1) ``` This will find the category with id 1 and eager load all of the associated posts, the associated posts' tags and comments, and every comment's guest association. @@ -1189,7 +1183,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) @@ -1301,7 +1295,7 @@ especially useful if a `default_scope` is specified in the model and should not applied for this particular query. ```ruby -Client.unscoped.all +Client.unscoped.load ``` This method removes all scoping and will do a normal query on the table. @@ -1358,7 +1352,7 @@ COMMIT The new record might not be saved to the database; that depends on whether validations passed or not (just like `create`). -Suppose we want to set the 'locked' attribute to true if we're +Suppose we want to set the 'locked' attribute to `false` if we're creating a new record, but we don't want to include it in the query. So we want to find the client named "Andy", or if that client doesn't exist, create a client named "Andy" which is not locked. @@ -1466,7 +1460,7 @@ Client.pluck(:id, :name) # => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']] ``` -`pluck` makes it possible to replace code like +`pluck` makes it possible to replace code like: ```ruby Client.select(:id).map { |c| c.id } @@ -1476,7 +1470,7 @@ Client.select(:id).map(&:id) Client.select(:id, :name).map { |c| [c.id, c.name] } ``` -with +with: ```ruby Client.pluck(:id) @@ -1484,6 +1478,37 @@ Client.pluck(:id) Client.pluck(:id, :name) ``` +Unlike `select`, `pluck` directly converts a database result into a Ruby `Array`, +without constructing `ActiveRecord` objects. This can mean better performance for +a large or often-running query. However, any model method overrides will +not be available. For example: + +```ruby +class Client < ActiveRecord::Base + def name + "I am #{super}" + end +end + +Client.select(:name).map &:name +# => ["I am David", "I am Jeremy", "I am Jose"] + +Client.pluck(:name) +# => ["David", "Jeremy", "Jose"] +``` + +Furthermore, unlike `select` and other `Relation` scopes, `pluck` triggers an immediate +query, and thus cannot be chained with any further scopes, although it can work with +scopes already constructed earlier: + +```ruby +Client.pluck(:name).limit(1) +# => NoMethodError: undefined method `limit' for #<Array:0x007ff34d3ad6d8> + +Client.limit(1).pluck(:name) +# => ["David"] +``` + ### `ids` `ids` can be used to pluck all the IDs for the relation using the table's primary key. @@ -1505,18 +1530,21 @@ Person.ids Existence of Objects -------------------- -If you simply want to check for the existence of the object there's a method called `exists?`. This method will query the database using the same query as `find`, but instead of returning an object or collection of objects it will return either `true` or `false`. +If you simply want to check for the existence of the object there's a method called `exists?`. +This method will query the database using the same query as `find`, but instead of returning an +object or collection of objects it will return either `true` or `false`. ```ruby Client.exists?(1) ``` -The `exists?` method also takes multiple ids, but the catch is that it will return true if any one of those records exists. +The `exists?` method also takes multiple values, but the catch is that it will return `true` if any +one of those records exists. ```ruby -Client.exists?(1,2,3) +Client.exists?(id: [1,2,3]) # or -Client.exists?([1,2,3]) +Client.exists?(name: ['John', 'Sergei']) ``` It's even possible to use `exists?` without any arguments on a model or a relation. @@ -1525,7 +1553,8 @@ It's even possible to use `exists?` without any arguments on a model or a relati Client.where(first_name: 'Ryan').exists? ``` -The above returns `true` if there is at least one client with the `first_name` 'Ryan' and `false` otherwise. +The above returns `true` if there is at least one client with the `first_name` 'Ryan' and `false` +otherwise. ```ruby Client.exists? @@ -1575,7 +1604,7 @@ Client.where(first_name: 'Ryan').count You can also use various finder methods on a relation for performing complex calculations: ```ruby -Client.includes("orders").where(first_name: 'Ryan', orders: {status: 'received'}).count +Client.includes("orders").where(first_name: 'Ryan', orders: { status: 'received' }).count ``` Which will execute: diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md index 0b2f0a47fa..cbd1ac9bdf 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -175,28 +175,28 @@ class Person < ActiveRecord::Base end >> p = Person.new -#=> #<Person id: nil, name: nil> +# => #<Person id: nil, name: nil> >> p.errors.messages -#=> {} +# => {} >> p.valid? -#=> false +# => false >> p.errors.messages -#=> {name:["can't be blank"]} +# => {name:["can't be blank"]} >> p = Person.create -#=> #<Person id: nil, name: nil> +# => #<Person id: nil, name: nil> >> p.errors.messages -#=> {name:["can't be blank"]} +# => {name:["can't be blank"]} >> p.save -#=> false +# => false >> p.save! -#=> ActiveRecord::RecordInvalid: Validation failed: Name can't be blank +# => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank >> Person.create! -#=> ActiveRecord::RecordInvalid: Validation failed: Name can't be blank +# => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank ``` `invalid?` is simply the inverse of `valid?`. It triggers your validations, @@ -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 ca023f7f66..e6cd70b0ce 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -37,9 +37,10 @@ For every single method defined as a core extension this guide has a note that s NOTE: Defined in `active_support/core_ext/object/blank.rb`. -That means that this single call is enough: +That means that you can require it like this: ```ruby +require 'active_support' require 'active_support/core_ext/object/blank' ``` @@ -52,6 +53,7 @@ The next level is to simply load all extensions to `Object`. As a rule of thumb, Thus, to load all extensions to `Object` (including `blank?`): ```ruby +require 'active_support' require 'active_support/core_ext/object' ``` @@ -60,6 +62,7 @@ require 'active_support/core_ext/object' You may prefer just to load all core extensions, there is a file for that: ```ruby +require 'active_support' require 'active_support/core_ext' ``` @@ -96,12 +99,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 ``` @@ -175,14 +179,14 @@ duplicate = array.dup duplicate.push 'another-string' # the object was duplicated, so the element was added only to the duplicate -array #=> ['string'] -duplicate #=> ['string', 'another-string'] +array # => ['string'] +duplicate # => ['string', 'another-string'] duplicate.first.gsub!('string', 'foo') # first element was not duplicated, it will be changed in both arrays -array #=> ['foo'] -duplicate #=> ['foo', 'another-string'] +array # => ['foo'] +duplicate # => ['foo', 'another-string'] ``` As you can see, after duplicating the `Array` instance, we got another object, therefore we can modify it and the original object will stay unchanged. This is not true for array's elements, however. Since `dup` does not make deep copy, the string inside the array is still the same object. @@ -195,8 +199,8 @@ duplicate = array.deep_dup duplicate.first.gsub!('string', 'foo') -array #=> ['string'] -duplicate #=> ['foo'] +array # => ['string'] +duplicate # => ['foo'] ``` If the object is not duplicable, `deep_dup` will just return it: @@ -420,11 +424,9 @@ NOTE: Defined in `active_support/core_ext/object/with_options.rb`. ### JSON support -Active Support provides a better implemention 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 @@ -1091,6 +1093,15 @@ end we can access `field_error_proc` in views. +Also, you can pass a block to `cattr_*` to set up the attribute with a default value: + +```ruby +class MysqlAdapter < AbstractAdapter + # Generates class methods to access @@emulate_booleans with default value of true. + cattr_accessor(:emulate_booleans) { true } +end +``` + The generation of the reader instance method can be prevented by setting `:instance_reader` to `false` and the generation of the writer instance method can be prevented by setting `:instance_writer` to `false`. Generation of both methods can be prevented by setting `:instance_accessor` to `false`. In all cases, the value must be exactly `false` and not any false value. ```ruby @@ -1543,7 +1554,7 @@ ActiveSupport::Inflector.inflections do |inflect| inflect.acronym 'SSL' end -"SSLError".underscore.camelize #=> "SSLError" +"SSLError".underscore.camelize # => "SSLError" ``` `camelize` is aliased to `camelcase`. @@ -1773,6 +1784,12 @@ The method `humanize` gives you a sensible name for display out of an attribute "comments_count".humanize # => "Comments count" ``` +The capitalization of the first word can be turned off by setting the optional parameter `capitalize` to false: + +```ruby +"author_id".humanize(capitalize: false) # => "author" +``` + The helper method `full_messages` uses `humanize` as a fallback to include attribute names: ```ruby @@ -1999,7 +2016,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 +2461,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 +2687,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 @@ -2736,14 +2715,14 @@ NOTE: Defined in `active_support/core_ext/hash/except.rb`. The method `transform_keys` accepts a block and returns a hash that has applied the block operations to each of the keys in the receiver: ```ruby -{nil => nil, 1 => 1, a: :a}.transform_keys{ |key| key.to_s.upcase } +{nil => nil, 1 => 1, a: :a}.transform_keys { |key| key.to_s.upcase } # => {"" => nil, "A" => :a, "1" => 1} ``` The result in case of collision is undefined: ```ruby -{"a" => 1, a: 2}.transform_keys{ |key| key.to_s.upcase } +{"a" => 1, a: 2}.transform_keys { |key| key.to_s.upcase } # => {"A" => 2}, in my test, can't rely on this result though ``` @@ -2751,11 +2730,11 @@ This method may be useful for example to build specialized conversions. For inst ```ruby def stringify_keys - transform_keys{ |key| key.to_s } + transform_keys { |key| key.to_s } end ... def symbolize_keys - transform_keys{ |key| key.to_sym rescue key } + transform_keys { |key| key.to_sym rescue key } end ``` @@ -2764,7 +2743,7 @@ There's also the bang variant `transform_keys!` that applies the block operation Besides that, one can use `deep_transform_keys` and `deep_transform_keys!` to perform the block operation on all the keys in the given hash and all the hashes nested into it. An example of the result is: ```ruby -{nil => nil, 1 => 1, nested: {a: 3, 5 => 5}}.deep_transform_keys{ |key| key.to_s.upcase } +{nil => nil, 1 => 1, nested: {a: 3, 5 => 5}}.deep_transform_keys { |key| key.to_s.upcase } # => {""=>nil, "1"=>1, "NESTED"=>{"A"=>3, "5"=>5}} ``` @@ -3843,13 +3822,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 +3851,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/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md index 969596f470..6c77a40d42 100644 --- a/guides/source/active_support_instrumentation.md +++ b/guides/source/active_support_instrumentation.md @@ -396,6 +396,15 @@ INFO. Cache stores my add their own keys } ``` +Railties +-------- + +### load_config_initializer.railties + +| Key | Value | +| -------------- | ----------------------------------------------------- | +| `:initializer` | Path to loaded initializer from `config/initializers` | + Rails ----- diff --git a/guides/source/api_documentation_guidelines.md b/guides/source/api_documentation_guidelines.md index 98ead9570f..d403de5fd3 100644 --- a/guides/source/api_documentation_guidelines.md +++ b/guides/source/api_documentation_guidelines.md @@ -45,7 +45,7 @@ Use the article "an" for "SQL", as in "an SQL statement". Also "an SQLite databa English ------- -Please use American English (<em>color</em>, <em>center</em>, <em>modularize</em>, etc).. See [a list of American and British English spelling differences here](http://en.wikipedia.org/wiki/American_and_British_English_spelling_differences). +Please use American English (<em>color</em>, <em>center</em>, <em>modularize</em>, etc). See [a list of American and British English spelling differences here](http://en.wikipedia.org/wiki/American_and_British_English_spelling_differences). Example Code ------------ diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index 72aff1e0dd..39448e92d5 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -151,8 +151,7 @@ environments. You can enable or disable it in your configuration through the More reading: * [Optimize caching](http://code.google.com/speed/page-speed/docs/caching.html) -* [Revving Filenames: don't use -* querystring](http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/) +* [Revving Filenames: don't use querystring](http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/) How to Use the Asset Pipeline @@ -405,11 +404,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 +501,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 +1042,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 c58dd2e90a..c0482f6106 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -261,7 +261,10 @@ With `through: :sections` specified, Rails will now understand: ### The `has_one :through` Association -A `has_one :through` association sets up a one-to-one connection with another model. This association indicates that the declaring model can be matched with one instance of another model by proceeding _through_ a third model. For example, if each supplier has one account, and each account is associated with one account history, then the customer model could look like this: +A `has_one :through` association sets up a one-to-one connection with another model. This association indicates +that the declaring model can be matched with one instance of another model by proceeding _through_ a third model. +For example, if each supplier has one account, and each account is associated with one account history, then the +supplier model could look like this: ```ruby class Supplier < ActiveRecord::Base @@ -337,7 +340,7 @@ class CreateAssembliesAndParts < ActiveRecord::Migration t.timestamps end - create_table :assemblies_parts do |t| + create_table :assemblies_parts, id: false do |t| t.belongs_to :assembly t.belongs_to :part end @@ -715,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)` @@ -1019,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)` @@ -1137,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` @@ -1286,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, ...)` @@ -1775,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/caching_with_rails.md b/guides/source/caching_with_rails.md index 3cf6631c32..0d45e5fb28 100644 --- a/guides/source/caching_with_rails.md +++ b/guides/source/caching_with_rails.md @@ -189,7 +189,7 @@ The main methods to call are `read`, `write`, `delete`, `exist?`, and `fetch`. T There are some common options used by all cache implementations. These can be passed to the constructor or the various methods to interact with entries. -* `:namespace` - This option can be used to create a namespace within the cache store. It is especially useful if your application shares a cache with other applications. The default value will include the application name and Rails environment. +* `:namespace` - This option can be used to create a namespace within the cache store. It is especially useful if your application shares a cache with other applications. * `:compress` - This option can be used to indicate that compression should be used in the cache. This can be useful for transferring large cache entries over a slow network. @@ -225,7 +225,7 @@ This is the default cache store implementation. ### ActiveSupport::Cache::MemCacheStore -This cache store uses Danga's `memcached` server to provide a centralized cache for your application. Rails uses the bundled `dalli` gem by default. This is currently the most popular cache store for production websites. It can be used to provide a single, shared cache cluster with very a high performance and redundancy. +This cache store uses Danga's `memcached` server to provide a centralized cache for your application. Rails uses the bundled `dalli` gem by default. This is currently the most popular cache store for production websites. It can be used to provide a single, shared cache cluster with very high performance and redundancy. When initializing the cache, you need to specify the addresses for all memcached servers in your cluster. If none is specified, it will assume memcached is running on the local host on the default port, but this is not an ideal set up for larger sites. @@ -327,7 +327,7 @@ class ProductsController < ApplicationController end ``` -Instead of a options hash, you can also simply pass in a model, Rails will use the `updated_at` and `cache_key` methods for setting `last_modified` and `etag`: +Instead of an options hash, you can also simply pass in a model, Rails will use the `updated_at` and `cache_key` methods for setting `last_modified` and `etag`: ```ruby class ProductsController < ApplicationController diff --git a/guides/source/command_line.md b/guides/source/command_line.md index f11e08a0c1..3b80faec7f 100644 --- a/guides/source/command_line.md +++ b/guides/source/command_line.md @@ -1,8 +1,6 @@ The Rails Command Line ====================== -Rails comes with every command line tool you'll need to - After reading this guide, you will know: * How to create a Rails application. @@ -58,8 +56,6 @@ Rails will set you up with what seems like a huge amount of stuff for such a tin The `rails server` command launches a small web server named WEBrick which comes bundled with Ruby. You'll use this any time you want to access your application through a web browser. -INFO: WEBrick isn't your only option for serving Rails. We'll get to that [later](#server-with-different-backends). - With no further work, `rails server` will run our new shiny Rails app: ```bash @@ -381,13 +377,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 @@ -493,7 +490,9 @@ The `tmp:` namespaced tasks will help you clear and create the `Rails.root/tmp` ### Custom Rake Tasks -Custom rake tasks have a `.rake` extension and are placed in `Rails.root/lib/tasks`. +Custom rake tasks have a `.rake` extension and are placed in +`Rails.root/lib/tasks`. You can create these custom rake tasks with the +`bin/rails generate task` command. ```ruby desc "I am short, but comprehensive description for my cool task" diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 3cf5cdd71f..8ac34c9716 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -103,7 +103,7 @@ numbers. New applications filter out passwords by adding the following `config.f * `config.force_ssl` forces all requests to be under HTTPS protocol by using `ActionDispatch::SSL` middleware. -* `config.log_formatter` defines the formatter of the Rails logger. This option defaults to a instance of `ActiveSupport::Logger::SimpleFormatter` for all modes except production, where it defaults to `Logger::Formatter`. +* `config.log_formatter` defines the formatter of the Rails logger. This option defaults to an instance of `ActiveSupport::Logger::SimpleFormatter` for all modes except production, where it defaults to `Logger::Formatter`. * `config.log_level` defines the verbosity of the Rails logger. This option defaults to `:debug` for all modes except production, where it defaults to `:info`. @@ -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 @@ -528,7 +536,7 @@ Change the username and password in the `development` section as appropriate. By default Rails ships with three environments: "development", "test", and "production". While these are sufficient for most use cases, there are circumstances when you want more environments. -Imagine you have a server which mirrors the production environment but is only used for testing. Such a server is commonly called a "staging server". To define an environment called "staging" for this server just by create a file called `config/environments/staging.rb`. Please use the contents of any existing file in `config/environments` as a starting point and make the necessary changes from there. +Imagine you have a server which mirrors the production environment but is only used for testing. Such a server is commonly called a "staging server". To define an environment called "staging" for this server, just create a file called `config/environments/staging.rb`. Please use the contents of any existing file in `config/environments` as a starting point and make the necessary changes from there. That environment is no different than the default ones, start a server with `rails server -e staging`, a console with `rails console staging`, `Rails.env.staging?` works, etc. @@ -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: @@ -759,4 +767,15 @@ Since the connection pooling is handled inside of Active Record by default, all Any one request will check out a connection the first time it requires access to the database, after which it will check the connection back in, at the end of the request, meaning that the additional connection slot will be available again for the next request in the queue. +If you try to use more connections than are available, Active Record will block +and wait for a connection from the pool. When it cannot get connection, a timeout +error similar to given below will be thrown. + +```ruby +ActiveRecord::ConnectionTimeoutError - could not obtain a database connection within 5 seconds. The max pool size is currently 5; consider increasing it: +``` + +If you get the above error, you might want to increase the size of connection +pool by incrementing the `pool` option in `database.yml` + NOTE. If you have enabled `Rails.threadsafe!` mode then there could be a chance that several threads may be accessing multiple connections simultaneously. So depending on your current request load, you could very well have multiple threads contending for a limited amount of connections. diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md index fc6b2f992a..8b3ff24ee1 100644 --- a/guides/source/contributing_to_ruby_on_rails.md +++ b/guides/source/contributing_to_ruby_on_rails.md @@ -219,7 +219,7 @@ When working with documentation, please take into account the [API Documentation NOTE: As explained earlier, ordinary code patches should have proper documentation coverage. Docrails is only used for isolated documentation improvements. -NOTE: To help our CI servers you can add [ci skip] to your documentation commit message to skip build on that commit. Please remember to use it for commits containing only documentation changes. +NOTE: To help our CI servers you should add [ci skip] to your documentation commit message to skip build on that commit. Please remember to use it for commits containing only documentation changes. WARNING: Docrails has a very strict policy: no code can be touched whatsoever, no matter how trivial or small the change. Only RDoc and guides can be edited via docrails. Also, CHANGELOGs should never be edited in docrails. @@ -259,7 +259,7 @@ workflow with the [rails-dev-box](https://github.com/rails/rails-dev-box). As a compromise, test what your code obviously affects, and if the change is not in railties run the whole test suite of the affected component. If all is -green that's enough to propose your contribution. We have [Travis CI](https://travis-ci.org/rails/rails) +green that's enough to propose your contribution. We have [Travis CI](https://travis\-ci.org/rails/rails) as a safety net for catching unexpected breakages elsewhere. @@ -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. +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. -Write your branch name in the branch field (this is filled with "master" by default) and press "Update Commit Range". +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 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. +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/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md index 14d65e4747..226137c89a 100644 --- a/guides/source/debugging_rails_applications.md +++ b/guides/source/debugging_rails_applications.md @@ -233,7 +233,7 @@ only evaluated if the output level is the same or included in the allowed level (i.e. lazy loading). The same code rewritten would be: ```ruby -logger.debug {"Person attibutes hash: #{@person.attributes.inspect}"} +logger.debug {"Person attributes hash: #{@person.attributes.inspect}"} ``` The contents of the block, and therefore the string interpolation, is only @@ -386,7 +386,7 @@ Finally, to see where you are in the code again you can type `list=` 7 8 respond_to do |format| 9 format.html # index.html.erb - 10 format.json { render :json => @posts } + 10 format.json { render json: @posts } ``` ### The Context 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/engines.md b/guides/source/engines.md index bc404ccb7f..2266b1fd7f 100644 --- a/guides/source/engines.md +++ b/guides/source/engines.md @@ -253,7 +253,7 @@ The helper inside `app/helpers/blorgh/posts_helper.rb` is also namespaced: ```ruby module Blorgh - class PostsHelper + module PostsHelper ... end end @@ -307,7 +307,11 @@ create test/models/blorgh/comment_test.rb create test/fixtures/blorgh/comments.yml ``` -This generator call will generate just the necessary model files it needs, namespacing the files under a `blorgh` directory and creating a model class called `Blorgh::Comment`. +This generator call will generate just the necessary model files it needs, namespacing the files under a `blorgh` directory and creating a model class called `Blorgh::Comment`. Now run the migration to create our blorgh_comments table: + +```bash +$ rake db:migrate +``` To show the comments on a post, edit `app/views/blorgh/posts/show.html.erb` and add this line before the "Edit" link: @@ -399,9 +403,9 @@ def create end private -def comment_params - params.require(:comment).permit(:text) -end + def comment_params + params.require(:comment).permit(:text) + end ``` This is the final part required to get the new comment form working. Displaying the comments however, is not quite right yet. If you were to create a comment right now you would see this error: @@ -850,7 +854,6 @@ module Blorgh::Concerns::Models::Post before_save :set_author private - def set_author self.author = User.find_or_create_by(name: author_name) end @@ -872,7 +875,7 @@ end When Rails looks for a view to render, it will first look in the `app/views` directory of the application. If it cannot find the view there, then it will check in the `app/views` directories of all engines which have this directory. -In the `blorgh` engine, there is a currently a file at `app/views/blorgh/posts/index.html.erb`. When the engine is asked to render the view for `Blorgh::PostsController`'s `index` action, it will first see if it can find it at `app/views/blorgh/posts/index.html.erb` within the application and then if it cannot it will look inside the engine. +When the application is asked to render the view for `Blorgh::PostsController`'s index action, it will look the path `app/views/blorgh/posts/index.html.erb`, first within the application. If it cannot find it, it will look inside the engine. You can override this view in the application by simply creating a new file at `app/views/blorgh/posts/index.html.erb`. Then you can completely change what this view would normally output. @@ -951,7 +954,7 @@ INFO. Remember that in order to use languages like Sass or CoffeeScript, you sho There are some situations where your engine's assets are not required by the host application. For example, say that you've created an admin functionality that only exists for your engine. In this case, the host application doesn't need to require `admin.css` -or `admin.js`. Only the gem's admin layout needs these assets. It doesn't make sense for the host app to include `"blorg/admin.css"` in it's stylesheets. In this situation, you should explicitly define these assets for precompilation. +or `admin.js`. Only the gem's admin layout needs these assets. It doesn't make sense for the host app to include `"blorgh/admin.css"` in it's stylesheets. In this situation, you should explicitly define these assets for precompilation. This tells sprockets to add your engine assets when `rake assets:precompile` is ran. You can define assets for precompilation in `engine.rb` diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md index 578cfbe105..4b6d8a93f0 100644 --- a/guides/source/form_helpers.md +++ b/guides/source/form_helpers.md @@ -67,7 +67,7 @@ To create this form you will use `form_tag`, `label_tag`, `text_field_tag`, and This will generate the following HTML: ```html -<form accept-charset="UTF-8" action="/search" method="get"> +<form accept-charset="UTF-8" action="/search" method="get"><div style="margin:0;padding:0;display:inline"><input name="utf8" type="hidden" value="✓" /></div> <label for="q">Search for:</label> <input id="q" name="q" type="text" /> <input name="commit" type="submit" value="Search" /> @@ -914,9 +914,9 @@ def create end private -def person_params - params.require(:person).permit(:name, addresses_attributes: [:id, :kind, :street]) -end + def person_params + params.require(:person).permit(:name, addresses_attributes: [:id, :kind, :street]) + end ``` ### Removing Objects @@ -973,4 +973,4 @@ As a convenience you can instead pass the symbol `:all_blank` which will create ### Adding Fields on the Fly -Rather than rendering multiple sets of fields ahead of time you may wish to add them only when a user clicks on an 'Add new child' button. Rails does not provide any builtin support for this. When generating new sets of fields you must ensure the key of the associated array is unique - the current javascript date (milliseconds after the epoch) is a common choice. +Rather than rendering multiple sets of fields ahead of time you may wish to add them only when a user clicks on an 'Add new address' button. Rails does not provide any builtin support for this. When generating new sets of fields you must ensure the key of the associated array is unique - the current JavaScript date (milliseconds after the epoch) is a common choice. diff --git a/guides/source/generators.md b/guides/source/generators.md index a8a34d0ac4..e9c8ef0225 100644 --- a/guides/source/generators.md +++ b/guides/source/generators.md @@ -35,7 +35,7 @@ $ rails generate helper --help Creating Your First Generator ----------------------------- -Since Rails 3.0, generators are built on top of [Thor](https://github.com/wycats/thor). Thor provides powerful options parsing and a great API for manipulating files. For instance, let's build a generator that creates an initializer file named `initializer.rb` inside `config/initializers`. +Since Rails 3.0, generators are built on top of [Thor](https://github.com/erikhuda/thor). Thor provides powerful options parsing and a great API for manipulating files. For instance, let's build a generator that creates an initializer file named `initializer.rb` inside `config/initializers`. The first step is to create a file at `lib/generators/initializer_generator.rb` with the following content: @@ -47,7 +47,7 @@ class InitializerGenerator < Rails::Generators::Base end ``` -NOTE: `create_file` is a method provided by `Thor::Actions`. Documentation for `create_file` and other Thor methods can be found in [Thor's documentation](http://rdoc.info/github/wycats/thor/master/Thor/Actions.html) +NOTE: `create_file` is a method provided by `Thor::Actions`. Documentation for `create_file` and other Thor methods can be found in [Thor's documentation](http://rdoc.info/github/erikhuda/thor/master/Thor/Actions.html) Our new generator is quite simple: it inherits from `Rails::Generators::Base` and has one method definition. When a generator is invoked, each public method in the generator is executed sequentially in the order that it is defined. Finally, we invoke the `create_file` method that will create a file at the given destination with the given content. If you are familiar with the Rails Application Templates API, you'll feel right at home with the new generators API. @@ -171,7 +171,7 @@ Before we customize our workflow, let's first see what our scaffold looks like: ```bash $ rails generate scaffold User name:string invoke active_record - create db/migrate/20091120125558_create_users.rb + create db/migrate/20130924151154_create_users.rb create app/models/user.rb invoke test_unit create test/models/user_test.rb @@ -193,6 +193,9 @@ $ rails generate scaffold User name:string create app/helpers/users_helper.rb invoke test_unit create test/helpers/users_helper_test.rb + invoke jbuilder + create app/views/users/index.json.jbuilder + create app/views/users/show.json.jbuilder invoke assets invoke coffee create app/assets/javascripts/users.js.coffee @@ -204,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| @@ -212,20 +215,28 @@ 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: ```bash $ rails generate generator rails/my_helper + create lib/generators/rails/my_helper + create lib/generators/rails/my_helper/my_helper_generator.rb + create lib/generators/rails/my_helper/USAGE + create lib/generators/rails/my_helper/templates ``` -After that, we can delete both the `templates` directory and the `source_root` class method from our new generators, because we are not going to need them. So our new generator looks like the following: +After that, we can delete both the `templates` directory and the `source_root` +class method call from our new generator, because we are not going to need them. +Add the method below, so our generator looks like the following: ```ruby +# lib/generators/rails/my_helper/my_helper_generator.rb class Rails::MyHelperGenerator < Rails::Generators::NamedBase def create_helper_file create_file "app/helpers/#{file_name}_helper.rb", <<-FILE @@ -241,6 +252,7 @@ We can try out our new generator by creating a helper for users: ```bash $ rails generate my_helper products + create app/helpers/products_helper.rb ``` And it will generate the following helper file in `app/helpers`: @@ -259,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 ``` @@ -279,6 +292,7 @@ Since Rails 3.0, this is easy to do due to the hooks concept. Our new helper doe To do that, we can change the generator this way: ```ruby +# lib/generators/rails/my_helper/my_helper_generator.rb class Rails::MyHelperGenerator < Rails::Generators::NamedBase def create_helper_file create_file "app/helpers/#{file_name}_helper.rb", <<-FILE @@ -322,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 ``` @@ -340,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 @@ -351,7 +367,7 @@ Now, if you create a Comment scaffold, you will see that the shoulda generators ```bash $ rails generate scaffold Comment body:text invoke active_record - create db/migrate/20091120151323_create_comments.rb + create db/migrate/20130924143118_create_comments.rb create app/models/comment.rb invoke shoulda create test/models/comment_test.rb @@ -373,6 +389,9 @@ $ rails generate scaffold Comment body:text create app/helpers/comments_helper.rb invoke shoulda create test/helpers/comments_helper_test.rb + invoke jbuilder + create app/views/comments/index.json.jbuilder + create app/views/comments/show.json.jbuilder invoke assets invoke coffee create app/assets/javascripts/comments.js.coffee @@ -422,7 +441,7 @@ Generator methods The following are methods available for both generators and templates for Rails. -NOTE: Methods provided by Thor are not covered this guide and can be found in [Thor's documentation](http://rdoc.info/github/wycats/thor/master/Thor/Actions.html) +NOTE: Methods provided by Thor are not covered this guide and can be found in [Thor's documentation](http://rdoc.info/github/erikhuda/thor/master/Thor/Actions.html) ### `gem` diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index 81e57aee34..2f322d15da 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -22,8 +22,8 @@ with Rails. However, to get the most out of it, you need to have some prerequisites installed: * The [Ruby](http://www.ruby-lang.org/en/downloads) language version 1.9.3 or newer -* The [RubyGems](http://rubygems.org/) packaging system - * To learn more about RubyGems, please read the [RubyGems User Guide](http://docs.rubygems.org/read/book/1) +* The [RubyGems](http://rubygems.org) packaging system + * To learn more about RubyGems, please read the [RubyGems Guides](http://guides.rubygems.org) * A working installation of the [SQLite3 Database](http://www.sqlite.org) Rails is a web application framework running on the Ruby programming language. @@ -54,9 +54,11 @@ learned elsewhere, you may have a less happy experience. The Rails philosophy includes two major guiding principles: -* DRY - "Don't Repeat Yourself" - suggests that writing the same code over and over again is a bad thing. -* Convention Over Configuration - means that Rails makes assumptions about what you want to do and how you're going to -do it, rather than requiring you to specify every little thing through endless configuration files. +* DRY - "Don't Repeat Yourself" - suggests that writing the same code over and + over again is a bad thing. +* Convention Over Configuration - means that Rails makes assumptions about what + you want to do and how you're going to do it, rather than requiring you to + specify every little thing through endless configuration files. Creating a New Rails Project ---------------------------- @@ -94,10 +96,11 @@ $ gem install rails ``` TIP. A number of tools exist to help you quickly install Ruby and Ruby -on Rails on your system. Windows users can use [Rails Installer](http://railsinstaller.org), while Mac OS X users can use -[Rails One Click](http://railsoneclick.com). +on Rails on your system. Windows users can use [Rails Installer](http://railsinstaller.org), +while Mac OS X users can use [Rails One Click](http://railsoneclick.com). -To verify that you have everything installed correctly, you should be able to run the following: +To verify that you have everything installed correctly, you should be able to +run the following: ```bash $ rails --version @@ -107,29 +110,38 @@ If it says something like "Rails 4.0.0", you are ready to continue. ### Creating the Blog Application -Rails comes with a number of scripts called generators that are designed to make your development life easier by creating everything that's necessary to start working on a particular task. One of these is the new application generator, which will provide you with the foundation of a fresh Rails application so that you don't have to write it yourself. +Rails comes with a number of scripts called generators that are designed to make +your development life easier by creating everything that's necessary to start +working on a particular task. One of these is the new application generator, +which will provide you with the foundation of a fresh Rails application so that +you don't have to write it yourself. -To use this generator, open a terminal, navigate to a directory where you have rights to create files, and type: +To use this generator, open a terminal, navigate to a directory where you have +rights to create files, and type: ```bash $ rails new blog ``` -This will create a Rails application called Blog in a directory called blog and install the gem dependencies that are already mentioned in `Gemfile` using `bundle install`. +This will create a Rails application called Blog in a directory called blog and +install the gem dependencies that are already mentioned in `Gemfile` using +`bundle install`. -TIP: You can see all of the command line options that the Rails -application builder accepts by running `rails new -h`. +TIP: You can see all of the command line options that the Rails application +builder accepts by running `rails new -h`. -After you create the blog application, switch to its folder to continue work directly in that application: +After you create the blog application, switch to its folder to continue work +directly in that application: ```bash $ cd blog ``` -The `rails new blog` command we ran above created a folder in your -working directory called `blog`. The `blog` directory has a number of -auto-generated files and folders that make up the structure of a Rails -application. Most of the work in this tutorial will happen in the `app/` folder, but here's a basic rundown on the function of each of the files and folders that Rails created by default: +The `rails new blog` command we ran above created a folder in your working +directory called `blog`. The `blog` directory has a number of auto-generated +files and folders that make up the structure of a Rails application. Most of the +work in this tutorial will happen in the `app/` folder, but here's a basic +rundown on the function of each of the files and folders that Rails created by default: | File/Folder | Purpose | | ----------- | ------- | @@ -151,35 +163,65 @@ application. Most of the work in this tutorial will happen in the `app/` folder, Hello, Rails! ------------- -To begin with, let's get some text up on screen quickly. To do this, you need to get your Rails application server running. +To begin with, let's get some text up on screen quickly. To do this, you need to +get your Rails application server running. ### Starting up the Web Server -You actually have a functional Rails application already. To see it, you need to start a web server on your development machine. You can do this by running the following in the root directory of your rails application: +You actually have a functional Rails application already. To see it, you need to +start a web server on your development machine. You can do this by running the +following in the root directory of your rails application: ```bash $ rails server ``` -TIP: Compiling CoffeeScript to JavaScript requires a JavaScript runtime and the absence of a runtime will give you an `execjs` error. Usually Mac OS X and Windows come with a JavaScript runtime installed. Rails adds the `therubyracer` gem to Gemfile in a commented line for new apps and you can uncomment if you need it. `therubyrhino` is the recommended runtime for JRuby users and is added by default to Gemfile in apps generated under JRuby. You can investigate about all the supported runtimes at [ExecJS](https://github.com/sstephenson/execjs#readme). +TIP: Compiling CoffeeScript to JavaScript requires a JavaScript runtime and the +absence of a runtime will give you an `execjs` error. Usually Mac OS X and +Windows come with a JavaScript runtime installed. Rails adds the `therubyracer` +gem to Gemfile in a commented line for new apps and you can uncomment if you +need it. `therubyrhino` is the recommended runtime for JRuby users and is added +by default to Gemfile in apps generated under JRuby. You can investigate about +all the supported runtimes at [ExecJS](https://github.com/sstephenson/execjs#readme). -This will fire up WEBrick, a webserver built into Ruby by default. To see your application in action, open a browser window and navigate to <http://localhost:3000>. You should see the Rails default information page: +This will fire up WEBrick, a webserver built into Ruby by default. To see your +application in action, open a browser window and navigate to <http://localhost:3000>. +You should see the Rails default information page:  -TIP: To stop the web server, hit Ctrl+C in the terminal window where it's running. To verify the server has stopped you should see your command prompt cursor again. For most UNIX-like systems including Mac OS X this will be a dollar sign `$`. In development mode, Rails does not generally require you to restart the server; changes you make in files will be automatically picked up by the server. +TIP: To stop the web server, hit Ctrl+C in the terminal window where it's +running. To verify the server has stopped you should see your command prompt +cursor again. For most UNIX-like systems including Mac OS X this will be a +dollar sign `$`. In development mode, Rails does not generally require you to +restart the server; changes you make in files will be automatically picked up by +the server. -The "Welcome Aboard" page is the _smoke test_ for a new Rails application: it makes sure that you have your software configured correctly enough to serve a page. You can also click on the _About your application's environment_ link to see a summary of your application's environment. +The "Welcome Aboard" page is the _smoke test_ for a new Rails application: it +makes sure that you have your software configured correctly enough to serve a +page. You can also click on the _About your application's environment_ link to +see a summary of your application's environment. ### Say "Hello", Rails -To get Rails saying "Hello", you need to create at minimum a _controller_ and a _view_. +To get Rails saying "Hello", you need to create at minimum a _controller_ and a +_view_. -A controller's purpose is to receive specific requests for the application. _Routing_ decides which controller receives which requests. Often, there is more than one route to each controller, and different routes can be served by different _actions_. Each action's purpose is to collect information to provide it to a view. +A controller's purpose is to receive specific requests for the application. +_Routing_ decides which controller receives which requests. Often, there is more +than one route to each controller, and different routes can be served by +different _actions_. Each action's purpose is to collect information to provide +it to a view. -A view's purpose is to display this information in a human readable format. An important distinction to make is that it is the _controller_, not the view, where information is collected. The view should just display that information. By default, view templates are written in a language called ERB (Embedded Ruby) which is converted by the request cycle in Rails before being sent to the user. +A view's purpose is to display this information in a human readable format. An +important distinction to make is that it is the _controller_, not the view, +where information is collected. The view should just display that information. +By default, view templates are written in a language called ERB (Embedded Ruby) +which is converted by the request cycle in Rails before being sent to the user. -To create a new controller, you will need to run the "controller" generator and tell it you want a controller called "welcome" with an action called "index", just like this: +To create a new controller, you will need to run the "controller" generator and +tell it you want a controller called "welcome" with an action called "index", +just like this: ```bash $ rails generate controller welcome index @@ -206,9 +248,12 @@ invoke scss create app/assets/stylesheets/welcome.css.scss ``` -Most important of these are of course the controller, located at `app/controllers/welcome_controller.rb` and the view, located at `app/views/welcome/index.html.erb`. +Most important of these are of course the controller, located at `app/controllers/welcome_controller.rb` +and the view, located at `app/views/welcome/index.html.erb`. -Open the `app/views/welcome/index.html.erb` file in your text editor. Delete all of the existing code in the file, and replace it with the following single line of code: +Open the `app/views/welcome/index.html.erb` file in your text editor. Delete all +of the existing code in the file, and replace it with the following single line +of code: ```html <h1>Hello, Rails!</h1> @@ -216,7 +261,10 @@ Open the `app/views/welcome/index.html.erb` file in your text editor. Delete all ### Setting the Application Home Page -Now that we have made the controller and view, we need to tell Rails when we want Hello Rails! to show up. In our case, we want it to show up when we navigate to the root URL of our site, <http://localhost:3000>. At the moment, "Welcome Aboard" is occupying that spot. +Now that we have made the controller and view, we need to tell Rails when we +want `Hello, Rails!` to show up. In our case, we want it to show up when we +navigate to the root URL of our site, <http://localhost:3000>. At the moment, +"Welcome Aboard" is occupying that spot. Next, you have to tell Rails where your actual home page is located. @@ -233,27 +281,44 @@ Blog::Application.routes.draw do # root "welcome#index" ``` -This is your application's _routing file_ which holds entries in a special DSL (domain-specific language) that tells Rails how to connect incoming requests to controllers and actions. This file contains many sample routes on commented lines, and one of them actually shows you how to connect the root of your site to a specific controller and action. Find the line beginning with `root` and uncomment it. It should look something like the following: +This is your application's _routing file_ which holds entries in a special DSL +(domain-specific language) that tells Rails how to connect incoming requests to +controllers and actions. This file contains many sample routes on commented +lines, and one of them actually shows you how to connect the root of your site +to a specific controller and action. Find the line beginning with `root` and +uncomment it. It should look something like the following: ```ruby root "welcome#index" ``` -The `root "welcome#index"` tells Rails to map requests to the root of the application to the welcome controller's index action and `get "welcome/index"` tells Rails to map requests to <http://localhost:3000/welcome/index> to the welcome controller's index action. This was created earlier when you ran the controller generator (`rails generate controller welcome index`). +The `root "welcome#index"` tells Rails to map requests to the root of the +application to the welcome controller's index action and `get "welcome/index"` +tells Rails to map requests to <http://localhost:3000/welcome/index> to the +welcome controller's index action. This was created earlier when you ran the +controller generator (`rails generate controller welcome index`). -If you navigate to <http://localhost:3000> in your browser, you'll see the `Hello, Rails!` message you put into `app/views/welcome/index.html.erb`, indicating that this new route is indeed going to `WelcomeController`'s `index` action and is rendering the view correctly. +If you navigate to <http://localhost:3000> in your browser, you'll see the +`Hello, Rails!` message you put into `app/views/welcome/index.html.erb`, +indicating that this new route is indeed going to `WelcomeController`'s `index` +action and is rendering the view correctly. TIP: For more information about routing, refer to [Rails Routing from the Outside In](routing.html). Getting Up and Running ---------------------- -Now that you've seen how to create a controller, an action and a view, let's create something with a bit more substance. +Now that you've seen how to create a controller, an action and a view, let's +create something with a bit more substance. -In the Blog application, you will now create a new _resource_. A resource is the term used for a collection of similar objects, such as posts, people or animals. You can create, read, update and destroy items for a resource and these operations are referred to as _CRUD_ operations. +In the Blog application, you will now create a new _resource_. A resource is the +term used for a collection of similar objects, such as posts, people or animals. +You can create, read, update and destroy items for a resource and these +operations are referred to as _CRUD_ operations. -Rails provides a `resources` method which can be used to declare a standard REST resource. -Here's what `config/routes.rb` should look like after the _post resource_ is declared. +Rails provides a `resources` method which can be used to declare a standard REST +resource. Here's what `config/routes.rb` should look like after the _post resource_ +is declared. ```ruby Blog::Application.routes.draw do @@ -283,32 +348,45 @@ edit_post GET /posts/:id/edit(.:format) posts#edit root / welcome#index ``` -In the next section, you will add the ability to create new posts in your application and be able to view them. This is the "C" and the "R" from CRUD: creation and reading. The form for doing this will look like this: +In the next section, you will add the ability to create new posts in your +application and be able to view them. This is the "C" and the "R" from CRUD: +creation and reading. The form for doing this will look like this:  -It will look a little basic for now, but that's ok. We'll look at improving the styling for it afterwards. +It will look a little basic for now, but that's ok. We'll look at improving the +styling for it afterwards. ### Laying down the ground work -The first thing that you are going to need to create a new post within the application is a place to do that. A great place for that would be at `/posts/new`. With the route already defined, requests can now be made to `/posts/new` in the application. Navigate to <http://localhost:3000/posts/new> and you'll see a routing error: +The first thing that you are going to need to create a new post within the +application is a place to do that. A great place for that would be at `/posts/new`. +With the route already defined, requests can now be made to `/posts/new` in the +application. Navigate to <http://localhost:3000/posts/new> and you'll see a +routing error:  -This error occurs because the route needs to have a controller defined in order to serve the request. The solution to this particular problem is simple: create a controller called `PostsController`. You can do this by running this command: +This error occurs because the route needs to have a controller defined in order +to serve the request. The solution to this particular problem is simple: create +a controller called `PostsController`. You can do this by running this command: ```bash $ rails g controller posts ``` -If you open up the newly generated `app/controllers/posts_controller.rb` you'll see a fairly empty controller: +If you open up the newly generated `app/controllers/posts_controller.rb` you'll +see a fairly empty controller: ```ruby class PostsController < ApplicationController end ``` -A controller is simply a class that is defined to inherit from `ApplicationController`. It's inside this class that you'll define methods that will become the actions for this controller. These actions will perform CRUD operations on the posts within our system. +A controller is simply a class that is defined to inherit from `ApplicationController`. +It's inside this class that you'll define methods that will become the actions +for this controller. These actions will perform CRUD operations on the posts +within our system. NOTE: There are `public`, `private` and `protected` methods in `Ruby` (for more details you can check on [Programming Ruby](http://www.ruby-doc.org/docs/ProgrammingRuby/)). @@ -318,44 +396,77 @@ If you refresh <http://localhost:3000/posts/new> now, you'll get a new error:  -This error indicates that Rails cannot find the `new` action inside the `PostsController` that you just generated. This is because when controllers are generated in Rails they are empty by default, unless you tell it you wanted actions during the generation process. +This error indicates that Rails cannot find the `new` action inside the `PostsController` +that you just generated. This is because when controllers are generated in Rails +they are empty by default, unless you tell it you wanted actions during the +generation process. -To manually define an action inside a controller, all you need to do is to define a new method inside the controller. Open `app/controllers/posts_controller.rb` and inside the `PostsController` class, define a `new` method like this: +To manually define an action inside a controller, all you need to do is to +define a new method inside the controller. Open `app/controllers/posts_controller.rb` +and inside the `PostsController` class, define a `new` method like this: ```ruby def new end ``` -With the `new` method defined in `PostsController`, if you refresh <http://localhost:3000/posts/new> you'll see another error: +With the `new` method defined in `PostsController`, if you refresh <http://localhost:3000/posts/new> +you'll see another error:  -You're getting this error now because Rails expects plain actions like this one to have views associated with them to display their information. With no view available, Rails errors out. +You're getting this error now because Rails expects plain actions like this one +to have views associated with them to display their information. With no view +available, Rails errors out. -In the above image, the bottom line has been truncated. Let's see what the full thing looks like: +In the above image, the bottom line has been truncated. Let's see what the full +thing looks like: <blockquote> Missing template posts/new, application/new with {locale:[:en], formats:[:html], handlers:[:erb, :builder, :coffee]}. Searched in: * "/path/to/blog/app/views" </blockquote> -That's quite a lot of text! Let's quickly go through and understand what each part of it does. - -The first part identifies what template is missing. In this case, it's the `posts/new` template. Rails will first look for this template. If not found, then it will attempt to load a template called `application/new`. It looks for one here because the `PostsController` inherits from `ApplicationController`. - -The next part of the message contains a hash. The `:locale` key in this hash simply indicates what spoken language template should be retrieved. By default, this is the English - or "en" - template. The next key, `:formats` specifies the format of template to be served in response. The default format is `:html`, and so Rails is looking for an HTML template. The final key, `:handlers`, is telling us what _template handlers_ could be used to render our template. `:erb` is most commonly used for HTML templates, `:builder` is used for XML templates, and `:coffee` uses CoffeeScript to build JavaScript templates. - -The final part of this message tells us where Rails has looked for the templates. Templates within a basic Rails application like this are kept in a single location, but in more complex applications it could be many different paths. - -The simplest template that would work in this case would be one located at `app/views/posts/new.html.erb`. The extension of this file name is key: the first extension is the _format_ of the template, and the second extension is the _handler_ that will be used. Rails is attempting to find a template called `posts/new` within `app/views` for the application. The format for this template can only be `html` and the handler must be one of `erb`, `builder` or `coffee`. Because you want to create a new HTML form, you will be using the `ERB` language. Therefore the file should be called `posts/new.html.erb` and needs to be located inside the `app/views` directory of the application. - -Go ahead now and create a new file at `app/views/posts/new.html.erb` and write this content in it: +That's quite a lot of text! Let's quickly go through and understand what each +part of it does. + +The first part identifies what template is missing. In this case, it's the +`posts/new` template. Rails will first look for this template. If not found, +then it will attempt to load a template called `application/new`. It looks for +one here because the `PostsController` inherits from `ApplicationController`. + +The next part of the message contains a hash. The `:locale` key in this hash +simply indicates what spoken language template should be retrieved. By default, +this is the English - or "en" - template. The next key, `:formats` specifies the +format of template to be served in response. The default format is `:html`, and +so Rails is looking for an HTML template. The final key, `:handlers`, is telling +us what _template handlers_ could be used to render our template. `:erb` is most +commonly used for HTML templates, `:builder` is used for XML templates, and +`:coffee` uses CoffeeScript to build JavaScript templates. + +The final part of this message tells us where Rails has looked for the templates. +Templates within a basic Rails application like this are kept in a single +location, but in more complex applications it could be many different paths. + +The simplest template that would work in this case would be one located at +`app/views/posts/new.html.erb`. The extension of this file name is key: the +first extension is the _format_ of the template, and the second extension is the +_handler_ that will be used. Rails is attempting to find a template called +`posts/new` within `app/views` for the application. The format for this template +can only be `html` and the handler must be one of `erb`, `builder` or `coffee`. +Because you want to create a new HTML form, you will be using the `ERB` +language. Therefore the file should be called `posts/new.html.erb` and needs to +be located inside the `app/views` directory of the application. + +Go ahead now and create a new file at `app/views/posts/new.html.erb` and write +this content in it: ```html <h1>New Post</h1> ``` -When you refresh <http://localhost:3000/posts/new> you'll now see that the page has a title. The route, controller, action and view are now working harmoniously! It's time to create the form for a new post. +When you refresh <http://localhost:3000/posts/new> you'll now see that the page +has a title. The route, controller, action and view are now working +harmoniously! It's time to create the form for a new post. ### The first form @@ -381,14 +492,21 @@ method called `form_for`. To use this method, add this code into `app/views/post <% end %> ``` -If you refresh the page now, you'll see the exact same form as in the example. Building forms in Rails is really just that easy! +If you refresh the page now, you'll see the exact same form as in the example. +Building forms in Rails is really just that easy! When you call `form_for`, you pass it an identifying object for this form. In this case, it's the symbol `:post`. This tells the `form_for` helper what this form is for. Inside the block for this method, the -`FormBuilder` object - represented by `f` - is used to build two labels and two text fields, one each for the title and text of a post. Finally, a call to `submit` on the `f` object will create a submit button for the form. +`FormBuilder` object - represented by `f` - is used to build two labels and two +text fields, one each for the title and text of a post. Finally, a call to +`submit` on the `f` object will create a submit button for the form. -There's one problem with this form though. If you inspect the HTML that is generated, by viewing the source of the page, you will see that the `action` attribute for the form is pointing at `/posts/new`. This is a problem because this route goes to the very page that you're on right at the moment, and that route should only be used to display the form for a new post. +There's one problem with this form though. If you inspect the HTML that is +generated, by viewing the source of the page, you will see that the `action` +attribute for the form is pointing at `/posts/new`. This is a problem because +this route goes to the very page that you're on right at the moment, and that +route should only be used to display the form for a new post. The form needs to use a different URL in order to go somewhere else. This can be done quite simply with the `:url` option of `form_for`. @@ -404,6 +522,7 @@ Edit the `form_for` line inside `app/views/posts/new.html.erb` to look like this In this example, the `posts_path` helper is passed to the `:url` option. To see what Rails will do with this, we look back at the output of `rake routes`: + ```bash $ rake routes Prefix Verb URI Pattern Controller#Action @@ -417,21 +536,28 @@ edit_post GET /posts/:id/edit(.:format) posts#edit DELETE /posts/:id(.:format) posts#destroy root / welcome#index ``` + The `posts_path` helper tells Rails to point the form to the URI Pattern associated with the `posts` prefix; and the form will (by default) send a `POST` request to that route. This is associated with the `create` action of the current controller, the `PostsController`. -With the form and its associated route defined, you will be able to fill in the form and then click the submit button to begin the process of creating a new post, so go ahead and do that. When you submit the form, you should see a familiar error: +With the form and its associated route defined, you will be able to fill in the +form and then click the submit button to begin the process of creating a new +post, so go ahead and do that. When you submit the form, you should see a +familiar error:  -You now need to create the `create` action within the `PostsController` for this to work. +You now need to create the `create` action within the `PostsController` for this +to work. ### Creating posts -To make the "Unknown action" go away, you can define a `create` action within the `PostsController` class in `app/controllers/posts_controller.rb`, underneath the `new` action: +To make the "Unknown action" go away, you can define a `create` action within +the `PostsController` class in `app/controllers/posts_controller.rb`, underneath +the `new` action: ```ruby class PostsController < ApplicationController @@ -443,9 +569,14 @@ class PostsController < ApplicationController end ``` -If you re-submit the form now, you'll see another familiar error: a template is missing. That's ok, we can ignore that for now. What the `create` action should be doing is saving our new post to a database. +If you re-submit the form now, you'll see another familiar error: a template is +missing. That's ok, we can ignore that for now. What the `create` action should +be doing is saving our new post to a database. -When a form is submitted, the fields of the form are sent to Rails as _parameters_. These parameters can then be referenced inside the controller actions, typically to perform a particular task. To see what these parameters look like, change the `create` action to this: +When a form is submitted, the fields of the form are sent to Rails as +_parameters_. These parameters can then be referenced inside the controller +actions, typically to perform a particular task. To see what these parameters +look like, change the `create` action to this: ```ruby def create @@ -453,15 +584,23 @@ def create end ``` -The `render` method here is taking a very simple hash with a key of `text` and value of `params[:post].inspect`. The `params` method is the object which represents the parameters (or fields) coming in from the form. The `params` method returns an `ActiveSupport::HashWithIndifferentAccess` object, which allows you to access the keys of the hash using either strings or symbols. In this situation, the only parameters that matter are the ones from the form. +The `render` method here is taking a very simple hash with a key of `text` and +value of `params[:post].inspect`. The `params` method is the object which +represents the parameters (or fields) coming in from the form. The `params` +method returns an `ActiveSupport::HashWithIndifferentAccess` object, which +allows you to access the keys of the hash using either strings or symbols. In +this situation, the only parameters that matter are the ones from the form. -If you re-submit the form one more time you'll now no longer get the missing template error. Instead, you'll see something that looks like the following: +If you re-submit the form one more time you'll now no longer get the missing +template error. Instead, you'll see something that looks like the following: ```ruby {"title"=>"First post!", "text"=>"This is my first post."} ``` -This action is now displaying the parameters for the post that are coming in from the form. However, this isn't really all that helpful. Yes, you can see the parameters but nothing in particular is being done with them. +This action is now displaying the parameters for the post that are coming in +from the form. However, this isn't really all that helpful. Yes, you can see the +parameters but nothing in particular is being done with them. ### Creating the Post model @@ -550,7 +689,7 @@ invoking the command: `rake db:migrate RAILS_ENV=production`. ### Saving data in the controller -Back in `posts_controller`, we need to change the `create` action +Back in `PostsController`, we need to change the `create` action to use the new `Post` model to save the data in the database. Open `app/controllers/posts_controller.rb` and change the `create` action to look like this: @@ -603,8 +742,9 @@ private See the `permit`? It allows us to accept both `title` and `text` in this action. -TIP: Note that `def post_params` is private. This new approach prevents an attacker from -setting the model's attributes by manipulating the hash passed to the model. +TIP: Note that `def post_params` is private. This new approach prevents an +attacker from setting the model's attributes by manipulating the hash passed to +the model. For more information, refer to [this blog post about Strong Parameters](http://weblog.rubyonrails.org/2012/3/21/strong-parameters/). @@ -614,7 +754,8 @@ If you submit the form again now, Rails will complain about not finding the `show` action. That's not very useful though, so let's add the `show` action before proceeding. -As we have seen in the output of `rake routes`, the route for `show` action is as follows: +As we have seen in the output of `rake routes`, the route for `show` action is +as follows: ```ruby post GET /posts/:id(.:format) posts#show @@ -667,7 +808,7 @@ The route for this as per output of `rake routes` is: posts GET /posts(.:format) posts#index ``` -And an action for that route inside the `PostsController` in the `app/controllers/posts_controller.rb` file: +Add the corresponding `index` action for that route inside the `PostsController` in the `app/controllers/posts_controller.rb` file: ```ruby def index @@ -695,7 +836,8 @@ And then finally a view for this action, located at `app/views/posts/index.html. </table> ``` -Now if you go to `http://localhost:3000/posts` you will see a list of all the posts that you have created. +Now if you go to `http://localhost:3000/posts` you will see a list of all the +posts that you have created. ### Adding links @@ -706,20 +848,24 @@ Open `app/views/welcome/index.html.erb` and modify it as follows: ```html+erb <h1>Hello, Rails!</h1> -<%= link_to "My Blog", controller: "posts" %> +<%= link_to 'My Blog', controller: 'posts' %> ``` The `link_to` method is one of Rails' built-in view helpers. It creates a hyperlink based on text to display and where to go - in this case, to the path for posts. -Let's add links to the other views as well, starting with adding this "New Post" link to `app/views/posts/index.html.erb`, placing it above the `<table>` tag: +Let's add links to the other views as well, starting with adding this "New Post" +link to `app/views/posts/index.html.erb`, placing it above the `<table>` tag: ```erb <%= link_to 'New post', new_post_path %> ``` -This link will allow you to bring up the form that lets you create a new post. You should also add a link to this template - `app/views/posts/new.html.erb` - to go back to the `index` action. Do this by adding this underneath the form in this template: +This link will allow you to bring up the form that lets you create a new post. +You should also add a link to this template - `app/views/posts/new.html.erb` - +to go back to the `index` action. Do this by adding this underneath the form in +this template: ```erb <%= form_for :post do |f| %> @@ -729,7 +875,9 @@ This link will allow you to bring up the form that lets you create a new post. Y <%= link_to 'Back', posts_path %> ``` -Finally, add another link to the `app/views/posts/show.html.erb` template to go back to the `index` action as well, so that people who are viewing a single post can go back and view the whole list again: +Finally, add another link to the `app/views/posts/show.html.erb` template to go +back to the `index` action as well, so that people who are viewing a single post +can go back and view the whole list again: ```html+erb <p> @@ -815,8 +963,11 @@ private The `new` action is now creating a new instance variable called `@post`, and you'll see why that is in just a few moments. -Notice that inside the `create` action we use `render` instead of `redirect_to` when `save` -returns `false`. The `render` method is used so that the `@post` object is passed back to the `new` template when it is rendered. This rendering is done within the same request as the form submission, whereas the `redirect_to` will tell the browser to issue another request. +Notice that inside the `create` action we use `render` instead of `redirect_to` +when `save` returns `false`. The `render` method is used so that the `@post` +object is passed back to the `new` template when it is rendered. This rendering +is done within the same request as the form submission, whereas the `redirect_to` +will tell the browser to issue another request. If you reload <http://localhost:3000/posts/new> and @@ -861,9 +1012,10 @@ A few things are going on. We check if there are any errors with errors with `@post.errors.full_messages`. `pluralize` is a rails helper that takes a number and a string as its -arguments. If the number is greater than one, the string will be automatically pluralized. +arguments. If the number is greater than one, the string will be automatically +pluralized. -The reason why we added `@post = Post.new` in `posts_controller` is that +The reason why we added `@post = Post.new` in the `PostsController` is that otherwise `@post` would be `nil` in our view, and calling `@post.errors.any?` would throw an error. @@ -878,9 +1030,10 @@ attempt to do just that on the new post form [(http://localhost:3000/posts/new)] ### Updating Posts -We've covered the "CR" part of CRUD. Now let's focus on the "U" part, updating posts. +We've covered the "CR" part of CRUD. Now let's focus on the "U" part, updating +posts. -The first step we'll take is adding an `edit` action to `posts_controller`. +The first step we'll take is adding an `edit` action to the `PostsController`. ```ruby def edit @@ -981,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 %> @@ -1053,8 +1206,8 @@ which URI and method to use. For more information about this use of `form_for`, see [Resource-oriented style](//api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for-label-Resource-oriented+style). -Now, let's update the `app/views/posts/new.html.erb` view to use this new partial, rewriting it -completely: +Now, let's update the `app/views/posts/new.html.erb` view to use this new +partial, rewriting it completely: ```html+erb <h1>New post</h1> @@ -1093,8 +1246,8 @@ people to craft malicious URLs like this: ``` We use the `delete` method for destroying resources, and this route is mapped to -the `destroy` action inside `app/controllers/posts_controller.rb`, which doesn't exist yet, but is -provided below: +the `destroy` action inside `app/controllers/posts_controller.rb`, which doesn't +exist yet, but is provided below: ```ruby def destroy @@ -1135,13 +1288,14 @@ together. </table> ``` -Here we're using `link_to` in a different way. We pass the named route as the second argument, -and then the options as another argument. The `:method` and `:'data-confirm'` -options are used as HTML5 attributes so that when the link is clicked, -Rails will first show a confirm dialog to the user, and then submit the link with method `delete`. -This is done via the JavaScript file `jquery_ujs` which is automatically included -into your application's layout (`app/views/layouts/application.html.erb`) when you -generated the application. Without this file, the confirmation dialog box wouldn't appear. +Here we're using `link_to` in a different way. We pass the named route as the +second argument, and then the options as another argument. The `:method` and +`:'data-confirm'` options are used as HTML5 attributes so that when the link is +clicked, Rails will first show a confirm dialog to the user, and then submit the +link with method `delete`. This is done via the JavaScript file `jquery_ujs` +which is automatically included into your application's layout +(`app/views/layouts/application.html.erb`) when you generated the application. +Without this file, the confirmation dialog box wouldn't appear.  @@ -1156,8 +1310,8 @@ For more information about routing, see Adding a Second Model --------------------- -It's time to add a second model to the application. The second model will handle comments on -posts. +It's time to add a second model to the application. The second model will handle +comments on posts. ### Generating a Model @@ -1186,7 +1340,7 @@ class Comment < ActiveRecord::Base end ``` -This is very similar to the `post.rb` model that you saw earlier. The difference +This is very similar to the `Post` model that you saw earlier. The difference is the line `belongs_to :post`, which sets up an Active Record _association_. You'll learn a little about associations in the next section of this guide. @@ -1235,8 +1389,8 @@ this way: * One post can have many comments. In fact, this is very close to the syntax that Rails uses to declare this -association. You've already seen the line of code inside the `Comment` model (app/models/comment.rb) that -makes each comment belong to a Post: +association. You've already seen the line of code inside the `Comment` model +(app/models/comment.rb) that makes each comment belong to a Post: ```ruby class Comment < ActiveRecord::Base @@ -1336,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 @@ -1664,6 +1818,7 @@ class CommentsController < ApplicationController @post = Post.find(params[:post_id]) ... end + # snipped for brevity ``` @@ -1681,7 +1836,7 @@ along with a number of others. ### Other Security Considerations Security, especially in web applications, is a broad and detailed area. Security -in your Rails application is covered in more depth in +in your Rails application is covered in more depth in The [Ruby on Rails Security Guide](security.html) @@ -1698,12 +1853,19 @@ free to consult these support resources: * The [Ruby on Rails mailing list](http://groups.google.com/group/rubyonrails-talk) * The [#rubyonrails](irc://irc.freenode.net/#rubyonrails) channel on irc.freenode.net -Rails also comes with built-in help that you can generate using the rake command-line utility: +Rails also comes with built-in help that you can generate using the rake +command-line utility: -* Running `rake doc:guides` will put a full copy of the Rails Guides in the `doc/guides` folder of your application. Open `doc/guides/index.html` in your web browser to explore the Guides. -* Running `rake doc:rails` will put a full copy of the API documentation for Rails in the `doc/api` folder of your application. Open `doc/api/index.html` in your web browser to explore the API documentation. +* Running `rake doc:guides` will put a full copy of the Rails Guides in the + `doc/guides` folder of your application. Open `doc/guides/index.html` in your + web browser to explore the Guides. +* Running `rake doc:rails` will put a full copy of the API documentation for + Rails in the `doc/api` folder of your application. Open `doc/api/index.html` + in your web browser to explore the API documentation. -TIP: To be able to generate the Rails Guides locally with the `doc:guides` rake task you need to install the RedCloth gem. Add it to your `Gemfile` and run `bundle install` and you're ready to go. +TIP: To be able to generate the Rails Guides locally with the `doc:guides` rake +task you need to install the RedCloth gem. Add it to your `Gemfile` and run +`bundle install` and you're ready to go. Configuration Gotchas --------------------- diff --git a/guides/source/i18n.md b/guides/source/i18n.md index ead9c6b94d..401b1fb729 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -28,7 +28,7 @@ After reading this guide, you will know: -------------------------------------------------------------------------------- -NOTE: The Ruby I18n framework provides you with all necessary means for internationalization/localization of your Rails application. You may, however, use any of various plugins and extensions available, which add additional functionality or features. See the Rails [I18n Wiki](http://rails-i18n.org/wiki) for more information. +NOTE: The Ruby I18n framework provides you with all necessary means for internationalization/localization of your Rails application. You may, however, use any of various plugins and extensions available, which add additional functionality or features. See the Ruby [I18n Wiki](http://ruby-i18n.org/wiki) for more information. How I18n in Ruby on Rails Works ------------------------------- @@ -101,7 +101,7 @@ This means, that in the `:en` locale, the key _hello_ will map to the _Hello wor The I18n library will use **English** as a **default locale**, i.e. if you don't set a different locale, `:en` will be used for looking up translations. -NOTE: The i18n library takes a **pragmatic approach** to locale keys (after [some discussion](http://groups.google.com/group/rails-i18n/browse_thread/thread/14dede2c7dbe9470/80eec34395f64f3c?hl=en), including only the _locale_ ("language") part, like `:en`, `:pl`, not the _region_ part, like `:en-US` or `:en-GB`, which are traditionally used for separating "languages" and "regional setting" or "dialects". Many international applications use only the "language" element of a locale such as `:cs`, `:th` or `:es` (for Czech, Thai and Spanish). However, there are also regional differences within different language groups that may be important. For instance, in the `:en-US` locale you would have $ as a currency symbol, while in `:en-GB`, you would have £. Nothing stops you from separating regional and other settings in this way: you just have to provide full "English - United Kingdom" locale in a `:en-GB` dictionary. Various [Rails I18n plugins](http://rails-i18n.org/wiki) such as [Globalize3](https://github.com/svenfuchs/globalize3) may help you implement it. +NOTE: The i18n library takes a **pragmatic approach** to locale keys (after [some discussion](http://groups.google.com/group/rails-i18n/browse_thread/thread/14dede2c7dbe9470/80eec34395f64f3c?hl=en)), including only the _locale_ ("language") part, like `:en`, `:pl`, not the _region_ part, like `:en-US` or `:en-GB`, which are traditionally used for separating "languages" and "regional setting" or "dialects". Many international applications use only the "language" element of a locale such as `:cs`, `:th` or `:es` (for Czech, Thai and Spanish). However, there are also regional differences within different language groups that may be important. For instance, in the `:en-US` locale you would have $ as a currency symbol, while in `:en-GB`, you would have £. Nothing stops you from separating regional and other settings in this way: you just have to provide full "English - United Kingdom" locale in a `:en-GB` dictionary. Various [Rails I18n plugins](http://rails-i18n.org/wiki) such as [Globalize3](https://github.com/globalize/globalize) may help you implement it. The **translations load path** (`I18n.load_path`) is just a Ruby Array of paths to your translation files that will be loaded automatically and available in your application. You can pick whatever directory and translation file naming scheme makes sense for you. @@ -282,13 +282,14 @@ def set_locale I18n.locale = extract_locale_from_accept_language_header logger.debug "* Locale set to '#{I18n.locale}'" end + private -def extract_locale_from_accept_language_header - request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first -end + def extract_locale_from_accept_language_header + request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first + end ``` -Of course, in a production environment you would need much more robust code, and could use a plugin such as Iain Hecker's [http_accept_language](https://github.com/iain/http_accept_language/tree/master) or even Rack middleware such as Ryan Tomayko's [locale](https://github.com/rack/rack-contrib/blob/master/lib/rack/contrib/locale.rb). +Of course, in a production environment you would need much more robust code, and could use a plugin such as Iain Hecker's [http\_accept\_language](https://github.com/iain/http_accept_language/tree/master) or even Rack middleware such as Ryan Tomayko's [locale](https://github.com/rack/rack-contrib/blob/master/lib/rack/contrib/locale.rb). #### Using GeoIP (or Similar) Database @@ -315,6 +316,17 @@ end ``` ```ruby +# app/controllers/application_controller.rb +class ApplicationController < ActionController::Base + before_action :set_locale + + def set_locale + I18n.locale = params[:locale] || I18n.default_locale + end +end +``` + +```ruby # app/controllers/home_controller.rb class HomeController < ApplicationController def index @@ -480,12 +492,14 @@ Overview of the I18n API Features You should have good understanding of using the i18n library now, knowing all necessary aspects of internationalizing a basic Rails application. In the following chapters, we'll cover it's features in more depth. +These chapters will show examples using both the `I18n.translate` method as well as the [`translate` view helper method](http://api.rubyonrails.org/classes/ActionView/Helpers/TranslationHelper.html#method-i-translate) (noting the additional feature provide by the view helper method). + Covered are features like these: * looking up translations * interpolating data into translations * pluralizing translations -* using safe HTML translations +* using safe HTML translations (view helper method only) * localizing dates, numbers, currency, etc. ### Looking up Translations @@ -573,6 +587,8 @@ you can look up the `books.index.title` value **inside** `app/views/books/index. <%= t '.title' %> ``` +NOTE: Automatic translation scoping by partial is only available from the `translate` view helper method. + ### Interpolation In many cases you want to abstract your translations so that **variables can be interpolated into the translation**. For this reason the I18n API provides an interpolation feature. @@ -642,7 +658,7 @@ I18n.default_locale = :de ### Using Safe HTML Translations -Keys with a '_html' suffix and keys named 'html' are marked as HTML safe. Use them in views without escaping. +Keys with a '_html' suffix and keys named 'html' are marked as HTML safe. When you use them in views the HTML will not be escaped. ```yaml # config/locales/en.yml @@ -661,6 +677,8 @@ en: <div><%= t('title.html') %></div> ``` +NOTE: Automatic conversion to HTML safe translate text is only available from the `translate` view helper method. +  How to Store your Custom Translations @@ -742,7 +760,7 @@ en: other: Dudes ``` -Then `User.model_name.human(:count => 2)` will return "Dudes". With `:count => 1` or without params will return "Dude". +Then `User.model_name.human(count: 2)` will return "Dudes". With `count: 1` or without params will return "Dude". #### Error Message Scopes @@ -831,6 +849,7 @@ So, for example, instead of the default error message `"can not be blank"` you c | numericality | :equal_to | :equal_to | count | | numericality | :less_than | :less_than | count | | numericality | :less_than_or_equal_to | :less_than_or_equal_to | count | +| numericality | :only_integer | :not_an_integer | - | | numericality | :odd | :odd | - | | numericality | :even | :even | - | @@ -883,15 +902,15 @@ Rails uses fixed strings and other localizations, such as format strings and oth #### Action View Helper Methods -* `distance_of_time_in_words` translates and pluralizes its result and interpolates the number of seconds, minutes, hours, and so on. See [datetime.distance_in_words](https://github.com/rails/rails/blob/master/actionview/lib/action_view/locale/en.yml#L51) translations. +* `distance_of_time_in_words` translates and pluralizes its result and interpolates the number of seconds, minutes, hours, and so on. See [datetime.distance\_in\_words](https://github.com/rails/rails/blob/master/actionview/lib/action_view/locale/en.yml#L4) translations. -* `datetime_select` and `select_month` use translated month names for populating the resulting select tag. See [date.month_names](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L15) for translations. `datetime_select` also looks up the order option from [date.order](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L18) (unless you pass the option explicitly). All date selection helpers translate the prompt using the translations in the [datetime.prompts](https://github.com/rails/rails/blob/master/actionview/lib/action_view/locale/en.yml#L83) scope if applicable. +* `datetime_select` and `select_month` use translated month names for populating the resulting select tag. See [date.month_names](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L15) for translations. `datetime_select` also looks up the order option from [date.order](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L18) (unless you pass the option explicitly). All date selection helpers translate the prompt using the translations in the [datetime.prompts](https://github.com/rails/rails/blob/master/actionview/lib/action_view/locale/en.yml#L39) scope if applicable. -* The `number_to_currency`, `number_with_precision`, `number_to_percentage`, `number_with_delimiter`, and `number_to_human_size` helpers use the number format settings located in the [number](https://github.com/rails/rails/blob/master/actionview/lib/action_view/locale/en.yml#L2) scope. +* The `number_to_currency`, `number_with_precision`, `number_to_percentage`, `number_with_delimiter`, and `number_to_human_size` helpers use the number format settings located in the [number](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L37) scope. #### Active Model Methods -* `model_name.human` and `human_attribute_name` use translations for model names and attribute names if available in the [activerecord.models](https://github.com/rails/rails/blob/master/activerecord/lib/active_record/locale/en.yml#L29) scope. They also support translations for inherited class names (e.g. for use with STI) as explained above in "Error message scopes". +* `model_name.human` and `human_attribute_name` use translations for model names and attribute names if available in the [activerecord.models](https://github.com/rails/rails/blob/master/activerecord/lib/active_record/locale/en.yml#L36) scope. They also support translations for inherited class names (e.g. for use with STI) as explained above in "Error message scopes". * `ActiveModel::Errors#generate_message` (which is used by Active Model validations but may also be used manually) uses `model_name.human` and `human_attribute_name` (see above). It also translates the error message and supports translations for inherited class names as explained above in "Error message scopes". @@ -899,7 +918,7 @@ Rails uses fixed strings and other localizations, such as format strings and oth #### Active Support Methods -* `Array#to_sentence` uses format settings as given in the [support.array](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L30) scope. +* `Array#to_sentence` uses format settings as given in the [support.array](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L33) scope. Customize your I18n Setup ------------------------- @@ -1016,7 +1035,7 @@ If you found this guide useful, please consider recommending its authors on [wor Footnotes --------- -[^1]: Or, to quote [Wikipedia](http://en.wikipedia.org/wiki/Internationalization_and_localization:) _"Internationalization is the process of designing a software application so that it can be adapted to various languages and regions without engineering changes. Localization is the process of adapting software for a specific region or language by adding locale-specific components and translating text."_ +[^1]: Or, to quote [Wikipedia](http://en.wikipedia.org/wiki/Internationalization_and_localization): _"Internationalization is the process of designing a software application so that it can be adapted to various languages and regions without engineering changes. Localization is the process of adapting software for a specific region or language by adding locale-specific components and translating text."_ [^2]: Other backends might allow or require to use other formats, e.g. a GetText backend might allow to read GetText files. diff --git a/guides/source/initialization.md b/guides/source/initialization.md index c78eef5bf0..5e2e0ad3e3 100644 --- a/guides/source/initialization.md +++ b/guides/source/initialization.md @@ -29,9 +29,42 @@ quickly. Launch! ------- -Let's start to boot and initialize the app. It all begins with your app's -`bin/rails` executable. A Rails application is usually started by running -`rails console` or `rails server`. +Let's start to boot and initialize the app. A Rails application is usually +started by running `rails console` or `rails server`. + +### `railties/bin/rails` + +The `rails` in the command `rails server` is a ruby executable in your load +path. This executable contains the following lines: + +```ruby +version = ">= 0" +load Gem.bin_path('railties', 'rails', version) +``` + +If you try out this command in a Rails console, you would see that this loads +`railties/bin/rails`. A part of the file `railties/bin/rails.rb` has the +following code: + +```ruby +require "rails/cli" +``` + +The file `railties/lib/rails/cli` in turn calls +`Rails::AppRailsLoader.exec_app_rails`. + +### `railties/lib/rails/app_rails_loader.rb` + +The primary goal of the function `exec_app_rails` is to execute your app's +`bin/rails`. If the current directory does not have a `bin/rails`, it will +navigate upwards until it finds a `bin/rails` executable. Thus one can invoke a +`rails` command from anywhere inside a rails application. + +For `rails server` the equivalent of the following command is executed: + +```bash +$ exec ruby bin/rails server +``` ### `bin/rails` @@ -54,7 +87,7 @@ The `APP_PATH` constant will be used later in `rails/commands`. The `config/boot # 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']) ``` In a standard Rails application, there's a `Gemfile` which declares all @@ -93,7 +126,9 @@ A standard Rails application depends on several gems, specifically: ### `rails/commands.rb` -Once `config/boot.rb` has finished, the next file that is required is `rails/commands` which will execute a command based on the arguments passed in. In this case, the `ARGV` array simply contains `server` which is extracted into the `command` variable using these lines: +Once `config/boot.rb` has finished, the next file that is required is +`rails/commands`, which helps in expanding aliases. In the current case, the +`ARGV` array simply contains `server` which will be passed over: ```ruby ARGV << '--help' if ARGV.empty? @@ -109,31 +144,64 @@ aliases = { command = ARGV.shift command = aliases[command] || command + +require 'rails/commands/commands_tasks' + +Rails::CommandsTasks.new(ARGV).run_command!(command) ``` TIP: As you can see, an empty ARGV list will make Rails show the help snippet. -If we used `s` rather than `server`, Rails will use the `aliases` defined in the file and match them to their respective commands. With the `server` command, Rails will run this code: +If we had used `s` rather than `server`, Rails would have used the `aliases` +defined here to find the matching command. + +### `rails/commands/command_tasks.rb` + +When one types an incorrect rails command, the `run_command` is responsible for +throwing an error message. If the command is valid, a method of the same name +is called. ```ruby -when 'server' - # Change to the application's path if there is no config.ru file in current directory. - # This allows us to run `rails server` from other directories, but still get - # the main config.ru and properly set the tmp directory. - Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exists?(File.expand_path("config.ru")) +COMMAND_WHITELIST = %(plugin generate destroy console server dbconsole application runner new version help) + +def run_command!(command) + if COMMAND_WHITELIST.include?(command) + send(command) + else + write_error_message(command) + end +end +``` + +With the `server` command, Rails will further run the following code: + +```ruby +def set_application_directory! + Dir.chdir(File.expand_path('../../', APP_PATH)) unless + File.exist?(File.expand_path("config.ru")) +end + +def server + set_application_directory! + require_command!("server") - require 'rails/commands/server' Rails::Server.new.tap do |server| - # We need to require application after the server sets environment, - # otherwise the --environment option given to the server won't propagate. require APP_PATH Dir.chdir(Rails.application.root) server.start end +end + +def require_command!(command) + require "rails/commands/#{command}" +end ``` -This file will change into the Rails root directory (a path two directories up from `APP_PATH` which points at `config/application.rb`), but only if the `config.ru` file isn't found. This then requires `rails/commands/server` which sets up the `Rails::Server` class. +This file will change into the Rails root directory (a path two directories up +from `APP_PATH` which points at `config/application.rb`), but only if the +`config.ru` file isn't found. This then requires `rails/commands/server` which +sets up the `Rails::Server` class. ```ruby require 'fileutils' @@ -217,12 +285,12 @@ With the `default_options` set to this: ```ruby def default_options { - :environment => ENV['RACK_ENV'] || "development", - :pid => nil, - :Port => 9292, - :Host => "0.0.0.0", - :AccessLog => [], - :config => "config.ru" + environment: ENV['RACK_ENV'] || "development", + pid: nil, + Port: 9292, + Host: "0.0.0.0", + AccessLog: [], + config: "config.ru" } end ``` @@ -261,37 +329,43 @@ and it's free for you to change based on your needs. ### `Rails::Server#start` -After `config/application` is loaded, `server.start` is called. This method is defined like this: +After `config/application` is loaded, `server.start` is called. This method is +defined like this: ```ruby def start - url = "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}" - puts "=> Booting #{ActiveSupport::Inflector.demodulize(server)}" - puts "=> Rails #{Rails.version} application starting in #{Rails.env} on #{url}" - puts "=> Run `rails server -h` for more startup options" + print_boot_information trap(:INT) { exit } - puts "=> Ctrl-C to shutdown server" unless options[:daemonize] + create_tmp_directories + log_to_stdout if options[:log_stdout] + + super + ... +end - #Create required tmp directories if not found - %w(cache pids sessions sockets).each do |dir_to_make| - FileUtils.mkdir_p(Rails.root.join('tmp', dir_to_make)) +private + + def print_boot_information + ... + puts "=> Run `rails server -h` for more startup options" + puts "=> Ctrl-C to shutdown server" unless options[:daemonize] + end + + def create_tmp_directories + %w(cache pids sessions sockets).each do |dir_to_make| + FileUtils.mkdir_p(File.join(Rails.root, 'tmp', dir_to_make)) + end end - unless options[:daemonize] + def log_to_stdout wrapped_app # touch the app so the logger is set up console = ActiveSupport::Logger.new($stdout) console.formatter = Rails.logger.formatter + console.level = Rails.logger.level Rails.logger.extend(ActiveSupport::Logger.broadcast(console)) end - - super -ensure - # The '-h' option calls exit before @options is set. - # If we call 'options' with it unset, we get double help banners. - puts 'Exiting' unless @options && options[:daemonize] -end ``` This is where the first output of the Rails initialization happens. This @@ -468,14 +542,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/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md index b5d66d08ba..c6a3449ace 100644 --- a/guides/source/layouts_and_rendering.md +++ b/guides/source/layouts_and_rendering.md @@ -122,8 +122,7 @@ X-Runtime: 0.014297 Set-Cookie: _blog_session=...snip...; path=/; HttpOnly Cache-Control: no-cache - - $ +$ ``` We see there is an empty response (no data after the `Cache-Control` line), but the request was successful because Rails has set the response to 200 OK. You can set the `:status` option on render to change this response. Rendering nothing can be useful for Ajax requests where all you want to send back to the browser is an acknowledgment that the request was completed. @@ -137,7 +136,7 @@ If you want to render the view that corresponds to a different template within t ```ruby def update @book = Book.find(params[:id]) - if @book.update(params[:book]) + if @book.update(book_params) redirect_to(@book) else render "edit" @@ -152,7 +151,7 @@ If you prefer, you can use a symbol instead of a string to specify the action to ```ruby def update @book = Book.find(params[:id]) - if @book.update(params[:book]) + if @book.update(book_params) redirect_to(@book) else render :edit 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 414ce46a4e..71a177bca7 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 @@ -301,11 +301,12 @@ braces. You can use the following modifiers: * `precision` Defines the precision for the `decimal` fields * `scale` Defines the scale for the `decimal` fields * `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} +$ rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplier:references{polymorphic} ``` will produce a migration that looks like this @@ -313,7 +314,7 @@ will produce a migration that looks like this ```ruby class AddDetailsToProducts < ActiveRecord::Migration def change - add_column :products, :price, precision: 5, scale: 2 + add_column :products, :price, :decimal, precision: 5, scale: 2 add_reference :products, :supplier, polymorphic: true, index: true end end @@ -344,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| @@ -358,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 @@ -377,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 @@ -399,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| @@ -419,7 +420,7 @@ If the helpers provided by Active Record aren't enough you can use the `execute` method to execute arbitrary SQL: ```ruby -Products.connection.execute('UPDATE `products` SET `price`=`free` WHERE 1') +Product.connection.execute('UPDATE `products` SET `price`=`free` WHERE 1') ``` For more details and examples of individual methods, check the API documentation. @@ -463,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 @@ -647,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 @@ -664,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 @@ -682,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 @@ -703,16 +704,16 @@ The `rake db:reset` task will drop the database and set it up again. This is functionally equivalent to `rake db:drop db:setup`. NOTE: This is not the same as running all the migrations. It will only use the -contents of the current schema.rb file. If a migration can't be rolled back, -'rake db:reset' may not help you. To find out more about dumping the schema see -'[schema dumping and you](#schema-dumping-and-you).' +contents of the current `schema.rb` file. If a migration can't be rolled back, +`rake db:reset` may not help you. To find out more about dumping the schema see +[Schema Dumping and You](#schema-dumping-and-you) section. ### Running Specific Migrations 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 @@ -754,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 @@ -1039,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 b52504b104..d0aa2e55a2 100644 --- a/guides/source/plugins.md +++ b/guides/source/plugins.md @@ -15,7 +15,7 @@ After reading this guide, you will know: This guide describes how to build a test-driven plugin that will: * Extend core Ruby classes like Hash and String. -* Add methods to ActiveRecord::Base in the tradition of the 'acts_as' plugins. +* Add methods to `ActiveRecord::Base` in the tradition of the `acts_as` plugins. * Give you information about where to put generators in your plugin. For the purpose of this guide pretend for a moment that you are an avid bird watcher. @@ -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 @@ -126,8 +132,8 @@ $ rails console Add an "acts_as" Method to Active Record ---------------------------------------- -A common pattern in plugins is to add a method called 'acts_as_something' to models. In this case, you -want to write a method called 'acts_as_yaffle' that adds a 'squawk' method to your Active Record models. +A common pattern in plugins is to add a method called `acts_as_something` to models. In this case, you +want to write a method called `acts_as_yaffle` that adds a `squawk` method to your Active Record models. To begin, set up your files so that you have: @@ -162,9 +168,9 @@ end ### Add a Class Method -This plugin will expect that you've added a method to your model named 'last_squawk'. However, the -plugin users might have already defined a method on their model named 'last_squawk' that they use -for something else. This plugin will allow the name to be changed by adding a class method called 'yaffle_text_field'. +This plugin will expect that you've added a method to your model named `last_squawk`. However, the +plugin users might have already defined a method on their model named `last_squawk` that they use +for something else. This plugin will allow the name to be changed by adding a class method called `yaffle_text_field`. To start out, write a failing test that shows the behavior you'd like: diff --git a/guides/source/rails_on_rack.md b/guides/source/rails_on_rack.md index 642c70fd9d..b42c8fb81b 100644 --- a/guides/source/rails_on_rack.md +++ b/guides/source/rails_on_rack.md @@ -83,7 +83,7 @@ To use `rackup` instead of Rails' `rails server`, you can put the following insi # Rails.root/config.ru require ::File.expand_path('../config/environment', __FILE__) -use Rack::Debugger +use Rails::Rack::Debugger use Rack::ContentLength run Rails.application ``` @@ -182,18 +182,17 @@ You can swap an existing middleware in the middleware stack using `config.middle config.middleware.swap ActionDispatch::ShowExceptions, Lifo::ShowExceptions ``` -#### Middleware Stack is an Enumerable +#### Deleting a Middleware -The middleware stack behaves just like a normal `Enumerable`. You can use any `Enumerable` methods to manipulate or interrogate the stack. The middleware stack also implements some `Array` methods including `[]`, `unshift` and `delete`. Methods described in the section above are just convenience methods. - -Append following lines to your application configuration: +Add the following lines to your application configuration: ```ruby # config/application.rb config.middleware.delete "Rack::Lock" ``` -And now if you inspect the middleware stack, you'll find that `Rack::Lock` will not be part of it. +And now if you inspect the middleware stack, you'll find that `Rack::Lock` is +not a part of it. ```bash $ rake middleware @@ -225,116 +224,100 @@ 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: - **`ActionDispatch::Static`** +**`Rack::Sendfile`** + +* Sets server specific X-Sendfile header. Configure this via `config.action_dispatch.x_sendfile_header` option. -* Used to serve static assets. Disabled if `config.serve_static_assets` is true. +**`ActionDispatch::Static`** - **`Rack::Lock`** +* Used to serve static assets. Disabled if `config.serve_static_assets` is `false`. + +**`Rack::Lock`** * Sets `env["rack.multithread"]` flag to `false` and wraps the application within a Mutex. - **`ActiveSupport::Cache::Strategy::LocalCache::Middleware`** +**`ActiveSupport::Cache::Strategy::LocalCache::Middleware`** * Used for memory caching. This cache is not thread safe. - **`Rack::Runtime`** +**`Rack::Runtime`** * Sets an X-Runtime header, containing the time (in seconds) taken to execute the request. - **`Rack::MethodOverride`** +**`Rack::MethodOverride`** * Allows the method to be overridden if `params[:_method]` is set. This is the middleware which supports the PUT and DELETE HTTP method types. - **`ActionDispatch::RequestId`** +**`ActionDispatch::RequestId`** * Makes a unique `X-Request-Id` header available to the response and enables the `ActionDispatch::Request#uuid` method. - **`Rails::Rack::Logger`** +**`Rails::Rack::Logger`** * Notifies the logs that the request has began. After request is complete, flushes all the logs. - **`ActionDispatch::ShowExceptions`** +**`ActionDispatch::ShowExceptions`** * Rescues any exception returned by the application and calls an exceptions app that will wrap it in a format for the end user. - **`ActionDispatch::DebugExceptions`** +**`ActionDispatch::DebugExceptions`** * Responsible for logging exceptions and showing a debugging page in case the request is local. - **`ActionDispatch::RemoteIp`** +**`ActionDispatch::RemoteIp`** * Checks for IP spoofing attacks. - **`ActionDispatch::Reloader`** +**`ActionDispatch::Reloader`** * Provides prepare and cleanup callbacks, intended to assist with code reloading during development. - **`ActionDispatch::Callbacks`** +**`ActionDispatch::Callbacks`** * Runs the prepare callbacks before serving the request. - **`ActiveRecord::Migration::CheckPending`** +**`ActiveRecord::Migration::CheckPending`** * Checks pending migrations and raises `ActiveRecord::PendingMigrationError` if any migrations are pending. - **`ActiveRecord::ConnectionAdapters::ConnectionManagement`** +**`ActiveRecord::ConnectionAdapters::ConnectionManagement`** * Cleans active connections after each request, unless the `rack.test` key in the request environment is set to `true`. - **`ActiveRecord::QueryCache`** +**`ActiveRecord::QueryCache`** * Enables the Active Record query cache. - **`ActionDispatch::Cookies`** +**`ActionDispatch::Cookies`** * Sets cookies for the request. - **`ActionDispatch::Session::CookieStore`** +**`ActionDispatch::Session::CookieStore`** * Responsible for storing the session in cookies. - **`ActionDispatch::Flash`** +**`ActionDispatch::Flash`** * Sets up the flash keys. Only available if `config.action_controller.session_store` is set to a value. - **`ActionDispatch::ParamsParser`** +**`ActionDispatch::ParamsParser`** * Parses out parameters from the request into `params`. - **`ActionDispatch::Head`** +**`ActionDispatch::Head`** * Converts HEAD requests to `GET` requests and serves them as so. - **`Rack::ConditionalGet`** +**`Rack::ConditionalGet`** * Adds support for "Conditional `GET`" so that server responds with nothing if page wasn't changed. - **`Rack::ETag`** +**`Rack::ETag`** * Adds ETag header on all String bodies. ETags are used to validate cache. TIP: It's possible to use any of the above middlewares in your custom Rack stack. -### Using Rack Builder - -The following shows how to replace use `Rack::Builder` instead of the Rails supplied `MiddlewareStack`. - -<strong>Clear the existing Rails middleware stack</strong> - -```ruby -# config/application.rb -config.middleware.clear -``` - -<br> -<strong>Add a `config.ru` file to `Rails.root`</strong> - -```ruby -# config.ru -use MyOwnStackFromScratch -run Rails.application -``` - Resources --------- diff --git a/guides/source/routing.md b/guides/source/routing.md index 76c4c25108..019861c3d6 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)`). @@ -338,7 +338,7 @@ shallow do end ``` -There exists two options for `scope` to customize shallow routes. `:shallow_path` prefixes member paths with the specified parameter: +There exist two options for `scope` to customize shallow routes. `:shallow_path` prefixes member paths with the specified parameter: ```ruby scope shallow_path: "sekret" do @@ -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,19 +372,19 @@ 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 -Routing Concerns allows you to declare common routes that can be reused inside others resources and routes. To define a concern: +Routing Concerns allows you to declare common routes that can be reused inside other resources and routes. To define a concern: ```ruby concern :commentable do @@ -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. @@ -863,8 +866,8 @@ resources :user_permissions, controller: 'admin/user_permissions' This will route to the `Admin::UserPermissions` controller. NOTE: Only the directory notation is supported. Specifying the -controller with Ruby constant notation (eg. `:controller => -'Admin::UserPermissions'`) can lead to routing problems and results in +controller with Ruby constant notation (eg. `controller: 'Admin::UserPermissions'`) +can lead to routing problems and results in a warning. ### Specifying Constraints @@ -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/ruby_on_rails_guides_guidelines.md b/guides/source/ruby_on_rails_guides_guidelines.md index 5564b0648b..8faf03e58c 100644 --- a/guides/source/ruby_on_rails_guides_guidelines.md +++ b/guides/source/ruby_on_rails_guides_guidelines.md @@ -51,7 +51,7 @@ Use the same typography as in regular text: API Documentation Guidelines ---------------------------- -The guides and the API should be coherent and consistent where appropriate. Please have a look at these particular sections of the [API Documentation Guidelines](api_documentation_guidelines.html:) +The guides and the API should be coherent and consistent where appropriate. Please have a look at these particular sections of the [API Documentation Guidelines](api_documentation_guidelines.html): * [Wording](api_documentation_guidelines.html#wording) * [Example Code](api_documentation_guidelines.html#example-code) diff --git a/guides/source/security.md b/guides/source/security.md index 97b7355771..25428998f2 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -17,7 +17,7 @@ After reading this guide, you will know: Introduction ------------ -Web application frameworks are made to help developers building web applications. Some of them also help you with securing the web application. In fact one framework is not more secure than another: If you use it correctly, you will be able to build secure apps with many frameworks. Ruby on Rails has some clever helper methods, for example against SQL injection, so that this is hardly a problem. It's nice to see that all of the Rails applications I audited had a good level of security. +Web application frameworks are made to help developers build web applications. Some of them also help you with securing the web application. In fact one framework is not more secure than another: If you use it correctly, you will be able to build secure apps with many frameworks. Ruby on Rails has some clever helper methods, for example against SQL injection, so that this is hardly a problem. It's nice to see that all of the Rails applications I audited had a good level of security. In general there is no such thing as plug-n-play security. Security depends on the people using the framework, and sometimes on the development method. And it depends on all layers of a web application environment: The back-end storage, the web server and the web application itself (and possibly other layers or applications). @@ -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 @@ -481,7 +481,7 @@ A good password is a long alphanumeric combination of mixed cases. As this is qu INFO: _A common pitfall in Ruby's regular expressions is to match the string's beginning and end by ^ and $, instead of \A and \z._ -Ruby uses a slightly different approach than many other languages to match the end and the beginning of a string. That is why even many Ruby and Rails books make this wrong. So how is this a security threat? Say you wanted to loosely validate a URL field and you used a simple regular expression like this: +Ruby uses a slightly different approach than many other languages to match the end and the beginning of a string. That is why even many Ruby and Rails books get this wrong. So how is this a security threat? Say you wanted to loosely validate a URL field and you used a simple regular expression like this: ```ruby /^https?:\/\/[^\n]+$/i @@ -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 1b0a0abf9a..2fd0ed209d 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. @@ -438,10 +449,12 @@ Now that we have used Rails scaffold generator for our `Post` resource, it has a Let me take you through one such test, `test_should_get_index` from the file `posts_controller_test.rb`. ```ruby -test "should get index" do - get :index - assert_response :success - assert_not_nil assigns(:posts) +class PostsControllerTest < ActionController::TestCase + test "should get index" do + get :index + assert_response :success + assert_not_nil assigns(:posts) + end end ``` @@ -532,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 @@ -757,24 +770,24 @@ class UserFlowsTest < ActionDispatch::IntegrationTest private - module CustomDsl - def browses_site - get "/products/all" - assert_response :success - assert assigns(:products) + module CustomDsl + def browses_site + get "/products/all" + assert_response :success + assert assigns(:products) + end end - end - def login(user) - open_session do |sess| - sess.extend(CustomDsl) - u = users(user) - sess.https! - sess.post "/login", username: u.username, password: u.password - assert_equal '/welcome', path - sess.https!(false) + def login(user) + open_session do |sess| + sess.extend(CustomDsl) + u = users(user) + sess.https! + sess.post "/login", username: u.username, password: u.password + assert_equal '/welcome', path + sess.https!(false) + end end - end end ``` @@ -785,7 +798,7 @@ You don't need to set up and run your tests by hand on a test-by-test basis. Rai | Tasks | Description | | ----------------------- | ----------- | -| `rake test` | Runs all unit, functional and integration tests. You can also simply run `rake test` as Rails will run all the tests by default| +| `rake test` | Runs all unit, functional and integration tests. You can also simply run `rake` as Rails will run all the tests by default| | `rake test:controllers` | Runs all the controller tests from `test/controllers`| | `rake test:functionals` | Runs all the functional tests from `test/controllers`, `test/mailers`, and `test/functional`| | `rake test:helpers` | Runs all the helper tests from `test/helpers`| @@ -887,10 +900,9 @@ class PostsControllerTest < ActionController::TestCase private - def initialize_post - @post = posts(:one) - end - + def initialize_post + @post = posts(:one) + end end ``` diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 357918a73c..004d6bd466 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -170,6 +170,7 @@ this gem such as `whitelist_attributes` or `mass_assignment_sanitizer` options. ``` * Rails 4.0 has deprecated `ActiveRecord::Fixtures` in favor of `ActiveRecord::FixtureSet`. + * Rails 4.0 has deprecated `ActiveRecord::TestCase` in favor of `ActiveSupport::TestCase`. * Rails 4.0 has deprecated the old-style hash based finder API. This means that @@ -338,25 +339,26 @@ 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 ------------------------------------- If your application is currently on any version of Rails older than 3.1.x, you should upgrade to Rails 3.1 before attempting an update to Rails 3.2. -The following changes are meant for upgrading your application to Rails 3.2.12, the latest 3.2.x version of Rails. +The following changes are meant for upgrading your application to Rails 3.2.15, +the last 3.2.x version of Rails. ### Gemfile Make the following changes to your `Gemfile`. ```ruby -gem 'rails', '= 3.2.12' +gem 'rails', '3.2.15' group :assets do - gem 'sass-rails', '~> 3.2.3' - gem 'coffee-rails', '~> 3.2.1' + gem 'sass-rails', '~> 3.2.6' + gem 'coffee-rails', '~> 3.2.2' gem 'uglifier', '>= 1.0.3' end ``` @@ -392,21 +394,21 @@ Upgrading from Rails 3.0 to Rails 3.1 If your application is currently on any version of Rails older than 3.0.x, you should upgrade to Rails 3.0 before attempting an update to Rails 3.1. -The following changes are meant for upgrading your application to Rails 3.1.11, the latest 3.1.x version of Rails. +The following changes are meant for upgrading your application to Rails 3.1.12, the last 3.1.x version of Rails. ### Gemfile Make the following changes to your `Gemfile`. ```ruby -gem 'rails', '= 3.1.11' +gem 'rails', '3.1.12' gem 'mysql2' # Needed for the new asset pipeline group :assets do - gem 'sass-rails', "~> 3.1.5" - gem 'coffee-rails', "~> 3.1.1" - gem 'uglifier', ">= 1.0.3" + gem 'sass-rails', '~> 3.1.7' + gem 'coffee-rails', '~> 3.1.1' + gem 'uglifier', '>= 1.0.3' end # jQuery is the default JavaScript library in Rails 3.1 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 cc9722e59c..21ac596ab9 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,92 @@ +* Remove turbolinks when generating a new application based on a template that skips it. + + Example: + + Skips turbolinks adding `add_gem_entry_filter { |gem| gem.name != "turbolinks" }` + to the template. + + *Lauro Caetano* + +* Instrument an `load_config_initializer.railties` event on each load of configuration initializer + from `config/initializers`. Subscribers should be attached before `load_config_initializers` + initializer completed. + + Registering subscriber examples: + + # config/application.rb + module RailsApp + class Application < Rails::Application + ActiveSupport::Notifications.subscribe('load_config_initializer.railties') do |*args| + event = ActiveSupport::Notifications::Event.new(*args) + puts "Loaded initializer #{event.payload[:initializer]} (#{event.duration}ms)" + end + end + end + + # my_engine/lib/my_engine/engine.rb + module MyEngine + class Engine < ::Rails::Engine + config.before_initialize do + ActiveSupport::Notifications.subscribe('load_config_initializer.railties') do |*args| + event = ActiveSupport::Notifications::Event.new(*args) + puts "Loaded initializer #{event.payload[:initializer]} (#{event.duration}ms)" + end + end + end + end + + *Paul Nikitochkin* + +* Support for Pathnames in eager load paths. + + *Mike Pack* + +* Fixed missing line and shadow on service pages(404, 422, 500). + + *Dmitry Korotkov* + +* `BACKTRACE` environment variable to show unfiltered backtraces for + test failures. + + 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. + + *Richard Schneeman* + +* Don't require passing path to app before options in `rails new` + and `rails plugin new` + + *Piotr Sarnacki* + * rake notes now searches *.less files *Josh Crowder* @@ -5,21 +94,21 @@ * Generate nested route for namespaced controller generated using `rails g controller`. Fixes #11532. - + Example: - + rails g controller admin/dashboard index - + # Before: get "dashboard/index" - + # After: namespace :admin do get "dashboard/index" end - + *Prathamesh Sonpatki* - + * Fix the event name of action_dispatch requests. *Rafael Mendonça França* 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/api/task.rb b/railties/lib/rails/api/task.rb index 1f9568fb5f..3e32576040 100644 --- a/railties/lib/rails/api/task.rb +++ b/railties/lib/rails/api/task.rb @@ -16,8 +16,7 @@ module Rails :include => %w( README.rdoc lib/active_record/**/*.rb - ), - :exclude => 'lib/active_record/vendor/*' + ) }, 'activemodel' => { @@ -33,23 +32,22 @@ module Rails lib/abstract_controller/**/*.rb lib/action_controller/**/*.rb lib/action_dispatch/**/*.rb - ), - :exclude => 'lib/action_controller/vendor/*' + ) }, 'actionview' => { :include => %w( README.rdoc lib/action_view/**/*.rb - ) + ), + :exclude => 'lib/action_view/vendor/*' }, 'actionmailer' => { :include => %w( README.rdoc lib/action_mailer/**/*.rb - ), - :exclude => 'lib/action_mailer/vendor/*' + ) }, 'railties' => { diff --git a/railties/lib/rails/app_rails_loader.rb b/railties/lib/rails/app_rails_loader.rb index 4a17803f1c..1610751844 100644 --- a/railties/lib/rails/app_rails_loader.rb +++ b/railties/lib/rails/app_rails_loader.rb @@ -2,7 +2,7 @@ require 'pathname' module Rails module AppRailsLoader - RUBY = File.join(*RbConfig::CONFIG.values_at("bindir", "ruby_install_name")) + RbConfig::CONFIG["EXEEXT"] + RUBY = Gem.ruby EXECUTABLES = ['bin/rails', 'script/rails'] BUNDLER_WARNING = <<EOS Looks like your app's ./bin/rails is a stub that was generated by Bundler. @@ -49,13 +49,13 @@ EOS # call to generate a new application, so restore the original cwd. Dir.chdir(original_cwd) and return if Pathname.new(Dir.pwd).root? - # Otherwise keep moving upwards in search of a executable. + # Otherwise keep moving upwards in search of an executable. Dir.chdir('..') end 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 11524c4ef5..de60423784 100644 --- a/railties/lib/rails/commands/commands_tasks.rb +++ b/railties/lib/rails/commands/commands_tasks.rb @@ -51,6 +51,10 @@ EOT generate_or_destroy(:generate) end + def destroy + generate_or_destroy(:destroy) + end + def console require_command!("console") options = Rails::Console.parse_arguments(argv) @@ -135,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/runner.rb b/railties/lib/rails/commands/runner.rb index 2b77d5d387..dd2ee5639e 100644 --- a/railties/lib/rails/commands/runner.rb +++ b/railties/lib/rails/commands/runner.rb @@ -50,5 +50,5 @@ elsif File.exist?(code_or_file) $0 = code_or_file Kernel.load code_or_file else - eval(code_or_file) + eval(code_or_file, binding, __FILE__, __LINE__) end diff --git a/railties/lib/rails/commands/server.rb b/railties/lib/rails/commands/server.rb index 87d6505ed5..fec4962fb5 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 @@ -21,7 +22,7 @@ module Rails opts.on("-e", "--environment=name", String, "Specifies the environment to run this server under (test/development/production).", "Default: development") { |v| options[:environment] = v } - opts.on("-P","--pid=pid",String, + opts.on("-P", "--pid=pid", String, "Specifies the PID file.", "Default: tmp/pids/server.pid") { |v| options[:pid] = v } @@ -32,7 +33,8 @@ module Rails opt_parser.parse! args - options[:server] = args.shift + options[:log_stdout] = options[:daemonize].blank? && (options[:environment] || Rails.env) == "development" + options[:server] = args.shift options end end @@ -59,30 +61,10 @@ module Rails end def start - url = "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}" - puts "=> Booting #{ActiveSupport::Inflector.demodulize(server)}" - puts "=> Rails #{Rails.version} application starting in #{Rails.env} on #{url}" - puts "=> Run `rails server -h` for more startup options" - if options[:Host].to_s.match(/0\.0\.0\.0/) - puts "=> Notice: server is listening on all interfaces (#{options[:Host]}). Consider using 127.0.0.1 (--binding option)" - end + print_boot_information trap(:INT) { exit } - puts "=> Ctrl-C to shutdown server" unless options[:daemonize] - - #Create required tmp directories if not found - %w(cache pids sessions sockets).each do |dir_to_make| - FileUtils.mkdir_p(File.join(Rails.root, 'tmp', dir_to_make)) - end - - unless options[:daemonize] - wrapped_app # touch the app so the logger is set up - - console = ActiveSupport::Logger.new($stdout) - console.formatter = Rails.logger.formatter - console.level = Rails.logger.level - - Rails.logger.extend(ActiveSupport::Logger.broadcast(console)) - end + create_tmp_directories + log_to_stdout if options[:log_stdout] super ensure @@ -93,7 +75,7 @@ module Rails def middleware middlewares = [] - middlewares << [Rails::Rack::Debugger] if options[:debugger] + middlewares << [Rails::Rack::Debugger] if options[:debugger] middlewares << [::Rack::ContentLength] # FIXME: add Rack::Lock in the case people are using webrick. @@ -113,14 +95,45 @@ module Rails def default_options super.merge({ - Port: 3000, - DoNotReverseLookup: true, - environment: (ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development").dup, - daemonize: false, - debugger: false, - pid: File.expand_path("tmp/pids/server.pid"), - config: File.expand_path("config.ru") + Port: 3000, + DoNotReverseLookup: true, + environment: (ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development").dup, + daemonize: false, + debugger: false, + pid: File.expand_path("tmp/pids/server.pid"), + config: File.expand_path("config.ru") }) end + + private + + def print_boot_information + url = "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}" + puts "=> Booting #{ActiveSupport::Inflector.demodulize(server)}" + puts "=> Rails #{Rails.version} application starting in #{Rails.env} on #{url}" + puts "=> Run `rails server -h` for more startup options" + + if options[:Host].to_s.match(/0\.0\.0\.0/) + puts "=> Notice: server is listening on all interfaces (#{options[:Host]}). Consider using 127.0.0.1 (--binding option)" + end + + puts "=> Ctrl-C to shutdown server" unless options[:daemonize] + end + + def create_tmp_directories + %w(cache pids sessions sockets).each do |dir_to_make| + FileUtils.mkdir_p(File.join(Rails.root, 'tmp', dir_to_make)) + end + end + + def log_to_stdout + wrapped_app # touch the app so the logger is set up + + console = ActiveSupport::Logger.new($stdout) + console.formatter = Rails.logger.formatter + console.level = Rails.logger.level + + Rails.logger.extend(ActiveSupport::Logger.broadcast(console)) + end end 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/engine.rb b/railties/lib/rails/engine.rb index f25f629aa5..54a0f1c002 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -260,7 +260,7 @@ module Rails # # class FooController < ApplicationController # def index - # my_engine.root_url #=> /my_engine/ + # my_engine.root_url # => /my_engine/ # end # end # @@ -269,7 +269,7 @@ module Rails # module MyEngine # class BarController # def index - # main_app.foo_path #=> /foo + # main_app.foo_path # => /foo # end # end # end @@ -351,8 +351,13 @@ module Rails Rails::Railtie::Configuration.eager_load_namespaces << base base.called_from = begin - # Remove the line number from backtraces making sure we don't leave anything behind - call_stack = caller.map { |p| p.sub(/:\d+.*/, '') } + call_stack = if Kernel.respond_to?(:caller_locations) + caller_locations.map(&:path) + else + # Remove the line number from backtraces making sure we don't leave anything behind + caller.map { |p| p.sub(/:\d+.*/, '') } + end + File.dirname(call_stack.detect { |p| p !~ %r[railties[\w.-]*/lib/rails|rack[\w.-]*/lib/rack] }) end end @@ -460,7 +465,7 @@ module Rails # files inside eager_load paths. def eager_load! config.eager_load_paths.each do |load_path| - matcher = /\A#{Regexp.escape(load_path)}\/(.*)\.rb\Z/ + matcher = /\A#{Regexp.escape(load_path.to_s)}\/(.*)\.rb\Z/ Dir.glob("#{load_path}/**/*.rb").sort.each do |file| require_dependency file.sub(matcher, '\1') end @@ -606,7 +611,7 @@ module Rails initializer :load_config_initializers do config.paths["config/initializers"].existent.sort.each do |initializer| - load(initializer) + load_config_initializer(initializer) end end @@ -640,6 +645,12 @@ module Rails protected + def load_config_initializer(initializer) + ActiveSupport::Notifications.instrument('load_config_initializer.railties', initializer: initializer) do + load(initializer) + end + end + def run_tasks_blocks(*) #:nodoc: super paths["lib/tasks"].existent.sort.each { |ext| load(ext) } diff --git a/railties/lib/rails/engine/railties.rb b/railties/lib/rails/engine/railties.rb index 595256f36a..9969a1475d 100644 --- a/railties/lib/rails/engine/railties.rb +++ b/railties/lib/rails/engine/railties.rb @@ -16,8 +16,6 @@ module Rails def -(others) _all - others end - - delegate :engines, to: "self.class" end end end diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index 6b34db3e3f..dce734b54e 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,12 +11,11 @@ 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' autoload :ActiveModel, 'rails/generators/active_model' + autoload :Base, 'rails/generators/base' autoload :Migration, 'rails/generators/migration' autoload :NamedBase, 'rails/generators/named_base' autoload :ResourceHelpers, 'rails/generators/resource_helpers' diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index fb495c918c..2022b4ed3d 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -1,10 +1,10 @@ require 'digest/md5' -require 'securerandom' require 'active_support/core_ext/string/strip' require 'rails/version' unless defined?(Rails::VERSION) -require 'rbconfig' require 'open-uri' require 'uri' +require 'rails/generators' +require 'active_support/core_ext/array/extract_options' module Rails module Generators @@ -18,6 +18,10 @@ module Rails argument :app_path, type: :string + def self.strict_args_position + false + end + def self.add_shared_options_for(name) class_option :template, type: :string, aliases: '-m', desc: "Path to some #{name} template (can be a filesystem path or URL)" @@ -72,13 +76,48 @@ 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, + platform_dependent_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 @@ -92,21 +131,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 if target.respond_to?(:app_const, true) + @app_const_base = target.send :app_const_base if target.respond_to?(:app_const_base, true) + @app_name = target.send :app_name if target.respond_to?(:app_name, true) + @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] @@ -118,11 +217,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? @@ -133,22 +230,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 @@ -180,77 +294,75 @@ module Rails end def assets_gemfile_entry - 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' + return [] if options[:skip_sprockets] - # 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 + def platform_dependent_gemfile_entry + gems = [] + if RUBY_ENGINE == 'rbx' + gems << GemfileEntry.version('rubysl', nil) end + gems + 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..67bab96a22 100644 --- a/railties/lib/rails/generators/base.rb +++ b/railties/lib/rails/generators/base.rb @@ -7,8 +7,6 @@ rescue LoadError exit end -require 'rails/generators/actions' - module Rails module Generators class Error < Thor::Error # :nodoc: @@ -211,7 +209,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 @@ -298,7 +296,7 @@ module Rails end end - # Return the default value for the option name given doing a lookup in + # Returns the default value for the option name given doing a lookup in # Rails::Generators.options. def self.default_value_for_option(name, options) default_for_option(Rails::Generators.options, name, options, options[:default]) @@ -368,12 +366,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/named_base.rb b/railties/lib/rails/generators/named_base.rb index e712c747b0..5a92ab3e95 100644 --- a/railties/lib/rails/generators/named_base.rb +++ b/railties/lib/rails/generators/named_base.rb @@ -90,7 +90,7 @@ module Rails end def namespaced_path - @namespaced_path ||= namespace.name.split("::").map {|m| m.underscore }[0] + @namespaced_path ||= namespace.name.split("::").first.underscore end def class_name diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 041bfcb733..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 @@ -151,23 +153,19 @@ module Rails desc: "Show Rails version number and quit" def initialize(*args) - if args[0].blank? - if args[1].blank? - # rails new - raise Error, "Application name should be provided in arguments. For details run: rails --help" - else - # rails new --skip-bundle my_new_application - raise Error, "Options should be given after the application name. For details run: rails --help" - end - end - super + unless app_path + raise Error, "Application name should be provided in arguments. For details run: rails --help" + end + if !options[:skip_active_record] && !DATABASES.include?(options[:database]) raise Error, "Invalid value for --database option. Supported for preconfiguration are: #{DATABASES.join(", ")}." end end + public_task :set_default_accessors! + public_task :apply_rails_template public_task :create_root def create_root_files @@ -231,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 @@ -245,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 @@ -308,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(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(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 adc83353b4..68bd62d4b1 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -1,30 +1,35 @@ 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' - -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.0' +# gem 'bcrypt-ruby', '~> 3.1.2' # Use unicorn as the app server # gem 'unicorn' # Use Capistrano for deployment -# gem 'capistrano', group: :development +# gem 'capistrano-rails', group: :development <% unless defined?(JRUBY_VERSION) -%> # Use debugger # gem 'debugger', group: [:development, :test] <% end -%> + +<% if RUBY_PLATFORM.match(/bccwin|cygwin|emx|mingw|mswin|wince/) -%> +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem 'tzinfo-data', platforms: [:mingw, :mswin] +<% end -%> diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt b/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt index 8b91313e51..07ea09cdbd 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt +++ b/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt @@ -13,6 +13,8 @@ <% unless options[:skip_javascript] -%> //= require <%= options[:javascript] %> //= require <%= options[:javascript] %>_ujs +<% if gemfile_entries.any? { |m| m.name == "turbolinks" } -%> //= require turbolinks <% end -%> +<% end -%> //= require_tree . 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/databases/frontbase.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml index 4807986333..c6dfd50d40 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml @@ -6,26 +6,23 @@ # Configure Using Gemfile # gem 'ruby-frontbase' # -development: +default: &default adapter: frontbase host: localhost - database: <%= app_name %>_development username: <%= app_name %> password: '' +development: + <<: *default + database: <%= app_name %>_development + # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: - adapter: frontbase - host: localhost + <<: *default database: <%= app_name %>_test - username: <%= app_name %> - password: '' production: - adapter: frontbase - host: localhost + <<: *default database: <%= app_name %>_production - username: <%= app_name %> - password: '' diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml index 3d689a110a..fe53cd0ea2 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml @@ -33,12 +33,11 @@ # # For more details on the installation and the connection parameters below, # please refer to the latest documents at http://rubyforge.org/docman/?group_id=2361 - -development: +# +default: &default adapter: ibm_db username: db2inst1 password: - database: <%= app_name[0,4] %>_dev #schema: db2inst1 #host: localhost #port: 50000 @@ -51,36 +50,17 @@ development: #authentication: SERVER #parameterized: false +development: + <<: *default + database: <%= app_name[0,4] %>_dev + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. test: - adapter: ibm_db - username: db2inst1 - password: + <<: *default database: <%= app_name[0,4] %>_tst - #schema: db2inst1 - #host: localhost - #port: 50000 - #account: my_account - #app_user: my_app_user - #application: my_application - #workstation: my_workstation - #security: SSL - #timeout: 10 - #authentication: SERVER - #parameterized: false production: - adapter: ibm_db - username: db2inst1 - password: + <<: *default database: <%= app_name[0,8] %> - #schema: db2inst1 - #host: localhost - #port: 50000 - #account: my_account - #app_user: my_app_user - #application: my_application - #workstation: my_workstation - #security: SSL - #timeout: 10 - #authentication: SERVER - #parameterized: false
\ No newline at end of file diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml index 1d2bf08b91..be1dae332f 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml @@ -5,58 +5,54 @@ # Configure using Gemfile: # gem 'activerecord-jdbcmssql-adapter' # -#development: -# adapter: mssql -# username: <%= app_name %> -# password: -# host: localhost -# database: <%= app_name %>_development +# development: +# adapter: mssql +# username: <%= app_name %> +# password: +# host: localhost +# database: <%= app_name %>_development # # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. # -#test: -# adapter: mssql -# username: <%= app_name %> -# password: -# host: localhost -# database: <%= app_name %>_test +# test: +# adapter: mssql +# username: <%= app_name %> +# password: +# host: localhost +# database: <%= app_name %>_test # -#production: -# adapter: mssql -# username: <%= app_name %> -# password: -# host: localhost -# database: <%= app_name %>_production +# production: +# adapter: mssql +# username: <%= app_name %> +# password: +# host: localhost +# database: <%= app_name %>_production # If you are using oracle, db2, sybase, informix or prefer to use the plain # JDBC adapter, configure your database setting as the example below (requires # you to download and manually install the database vendor's JDBC driver .jar -# file). See your driver documentation for the apropriate driver class and +# file). See your driver documentation for the appropriate driver class and # connection string: -development: +default: &default adapter: jdbc username: <%= app_name %> password: driver: + +development: + <<: *default url: jdbc:db://localhost/<%= app_name %>_development # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. - test: - adapter: jdbc - username: <%= app_name %> - password: - driver: + <<: *default url: jdbc:db://localhost/<%= app_name %>_test production: - adapter: jdbc - username: <%= app_name %> - password: - driver: + <<: *default url: jdbc:db://localhost/<%= app_name %>_production diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml index 5a594ac1f3..26e2a5976c 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml @@ -8,26 +8,24 @@ # # And be sure to use new-style password hashing: # http://dev.mysql.com/doc/refman/5.0/en/old-client.html -development: +# +default: &default adapter: mysql - database: <%= app_name %>_development username: root password: host: localhost +development: + <<: *default + database: <%= app_name %>_development + # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: - adapter: mysql + <<: *default database: <%= app_name %>_test - username: root - password: - host: localhost production: - adapter: mysql + <<: *default database: <%= app_name %>_production - username: root - password: - host: localhost diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml index e1a00d076f..ccd44cf54b 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml @@ -2,14 +2,17 @@ # # Configure Using Gemfile # gem 'activerecord-jdbcpostgresql-adapter' - -development: +# +default: &default adapter: postgresql encoding: unicode - database: <%= app_name %>_development username: <%= app_name %> password: +development: + <<: *default + database: <%= app_name %>_development + # Connect on a TCP socket. Omitted by default since the client uses a # domain socket that doesn't need configuration. Windows does not have # domain sockets, so uncomment these lines. @@ -29,15 +32,9 @@ development: # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: - adapter: postgresql - encoding: unicode + <<: *default database: <%= app_name %>_test - username: <%= app_name %> - password: production: - adapter: postgresql - encoding: unicode + <<: *default database: <%= app_name %>_production - username: <%= app_name %> - password: diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml index 175f3eb3db..28c36eb82f 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml @@ -4,17 +4,20 @@ # Configure Using Gemfile # gem 'activerecord-jdbcsqlite3-adapter' # -development: +default: &default adapter: sqlite3 + +development: + <<: *default database: db/development.sqlite3 # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: - adapter: sqlite3 + <<: *default database: db/test.sqlite3 production: - adapter: sqlite3 + <<: *default database: db/production.sqlite3 diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml index c3349912aa..3fc7ce28a1 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml @@ -8,10 +8,10 @@ # # And be sure to use new-style password hashing: # http://dev.mysql.com/doc/refman/5.0/en/old-client.html -development: +# +default: &default adapter: mysql2 encoding: utf8 - database: <%= app_name %>_development pool: 5 username: root password: @@ -21,31 +21,17 @@ development: host: localhost <% end -%> +development: + <<: *default + database: <%= app_name %>_development + # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: - adapter: mysql2 - encoding: utf8 + <<: *default database: <%= app_name %>_test - pool: 5 - username: root - password: -<% if mysql_socket -%> - socket: <%= mysql_socket %> -<% else -%> - host: localhost -<% end -%> production: - adapter: mysql2 - encoding: utf8 + <<: *default database: <%= app_name %>_production - pool: 5 - username: root - password: -<% if mysql_socket -%> - socket: <%= mysql_socket %> -<% else -%> - host: localhost -<% end -%> diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml index b661a60389..d5b52c969b 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml @@ -16,24 +16,22 @@ # prefetch_rows: 100 # cursor_sharing: similar # - -development: +default: &default adapter: oracle - database: <%= app_name %>_development username: <%= app_name %> password: +development: + <<: *default + database: <%= app_name %>_development + # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: - adapter: oracle + <<: *default database: <%= app_name %>_test - username: <%= app_name %> - password: production: - adapter: oracle + <<: *default database: <%= app_name %>_production - username: <%= app_name %> - password: diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml index eb569b7dab..eaeb82bddd 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml @@ -14,14 +14,19 @@ # Configure Using Gemfile # gem 'pg' # -development: +default: &default adapter: postgresql encoding: unicode - database: <%= app_name %>_development + # For details on connection pooling, see rails configration guide + # http://guides.rubyonrails.org/configuring.html#database-pooling pool: 5 username: <%= app_name %> password: +development: + <<: *default + database: <%= app_name %>_development + # Connect on a TCP socket. Omitted by default since the client uses a # domain socket that doesn't need configuration. Windows does not have # domain sockets, so uncomment these lines. @@ -44,17 +49,9 @@ development: # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: - adapter: postgresql - encoding: unicode + <<: *default database: <%= app_name %>_test - pool: 5 - username: <%= app_name %> - password: production: - adapter: postgresql - encoding: unicode + <<: *default database: <%= app_name %>_production - pool: 5 - username: <%= app_name %> - password: diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml index 51a4dd459d..1c1a37ca8d 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml @@ -3,23 +3,23 @@ # # Ensure the SQLite 3 gem is defined in your Gemfile # gem 'sqlite3' -development: +# +default: &default adapter: sqlite3 - database: db/development.sqlite3 pool: 5 timeout: 5000 +development: + <<: *default + database: db/development.sqlite3 + # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: - adapter: sqlite3 + <<: *default database: db/test.sqlite3 - pool: 5 - timeout: 5000 production: - adapter: sqlite3 + <<: *default database: db/production.sqlite3 - pool: 5 - timeout: 5000 diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml index 7ef89d6608..4855f66c0d 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml @@ -21,37 +21,27 @@ # If you can connect with "tsql -S servername", your basic FreeTDS installation is working. # 'man tsql' for more info # Set timeout to a larger number if valid queries against a live db fail - -development: +# +default: &default adapter: sqlserver encoding: utf8 reconnect: false - database: <%= app_name %>_development username: <%= app_name %> password: timeout: 25 dataserver: from_freetds.conf +development: + <<: *default + database: <%= app_name %>_development # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: - adapter: sqlserver - encoding: utf8 - reconnect: false + <<: *default database: <%= app_name %>_test - username: <%= app_name %> - password: - timeout: 25 - dataserver: from_freetds.conf production: - adapter: sqlserver - encoding: utf8 - reconnect: false + <<: *default database: <%= app_name %>_production - username: <%= app_name %> - password: - timeout: 25 - dataserver: from_freetds.conf 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/app/templates/public/404.html b/railties/lib/rails/generators/rails/app/templates/public/404.html index a0daa0c156..b612547fc2 100644 --- a/railties/lib/rails/generators/rails/app/templates/public/404.html +++ b/railties/lib/rails/generators/rails/app/templates/public/404.html @@ -2,17 +2,23 @@ <html> <head> <title>The page you were looking for doesn't exist (404)</title> + <meta name="viewport" content="width=device-width,initial-scale=1"> <style> body { background-color: #EFEFEF; color: #2E2F30; text-align: center; font-family: arial, sans-serif; + margin: 0; } div.dialog { - width: 25em; - margin: 4em auto 0 auto; + width: 95%; + max-width: 33em; + margin: 4em auto 0; + } + + div.dialog > div { border: 1px solid #CCC; border-right-color: #999; border-left-color: #999; @@ -21,7 +27,8 @@ border-top-left-radius: 9px; border-top-right-radius: 9px; background-color: white; - padding: 7px 4em 0 4em; + padding: 7px 12% 0; + box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); } h1 { @@ -30,19 +37,19 @@ line-height: 1.5em; } - body > p { - width: 33em; - margin: 0 auto 1em; - padding: 1em 0; + div.dialog > p { + margin: 0 0 1em; + padding: 1em; background-color: #F7F7F7; border: 1px solid #CCC; border-right-color: #999; + border-left-color: #999; border-bottom-color: #999; border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; border-top-color: #DADADA; color: #666; - box-shadow:0 3px 8px rgba(50, 50, 50, 0.17); + box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); } </style> </head> @@ -50,9 +57,11 @@ <body> <!-- This file lives in public/404.html --> <div class="dialog"> - <h1>The page you were looking for doesn't exist.</h1> - <p>You may have mistyped the address or the page may have moved.</p> + <div> + <h1>The page you were looking for doesn't exist.</h1> + <p>You may have mistyped the address or the page may have moved.</p> + </div> + <p>If you are the application owner check the logs for more information.</p> </div> - <p>If you are the application owner check the logs for more information.</p> </body> </html> diff --git a/railties/lib/rails/generators/rails/app/templates/public/422.html b/railties/lib/rails/generators/rails/app/templates/public/422.html index fbb4b84d72..a21f82b3bd 100644 --- a/railties/lib/rails/generators/rails/app/templates/public/422.html +++ b/railties/lib/rails/generators/rails/app/templates/public/422.html @@ -2,17 +2,23 @@ <html> <head> <title>The change you wanted was rejected (422)</title> + <meta name="viewport" content="width=device-width,initial-scale=1"> <style> body { background-color: #EFEFEF; color: #2E2F30; text-align: center; font-family: arial, sans-serif; + margin: 0; } div.dialog { - width: 25em; - margin: 4em auto 0 auto; + width: 95%; + max-width: 33em; + margin: 4em auto 0; + } + + div.dialog > div { border: 1px solid #CCC; border-right-color: #999; border-left-color: #999; @@ -21,7 +27,8 @@ border-top-left-radius: 9px; border-top-right-radius: 9px; background-color: white; - padding: 7px 4em 0 4em; + padding: 7px 12% 0; + box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); } h1 { @@ -30,19 +37,19 @@ line-height: 1.5em; } - body > p { - width: 33em; - margin: 0 auto 1em; - padding: 1em 0; + div.dialog > p { + margin: 0 0 1em; + padding: 1em; background-color: #F7F7F7; border: 1px solid #CCC; border-right-color: #999; + border-left-color: #999; border-bottom-color: #999; border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; border-top-color: #DADADA; color: #666; - box-shadow:0 3px 8px rgba(50, 50, 50, 0.17); + box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); } </style> </head> @@ -50,9 +57,11 @@ <body> <!-- This file lives in public/422.html --> <div class="dialog"> - <h1>The change you wanted was rejected.</h1> - <p>Maybe you tried to change something you didn't have access to.</p> + <div> + <h1>The change you wanted was rejected.</h1> + <p>Maybe you tried to change something you didn't have access to.</p> + </div> + <p>If you are the application owner check the logs for more information.</p> </div> - <p>If you are the application owner check the logs for more information.</p> </body> </html> diff --git a/railties/lib/rails/generators/rails/app/templates/public/500.html b/railties/lib/rails/generators/rails/app/templates/public/500.html index e9052d35bf..061abc587d 100644 --- a/railties/lib/rails/generators/rails/app/templates/public/500.html +++ b/railties/lib/rails/generators/rails/app/templates/public/500.html @@ -2,17 +2,23 @@ <html> <head> <title>We're sorry, but something went wrong (500)</title> + <meta name="viewport" content="width=device-width,initial-scale=1"> <style> body { background-color: #EFEFEF; color: #2E2F30; text-align: center; font-family: arial, sans-serif; + margin: 0; } div.dialog { - width: 25em; - margin: 4em auto 0 auto; + width: 95%; + max-width: 33em; + margin: 4em auto 0; + } + + div.dialog > div { border: 1px solid #CCC; border-right-color: #999; border-left-color: #999; @@ -21,7 +27,8 @@ border-top-left-radius: 9px; border-top-right-radius: 9px; background-color: white; - padding: 7px 4em 0 4em; + padding: 7px 12% 0; + box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); } h1 { @@ -30,19 +37,19 @@ line-height: 1.5em; } - body > p { - width: 33em; - margin: 0 auto 1em; - padding: 1em 0; + div.dialog > p { + margin: 0 0 1em; + padding: 1em; background-color: #F7F7F7; border: 1px solid #CCC; border-right-color: #999; + border-left-color: #999; border-bottom-color: #999; border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; border-top-color: #DADADA; color: #666; - box-shadow:0 3px 8px rgba(50, 50, 50, 0.17); + box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); } </style> </head> @@ -50,8 +57,10 @@ <body> <!-- This file lives in public/500.html --> <div class="dialog"> - <h1>We're sorry, but something went wrong.</h1> + <div> + <h1>We're sorry, but something went wrong.</h1> + </div> + <p>If you are the application owner check the logs for more information.</p> </div> - <p>If you are the application owner check the logs for more information.</p> </body> </html> diff --git a/railties/lib/rails/generators/rails/controller/controller_generator.rb b/railties/lib/rails/generators/rails/controller/controller_generator.rb index 822f35fb42..ef84447df9 100644 --- a/railties/lib/rails/generators/rails/controller/controller_generator.rb +++ b/railties/lib/rails/generators/rails/controller/controller_generator.rb @@ -32,18 +32,18 @@ module Rails # namespace :foo do # namespace :bar do namespace_ladder = class_path.each_with_index.map do |ns, i| - %{#{" " * i * 2}namespace :#{ns} do\n } + indent("namespace :#{ns} do\n", i * 2) end.join # Create route # get "baz/index" - route = %{#{" " * depth * 2}get "#{file_name}/#{action}"\n} + route = indent(%{get "#{file_name}/#{action}"\n}, depth * 2) # Create `end` ladder # end # end end_ladder = (1..depth).reverse_each.map do |i| - "#{" " * i * 2}end\n" + indent("end\n", i * 2) end.join # Combine the 3 parts to generate complete route entry 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 13f5472ede..dbe1e37d8e 100644 --- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb +++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb @@ -176,12 +176,16 @@ task default: :test "skip adding entry to Gemfile" def initialize(*args) - raise Error, "Options should be given after the plugin name. For details run: rails plugin new --help" if args[0].blank? - @dummy_path = nil super + + unless plugin_path + raise Error, "Plugin name should be provided in arguments. For details run: rails plugin new --help" + end end + public_task :set_default_accessors! + public_task :apply_rails_template public_task :create_root def create_root_files @@ -238,7 +242,6 @@ task default: :test build(:leftovers) end - public_task :apply_rails_template, :run_bundle def name @name ||= begin @@ -252,6 +255,9 @@ task default: :test end end + public_task :run_bundle + public_task :replay_template + protected def app_templates_dir @@ -317,7 +323,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/info_controller.rb b/railties/lib/rails/info_controller.rb index fa5668a5b5..85ea11b4e7 100644 --- a/railties/lib/rails/info_controller.rb +++ b/railties/lib/rails/info_controller.rb @@ -13,10 +13,12 @@ class Rails::InfoController < ActionController::Base # :nodoc: def properties @info = Rails::Info.to_html + @page_title = 'Properties' end def routes @routes_inspector = ActionDispatch::Routing::RoutesInspector.new(_routes.routes) + @page_title = 'Routes' end protected 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/railtie/configuration.rb b/railties/lib/rails/railtie/configuration.rb index 0cbbf04da2..eb3b2d8ef4 100644 --- a/railties/lib/rails/railtie/configuration.rb +++ b/railties/lib/rails/railtie/configuration.rb @@ -80,7 +80,7 @@ module Rails to_prepare_blocks << blk if blk end - def respond_to?(name) + def respond_to?(name, include_private = false) super || @@options.key?(name.to_sym) end diff --git a/railties/lib/rails/source_annotation_extractor.rb b/railties/lib/rails/source_annotation_extractor.rb index b806b922b7..3cf6a005ea 100644 --- a/railties/lib/rails/source_annotation_extractor.rb +++ b/railties/lib/rails/source_annotation_extractor.rb @@ -67,7 +67,7 @@ class SourceAnnotationExtractor # Returns a hash that maps filenames under +dir+ (recursively) to arrays # with their annotations. Only files with annotations are included. Files # with extension +.builder+, +.rb+, +.erb+, +.haml+, +.slim+, +.css+, - # +.scss+, +.js+, +.coffee+, and +.rake+ + # +.scss+, +.js+, +.coffee+, +.rake+, +.sass+ and +.less+ # are taken into account. def find_in(dir) results = {} 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/templates/layouts/application.html.erb b/railties/lib/rails/templates/layouts/application.html.erb index 7352d48e7b..50a4755e45 100644 --- a/railties/lib/rails/templates/layouts/application.html.erb +++ b/railties/lib/rails/templates/layouts/application.html.erb @@ -2,7 +2,7 @@ <html lang="en"> <head> <meta charset="utf-8" /> - <title>Routes</title> + <title><%= @page_title %></title> <style> body { background-color: #fff; color: #333; } 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 4de8fcaa38..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 @@ -293,7 +293,7 @@ module ApplicationTests test "precompile should handle utf8 filenames" do filename = "レイルズ.png" - app_file "app/assets/images/#{filename}", "not a image really" + app_file "app/assets/images/#{filename}", "not an image really" add_to_config "config.assets.precompile = [ /\.png$/, /application.(css|js)$/ ]" precompile! @@ -305,7 +305,7 @@ module ApplicationTests require "#{app_path}/config/environment" get "/assets/#{URI.parser.escape(asset_path)}" - assert_match "not a image really", last_response.body + assert_match "not an image really", last_response.body assert_file_exists("#{app_path}/public/assets/#{asset_path}") end diff --git a/railties/test/application/basic_rendering_test.rb b/railties/test/application/basic_rendering_test.rb deleted file mode 100644 index 00ba433a05..0000000000 --- a/railties/test/application/basic_rendering_test.rb +++ /dev/null @@ -1,62 +0,0 @@ -require 'isolation/abstract_unit' -require 'rack/test' - -module ApplicationTests - class BasicRenderingTest < ActiveSupport::TestCase - include ActiveSupport::Testing::Isolation - include Rack::Test::Methods - - def setup - build_app - end - - def teardown - teardown_app - end - - test "Rendering without ActionView" do - gsub_app_file 'config/application.rb', "require 'rails/all'", <<-RUBY - require "active_model/railtie" - require "action_controller/railtie" - RUBY - - # Turn off ActionView and jquery-rails (it depends on AV) - $:.reject! {|path| path =~ /(actionview|jquery\-rails)/ } - boot_rails - - app_file 'app/controllers/pages_controller.rb', <<-RUBY - class PagesController < ApplicationController - def render_hello_world - render text: "Hello World!" - end - - def render_nothing - render nothing: true - end - - def no_render; end - - def raise_error - render foo: "bar" - end - end - RUBY - - get '/pages/render_hello_world' - assert_equal 200, last_response.status - assert_equal "Hello World!", last_response.body - assert_equal "text/plain; charset=utf-8", last_response.content_type - - get '/pages/render_nothing' - assert_equal 200, last_response.status - assert_equal " ", last_response.body - assert_equal "text/plain; charset=utf-8", last_response.content_type - - get '/pages/no_render' - assert_equal 500, last_response.status - - get '/pages/raise_error' - assert_equal 500, last_response.status - end - end -end diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index c51488e0e1..03a735b1c1 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -679,5 +679,12 @@ module ApplicationTests end assert_equal Logger::INFO, Rails.logger.level end + + test "respond_to? accepts include_private" do + make_basic_app + + assert_not Rails.configuration.respond_to?(:method_missing) + assert Rails.configuration.respond_to?(:method_missing, true) + end end end diff --git a/railties/test/application/initializers/load_path_test.rb b/railties/test/application/initializers/load_path_test.rb index b36628ee37..cd05956356 100644 --- a/railties/test/application/initializers/load_path_test.rb +++ b/railties/test/application/initializers/load_path_test.rb @@ -71,6 +71,20 @@ module ApplicationTests assert Zoo end + test "eager loading accepts Pathnames" do + app_file "lib/foo.rb", <<-RUBY + module Foo; end + RUBY + + add_to_config <<-RUBY + config.eager_load = true + config.eager_load_paths << Pathname.new("#{app_path}/lib") + RUBY + + require "#{app_path}/config/environment" + assert Foo + end + test "load environment with global" do $initialize_test_set_from_env = nil app_file "config/environments/development.rb", <<-RUBY diff --git a/railties/test/application/initializers/notifications_test.rb b/railties/test/application/initializers/notifications_test.rb index baae6fd928..95655b74cf 100644 --- a/railties/test/application/initializers/notifications_test.rb +++ b/railties/test/application/initializers/notifications_test.rb @@ -39,5 +39,18 @@ module ApplicationTests assert_equal 1, logger.logged(:debug).size assert_match(/SHOW tables/, logger.logged(:debug).last) end + + test 'rails load_config_initializer event is instrumented' do + app_file 'config/initializers/foo.rb', '' + + events = [] + callback = ->(*_) { events << _ } + ActiveSupport::Notifications.subscribed(callback, 'load_config_initializer.railties') do + app + end + + assert_equal %w[load_config_initializer.railties], events.map(&:first) + assert_includes events.first.last[:initializer], 'config/initializers/foo.rb' + end end end 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/multiple_applications_test.rb b/railties/test/application/multiple_applications_test.rb index 03c343c475..5bfea599e0 100644 --- a/railties/test/application/multiple_applications_test.rb +++ b/railties/test/application/multiple_applications_test.rb @@ -110,7 +110,7 @@ module ApplicationTests assert_equal 0, $run_count, "Without loading the initializers, the count should be 0" - # Set config.eager_load to false so that a eager_load warning doesn't pop up + # Set config.eager_load to false so that an eager_load warning doesn't pop up AppTemplate::Application.new { config.eager_load = false }.initialize! assert_equal 3, $run_count, "There should have been three initializers that incremented the count" 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/notes_test.rb b/railties/test/application/rake/notes_test.rb index 01d751e822..05f6338b68 100644 --- a/railties/test/application/rake/notes_test.rb +++ b/railties/test/application/rake/notes_test.rb @@ -28,6 +28,7 @@ module ApplicationTests app_file "app/assets/stylesheets/application.css.less", "// TODO: note in less" app_file "app/controllers/application_controller.rb", 1000.times.map { "" }.join("\n") << "# TODO: note in ruby" app_file "lib/tasks/task.rake", "# TODO: note in rake" + app_file 'app/views/home/index.html.builder', '# TODO: note in builder' boot_rails require 'rake' @@ -51,8 +52,9 @@ module ApplicationTests assert_match(/note in sass/, output) assert_match(/note in less/, output) assert_match(/note in rake/, output) + assert_match(/note in builder/, output) - assert_equal 11, lines.size + assert_equal 12, lines.size lines.each do |line| assert_equal 4, line[0].size diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index c1cb1c1eba..08316d80e6 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -111,7 +111,7 @@ module ApplicationTests RUBY output = Dir.chdir(app_path){ `rake routes` } - assert_equal "Prefix Verb URI Pattern Controller#Action\ncart GET /cart(.:format) cart#show\n", output + assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output end def test_rake_routes_with_controller_environment @@ -124,7 +124,7 @@ module ApplicationTests ENV['CONTROLLER'] = 'cart' output = Dir.chdir(app_path){ `rake routes` } - assert_equal "Prefix Verb URI Pattern Controller#Action\ncart GET /cart(.:format) cart#show\n", output + assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output end def test_rake_routes_displays_message_when_no_routes_are_defined @@ -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 cb57b3c0cd..ba688f1e9e 100644 --- a/railties/test/commands/server_test.rb +++ b/railties/test/commands/server_test.rb @@ -27,16 +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 + 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", "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..6f03cf3083 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 @@ -28,6 +29,7 @@ DEFAULT_APP_FILES = %w( lib/tasks lib/assets log + test/test_helper.rb test/fixtures test/controllers test/models @@ -36,6 +38,8 @@ DEFAULT_APP_FILES = %w( test/integration vendor vendor/assets + vendor/assets/stylesheets + vendor/assets/javascripts tmp/cache tmp/cache/assets ) @@ -57,6 +61,7 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file("app/views/layouts/application.html.erb", /stylesheet_link_tag\s+"application", media: "all", "data-turbolinks-track" => true/) assert_file("app/views/layouts/application.html.erb", /javascript_include_tag\s+"application", "data-turbolinks-track" => true/) assert_file("app/assets/stylesheets/application.css") + assert_file("app/assets/javascripts/application.js") end def test_invalid_application_name_raises_an_error @@ -155,6 +160,58 @@ 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_skip_turbolinks_when_it_is_not_on_gemfile + 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 "app/views/layouts/application.html.erb" do |contents| + assert_no_match 'turbolinks', contents + end + + assert_file "app/views/layouts/application.html.erb" do |contents| + assert_no_match('data-turbolinks-track', contents) + end + + assert_file "app/assets/javascripts/application.js" 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,28 +311,15 @@ 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 - def test_creation_of_a_test_directory - run_generator - assert_file 'test' - end - - def test_creation_of_app_assets_images_directory - run_generator - assert_file "app/assets/images" - end - - def test_creation_of_vendor_assets_javascripts_directory - run_generator - assert_file "vendor/assets/javascripts" - end - - def test_creation_of_vendor_assets_stylesheets_directory - run_generator - assert_file "vendor/assets/stylesheets" + def test_inclusion_of_plateform_dependent_gems + run_generator([destination_root]) + if RUBY_ENGINE == 'rbx' + assert_gem 'rubysl' + end end def test_jquery_is_the_default_javascript_library @@ -284,7 +328,7 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_match %r{^//= require jquery}, contents assert_match %r{^//= require jquery_ujs}, contents end - assert_file "Gemfile", /^gem 'jquery-rails'/ + assert_gem "jquery-rails" end def test_other_javascript_libraries @@ -298,18 +342,26 @@ 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) + assert_no_match(/jquery-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 +369,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 +419,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/controller_generator_test.rb b/railties/test/generators/controller_generator_test.rb index 9c664a903a..2268f04839 100644 --- a/railties/test/generators/controller_generator_test.rb +++ b/railties/test/generators/controller_generator_test.rb @@ -43,6 +43,12 @@ class ControllerGeneratorTest < Rails::Generators::TestCase assert_file "app/assets/stylesheets/account.css" end + def test_does_not_invoke_assets_if_required + run_generator ["account", "--skip-assets"] + assert_no_file "app/assets/javascripts/account.js" + assert_no_file "app/assets/stylesheets/account.css" + end + def test_invokes_default_test_framework run_generator assert_file "test/controllers/account_controller_test.rb" 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/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index 068eb66bc6..c6b91e7cba 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -293,7 +293,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "Gemfile" do |contents| assert_no_match('gemspec', contents) assert_match(/gem "rails", "~> #{Rails.version}"/, contents) - assert_match(/group :development do\n gem "sqlite3"\nend/, contents) + assert_match_sqlite3(contents) assert_no_match(/# gem "jquery-rails"/, contents) end end @@ -304,7 +304,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "Gemfile" do |contents| assert_no_match('gemspec', contents) assert_match(/gem "rails", "~> #{Rails.version}"/, contents) - assert_match(/group :development do\n gem "sqlite3"\nend/, contents) + assert_match_sqlite3(contents) end end @@ -347,4 +347,12 @@ protected def default_files ::DEFAULT_PLUGIN_FILES end + + def assert_match_sqlite3(contents) + unless defined?(JRUBY_VERSION) + assert_match(/group :development do\n gem "sqlite3"\nend/, contents) + else + assert_match(/group :development do\n gem "activerecord-jdbcsqlite3-adapter"\nend/, contents) + end + end end 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/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb index 369a0ee46c..7184639d23 100644 --- a/railties/test/generators/shared_generator_tests.rb +++ b/railties/test/generators/shared_generator_tests.rb @@ -46,11 +46,6 @@ module SharedGeneratorTests assert_no_file "test" end - def test_options_before_application_name_raises_an_error - content = capture(:stderr){ run_generator(["--pretend", destination_root]) } - assert_match(/Options should be given after the \w+ name. For details run: rails( plugin new)? --help\n/, content) - end - def test_name_collision_raises_an_error reserved_words = %w[application destroy plugin runner test] reserved_words.each do |reserved| diff --git a/railties/test/generators/task_generator_test.rb b/railties/test/generators/task_generator_test.rb index 9399be9510..d5bd44b9db 100644 --- a/railties/test/generators/task_generator_test.rb +++ b/railties/test/generators/task_generator_test.rb @@ -7,6 +7,18 @@ class TaskGeneratorTest < Rails::Generators::TestCase def test_task_is_created run_generator - assert_file "lib/tasks/feeds.rake", /namespace :feeds/ + assert_file "lib/tasks/feeds.rake" do |content| + assert_match(/namespace :feeds/, content) + assert_match(/task foo:/, content) + assert_match(/task bar:/, content) + end + end + + def test_task_on_revoke + task_path = 'lib/tasks/feeds.rake' + run_generator + assert_file task_path + run_generator ['feeds'], behavior: :revoke + assert_no_file task_path 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 12f18b9dbf..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 @@ -180,7 +180,7 @@ class PathsTest < ActiveSupport::TestCase assert_equal 1, @root.eager_load.select {|p| p == @root["app"].expanded.first }.size end - test "paths added to a eager_load path should be added to the eager_load collection" do + test "paths added to an eager_load path should be added to the eager_load collection" do @root["app"] = "/app" @root["app"].eager_load! @root["app"] << "/app2" 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 |