diff options
925 files changed, 20248 insertions, 10549 deletions
diff --git a/.travis.yml b/.travis.yml index 9e7a449010..43b08044d3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,20 +5,25 @@ before_install: rvm: - 1.9.3 - 2.0.0 - - 2.1.1 + - 2.1 + - ruby-head - rbx-2 - jruby env: - "GEM=railties" - - "GEM=ap,am,amo,as,av" + - "GEM=ap" + - "GEM=am,amo,as,av" - "GEM=ar:mysql" - "GEM=ar:mysql2" - "GEM=ar:sqlite3" - "GEM=ar:postgresql" matrix: allow_failures: + - rvm: 1.9.3 + env: "GEM=ar:mysql" - rvm: rbx-2 - rvm: jruby + - rvm: ruby-head fast_finish: true notifications: email: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 19b7b638b6..9ba2e53ef2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,5 +12,6 @@ Ruby on Rails is a volunteer effort. We encourage you to pitch in. [Join the tea * If you have a change or new feature in mind, please [suggest it on the rubyonrails-core mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core) and start writing code. -Thanks! :heart: :heart: :heart: <br /> +Thanks! :heart: :heart: :heart: + Rails Team @@ -7,12 +7,14 @@ gemspec # ensure correct loading order gem 'mocha', '~> 0.14', require: false +gem 'rack', github: 'rack/rack' gem 'rack-cache', '~> 1.2' gem 'jquery-rails', '~> 3.1.0' gem 'turbolinks' gem 'coffee-rails', '~> 4.0.0' -gem 'arel', github: 'rails/arel' -gem 'sprockets-rails', github: 'rails/sprockets-rails', branch: '2-1-stable' +gem 'arel', github: 'rails/arel', branch: 'master' +gem 'sprockets-rails', github: 'rails/sprockets-rails', branch: 'master' +gem 'i18n', github: 'svenfuchs/i18n', branch: 'master' # require: false so bcrypt is loaded only when has_secure_password is used. # This is to avoid ActiveModel (and by extension the entire framework) @@ -25,7 +27,7 @@ gem 'uglifier', '>= 1.3.0', require: false group :doc do gem 'sdoc', '~> 0.4.0' - gem 'redcarpet', '~> 2.2.2', platforms: :ruby + gem 'redcarpet', '~> 3.1.2', platforms: :ruby gem 'w3c_validators' gem 'kindlerb' end @@ -38,6 +40,9 @@ local_gemfile = File.dirname(__FILE__) + "/.Gemfile" instance_eval File.read local_gemfile if File.exist? local_gemfile group :test do + # FIX: Our test suite isn't ready to run in random order yet + gem 'minitest', '< 5.3.4' + platforms :mri_19 do gem 'ruby-prof', '~> 0.11.2' end @@ -82,10 +87,16 @@ platforms :jruby do end end +platforms :rbx do + # The rubysl-yaml gem doesn't ship with Psych by default + # as it needs libyaml that isn't always available. + gem 'psych', '~> 2.0' +end + # gems that are necessary for ActiveRecord tests with Oracle database if ENV['ORACLE_ENHANCED'] platforms :ruby do - gem 'ruby-oci8', '>= 2.0.4' + gem 'ruby-oci8', '~> 2.1' end gem 'activerecord-oracle_enhanced-adapter', github: 'rsim/oracle-enhanced', branch: 'master' end @@ -2,7 +2,7 @@ Rails is a web-application framework that includes everything needed to create database-backed web applications according to the -[Model-View-Controller (MVC)](http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) +[Model-View-Controller (MVC)](http://en.wikipedia.org/wiki/Model-view-controller) pattern. Understanding the MVC pattern is key to understanding Rails. MVC divides your diff --git a/RELEASING_RAILS.rdoc b/RELEASING_RAILS.rdoc index c6c1c12e87..f8c40f0b02 100644 --- a/RELEASING_RAILS.rdoc +++ b/RELEASING_RAILS.rdoc @@ -25,6 +25,18 @@ for Rails. You can check the status of his tests here: Do not release with Red AWDwR tests. +=== Are the supported plugins working? If not, make it work. + +Some Rails plugins are important and need to be supported until Rails 5. +As these plugins are outside the Rails repository it is easy to break then without knowing +after some refactoring or bug fix, so it is important to check if the following plugins +are working with the versions that will be released: + +* https://github.com/rails/protected_attributes +* https://github.com/rails/activerecord-deprecated_finders + +Do not release red plugins tests. + === Do we have any Git dependencies? If so, contact those authors. Having Git dependencies indicates that we depend on unreleased code. diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index 6203699405..4e3a9daf7d 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -1 +1,17 @@ +* Add `config.action_mailer.show_previews` configuration option. + + This config option can be used to enable the mail preview in environments + other than development (such as staging). + + Defaults to `true` in development and false elsewhere. + + *Leonard Garvey* + +* Allow preview interceptors to be registered through + `config.action_mailer.preview_interceptors`. + + See #15739. + + *Yves Senn* + Please check [4-1-stable](https://github.com/rails/rails/blob/4-1-stable/actionmailer/CHANGELOG.md) for previous changes. diff --git a/actionmailer/README.rdoc b/actionmailer/README.rdoc index 67b64fe469..ceca912ada 100644 --- a/actionmailer/README.rdoc +++ b/actionmailer/README.rdoc @@ -156,7 +156,11 @@ API documentation is at * http://api.rubyonrails.org -Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here: +Bug reports can be filed for the Ruby on Rails project here: * https://github.com/rails/rails/issues +Feature requests should be discussed on the rails-core mailing list here: + +* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core + diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec index 9b25feaf75..01d97b7213 100644 --- a/actionmailer/actionmailer.gemspec +++ b/actionmailer/actionmailer.gemspec @@ -22,5 +22,5 @@ Gem::Specification.new do |s| s.add_dependency 'actionpack', version s.add_dependency 'actionview', version - s.add_dependency 'mail', '~> 2.5.4' + s.add_dependency 'mail', ['~> 2.5', '>= 2.5.4'] end diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 652ce0b3d8..048f2ddc35 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -301,12 +301,13 @@ module ActionMailer # end # end # - # Callbacks in ActionMailer are implemented using AbstractController::Callbacks, so you - # can define and configure callbacks in the same manner that you would use callbacks in - # classes that inherit from ActionController::Base. + # Callbacks in Action Mailer are implemented using + # <tt>AbstractController::Callbacks</tt>, so you can define and configure + # callbacks in the same manner that you would use callbacks in classes that + # inherit from <tt>ActionController::Base</tt>. # # Note that unless you have a specific reason to do so, you should prefer using before_action - # rather than after_action in your ActionMailer classes so that headers are parsed properly. + # rather than after_action in your Action Mailer classes so that headers are parsed properly. # # = Previewing emails # @@ -339,7 +340,7 @@ module ActionMailer # end # end # - # config.action_mailer.register_preview_interceptor :css_inline_styler + # config.action_mailer.preview_interceptors :css_inline_styler # # Note that interceptors need to be registered both with <tt>register_interceptor</tt> # and <tt>register_preview_interceptor</tt> if they should operate on both sending and @@ -368,8 +369,8 @@ module ActionMailer # This is a symbol and one of <tt>:plain</tt> (will send the password in the clear), <tt>:login</tt> (will # send password Base64 encoded) or <tt>:cram_md5</tt> (combines a Challenge/Response mechanism to exchange # information and a cryptographic Message Digest 5 algorithm to hash important information) - # * <tt>:enable_starttls_auto</tt> - When set to true, detects if STARTTLS is enabled in your SMTP server - # and starts to use it. + # * <tt>:enable_starttls_auto</tt> - Detects if STARTTLS is enabled in your SMTP server and starts + # to use it. Defaults to <tt>true</tt>. # * <tt>:openssl_verify_mode</tt> - When using TLS, you can set how OpenSSL checks the certificate. This is # really useful if you need to validate a self-signed and/or a wildcard certificate. You can use the name # of an OpenSSL verify constant (<tt>'none'</tt>, <tt>'peer'</tt>, <tt>'client_once'</tt>, @@ -393,7 +394,7 @@ module ActionMailer # implement for a custom delivery agent. # # * <tt>perform_deliveries</tt> - Determines whether emails are actually sent from Action Mailer when you - # call <tt>.deliver</tt> on an mail message or on an Action Mailer method. This is on by default but can + # call <tt>.deliver</tt> on an email message or on an Action Mailer method. This is on by default but can # be turned off to aid in functional testing. # # * <tt>deliveries</tt> - Keeps an array of all the emails sent out through the Action Mailer with diff --git a/actionmailer/lib/action_mailer/log_subscriber.rb b/actionmailer/lib/action_mailer/log_subscriber.rb index eb6fb11d81..5b57c75ec3 100644 --- a/actionmailer/lib/action_mailer/log_subscriber.rb +++ b/actionmailer/lib/action_mailer/log_subscriber.rb @@ -6,24 +6,27 @@ module ActionMailer class LogSubscriber < ActiveSupport::LogSubscriber # An email was delivered. def deliver(event) - return unless logger.info? - recipients = Array(event.payload[:to]).join(', ') - info("\nSent mail to #{recipients} (#{event.duration.round(1)}ms)") - debug(event.payload[:mail]) + info do + recipients = Array(event.payload[:to]).join(', ') + "\nSent mail to #{recipients} (#{event.duration.round(1)}ms)" + end + + debug { event.payload[:mail] } end # An email was received. def receive(event) - return unless logger.info? - info("\nReceived mail (#{event.duration.round(1)}ms)") - debug(event.payload[:mail]) + info { "\nReceived mail (#{event.duration.round(1)}ms)" } + 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") + debug do + mailer = event.payload[:mailer] + action = event.payload[:action] + "\n#{mailer}##{action}: processed outbound mail in #{event.duration.round(1)}ms" + end end # Use the logger configured for ActionMailer::Base diff --git a/actionmailer/lib/action_mailer/mail_helper.rb b/actionmailer/lib/action_mailer/mail_helper.rb index 54ad9f066f..483277af04 100644 --- a/actionmailer/lib/action_mailer/mail_helper.rb +++ b/actionmailer/lib/action_mailer/mail_helper.rb @@ -11,8 +11,8 @@ module ActionMailer }.join("\n\n") # Make list points stand on their own line - formatted.gsub!(/[ ]*([*]+) ([^*]*)/) { |s| " #{$1} #{$2.strip}\n" } - formatted.gsub!(/[ ]*([#]+) ([^#]*)/) { |s| " #{$1} #{$2.strip}\n" } + formatted.gsub!(/[ ]*([*]+) ([^*]*)/) { " #{$1} #{$2.strip}\n" } + formatted.gsub!(/[ ]*([#]+) ([^#]*)/) { " #{$1} #{$2.strip}\n" } formatted end diff --git a/actionmailer/lib/action_mailer/preview.rb b/actionmailer/lib/action_mailer/preview.rb index 33a9faa7e7..33de1dc049 100644 --- a/actionmailer/lib/action_mailer/preview.rb +++ b/actionmailer/lib/action_mailer/preview.rb @@ -11,6 +11,14 @@ module ActionMailer # mattr_accessor :preview_path, instance_writer: false + # Enable or disable mailer previews through app configuration: + # + # config.action_mailer.show_previews = true + # + # Defaults to true for development environment + # + mattr_accessor :show_previews, instance_writer: false + # :nodoc: mattr_accessor :preview_interceptors, instance_writer: false self.preview_interceptors = [] @@ -94,6 +102,10 @@ module ActionMailer Base.preview_path end + def show_previews #:nodoc: + Base.show_previews + end + def inform_preview_interceptors(message) #:nodoc: Base.preview_interceptors.each do |interceptor| interceptor.previewing_email(message) diff --git a/actionmailer/lib/action_mailer/railtie.rb b/actionmailer/lib/action_mailer/railtie.rb index 8d1e40297b..6f760732e2 100644 --- a/actionmailer/lib/action_mailer/railtie.rb +++ b/actionmailer/lib/action_mailer/railtie.rb @@ -18,8 +18,9 @@ module ActionMailer options.assets_dir ||= paths["public"].first options.javascripts_dir ||= paths["public/javascripts"].first options.stylesheets_dir ||= paths["public/stylesheets"].first + options.show_previews = Rails.env.development? if options.show_previews.nil? - if Rails.env.development? + if options.show_previews options.preview_path ||= defined?(Rails.root) ? "#{Rails.root}/test/mailers/previews" : nil end @@ -33,9 +34,17 @@ module ActionMailer include app.routes.mounted_helpers register_interceptors(options.delete(:interceptors)) + register_preview_interceptors(options.delete(:preview_interceptors)) register_observers(options.delete(:observers)) options.each { |k,v| send("#{k}=", v) } + + if options.show_previews + app.routes.append do + get '/rails/mailers' => "rails/mailers#index" + get '/rails/mailers/*path' => "rails/mailers#preview" + end + end end end diff --git a/actionmailer/lib/action_mailer/test_case.rb b/actionmailer/lib/action_mailer/test_case.rb index 207f949fe2..a5442c0316 100644 --- a/actionmailer/lib/action_mailer/test_case.rb +++ b/actionmailer/lib/action_mailer/test_case.rb @@ -20,6 +20,7 @@ module ActionMailer class_attribute :_mailer_class setup :initialize_test_deliveries setup :set_expected_mail + teardown :restore_test_deliveries end module ClassMethods @@ -54,8 +55,15 @@ module ActionMailer protected def initialize_test_deliveries + @old_delivery_method = ActionMailer::Base.delivery_method + @old_perform_deliveries = ActionMailer::Base.perform_deliveries ActionMailer::Base.delivery_method = :test ActionMailer::Base.perform_deliveries = true + end + + def restore_test_deliveries + ActionMailer::Base.delivery_method = @old_delivery_method + ActionMailer::Base.perform_deliveries = @old_perform_deliveries ActionMailer::Base.deliveries.clear end diff --git a/actionmailer/lib/action_mailer/test_helper.rb b/actionmailer/lib/action_mailer/test_helper.rb index 6452bf616c..06da0dd27e 100644 --- a/actionmailer/lib/action_mailer/test_helper.rb +++ b/actionmailer/lib/action_mailer/test_helper.rb @@ -1,4 +1,6 @@ module ActionMailer + # Provides helper methods for testing Action Mailer, including #assert_emails + # and #assert_no_emails module TestHelper # Asserts that the number of emails sent matches the given number. # diff --git a/actionmailer/lib/action_mailer/version.rb b/actionmailer/lib/action_mailer/version.rb index a98aec913f..06f80a8fdc 100644 --- a/actionmailer/lib/action_mailer/version.rb +++ b/actionmailer/lib/action_mailer/version.rb @@ -1,7 +1,8 @@ require_relative 'gem_version' module ActionMailer - # Returns the version of the currently loaded ActionMailer as a <tt>Gem::Version</tt> + # Returns the version of the currently loaded Action Mailer as a + # <tt>Gem::Version</tt>. def self.version gem_version end diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb index 93d16f491d..98d266bd73 100644 --- a/actionmailer/test/abstract_unit.rb +++ b/actionmailer/test/abstract_unit.rb @@ -23,34 +23,12 @@ ActiveSupport::Deprecation.debug = true # Disable available locale checks to avoid warnings running the test suite. I18n.enforce_available_locales = false -# Bogus template processors -ActionView::Template.register_template_handler :haml, lambda { |template| "Look its HAML!".inspect } -ActionView::Template.register_template_handler :bak, lambda { |template| "Lame backup".inspect } - FIXTURE_LOAD_PATH = File.expand_path('fixtures', File.dirname(__FILE__)) ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH -class MockSMTP - def self.deliveries - @@deliveries - end - - def initialize - @@deliveries = [] - end - - def sendmail(mail, from, to) - @@deliveries << [mail, from, to] - end - - def start(*args) - yield self - end -end - -class Net::SMTP - def self.new(*args) - MockSMTP.new +class Rails + def self.root + File.expand_path('../', File.dirname(__FILE__)) end end @@ -71,3 +49,5 @@ end def jruby_skip(message = '') skip message if defined?(JRUBY_VERSION) end + +require 'mocha/setup' # FIXME: stop using mocha diff --git a/actionmailer/test/asset_host_test.rb b/actionmailer/test/asset_host_test.rb index 00f1348a53..9ba67c2842 100644 --- a/actionmailer/test/asset_host_test.rb +++ b/actionmailer/test/asset_host_test.rb @@ -31,25 +31,10 @@ class AssetHostTest < ActiveSupport::TestCase def test_asset_host_as_one_argument_proc AssetHostMailer.config.asset_host = Proc.new { |source| if source.starts_with?('/images') - "http://images.example.com" - else - "http://assets.example.com" + 'http://images.example.com' end } mail = AssetHostMailer.email_with_asset assert_equal %Q{<img alt="Somelogo" src="http://images.example.com/images/somelogo.png" />}, mail.body.to_s.strip end - - def test_asset_host_as_two_argument_proc - ActionController::Base.config.asset_host = Proc.new {|source,request| - if request && request.ssl? - "https://www.example.com" - else - "http://www.example.com" - end - } - mail = nil - assert_nothing_raised { mail = AssetHostMailer.email_with_asset } - assert_equal %Q{<img alt="Somelogo" src="http://www.example.com/images/somelogo.png" />}, mail.body.to_s.strip - end end diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index b66e5bd6a3..229ded8e04 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -10,10 +10,15 @@ require 'mailers/proc_mailer' require 'mailers/asset_mailer' class BaseTest < ActiveSupport::TestCase - def teardown - ActionMailer::Base.asset_host = nil - ActionMailer::Base.assets_dir = nil - ActionMailer::Base.preview_interceptors.clear + setup do + @original_asset_host = ActionMailer::Base.asset_host + @original_assets_dir = ActionMailer::Base.assets_dir + end + + teardown do + ActionMailer::Base.asset_host = @original_asset_host + ActionMailer::Base.assets_dir = @original_assets_dir + BaseMailer.deliveries.clear end test "method call to mail does not raise error" do @@ -104,6 +109,7 @@ class BaseTest < ActiveSupport::TestCase test "attachment gets content type from filename" do email = BaseMailer.attachment_with_content assert_equal('invoice.pdf', email.attachments[0].filename) + assert_equal('application/pdf', email.attachments[0].mime_type) end test "attachment with hash" do @@ -201,25 +207,29 @@ class BaseTest < ActiveSupport::TestCase end test "subject gets default from I18n" do - BaseMailer.default subject: nil - email = BaseMailer.welcome(subject: nil) - assert_equal "Welcome", email.subject + with_default BaseMailer, subject: nil do + email = BaseMailer.welcome(subject: nil) + assert_equal "Welcome", email.subject - I18n.backend.store_translations('en', base_mailer: {welcome: {subject: "New Subject!"}}) - email = BaseMailer.welcome(subject: nil) - assert_equal "New Subject!", email.subject + with_translation 'en', base_mailer: {welcome: {subject: "New Subject!"}} do + email = BaseMailer.welcome(subject: nil) + assert_equal "New Subject!", email.subject + end + end end test 'default subject can have interpolations' do - I18n.backend.store_translations('en', base_mailer: {with_subject_interpolations: {subject: 'Will the real %{rapper_or_impersonator} please stand up?'}}) - email = BaseMailer.with_subject_interpolations - assert_equal 'Will the real Slim Shady please stand up?', email.subject + with_translation 'en', base_mailer: {with_subject_interpolations: {subject: 'Will the real %{rapper_or_impersonator} please stand up?'}} do + email = BaseMailer.with_subject_interpolations + assert_equal 'Will the real Slim Shady please stand up?', email.subject + end end test "translations are scoped properly" do - I18n.backend.store_translations('en', base_mailer: {email_with_translations: {greet_user: "Hello %{name}!"}}) - email = BaseMailer.email_with_translations - assert_equal 'Hello lifo!', email.body.encoded + with_translation 'en', base_mailer: {email_with_translations: {greet_user: "Hello %{name}!"}} do + email = BaseMailer.email_with_translations + assert_equal 'Hello lifo!', email.body.encoded + end end # Implicit multipart @@ -407,14 +417,12 @@ class BaseTest < ActiveSupport::TestCase end test "calling just the action should return the generated mail object" do - BaseMailer.deliveries.clear email = BaseMailer.welcome assert_equal(0, BaseMailer.deliveries.length) assert_equal('The first email on new API!', email.subject) end test "calling deliver on the action should deliver the mail object" do - BaseMailer.deliveries.clear BaseMailer.expects(:deliver_mail).once mail = BaseMailer.welcome.deliver assert_equal 'The first email on new API!', mail.subject @@ -422,7 +430,6 @@ class BaseTest < ActiveSupport::TestCase test "calling deliver on the action should increment the deliveries collection if using the test mailer" do BaseMailer.delivery_method = :test - BaseMailer.deliveries.clear BaseMailer.welcome.deliver assert_equal(1, BaseMailer.deliveries.length) end @@ -442,7 +449,6 @@ class BaseTest < ActiveSupport::TestCase end test "should raise if missing template in implicit render" do - BaseMailer.deliveries.clear assert_raises ActionView::MissingTemplate do BaseMailer.implicit_different_template('missing_template').deliver end @@ -479,18 +485,17 @@ class BaseTest < ActiveSupport::TestCase end test "assets tags should use a Mailer's asset_host settings when available" do - begin - ActionMailer::Base.config.asset_host = "http://global.com" - ActionMailer::Base.config.assets_dir = "global/" + ActionMailer::Base.config.asset_host = "http://global.com" + ActionMailer::Base.config.assets_dir = "global/" - AssetMailer.asset_host = "http://local.com" + TempAssetMailer = Class.new(AssetMailer) do + self.mailer_name = "asset_mailer" + self.asset_host = "http://local.com" + end - mail = AssetMailer.welcome + mail = TempAssetMailer.welcome - assert_equal(%{<img alt="Dummy" src="http://local.com/images/dummy.png" />}, mail.body.to_s.strip) - ensure - AssetMailer.asset_host = ActionMailer::Base.config.asset_host - end + assert_equal(%{<img alt="Dummy" src="http://local.com/images/dummy.png" />}, mail.body.to_s.strip) end test 'the view is not rendered when mail was never called' do @@ -518,32 +523,40 @@ class BaseTest < ActiveSupport::TestCase end test "you can register an observer to the mail object that gets informed on email delivery" do - ActionMailer::Base.register_observer(MyObserver) - mail = BaseMailer.welcome - MyObserver.expects(:delivered_email).with(mail) - mail.deliver + mail_side_effects do + ActionMailer::Base.register_observer(MyObserver) + mail = BaseMailer.welcome + MyObserver.expects(:delivered_email).with(mail) + mail.deliver + end end test "you can register an observer using its stringified name to the mail object that gets informed on email delivery" do - ActionMailer::Base.register_observer("BaseTest::MyObserver") - mail = BaseMailer.welcome - MyObserver.expects(:delivered_email).with(mail) - mail.deliver + mail_side_effects do + ActionMailer::Base.register_observer("BaseTest::MyObserver") + mail = BaseMailer.welcome + MyObserver.expects(:delivered_email).with(mail) + mail.deliver + end end test "you can register an observer using its symbolized underscored name to the mail object that gets informed on email delivery" do - ActionMailer::Base.register_observer(:"base_test/my_observer") - mail = BaseMailer.welcome - MyObserver.expects(:delivered_email).with(mail) - mail.deliver + mail_side_effects do + ActionMailer::Base.register_observer(:"base_test/my_observer") + mail = BaseMailer.welcome + MyObserver.expects(:delivered_email).with(mail) + mail.deliver + end end test "you can register multiple observers to the mail object that both get informed on email delivery" do - ActionMailer::Base.register_observers("BaseTest::MyObserver", MySecondObserver) - mail = BaseMailer.welcome - MyObserver.expects(:delivered_email).with(mail) - MySecondObserver.expects(:delivered_email).with(mail) - mail.deliver + mail_side_effects do + ActionMailer::Base.register_observers("BaseTest::MyObserver", MySecondObserver) + mail = BaseMailer.welcome + MyObserver.expects(:delivered_email).with(mail) + MySecondObserver.expects(:delivered_email).with(mail) + mail.deliver + end end class MyInterceptor @@ -556,72 +569,41 @@ class BaseTest < ActiveSupport::TestCase def self.previewing_email(mail); end end - class BaseMailerPreview < ActionMailer::Preview - def welcome - BaseMailer.welcome - end - end - test "you can register an interceptor to the mail object that gets passed the mail object before delivery" do - ActionMailer::Base.register_interceptor(MyInterceptor) - mail = BaseMailer.welcome - MyInterceptor.expects(:delivering_email).with(mail) - mail.deliver + mail_side_effects do + ActionMailer::Base.register_interceptor(MyInterceptor) + mail = BaseMailer.welcome + MyInterceptor.expects(:delivering_email).with(mail) + mail.deliver + end end test "you can register an interceptor using its stringified name to the mail object that gets passed the mail object before delivery" do - ActionMailer::Base.register_interceptor("BaseTest::MyInterceptor") - mail = BaseMailer.welcome - MyInterceptor.expects(:delivering_email).with(mail) - mail.deliver + mail_side_effects do + ActionMailer::Base.register_interceptor("BaseTest::MyInterceptor") + mail = BaseMailer.welcome + MyInterceptor.expects(:delivering_email).with(mail) + mail.deliver + end end test "you can register an interceptor using its symbolized underscored name to the mail object that gets passed the mail object before delivery" do - ActionMailer::Base.register_interceptor(:"base_test/my_interceptor") - mail = BaseMailer.welcome - MyInterceptor.expects(:delivering_email).with(mail) - mail.deliver + mail_side_effects do + ActionMailer::Base.register_interceptor(:"base_test/my_interceptor") + mail = BaseMailer.welcome + MyInterceptor.expects(:delivering_email).with(mail) + mail.deliver + end end test "you can register multiple interceptors to the mail object that both get passed the mail object before delivery" do - ActionMailer::Base.register_interceptors("BaseTest::MyInterceptor", MySecondInterceptor) - mail = BaseMailer.welcome - MyInterceptor.expects(:delivering_email).with(mail) - MySecondInterceptor.expects(:delivering_email).with(mail) - mail.deliver - end - - test "you can register a preview interceptor to the mail object that gets passed the mail object before previewing" do - ActionMailer::Base.register_preview_interceptor(MyInterceptor) - mail = BaseMailer.welcome - BaseMailerPreview.any_instance.stubs(:welcome).returns(mail) - MyInterceptor.expects(:previewing_email).with(mail) - BaseMailerPreview.call(:welcome) - end - - test "you can register a preview interceptor using its stringified name to the mail object that gets passed the mail object before previewing" do - ActionMailer::Base.register_preview_interceptor("BaseTest::MyInterceptor") - mail = BaseMailer.welcome - BaseMailerPreview.any_instance.stubs(:welcome).returns(mail) - MyInterceptor.expects(:previewing_email).with(mail) - BaseMailerPreview.call(:welcome) - end - - test "you can register an interceptor using its symbolized underscored name to the mail object that gets passed the mail object before previewing" do - ActionMailer::Base.register_preview_interceptor(:"base_test/my_interceptor") - mail = BaseMailer.welcome - BaseMailerPreview.any_instance.stubs(:welcome).returns(mail) - MyInterceptor.expects(:previewing_email).with(mail) - BaseMailerPreview.call(:welcome) - end - - test "you can register multiple preview interceptors to the mail object that both get passed the mail object before previewing" do - ActionMailer::Base.register_preview_interceptors("BaseTest::MyInterceptor", MySecondInterceptor) - mail = BaseMailer.welcome - BaseMailerPreview.any_instance.stubs(:welcome).returns(mail) - MyInterceptor.expects(:previewing_email).with(mail) - MySecondInterceptor.expects(:previewing_email).with(mail) - BaseMailerPreview.call(:welcome) + mail_side_effects do + ActionMailer::Base.register_interceptors("BaseTest::MyInterceptor", MySecondInterceptor) + mail = BaseMailer.welcome + MyInterceptor.expects(:delivering_email).with(mail) + MySecondInterceptor.expects(:delivering_email).with(mail) + mail.deliver + end end test "being able to put proc's into the defaults hash and they get evaluated on mail sending" do @@ -770,4 +752,77 @@ class BaseTest < ActiveSupport::TestCase ensure klass.default_params = old end + + # A simple hack to restore the observers and interceptors for Mail, as it + # does not have an unregister API yet. + def mail_side_effects + old_observers = Mail.class_variable_get(:@@delivery_notification_observers) + old_delivery_interceptors = Mail.class_variable_get(:@@delivery_interceptors) + yield + ensure + Mail.class_variable_set(:@@delivery_notification_observers, old_observers) + Mail.class_variable_set(:@@delivery_interceptors, old_delivery_interceptors) + end + + def with_translation(locale, data) + I18n.backend.store_translations(locale, data) + yield + ensure + I18n.backend.reload! + end +end + +class BasePreviewInterceptorsTest < ActiveSupport::TestCase + teardown do + ActionMailer::Base.preview_interceptors.clear + end + + class BaseMailerPreview < ActionMailer::Preview + def welcome + BaseMailer.welcome + end + end + + class MyInterceptor + def self.delivering_email(mail); end + def self.previewing_email(mail); end + end + + class MySecondInterceptor + def self.delivering_email(mail); end + def self.previewing_email(mail); end + end + + test "you can register a preview interceptor to the mail object that gets passed the mail object before previewing" do + ActionMailer::Base.register_preview_interceptor(MyInterceptor) + mail = BaseMailer.welcome + BaseMailerPreview.any_instance.stubs(:welcome).returns(mail) + MyInterceptor.expects(:previewing_email).with(mail) + BaseMailerPreview.call(:welcome) + end + + test "you can register a preview interceptor using its stringified name to the mail object that gets passed the mail object before previewing" do + ActionMailer::Base.register_preview_interceptor("BasePreviewInterceptorsTest::MyInterceptor") + mail = BaseMailer.welcome + BaseMailerPreview.any_instance.stubs(:welcome).returns(mail) + MyInterceptor.expects(:previewing_email).with(mail) + BaseMailerPreview.call(:welcome) + end + + test "you can register an interceptor using its symbolized underscored name to the mail object that gets passed the mail object before previewing" do + ActionMailer::Base.register_preview_interceptor(:"base_preview_interceptors_test/my_interceptor") + mail = BaseMailer.welcome + BaseMailerPreview.any_instance.stubs(:welcome).returns(mail) + MyInterceptor.expects(:previewing_email).with(mail) + BaseMailerPreview.call(:welcome) + end + + test "you can register multiple preview interceptors to the mail object that both get passed the mail object before previewing" do + ActionMailer::Base.register_preview_interceptors("BasePreviewInterceptorsTest::MyInterceptor", MySecondInterceptor) + mail = BaseMailer.welcome + BaseMailerPreview.any_instance.stubs(:welcome).returns(mail) + MyInterceptor.expects(:previewing_email).with(mail) + MySecondInterceptor.expects(:previewing_email).with(mail) + BaseMailerPreview.call(:welcome) + end end diff --git a/actionmailer/test/delivery_methods_test.rb b/actionmailer/test/delivery_methods_test.rb index 609903620b..a76ac6d295 100644 --- a/actionmailer/test/delivery_methods_test.rb +++ b/actionmailer/test/delivery_methods_test.rb @@ -47,12 +47,12 @@ class DefaultsDeliveryMethodsTest < ActiveSupport::TestCase end class CustomDeliveryMethodsTest < ActiveSupport::TestCase - def setup + setup do @old_delivery_method = ActionMailer::Base.delivery_method ActionMailer::Base.add_delivery_method :custom, MyCustomDelivery end - def teardown + teardown do ActionMailer::Base.delivery_method = @old_delivery_method new = ActionMailer::Base.delivery_methods.dup new.delete(:custom) @@ -93,23 +93,22 @@ class MailDeliveryTest < ActiveSupport::TestCase end end - def setup - ActionMailer::Base.delivery_method = :smtp + setup do + @old_delivery_method = DeliveryMailer.delivery_method end - def teardown - DeliveryMailer.delivery_method = :smtp - DeliveryMailer.perform_deliveries = true - DeliveryMailer.raise_delivery_errors = true + teardown do + DeliveryMailer.delivery_method = @old_delivery_method + DeliveryMailer.deliveries.clear end test "ActionMailer should be told when Mail gets delivered" do - DeliveryMailer.deliveries.clear DeliveryMailer.expects(:deliver_mail).once DeliveryMailer.welcome.deliver end test "delivery method can be customized per instance" do + Mail::SMTP.any_instance.expects(:deliver!) email = DeliveryMailer.welcome.deliver assert_instance_of Mail::SMTP, email.delivery_method email = DeliveryMailer.welcome(delivery_method: :test).deliver @@ -119,7 +118,6 @@ class MailDeliveryTest < ActiveSupport::TestCase test "delivery method can be customized in subclasses not changing the parent" do DeliveryMailer.delivery_method = :test assert_equal :smtp, ActionMailer::Base.delivery_method - $BREAK = true email = DeliveryMailer.welcome.deliver assert_instance_of Mail::TestMailer, email.delivery_method end @@ -176,22 +174,29 @@ class MailDeliveryTest < ActiveSupport::TestCase end test "does not perform deliveries if requested" do - DeliveryMailer.perform_deliveries = false - DeliveryMailer.deliveries.clear - Mail::Message.any_instance.expects(:deliver!).never - DeliveryMailer.welcome.deliver + old_perform_deliveries = DeliveryMailer.perform_deliveries + begin + DeliveryMailer.perform_deliveries = false + Mail::Message.any_instance.expects(:deliver!).never + DeliveryMailer.welcome.deliver + ensure + DeliveryMailer.perform_deliveries = old_perform_deliveries + end end test "does not append the deliveries collection if told not to perform the delivery" do - DeliveryMailer.perform_deliveries = false - DeliveryMailer.deliveries.clear - DeliveryMailer.welcome.deliver - assert_equal(0, DeliveryMailer.deliveries.length) + old_perform_deliveries = DeliveryMailer.perform_deliveries + begin + DeliveryMailer.perform_deliveries = false + DeliveryMailer.welcome.deliver + assert_equal [], DeliveryMailer.deliveries + ensure + DeliveryMailer.perform_deliveries = old_perform_deliveries + end end test "raise errors on bogus deliveries" do DeliveryMailer.delivery_method = BogusDelivery - DeliveryMailer.deliveries.clear assert_raise RuntimeError do DeliveryMailer.welcome.deliver end @@ -199,27 +204,34 @@ class MailDeliveryTest < ActiveSupport::TestCase test "does not increment the deliveries collection on error" do DeliveryMailer.delivery_method = BogusDelivery - DeliveryMailer.deliveries.clear assert_raise RuntimeError do DeliveryMailer.welcome.deliver end - assert_equal(0, DeliveryMailer.deliveries.length) + assert_equal [], DeliveryMailer.deliveries end test "does not raise errors on bogus deliveries if set" do - DeliveryMailer.delivery_method = BogusDelivery - DeliveryMailer.raise_delivery_errors = false - assert_nothing_raised do - DeliveryMailer.welcome.deliver + old_raise_delivery_errors = DeliveryMailer.raise_delivery_errors + begin + DeliveryMailer.delivery_method = BogusDelivery + DeliveryMailer.raise_delivery_errors = false + assert_nothing_raised do + DeliveryMailer.welcome.deliver + end + ensure + DeliveryMailer.raise_delivery_errors = old_raise_delivery_errors end end test "does not increment the deliveries collection on bogus deliveries" do - DeliveryMailer.delivery_method = BogusDelivery - DeliveryMailer.raise_delivery_errors = false - DeliveryMailer.deliveries.clear - DeliveryMailer.welcome.deliver - assert_equal(0, DeliveryMailer.deliveries.length) + old_raise_delivery_errors = DeliveryMailer.raise_delivery_errors + begin + DeliveryMailer.delivery_method = BogusDelivery + DeliveryMailer.raise_delivery_errors = false + DeliveryMailer.welcome.deliver + assert_equal [], DeliveryMailer.deliveries + ensure + DeliveryMailer.raise_delivery_errors = old_raise_delivery_errors + end end - end diff --git a/actionmailer/test/fixtures/test_helper_mailer/welcome b/actionmailer/test/fixtures/test_helper_mailer/welcome new file mode 100644 index 0000000000..61ce70d578 --- /dev/null +++ b/actionmailer/test/fixtures/test_helper_mailer/welcome @@ -0,0 +1 @@ +Welcome!
\ No newline at end of file diff --git a/actionmailer/test/i18n_with_controller_test.rb b/actionmailer/test/i18n_with_controller_test.rb index 14a1b11b6d..ee36b89dd6 100644 --- a/actionmailer/test/i18n_with_controller_test.rb +++ b/actionmailer/test/i18n_with_controller_test.rb @@ -10,15 +10,15 @@ class I18nTestMailer < ActionMailer::Base def mail_with_i18n_subject(recipient) @recipient = recipient I18n.locale = :de - mail(to: recipient, subject: "#{I18n.t :email_subject} #{recipient}", + mail(to: recipient, subject: I18n.t(:email_subject), from: "system@loudthinking.com", date: Time.local(2004, 12, 12)) end end class TestController < ActionController::Base def send_mail - I18nTestMailer.mail_with_i18n_subject("test@localhost").deliver - render text: 'Mail sent' + email = I18nTestMailer.mail_with_i18n_subject("test@localhost").deliver + render text: "Mail sent - Subject: #{email.subject}" end end @@ -28,20 +28,43 @@ class ActionMailerI18nWithControllerTest < ActionDispatch::IntegrationTest get ':controller(/:action(/:id))' end - def app - Routes + class RoutedRackApp + attr_reader :routes + + def initialize(routes, &blk) + @routes = routes + @stack = ActionDispatch::MiddlewareStack.new(&blk).build(@routes) + end + + def call(env) + @stack.call(env) + end end - def setup - I18n.backend.store_translations('de', email_subject: '[Signed up] Welcome') + APP = RoutedRackApp.new(Routes) + + def app + APP end - def teardown - I18n.locale = :en + teardown do + I18n.locale = I18n.default_locale end def test_send_mail - get '/test/send_mail' - assert_equal "Mail sent", @response.body + Mail::SMTP.any_instance.expects(:deliver!) + with_translation 'de', email_subject: '[Anmeldung] Willkommen' do + get '/test/send_mail' + assert_equal "Mail sent - Subject: [Anmeldung] Willkommen", @response.body + end + end + + protected + + def with_translation(locale, data) + I18n.backend.store_translations(locale, data) + yield + ensure + I18n.backend.reload! end end diff --git a/actionmailer/test/log_subscriber_test.rb b/actionmailer/test/log_subscriber_test.rb index 5f0bee88fd..e7a73d6c8e 100644 --- a/actionmailer/test/log_subscriber_test.rb +++ b/actionmailer/test/log_subscriber_test.rb @@ -1,7 +1,7 @@ -require "abstract_unit" +require 'abstract_unit' require 'mailers/base_mailer' -require "active_support/log_subscriber/test_helper" -require "action_mailer/log_subscriber" +require 'active_support/log_subscriber/test_helper' +require 'action_mailer/log_subscriber' class AMLogSubscriberTest < ActionMailer::TestCase include ActiveSupport::LogSubscriber::TestHelper @@ -31,6 +31,8 @@ class AMLogSubscriberTest < ActionMailer::TestCase 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) + ensure + BaseMailer.deliveries.clear end def test_receive_is_notified diff --git a/actionmailer/test/mail_layout_test.rb b/actionmailer/test/mail_layout_test.rb index 7f959282cb..166dd096d4 100644 --- a/actionmailer/test/mail_layout_test.rb +++ b/actionmailer/test/mail_layout_test.rb @@ -44,16 +44,6 @@ class ExplicitLayoutMailer < ActionMailer::Base end class LayoutMailerTest < ActiveSupport::TestCase - def setup - set_delivery_method :test - ActionMailer::Base.perform_deliveries = true - ActionMailer::Base.deliveries.clear - end - - def teardown - restore_delivery_method - end - def test_should_pickup_default_layout mail = AutoLayoutMailer.hello assert_equal "Hello from layout Inside", mail.body.to_s.strip diff --git a/actionmailer/test/test_helper_test.rb b/actionmailer/test/test_helper_test.rb index 7c7f0b6fdc..1ff08a3b6e 100644 --- a/actionmailer/test/test_helper_test.rb +++ b/actionmailer/test/test_helper_test.rb @@ -1,3 +1,4 @@ +# encoding: utf-8 require 'abstract_unit' class TestHelperMailer < ActionMailer::Base @@ -36,6 +37,14 @@ class TestHelperMailerTest < ActionMailer::TestCase assert_equal "UTF-8", charset end + def test_encode + assert_equal '=?UTF-8?Q?This_is_=E3=81=82_string?=', encode('This is あ string') + end + + def test_read_fixture + assert_equal ['Welcome!'], read_fixture('welcome') + end + def test_assert_emails assert_nothing_raised do assert_emails 1 do diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 15833641bb..c30217b8fe 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,158 @@ +* Allows ActionDispatch::Request::LOCALHOST to match any IPv4 127.0.0.0/8 + loopback address. + + *Earl St Sauver*, *Sven Riedel* + +* Preserve original path in `ShowExceptions` middleware by stashing it as + `env["action_dispatch.original_path"]` + + `ActionDispatch::ShowExceptions` overwrites `PATH_INFO` with the status code + for the exception defined in `ExceptionWrapper`, so the path + the user was visiting when an exception occurred was not previously + available to any custom exceptions_app. The original `PATH_INFO` is now + stashed in `env["action_dispatch.original_path"]`. + + *Grey Baker* + +* Use `String#bytesize` instead of `String#size` when checking for cookie + overflow. + + *Agis Anastasopoulos* + +* `render nothing: true` or rendering a `nil` body no longer add a single + space to the response body. + + The old behavior was added as a workaround for a bug in an early version of + Safari, where the HTTP headers are not returned correctly if the response + body has a 0-length. This is been fixed since and the workaround is no + longer necessary. + + Use `render body: ' '` if the old behavior is desired. + + See #14883 for details. + + *Godfrey Chan* + +* Prepend a JS comment to JSONP callbacks. Addresses CVE-2014-4671 + ("Rosetta Flash") + + *Greg Campbell* + +* Because URI paths may contain non US-ASCII characters we need to force + the encoding of any unescaped URIs to UTF-8 if they are US-ASCII. + This essentially replicates the functionality of the monkey patch to + URI.parser.unescape in active_support/core_ext/uri.rb. + + Fixes #16104. + + *Karl Entwistle* + +* Generate shallow paths for all children of shallow resources. + + Fixes #15783. + + *Seb Jacobs* + +* JSONP responses are now rendered with the `text/javascript` content type + when rendering through a `respond_to` block. + + Fixes #15081. + + *Lucas Mazza* + +* Add `config.action_controller.always_permitted_parameters` to configure which + parameters are permitted globally. The default value of this configuration is + `['controller', 'action']`. + + *Gary S. Weaver*, *Rafael Chacon* + +* Fix env['PATH_INFO'] missing leading slash when a rack app mounted at '/'. + + Fixes #15511. + + *Larry Lv* + +* ActionController::Parameters#require now accepts `false` values. + + Fixes #15685. + + *Sergio Romano* + +* With authorization header `Authorization: Token token=`, `authenticate` now + recognize token as nil, instead of "token". + + Fixes #14846. + + *Larry Lv* + +* Ensure the controller is always notified as soon as the client disconnects + during live streaming, even when the controller is blocked on a write. + + *Nicholas Jakobsen*, *Matthew Draper* + +* Routes specifying 'to:' must be a string that contains a "#" or a rack + application. Use of a symbol should be replaced with `action: symbol`. + Use of a string without a "#" should be replaced with `controller: string`. + + *Aaron Patterson* + +* Fix URL generation with `:trailing_slash` such that it does not add + a trailing slash after `.:format` + + *Dan Langevin* + +* Build full URI as string when processing path in integration tests for + performance reasons. + + *Guo Xiang Tan* + +* Fix `'Stack level too deep'` when rendering `head :ok` in an action method + called 'status' in a controller. + + Fixes #13905. + + *Christiaan Van den Poel* + +* Add MKCALENDAR HTTP method (RFC 4791). + + *Sergey Karpesh* + +* Instrument fragment cache metrics. + + Adds `:controller`: and `:action` keys to the instrumentation payload + for the `*_fragment.action_controller` notifications. This allows tracking + e.g. the fragment cache hit rates for each controller action. + + *Daniel Schierbeck* + +* Always use the provided port if the protocol is relative. + + Fixes #15043. + + *Guilherme Cavalcanti*, *Andrew White* + +* Moved `params[request_forgery_protection_token]` into its own method + and improved tests. + + Fixes #11316. + + *Tom Kadwill* + +* Added verification of route constraints given as a Proc or an object responding + to `:matches?`. Previously, when given an non-complying object, it would just + silently fail to enforce the constraint. It will now raise an `ArgumentError` + when setting up the routes. + + *Xavier Defrang* + +* Properly treat the entire IPv6 User Local Address space as private for + purposes of remote IP detection. Also handle uppercase private IPv6 + addresses. + + Fixes #12638. + + *Caleb Spare* + * Fixed an issue with migrating legacy json cookies. Previously, the `VerifyAndUpgradeLegacySignedMessage` assumes all incoming @@ -19,7 +174,7 @@ 4. Use `escape_segment` rather than `escape_path` in URL generation For point 4 there are two exceptions. Firstly, when a route uses wildcard segments - (e.g. *foo) then we use `escape_path` as the value may contain '/' characters. This + (e.g. `*foo`) then we use `escape_path` as the value may contain '/' characters. This means that wildcard routes can't be optimized. Secondly, if a `:controller` segment is used in the path then this uses `escape_path` as the controller may be namespaced. @@ -49,12 +204,12 @@ *Andrew White*, *James Coglan* -* Append link to bad code to backtrace when exception is SyntaxError. +* Append link to bad code to backtrace when exception is `SyntaxError`. *Boris Kuznetsov* * Swapped the parameters of assert_equal in `assert_select` so that the - proper values were printed correctly + proper values were printed correctly. Fixes #14422. diff --git a/actionpack/README.rdoc b/actionpack/README.rdoc index 2f6575c3b5..02a24a7412 100644 --- a/actionpack/README.rdoc +++ b/actionpack/README.rdoc @@ -48,6 +48,11 @@ API documentation is at * http://api.rubyonrails.org -Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here: +Bug reports can be filed for the Ruby on Rails project here: * https://github.com/rails/rails/issues + +Feature requests should be discussed on the rails-core mailing list here: + +* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core + diff --git a/actionpack/RUNNING_UNIT_TESTS.rdoc b/actionpack/RUNNING_UNIT_TESTS.rdoc index 2f923136d9..f96a9d9da5 100644 --- a/actionpack/RUNNING_UNIT_TESTS.rdoc +++ b/actionpack/RUNNING_UNIT_TESTS.rdoc @@ -4,7 +4,7 @@ The easiest way to run the unit tests is through Rake. The default task runs the entire test suite for all classes. For more information, check out the full array of rake tasks with "rake -T". -Rake can be found at http://rake.rubyforge.org. +Rake can be found at http://docs.seattlerb.org/rake/. == Running by hand diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index 1d6009bab8..d509891fe3 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -21,7 +21,7 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', version - s.add_dependency 'rack', '~> 1.5.2' + s.add_dependency 'rack', '~> 1.6.0.alpha' s.add_dependency 'rack-test', '~> 0.6.2' s.add_dependency 'actionview', version diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index af5de815bb..15faabf977 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -8,7 +8,8 @@ module AbstractController class Error < StandardError #:nodoc: end - class ActionNotFound < StandardError #:nodoc: + # Raised when a non-existing controller action is triggered. + class ActionNotFound < StandardError end # <tt>AbstractController::Base</tt> is a low-level API. Nobody should be @@ -120,14 +121,14 @@ module AbstractController # # The actual method that is called is determined by calling # #method_for_action. If no method can handle the action, then an - # ActionNotFound error is raised. + # AbstractController::ActionNotFound error is raised. # # ==== Returns # * <tt>self</tt> def process(action, *args) - @_action_name = action_name = action.to_s + @_action_name = action.to_s - unless action_name = method_for_action(action_name) + unless action_name = _find_action_name(@_action_name) raise ActionNotFound, "The action '#{action}' could not be found for #{self.class.name}" end @@ -160,7 +161,7 @@ module AbstractController # ==== Returns # * <tt>TrueClass</tt>, <tt>FalseClass</tt> def available_action?(action_name) - method_for_action(action_name).present? + _find_action_name(action_name).present? end private @@ -204,6 +205,24 @@ module AbstractController end # Takes an action name and returns the name of the method that will + # handle the action. + # + # It checks if the action name is valid and returns false otherwise. + # + # See method_for_action for more information. + # + # ==== Parameters + # * <tt>action_name</tt> - An action name to find a method name for + # + # ==== Returns + # * <tt>string</tt> - The name of the method that handles the action + # * false - No valid method name could be found. + # Raise AbstractController::ActionNotFound. + def _find_action_name(action_name) + _valid_action_name?(action_name) && method_for_action(action_name) + end + + # Takes an action name and returns the name of the method that will # handle the action. In normal cases, this method returns the same # name as it receives. By default, if #method_for_action receives # a name that is not an action, it will look for an #action_missing @@ -218,14 +237,14 @@ module AbstractController # the case. # # If none of these conditions are true, and method_for_action - # returns nil, an ActionNotFound exception will be raised. + # returns nil, an AbstractController::ActionNotFound exception will be raised. # # ==== Parameters # * <tt>action_name</tt> - An action name to find a method name for # # ==== Returns # * <tt>string</tt> - The name of the method that handles the action - # * <tt>nil</tt> - No method name could be found. Raise ActionNotFound. + # * <tt>nil</tt> - No method name could be found. def method_for_action(action_name) if action_method?(action_name) action_name @@ -233,5 +252,10 @@ module AbstractController "_handle_action_missing" end end + + # Checks if the action name is valid and returns false otherwise. + def _valid_action_name?(action_name) + !action_name.to_s.include? File::SEPARATOR + end end end diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index 69aca308d6..ca5c80cd71 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -42,20 +42,18 @@ module AbstractController end end - # Skip before, after, and around action callbacks matching any of the names - # Aliased as skip_filter. + # Skip before, after, and around action callbacks matching any of the names. # # ==== Parameters # * <tt>names</tt> - A list of valid names that could be used for # callbacks. Note that skipping uses Ruby equality, so it's # impossible to skip a callback defined using an anonymous proc - # using #skip_filter + # using #skip_action_callback def skip_action_callback(*names) skip_before_action(*names) skip_after_action(*names) skip_around_action(*names) end - alias_method :skip_filter, :skip_action_callback # Take callback names and an optional callback proc, normalize them, @@ -85,7 +83,6 @@ module AbstractController # :call-seq: before_action(names, block) # # Append a callback before actions. See _insert_callbacks for parameter details. - # Aliased as before_filter. ## # :method: prepend_before_action @@ -93,7 +90,6 @@ module AbstractController # :call-seq: prepend_before_action(names, block) # # Prepend a callback before actions. See _insert_callbacks for parameter details. - # Aliased as prepend_before_filter. ## # :method: skip_before_action @@ -101,7 +97,6 @@ module AbstractController # :call-seq: skip_before_action(names) # # Skip a callback before actions. See _insert_callbacks for parameter details. - # Aliased as skip_before_filter. ## # :method: append_before_action @@ -109,7 +104,6 @@ module AbstractController # :call-seq: append_before_action(names, block) # # Append a callback before actions. See _insert_callbacks for parameter details. - # Aliased as append_before_filter. ## # :method: after_action @@ -117,7 +111,6 @@ module AbstractController # :call-seq: after_action(names, block) # # Append a callback after actions. See _insert_callbacks for parameter details. - # Aliased as after_filter. ## # :method: prepend_after_action @@ -125,7 +118,6 @@ module AbstractController # :call-seq: prepend_after_action(names, block) # # Prepend a callback after actions. See _insert_callbacks for parameter details. - # Aliased as prepend_after_filter. ## # :method: skip_after_action @@ -133,7 +125,6 @@ module AbstractController # :call-seq: skip_after_action(names) # # Skip a callback after actions. See _insert_callbacks for parameter details. - # Aliased as skip_after_filter. ## # :method: append_after_action @@ -141,7 +132,6 @@ module AbstractController # :call-seq: append_after_action(names, block) # # Append a callback after actions. See _insert_callbacks for parameter details. - # Aliased as append_after_filter. ## # :method: around_action @@ -149,7 +139,6 @@ module AbstractController # :call-seq: around_action(names, block) # # Append a callback around actions. See _insert_callbacks for parameter details. - # Aliased as around_filter. ## # :method: prepend_around_action @@ -157,7 +146,6 @@ module AbstractController # :call-seq: prepend_around_action(names, block) # # Prepend a callback around actions. See _insert_callbacks for parameter details. - # Aliased as prepend_around_filter. ## # :method: skip_around_action @@ -165,7 +153,6 @@ module AbstractController # :call-seq: skip_around_action(names) # # Skip a callback around actions. See _insert_callbacks for parameter details. - # Aliased as skip_around_filter. ## # :method: append_around_action @@ -173,7 +160,6 @@ module AbstractController # :call-seq: append_around_action(names, block) # # Append a callback around actions. See _insert_callbacks for parameter details. - # Aliased as append_around_filter. # set up before_action, prepend_before_action, skip_before_action, etc. # for each of before, after, and around. @@ -183,7 +169,6 @@ module AbstractController set_callback(:process_action, callback, name, options) end end - alias_method :"#{callback}_filter", :"#{callback}_action" define_method "prepend_#{callback}_action" do |*names, &blk| @@ -191,7 +176,6 @@ module AbstractController set_callback(:process_action, callback, name, options.merge(:prepend => true)) end end - alias_method :"prepend_#{callback}_filter", :"prepend_#{callback}_action" # Skip a before, after or around callback. See _insert_callbacks @@ -201,12 +185,11 @@ module AbstractController skip_callback(:process_action, callback, name, options) end end - alias_method :"skip_#{callback}_filter", :"skip_#{callback}_action" # *_action is the same as append_*_action - alias_method :"append_#{callback}_action", :"#{callback}_action" # alias_method :append_before_action, :before_action - alias_method :"append_#{callback}_filter", :"#{callback}_action" # alias_method :append_before_filter, :before_action + alias_method :"append_#{callback}_action", :"#{callback}_action" + alias_method :"append_#{callback}_filter", :"#{callback}_action" end end end diff --git a/actionpack/lib/abstract_controller/url_for.rb b/actionpack/lib/abstract_controller/url_for.rb index 4a95e1f276..72d07b0927 100644 --- a/actionpack/lib/abstract_controller/url_for.rb +++ b/actionpack/lib/abstract_controller/url_for.rb @@ -11,7 +11,7 @@ module AbstractController def _routes raise "In order to use #url_for, you must include routing helpers explicitly. " \ - "For instance, `include Rails.application.routes.url_helpers" + "For instance, `include Rails.application.routes.url_helpers`." end module ClassMethods diff --git a/actionpack/lib/action_controller/caching/fragments.rb b/actionpack/lib/action_controller/caching/fragments.rb index 879d5fdd94..2694d4c12f 100644 --- a/actionpack/lib/action_controller/caching/fragments.rb +++ b/actionpack/lib/action_controller/caching/fragments.rb @@ -90,7 +90,13 @@ module ActionController end def instrument_fragment_cache(name, key) # :nodoc: - ActiveSupport::Notifications.instrument("#{name}.action_controller", :key => key){ yield } + payload = { + controller: controller_name, + action: action_name, + key: key + } + + ActiveSupport::Notifications.instrument("#{name}.action_controller", payload) { yield } end end end diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index b1acca2435..89fa75f025 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -16,50 +16,51 @@ module ActionController end def process_action(event) - return unless logger.info? - - payload = event.payload - additions = ActionController::Base.log_process_action(payload) - - status = payload[:status] - if status.nil? && payload[:exception].present? - exception_class_name = payload[:exception].first - status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name) + info do + payload = event.payload + additions = ActionController::Base.log_process_action(payload) + + status = payload[:status] + if status.nil? && payload[:exception].present? + exception_class_name = payload[:exception].first + status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name) + end + message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms" + message << " (#{additions.join(" | ")})" unless additions.blank? + message end - message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms" - message << " (#{additions.join(" | ")})" unless additions.blank? - - info(message) end def halted_callback(event) - info("Filter chain halted as #{event.payload[:filter].inspect} rendered or redirected") + info { "Filter chain halted as #{event.payload[:filter].inspect} rendered or redirected" } end def send_file(event) - info("Sent file #{event.payload[:path]} (#{event.duration.round(1)}ms)") + info { "Sent file #{event.payload[:path]} (#{event.duration.round(1)}ms)" } end def redirect_to(event) - info("Redirected to #{event.payload[:location]}") + info { "Redirected to #{event.payload[:location]}" } end def send_data(event) - info("Sent data #{event.payload[:filename]} (#{event.duration.round(1)}ms)") + info { "Sent data #{event.payload[:filename]} (#{event.duration.round(1)}ms)" } end def unpermitted_parameters(event) - unpermitted_keys = event.payload[:keys] - debug("Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.join(", ")}") + debug do + unpermitted_keys = event.payload[:keys] + "Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.join(", ")}" + end end def deep_munge(event) - message = "Value for params[:#{event.payload[:keys].join('][:')}] was set "\ - "to nil, because it was one of [], [null] or [null, null, ...]. "\ - "Go to http://guides.rubyonrails.org/security.html#unsafe-query-generation "\ - "for more information."\ - - debug(message) + debug do + "Value for params[:#{event.payload[:keys].join('][:')}] was set "\ + "to nil, because it was one of [], [null] or [null, null, ...]. "\ + "Go to http://guides.rubyonrails.org/security.html#unsafe-query-generation "\ + "for more information."\ + end end %w(write_fragment read_fragment exist_fragment? diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index 0f4cc7a8f5..9a427ebfdb 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -30,10 +30,8 @@ module ActionController end end - def build(action, app=nil, &block) - app ||= block + def build(action, app = Proc.new) action = action.to_s - raise "MiddlewareStack#build requires an app" unless app middlewares.reverse.inject(app) do |a, middleware| middleware.valid?(action) ? middleware.build(a) : a @@ -223,14 +221,23 @@ module ActionController # Makes the controller a Rack endpoint that runs the action in the given # +env+'s +action_dispatch.request.path_parameters+ key. def self.call(env) - action(env['action_dispatch.request.path_parameters'][:action]).call(env) + req = ActionDispatch::Request.new env + action(req.path_parameters[:action]).call(env) end # Returns a Rack endpoint for the given action name. def self.action(name, klass = ActionDispatch::Request) - middleware_stack.build(name.to_s) do |env| - new.dispatch(name, klass.new(env)) + if middleware_stack.any? + middleware_stack.build(name) do |env| + new.dispatch(name, klass.new(env)) + end + else + lambda { |env| new.dispatch(name, klass.new(env)) } end end + + def _status_code + @_status + end end end diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index 43407f5b78..3d2badf9c2 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -14,6 +14,8 @@ module ActionController # return head(:method_not_allowed) unless request.post? # return head(:bad_request) unless valid_request? # render + # + # See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list of valid +status+ symbols. def head(status, options = {}) options, status = status, nil if status.is_a?(Hash) status ||= options.delete(:status) || :ok @@ -27,7 +29,7 @@ module ActionController self.status = status self.location = url_for(location) if location - if include_content?(self.status) + if include_content?(self._status_code) self.content_type = content_type || (Mime[formats.first] if formats) self.response.charset = false if self.response self.response_body = " " diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 2eb7853aa6..25c123edf7 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -90,17 +90,29 @@ module ActionController end def authenticate(request, &login_procedure) - unless request.authorization.blank? + if has_basic_credentials?(request) login_procedure.call(*user_name_and_password(request)) end end + def has_basic_credentials?(request) + request.authorization.present? && (auth_scheme(request) == 'Basic') + end + def user_name_and_password(request) decode_credentials(request).split(':', 2) end def decode_credentials(request) - ::Base64.decode64(request.authorization.split(' ', 2).last || '') + ::Base64.decode64(auth_param(request) || '') + end + + def auth_scheme(request) + request.authorization.split(' ', 2).first + end + + def auth_param(request) + request.authorization.split(' ', 2).second end def encode_credentials(user_name, password) @@ -109,8 +121,8 @@ module ActionController def authentication_request(controller, realm) controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.gsub(/"/, "")}") - controller.response_body = "HTTP Basic: Access denied.\n" controller.status = 401 + controller.response_body = "HTTP Basic: Access denied.\n" end end @@ -244,8 +256,8 @@ module ActionController def authentication_request(controller, realm, message = nil) message ||= "HTTP Digest: Access denied.\n" authentication_header(controller, realm) - controller.response_body = message controller.status = 401 + controller.response_body = message end def secret_token(request) @@ -437,7 +449,7 @@ module ActionController authorization_request = request.authorization.to_s if authorization_request[TOKEN_REGEX] params = token_params_from authorization_request - [params.shift.last, Hash[params].with_indifferent_access] + [params.shift[1], Hash[params].with_indifferent_access] end end @@ -452,14 +464,14 @@ module ActionController # This removes the `"` characters wrapping the value. def rewrite_param_values(array_params) - array_params.each { |param| param.last.gsub! %r/^"|"$/, '' } + array_params.each { |param| (param[1] || "").gsub! %r/^"|"$/, '' } end # This method takes an authorization body and splits up the key-value # pairs by the standardized `:`, `;`, or `\t` delimiters defined in # `AUTHN_PAIR_DELIMITERS`. def raw_params(auth) - auth.sub(TOKEN_REGEX, '').split(/"\s*#{AUTHN_PAIR_DELIMITERS}\s*/) + auth.sub(TOKEN_REGEX, '').split(/\s*#{AUTHN_PAIR_DELIMITERS}\s*/) end # Encodes the given token and options into an Authorization header value. diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index acf40b2e16..706ce04062 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -102,16 +102,30 @@ module ActionController end end - @stream.write "data: #{json}\n\n" + message = json.gsub("\n", "\ndata: ") + @stream.write "data: #{message}\n\n" end end + class ClientDisconnected < RuntimeError + end + class Buffer < ActionDispatch::Response::Buffer #:nodoc: include MonitorMixin + # Ignore that the client has disconnected. + # + # If this value is `true`, calling `write` after the client + # disconnects will result in the written content being silently + # discarded. If this value is `false` (the default), a + # ClientDisconnected exception will be raised. + attr_accessor :ignore_disconnect + def initialize(response) @error_callback = lambda { true } @cv = new_cond + @aborted = false + @ignore_disconnect = false super(response, SizedQueue.new(10)) end @@ -122,6 +136,17 @@ module ActionController end super + + unless connected? + @buf.clear + + unless @ignore_disconnect + # Raise ClientDisconnected, which is a RuntimeError (not an + # IOError), because that's more appropriate for something beyond + # the developer's control. + raise ClientDisconnected, "client disconnected" + end + end end def each @@ -132,6 +157,10 @@ module ActionController @response.sent! end + # Write a 'close' event to the buffer; the producer/writing thread + # uses this to notify us that it's finished supplying content. + # + # See also #abort. def close synchronize do super @@ -140,6 +169,26 @@ module ActionController end end + # Inform the producer/writing thread that the client has + # disconnected; the reading thread is no longer interested in + # anything that's being written. + # + # See also #close. + def abort + synchronize do + @aborted = true + @buf.clear + end + end + + # Is the client still connected and waiting for content? + # + # The result of calling `write` when this is `false` is determined + # by `ignore_disconnect`. + def connected? + !@aborted + end + def await_close synchronize do @cv.wait_until { @closed } @@ -156,7 +205,7 @@ module ActionController end class Response < ActionDispatch::Response #:nodoc: all - class Header < DelegateClass(Hash) + class Header < DelegateClass(Hash) # :nodoc: def initialize(response, header) @response = response super(header) diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 1974bbf529..00e7e980f8 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -322,7 +322,7 @@ module ActionController #:nodoc: # end # end # - # * for a javascript request - if the template isn't found, an exception is + # * for a JavaScript request - if the template isn't found, an exception is # raised. # * for other requests - i.e. data formats such as xml, json, csv etc, if # the resource passed to +respond_with+ responds to <code>to_<format></code>, @@ -335,7 +335,7 @@ module ActionController #:nodoc: # As outlined above, the +resources+ argument passed to +respond_with+ # can play two roles. It can be used to generate the redirect url # for successful html requests (e.g. for +create+ actions when - # no template exists), while for formats other than html and javascript + # no template exists), while for formats other than html and JavaScript # it is the object that gets rendered, by being converted directly to the # required format (again assuming no template exists). # @@ -352,7 +352,7 @@ module ActionController #:nodoc: # # This would cause +respond_with+ to redirect to <code>project_task_url</code> # instead of <code>task_url</code>. For request formats other than html or - # javascript, if multiple resources are passed in this way, it is the last + # JavaScript, if multiple resources are passed in this way, it is the last # one specified that is rendered. # # === Customizing response behavior diff --git a/actionpack/lib/action_controller/metal/rack_delegation.rb b/actionpack/lib/action_controller/metal/rack_delegation.rb index bdf6e88699..6921834044 100644 --- a/actionpack/lib/action_controller/metal/rack_delegation.rb +++ b/actionpack/lib/action_controller/metal/rack_delegation.rb @@ -6,7 +6,7 @@ module ActionController extend ActiveSupport::Concern delegate :headers, :status=, :location=, :content_type=, - :status, :location, :content_type, :to => "@_response" + :status, :location, :content_type, :_status_code, :to => "@_response" def dispatch(action, request) set_response!(request) diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index 2812038938..ca8c0278d0 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -14,7 +14,7 @@ module ActionController include ActionController::RackDelegation include ActionController::UrlFor - # Redirects the browser to the target specified in +options+. This parameter can take one of three forms: + # Redirects the browser to the target specified in +options+. This parameter can be any one of: # # * <tt>Hash</tt> - The URL will be generated by calling url_for with the +options+. # * <tt>Record</tt> - The URL will be generated by calling url_for with the +options+, which will reference a named URL for that record. @@ -24,6 +24,8 @@ module ActionController # * <tt>:back</tt> - Back to the page that issued the request. Useful for forms that are triggered from multiple places. # Short-hand for <tt>redirect_to(request.env["HTTP_REFERER"])</tt> # + # === Examples: + # # redirect_to action: "show", id: 5 # redirect_to post # redirect_to "http://www.rubyonrails.org" @@ -32,7 +34,7 @@ module ActionController # redirect_to :back # redirect_to proc { edit_post_url(@post) } # - # The redirection happens as a "302 Found" header unless otherwise specified. + # The redirection happens as a "302 Found" header unless otherwise specified using the <tt>:status</tt> option: # # redirect_to post_url(@post), status: :found # redirect_to action: 'atom', status: :moved_permanently @@ -60,15 +62,18 @@ module ActionController # redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id } # redirect_to({ action: 'atom' }, alert: "Something serious happened") # - # When using <tt>redirect_to :back</tt>, if there is no referrer, ActionController::RedirectBackError will be raised. You may specify some fallback - # behavior for this case by rescuing ActionController::RedirectBackError. + # When using <tt>redirect_to :back</tt>, if there is no referrer, + # <tt>ActionController::RedirectBackError</tt> will be raised. You + # may specify some fallback behavior for this case by rescuing + # <tt>ActionController::RedirectBackError</tt>. def redirect_to(options = {}, response_status = {}) #:doc: raise ActionControllerError.new("Cannot redirect to nil!") unless options + raise ActionControllerError.new("Cannot redirect to a parameter hash!") if options.is_a?(ActionController::Parameters) raise AbstractController::DoubleRenderError if response_body self.status = _extract_redirect_to_status(options, response_status) self.location = _compute_redirect_to_location(options) - self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.h(location)}\">redirected</a>.</body></html>" + self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(location)}\">redirected</a>.</body></html>" end def _compute_redirect_to_location(options) #:nodoc: diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 0443b73953..02c4e563f5 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -6,6 +6,11 @@ module ActionController Renderers.add(key, &block) end + # See <tt>Renderers.remove</tt> + def self.remove_renderer(key) + Renderers.remove(key) + end + class MissingRenderer < LoadError def initialize(format) super "No renderer defined for format: #{format}" @@ -73,7 +78,7 @@ module ActionController # respond_to do |format| # format.html # format.csv { render csv: @csvable, filename: @csvable.name } - # } + # end # end # To use renderers and their mime types in more concise ways, see # <tt>ActionController::MimeResponds::ClassMethods.respond_to</tt> and @@ -83,6 +88,17 @@ module ActionController RENDERERS << key.to_sym end + # This method is the opposite of add method. + # + # Usage: + # + # ActionController::Renderers.remove(:csv) + def self.remove(key) + RENDERERS.delete(key.to_sym) + method = "_render_option_#{key}" + remove_method(method) if method_defined?(method) + end + module All extend ActiveSupport::Concern include Renderers @@ -96,8 +112,11 @@ module ActionController json = json.to_json(options) unless json.kind_of?(String) if options[:callback].present? - self.content_type ||= Mime::JS - "#{options[:callback]}(#{json})" + if self.content_type.nil? || self.content_type == Mime::JSON + self.content_type = Mime::JS + end + + "/**/#{options[:callback]}(#{json})" else self.content_type ||= Mime::JSON json diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index 93e7d6954c..7bbff0450a 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -67,8 +67,8 @@ module ActionController options[:html] = ERB::Util.html_escape(options[:html]) end - if options.delete(:nothing) || _any_render_format_is_nil?(options) - options[:body] = " " + if options.delete(:nothing) + options[:body] = nil end if options[:status] @@ -86,10 +86,6 @@ module ActionController end end - def _any_render_format_is_nil?(options) - RENDER_FORMATS_IN_PRIORITY.any? { |format| options.key?(format) && options[format].nil? } - end - # Process controller specific options, as status, content-type and location. def _process_options(options) #:nodoc: status, content_type, location = options.values_at(:status, :content_type, :location) diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index e3b1f5ae7c..0efa0fb259 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -77,7 +77,7 @@ module ActionController #:nodoc: end module ClassMethods - # Turn on request forgery protection. Bear in mind that only non-GET, HTML/JavaScript requests are checked. + # Turn on request forgery protection. Bear in mind that GET and HEAD requests are not checked. # # class ApplicationController < ActionController::Base # protect_from_forgery @@ -247,7 +247,7 @@ module ActionController #:nodoc: # * Does the X-CSRF-Token header match the form_authenticity_token def verified_request? !protect_against_forgery? || request.get? || request.head? || - form_authenticity_token == params[request_forgery_protection_token] || + form_authenticity_token == form_authenticity_param || form_authenticity_token == request.headers['X-CSRF-Token'] end diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb index 62d5931b45..04401cad7b 100644 --- a/actionpack/lib/action_controller/metal/streaming.rb +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -183,7 +183,7 @@ module ActionController #:nodoc: # You may also want to configure other parameters like <tt>:tcp_nodelay</tt>. # Please check its documentation for more information: http://unicorn.bogomips.org/Unicorn/Configurator.html#method-i-listen # - # If you are using Unicorn with Nginx, you may need to tweak Nginx. + # If you are using Unicorn with NGINX, you may need to tweak NGINX. # Streaming should work out of the box on Rainbows. # # ==== Passenger diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index d86d49c9dc..bc27ecaa20 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/array/wrap' +require 'active_support/deprecation' require 'active_support/rescuable' require 'action_dispatch/http/upload' require 'stringio' @@ -39,7 +40,7 @@ module ActionController # == Action Controller \Parameters # # Allows to choose which attributes should be whitelisted for mass updating - # and thus prevent accidentally exposing that which shouldn’t be exposed. + # and thus prevent accidentally exposing that which shouldn't be exposed. # Provides two methods for this purpose: #require and #permit. The former is # used to mark parameters as required. The latter is used to set the parameter # as permitted and limit which attributes should be allowed for mass updating. @@ -101,9 +102,23 @@ module ActionController cattr_accessor :permit_all_parameters, instance_accessor: false cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false - # Never raise an UnpermittedParameters exception because of these params - # are present. They are added by Rails and it's of no concern. - NEVER_UNPERMITTED_PARAMS = %w( controller action ) + # By default, never raise an UnpermittedParameters exception if these + # params are present. The default includes both 'controller' and 'action' + # because they are added by Rails and should be of no concern. One way + # to change these is to specify `always_permitted_parameters` in your + # config. For instance: + # + # config.always_permitted_parameters = %w( controller action format ) + cattr_accessor :always_permitted_parameters + self.always_permitted_parameters = %w( controller action ) + + def self.const_missing(const_name) + super unless const_name == :NEVER_UNPERMITTED_PARAMS + ActiveSupport::Deprecation.warn "`ActionController::Parameters::NEVER_UNPERMITTED_PARAMS`"\ + " has been deprecated. Use "\ + "`ActionController::Parameters.always_permitted_parameters` instead." + self.always_permitted_parameters + end # Returns a new instance of <tt>ActionController::Parameters</tt>. # Also, sets the +permitted+ attribute to the default value of @@ -129,6 +144,10 @@ module ActionController # Attribute that keeps track of converted arrays, if any, to avoid double # looping in the common use case permit + mass-assignment. Defined in a # method to instantiate it only if needed. + # + # Testing membership still loops, but it's going to be faster than our own + # loop that converts values. Also, we are not going to build a new array + # object per fetch. def converted_arrays @converted_arrays ||= Set.new end @@ -158,8 +177,8 @@ module ActionController def permit! each_pair do |key, value| value = convert_hashes_to_parameters(key, value) - Array.wrap(value).each do |_| - _.permit! if _.respond_to? :permit! + Array.wrap(value).each do |v| + v.permit! if v.respond_to? :permit! end end @@ -180,7 +199,12 @@ module ActionController # ActionController::Parameters.new(person: {}).require(:person) # # => ActionController::ParameterMissing: param not found: person def require(key) - self[key].presence || raise(ParameterMissing.new(key)) + value = self[key] + if value.present? || value == false + value + else + raise ParameterMissing.new(key) + end end # Alias of #require. @@ -380,7 +404,7 @@ module ActionController end def unpermitted_keys(params) - self.keys - params.keys - NEVER_UNPERMITTED_PARAMS + self.keys - params.keys - self.always_permitted_parameters end # diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index 37d4a96ee1..07265be3fe 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -23,12 +23,12 @@ module ActionController include AbstractController::UrlFor def url_options - @_url_options ||= super.reverse_merge( + @_url_options ||= { :host => request.host, :port => request.optional_port, :protocol => request.protocol, - :_recall => request.symbolized_path_parameters - ).freeze + :_recall => request.path_parameters + }.merge(super).freeze if (same_origin = _routes.equal?(env["action_dispatch.routes".freeze])) || (script_name = env["ROUTES_#{_routes.object_id}_SCRIPT_NAME"]) || diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index a2fc814221..28b20052b5 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -23,6 +23,10 @@ module ActionController options = app.config.action_controller ActionController::Parameters.permit_all_parameters = options.delete(:permit_all_parameters) { false } + if app.config.action_controller[:always_permitted_parameters] + ActionController::Parameters.always_permitted_parameters = + app.config.action_controller.delete(:always_permitted_parameters) + end ActionController::Parameters.action_on_unpermitted_parameters = options.delete(:action_on_unpermitted_parameters) do (Rails.env.test? || Rails.env.development?) ? :log : false end diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index caaebc537a..71cb224f22 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -12,11 +12,13 @@ module ActionController teardown :teardown_subscriptions end + RENDER_TEMPLATE_INSTANCE_VARIABLES = %w{partials templates layouts files}.freeze + def setup_subscriptions - @_partials = Hash.new(0) - @_templates = Hash.new(0) - @_layouts = Hash.new(0) - @_files = Hash.new(0) + RENDER_TEMPLATE_INSTANCE_VARIABLES.each do |instance_variable| + instance_variable_set("@_#{instance_variable}", Hash.new(0)) + end + @_subscribers = [] @_subscribers << ActiveSupport::Notifications.subscribe("render_template.action_view") do |_name, _start, _finish, _id, payload| @@ -30,25 +32,21 @@ module ActionController end @_subscribers << ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload| - path = payload[:virtual_path] - next unless path - partial = path =~ /^.*\/_[^\/]*$/ - - if partial - @_partials[path] += 1 - @_partials[path.split("/").last] += 1 - end - - @_templates[path] += 1 - end + if virtual_path = payload[:virtual_path] + partial = virtual_path =~ /^.*\/_[^\/]*$/ - @_subscribers << ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload| - next if payload[:virtual_path] # files don't have virtual path + if partial + @_partials[virtual_path] += 1 + @_partials[virtual_path.split("/").last] += 1 + end - path = payload[:identifier] - if path - @_files[path] += 1 - @_files[path.split("/").last] += 1 + @_templates[virtual_path] += 1 + else + path = payload[:identifier] + if path + @_files[path] += 1 + @_files[path.split("/").last] += 1 + end end end end @@ -60,12 +58,16 @@ module ActionController end def process(*args) - @_partials = Hash.new(0) - @_templates = Hash.new(0) - @_layouts = Hash.new(0) + reset_template_assertion super end + def reset_template_assertion + RENDER_TEMPLATE_INSTANCE_VARIABLES.each do |instance_variable| + instance_variable_get("@_#{instance_variable}").clear + end + end + # Asserts that the request was rendered with the appropriate template file or partials. # # # assert that the "new" view template was rendered @@ -199,7 +201,7 @@ module ActionController value = value.dup end - if extra_keys.include?(key.to_sym) + if extra_keys.include?(key) non_path_parameters[key] = value else if value.is_a?(Array) @@ -208,7 +210,7 @@ module ActionController value = value.to_param end - path_parameters[key.to_s] = value + path_parameters[key] = value end end @@ -233,7 +235,6 @@ module ActionController @formats = nil @env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ } @env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ } - @symbolized_path_params = nil @method = @request_method = nil @fullpath = @ip = @remote_ip = @protocol = nil @env['action_dispatch.request.query_parameters'] = {} @@ -550,6 +551,31 @@ module ActionController end end + # Simulate a HTTP request to +action+ by specifying request method, + # parameters and set/volley the response. + # + # - +action+: The controller action to call. + # - +http_method+: Request method used to send the http request. Possible values + # are +GET+, +POST+, +PATCH+, +PUT+, +DELETE+, +HEAD+. Defaults to +GET+. + # - +parameters+: The HTTP parameters. This may be +nil+, a hash, or a + # string that is appropriately encoded (+application/x-www-form-urlencoded+ + # or +multipart/form-data+). + # - +session+: A hash of parameters to store in the session. This may be +nil+. + # - +flash+: A hash of parameters to store in the flash. This may be +nil+. + # + # Example calling +create+ action and sending two params: + # + # process :create, 'POST', user: { name: 'Gaurish Sharma', email: 'user@example.com' } + # + # Example sending parameters, +nil+ session and setting a flash message: + # + # process :view, 'GET', { id: 7 }, nil, { notice: 'This is flash message' } + # + # To simulate +GET+, +POST+, +PATCH+, +PUT+, +DELETE+ and +HEAD+ requests + # prefer using #get, #post, #patch, #put, #delete and #head methods + # respectively which will make tests more expressive. + # + # Note that the request method is not verified. def process(action, http_method = 'GET', *args) check_required_ivars @@ -558,6 +584,7 @@ module ActionController end parameters, session, flash = args + parameters ||= {} # Ensure that numbers and symbols passed as params are converted to # proper params, as is the case when engaging rack. @@ -567,7 +594,6 @@ module ActionController unless @controller.respond_to?(:recycle!) @controller.extend(Testing::Functional) - @controller.class.class_eval { include Testing } end @request.recycle! @@ -576,7 +602,6 @@ module ActionController @request.env['REQUEST_METHOD'] = http_method - parameters ||= {} controller_class_name = @controller.class.anonymous? ? "anonymous" : @controller.class.controller_path @@ -604,8 +629,11 @@ module ActionController @response.prepare! @assigns = @controller.respond_to?(:view_assigns) ? @controller.view_assigns : {} - @request.session['flash'] = @request.flash.to_session_value - @request.session.delete('flash') if @request.session['flash'].blank? + + if flash_value = @request.flash.to_session_value + @request.session['flash'] = flash_value + end + @response end @@ -670,7 +698,7 @@ module ActionController :only_path => true, :action => action, :relative_url_root => nil, - :_recall => @request.symbolized_path_parameters) + :_recall => @request.path_parameters) url, query_string = @routes.url_for(options).split("?", 2) @@ -681,7 +709,7 @@ module ActionController end def html_format?(parameters) - return true unless parameters.is_a?(Hash) + return true unless parameters.key?(:format) Mime.fetch(parameters[:format]) { Mime['html'] }.html? end end diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index f9b278349e..63a3cbc90b 100644 --- a/actionpack/lib/action_dispatch/http/cache.rb +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -92,7 +92,7 @@ module ActionDispatch LAST_MODIFIED = "Last-Modified".freeze ETAG = "ETag".freeze CACHE_CONTROL = "Cache-Control".freeze - SPECIAL_KEYS = %w[extras no-cache max-age public must-revalidate] + SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public must-revalidate]) def cache_control_segments if cache_control = self[CACHE_CONTROL] diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb index 2666cd4b0a..bc5410dc38 100644 --- a/actionpack/lib/action_dispatch/http/headers.rb +++ b/actionpack/lib/action_dispatch/http/headers.rb @@ -1,34 +1,63 @@ module ActionDispatch module Http + # Provides access to the request's HTTP headers from the environment. + # + # env = { "CONTENT_TYPE" => "text/plain" } + # headers = ActionDispatch::Http::Headers.new(env) + # headers["Content-Type"] # => "text/plain" class Headers - CGI_VARIABLES = %w( - CONTENT_TYPE CONTENT_LENGTH - HTTPS AUTH_TYPE GATEWAY_INTERFACE - PATH_INFO PATH_TRANSLATED QUERY_STRING - REMOTE_ADDR REMOTE_HOST REMOTE_IDENT REMOTE_USER - REQUEST_METHOD SCRIPT_NAME - SERVER_NAME SERVER_PORT SERVER_PROTOCOL SERVER_SOFTWARE - ) + CGI_VARIABLES = Set.new(%W[ + AUTH_TYPE + CONTENT_LENGTH + CONTENT_TYPE + GATEWAY_INTERFACE + HTTPS + PATH_INFO + PATH_TRANSLATED + QUERY_STRING + REMOTE_ADDR + REMOTE_HOST + REMOTE_IDENT + REMOTE_USER + REQUEST_METHOD + SCRIPT_NAME + SERVER_NAME + SERVER_PORT + SERVER_PROTOCOL + SERVER_SOFTWARE + ]).freeze + HTTP_HEADER = /\A[A-Za-z0-9-]+\z/ include Enumerable attr_reader :env - def initialize(env = {}) + def initialize(env = {}) # :nodoc: @env = env end + # Returns the value for the given key mapped to @env. def [](key) @env[env_name(key)] end + # Sets the given value for the key mapped to @env. def []=(key, value) @env[env_name(key)] = value end - def key?(key); @env.key? key; end + def key?(key) + @env.key? env_name(key) + end alias :include? :key? + # Returns the value for the given key mapped to @env. + # + # If the key is not found and an optional code block is not provided, + # raises a <tt>KeyError</tt> exception. + # + # If the code block is provided, then it will be run and + # its result returned. def fetch(key, *args, &block) @env.fetch env_name(key), *args, &block end @@ -37,12 +66,17 @@ module ActionDispatch @env.each(&block) end + # Returns a new Http::Headers instance containing the contents of + # <tt>headers_or_env</tt> and the original instance. def merge(headers_or_env) headers = Http::Headers.new(env.dup) headers.merge!(headers_or_env) headers end + # Adds the contents of <tt>headers_or_env</tt> to original instance + # entries; duplicate keys are overwritten with the values from + # <tt>headers_or_env</tt>. def merge!(headers_or_env) headers_or_env.each do |key, value| self[env_name(key)] = value @@ -50,6 +84,8 @@ module ActionDispatch end private + # Converts a HTTP header name to an environment variable name if it is + # not contained within the headers hash. def env_name(key) key = key.to_s if key =~ HTTP_HEADER diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index 0b2b60d2e4..9c8f65deac 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -54,8 +54,14 @@ module ActionDispatch end def formats - @env["action_dispatch.request.formats"] ||= - if parameters[:format] + @env["action_dispatch.request.formats"] ||= begin + params_readable = begin + parameters[:format] + rescue ActionController::BadRequest + false + end + + if params_readable Array(Mime[parameters[:format]]) elsif use_accept_header && valid_accept_header accepts @@ -64,8 +70,8 @@ module ActionDispatch else [Mime::HTML] end + end end - # Sets the \variant for template. def variant=(variant) if variant.is_a?(Symbol) diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index dcb299ed03..20ae48d458 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -1,13 +1,11 @@ require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/hash/indifferent_access' +require 'active_support/deprecation' module ActionDispatch module Http module Parameters - def initialize(env) - super - @symbolized_path_params = nil - end + PARAMETERS_KEY = 'action_dispatch.request.path_parameters' # Returns both GET and POST \parameters in a single hash. def parameters @@ -18,55 +16,42 @@ module ActionDispatch query_parameters.dup end params.merge!(path_parameters) - params.with_indifferent_access end end alias :params :parameters def path_parameters=(parameters) #:nodoc: - @symbolized_path_params = nil - @env.delete("action_dispatch.request.parameters") - @env["action_dispatch.request.path_parameters"] = parameters + @env.delete('action_dispatch.request.parameters') + @env[PARAMETERS_KEY] = parameters end - # The same as <tt>path_parameters</tt> with explicitly symbolized keys. def symbolized_path_parameters - @symbolized_path_params ||= path_parameters.symbolize_keys + ActiveSupport::Deprecation.warn( + "`symbolized_path_parameters` is deprecated. Please use `path_parameters`" + ) + path_parameters end # Returns a hash with the \parameters used to form the \path of the request. # Returned hash keys are strings: # # {'action' => 'my_action', 'controller' => 'my_controller'} - # - # See <tt>symbolized_path_parameters</tt> for symbolized keys. def path_parameters - @env["action_dispatch.request.path_parameters"] ||= {} - end - - def reset_parameters #:nodoc: - @env.delete("action_dispatch.request.parameters") + @env[PARAMETERS_KEY] ||= {} end private - # Convert nested Hash to HashWithIndifferentAccess - # and UTF-8 encode both keys and values in nested Hash. + # Convert nested Hash to HashWithIndifferentAccess. # - # TODO: Validate that the characters are UTF-8. If they aren't, - # you'll get a weird error down the road, but our form handling - # should really prevent that from happening def normalize_encode_params(params) case params - when String - params.force_encoding(Encoding::UTF_8).encode! when Hash if params.has_key?(:tempfile) UploadedFile.new(params) else params.each_with_object({}) do |(key, val), new_hash| - new_key = key.is_a?(String) ? key.dup.force_encoding(Encoding::UTF_8).encode! : key - new_hash[new_key] = if val.is_a?(Array) + new_hash[key] = if val.is_a?(Array) val.map! { |el| normalize_encode_params(el) } else normalize_encode_params(val) diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index daa06e96e6..a519d6c1fc 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -23,7 +23,7 @@ module ActionDispatch autoload :Session, 'action_dispatch/request/session' autoload :Utils, 'action_dispatch/request/utils' - LOCALHOST = Regexp.union [/^127\.0\.0\.\d{1,3}$/, /^::1$/, /^0:0:0:0:0:0:0:1(%.*)?$/] + LOCALHOST = Regexp.union [/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, /^::1$/, /^0:0:0:0:0:0:0:1(%.*)?$/] ENV_METHODS = %w[ AUTH_TYPE GATEWAY_INTERFACE PATH_TRANSLATED REMOTE_HOST @@ -53,6 +53,17 @@ module ActionDispatch @uuid = nil end + def check_path_parameters! + # If any of the path parameters has an invalid encoding then + # raise since it's likely to trigger errors further on. + path_parameters.each do |key, value| + next unless value.respond_to?(:valid_encoding?) + unless value.valid_encoding? + raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}" + end + end + end + def key?(key) @env.key?(key) end @@ -64,6 +75,7 @@ module ActionDispatch # Ordered Collections Protocol (WebDAV) (http://www.ietf.org/rfc/rfc3648.txt) # Web Distributed Authoring and Versioning (WebDAV) Access Control Protocol (http://www.ietf.org/rfc/rfc3744.txt) # Web Distributed Authoring and Versioning (WebDAV) SEARCH (http://www.ietf.org/rfc/rfc5323.txt) + # Calendar Extensions to WebDAV (http://www.ietf.org/rfc/rfc4791.txt) # PATCH Method for HTTP (http://www.ietf.org/rfc/rfc5789.txt) RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT) RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK) @@ -71,9 +83,10 @@ module ActionDispatch RFC3648 = %w(ORDERPATCH) RFC3744 = %w(ACL) RFC5323 = %w(SEARCH) + RFC4791 = %w(MKCALENDAR) RFC5789 = %w(PATCH) - HTTP_METHODS = RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC5789 + HTTP_METHODS = RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789 HTTP_METHOD_LOOKUP = {} @@ -196,8 +209,8 @@ module ActionDispatch end # Returns true if the "X-Requested-With" header contains "XMLHttpRequest" - # (case-insensitive). All major JavaScript libraries send this header with - # every Ajax request. + # (case-insensitive), which may need to be manually added depending on the + # choice of JavaScript libraries and frameworks. def xml_http_request? @env['HTTP_X_REQUESTED_WITH'] =~ /XMLHttpRequest/i end @@ -278,7 +291,7 @@ module ActionDispatch # Override Rack's GET method to support indifferent access def GET - @env["action_dispatch.request.query_parameters"] ||= Utils.deep_munge((normalize_encode_params(super) || {})) + @env["action_dispatch.request.query_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {})) rescue TypeError => e raise ActionController::BadRequest.new(:query, e) end @@ -286,7 +299,7 @@ module ActionDispatch # Override Rack's POST method to support indifferent access def POST - @env["action_dispatch.request.request_parameters"] ||= Utils.deep_munge((normalize_encode_params(super) || {})) + @env["action_dispatch.request.request_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {})) rescue TypeError => e raise ActionController::BadRequest.new(:request, e) end diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 3d27ff2b24..2fab6be1a5 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -97,6 +97,9 @@ module ActionDispatch # :nodoc: x end + def abort + end + def close @response.commit! @closed = true @@ -207,18 +210,6 @@ module ActionDispatch # :nodoc: end alias_method :status_message, :message - def respond_to?(method, include_private = false) - if method.to_s == 'to_path' - stream.respond_to?(method) - else - super - end - end - - def to_path - stream.to_path - end - # Returns the content of the response as a string. This contains the contents # of any calls to <tt>render</tt>. def body @@ -271,6 +262,17 @@ module ActionDispatch # :nodoc: stream.close if stream.respond_to?(:close) end + def abort + if stream.respond_to?(:abort) + stream.abort + elsif stream.respond_to?(:close) + # `stream.close` should really be reserved for a close from the + # other direction, but we must fall back to it for + # compatibility. + stream.close + end + end + # Turns the Response into a Rack-compatible array of the status, headers, # and body. def to_a @@ -296,6 +298,9 @@ module ActionDispatch # :nodoc: cookies end + def _status_code + @status + end private def before_committed @@ -334,6 +339,38 @@ module ActionDispatch # :nodoc: !@sending_file && @charset != false end + class RackBody + def initialize(response) + @response = response + end + + def each(*args, &block) + @response.each(*args, &block) + end + + def close + # Rack "close" maps to Response#abort, and *not* Response#close + # (which is used when the controller's finished writing) + @response.abort + end + + def body + @response.body + end + + def respond_to?(method, include_private = false) + if method.to_s == 'to_path' + @response.stream.respond_to?(method) + else + super + end + end + + def to_path + @response.stream.to_path + end + end + def rack_response(status, header) assign_default_content_type_and_charset!(header) handle_conditional_get! @@ -344,7 +381,7 @@ module ActionDispatch # :nodoc: header.delete CONTENT_TYPE [status, header, []] else - [status, header, Rack::BodyProxy.new(self){}] + [status, header, RackBody.new(self)] end end end diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb index 45bf751d09..540e11a4a0 100644 --- a/actionpack/lib/action_dispatch/http/upload.rb +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -27,7 +27,8 @@ module ActionDispatch @tempfile = hash[:tempfile] raise(ArgumentError, ':tempfile is required') unless @tempfile - @original_filename = encode_filename(hash[:filename]) + @original_filename = hash[:filename] + @original_filename &&= @original_filename.encode "UTF-8" @content_type = hash[:type] @headers = hash[:head] end @@ -66,13 +67,6 @@ module ActionDispatch def eof? @tempfile.eof? end - - private - - def encode_filename(filename) - # Encode the filename in the utf8 encoding, unless it is nil - filename.force_encoding(Encoding::UTF_8).encode! if filename - end end end end diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index 6f5a52c568..473f692b05 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -5,99 +5,128 @@ module ActionDispatch module Http module URL IP_HOST_REGEXP = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ - HOST_REGEXP = /(^.*:\/\/)?([^:]+)(?::(\d+$))?/ + HOST_REGEXP = /(^[^:]+:\/\/)?([^:]+)(?::(\d+$))?/ PROTOCOL_REGEXP = /^([^:]+)(:)?(\/\/)?$/ mattr_accessor :tld_length self.tld_length = 1 class << self - def extract_domain(host, tld_length = @@tld_length) - host.split('.').last(1 + tld_length).join('.') if named_host?(host) + def extract_domain(host, tld_length) + extract_domain_from(host, tld_length) if named_host?(host) end - def extract_subdomains(host, tld_length = @@tld_length) + def extract_subdomains(host, tld_length) if named_host?(host) - parts = host.split('.') - parts[0..-(tld_length + 2)] + extract_subdomains_from(host, tld_length) else [] end end - def extract_subdomain(host, tld_length = @@tld_length) + def extract_subdomain(host, tld_length) extract_subdomains(host, tld_length).join('.') end - def url_for(options = {}) - options = options.dup - path = options.delete(:script_name).to_s.chomp("/") - path << options.delete(:path).to_s - - params = options[:params].is_a?(Hash) ? options[:params] : options.slice(:params) - params.reject! { |_,v| v.to_param.nil? } - - result = build_host_url(options) - if options[:trailing_slash] - if path.include?('?') - result << path.sub(/\?/, '/\&') - else - result << path.sub(/[^\/]\z|\A\z/, '\&/') - end + def url_for(options) + if options[:only_path] + path_for options else - result << path + full_url_for options end - result << "?#{params.to_query}" unless params.empty? - result << "##{Journey::Router::Utils.escape_fragment(options[:anchor].to_param.to_s)}" if options[:anchor] - result end - private + def full_url_for(options) + host = options[:host] + protocol = options[:protocol] + port = options[:port] - def build_host_url(options) - if options[:host].blank? && options[:only_path].blank? + unless host raise ArgumentError, 'Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true' end - result = "" + build_host_url(host, port, protocol, options, path_for(options)) + end + + def path_for(options) + result = options[:script_name].to_s.chomp("/") + result << options[:path].to_s + + result = add_trailing_slash(result) if options[:trailing_slash] - unless options[:only_path] - if match = options[:host].match(HOST_REGEXP) - options[:protocol] ||= match[1] unless options[:protocol] == false - options[:host] = match[2] - options[:port] = match[3] unless options.key?(:port) - end + result = add_params options, result + add_anchor options, result + end + + private - options[:protocol] = normalize_protocol(options) - options[:host] = normalize_host(options) - options[:port] = normalize_port(options) + def add_params(options, result) + if options.key? :params + param = options[:params] + params = param.is_a?(Hash) ? param : { params: param } - result << options[:protocol] - result << rewrite_authentication(options) - result << options[:host] - result << ":#{options[:port]}" if options[:port] + params.reject! { |_,v| v.to_param.nil? } + result << "?#{params.to_query}" unless params.empty? end result end - def named_host?(host) - host && IP_HOST_REGEXP !~ host + def add_anchor(options, result) + result << "##{Journey::Router::Utils.escape_fragment(options[:anchor].to_param.to_s)}" if options[:anchor] + result + end + + def extract_domain_from(host, tld_length) + host.split('.').last(1 + tld_length).join('.') end - def same_host?(options) - (options[:subdomain] == true || !options.key?(:subdomain)) && options[:domain].nil? + def extract_subdomains_from(host, tld_length) + parts = host.split('.') + parts[0..-(tld_length + 2)] end - def rewrite_authentication(options) + def add_trailing_slash(path) + # includes querysting + if path.include?('?') + path.sub!(/\?/, '/\&') + # does not have a .format + elsif !path.include?(".") + path.sub!(/[^\/]\z|\A\z/, '\&/') + end + + path + end + + def build_host_url(host, port, protocol, options, path) + if match = host.match(HOST_REGEXP) + protocol ||= match[1] unless protocol == false + host = match[2] + port = match[3] unless options.key? :port + end + + protocol = normalize_protocol protocol + host = normalize_host(host, options) + + result = protocol.dup + if options[:user] && options[:password] - "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@" - else - "" + result << "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@" end + + result << host + normalize_port(port, protocol) { |normalized_port| + result << ":#{normalized_port}" + } + + result.concat path end - def normalize_protocol(options) - case options[:protocol] + def named_host?(host) + IP_HOST_REGEXP !~ host + end + + def normalize_protocol(protocol) + case protocol when nil "http://" when false, "//" @@ -105,36 +134,39 @@ module ActionDispatch when PROTOCOL_REGEXP "#{$1}://" else - raise ArgumentError, "Invalid :protocol option: #{options[:protocol].inspect}" + raise ArgumentError, "Invalid :protocol option: #{protocol.inspect}" end end - def normalize_host(options) - return options[:host] if !named_host?(options[:host]) || same_host?(options) + def normalize_host(_host, options) + return _host unless named_host?(_host) tld_length = options[:tld_length] || @@tld_length + subdomain = options.fetch :subdomain, true + domain = options[:domain] host = "" - if options[:subdomain] == true || !options.key?(:subdomain) - host << extract_subdomain(options[:host], tld_length).to_param - elsif options[:subdomain].present? - host << options[:subdomain].to_param + if subdomain == true + return _host if domain.nil? + + host << extract_subdomains_from(_host, tld_length).join('.') + elsif subdomain + host << subdomain.to_param end host << "." unless host.empty? - host << (options[:domain] || extract_domain(options[:host], tld_length)) + host << (domain || extract_domain_from(_host, tld_length)) host end - def normalize_port(options) - return nil if options[:port].nil? || options[:port] == false + def normalize_port(port, protocol) + return unless port - case options[:protocol] - when "//" - nil + case protocol + when "//" then yield port when "https://" - options[:port].to_i == 443 ? nil : options[:port] + yield port unless port.to_i == 443 else - options[:port].to_i == 80 ? nil : options[:port] + yield port unless port.to_i == 80 end end end diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb index 57f0963731..59b353b1b7 100644 --- a/actionpack/lib/action_dispatch/journey/formatter.rb +++ b/actionpack/lib/action_dispatch/journey/formatter.rb @@ -12,12 +12,12 @@ module ActionDispatch @cache = nil end - def generate(type, name, options, recall = {}, parameterize = nil) - constraints = recall.merge(options) + def generate(name, options, path_parameters, parameterize = nil) + constraints = path_parameters.merge(options) missing_keys = [] match_route(name, constraints) do |route| - parameterized_parts = extract_parameterized_parts(route, options, recall, parameterize) + parameterized_parts = extract_parameterized_parts(route, options, path_parameters, parameterize) # Skip this route unless a name has been provided or it is a # standard Rails route since we can't determine whether an options @@ -30,6 +30,12 @@ module ActionDispatch parameterized_parts.key?(key) || route.defaults.key?(key) end + defaults = route.defaults + required_parts = route.required_parts + parameterized_parts.delete_if do |key, value| + value.to_s == defaults[key].to_s && !required_parts.include?(key) + end + return [route.format(parameterized_parts), params] end @@ -74,12 +80,12 @@ module ActionDispatch if named_routes.key?(name) yield named_routes[name] else - routes = non_recursive(cache, options.to_a) + routes = non_recursive(cache, options) hash = routes.group_by { |_, r| r.score(options) } hash.keys.sort.reverse_each do |score| - next if score < 0 + break if score < 0 hash[score].sort_by { |i, _| i }.each do |_, route| yield route @@ -90,14 +96,14 @@ module ActionDispatch def non_recursive(cache, options) routes = [] - stack = [cache] + queue = [cache] - while stack.any? - c = stack.shift + while queue.any? + c = queue.shift routes.concat(c[:___routes]) if c.key?(:___routes) options.each do |pair| - stack << c[pair] if c.key?(pair) + queue << c[pair] if c.key?(pair) end end @@ -126,11 +132,6 @@ module ActionDispatch } end - # Returns +true+ if no missing keys are present, otherwise +false+. - def verify_required_parts!(route, parts) - missing_keys(route, parts).empty? - end - def build_cache root = { ___routes: [] } routes.each_with_index do |route, i| diff --git a/actionpack/lib/action_dispatch/journey/nodes/node.rb b/actionpack/lib/action_dispatch/journey/nodes/node.rb index 935442ef66..bb01c087bc 100644 --- a/actionpack/lib/action_dispatch/journey/nodes/node.rb +++ b/actionpack/lib/action_dispatch/journey/nodes/node.rb @@ -93,6 +93,10 @@ module ActionDispatch class Star < Unary # :nodoc: def type; :STAR; end + + def name + left.name.tr '*:', '' + end end class Binary < Node # :nodoc: diff --git a/actionpack/lib/action_dispatch/journey/parser.rb b/actionpack/lib/action_dispatch/journey/parser.rb index 430812fafe..d129ba7e16 100644 --- a/actionpack/lib/action_dispatch/journey/parser.rb +++ b/actionpack/lib/action_dispatch/journey/parser.rb @@ -1,7 +1,7 @@ # # DO NOT MODIFY!!!! -# This file is automatically generated by Racc 1.4.9 -# from Racc grammar file "". +# This file is automatically generated by Racc 1.4.11 +# from Racc grammer file "". # require 'racc/parser.rb' @@ -9,42 +9,38 @@ require 'racc/parser.rb' require 'action_dispatch/journey/parser_extras' module ActionDispatch - module Journey # :nodoc: - class Parser < Racc::Parser # :nodoc: + module Journey + class Parser < Racc::Parser ##### State transition tables begin ### racc_action_table = [ - 17, 21, 13, 15, 14, 7, nil, 16, 8, 19, - 13, 15, 14, 7, 23, 16, 8, 19, 13, 15, - 14, 7, nil, 16, 8, 13, 15, 14, 7, nil, - 16, 8, 13, 15, 14, 7, nil, 16, 8 ] + 13, 15, 14, 7, 21, 16, 8, 19, 13, 15, + 14, 7, 17, 16, 8, 13, 15, 14, 7, 24, + 16, 8, 13, 15, 14, 7, 19, 16, 8 ] racc_action_check = [ - 1, 17, 1, 1, 1, 1, nil, 1, 1, 1, - 20, 20, 20, 20, 20, 20, 20, 20, 7, 7, - 7, 7, nil, 7, 7, 19, 19, 19, 19, nil, - 19, 19, 0, 0, 0, 0, nil, 0, 0 ] + 2, 2, 2, 2, 17, 2, 2, 2, 0, 0, + 0, 0, 1, 0, 0, 19, 19, 19, 19, 20, + 19, 19, 7, 7, 7, 7, 22, 7, 7 ] racc_action_pointer = [ - 30, 0, nil, nil, nil, nil, nil, 16, nil, nil, - nil, nil, nil, nil, nil, nil, nil, 1, nil, 23, - 8, nil, nil, nil ] + 6, 12, -2, nil, nil, nil, nil, 20, nil, nil, + nil, nil, nil, nil, nil, nil, nil, 4, nil, 13, + 13, nil, 17, nil, nil ] racc_action_default = [ - -18, -18, -2, -3, -4, -5, -6, -18, -9, -10, - -11, -12, -13, -14, -15, -16, -17, -18, -1, -18, - -18, 24, -8, -7 ] + -19, -19, -2, -3, -4, -5, -6, -19, -10, -11, + -12, -13, -14, -15, -16, -17, -18, -19, -1, -19, + -19, 25, -8, -9, -7 ] racc_goto_table = [ - 18, 1, nil, nil, nil, nil, nil, nil, 20, nil, - nil, nil, nil, nil, nil, nil, nil, nil, 22, 18 ] + 1, 22, 18, 23, nil, nil, nil, 20 ] racc_goto_check = [ - 2, 1, nil, nil, nil, nil, nil, nil, 1, nil, - nil, nil, nil, nil, nil, nil, nil, nil, 2, 2 ] + 1, 2, 1, 3, nil, nil, nil, 1 ] racc_goto_pointer = [ - nil, 1, -1, nil, nil, nil, nil, nil, nil, nil, + nil, 0, -18, -16, nil, nil, nil, nil, nil, nil, nil ] racc_goto_default = [ @@ -61,19 +57,20 @@ racc_reduce_table = [ 1, 12, :_reduce_none, 3, 15, :_reduce_7, 3, 13, :_reduce_8, - 1, 16, :_reduce_9, + 3, 13, :_reduce_9, + 1, 16, :_reduce_10, 1, 14, :_reduce_none, 1, 14, :_reduce_none, 1, 14, :_reduce_none, 1, 14, :_reduce_none, - 1, 19, :_reduce_14, - 1, 17, :_reduce_15, - 1, 18, :_reduce_16, - 1, 20, :_reduce_17 ] + 1, 19, :_reduce_15, + 1, 17, :_reduce_16, + 1, 18, :_reduce_17, + 1, 20, :_reduce_18 ] -racc_reduce_n = 18 +racc_reduce_n = 19 -racc_shift_n = 24 +racc_shift_n = 25 racc_token_table = { false => 0, @@ -137,12 +134,12 @@ Racc_debug_parser = false # reduce 0 omitted def _reduce_1(val, _values, result) - result = Cat.new(val.first, val.last) + result = Cat.new(val.first, val.last) result end def _reduce_2(val, _values, result) - result = val.first + result = val.first result end @@ -155,21 +152,24 @@ end # reduce 6 omitted def _reduce_7(val, _values, result) - result = Group.new(val[1]) + result = Group.new(val[1]) result end def _reduce_8(val, _values, result) - result = Or.new([val.first, val.last]) + result = Or.new([val.first, val.last]) result end def _reduce_9(val, _values, result) - result = Star.new(Symbol.new(val.last)) + result = Or.new([val.first, val.last]) result end -# reduce 10 omitted +def _reduce_10(val, _values, result) + result = Star.new(Symbol.new(val.last)) + result +end # reduce 11 omitted @@ -177,23 +177,25 @@ end # reduce 13 omitted -def _reduce_14(val, _values, result) - result = Slash.new('/') - result -end +# reduce 14 omitted def _reduce_15(val, _values, result) - result = Symbol.new(val.first) + result = Slash.new('/') result end def _reduce_16(val, _values, result) - result = Literal.new(val.first) + result = Symbol.new(val.first) result end def _reduce_17(val, _values, result) - result = Dot.new(val.first) + result = Literal.new(val.first) + result +end + +def _reduce_18(val, _values, result) + result = Dot.new(val.first) result end diff --git a/actionpack/lib/action_dispatch/journey/parser.y b/actionpack/lib/action_dispatch/journey/parser.y index 040f8d5922..0ead222551 100644 --- a/actionpack/lib/action_dispatch/journey/parser.y +++ b/actionpack/lib/action_dispatch/journey/parser.y @@ -4,7 +4,7 @@ token SLASH LITERAL SYMBOL LPAREN RPAREN DOT STAR OR rule expressions - : expressions expression { result = Cat.new(val.first, val.last) } + : expression expressions { result = Cat.new(val.first, val.last) } | expression { result = val.first } | or ; @@ -17,7 +17,8 @@ rule : LPAREN expressions RPAREN { result = Group.new(val[1]) } ; or - : expressions OR expression { result = Or.new([val.first, val.last]) } + : expression OR expression { result = Or.new([val.first, val.last]) } + | expression OR or { result = Or.new([val.first, val.last]) } ; star : STAR { result = Star.new(Symbol.new(val.last)) } diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb index fb155e516f..3af940a02f 100644 --- a/actionpack/lib/action_dispatch/journey/path/pattern.rb +++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb @@ -1,27 +1,20 @@ +require 'action_dispatch/journey/router/strexp' + module ActionDispatch module Journey # :nodoc: module Path # :nodoc: class Pattern # :nodoc: attr_reader :spec, :requirements, :anchored - def initialize(strexp) - parser = Journey::Parser.new - - @anchored = true + def self.from_string string + new Journey::Router::Strexp.build(string, {}, ["/.?"], true) + end - case strexp - when String - @spec = parser.parse(strexp) - @requirements = {} - @separators = "/.?" - when Router::Strexp - @spec = parser.parse(strexp.path) - @requirements = strexp.requirements - @separators = strexp.separators.join - @anchored = strexp.anchor - else - raise ArgumentError, "Bad expression: #{strexp}" - end + def initialize(strexp) + @spec = strexp.ast + @requirements = strexp.requirements + @separators = strexp.separators.join + @anchored = strexp.anchor @names = nil @optional_names = nil @@ -30,6 +23,10 @@ module ActionDispatch @offsets = nil end + def build_formatter + Visitors::FormatBuilder.new.accept(spec) + end + def ast @spec.grep(Nodes::Symbol).each do |node| re = @requirements[node.to_sym] diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb index 2b399d3ee3..9f0a3af902 100644 --- a/actionpack/lib/action_dispatch/journey/route.rb +++ b/actionpack/lib/action_dispatch/journey/route.rb @@ -16,14 +16,6 @@ module ActionDispatch @app = app @path = path - # Unwrap any constraints so we can see what's inside for route generation. - # This allows the formatter to skip over any mounted applications or redirects - # that shouldn't be matched when using a url_for without a route name. - while app.is_a?(Routing::Mapper::Constraints) do - app = app.app - end - @dispatcher = app.is_a?(Routing::RouteSet::Dispatcher) - @constraints = constraints @defaults = defaults @required_defaults = nil @@ -31,6 +23,7 @@ module ActionDispatch @parts = nil @decorated_ast = nil @precedence = 0 + @path_formatter = @path.build_formatter end def ast @@ -72,15 +65,7 @@ module ActionDispatch alias :segment_keys :parts def format(path_options) - path_options.delete_if do |key, value| - value.to_s == defaults[key].to_s && !required_parts.include?(key) - end - - Visitors::Formatter.new(path_options).accept(path.spec) - end - - def optimized_path - Visitors::OptimizedPath.new.accept(path.spec) + @path_formatter.evaluate path_options end def optional_parts @@ -106,7 +91,7 @@ module ActionDispatch end def dispatcher? - @dispatcher + @app.dispatcher? end def matches?(request) diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb index 36561c71a1..21817b374c 100644 --- a/actionpack/lib/action_dispatch/journey/router.rb +++ b/actionpack/lib/action_dispatch/journey/router.rb @@ -20,60 +20,32 @@ module ActionDispatch # :nodoc: VERSION = '2.0.0' - class NullReq # :nodoc: - attr_reader :env - def initialize(env) - @env = env - end - - def request_method - env['REQUEST_METHOD'] - end - - def path_info - env['PATH_INFO'] - end - - def ip - env['REMOTE_ADDR'] - end - - def [](k) - env[k] - end - end - - attr_reader :request_class, :formatter attr_accessor :routes - def initialize(routes, options) - @options = options - @params_key = options[:parameters_key] - @request_class = options[:request_class] || NullReq - @routes = routes + def initialize(routes) + @routes = routes end - def call(env) - env['PATH_INFO'] = Utils.normalize_path(env['PATH_INFO']) - - find_routes(env).each do |match, parameters, route| - script_name, path_info, set_params = env.values_at('SCRIPT_NAME', - 'PATH_INFO', - @params_key) + def serve(req) + find_routes(req).each do |match, parameters, route| + set_params = req.path_parameters + path_info = req.path_info + script_name = req.script_name unless route.path.anchored - env['SCRIPT_NAME'] = (script_name.to_s + match.to_s).chomp('/') - env['PATH_INFO'] = match.post_match + req.script_name = (script_name.to_s + match.to_s).chomp('/') + req.path_info = match.post_match + req.path_info = "/" + req.path_info unless req.path_info.start_with? "/" end - env[@params_key] = (set_params || {}).merge parameters + req.path_parameters = set_params.merge parameters - status, headers, body = route.app.call(env) + status, headers, body = route.app.serve(req) if 'pass' == headers['X-Cascade'] - env['SCRIPT_NAME'] = script_name - env['PATH_INFO'] = path_info - env[@params_key] = set_params + req.script_name = script_name + req.path_info = path_info + req.path_parameters = set_params next end @@ -83,14 +55,14 @@ module ActionDispatch return [404, {'X-Cascade' => 'pass'}, ['Not Found']] end - def recognize(req) - find_routes(req.env).each do |match, parameters, route| + def recognize(rails_req) + find_routes(rails_req).each do |match, parameters, route| unless route.path.anchored - req.env['SCRIPT_NAME'] = match.to_s - req.env['PATH_INFO'] = match.post_match.sub(/^([^\/])/, '/\1') + rails_req.script_name = match.to_s + rails_req.path_info = match.post_match.sub(/^([^\/])/, '/\1') end - yield(route, nil, parameters) + yield(route, parameters) end end @@ -124,29 +96,31 @@ module ActionDispatch simulator.memos(path) { [] } end - def find_routes env - req = request_class.new(env) - + def find_routes req routes = filter_routes(req.path_info).concat custom_routes.find_all { |r| r.path.match(req.path_info) } - routes.concat get_routes_as_head(routes) - routes.sort_by!(&:precedence).select! { |r| r.matches?(req) } + if req.env["REQUEST_METHOD"] === "HEAD" + routes.concat get_routes_as_head(routes) + end + + routes.select! { |r| r.matches?(req) } + routes.sort_by!(&:precedence) routes.map! { |r| match_data = r.path.match(req.path_info) - match_names = match_data.names.map { |n| n.to_sym } - match_values = match_data.captures.map { |v| v && Utils.unescape_uri(v) } - info = Hash[match_names.zip(match_values).find_all { |_, y| y }] - - [match_data, r.defaults.merge(info), r] + path_parameters = r.defaults.dup + match_data.names.zip(match_data.captures) { |name,val| + path_parameters[name.to_sym] = Utils.unescape_uri(val) if val + } + [match_data, path_parameters, r] } end def get_routes_as_head(routes) precedence = (routes.map(&:precedence).max || 0) + 1 - routes = routes.select { |r| + routes.select { |r| r.verb === "GET" && !(r.verb === "HEAD") }.map! { |r| Route.new(r.name, @@ -157,8 +131,6 @@ module ActionDispatch route.precedence = r.precedence + precedence end } - routes.flatten! - routes end end end diff --git a/actionpack/lib/action_dispatch/journey/router/strexp.rb b/actionpack/lib/action_dispatch/journey/router/strexp.rb index f97f1a223e..4b7738f335 100644 --- a/actionpack/lib/action_dispatch/journey/router/strexp.rb +++ b/actionpack/lib/action_dispatch/journey/router/strexp.rb @@ -6,18 +6,21 @@ module ActionDispatch alias :compile :new end - attr_reader :path, :requirements, :separators, :anchor + attr_reader :path, :requirements, :separators, :anchor, :ast - def initialize(path, requirements, separators, anchor = true) + def self.build(path, requirements, separators, anchor = true) + parser = Journey::Parser.new + ast = parser.parse path + new ast, path, requirements, separators, anchor + end + + def initialize(ast, path, requirements, separators, anchor = true) + @ast = ast @path = path @requirements = requirements @separators = separators @anchor = anchor end - - def names - @path.scan(/:\w+/).map { |s| s.tr(':', '') } - end end end end diff --git a/actionpack/lib/action_dispatch/journey/router/utils.rb b/actionpack/lib/action_dispatch/journey/router/utils.rb index ac4ecb1e65..2b0a6575d4 100644 --- a/actionpack/lib/action_dispatch/journey/router/utils.rb +++ b/actionpack/lib/action_dispatch/journey/router/utils.rb @@ -25,9 +25,10 @@ module ActionDispatch # http://tools.ietf.org/html/rfc3986 class UriEncoder # :nodoc: ENCODE = "%%%02X".freeze - ENCODING = Encoding::US_ASCII - EMPTY = "".force_encoding(ENCODING).freeze - DEC2HEX = (0..255).to_a.map{ |i| ENCODE % i }.map{ |s| s.force_encoding(ENCODING) } + US_ASCII = Encoding::US_ASCII + UTF_8 = Encoding::UTF_8 + EMPTY = "".force_encoding(US_ASCII).freeze + DEC2HEX = (0..255).to_a.map{ |i| ENCODE % i }.map{ |s| s.force_encoding(US_ASCII) } ALPHA = "a-zA-Z".freeze DIGIT = "0-9".freeze @@ -53,12 +54,13 @@ module ActionDispatch end def unescape_uri(uri) - uri.gsub(ESCAPED) { [$&[1, 2].hex].pack('C') }.force_encoding(uri.encoding) + encoding = uri.encoding == US_ASCII ? UTF_8 : uri.encoding + uri.gsub(ESCAPED) { [$&[1, 2].hex].pack('C') }.force_encoding(encoding) end protected def escape(component, pattern) - component.gsub(pattern){ |unsafe| percent_encode(unsafe) }.force_encoding(ENCODING) + component.gsub(pattern){ |unsafe| percent_encode(unsafe) }.force_encoding(US_ASCII) end def percent_encode(unsafe) diff --git a/actionpack/lib/action_dispatch/journey/visitors.rb b/actionpack/lib/action_dispatch/journey/visitors.rb index d9f634623d..52b4c8b489 100644 --- a/actionpack/lib/action_dispatch/journey/visitors.rb +++ b/actionpack/lib/action_dispatch/journey/visitors.rb @@ -1,14 +1,57 @@ # encoding: utf-8 -require 'thread_safe' - module ActionDispatch module Journey # :nodoc: + class Format + ESCAPE_PATH = ->(value) { Router::Utils.escape_path(value) } + ESCAPE_SEGMENT = ->(value) { Router::Utils.escape_segment(value) } + + class Parameter < Struct.new(:name, :escaper) + def escape(value); escaper.call value; end + end + + def self.required_path(symbol) + Parameter.new symbol, ESCAPE_PATH + end + + def self.required_segment(symbol) + Parameter.new symbol, ESCAPE_SEGMENT + end + + def initialize(parts) + @parts = parts + @children = [] + @parameters = [] + + parts.each_with_index do |object,i| + case object + when Journey::Format + @children << i + when Parameter + @parameters << i + end + end + end + + def evaluate(hash) + parts = @parts.dup + + @parameters.each do |index| + param = parts[index] + value = hash[param.name] + return ''.freeze unless value + parts[index] = param.escape value + end + + @children.each { |index| parts[index] = parts[index].evaluate(hash) } + + parts.join + end + end + module Visitors # :nodoc: class Visitor # :nodoc: - DISPATCH_CACHE = ThreadSafe::Cache.new { |h,k| - h[k] = :"visit_#{k}" - } + DISPATCH_CACHE = {} def accept(node) visit(node) @@ -38,11 +81,41 @@ module ActionDispatch def visit_STAR(n); unary(n); end def terminal(node); end - %w{ LITERAL SYMBOL SLASH DOT }.each do |t| - class_eval %{ def visit_#{t}(n); terminal(n); end }, __FILE__, __LINE__ + def visit_LITERAL(n); terminal(n); end + def visit_SYMBOL(n); terminal(n); end + def visit_SLASH(n); terminal(n); end + def visit_DOT(n); terminal(n); end + + private_instance_methods(false).each do |pim| + next unless pim =~ /^visit_(.*)$/ + DISPATCH_CACHE[$1.to_sym] = pim end end + class FormatBuilder < Visitor # :nodoc: + def accept(node); Journey::Format.new(super); end + def terminal(node); [node.left]; end + + def binary(node) + visit(node.left) + visit(node.right) + end + + def visit_GROUP(n); [Journey::Format.new(unary(n))]; end + + def visit_STAR(n) + [Journey::Format.required_path(n.left.to_sym)] + end + + def visit_SYMBOL(n) + symbol = n.to_sym + if symbol == :controller + [Journey::Format.required_path(symbol)] + else + [Journey::Format.required_segment(symbol)] + end + end + end + # Loop through the requirements AST class Each < Visitor # :nodoc: attr_reader :block @@ -52,8 +125,8 @@ module ActionDispatch end def visit(node) - super block.call(node) + super end end @@ -77,90 +150,6 @@ module ActionDispatch end end - class OptimizedPath < Visitor # :nodoc: - def accept(node) - Array(visit(node)) - end - - private - - def visit_CAT(node) - [visit(node.left), visit(node.right)].flatten - end - - def visit_SYMBOL(node) - node.left[1..-1].to_sym - end - - def visit_STAR(node) - visit(node.left) - end - - def visit_GROUP(node) - [] - end - - %w{ LITERAL SLASH DOT }.each do |t| - class_eval %{ def visit_#{t}(n); n.left; end }, __FILE__, __LINE__ - end - end - - # Used for formatting urls (url_for) - class Formatter < Visitor # :nodoc: - attr_reader :options - - def initialize(options) - @options = options - end - - private - def escape_path(value) - Router::Utils.escape_path(value) - end - - def escape_segment(value) - Router::Utils.escape_segment(value) - end - - def visit(node, optional = false) - case node.type - when :LITERAL, :SLASH, :DOT - node.left - when :STAR - visit_STAR(node.left) - when :GROUP - visit(node.left, true) - when :CAT - visit_CAT(node, optional) - when :SYMBOL - visit_SYMBOL(node, node.to_sym) - end - end - - def visit_CAT(node, optional) - left = visit(node.left, optional) - right = visit(node.right, optional) - - if optional && !(right && left) - "" - else - [left, right].join - end - end - - def visit_STAR(node) - if value = options[node.to_sym] - escape_path(value) - end - end - - def visit_SYMBOL(node, name) - if value = options[name] - name == :controller ? escape_path(value) : escape_segment(value) - end - end - end - class Dot < Visitor # :nodoc: def initialize @nodes = [] diff --git a/actionpack/lib/action_dispatch/journey/visualizer/index.html.erb b/actionpack/lib/action_dispatch/journey/visualizer/index.html.erb index 6aff10956a..9b28a65200 100644 --- a/actionpack/lib/action_dispatch/journey/visualizer/index.html.erb +++ b/actionpack/lib/action_dispatch/journey/visualizer/index.html.erb @@ -2,13 +2,13 @@ <html> <head> <title><%= title %></title> - <link rel="stylesheet" href="https://raw.github.com/gist/1706081/af944401f75ea20515a02ddb3fb43d23ecb8c662/reset.css" type="text/css"> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.css" type="text/css"> <style> <% stylesheets.each do |style| %> <%= style %> <% end %> </style> - <script src="https://raw.github.com/gist/1706081/df464722a05c3c2bec450b7b5c8240d9c31fa52d/d3.min.js" type="text/javascript"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.8/d3.min.js" type="text/javascript"></script> </head> <body> <div id="wrapper"> diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 22b16b628d..ac9e5effe2 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -289,8 +289,8 @@ module ActionDispatch end end - # Sets the cookie named +name+. The second argument may be the very cookie - # value, or a hash of options as documented above. + # Sets the cookie named +name+. The second argument may be the cookie's + # value or a hash of options as documented above. def []=(name, options) if options.is_a?(Hash) options.symbolize_keys! @@ -468,7 +468,7 @@ module ActionDispatch options = { :value => @verifier.generate(serialize(name, options)) } end - raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE + raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE @parent_jar[name] = options end @@ -526,7 +526,7 @@ module ActionDispatch options[:value] = @encryptor.encrypt_and_sign(serialize(name, options[:value])) - raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE + raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE @parent_jar[name] = options end diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index 4821d2a899..e90f8b9ce6 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -10,7 +10,7 @@ module ActionDispatch end end - # The flash provides a way to pass temporary objects between actions. Anything you place in the flash will be exposed + # The flash provides a way to pass temporary primitive-types (String, Array, Hash) between actions. Anything you place in the flash will be exposed # to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create # action that sets <tt>flash[:notice] = "Post successfully created"</tt> before redirecting to a display action that can # then expose the flash to its template. Actually, that exposure is automatically done. @@ -37,8 +37,11 @@ module ActionDispatch # flash.alert = "You must be logged in" # flash.notice = "Post successfully created" # - # This example just places a string in the flash, but you can put any object in there. And of course, you can put as - # many as you like at a time too. Just remember: They'll be gone by the time the next action has been performed. + # This example places a string in the flash. And of course, you can put as many as you like at a time too. If you want to pass + # non-primitive types, you will have to handle that in your application. Example: To show messages with links, you will have to + # use sanitize helper. + # + # Just remember: They'll be gone by the time the next action has been performed. # # See docs on the FlashHash class for more details about the flash. class Flash diff --git a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb index cbb2d475b1..6c8944e067 100644 --- a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb @@ -32,9 +32,8 @@ module ActionDispatch end def render_html(status) - found = false - path = "#{public_path}/#{status}.#{I18n.locale}.html" if I18n.locale - path = "#{public_path}/#{status}.html" unless path && (found = File.exist?(path)) + path = "#{public_path}/#{status}.#{I18n.locale}.html" + path = "#{public_path}/#{status}.html" unless (found = File.exist?(path)) if found || File.exist?(path) render_format(status, 'text/html', File.read(path)) diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb index c1df518b14..6a79b4e859 100644 --- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb +++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb @@ -11,7 +11,7 @@ module ActionDispatch # Some Rack servers concatenate repeated headers, like {HTTP RFC 2616}[http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2] # requires. Some Rack servers simply drop preceding headers, and only report # the value that was {given in the last header}[http://andre.arko.net/2011/12/26/repeated-headers-and-ruby-web-servers]. - # If you are behind multiple proxy servers (like Nginx to HAProxy to Unicorn) + # If you are behind multiple proxy servers (like NGINX to HAProxy to Unicorn) # then you should test your Rack server to make sure your data is good. # # IF YOU DON'T USE A PROXY, THIS MAKES YOU VULNERABLE TO IP SPOOFING. @@ -31,7 +31,7 @@ module ActionDispatch TRUSTED_PROXIES = %r{ ^127\.0\.0\.1$ | # localhost IPv4 ^::1$ | # localhost IPv6 - ^fc00: | # private IPv6 range fc00 + ^[fF][cCdD] | # private IPv6 range fc00::/7 ^10\. | # private IPv4 range 10.x.x.x ^172\.(1[6-9]|2[0-9]|3[0-1])\.| # private IPv4 range 172.16.0.0 .. 172.31.255.255 ^192\.168\. # private IPv4 range 192.168.x.x @@ -118,7 +118,7 @@ module ActionDispatch # # REMOTE_ADDR will be correct if the request is made directly against the # Ruby process, on e.g. Heroku. When the request is proxied by another - # server like HAProxy or Nginx, the IP address that made the original + # server like HAProxy or NGINX, the IP address that made the original # request will be put in an X-Forwarded-For header. If there are multiple # proxies, that header may contain a list of IPs. Other proxy services # set the Client-Ip header instead, so we check that too. diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index 0864e7ef2a..ed25c67ae5 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -49,7 +49,7 @@ module ActionDispatch # reasonably sure that your upgrade is otherwise complete. Additionally, # you should take care to make sure you are not relying on the ability to # decode signed cookies generated by your app in external applications or - # Javascript before upgrading. + # JavaScript before upgrading. # # Note that changing the secret key will invalidate all existing sessions! class CookieStore < Rack::Session::Abstract::ID diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index 1d4f0f89a6..f0779279c1 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -42,6 +42,7 @@ module ActionDispatch wrapper = ExceptionWrapper.new(env, exception) status = wrapper.status_code env["action_dispatch.exception"] = wrapper.exception + env["action_dispatch.original_path"] = env["PATH_INFO"] env["PATH_INFO"] = "/#{status}" response = @exceptions_app.call(env) response[1]['X-Cascade'] == 'pass' ? pass_response(status) : response diff --git a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb index cce0d75af4..6ffa242da4 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb @@ -148,8 +148,8 @@ // On key press perform a search for matching paths searchElem.onkeyup = function(e){ var userInput = searchElem.value, - defaultExactMatch = '<tr><th colspan="4">Paths Matching (' + sanitizePath(userInput) +'):</th></tr>', - defaultFuzzyMatch = '<tr><th colspan="4">Paths Containing (' + userInput +'):</th></tr>', + defaultExactMatch = '<tr><th colspan="4">Paths Matching (' + escape(sanitizePath(userInput)) +'):</th></tr>', + defaultFuzzyMatch = '<tr><th colspan="4">Paths Containing (' + escape(userInput) +'):</th></tr>', noExactMatch = '<tr><th colspan="4">No Exact Matches Found</th></tr>', noFuzzyMatch = '<tr><th colspan="4">No Fuzzy Matches Found</th></tr>'; diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index 9cd884daa3..ce03164ca9 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -1,6 +1,7 @@ # encoding: UTF-8 require 'active_support/core_ext/object/to_param' require 'active_support/core_ext/regexp' +require 'active_support/dependencies/autoload' module ActionDispatch # The routing module provides URL rewriting in native Ruby. It's a way to diff --git a/actionpack/lib/action_dispatch/routing/endpoint.rb b/actionpack/lib/action_dispatch/routing/endpoint.rb new file mode 100644 index 0000000000..88aa13c3e8 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/endpoint.rb @@ -0,0 +1,10 @@ +module ActionDispatch + module Routing + class Endpoint # :nodoc: + def dispatcher?; false; end + def redirect?; false; end + def matches?(req); true; end + def app; self; end + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index 71a0c5e826..ea3b2f419d 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -5,22 +5,15 @@ module ActionDispatch module Routing class RouteWrapper < SimpleDelegator def endpoint - rack_app ? rack_app.inspect : "#{controller}##{action}" + app.dispatcher? ? "#{controller}##{action}" : rack_app.inspect end def constraints requirements.except(:controller, :action) end - def rack_app(app = self.app) - @rack_app ||= begin - class_name = app.class.name.to_s - if class_name == "ActionDispatch::Routing::Mapper::Constraints" - rack_app(app.app) - elsif ActionDispatch::Routing::Redirect === app || class_name !~ /^ActionDispatch::Routing/ - app - end - end + def rack_app + app.app end def verb @@ -73,7 +66,7 @@ module ActionDispatch end def engine? - rack_app && rack_app.respond_to?(:routes) + rack_app.respond_to?(:routes) end end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 77718a14c1..32d963ba76 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -6,6 +6,8 @@ require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/module/remove_method' require 'active_support/inflector' require 'action_dispatch/routing/redirection' +require 'action_dispatch/routing/endpoint' +require 'active_support/deprecation' module ActionDispatch module Routing @@ -15,121 +17,190 @@ module ActionDispatch :controller, :action, :path_names, :constraints, :shallow, :blocks, :defaults, :options] - class Constraints #:nodoc: - def self.new(app, constraints, request = Rack::Request) - if constraints.any? - super(app, constraints, request) - else - app + class Constraints < Endpoint #:nodoc: + attr_reader :app, :constraints + + def initialize(app, constraints, dispatcher_p) + # Unwrap Constraints objects. I don't actually think it's possible + # to pass a Constraints object to this constructor, but there were + # multiple places that kept testing children of this object. I + # *think* they were just being defensive, but I have no idea. + if app.is_a?(self.class) + constraints += app.constraints + app = app.app end - end - attr_reader :app, :constraints + @dispatcher = dispatcher_p - def initialize(app, constraints, request) - @app, @constraints, @request = app, constraints, request + @app, @constraints, = app, constraints end - def matches?(env) - req = @request.new(env) + def dispatcher?; @dispatcher; end + def matches?(req) @constraints.all? do |constraint| (constraint.respond_to?(:matches?) && constraint.matches?(req)) || (constraint.respond_to?(:call) && constraint.call(*constraint_args(constraint, req))) end - ensure - req.reset_parameters end - def call(env) - matches?(env) ? @app.call(env) : [ 404, {'X-Cascade' => 'pass'}, [] ] + def serve(req) + return [ 404, {'X-Cascade' => 'pass'}, [] ] unless matches?(req) + + if dispatcher? + @app.serve req + else + @app.call req.env + end end private def constraint_args(constraint, request) - constraint.arity == 1 ? [request] : [request.symbolized_path_parameters, request] + constraint.arity == 1 ? [request] : [request.path_parameters, request] end end class Mapping #:nodoc: - IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix, :format] ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} - WILDCARD_PATH = %r{\*([^/\)]+)\)?$} - attr_reader :scope, :path, :options, :requirements, :conditions, :defaults + attr_reader :requirements, :conditions, :defaults + attr_reader :to, :default_controller, :default_action, :as, :anchor + + def self.build(scope, set, path, options) + options = scope[:options].merge(options) if scope[:options] - def initialize(set, scope, path, options) - @set, @scope, @path, @options = set, scope, path, options - @requirements, @conditions, @defaults = {}, {}, {} + options.delete :only + options.delete :except + options.delete :shallow_path + options.delete :shallow_prefix + options.delete :shallow - normalize_options! - normalize_path! - normalize_requirements! - normalize_conditions! - normalize_defaults! + defaults = (scope[:defaults] || {}).merge options.delete(:defaults) || {} + + new scope, set, path, defaults, options + end + + def initialize(scope, set, path, defaults, options) + @requirements, @conditions = {}, {} + @defaults = defaults + @set = set + + @to = options.delete :to + @default_controller = options.delete(:controller) || scope[:controller] + @default_action = options.delete(:action) || scope[:action] + @as = options.delete :as + @anchor = options.delete :anchor + + formatted = options.delete :format + via = Array(options.delete(:via) { [] }) + options_constraints = options.delete :constraints + + path = normalize_path! path, formatted + ast = path_ast path + path_params = path_params ast + + options = normalize_options!(options, formatted, path_params, ast, scope[:module]) + + + split_constraints(path_params, scope[:constraints]) if scope[:constraints] + constraints = constraints(options, path_params) + + split_constraints path_params, constraints + + @blocks = blocks(options_constraints, scope[:blocks]) + + if options_constraints.is_a?(Hash) + split_constraints path_params, options_constraints + options_constraints.each do |key, default| + if URL_OPTIONS.include?(key) && (String === default || Fixnum === default) + @defaults[key] ||= default + end + end + end + + normalize_format!(formatted) + + @conditions[:path_info] = path + @conditions[:parsed_path_info] = ast + + add_request_method(via, @conditions) + normalize_defaults!(options) end def to_route - [ app, conditions, requirements, defaults, options[:as], options[:anchor] ] + [ app(@blocks), conditions, requirements, defaults, as, anchor ] end private - def normalize_path! - raise ArgumentError, "path is required" if @path.blank? - @path = Mapper.normalize_path(@path) + def normalize_path!(path, format) + path = Mapper.normalize_path(path) - if required_format? - @path = "#{@path}.:format" - elsif optional_format? - @path = "#{@path}(.:format)" + if format == true + "#{path}.:format" + elsif optional_format?(path, format) + "#{path}(.:format)" + else + path end end - def required_format? - options[:format] == true - end - - def optional_format? - options[:format] != false && !path.include?(':format') && !path.end_with?('/') + def optional_format?(path, format) + format != false && !path.include?(':format') && !path.end_with?('/') end - def normalize_options! - @options.reverse_merge!(scope[:options]) if scope[:options] - path_without_format = path.sub(/\(\.:format\)$/, '') - + def normalize_options!(options, formatted, path_params, path_ast, modyoule) # Add a constraint for wildcard route to make it non-greedy and match the # optional format part of the route by default - if path_without_format.match(WILDCARD_PATH) && @options[:format] != false - @options[$1.to_sym] ||= /.+?/ + if formatted != false + path_ast.grep(Journey::Nodes::Star) do |node| + options[node.name.to_sym] ||= /.+?/ + end end - if path_without_format.match(':controller') - raise ArgumentError, ":controller segment is not allowed within a namespace block" if scope[:module] + if path_params.include?(:controller) + raise ArgumentError, ":controller segment is not allowed within a namespace block" if modyoule # Add a default constraint for :controller path segments that matches namespaced # controllers with default routes like :controller/:action/:id(.:format), e.g: # GET /admin/products/show/1 # => { controller: 'admin/products', action: 'show', id: '1' } - @options[:controller] ||= /.+?/ + options[:controller] ||= /.+?/ end - @options.merge!(default_controller_and_action) + if to.respond_to? :call + options + else + to_endpoint = split_to to + controller = to_endpoint[0] || default_controller + action = to_endpoint[1] || default_action + + controller = add_controller_module(controller, modyoule) + + options.merge! check_controller_and_action(path_params, controller, action) + end end - def normalize_requirements! - constraints.each do |key, requirement| - next unless segment_keys.include?(key) || key == :controller - verify_regexp_requirement(requirement) if requirement.is_a?(Regexp) - @requirements[key] = requirement + def split_constraints(path_params, constraints) + constraints.each_pair do |key, requirement| + if path_params.include?(key) || key == :controller + verify_regexp_requirement(requirement) if requirement.is_a?(Regexp) + @requirements[key] = requirement + else + @conditions[key] = requirement + end end + end - if options[:format] == true + def normalize_format!(formatted) + if formatted == true @requirements[:format] ||= /.+/ - elsif Regexp === options[:format] - @requirements[:format] = options[:format] - elsif String === options[:format] - @requirements[:format] = Regexp.compile(options[:format]) + elsif Regexp === formatted + @requirements[:format] = formatted + @defaults[:format] = nil + elsif String === formatted + @requirements[:format] = Regexp.compile(formatted) + @defaults[:format] = formatted end end @@ -143,169 +214,143 @@ module ActionDispatch end end - def normalize_defaults! - @defaults.merge!(scope[:defaults]) if scope[:defaults] - @defaults.merge!(options[:defaults]) if options[:defaults] - - options.each do |key, default| - unless Regexp === default || IGNORE_OPTIONS.include?(key) + def normalize_defaults!(options) + options.each_pair do |key, default| + unless Regexp === default @defaults[key] = default end end - - if options[:constraints].is_a?(Hash) - options[:constraints].each do |key, default| - if URL_OPTIONS.include?(key) && (String === default || Fixnum === default) - @defaults[key] ||= default - end - end - end - - if Regexp === options[:format] - @defaults[:format] = nil - elsif String === options[:format] - @defaults[:format] = options[:format] - end end - def normalize_conditions! - @conditions[:path_info] = path - - constraints.each do |key, condition| - unless segment_keys.include?(key) || key == :controller - @conditions[key] = condition - end + def verify_callable_constraint(callable_constraint) + unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?) + raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?" end + end - required_defaults = [] - options.each do |key, required_default| - 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 + def add_request_method(via, conditions) + return if via == [:all] - if !via_all && options[:via].blank? + if via.empty? msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \ "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \ "If you want to expose your action to GET, use `get` in the router:\n" \ " Instead of: match \"controller#action\"\n" \ " Do: get \"controller#action\"" - raise msg + raise ArgumentError, msg end - if via = options[:via] - @conditions[:request_method] = Array(via).map { |m| m.to_s.dasherize.upcase } - end + conditions[:request_method] = via.map { |m| m.to_s.dasherize.upcase } end - def app - Constraints.new(endpoint, blocks, @set.request_class) - end + def app(blocks) + return to if Redirect === to - def default_controller_and_action if to.respond_to?(:call) - { } + Constraints.new(to, blocks, false) else - if to.is_a?(String) - controller, action = to.split('#') - elsif to.is_a?(Symbol) - action = to.to_s - end - - controller ||= default_controller - action ||= default_action - - if @scope[:module] && !controller.is_a?(Regexp) - if controller =~ %r{\A/} - controller = controller[1..-1] - else - controller = [@scope[:module], controller].compact.join("/").presence - end - end - - if controller.is_a?(String) && controller =~ %r{\A/} - raise ArgumentError, "controller name should not start with a slash" + if blocks.any? + Constraints.new(dispatcher(defaults), blocks, true) + else + dispatcher(defaults) end + end + end - controller = controller.to_s unless controller.is_a?(Regexp) - action = action.to_s unless action.is_a?(Regexp) + def check_controller_and_action(path_params, controller, action) + hash = check_part(:controller, controller, path_params, {}) do |part| + translate_controller(part) { + message = "'#{part}' is not a supported controller name. This can lead to potential routing problems." + message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use" - if controller.blank? && segment_keys.exclude?(:controller) - message = "Missing :controller key on routes definition, please check your routes." raise ArgumentError, message - end + } + end - if action.blank? && segment_keys.exclude?(:action) - message = "Missing :action key on routes definition, please check your routes." - raise ArgumentError, message - end + check_part(:action, action, path_params, hash) { |part| + part.is_a?(Regexp) ? part : part.to_s + } + end - if controller.is_a?(String) && controller !~ /\A[a-z_0-9\/]*\z/ - message = "'#{controller}' is not a supported controller name. This can lead to potential routing problems." - message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use" + def check_part(name, part, path_params, hash) + if part + hash[name] = yield(part) + else + unless path_params.include?(name) + message = "Missing :#{name} key on routes definition, please check your routes." raise ArgumentError, message end - - hash = {} - hash[:controller] = controller unless controller.blank? - hash[:action] = action unless action.blank? - hash end - end - - def blocks - if options[:constraints].present? && !options[:constraints].is_a?(Hash) - [options[:constraints]] + hash + end + + def split_to(to) + case to + when Symbol + ActiveSupport::Deprecation.warn "defining a route where `to` is a symbol is deprecated. Please change \"to: :#{to}\" to \"action: :#{to}\"" + [nil, to.to_s] + when /#/ then to.split('#') + when String + ActiveSupport::Deprecation.warn "defining a route where `to` is a controller without an action is deprecated. Please change \"to: :#{to}\" to \"controller: :#{to}\"" + [to, nil] else - scope[:blocks] || [] + [] end end - def constraints - @constraints ||= {}.tap do |constraints| - constraints.merge!(scope[:constraints]) if scope[:constraints] - - options.except(*IGNORE_OPTIONS).each do |key, option| - constraints[key] = option if Regexp === option + def add_controller_module(controller, modyoule) + if modyoule && !controller.is_a?(Regexp) + if controller =~ %r{\A/} + controller[1..-1] + else + [modyoule, controller].compact.join("/") end - - constraints.merge!(options[:constraints]) if options[:constraints].is_a?(Hash) + else + controller end end - def segment_keys - @segment_keys ||= path_pattern.names.map{ |s| s.to_sym } - end + def translate_controller(controller) + return controller if Regexp === controller + return controller.to_s if controller =~ /\A[a-z_0-9][a-z_0-9\/]*\z/ - def path_pattern - Journey::Path::Pattern.new(strexp) - end - - def strexp - Journey::Router::Strexp.compile(path, requirements, SEPARATORS) + yield end - def endpoint - to.respond_to?(:call) ? to : dispatcher + def blocks(options_constraints, scope_blocks) + if options_constraints && !options_constraints.is_a?(Hash) + verify_callable_constraint(options_constraints) + [options_constraints] + else + scope_blocks || [] + end end - def dispatcher - Routing::RouteSet::Dispatcher.new(:defaults => defaults) + def constraints(options, path_params) + constraints = {} + required_defaults = [] + options.each_pair do |key, option| + if Regexp === option + constraints[key] = option + else + required_defaults << key unless path_params.include?(key) + end + end + @conditions[:required_defaults] = required_defaults + constraints end - def to - options[:to] + def path_params(ast) + ast.grep(Journey::Nodes::Symbol).map { |n| n.name.to_sym } end - def default_controller - options[:controller] || scope[:controller] + def path_ast(path) + parser = Journey::Parser.new + parser.parse path end - def default_action - options[:action] || scope[:action] + def dispatcher(defaults) + @set.dispatcher defaults end end @@ -340,18 +385,34 @@ module ActionDispatch match '/', { :as => :root, :via => :get }.merge!(options) end - # Matches a url pattern to one or more routes. Any symbols in a pattern - # are interpreted as url query parameters and thus available as +params+ - # in an action: + # Matches a url pattern to one or more routes. + # + # You should not use the `match` method in your router + # without specifying an HTTP method. + # + # If you want to expose your action to both GET and POST, use: # # # sets :controller, :action and :id in params - # match ':controller/:action/:id' + # match ':controller/:action/:id', via: [:get, :post] + # + # Note that +:controller+, +:action+ and +:id+ are interpreted as url + # query parameters and thus available through +params+ in an action. + # + # If you want to expose your action to GET, use `get` in the router: + # + # Instead of: + # + # match ":controller/:action/:id" + # + # Do: + # + # get ":controller/:action/:id" # # Two of these symbols are special, +:controller+ maps to the controller # and +:action+ to the controller's action. A pattern can also map # wildcard segments (globs) to params: # - # match 'songs/*category/:title', to: 'songs#show' + # get 'songs/*category/:title', to: 'songs#show' # # # 'songs/rock/classic/stairway-to-heaven' sets # # params[:category] = 'rock/classic' @@ -364,17 +425,17 @@ module ActionDispatch # When a pattern points to an internal route, the route's +:action+ and # +:controller+ should be set in options or hash shorthand. Examples: # - # match 'photos/:id' => 'photos#show' - # match 'photos/:id', to: 'photos#show' - # match 'photos/:id', controller: 'photos', action: 'show' + # match 'photos/:id' => 'photos#show', via: :get + # match 'photos/:id', to: 'photos#show', via: :get + # match 'photos/:id', controller: 'photos', action: 'show', via: :get # # A pattern can also point to a +Rack+ endpoint i.e. anything that # responds to +call+: # - # match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] } - # match 'photos/:id', to: PhotoRackApp + # match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] }, via: :get + # match 'photos/:id', to: PhotoRackApp, via: :get # # Yes, controller actions are just rack endpoints - # match 'photos/:id', to: PhotosController.action(:show) + # match 'photos/:id', to: PhotosController.action(:show), via: :get # # Because requesting various HTTP verbs with a single action has security # implications, you must either specify the actions in @@ -391,13 +452,19 @@ module ActionDispatch # [:action] # The route's action. # + # [:param] + # Overrides the default resource identifier `:id` (name of the + # dynamic segment used to generate the routes). + # You can access that segment from your controller using + # <tt>params[<:param>]</tt>. + # # [:path] # The path prefix for the routes. # # [:module] # The namespace for :controller. # - # match 'path', to: 'c#a', module: 'sekret', controller: 'posts' + # match 'path', to: 'c#a', module: 'sekret', controller: 'posts', via: :get # # => Sekret::PostsController # # See <tt>Scoping#namespace</tt> for its scope equivalent. @@ -416,9 +483,9 @@ module ActionDispatch # Points to a +Rack+ endpoint. Can be an object that responds to # +call+ or a string representing a controller's action. # - # match 'path', to: 'controller#action' - # match 'path', to: lambda { |env| [200, {}, ["Success!"]] } - # match 'path', to: RackApp + # match 'path', to: 'controller#action', via: :get + # match 'path', to: lambda { |env| [200, {}, ["Success!"]] }, via: :get + # match 'path', to: RackApp, via: :get # # [:on] # Shorthand for wrapping routes in a specific RESTful context. Valid @@ -443,14 +510,14 @@ module ActionDispatch # other than path can also be specified with any object # that responds to <tt>===</tt> (eg. String, Array, Range, etc.). # - # match 'path/:id', constraints: { id: /[A-Z]\d{5}/ } + # match 'path/:id', constraints: { id: /[A-Z]\d{5}/ }, via: :get # - # match 'json_only', constraints: { format: 'json' } + # match 'json_only', constraints: { format: 'json' }, via: :get # # class Whitelist # def matches?(request) request.remote_ip == '1.2.3.4' end # end - # match 'path', to: 'c#a', constraints: Whitelist.new + # match 'path', to: 'c#a', constraints: Whitelist.new, via: :get # # See <tt>Scoping#constraints</tt> for more examples with its scope # equivalent. @@ -459,7 +526,7 @@ module ActionDispatch # Sets defaults for parameters # # # Sets params[:format] to 'jpg' by default - # match 'path', to: 'c#a', defaults: { format: 'jpg' } + # match 'path', to: 'c#a', defaults: { format: 'jpg' }, via: :get # # See <tt>Scoping#defaults</tt> for its scope equivalent. # @@ -468,7 +535,7 @@ module ActionDispatch # false, the pattern matches any request prefixed with the given path. # # # Matches any request starting with 'path' - # match 'path', to: 'c#a', anchor: false + # match 'path', to: 'c#a', anchor: false, via: :get # # [:format] # Allows you to specify the default value for optional +format+ @@ -510,13 +577,21 @@ module ActionDispatch raise "A rack application must be specified" unless path - options[:as] ||= app_name(app) + rails_app = rails_app? app + + if rails_app + options[:as] ||= app.railtie_name + else + # non rails apps can't have an :as + options[:as] = nil + end + target_as = name_for_action(options[:as], path) options[:via] ||= :all match(path, options.merge(:to => app, :anchor => false, :format => false)) - define_generate_prefix(app, target_as) + define_generate_prefix(app, target_as) if rails_app self end @@ -537,35 +612,25 @@ module ActionDispatch end private - def app_name(app) - return unless app.respond_to?(:routes) - - if app.respond_to?(:railtie_name) - app.railtie_name - else - class_name = app.class.is_a?(Class) ? app.name : app.class.name - ActiveSupport::Inflector.underscore(class_name).tr("/", "_") - end + def rails_app?(app) + app.is_a?(Class) && app < Rails::Railtie end def define_generate_prefix(app, name) - return unless app.respond_to?(:routes) && app.routes.respond_to?(:define_mounted_helper) - _route = @set.named_routes.routes[name.to_sym] _routes = @set app.routes.define_mounted_helper(name) - app.routes.singleton_class.class_eval do - redefine_method :mounted? do - true - end - - redefine_method :_generate_prefix do |options| + app.routes.extend Module.new { + def mounted?; true; end + define_method :find_script_name do |options| + super(options) || begin prefix_options = options.slice(*_route.segment_keys) # we must actually delete prefix segment keys to avoid passing them to next url_for _route.segment_keys.each { |k| options.delete(k) } _routes.url_helpers.send("#{name}_path", prefix_options) + end end - end + } end end @@ -652,7 +717,7 @@ module ActionDispatch # resources :posts, module: "admin" # # If you want to route /admin/posts to +PostsController+ - # (without the Admin:: module prefix), you could use + # (without the <tt>Admin::</tt> module prefix), you could use # # scope "/admin" do # resources :posts @@ -1369,7 +1434,7 @@ module ActionDispatch end with_scope_level(:nested) do - if shallow? && shallow_nesting_depth > 1 + if shallow? && shallow_nesting_depth >= 1 shallow_scope(parent_resource.nested_scope, nested_options) { yield } else scope(parent_resource.nested_scope, nested_options) { yield } @@ -1403,7 +1468,20 @@ module ActionDispatch if rest.empty? && Hash === path options = path path, to = options.find { |name, _value| name.is_a?(String) } - options[:to] = to + + case to + when Symbol + options[:action] = to + when String + if to =~ /#/ + options[:to] = to + else + options[:controller] = to + end + else + options[:to] = to + end + options.delete(path) paths = [path] else @@ -1457,6 +1535,8 @@ module ActionDispatch def add_route(action, options) # :nodoc: path = path_for_action(action, options.delete(:path)) + raise ArgumentError, "path is required" if path.blank? + action = action.to_s.dup if action =~ /^[\w\-\/]+$/ @@ -1465,13 +1545,13 @@ module ActionDispatch action = nil end - if !options.fetch(:as, true) + if !options.fetch(:as, true) # if it's set to nil or false options.delete(:as) else options[:as] = name_for_action(options[:as], action) end - mapping = Mapping.new(@set, @scope, URI.parser.escape(path), options) + mapping = Mapping.build(@scope, @set, URI.parser.escape(path), options) app, conditions, requirements, defaults, as, anchor = mapping.to_route @set.add_route(app, conditions, requirements, defaults, as, anchor) end diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index cfd33d1f31..bd3696cda1 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -101,55 +101,45 @@ module ActionDispatch # polymorphic_url(Comment) # same as comments_url() # def polymorphic_url(record_or_hash_or_array, options = {}) - recipient = self - - if record_or_hash_or_array.kind_of?(Array) - record_or_hash_or_array = record_or_hash_or_array.compact - if record_or_hash_or_array.first.is_a?(ActionDispatch::Routing::RoutesProxy) - recipient = record_or_hash_or_array.shift - end - record_or_hash_or_array = record_or_hash_or_array[0] if record_or_hash_or_array.size == 1 + if Hash === record_or_hash_or_array + options = record_or_hash_or_array.merge(options) + record = options.delete :id + return polymorphic_url record, options end - record = extract_record(record_or_hash_or_array) - record = convert_to_model(record) - - args = Array === record_or_hash_or_array ? - record_or_hash_or_array.dup : - [ record_or_hash_or_array ] - - inflection = if options[:action] && options[:action].to_s == "new" - args.pop - :singular - elsif (record.respond_to?(:persisted?) && !record.persisted?) - args.pop - :plural - elsif record.is_a?(Class) - args.pop - :plural - else - :singular - end + opts = options.dup + action = opts.delete :action + type = opts.delete(:routing_type) || :url - args.delete_if {|arg| arg.is_a?(Symbol) || arg.is_a?(String)} - named_route = build_named_route_call(record_or_hash_or_array, record, inflection, options) + HelperMethodBuilder.polymorphic_method self, + record_or_hash_or_array, + action, + type, + opts - url_options = options.except(:action, :routing_type) - unless url_options.empty? - args.last.kind_of?(Hash) ? args.last.merge!(url_options) : args << url_options - end - - args.collect! { |a| convert_to_model(a) } - - recipient.send(named_route, *args) end # Returns the path component of a URL for the given record. It uses # <tt>polymorphic_url</tt> with <tt>routing_type: :path</tt>. def polymorphic_path(record_or_hash_or_array, options = {}) - polymorphic_url(record_or_hash_or_array, options.merge(:routing_type => :path)) + if Hash === record_or_hash_or_array + options = record_or_hash_or_array.merge(options) + record = options.delete :id + return polymorphic_path record, options + end + + opts = options.dup + action = opts.delete :action + type = :path + + HelperMethodBuilder.polymorphic_method self, + record_or_hash_or_array, + action, + type, + opts end + %w(edit new).each do |action| module_eval <<-EOT, __FILE__, __LINE__ + 1 def #{action}_polymorphic_url(record_or_hash, options = {}) # def edit_polymorphic_url(record_or_hash, options = {}) @@ -167,53 +157,169 @@ module ActionDispatch end private - def action_prefix(options) - options[:action] ? "#{options[:action]}_" : '' + + class HelperMethodBuilder # :nodoc: + CACHE = { 'path' => {}, 'url' => {} } + + def self.get(action, type) + type = type.to_s + CACHE[type].fetch(action) { build action, type } end - def routing_type(options) - options[:routing_type] || :url + def self.url; CACHE['url'.freeze][nil]; end + def self.path; CACHE['path'.freeze][nil]; end + + def self.build(action, type) + prefix = action ? "#{action}_" : "" + suffix = type + if action.to_s == 'new' + HelperMethodBuilder.singular prefix, suffix + else + HelperMethodBuilder.plural prefix, suffix + end end - def build_named_route_call(records, record, inflection, options = {}) - if records.is_a?(Array) - record = records.pop - route = records.map do |parent| - if parent.is_a?(Symbol) || parent.is_a?(String) - parent - else - model_name_from_record_or_class(parent).singular_route_key - end + def self.singular(prefix, suffix) + new(->(name) { name.singular_route_key }, prefix, suffix) + end + + def self.plural(prefix, suffix) + new(->(name) { name.route_key }, prefix, suffix) + end + + def self.polymorphic_method(recipient, record_or_hash_or_array, action, type, options) + builder = get action, type + + case record_or_hash_or_array + when Array + if record_or_hash_or_array.empty? || record_or_hash_or_array.include?(nil) + raise ArgumentError, "Nil location provided. Can't build URI." end + if record_or_hash_or_array.first.is_a?(ActionDispatch::Routing::RoutesProxy) + recipient = record_or_hash_or_array.shift + end + + method, args = builder.handle_list record_or_hash_or_array + when String, Symbol + method, args = builder.handle_string record_or_hash_or_array + when Class + method, args = builder.handle_class record_or_hash_or_array + + when nil + raise ArgumentError, "Nil location provided. Can't build URI." + else + method, args = builder.handle_model record_or_hash_or_array + end + + + if options.empty? + recipient.send(method, *args) else - route = [] + recipient.send(method, *args, options) end + end + + attr_reader :suffix, :prefix - if record.is_a?(Symbol) || record.is_a?(String) - route << record - elsif record - if inflection == :singular - route << model_name_from_record_or_class(record).singular_route_key + def initialize(key_strategy, prefix, suffix) + @key_strategy = key_strategy + @prefix = prefix + @suffix = suffix + end + + def handle_string(record) + [get_method_for_string(record), []] + end + + def handle_string_call(target, str) + target.send get_method_for_string str + end + + def handle_class(klass) + [get_method_for_class(klass), []] + end + + def handle_class_call(target, klass) + target.send get_method_for_class klass + end + + def handle_model(record) + args = [] + + model = record.to_model + name = if record.persisted? + args << model + model.class.model_name.singular_route_key + else + @key_strategy.call model.class.model_name + end + + named_route = prefix + "#{name}_#{suffix}" + + [named_route, args] + end + + def handle_model_call(target, model) + method, args = handle_model model + target.send(method, *args) + end + + def handle_list(list) + record_list = list.dup + record = record_list.pop + + args = [] + + route = record_list.map { |parent| + case parent + when Symbol, String + parent.to_s + when Class + args << parent + parent.model_name.singular_route_key else - route << model_name_from_record_or_class(record).route_key + args << parent.to_model + parent.to_model.class.model_name.singular_route_key end + } + + route << + case record + when Symbol, String + record.to_s + when Class + @key_strategy.call record.model_name else - raise ArgumentError, "Nil location provided. Can't build URI." + if record.persisted? + args << record.to_model + record.to_model.class.model_name.singular_route_key + else + @key_strategy.call record.to_model.class.model_name + end end - route << routing_type(options) + route << suffix - action_prefix(options) + route.join("_") + named_route = prefix + route.join("_") + [named_route, args] end - def extract_record(record_or_hash_or_array) - case record_or_hash_or_array - when Array; record_or_hash_or_array.last - when Hash; record_or_hash_or_array[:id] - else record_or_hash_or_array - end + private + + def get_method_for_class(klass) + name = @key_strategy.call klass.model_name + prefix + "#{name}_#{suffix}" + end + + def get_method_for_string(str) + prefix + "#{str}_#{suffix}" end + + [nil, 'new', 'edit'].each do |action| + CACHE['url'][action] = build action, 'url' + CACHE['path'][action] = build action, 'path' + end + end end end end - diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb index b08e62543b..3c1c4fadf6 100644 --- a/actionpack/lib/action_dispatch/routing/redirection.rb +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -3,10 +3,11 @@ require 'active_support/core_ext/uri' require 'active_support/core_ext/array/extract_options' require 'rack/utils' require 'action_controller/metal/exceptions' +require 'action_dispatch/routing/endpoint' module ActionDispatch module Routing - class Redirect # :nodoc: + class Redirect < Endpoint # :nodoc: attr_reader :status, :block def initialize(status, block) @@ -14,18 +15,15 @@ module ActionDispatch @block = block end - def call(env) - req = Request.new(env) + def redirect?; true; end - # 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? - raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}" - end - end + def call(env) + serve Request.new env + end - uri = URI.parse(path(req.symbolized_path_parameters, req)) + def serve(req) + req.check_path_parameters! + uri = URI.parse(path(req.path_parameters, req)) unless uri.host if relative_path?(uri.path) @@ -39,7 +37,7 @@ module ActionDispatch uri.host ||= req.host uri.port ||= req.port unless req.standard_port? - body = %(<html><body>You are being <a href="#{ERB::Util.h(uri.to_s)}">redirected</a>.</body></html>) + body = %(<html><body>You are being <a href="#{ERB::Util.unwrapped_html_escape(uri.to_s)}">redirected</a>.</body></html>) headers = { 'Location' => uri.to_s, diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 1ec6fa674b..80c705608d 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -1,11 +1,14 @@ require 'action_dispatch/journey' require 'forwardable' require 'thread_safe' +require 'active_support/concern' require 'active_support/core_ext/object/to_query' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/module/remove_method' require 'active_support/core_ext/array/extract_options' require 'action_controller/metal/exceptions' +require 'action_dispatch/http/request' +require 'action_dispatch/routing/endpoint' module ActionDispatch module Routing @@ -16,27 +19,17 @@ module ActionDispatch # alias inspect to to_s. alias inspect to_s - PARAMETERS_KEY = 'action_dispatch.request.path_parameters' - - class Dispatcher #:nodoc: - def initialize(options={}) - @defaults = options[:defaults] - @glob_param = options.delete(:glob) + class Dispatcher < Routing::Endpoint #:nodoc: + def initialize(defaults) + @defaults = defaults @controller_class_names = ThreadSafe::Cache.new end - def call(env) - params = env[PARAMETERS_KEY] + def dispatcher?; true; end - # 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?) - - unless value.valid_encoding? - raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}" - end - end + def serve(req) + req.check_path_parameters! + params = req.path_parameters prepare_params!(params) @@ -45,13 +38,12 @@ module ActionDispatch return [404, {'X-Cascade' => 'pass'}, []] end - dispatch(controller, params[:action], env) + dispatch(controller, params[:action], req.env) end def prepare_params!(params) normalize_controller!(params) merge_default_action!(params) - split_glob_param!(params) if @glob_param end # If this is a default_controller (i.e. a controller specified by the user) @@ -87,10 +79,6 @@ module ActionDispatch def merge_default_action!(params) params[:action] ||= 'index' end - - def split_glob_param!(params) - params[@glob_param] = params[@glob_param].split('/').map { |v| URI.parser.unescape(v) } - end end # A NamedRouteCollection instance is a collection of named routes, and also @@ -146,35 +134,34 @@ module ActionDispatch end class UrlHelper # :nodoc: - def self.create(route, options) + def self.create(route, options, route_name, url_strategy) if optimize_helper?(route) - OptimizedUrlHelper.new(route, options) + OptimizedUrlHelper.new(route, options, route_name, url_strategy) else - new route, options + new route, options, route_name, url_strategy end end def self.optimize_helper?(route) - !route.glob? && route.requirements.except(:controller, :action).empty? + !route.glob? && route.path.requirements.empty? end + attr_reader :url_strategy, :route_name + class OptimizedUrlHelper < UrlHelper # :nodoc: attr_reader :arg_size - def initialize(route, options) + def initialize(route, options, route_name, url_strategy) super - @klass = Journey::Router::Utils @required_parts = @route.required_parts @arg_size = @required_parts.size - @optimized_path = @route.optimized_path end - def call(t, args) - if args.size == arg_size && !args.last.is_a?(Hash) && optimize_routes_generation?(t) - options = @options.dup - options.merge!(t.url_options) if t.respond_to?(:url_options) + def call(t, args, inner_options) + if args.size == arg_size && !inner_options && optimize_routes_generation?(t) + options = t.url_options.merge @options options[:path] = optimized_helper(args) - ActionDispatch::Http::URL.url_for(options) + url_strategy.call options else super end @@ -183,18 +170,14 @@ module ActionDispatch private def optimized_helper(args) - params = Hash[parameterize_args(args)] + params = parameterize_args(args) missing_keys = missing_keys(params) unless missing_keys.empty? raise_generation_error(params, missing_keys) end - @optimized_path.map{ |segment| replace_segment(params, segment) }.join - end - - def replace_segment(params, segment) - Symbol === segment ? @klass.escape_segment(params[segment]) : segment + @route.format params end def optimize_routes_generation?(t) @@ -202,7 +185,9 @@ module ActionDispatch end def parameterize_args(args) - @required_parts.zip(args.map(&:to_param)) + params = {} + @required_parts.zip(args.map(&:to_param)) { |k,v| params[k] = v } + params end def missing_keys(args) @@ -218,27 +203,36 @@ module ActionDispatch end end - def initialize(route, options) + def initialize(route, options, route_name, url_strategy) @options = options @segment_keys = route.segment_keys.uniq @route = route + @url_strategy = url_strategy + @route_name = route_name end - def call(t, args) - t.url_for(handle_positional_args(t, args, @options, @segment_keys)) + def call(t, args, inner_options) + controller_options = t.url_options + options = controller_options.merge @options + hash = handle_positional_args(controller_options, + inner_options || {}, + args, + options, + @segment_keys) + + t._routes.url_for(hash, route_name, url_strategy) end - def handle_positional_args(t, args, options, keys) - inner_options = args.extract_options! - result = options.dup + def handle_positional_args(controller_options, inner_options, args, result, path_params) if args.size > 0 - if args.size < keys.size - 1 # take format into account - keys -= t.url_options.keys if t.respond_to?(:url_options) - keys -= options.keys + if args.size < path_params.size - 1 # take format into account + path_params -= controller_options.keys + path_params -= result.keys end - keys -= inner_options.keys - result.merge!(Hash[keys.zip(args)]) + path_params.each { |param| + result[param] = inner_options[param] || args.shift + } end result.merge!(inner_options) @@ -259,13 +253,15 @@ module ActionDispatch # # foo_url(bar, baz, bang, sort_by: 'baz') # - def define_url_helper(route, name, options) - helper = UrlHelper.create(route, options.dup) + def define_url_helper(route, name, opts, route_key, url_strategy) + helper = UrlHelper.create(route, opts, route_key, url_strategy) @module.remove_possible_method name @module.module_eval do define_method(name) do |*args| - helper.call self, args + options = nil + options = args.pop if args.last.is_a? Hash + helper.call self, args, options end end @@ -273,13 +269,18 @@ module ActionDispatch end def define_named_route_methods(name, route) - define_url_helper route, :"#{name}_path", - route.defaults.merge(:use_route => name, :only_path => true) - define_url_helper route, :"#{name}_url", - route.defaults.merge(:use_route => name, :only_path => false) + define_url_helper route, :"#{name}_path", route.defaults, name, PATH + define_url_helper route, :"#{name}_url", route.defaults, name, FULL end end + # :stopdoc: + # strategy for building urls to send to the client + PATH = ->(options) { ActionDispatch::Http::URL.path_for(options) } + FULL = ->(options) { ActionDispatch::Http::URL.full_url_for(options) } + UNKNOWN = ->(options) { ActionDispatch::Http::URL.url_for(options) } + # :startdoc: + attr_accessor :formatter, :set, :named_routes, :default_scope, :router attr_accessor :disable_clear_and_finalize, :resources_path_names attr_accessor :default_url_options, :request_class @@ -302,9 +303,7 @@ module ActionDispatch @finalized = false @set = Journey::Routes.new - @router = Journey::Router.new(@set, { - :parameters_key => PARAMETERS_KEY, - :request_class => request_class}) + @router = Journey::Router.new @set @formatter = Journey::Formatter.new @set end @@ -350,6 +349,10 @@ module ActionDispatch @prepend.each { |blk| eval_block(blk) } end + def dispatcher(defaults) + Routing::RouteSet::Dispatcher.new(defaults) + end + module MountedHelpers #:nodoc: extend ActiveSupport::Concern include UrlFor @@ -393,6 +396,8 @@ module ActionDispatch @_routes = routes class << self delegate :url_for, :optimize_routes_generation?, :to => '@_routes' + attr_reader :_routes + def url_options; {}; end end # Make named_routes available in the module singleton @@ -432,7 +437,9 @@ module ActionDispatch "http://guides.rubyonrails.org/routing.html#restricting-the-routes-created" end - path = build_path(conditions.delete(:path_info), requirements, SEPARATORS, anchor) + path = conditions.delete :path_info + ast = conditions.delete :parsed_path_info + path = build_path(path, ast, requirements, anchor) conditions = build_conditions(conditions, path.names.map { |x| x.to_sym }) route = @set.add_route(app, path, conditions, defaults, name) @@ -440,8 +447,9 @@ module ActionDispatch route end - def build_path(path, requirements, separators, anchor) + def build_path(path, ast, requirements, anchor) strexp = Journey::Router::Strexp.new( + ast, path, requirements, SEPARATORS, @@ -502,8 +510,8 @@ module ActionDispatch attr_reader :options, :recall, :set, :named_route - def initialize(options, recall, set) - @named_route = options.delete(:use_route) + def initialize(named_route, options, recall, set) + @named_route = named_route @options = options.dup @recall = recall.dup @set = set @@ -594,7 +602,7 @@ module ActionDispatch # Generates a path from routes, returns [path, params]. # If no route is generated the formatter will raise ActionController::UrlGenerationError def generate - @set.formatter.generate(:path_info, named_route, options, recall, PARAMETERIZE) + @set.formatter.generate(named_route, options, recall, PARAMETERIZE) end def different_controller? @@ -619,13 +627,15 @@ module ActionDispatch end def generate_extras(options, recall={}) - path, params = generate(options, recall) + route_key = options.delete :use_route + path, params = generate(route_key, options, recall) return path, params.keys end - def generate(options, recall = {}) - Generator.new(options, recall, self).generate + def generate(route_key, options, recall = {}) + Generator.new(route_key, options, recall, self).generate end + private :generate RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length, :trailing_slash, :anchor, :params, :only_path, :script_name, @@ -639,41 +649,52 @@ module ActionDispatch !mounted? && default_url_options.empty? end - def _generate_prefix(options = {}) - nil + def find_script_name(options) + options.delete :script_name end - # The +options+ argument must be +nil+ or a hash whose keys are *symbols*. - def url_for(options) - options = default_url_options.merge(options || {}) + # The +options+ argument must be a hash whose keys are *symbols*. + def url_for(options, route_name = nil, url_strategy = UNKNOWN) + options = default_url_options.merge options + + user = password = nil + + if options[:user] && options[:password] + user = options.delete :user + password = options.delete :password + end - user, password = extract_authentication(options) - recall = options.delete(:_recall) + recall = options.delete(:_recall) { {} } - original_script_name = options.delete(:original_script_name).presence - script_name = options.delete(:script_name).presence || _generate_prefix(options) + original_script_name = options.delete(:original_script_name) + script_name = find_script_name options if script_name && original_script_name script_name = original_script_name + script_name end - path_options = options.except(*RESERVED_OPTIONS) - path_options = yield(path_options) if block_given? + path_options = options.dup + RESERVED_OPTIONS.each { |ro| path_options.delete ro } + + path, params = generate(route_name, path_options, recall) + + if options.key? :params + params.merge! options[:params] + end - path, params = generate(path_options, recall || {}) - params.merge!(options[:params] || {}) + options[:path] = path + options[:script_name] = script_name + options[:params] = params + options[:user] = user + options[:password] = password - ActionDispatch::Http::URL.url_for(options.merge!({ - :path => path, - :script_name => script_name, - :params => params, - :user => user, - :password => password - })) + url_strategy.call options end def call(env) - @router.call(env) + req = request_class.new(env) + req.path_info = Journey::Router::Utils.normalize_path(req.path_info) + @router.serve(req) end def recognize_path(path, environment = {}) @@ -687,8 +708,8 @@ module ActionDispatch raise ActionController::RoutingError, e.message end - req = @request_class.new(env) - @router.recognize(req) do |route, _matches, params| + req = request_class.new(env) + @router.recognize(req) do |route, params| params.merge!(extras) params.each do |key, value| if value.is_a?(String) @@ -696,14 +717,12 @@ module ActionDispatch params[key] = URI.parser.unescape(value) end end - old_params = env[::ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] - env[::ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] = (old_params || {}).merge(params) - dispatcher = route.app - while dispatcher.is_a?(Mapper::Constraints) && dispatcher.matches?(env) do - dispatcher = dispatcher.app - end + old_params = req.path_parameters + req.path_parameters = old_params.merge params + app = route.app + if app.matches?(req) && app.dispatcher? + dispatcher = app.app - if dispatcher.is_a?(Dispatcher) if dispatcher.controller(params, false) dispatcher.prepare_params!(params) return params @@ -715,17 +734,6 @@ module ActionDispatch raise ActionController::RoutingError, "No route matches #{path.inspect}" end - - private - - def extract_authentication(options) - if options[:user] && options[:password] - [options.delete(:user), options.delete(:password)] - else - nil - end - end - end end end diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index 4a0ef40873..e1c73f8f07 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -152,13 +152,19 @@ module ActionDispatch when nil _routes.url_for(url_options.symbolize_keys) when Hash - _routes.url_for(options.symbolize_keys.reverse_merge!(url_options)) + route_name = options.delete :use_route + _routes.url_for(options.symbolize_keys.reverse_merge!(url_options), + route_name) when String options + when Symbol + HelperMethodBuilder.url.handle_string_call self, options when Array polymorphic_url(options, options.extract_options!) + when Class + HelperMethodBuilder.url.handle_class_call self, options else - polymorphic_url(options) + HelperMethodBuilder.url.handle_model_call self, options end end diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index 68feb26936..0adc6c84ff 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -73,7 +73,13 @@ module ActionDispatch if Regexp === fragment fragment else - @controller._compute_redirect_to_location(fragment) + handle = @controller || Class.new(ActionController::Metal) do + include ActionController::Redirecting + def initialize(request) + @_request = request + end + end.new(@request) + handle._compute_redirect_to_location(fragment) end end end diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index cc6b763093..12b796b95f 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -3,7 +3,6 @@ require 'uri' require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/object/try' require 'rack/test' -require 'minitest' module ActionDispatch module Integration #:nodoc: @@ -189,8 +188,8 @@ module ActionDispatch # This makes app.url_for and app.foo_path available in the console if app.respond_to?(:routes) singleton_class.class_eval do - include app.routes.url_helpers if app.routes.respond_to?(:url_helpers) - include app.routes.mounted_helpers if app.routes.respond_to?(:mounted_helpers) + include app.routes.url_helpers + include app.routes.mounted_helpers end end @@ -201,7 +200,7 @@ module ActionDispatch @url_options ||= default_url_options.dup.tap do |url_options| url_options.reverse_merge!(controller.url_options) if controller - if @app.respond_to?(:routes) && @app.routes.respond_to?(:default_url_options) + if @app.respond_to?(:routes) url_options.reverse_merge!(@app.routes.default_url_options) end @@ -270,12 +269,6 @@ module ActionDispatch path = location.query ? "#{location.path}?#{location.query}" : location.path end - unless ActionController::Base < ActionController::Testing - ActionController::Base.class_eval do - include ActionController::Testing - end - end - hostname, port = host.split(':') env = { @@ -300,13 +293,7 @@ module ActionDispatch # NOTE: rack-test v0.5 doesn't build a default uri correctly # Make sure requested path is always a full uri - uri = URI.parse('/') - uri.scheme ||= env['rack.url_scheme'] - uri.host ||= env['SERVER_NAME'] - uri.port ||= env['SERVER_PORT'].try(:to_i) - uri += path - - session.request(uri.to_s, env) + session.request(build_full_uri(path, env), env) @request_count += 1 @request = ActionDispatch::Request.new(session.last_request.env) @@ -319,6 +306,10 @@ module ActionDispatch return response.status end + + def build_full_uri(path, env) + "#{env['rack.url_scheme']}://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{path}" + end end module Runner @@ -338,6 +329,7 @@ module ActionDispatch xml_http_request xhr get_via_redirect post_via_redirect).each do |method| define_method(method) do |*args| reset! unless integration_session + reset_template_assertion # reset the html_document variable, but only for new get/post calls @html_document = nil unless method == 'cookies' || method == 'assigns' integration_session.__send__(method, *args).tap do @@ -356,7 +348,7 @@ module ActionDispatch # By default, a single session is automatically created for you, but you # can use this method to open multiple sessions that ought to be tested # simultaneously. - def open_session(app = nil) + def open_session dup.tap do |session| yield session if block_given? end diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb index 57c678843b..de3dc5f924 100644 --- a/actionpack/lib/action_dispatch/testing/test_request.rb +++ b/actionpack/lib/action_dispatch/testing/test_request.rb @@ -39,7 +39,7 @@ module ActionDispatch end def action=(action_name) - path_parameters["action"] = action_name.to_s + path_parameters[:action] = action_name.to_s end def if_modified_since=(last_modified) diff --git a/actionpack/test/abstract/collector_test.rb b/actionpack/test/abstract/collector_test.rb index b1a5044399..fc59bf19c4 100644 --- a/actionpack/test/abstract/collector_test.rb +++ b/actionpack/test/abstract/collector_test.rb @@ -24,15 +24,21 @@ module AbstractController test "does not respond to unknown mime types" do collector = MyCollector.new - assert !collector.respond_to?(:unknown) + assert_not_respond_to collector, :unknown end test "register mime types on method missing" do AbstractController::Collector.send(:remove_method, :js) - collector = MyCollector.new - assert !collector.respond_to?(:js) - collector.js - assert_respond_to collector, :js + begin + collector = MyCollector.new + assert_not_respond_to collector, :js + collector.js + assert_respond_to collector, :js + ensure + unless AbstractController::Collector.method_defined? :js + AbstractController::Collector.generate_method_for_mime :js + end + end end test "does not register unknown mime types" do diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 03a4741f42..4e17d57dad 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -15,6 +15,12 @@ silence_warnings do Encoding.default_external = "UTF-8" end +require 'drb' +require 'drb/unix' +require 'tempfile' + +PROCESS_COUNT = (ENV['N'] || 4).to_i + require 'active_support/testing/autorun' require 'abstract_controller' require 'action_controller' @@ -105,6 +111,9 @@ end module ActiveSupport class TestCase include ActionDispatch::DrawOnce + if ActiveSupport::Testing::Isolation.forking_env? && PROCESS_COUNT > 0 + parallelize_me! + end end end @@ -251,7 +260,6 @@ end module ActionController class Base - include ActionController::Testing # This stub emulates the Railtie including the URL helpers from a Rails application include SharedTestRoutes.url_helpers include SharedTestRoutes.mounted_helpers @@ -306,22 +314,95 @@ end module ActionDispatch module RoutingVerbs - def get(uri_or_host, path = nil) + def send_request(uri_or_host, method, path) host = uri_or_host.host unless path path ||= uri_or_host.path params = {'PATH_INFO' => path, - 'REQUEST_METHOD' => 'GET', + 'REQUEST_METHOD' => method, 'HTTP_HOST' => host} - routes.call(params)[2].join + routes.call(params) + end + + def request_path_params(path, options = {}) + method = options[:method] || 'GET' + resp = send_request URI('http://localhost' + path), method.to_s.upcase, nil + status = resp.first + if status == 404 + raise ActionController::RoutingError, "No route matches #{path.inspect}" + end + controller.request.path_parameters + end + + def get(uri_or_host, path = nil) + send_request(uri_or_host, 'GET', path)[2].join + end + + def post(uri_or_host, path = nil) + send_request(uri_or_host, 'POST', path)[2].join + end + + def put(uri_or_host, path = nil) + send_request(uri_or_host, 'PUT', path)[2].join + end + + def delete(uri_or_host, path = nil) + send_request(uri_or_host, 'DELETE', path)[2].join + end + + def patch(uri_or_host, path = nil) + send_request(uri_or_host, 'PATCH', path)[2].join end end end module RoutingTestHelpers - def url_for(set, options, recall = nil) - set.send(:url_for, options.merge(:only_path => true, :_recall => recall)) + def url_for(set, options) + route_name = options.delete :use_route + set.url_for options.merge(:only_path => true), route_name + end + + def make_set(strict = true) + tc = self + TestSet.new ->(c) { tc.controller = c }, strict + end + + class TestSet < ActionDispatch::Routing::RouteSet + attr_reader :strict + + def initialize(block, strict = false) + @block = block + @strict = strict + super() + end + + class Dispatcher < ActionDispatch::Routing::RouteSet::Dispatcher + def initialize(defaults, set, block) + super(defaults) + @block = block + @set = set + end + + def controller(params, default_controller=true) + super(params, @set.strict) + end + + def controller_reference(controller_param) + block = @block + set = @set + super if @set.strict + Class.new(ActionController::Base) { + include set.url_helpers + define_method(:process) { |name| block.call(self) } + def to_a; [200, {}, []]; end + } + end + end + + def dispatcher defaults + TestSet::Dispatcher.new defaults, self, @block + end end end @@ -360,3 +441,59 @@ end def jruby_skip(message = '') skip message if defined?(JRUBY_VERSION) end + +require 'mocha/setup' # FIXME: stop using mocha + +class ForkingExecutor + class Server + include DRb::DRbUndumped + + def initialize + @queue = Queue.new + end + + def record reporter, result + reporter.record result + end + + def << o + o[2] = DRbObject.new(o[2]) if o + @queue << o + end + def pop; @queue.pop; end + end + + def initialize size + @size = size + @queue = Server.new + file = File.join Dir.tmpdir, Dir::Tmpname.make_tmpname('tests', 'fd') + @url = "drbunix://#{file}" + @pool = nil + DRb.start_service @url, @queue + end + + def << work; @queue << work; end + + def shutdown + pool = @size.times.map { + fork { + DRb.stop_service + queue = DRbObject.new_with_uri @url + while job = queue.pop + klass = job[0] + method = job[1] + reporter = job[2] + result = Minitest.run_one_method klass, method + queue.record reporter, result + end + } + } + @size.times { @queue << nil } + pool.each { |pid| Process.waitpid pid } + end +end + +if ActiveSupport::Testing::Isolation.forking_env? && PROCESS_COUNT > 0 + # Use N processes (N defaults to 4) + Minitest.parallel_executor = ForkingExecutor.new(PROCESS_COUNT) +end diff --git a/actionpack/test/controller/assert_select_test.rb b/actionpack/test/controller/assert_select_test.rb index 580604df3a..f07d201563 100644 --- a/actionpack/test/controller/assert_select_test.rb +++ b/actionpack/test/controller/assert_select_test.rb @@ -56,13 +56,16 @@ class AssertSelectTest < ActionController::TestCase def setup super + @old_delivery_method = ActionMailer::Base.delivery_method + @old_perform_deliveries = ActionMailer::Base.perform_deliveries ActionMailer::Base.delivery_method = :test ActionMailer::Base.perform_deliveries = true - ActionMailer::Base.deliveries = [] end def teardown super + ActionMailer::Base.delivery_method = @old_delivery_method + ActionMailer::Base.perform_deliveries = @old_perform_deliveries ActionMailer::Base.deliveries.clear end diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index 58a86ce9af..c0e6a2ebd1 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -227,6 +227,22 @@ CACHED @store.read("views/test.host/functional_caching/inline_fragment_cached/#{template_digest("functional_caching/inline_fragment_cached")}")) end + def test_fragment_cache_instrumentation + payload = nil + + subscriber = proc do |*args| + event = ActiveSupport::Notifications::Event.new(*args) + payload = event.payload + end + + ActiveSupport::Notifications.subscribed(subscriber, "read_fragment.action_controller") do + get :inline_fragment_cached + end + + assert_equal "functional_caching", payload[:controller] + assert_equal "inline_fragment_cached", payload[:action] + end + def test_html_formatted_fragment_caching get :formatted_fragment_cached, :format => "html" assert_response :success diff --git a/actionpack/test/controller/content_type_test.rb b/actionpack/test/controller/content_type_test.rb index 03d5d27cf4..89667df3a4 100644 --- a/actionpack/test/controller/content_type_test.rb +++ b/actionpack/test/controller/content_type_test.rb @@ -68,12 +68,11 @@ class ContentTypeTest < ActionController::TestCase end def test_render_changed_charset_default - ActionDispatch::Response.default_charset = "utf-16" - get :render_defaults - assert_equal "utf-16", @response.charset - assert_equal Mime::HTML, @response.content_type - ensure - ActionDispatch::Response.default_charset = "utf-8" + with_default_charset "utf-16" do + get :render_defaults + assert_equal "utf-16", @response.charset + assert_equal Mime::HTML, @response.content_type + end end # :ported: @@ -105,12 +104,11 @@ class ContentTypeTest < ActionController::TestCase end def test_nil_default_for_erb - ActionDispatch::Response.default_charset = nil - get :render_default_for_erb - assert_equal Mime::HTML, @response.content_type - assert_nil @response.charset, @response.headers.inspect - ensure - ActionDispatch::Response.default_charset = "utf-8" + with_default_charset nil do + get :render_default_for_erb + assert_equal Mime::HTML, @response.content_type + assert_nil @response.charset, @response.headers.inspect + end end def test_default_for_erb @@ -130,6 +128,16 @@ class ContentTypeTest < ActionController::TestCase assert_equal Mime::HTML, @response.content_type assert_equal "utf-8", @response.charset end + + private + + def with_default_charset(charset) + old_default_charset = ActionDispatch::Response.default_charset + ActionDispatch::Response.default_charset = charset + yield + ensure + ActionDispatch::Response.default_charset = old_default_charset + end end class AcceptBasedContentTypeTest < ActionController::TestCase diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb index c87494aa64..b2b01b3fa9 100644 --- a/actionpack/test/controller/filters_test.rb +++ b/actionpack/test/controller/filters_test.rb @@ -2,13 +2,13 @@ require 'abstract_unit' class ActionController::Base class << self - %w(append_around_filter prepend_after_filter prepend_around_filter prepend_before_filter skip_after_filter skip_before_filter skip_filter).each do |pending| + %w(append_around_action prepend_after_action prepend_around_action prepend_before_action skip_after_action skip_before_action skip_action_callback).each do |pending| define_method(pending) do |*args| $stderr.puts "#{pending} unimplemented: #{args.inspect}" end unless method_defined?(pending) end - def before_filters + def before_actions filters = _process_action_callbacks.select { |c| c.kind == :before } filters.map! { |c| c.raw_filter } end @@ -28,8 +28,8 @@ end class FilterTest < ActionController::TestCase class TestController < ActionController::Base - before_filter :ensure_login - after_filter :clean_up + before_action :ensure_login + after_action :clean_up def show render :inline => "ran action" @@ -42,13 +42,13 @@ class FilterTest < ActionController::TestCase end def clean_up - @ran_after_filter ||= [] - @ran_after_filter << "clean_up" + @ran_after_action ||= [] + @ran_after_action << "clean_up" end end class ChangingTheRequirementsController < TestController - before_filter :ensure_login, :except => [:go_wild] + before_action :ensure_login, :except => [:go_wild] def go_wild render :text => "gobble" @@ -56,9 +56,9 @@ class FilterTest < ActionController::TestCase end class TestMultipleFiltersController < ActionController::Base - before_filter :try_1 - before_filter :try_2 - before_filter :try_3 + before_action :try_1 + before_action :try_2 + before_action :try_3 (1..3).each do |i| define_method "fail_#{i}" do @@ -78,8 +78,8 @@ class FilterTest < ActionController::TestCase end class RenderingController < ActionController::Base - before_filter :before_filter_rendering - after_filter :unreached_after_filter + before_action :before_action_rendering + after_action :unreached_after_action def show @ran_action = true @@ -87,29 +87,29 @@ class FilterTest < ActionController::TestCase end private - def before_filter_rendering + def before_action_rendering @ran_filter ||= [] - @ran_filter << "before_filter_rendering" + @ran_filter << "before_action_rendering" render :inline => "something else" end - def unreached_after_filter - @ran_filter << "unreached_after_filter_after_render" + def unreached_after_action + @ran_filter << "unreached_after_action_after_render" end end - class RenderingForPrependAfterFilterController < RenderingController - prepend_after_filter :unreached_prepend_after_filter + class RenderingForPrependAfterActionController < RenderingController + prepend_after_action :unreached_prepend_after_action private - def unreached_prepend_after_filter - @ran_filter << "unreached_preprend_after_filter_after_render" + def unreached_prepend_after_action + @ran_filter << "unreached_preprend_after_action_after_render" end end - class BeforeFilterRedirectionController < ActionController::Base - before_filter :before_filter_redirects - after_filter :unreached_after_filter + class BeforeActionRedirectionController < ActionController::Base + before_action :before_action_redirects + after_action :unreached_after_action def show @ran_action = true @@ -122,23 +122,23 @@ class FilterTest < ActionController::TestCase end private - def before_filter_redirects + def before_action_redirects @ran_filter ||= [] - @ran_filter << "before_filter_redirects" + @ran_filter << "before_action_redirects" redirect_to(:action => 'target_of_redirection') end - def unreached_after_filter - @ran_filter << "unreached_after_filter_after_redirection" + def unreached_after_action + @ran_filter << "unreached_after_action_after_redirection" end end - class BeforeFilterRedirectionForPrependAfterFilterController < BeforeFilterRedirectionController - prepend_after_filter :unreached_prepend_after_filter_after_redirection + class BeforeActionRedirectionForPrependAfterActionController < BeforeActionRedirectionController + prepend_after_action :unreached_prepend_after_action_after_redirection private - def unreached_prepend_after_filter_after_redirection - @ran_filter << "unreached_prepend_after_filter_after_redirection" + def unreached_prepend_after_action_after_redirection + @ran_filter << "unreached_prepend_after_action_after_redirection" end end @@ -151,8 +151,8 @@ class FilterTest < ActionController::TestCase render :inline => "ran action" end - def show_without_filter - render :inline => "ran action without filter" + def show_without_action + render :inline => "ran action without action" end private @@ -168,70 +168,70 @@ class FilterTest < ActionController::TestCase end class ConditionalCollectionFilterController < ConditionalFilterController - before_filter :ensure_login, :except => [ :show_without_filter, :another_action ] + before_action :ensure_login, :except => [ :show_without_action, :another_action ] end class OnlyConditionSymController < ConditionalFilterController - before_filter :ensure_login, :only => :show + before_action :ensure_login, :only => :show end class ExceptConditionSymController < ConditionalFilterController - before_filter :ensure_login, :except => :show_without_filter + before_action :ensure_login, :except => :show_without_action end class BeforeAndAfterConditionController < ConditionalFilterController - before_filter :ensure_login, :only => :show - after_filter :clean_up_tmp, :only => :show + before_action :ensure_login, :only => :show + after_action :clean_up_tmp, :only => :show end class OnlyConditionProcController < ConditionalFilterController - before_filter(:only => :show) {|c| c.instance_variable_set(:"@ran_proc_filter", true) } + before_action(:only => :show) {|c| c.instance_variable_set(:"@ran_proc_action", true) } end class ExceptConditionProcController < ConditionalFilterController - before_filter(:except => :show_without_filter) {|c| c.instance_variable_set(:"@ran_proc_filter", true) } + before_action(:except => :show_without_action) {|c| c.instance_variable_set(:"@ran_proc_action", true) } end class ConditionalClassFilter - def self.before(controller) controller.instance_variable_set(:"@ran_class_filter", true) end + def self.before(controller) controller.instance_variable_set(:"@ran_class_action", true) end end class OnlyConditionClassController < ConditionalFilterController - before_filter ConditionalClassFilter, :only => :show + before_action ConditionalClassFilter, :only => :show end class ExceptConditionClassController < ConditionalFilterController - before_filter ConditionalClassFilter, :except => :show_without_filter + before_action ConditionalClassFilter, :except => :show_without_action end class AnomolousYetValidConditionController < ConditionalFilterController - before_filter(ConditionalClassFilter, :ensure_login, Proc.new {|c| c.instance_variable_set(:"@ran_proc_filter1", true)}, :except => :show_without_filter) { |c| c.instance_variable_set(:"@ran_proc_filter2", true)} + before_action(ConditionalClassFilter, :ensure_login, Proc.new {|c| c.instance_variable_set(:"@ran_proc_action1", true)}, :except => :show_without_action) { |c| c.instance_variable_set(:"@ran_proc_action2", true)} end class OnlyConditionalOptionsFilter < ConditionalFilterController - before_filter :ensure_login, :only => :index, :if => Proc.new {|c| c.instance_variable_set(:"@ran_conditional_index_proc", true) } + before_action :ensure_login, :only => :index, :if => Proc.new {|c| c.instance_variable_set(:"@ran_conditional_index_proc", true) } end class ConditionalOptionsFilter < ConditionalFilterController - before_filter :ensure_login, :if => Proc.new { |c| true } - before_filter :clean_up_tmp, :if => Proc.new { |c| false } + before_action :ensure_login, :if => Proc.new { |c| true } + before_action :clean_up_tmp, :if => Proc.new { |c| false } end class ConditionalOptionsSkipFilter < ConditionalFilterController - before_filter :ensure_login - before_filter :clean_up_tmp + before_action :ensure_login + before_action :clean_up_tmp - skip_before_filter :ensure_login, if: -> { false } - skip_before_filter :clean_up_tmp, if: -> { true } + skip_before_action :ensure_login, if: -> { false } + skip_before_action :clean_up_tmp, if: -> { true } end class ClassController < ConditionalFilterController - before_filter ConditionalClassFilter + before_action ConditionalClassFilter end class PrependingController < TestController - prepend_before_filter :wonderful_life - # skip_before_filter :fire_flash + prepend_before_action :wonderful_life + # skip_before_action :fire_flash private def wonderful_life @@ -241,8 +241,8 @@ class FilterTest < ActionController::TestCase end class SkippingAndLimitedController < TestController - skip_before_filter :ensure_login - before_filter :ensure_login, :only => :index + skip_before_action :ensure_login + before_action :ensure_login, :only => :index def index render :text => 'ok' @@ -254,9 +254,9 @@ class FilterTest < ActionController::TestCase end class SkippingAndReorderingController < TestController - skip_before_filter :ensure_login - before_filter :find_record - before_filter :ensure_login + skip_before_action :ensure_login + before_action :find_record + before_action :ensure_login def index render :text => 'ok' @@ -270,10 +270,10 @@ class FilterTest < ActionController::TestCase end class ConditionalSkippingController < TestController - skip_before_filter :ensure_login, :only => [ :login ] - skip_after_filter :clean_up, :only => [ :login ] + skip_before_action :ensure_login, :only => [ :login ] + skip_after_action :clean_up, :only => [ :login ] - before_filter :find_user, :only => [ :change_password ] + before_action :find_user, :only => [ :change_password ] def login render :inline => "ran action" @@ -291,8 +291,8 @@ class FilterTest < ActionController::TestCase end class ConditionalParentOfConditionalSkippingController < ConditionalFilterController - before_filter :conditional_in_parent_before, :only => [:show, :another_action] - after_filter :conditional_in_parent_after, :only => [:show, :another_action] + before_action :conditional_in_parent_before, :only => [:show, :another_action] + after_action :conditional_in_parent_after, :only => [:show, :another_action] private @@ -308,20 +308,20 @@ class FilterTest < ActionController::TestCase end class ChildOfConditionalParentController < ConditionalParentOfConditionalSkippingController - skip_before_filter :conditional_in_parent_before, :only => :another_action - skip_after_filter :conditional_in_parent_after, :only => :another_action + skip_before_action :conditional_in_parent_before, :only => :another_action + skip_after_action :conditional_in_parent_after, :only => :another_action end class AnotherChildOfConditionalParentController < ConditionalParentOfConditionalSkippingController - skip_before_filter :conditional_in_parent_before, :only => :show + skip_before_action :conditional_in_parent_before, :only => :show end class ProcController < PrependingController - before_filter(proc { |c| c.instance_variable_set(:"@ran_proc_filter", true) }) + before_action(proc { |c| c.instance_variable_set(:"@ran_proc_action", true) }) end class ImplicitProcController < PrependingController - before_filter { |c| c.instance_variable_set(:"@ran_proc_filter", true) } + before_action { |c| c.instance_variable_set(:"@ran_proc_action", true) } end class AuditFilter @@ -367,7 +367,7 @@ class FilterTest < ActionController::TestCase end class AuditController < ActionController::Base - before_filter(AuditFilter) + before_action(AuditFilter) def show render :text => "hello" @@ -375,14 +375,14 @@ class FilterTest < ActionController::TestCase end class AroundFilterController < PrependingController - around_filter AroundFilter.new + around_action AroundFilter.new end class BeforeAfterClassFilterController < PrependingController begin filter = AroundFilter.new - before_filter filter - after_filter filter + before_action filter + after_action filter end end @@ -394,18 +394,18 @@ class FilterTest < ActionController::TestCase super() end - before_filter { |c| c.class.execution_log << " before procfilter " } - prepend_around_filter AroundFilter.new + before_action { |c| c.class.execution_log << " before procfilter " } + prepend_around_action AroundFilter.new - after_filter { |c| c.class.execution_log << " after procfilter " } - append_around_filter AppendedAroundFilter.new + after_action { |c| c.class.execution_log << " after procfilter " } + append_around_action AppendedAroundFilter.new end class MixedSpecializationController < ActionController::Base class OutOfOrder < StandardError; end - before_filter :first - before_filter :second, :only => :foo + before_action :first + before_action :second, :only => :foo def foo render :text => 'foo' @@ -426,7 +426,7 @@ class FilterTest < ActionController::TestCase end class DynamicDispatchController < ActionController::Base - before_filter :choose + before_action :choose %w(foo bar baz).each do |action| define_method(action) { render :text => action } @@ -439,9 +439,9 @@ class FilterTest < ActionController::TestCase end class PrependingBeforeAndAfterController < ActionController::Base - prepend_before_filter :before_all - prepend_after_filter :after_all - before_filter :between_before_all_and_after_all + prepend_before_action :before_all + prepend_after_action :after_all + before_action :between_before_all_and_after_all def before_all @ran_filter ||= [] @@ -473,7 +473,7 @@ class FilterTest < ActionController::TestCase end class RescuedController < ActionController::Base - around_filter RescuingAroundFilterWithBlock.new + around_action RescuingAroundFilterWithBlock.new def show raise ErrorToRescue.new("Something made the bad noise.") @@ -482,10 +482,10 @@ class FilterTest < ActionController::TestCase class NonYieldingAroundFilterController < ActionController::Base - before_filter :filter_one - around_filter :non_yielding_filter - before_filter :filter_two - after_filter :filter_three + before_action :filter_one + around_action :non_yielding_action + before_action :action_two + after_action :action_three def index render :inline => "index" @@ -498,24 +498,24 @@ class FilterTest < ActionController::TestCase @filters << "filter_one" end - def filter_two - @filters << "filter_two" + def action_two + @filters << "action_two" end - def non_yielding_filter + def non_yielding_action @filters << "it didn't yield" @filter_return_value end - def filter_three - @filters << "filter_three" + def action_three + @filters << "action_three" end end class ImplicitActionsController < ActionController::Base - before_filter :find_only, :only => :edit - before_filter :find_except, :except => :edit + before_action :find_only, :only => :edit + before_action :find_except, :except => :edit private @@ -528,7 +528,7 @@ class FilterTest < ActionController::TestCase end end - def test_non_yielding_around_filters_not_returning_false_do_not_raise + def test_non_yielding_around_actions_not_returning_false_do_not_raise controller = NonYieldingAroundFilterController.new controller.instance_variable_set "@filter_return_value", true assert_nothing_raised do @@ -536,7 +536,7 @@ class FilterTest < ActionController::TestCase end end - def test_non_yielding_around_filters_returning_false_do_not_raise + def test_non_yielding_around_actions_returning_false_do_not_raise controller = NonYieldingAroundFilterController.new controller.instance_variable_set "@filter_return_value", false assert_nothing_raised do @@ -544,64 +544,64 @@ class FilterTest < ActionController::TestCase end end - def test_after_filters_are_not_run_if_around_filter_returns_false + def test_after_actions_are_not_run_if_around_action_returns_false controller = NonYieldingAroundFilterController.new controller.instance_variable_set "@filter_return_value", false test_process(controller, "index") assert_equal ["filter_one", "it didn't yield"], controller.assigns['filters'] end - def test_after_filters_are_not_run_if_around_filter_does_not_yield + def test_after_actions_are_not_run_if_around_action_does_not_yield controller = NonYieldingAroundFilterController.new controller.instance_variable_set "@filter_return_value", true test_process(controller, "index") assert_equal ["filter_one", "it didn't yield"], controller.assigns['filters'] end - def test_added_filter_to_inheritance_graph - assert_equal [ :ensure_login ], TestController.before_filters + def test_added_action_to_inheritance_graph + assert_equal [ :ensure_login ], TestController.before_actions end def test_base_class_in_isolation - assert_equal [ ], ActionController::Base.before_filters + assert_equal [ ], ActionController::Base.before_actions end - def test_prepending_filter - assert_equal [ :wonderful_life, :ensure_login ], PrependingController.before_filters + def test_prepending_action + assert_equal [ :wonderful_life, :ensure_login ], PrependingController.before_actions end - def test_running_filters + def test_running_actions test_process(PrependingController) assert_equal %w( wonderful_life ensure_login ), assigns["ran_filter"] end - def test_running_filters_with_proc + def test_running_actions_with_proc test_process(ProcController) - assert assigns["ran_proc_filter"] + assert assigns["ran_proc_action"] end - def test_running_filters_with_implicit_proc + def test_running_actions_with_implicit_proc test_process(ImplicitProcController) - assert assigns["ran_proc_filter"] + assert assigns["ran_proc_action"] end - def test_running_filters_with_class + def test_running_actions_with_class test_process(AuditController) assert assigns["was_audited"] end - def test_running_anomolous_yet_valid_condition_filters + def test_running_anomolous_yet_valid_condition_actions test_process(AnomolousYetValidConditionController) assert_equal %w( ensure_login ), assigns["ran_filter"] - assert assigns["ran_class_filter"] - assert assigns["ran_proc_filter1"] - assert assigns["ran_proc_filter2"] + assert assigns["ran_class_action"] + assert assigns["ran_proc_action1"] + assert assigns["ran_proc_action2"] - test_process(AnomolousYetValidConditionController, "show_without_filter") + test_process(AnomolousYetValidConditionController, "show_without_action") assert_nil assigns["ran_filter"] - assert !assigns["ran_class_filter"] - assert !assigns["ran_proc_filter1"] - assert !assigns["ran_proc_filter2"] + assert !assigns["ran_class_action"] + assert !assigns["ran_proc_action1"] + assert !assigns["ran_proc_action2"] end def test_running_conditional_options @@ -614,59 +614,59 @@ class FilterTest < ActionController::TestCase assert_equal %w( ensure_login ), assigns["ran_filter"] end - def test_skipping_class_filters + def test_skipping_class_actions test_process(ClassController) - assert_equal true, assigns["ran_class_filter"] + assert_equal true, assigns["ran_class_action"] skipping_class_controller = Class.new(ClassController) do - skip_before_filter ConditionalClassFilter + skip_before_action ConditionalClassFilter end test_process(skipping_class_controller) - assert_nil assigns['ran_class_filter'] + assert_nil assigns['ran_class_action'] end - def test_running_collection_condition_filters + def test_running_collection_condition_actions test_process(ConditionalCollectionFilterController) assert_equal %w( ensure_login ), assigns["ran_filter"] - test_process(ConditionalCollectionFilterController, "show_without_filter") + test_process(ConditionalCollectionFilterController, "show_without_action") assert_nil assigns["ran_filter"] test_process(ConditionalCollectionFilterController, "another_action") assert_nil assigns["ran_filter"] end - def test_running_only_condition_filters + def test_running_only_condition_actions test_process(OnlyConditionSymController) assert_equal %w( ensure_login ), assigns["ran_filter"] - test_process(OnlyConditionSymController, "show_without_filter") + test_process(OnlyConditionSymController, "show_without_action") assert_nil assigns["ran_filter"] test_process(OnlyConditionProcController) - assert assigns["ran_proc_filter"] - test_process(OnlyConditionProcController, "show_without_filter") - assert !assigns["ran_proc_filter"] + assert assigns["ran_proc_action"] + test_process(OnlyConditionProcController, "show_without_action") + assert !assigns["ran_proc_action"] test_process(OnlyConditionClassController) - assert assigns["ran_class_filter"] - test_process(OnlyConditionClassController, "show_without_filter") - assert !assigns["ran_class_filter"] + assert assigns["ran_class_action"] + test_process(OnlyConditionClassController, "show_without_action") + assert !assigns["ran_class_action"] end - def test_running_except_condition_filters + def test_running_except_condition_actions test_process(ExceptConditionSymController) assert_equal %w( ensure_login ), assigns["ran_filter"] - test_process(ExceptConditionSymController, "show_without_filter") + test_process(ExceptConditionSymController, "show_without_action") assert_nil assigns["ran_filter"] test_process(ExceptConditionProcController) - assert assigns["ran_proc_filter"] - test_process(ExceptConditionProcController, "show_without_filter") - assert !assigns["ran_proc_filter"] + assert assigns["ran_proc_action"] + test_process(ExceptConditionProcController, "show_without_action") + assert !assigns["ran_proc_action"] test_process(ExceptConditionClassController) - assert assigns["ran_class_filter"] - test_process(ExceptConditionClassController, "show_without_filter") - assert !assigns["ran_class_filter"] + assert assigns["ran_class_action"] + test_process(ExceptConditionClassController, "show_without_action") + assert !assigns["ran_class_action"] end def test_running_only_condition_and_conditional_options @@ -674,70 +674,70 @@ class FilterTest < ActionController::TestCase assert_not assigns["ran_conditional_index_proc"] end - def test_running_before_and_after_condition_filters + def test_running_before_and_after_condition_actions test_process(BeforeAndAfterConditionController) assert_equal %w( ensure_login clean_up_tmp), assigns["ran_filter"] - test_process(BeforeAndAfterConditionController, "show_without_filter") + test_process(BeforeAndAfterConditionController, "show_without_action") assert_nil assigns["ran_filter"] end - def test_around_filter + def test_around_action test_process(AroundFilterController) assert assigns["before_ran"] assert assigns["after_ran"] end - def test_before_after_class_filter + def test_before_after_class_action test_process(BeforeAfterClassFilterController) assert assigns["before_ran"] assert assigns["after_ran"] end - def test_having_properties_in_around_filter + def test_having_properties_in_around_action test_process(AroundFilterController) assert_equal "before and after", assigns["execution_log"] end - def test_prepending_and_appending_around_filter + def test_prepending_and_appending_around_action test_process(MixedFilterController) assert_equal " before aroundfilter before procfilter before appended aroundfilter " + " after appended aroundfilter after procfilter after aroundfilter ", MixedFilterController.execution_log end - def test_rendering_breaks_filtering_chain + def test_rendering_breaks_actioning_chain response = test_process(RenderingController) assert_equal "something else", response.body assert !assigns["ran_action"] end - def test_before_filter_rendering_breaks_filtering_chain_for_after_filter + def test_before_action_rendering_breaks_actioning_chain_for_after_action test_process(RenderingController) - assert_equal %w( before_filter_rendering ), assigns["ran_filter"] + assert_equal %w( before_action_rendering ), assigns["ran_filter"] assert !assigns["ran_action"] end - def test_before_filter_redirects_breaks_filtering_chain_for_after_filter - test_process(BeforeFilterRedirectionController) + def test_before_action_redirects_breaks_actioning_chain_for_after_action + test_process(BeforeActionRedirectionController) assert_response :redirect - assert_equal "http://test.host/filter_test/before_filter_redirection/target_of_redirection", redirect_to_url - assert_equal %w( before_filter_redirects ), assigns["ran_filter"] + assert_equal "http://test.host/filter_test/before_action_redirection/target_of_redirection", redirect_to_url + assert_equal %w( before_action_redirects ), assigns["ran_filter"] end - def test_before_filter_rendering_breaks_filtering_chain_for_preprend_after_filter - test_process(RenderingForPrependAfterFilterController) - assert_equal %w( before_filter_rendering ), assigns["ran_filter"] + def test_before_action_rendering_breaks_actioning_chain_for_preprend_after_action + test_process(RenderingForPrependAfterActionController) + assert_equal %w( before_action_rendering ), assigns["ran_filter"] assert !assigns["ran_action"] end - def test_before_filter_redirects_breaks_filtering_chain_for_preprend_after_filter - test_process(BeforeFilterRedirectionForPrependAfterFilterController) + def test_before_action_redirects_breaks_actioning_chain_for_preprend_after_action + test_process(BeforeActionRedirectionForPrependAfterActionController) assert_response :redirect - assert_equal "http://test.host/filter_test/before_filter_redirection_for_prepend_after_filter/target_of_redirection", redirect_to_url - assert_equal %w( before_filter_redirects ), assigns["ran_filter"] + assert_equal "http://test.host/filter_test/before_action_redirection_for_prepend_after_action/target_of_redirection", redirect_to_url + assert_equal %w( before_action_redirects ), assigns["ran_filter"] end - def test_filters_with_mixed_specialization_run_in_order + def test_actions_with_mixed_specialization_run_in_order assert_nothing_raised do response = test_process(MixedSpecializationController, 'bar') assert_equal 'bar', response.body @@ -758,7 +758,7 @@ class FilterTest < ActionController::TestCase end end - def test_running_prepended_before_and_after_filter + def test_running_prepended_before_and_after_action test_process(PrependingBeforeAndAfterController) assert_equal %w( before_all between_before_all_and_after_all after_all ), assigns["ran_filter"] end @@ -775,26 +775,26 @@ class FilterTest < ActionController::TestCase assert_equal %w( find_record ensure_login ), assigns["ran_filter"] end - def test_conditional_skipping_of_filters + def test_conditional_skipping_of_actions test_process(ConditionalSkippingController, "login") assert_nil assigns["ran_filter"] test_process(ConditionalSkippingController, "change_password") assert_equal %w( ensure_login find_user ), assigns["ran_filter"] test_process(ConditionalSkippingController, "login") - assert !@controller.instance_variable_defined?("@ran_after_filter") + assert !@controller.instance_variable_defined?("@ran_after_action") test_process(ConditionalSkippingController, "change_password") - assert_equal %w( clean_up ), @controller.instance_variable_get("@ran_after_filter") + assert_equal %w( clean_up ), @controller.instance_variable_get("@ran_after_action") end - def test_conditional_skipping_of_filters_when_parent_filter_is_also_conditional + def test_conditional_skipping_of_actions_when_parent_action_is_also_conditional test_process(ChildOfConditionalParentController) assert_equal %w( conditional_in_parent_before conditional_in_parent_after ), assigns['ran_filter'] test_process(ChildOfConditionalParentController, 'another_action') assert_nil assigns['ran_filter'] end - def test_condition_skipping_of_filters_when_siblings_also_have_conditions + def test_condition_skipping_of_actions_when_siblings_also_have_conditions test_process(ChildOfConditionalParentController) assert_equal %w( conditional_in_parent_before conditional_in_parent_after ), assigns['ran_filter'] test_process(AnotherChildOfConditionalParentController) @@ -808,7 +808,7 @@ class FilterTest < ActionController::TestCase assert_nil assigns['ran_filter'] end - def test_a_rescuing_around_filter + def test_a_rescuing_around_action response = nil assert_nothing_raised do response = test_process(RescuedController) @@ -818,7 +818,7 @@ class FilterTest < ActionController::TestCase assert_equal("I rescued this: #<FilterTest::ErrorToRescue: Something made the bad noise.>", response.body) end - def test_filters_obey_only_and_except_for_implicit_actions + def test_actions_obey_only_and_except_for_implicit_actions test_process(ImplicitActionsController, 'show') assert_equal 'Except', assigns(:except) assert_nil assigns(:only) @@ -852,7 +852,7 @@ class PostsController < ActionController::Base include AroundExceptions end - module_eval %w(raises_before raises_after raises_both no_raise no_filter).map { |action| "def #{action}; default_action end" }.join("\n") + module_eval %w(raises_before raises_after raises_both no_raise no_action).map { |action| "def #{action}; default_action end" }.join("\n") private def default_action @@ -861,9 +861,9 @@ class PostsController < ActionController::Base end class ControllerWithSymbolAsFilter < PostsController - around_filter :raise_before, :only => :raises_before - around_filter :raise_after, :only => :raises_after - around_filter :without_exception, :only => :no_raise + around_action :raise_before, :only => :raises_before + around_action :raise_after, :only => :raises_after + around_action :without_exception, :only => :no_raise private def raise_before @@ -895,7 +895,7 @@ class ControllerWithFilterClass < PostsController end end - around_filter YieldingFilter, :only => :raises_after + around_action YieldingFilter, :only => :raises_after end class ControllerWithFilterInstance < PostsController @@ -906,11 +906,11 @@ class ControllerWithFilterInstance < PostsController end end - around_filter YieldingFilter.new, :only => :raises_after + around_action YieldingFilter.new, :only => :raises_after end class ControllerWithProcFilter < PostsController - around_filter(:only => :no_raise) do |c,b| + around_action(:only => :no_raise) do |c,b| c.instance_variable_set(:"@before", true) b.call c.instance_variable_set(:"@after", true) @@ -918,14 +918,14 @@ class ControllerWithProcFilter < PostsController end class ControllerWithNestedFilters < ControllerWithSymbolAsFilter - around_filter :raise_before, :raise_after, :without_exception, :only => :raises_both + around_action :raise_before, :raise_after, :without_exception, :only => :raises_both end class ControllerWithAllTypesOfFilters < PostsController - before_filter :before - around_filter :around - after_filter :after - around_filter :around_again + before_action :before + around_action :around + after_action :after + around_action :around_again private def before @@ -951,8 +951,8 @@ class ControllerWithAllTypesOfFilters < PostsController end class ControllerWithTwoLessFilters < ControllerWithAllTypesOfFilters - skip_filter :around_again - skip_filter :after + skip_action_callback :around_again + skip_action_callback :after end class YieldingAroundFiltersTest < ActionController::TestCase @@ -963,7 +963,7 @@ class YieldingAroundFiltersTest < ActionController::TestCase assert_nothing_raised { test_process(controller,'no_raise') } assert_nothing_raised { test_process(controller,'raises_before') } assert_nothing_raised { test_process(controller,'raises_after') } - assert_nothing_raised { test_process(controller,'no_filter') } + assert_nothing_raised { test_process(controller,'no_action') } end def test_with_symbol @@ -992,7 +992,7 @@ class YieldingAroundFiltersTest < ActionController::TestCase assert assigns['after'] end - def test_nested_filters + def test_nested_actions controller = ControllerWithNestedFilters assert_nothing_raised do begin @@ -1008,31 +1008,31 @@ class YieldingAroundFiltersTest < ActionController::TestCase end end - def test_filter_order_with_all_filter_types + def test_action_order_with_all_action_types test_process(ControllerWithAllTypesOfFilters,'no_raise') assert_equal 'before around (before yield) around_again (before yield) around_again (after yield) after around (after yield)', assigns['ran_filter'].join(' ') end - def test_filter_order_with_skip_filter_method + def test_action_order_with_skip_action_method test_process(ControllerWithTwoLessFilters,'no_raise') assert_equal 'before around (before yield) around (after yield)', assigns['ran_filter'].join(' ') end - def test_first_filter_in_multiple_before_filter_chain_halts + def test_first_action_in_multiple_before_action_chain_halts controller = ::FilterTest::TestMultipleFiltersController.new response = test_process(controller, 'fail_1') assert_equal ' ', response.body assert_equal 1, controller.instance_variable_get(:@try) end - def test_second_filter_in_multiple_before_filter_chain_halts + def test_second_action_in_multiple_before_action_chain_halts controller = ::FilterTest::TestMultipleFiltersController.new response = test_process(controller, 'fail_2') assert_equal ' ', response.body assert_equal 2, controller.instance_variable_get(:@try) end - def test_last_filter_in_multiple_before_filter_chain_halts + def test_last_action_in_multiple_before_action_chain_halts controller = ::FilterTest::TestMultipleFiltersController.new response = test_process(controller, 'fail_3') assert_equal ' ', response.body diff --git a/actionpack/test/controller/force_ssl_test.rb b/actionpack/test/controller/force_ssl_test.rb index 3655b90e32..00d4612ac9 100644 --- a/actionpack/test/controller/force_ssl_test.rb +++ b/actionpack/test/controller/force_ssl_test.rb @@ -93,8 +93,6 @@ class RedirectToSSL < ForceSSLController end class ForceSSLControllerLevelTest < ActionController::TestCase - tests ForceSSLControllerLevel - def test_banana_redirects_to_https get :banana assert_response 301 @@ -115,8 +113,6 @@ class ForceSSLControllerLevelTest < ActionController::TestCase end class ForceSSLCustomOptionsTest < ActionController::TestCase - tests ForceSSLCustomOptions - def setup @request.env['HTTP_HOST'] = 'www.example.com:80' end @@ -189,8 +185,6 @@ class ForceSSLCustomOptionsTest < ActionController::TestCase end class ForceSSLOnlyActionTest < ActionController::TestCase - tests ForceSSLOnlyAction - def test_banana_not_redirects_to_https get :banana assert_response 200 @@ -204,8 +198,6 @@ class ForceSSLOnlyActionTest < ActionController::TestCase end class ForceSSLExceptActionTest < ActionController::TestCase - tests ForceSSLExceptAction - def test_banana_not_redirects_to_https get :banana assert_response 200 @@ -219,8 +211,6 @@ class ForceSSLExceptActionTest < ActionController::TestCase end class ForceSSLIfConditionTest < ActionController::TestCase - tests ForceSSLIfCondition - def test_banana_not_redirects_to_https get :banana assert_response 200 @@ -234,8 +224,6 @@ class ForceSSLIfConditionTest < ActionController::TestCase end class ForceSSLFlashTest < ActionController::TestCase - tests ForceSSLFlash - def test_cheeseburger_redirects_to_https get :set_flash assert_response 302 @@ -315,7 +303,6 @@ class ForceSSLOptionalSegmentsTest < ActionController::TestCase end class RedirectToSSLTest < ActionController::TestCase - tests RedirectToSSL def test_banana_redirects_to_https_if_not_https get :banana assert_response 301 @@ -334,4 +321,4 @@ class RedirectToSSLTest < ActionController::TestCase assert_response 200 assert_equal 'ihaz', response.body end -end
\ No newline at end of file +end diff --git a/actionpack/test/controller/http_basic_authentication_test.rb b/actionpack/test/controller/http_basic_authentication_test.rb index 90548d4294..9052fc6962 100644 --- a/actionpack/test/controller/http_basic_authentication_test.rb +++ b/actionpack/test/controller/http_basic_authentication_test.rb @@ -129,6 +129,13 @@ class HttpBasicAuthenticationTest < ActionController::TestCase assert_response :unauthorized end + test "authentication request with wrong scheme" do + header = 'Bearer ' + encode_credentials('David', 'Goliath').split(' ', 2)[1] + @request.env['HTTP_AUTHORIZATION'] = header + get :search + assert_response :unauthorized + end + private def encode_credentials(username, password) diff --git a/actionpack/test/controller/http_token_authentication_test.rb b/actionpack/test/controller/http_token_authentication_test.rb index 86b94652ce..8c6c8a0aa7 100644 --- a/actionpack/test/controller/http_token_authentication_test.rb +++ b/actionpack/test/controller/http_token_authentication_test.rb @@ -132,13 +132,50 @@ class HttpTokenAuthenticationTest < ActionController::TestCase assert_equal(expected, actual) end - private + test "token_and_options returns empty string with empty token" do + token = '' + actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token)).first + expected = token + assert_equal(expected, actual) + end - def sample_request(token) - @sample_request ||= OpenStruct.new authorization: %{Token token="#{token}"} + test "token_and_options returns correct token with nounce option" do + token = "rcHu+HzSFw89Ypyhn/896A=" + nonce_hash = {nonce: "123abc"} + actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token, nonce_hash)) + expected_token = token + expected_nonce = {"nonce" => nonce_hash[:nonce]} + assert_equal(expected_token, actual.first) + assert_equal(expected_nonce, actual.last) end - def encode_credentials(token, options = {}) - ActionController::HttpAuthentication::Token.encode_credentials(token, options) + test "token_and_options returns nil with no value after the equal sign" do + actual = ActionController::HttpAuthentication::Token.token_and_options(malformed_request).first + expected = nil + assert_equal(expected, actual) + end + + test "raw_params returns a tuple of two key value pair strings" do + auth = sample_request("rcHu+HzSFw89Ypyhn/896A=").authorization.to_s + actual = ActionController::HttpAuthentication::Token.raw_params(auth) + expected = ["token=\"rcHu+HzSFw89Ypyhn/896A=\"", "nonce=\"def\""] + assert_equal(expected, actual) end + + private + + def sample_request(token, options = {nonce: "def"}) + authorization = options.inject([%{Token token="#{token}"}]) do |arr, (k, v)| + arr << "#{k}=\"#{v}\"" + end.join(", ") + @sample_request ||= OpenStruct.new authorization: authorization + end + + def malformed_request + @malformed_request ||= OpenStruct.new authorization: %{Token token=} + end + + def encode_credentials(token, options = {}) + ActionController::HttpAuthentication::Token.encode_credentials(token, options) + end end diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index e851cc6a63..78cce3aa64 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -1,6 +1,7 @@ require 'abstract_unit' require 'controller/fake_controllers' require 'action_view/vendor/html-scanner' +require 'rails/engine' class SessionTest < ActiveSupport::TestCase StubApp = lambda { |env| @@ -374,6 +375,10 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest follow_redirect! assert_response :success assert_equal "/get", path + + get '/moved' + assert_response :redirect + assert_redirected_to '/method' end end @@ -511,6 +516,8 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest end set.draw do + get 'moved' => redirect('/method') + match ':action', :to => controller, :via => [:get, :post], :as => :action get 'get/:action', :to => controller, :as => :get_action end @@ -589,7 +596,7 @@ class ApplicationIntegrationTest < ActionDispatch::IntegrationTest @routes ||= ActionDispatch::Routing::RouteSet.new end - class MountedApp + class MountedApp < Rails::Engine def self.routes @routes ||= ActionDispatch::Routing::RouteSet.new end @@ -769,3 +776,34 @@ class UrlOptionsIntegrationTest < ActionDispatch::IntegrationTest assert_equal "/foo/1/edit", url_for(:action => 'edit', :only_path => true) end end + +class HeadWithStatusActionIntegrationTest < ActionDispatch::IntegrationTest + class FooController < ActionController::Base + def status + head :ok + end + end + + def self.routes + @routes ||= ActionDispatch::Routing::RouteSet.new + end + + def self.call(env) + routes.call(env) + end + + def app + self.class + end + + routes.draw do + get "/foo/status" => 'head_with_status_action_integration_test/foo#status' + end + + test "get /foo/status with head result does not cause stack overflow error" do + assert_nothing_raised do + get '/foo/status' + end + assert_response :ok + end +end diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb index 947f64176b..0500b7c789 100644 --- a/actionpack/test/controller/live_stream_test.rb +++ b/actionpack/test/controller/live_stream_test.rb @@ -39,6 +39,13 @@ module ActionController ensure sse.close end + + def sse_with_multiple_line_message + sse = SSE.new(response.stream) + sse.write("first line.\nsecond line.") + ensure + sse.close + end end tests SSETestController @@ -87,6 +94,15 @@ module ActionController assert_match(/data: {\"name\":\"Ryan\"}/, second_response) assert_match(/id: 2/, second_response) end + + def test_sse_with_multiple_line_message + get :sse_with_multiple_line_message + + wait_for_response_stream_close + first_response, second_response = response.body.split("\n") + assert_match(/data: first line/, first_response) + assert_match(/data: second line/, second_response) + end end class LiveStreamTest < ActionController::TestCase @@ -186,6 +202,39 @@ module ActionController response.stream.write '' response.stream.write params[:widget][:didnt_check_for_nil] end + + def overfill_buffer_and_die + # Write until the buffer is full. It doesn't expose that + # information directly, so we must hard-code its size: + 10.times do + response.stream.write '.' + end + # .. plus one more, because the #each frees up a slot: + response.stream.write '.' + + latch.release + + # This write will block, and eventually raise + response.stream.write 'x' + + 20.times do + response.stream.write '.' + end + end + + def ignore_client_disconnect + response.stream.ignore_disconnect = true + + response.stream.write '' # commit + + # These writes will be ignored + 15.times do + response.stream.write 'x' + end + + logger.info 'Work complete' + latch.release + end end tests TestController @@ -248,6 +297,62 @@ module ActionController assert t.join(3), 'timeout expired before the thread terminated' end + def test_abort_with_full_buffer + @controller.latch = ActiveSupport::Concurrency::Latch.new + + @request.parameters[:format] = 'plain' + @controller.request = @request + @controller.response = @response + + got_error = ActiveSupport::Concurrency::Latch.new + @response.stream.on_error do + ActionController::Base.logger.warn 'Error while streaming' + got_error.release + end + + t = Thread.new(@response) { |resp| + resp.await_commit + _, _, body = resp.to_a + body.each do |part| + @controller.latch.await + body.close + break + end + } + + capture_log_output do |output| + @controller.process :overfill_buffer_and_die + t.join + got_error.await + assert_match 'Error while streaming', output.rewind && output.read + end + end + + def test_ignore_client_disconnect + @controller.latch = ActiveSupport::Concurrency::Latch.new + + @controller.request = @request + @controller.response = @response + + t = Thread.new(@response) { |resp| + resp.await_commit + _, _, body = resp.to_a + body.each do |part| + body.close + break + end + } + + capture_log_output do |output| + @controller.process :ignore_client_disconnect + t.join + Timeout.timeout(3) do + @controller.latch.await + end + assert_match 'Work complete', output.rewind && output.read + end + end + def test_thread_locals_get_copied @controller.tc = self Thread.current[:originating_thread] = Thread.current.object_id diff --git a/actionpack/test/controller/localized_templates_test.rb b/actionpack/test/controller/localized_templates_test.rb index c95ef8a0c7..27871ef351 100644 --- a/actionpack/test/controller/localized_templates_test.rb +++ b/actionpack/test/controller/localized_templates_test.rb @@ -8,22 +8,24 @@ end class LocalizedTemplatesTest < ActionController::TestCase tests LocalizedController + setup do + @old_locale = I18n.locale + end + + teardown do + I18n.locale = @old_locale + end + def test_localized_template_is_used - old_locale = I18n.locale I18n.locale = :de get :hello_world assert_equal "Gutten Tag", @response.body - ensure - I18n.locale = old_locale end def test_default_locale_template_is_used_when_locale_is_missing - old_locale = I18n.locale I18n.locale = :dk get :hello_world assert_equal "Hello World", @response.body - ensure - I18n.locale = old_locale end def test_use_fallback_locales @@ -36,13 +38,9 @@ class LocalizedTemplatesTest < ActionController::TestCase end def test_localized_template_has_correct_header_with_no_format_in_template_name - old_locale = I18n.locale I18n.locale = :it - get :hello_world assert_equal "Ciao Mondo", @response.body assert_equal "text/html", @response.content_type - ensure - I18n.locale = old_locale end end diff --git a/actionpack/test/controller/log_subscriber_test.rb b/actionpack/test/controller/log_subscriber_test.rb index 18037b3d2f..49be7caf38 100644 --- a/actionpack/test/controller/log_subscriber_test.rb +++ b/actionpack/test/controller/log_subscriber_test.rb @@ -85,7 +85,7 @@ class ACLogSubscriberTest < ActionController::TestCase @old_logger = ActionController::Base.logger - @cache_path = File.expand_path('../temp/test_cache', File.dirname(__FILE__)) + @cache_path = File.join Dir.tmpdir, Dir::Tmpname.make_tmpname('tmp', 'cache') @controller.cache_store = :file_store, @cache_path ActionController::LogSubscriber.attach_to :action_controller end diff --git a/actionpack/test/controller/mime/accept_format_test.rb b/actionpack/test/controller/mime/accept_format_test.rb index c03c7edeb8..811c507af2 100644 --- a/actionpack/test/controller/mime/accept_format_test.rb +++ b/actionpack/test/controller/mime/accept_format_test.rb @@ -9,8 +9,6 @@ class StarStarMimeController < ActionController::Base end class StarStarMimeControllerTest < ActionController::TestCase - tests StarStarMimeController - def test_javascript_with_format @request.accept = "text/javascript" get :index, :format => 'js' diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb index ce6d135d92..1bc7ad3015 100644 --- a/actionpack/test/controller/mime/respond_to_test.rb +++ b/actionpack/test/controller/mime/respond_to_test.rb @@ -128,6 +128,12 @@ class RespondToController < ActionController::Base end end + def json_with_callback + respond_to do |type| + type.json { render :json => 'JS', :callback => 'alert' } + end + end + def iphone_with_html_response_type request.format = :iphone if request.env["HTTP_ACCEPT"] == "text/iphone" @@ -258,8 +264,6 @@ class RespondToController < ActionController::Base end class RespondToControllerTest < ActionController::TestCase - tests RespondToController - def setup super @request.host = "www.example.com" @@ -513,6 +517,13 @@ class RespondToControllerTest < ActionController::TestCase assert_equal '<html><div id="html">HTML for all_types_with_layout</div></html>', @response.body end + def test_json_with_callback_sets_javascript_content_type + @request.accept = 'application/json' + get :json_with_callback + assert_equal '/**/alert(JS)', @response.body + assert_equal 'text/javascript', @response.content_type + end + def test_xhr xhr :get, :js_or_html assert_equal 'JS', @response.body diff --git a/actionpack/test/controller/mime/respond_with_test.rb b/actionpack/test/controller/mime/respond_with_test.rb index a70592fa1b..115f3b2f41 100644 --- a/actionpack/test/controller/mime/respond_with_test.rb +++ b/actionpack/test/controller/mime/respond_with_test.rb @@ -2,6 +2,10 @@ require 'abstract_unit' require 'controller/fake_models' class RespondWithController < ActionController::Base + class CustomerWithJson < Customer + def to_json; super; end + end + respond_to :html, :json, :touch respond_to :xml, :except => :using_resource_with_block respond_to :js, :only => [ :using_resource_with_block, :using_resource, 'using_hash_resource' ] @@ -38,6 +42,10 @@ class RespondWithController < ActionController::Base respond_with(resource, :location => "http://test.host/", :status => :created) end + def using_resource_with_json + respond_with(CustomerWithJson.new("david", request.delete? ? nil : 13)) + end + def using_invalid_resource_with_template respond_with(resource) end @@ -138,8 +146,6 @@ class EmptyRespondWithController < ActionController::Base end class RespondWithControllerTest < ActionController::TestCase - tests RespondWithController - def setup super @request.host = "www.example.com" @@ -382,9 +388,8 @@ class RespondWithControllerTest < ActionController::TestCase end def test_using_resource_for_put_with_json_yields_no_content_on_success - Customer.any_instance.stubs(:to_json).returns('{"name": "David"}') @request.accept = "application/json" - put :using_resource + put :using_resource_with_json assert_equal "application/json", @response.content_type assert_equal 204, @response.status assert_equal "", @response.body @@ -433,10 +438,9 @@ class RespondWithControllerTest < ActionController::TestCase end def test_using_resource_for_delete_with_json_yields_no_content_on_success - Customer.any_instance.stubs(:to_json).returns('{"name": "David"}') Customer.any_instance.stubs(:destroyed?).returns(true) @request.accept = "application/json" - delete :using_resource + delete :using_resource_with_json assert_equal "application/json", @response.content_type assert_equal 204, @response.status assert_equal "", @response.body @@ -645,6 +649,8 @@ class RespondWithControllerTest < ActionController::TestCase get :index, format: 'csv' assert_equal Mime::CSV, @response.content_type assert_equal "c,s,v", @response.body + ensure + ActionController::Renderers.remove :csv end def test_raises_missing_renderer_if_an_api_behavior_with_no_renderer @@ -654,6 +660,23 @@ class RespondWithControllerTest < ActionController::TestCase end end + def test_removing_renderers + ActionController::Renderers.add :csv do |obj, options| + send_data obj.to_csv, type: Mime::CSV + end + @controller = CsvRespondWithController.new + @request.accept = "text/csv" + get :index, format: 'csv' + assert_equal Mime::CSV, @response.content_type + + ActionController::Renderers.remove :csv + assert_raise ActionController::MissingRenderer do + get :index, format: 'csv' + end + ensure + ActionController::Renderers.remove :csv + end + def test_error_is_raised_if_no_respond_to_is_declared_and_respond_with_is_called @controller = EmptyRespondWithController.new @request.accept = "*/*" diff --git a/actionpack/test/controller/new_base/bare_metal_test.rb b/actionpack/test/controller/new_base/bare_metal_test.rb index 7396c850ad..246ba099af 100644 --- a/actionpack/test/controller/new_base/bare_metal_test.rb +++ b/actionpack/test/controller/new_base/bare_metal_test.rb @@ -2,6 +2,8 @@ require "abstract_unit" module BareMetalTest class BareController < ActionController::Metal + include ActionController::RackDelegation + def index self.response_body = "Hello world" end @@ -81,8 +83,8 @@ module BareMetalTest assert_nil headers['Content-Length'] end - test "head :continue (101) does not return a content-type header" do - headers = HeadController.action(:continue).call(Rack::MockRequest.env_for("/")).second + test "head :switching_protocols (101) does not return a content-type header" do + headers = HeadController.action(:switching_protocols).call(Rack::MockRequest.env_for("/")).second assert_nil headers['Content-Type'] assert_nil headers['Content-Length'] end diff --git a/actionpack/test/controller/new_base/render_body_test.rb b/actionpack/test/controller/new_base/render_body_test.rb index fad848349a..f4a3db8b41 100644 --- a/actionpack/test/controller/new_base/render_body_test.rb +++ b/actionpack/test/controller/new_base/render_body_test.rb @@ -111,17 +111,17 @@ module RenderBody assert_status 404 end - test "rendering body with nil returns an empty body padded for Safari" do + test "rendering body with nil returns an empty body" do get "/render_body/with_layout/with_nil" - assert_body " " + assert_body "" assert_status 200 end - test "Rendering body with nil and custom status code returns an empty body padded for Safari and the status" do + test "Rendering body with nil and custom status code returns an empty body and the status" do get "/render_body/with_layout/with_nil_and_status" - assert_body " " + assert_body "" assert_status 403 end diff --git a/actionpack/test/controller/new_base/render_html_test.rb b/actionpack/test/controller/new_base/render_html_test.rb index bfe0271df7..fe11501eeb 100644 --- a/actionpack/test/controller/new_base/render_html_test.rb +++ b/actionpack/test/controller/new_base/render_html_test.rb @@ -114,17 +114,17 @@ module RenderHtml assert_status 404 end - test "rendering text with nil returns an empty body padded for Safari" do + test "rendering text with nil returns an empty body" do get "/render_html/with_layout/with_nil" - assert_body " " + assert_body "" assert_status 200 end - test "Rendering text with nil and custom status code returns an empty body padded for Safari and the status" do + test "Rendering text with nil and custom status code returns an empty body and the status" do get "/render_html/with_layout/with_nil_and_status" - assert_body " " + assert_body "" assert_status 403 end diff --git a/actionpack/test/controller/new_base/render_implicit_action_test.rb b/actionpack/test/controller/new_base/render_implicit_action_test.rb index 1e2191d417..5b4885f7e0 100644 --- a/actionpack/test/controller/new_base/render_implicit_action_test.rb +++ b/actionpack/test/controller/new_base/render_implicit_action_test.rb @@ -6,7 +6,7 @@ module RenderImplicitAction "render_implicit_action/simple/hello_world.html.erb" => "Hello world!", "render_implicit_action/simple/hyphen-ated.html.erb" => "Hello hyphen-ated!", "render_implicit_action/simple/not_implemented.html.erb" => "Not Implemented" - )] + ), ActionView::FileSystemResolver.new(File.expand_path('../../../controller', __FILE__))] def hello_world() end end @@ -33,10 +33,25 @@ module RenderImplicitAction assert_status 200 end + test "render does not traverse the file system" do + assert_raises(AbstractController::ActionNotFound) do + action_name = %w(.. .. fixtures shared).join(File::SEPARATOR) + SimpleController.action(action_name).call(Rack::MockRequest.env_for("/")) + end + end + test "available_action? returns true for implicit actions" do assert SimpleController.new.available_action?(:hello_world) assert SimpleController.new.available_action?(:"hyphen-ated") assert SimpleController.new.available_action?(:not_implemented) end + + test "available_action? does not allow File::SEPARATOR on the name" do + action_name = %w(evil .. .. path).join(File::SEPARATOR) + assert_equal false, SimpleController.new.available_action?(action_name.to_sym) + + action_name = %w(evil path).join(File::SEPARATOR) + assert_equal false, SimpleController.new.available_action?(action_name.to_sym) + end end end diff --git a/actionpack/test/controller/new_base/render_plain_test.rb b/actionpack/test/controller/new_base/render_plain_test.rb index dba2e9f13e..0e36d36b50 100644 --- a/actionpack/test/controller/new_base/render_plain_test.rb +++ b/actionpack/test/controller/new_base/render_plain_test.rb @@ -106,17 +106,17 @@ module RenderPlain assert_status 404 end - test "rendering text with nil returns an empty body padded for Safari" do + test "rendering text with nil returns an empty body" do get "/render_plain/with_layout/with_nil" - assert_body " " + assert_body "" assert_status 200 end - test "Rendering text with nil and custom status code returns an empty body padded for Safari and the status" do + test "Rendering text with nil and custom status code returns an empty body and the status" do get "/render_plain/with_layout/with_nil_and_status" - assert_body " " + assert_body "" assert_status 403 end diff --git a/actionpack/test/controller/new_base/render_template_test.rb b/actionpack/test/controller/new_base/render_template_test.rb index b7a9cf92f2..e87811776a 100644 --- a/actionpack/test/controller/new_base/render_template_test.rb +++ b/actionpack/test/controller/new_base/render_template_test.rb @@ -9,7 +9,7 @@ module RenderTemplate "locals.html.erb" => "The secret is <%= secret %>", "xml_template.xml.builder" => "xml.html do\n xml.p 'Hello'\nend", "with_raw.html.erb" => "Hello <%=raw '<strong>this is raw</strong>' %>", - "with_implicit_raw.html.erb" => "Hello <%== '<strong>this is also raw</strong>' %> in a html template", + "with_implicit_raw.html.erb" => "Hello <%== '<strong>this is also raw</strong>' %> in an html template", "with_implicit_raw.text.erb" => "Hello <%== '<strong>this is also raw</strong>' %> in a text template", "test/with_json.html.erb" => "<%= render :template => 'test/with_json', :formats => [:json] %>", "test/with_json.json.erb" => "<%= render :template => 'test/final', :formats => [:json] %>", @@ -114,7 +114,7 @@ module RenderTemplate get :with_implicit_raw - assert_body "Hello <strong>this is also raw</strong> in a html template" + assert_body "Hello <strong>this is also raw</strong> in an html template" assert_status 200 get :with_implicit_raw, format: 'text' diff --git a/actionpack/test/controller/new_base/render_text_test.rb b/actionpack/test/controller/new_base/render_text_test.rb index abb81d7e71..10bad57cd6 100644 --- a/actionpack/test/controller/new_base/render_text_test.rb +++ b/actionpack/test/controller/new_base/render_text_test.rb @@ -106,17 +106,17 @@ module RenderText assert_status 404 end - test "rendering text with nil returns an empty body padded for Safari" do + test "rendering text with nil returns an empty body" do get "/render_text/with_layout/with_nil" - assert_body " " + assert_body "" assert_status 200 end - test "Rendering text with nil and custom status code returns an empty body padded for Safari and the status" do + test "Rendering text with nil and custom status code returns an empty body and the status" do get "/render_text/with_layout/with_nil_and_status" - assert_body " " + assert_body "" assert_status 403 end diff --git a/actionpack/test/controller/parameters/always_permitted_parameters_test.rb b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb new file mode 100644 index 0000000000..059f310d49 --- /dev/null +++ b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb @@ -0,0 +1,29 @@ +require 'abstract_unit' +require 'action_controller/metal/strong_parameters' + +class AlwaysPermittedParametersTest < ActiveSupport::TestCase + def setup + ActionController::Parameters.action_on_unpermitted_parameters = :raise + ActionController::Parameters.always_permitted_parameters = %w( controller action format ) + end + + def teardown + ActionController::Parameters.action_on_unpermitted_parameters = false + ActionController::Parameters.always_permitted_parameters = %w( controller action ) + end + + test "shows deprecations warning on NEVER_UNPERMITTED_PARAMS" do + assert_deprecated do + ActionController::Parameters::NEVER_UNPERMITTED_PARAMS + end + end + + test "permits parameters that are whitelisted" do + params = ActionController::Parameters.new({ + book: { pages: 65 }, + format: "json" + }) + permitted = params.permit book: [:pages] + assert permitted.permitted? + end +end diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb index 33a91d72d9..aa894ffa17 100644 --- a/actionpack/test/controller/parameters/parameters_permit_test.rb +++ b/actionpack/test/controller/parameters/parameters_permit_test.rb @@ -167,9 +167,26 @@ class ParametersPermitTest < ActiveSupport::TestCase end end + # Strong params has an optimization to avoid looping every time you read + # a key whose value is an array and building a new object. We check that + # optimization here. test 'arrays are converted at most once' do params = ActionController::Parameters.new(foo: [{}]) - assert params[:foo].equal?(params[:foo]) + assert_same params[:foo], params[:foo] + end + + # Strong params has an internal cache to avoid duplicated loops in the most + # common usage pattern. See the docs of the method `converted_arrays`. + # + # This test checks that if we push a hash to an array (in-place modification) + # the cache does not get fooled, the hash is still wrapped as strong params, + # and not permitted. + test 'mutated arrays are detected' do + params = ActionController::Parameters.new(users: [{id: 1}]) + + permitted = params.permit(users: [:id]) + permitted[:users] << {injected: 1} + assert_not permitted[:users].last.permitted? end test "fetch doesnt raise ParameterMissing exception if there is a default" do diff --git a/actionpack/test/controller/params_wrapper_test.rb b/actionpack/test/controller/params_wrapper_test.rb index 11ccb6cf3b..645ecae220 100644 --- a/actionpack/test/controller/params_wrapper_test.rb +++ b/actionpack/test/controller/params_wrapper_test.rb @@ -337,14 +337,26 @@ class IrregularInflectionParamsWrapperTest < ActionController::TestCase tests ParamswrappernewsController def test_uses_model_attribute_names_with_irregular_inflection - ActiveSupport::Inflector.inflections do |inflect| - inflect.irregular 'paramswrappernews_item', 'paramswrappernews' - end + with_dup do + ActiveSupport::Inflector.inflections do |inflect| + inflect.irregular 'paramswrappernews_item', 'paramswrappernews' + end - with_default_wrapper_options do - @request.env['CONTENT_TYPE'] = 'application/json' - post :parse, { 'username' => 'sikachu', 'test_attr' => 'test_value' } - assert_parameters({ 'username' => 'sikachu', 'test_attr' => 'test_value', 'paramswrappernews_item' => { 'test_attr' => 'test_value' }}) + with_default_wrapper_options do + @request.env['CONTENT_TYPE'] = 'application/json' + post :parse, { 'username' => 'sikachu', 'test_attr' => 'test_value' } + assert_parameters({ 'username' => 'sikachu', 'test_attr' => 'test_value', 'paramswrappernews_item' => { 'test_attr' => 'test_value' }}) + end end end + + private + + def with_dup + original = ActiveSupport::Inflector::Inflections.instance_variable_get(:@__instance__)[:en] + ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original.dup) + yield + ensure + ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original) + end end diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb index 4331333b98..103ca9c776 100644 --- a/actionpack/test/controller/redirect_test.rb +++ b/actionpack/test/controller/redirect_test.rb @@ -90,6 +90,10 @@ class RedirectController < ActionController::Base redirect_to nil end + def redirect_to_params + redirect_to ActionController::Parameters.new(status: 200, protocol: 'javascript', f: '%0Aeval(name)') + end + def redirect_to_with_block redirect_to proc { "http://www.rubyonrails.org/" } end @@ -281,6 +285,12 @@ class RedirectTest < ActionController::TestCase end end + def test_redirect_to_params + assert_raise(ActionController::ActionControllerError) do + get :redirect_to_params + end + end + def test_redirect_to_with_block get :redirect_to_with_block assert_response :redirect diff --git a/actionpack/test/controller/render_json_test.rb b/actionpack/test/controller/render_json_test.rb index de8d1cbd9b..ada978aa11 100644 --- a/actionpack/test/controller/render_json_test.rb +++ b/actionpack/test/controller/render_json_test.rb @@ -101,7 +101,7 @@ class RenderJsonTest < ActionController::TestCase def test_render_json_with_callback xhr :get, :render_json_hello_world_with_callback - assert_equal 'alert({"hello":"world"})', @response.body + assert_equal '/**/alert({"hello":"world"})', @response.body assert_equal 'text/javascript', @response.content_type end diff --git a/actionpack/test/controller/render_other_test.rb b/actionpack/test/controller/render_other_test.rb index b5e74e373d..af50e11261 100644 --- a/actionpack/test/controller/render_other_test.rb +++ b/actionpack/test/controller/render_other_test.rb @@ -1,9 +1,5 @@ require 'abstract_unit' -ActionController.add_renderer :simon do |says, options| - self.content_type = Mime::TEXT - self.response_body = "Simon says: #{says}" -end class RenderOtherTest < ActionController::TestCase class TestController < ActionController::Base @@ -15,7 +11,14 @@ class RenderOtherTest < ActionController::TestCase tests TestController def test_using_custom_render_option + ActionController.add_renderer :simon do |says, options| + self.content_type = Mime::TEXT + self.response_body = "Simon says: #{says}" + end + get :render_simon_says assert_equal "Simon says: foo", @response.body + ensure + ActionController.remove_renderer :simon end end diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index 26806fb03f..9926130c02 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -242,6 +242,8 @@ class MetalTestController < ActionController::Metal include AbstractController::Rendering include ActionView::Rendering include ActionController::Rendering + include ActionController::RackDelegation + def accessing_logger_in_template render :inline => "<%= logger.class %>" @@ -387,10 +389,6 @@ end class EtagRenderTest < ActionController::TestCase tests TestControllerWithExtraEtags - def setup - super - end - def test_multiple_etags @request.if_none_match = etag(["123", 'ab', :cde, [:f]]) get :fresh diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index 5ab5141966..2a5aad9c0e 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -127,11 +127,12 @@ module RequestForgeryProtectionTests @token = "cf50faa3fe97702ca1ae" SecureRandom.stubs(:base64).returns(@token) + @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token ActionController::Base.request_forgery_protection_token = :custom_authenticity_token end def teardown - ActionController::Base.request_forgery_protection_token = nil + ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token end def test_should_render_form_with_token_tag @@ -376,11 +377,12 @@ class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController include RequestForgeryProtectionTests setup do + @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token ActionController::Base.request_forgery_protection_token = :custom_authenticity_token end teardown do - ActionController::Base.request_forgery_protection_token = nil + ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token end test 'should emit a csrf-param meta tag and a csrf-token meta tag' do @@ -462,16 +464,38 @@ end class CustomAuthenticityParamControllerTest < ActionController::TestCase def setup super - ActionController::Base.request_forgery_protection_token = :custom_token_name + @old_logger = ActionController::Base.logger + @logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + @token = "foobar" + @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token + ActionController::Base.request_forgery_protection_token = @token end def teardown - ActionController::Base.request_forgery_protection_token = :authenticity_token + ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token super end - def test_should_allow_custom_token - post :index, :custom_token_name => 'foobar' - assert_response :ok + def test_should_not_warn_if_form_authenticity_param_matches_form_authenticity_token + ActionController::Base.logger = @logger + SecureRandom.stubs(:base64).returns(@token) + + begin + post :index, :custom_token_name => 'foobar' + assert_equal 0, @logger.logged(:warn).size + ensure + ActionController::Base.logger = @old_logger + end + end + + def test_should_warn_if_form_authenticity_param_does_not_match_form_authenticity_token + ActionController::Base.logger = @logger + + begin + post :index, :custom_token_name => 'bazqux' + assert_equal 1, @logger.logged(:warn).size + ensure + ActionController::Base.logger = @old_logger + end end end diff --git a/actionpack/test/controller/required_params_test.rb b/actionpack/test/controller/required_params_test.rb index 25d0337212..6803dbbb62 100644 --- a/actionpack/test/controller/required_params_test.rb +++ b/actionpack/test/controller/required_params_test.rb @@ -24,10 +24,26 @@ class ActionControllerRequiredParamsTest < ActionController::TestCase post :create, { book: { name: "Mjallo!" } } assert_response :ok end + + test "required parameters with false value will not raise" do + post :create, { book: { name: false } } + assert_response :ok + end end class ParametersRequireTest < ActiveSupport::TestCase - test "required parameters must be present not merely not nil" do + + test "required parameters should accept and return false value" do + assert_equal(false, ActionController::Parameters.new(person: false).require(:person)) + end + + test "required parameters must not be nil" do + assert_raises(ActionController::ParameterMissing) do + ActionController::Parameters.new(person: nil).require(:person) + end + end + + test "required parameters must not be empty" do assert_raises(ActionController::ParameterMissing) do ActionController::Parameters.new(person: {}).require(:person) end diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index df453a0251..c18914cc8e 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -77,17 +77,18 @@ class LegacyRouteSetTests < ActiveSupport::TestCase include ActionDispatch::RoutingVerbs attr_reader :rs + attr_accessor :controller alias :routes :rs def setup - @rs = ::ActionDispatch::Routing::RouteSet.new + @rs = make_set @response = nil end def test_symbols_with_dashes rs.draw do get '/:artist/:song-omg', :to => lambda { |env| - resp = ActiveSupport::JSON.encode env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] + resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters [200, {}, [resp]] } end @@ -99,7 +100,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_id_with_dash rs.draw do get '/journey/:id', :to => lambda { |env| - resp = ActiveSupport::JSON.encode env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] + resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters [200, {}, [resp]] } end @@ -111,7 +112,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_dash_with_custom_regexp rs.draw do get '/:artist/:song-omg', :constraints => { :song => /\d+/ }, :to => lambda { |env| - resp = ActiveSupport::JSON.encode env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] + resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters [200, {}, [resp]] } end @@ -124,7 +125,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_pre_dash rs.draw do get '/:artist/omg-:song', :to => lambda { |env| - resp = ActiveSupport::JSON.encode env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] + resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters [200, {}, [resp]] } end @@ -136,7 +137,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_pre_dash_with_custom_regexp rs.draw do get '/:artist/omg-:song', :constraints => { :song => /\d+/ }, :to => lambda { |env| - resp = ActiveSupport::JSON.encode env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] + resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters [200, {}, [resp]] } end @@ -243,6 +244,32 @@ class LegacyRouteSetTests < ActiveSupport::TestCase assert_equal 'clients', get(URI('http://clients.example.org/')) end + def test_scoped_lambda + scope_called = false + rs.draw do + scope '/foo', :constraints => lambda { |req| scope_called = true } do + get '/', :to => lambda { |env| [200, {}, %w{default}] } + end + end + + assert_equal 'default', get(URI('http://www.example.org/foo/')) + assert scope_called, "scope constraint should be called" + end + + def test_scoped_lambda_with_get_lambda + inner_called = false + + rs.draw do + scope '/foo', :constraints => lambda { |req| flunk "should not be called" } do + get '/', :constraints => lambda { |req| inner_called = true }, + :to => lambda { |env| [200, {}, %w{default}] } + end + end + + assert_equal 'default', get(URI('http://www.example.org/foo/')) + assert inner_called, "inner constraint should be called" + end + def test_empty_string_match rs.draw do get '/:username', :constraints => { :username => /[^\/]+/ }, @@ -291,11 +318,16 @@ class LegacyRouteSetTests < ActiveSupport::TestCase assert_equal '/admin/user/show/10', url_for(rs, { :controller => 'admin/user', :action => 'show', :id => 10 }) - assert_equal '/admin/user/show', url_for(rs, { :action => 'show' }, { :controller => 'admin/user', :action => 'list', :id => '10' }) - assert_equal '/admin/user/list/10', url_for(rs, {}, { :controller => 'admin/user', :action => 'list', :id => '10' }) + get URI('http://test.host/admin/user/list/10') + + assert_equal({ :controller => 'admin/user', :action => 'list', :id => '10' }, + controller.request.path_parameters) - assert_equal '/admin/stuff', url_for(rs, { :controller => 'stuff' }, { :controller => 'admin/user', :action => 'list', :id => '10' }) - assert_equal '/stuff', url_for(rs, { :controller => '/stuff' }, { :controller => 'admin/user', :action => 'list', :id => '10' }) + assert_equal '/admin/user/show', controller.url_for({ :action => 'show', :only_path => true }) + assert_equal '/admin/user/list/10', controller.url_for({:only_path => true}) + + assert_equal '/admin/stuff', controller.url_for({ :controller => 'stuff', :only_path => true }) + assert_equal '/stuff', controller.url_for({ :controller => '/stuff', :only_path => true }) end def test_ignores_leading_slash @@ -419,14 +451,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase get 'page' => 'content#show_page', :as => 'pages', :host => 'foo.com' end routes = setup_for_named_route - routes.expects(:url_for).with({ - :host => 'foo.com', - :only_path => false, - :controller => 'content', - :action => 'show_page', - :use_route => 'pages' - }).once - routes.send(:pages_url) + assert_equal "http://foo.com/page", routes.pages_url end def setup_for_named_route(options = {}) @@ -499,9 +524,10 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_changing_controller rs.draw { get ':controller/:action/:id' } + get URI('http://test.host/admin/user/index/10') + assert_equal '/admin/stuff/show/10', - url_for(rs, {:controller => 'stuff', :action => 'show', :id => 10}, - {:controller => 'admin/user', :action => 'index'}) + controller.url_for({:controller => 'stuff', :action => 'show', :id => 10, :only_path => true}) end def test_paths_escaped @@ -560,8 +586,12 @@ class LegacyRouteSetTests < ActiveSupport::TestCase get '*path' => 'content#show_file' end + get URI('http://test.host/pages/boo') + assert_equal({:controller=>"content", :action=>"show_file", :path=>"pages/boo"}, + controller.request.path_parameters) + assert_equal '/pages/boo', - url_for(rs, {}, { :controller => 'content', :action => 'show_file', :path => %w(pages boo) }) + controller.url_for(:only_path => true) end def test_backwards @@ -570,7 +600,8 @@ class LegacyRouteSetTests < ActiveSupport::TestCase get ':controller(/:action(/:id))' end - assert_equal '/page/20', url_for(rs, { :id => 20 }, { :controller => 'pages', :action => 'show' }) + get URI('http://test.host/pages/show') + assert_equal '/page/20', controller.url_for({ :id => 20, :only_path => true }) assert_equal '/page/20', url_for(rs, { :controller => 'pages', :id => 20, :action => 'show' }) assert_equal '/pages/boo', url_for(rs, { :controller => 'pages', :action => 'boo' }) end @@ -611,7 +642,8 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_action_expiry rs.draw { get ':controller(/:action(/:id))' } - assert_equal '/content', url_for(rs, { :controller => 'content' }, { :controller => 'content', :action => 'show' }) + get URI('http://test.host/content/show') + assert_equal '/content', controller.url_for(:controller => 'content', :only_path => true) end def test_requirement_should_prevent_optional_id @@ -654,14 +686,18 @@ class LegacyRouteSetTests < ActiveSupport::TestCase assert_equal '/pages/2005/6/12', url_for(rs, { :controller => 'content', :action => 'list_pages', :year => 2005, :month => 6, :day => 12 }) + get URI('http://test.host/pages/2005/6/12') + assert_equal({ :controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12' }, + controller.request.path_parameters) + assert_equal '/pages/2005/6/4', - url_for(rs, { :day => 4 }, { :controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12' }) + controller.url_for({ :day => 4, :only_path => true }) assert_equal '/pages/2005/6', - url_for(rs, { :day => nil }, { :controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12' }) + controller.url_for({ :day => nil, :only_path => true }) assert_equal '/pages/2005', - url_for(rs, { :day => nil, :month => nil }, { :controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12' }) + controller.url_for({ :day => nil, :month => nil, :only_path => true }) end def test_root_url_generation_with_controller_and_action @@ -819,9 +855,15 @@ end class RouteSetTest < ActiveSupport::TestCase include RoutingTestHelpers + include ActionDispatch::RoutingVerbs - def set - @set ||= ROUTING::RouteSet.new + attr_reader :set + alias :routes :set + attr_accessor :controller + + def setup + super + @set = make_set end def request @@ -922,7 +964,8 @@ class RouteSetTest < ActiveSupport::TestCase get '/admin/users' => 'admin/users#index', :as => "users" end - MockController.build(set.url_helpers).new + get URI('http://test.host/people') + controller end def test_named_route_url_method @@ -1019,12 +1062,12 @@ class RouteSetTest < ActiveSupport::TestCase get '/:controller(/:action(/:id))' end - assert_equal({:controller => 'pages', :action => 'index'}, set.recognize_path('/pages')) - assert_equal({:controller => 'pages', :action => 'index'}, set.recognize_path('/pages/index')) - assert_equal({:controller => 'pages', :action => 'list'}, set.recognize_path('/pages/list')) + assert_equal({:controller => 'pages', :action => 'index'}, request_path_params('/pages')) + assert_equal({:controller => 'pages', :action => 'index'}, request_path_params('/pages/index')) + assert_equal({:controller => 'pages', :action => 'list'}, request_path_params('/pages/list')) - assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, set.recognize_path('/pages/show/10')) - assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, set.recognize_path('/page/10')) + assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, request_path_params('/pages/show/10')) + assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, request_path_params('/page/10')) end def test_route_constraints_on_request_object_with_anchors_are_valid @@ -1076,9 +1119,7 @@ class RouteSetTest < ActiveSupport::TestCase get "/people" => "missing#index" end - assert_raise(ActionController::RoutingError) { - set.recognize_path("/people", :method => :get) - } + assert_raises(ActionController::RoutingError) { request_path_params '/people' } end def test_recognize_with_encoded_id_and_regex @@ -1086,8 +1127,8 @@ class RouteSetTest < ActiveSupport::TestCase get 'page/:id' => 'pages#show', :id => /[a-zA-Z0-9\+]+/ end - assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, set.recognize_path('/page/10')) - assert_equal({:controller => 'pages', :action => 'show', :id => 'hello+world'}, set.recognize_path('/page/hello+world')) + assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, request_path_params('/page/10')) + assert_equal({:controller => 'pages', :action => 'show', :id => 'hello+world'}, request_path_params('/page/hello+world')) end def test_recognize_with_http_methods @@ -1100,40 +1141,40 @@ class RouteSetTest < ActiveSupport::TestCase delete "/people/:id" => "people#destroy" end - params = set.recognize_path("/people", :method => :get) + params = request_path_params("/people", :method => :get) assert_equal("index", params[:action]) - params = set.recognize_path("/people", :method => :post) + params = request_path_params("/people", :method => :post) assert_equal("create", params[:action]) - params = set.recognize_path("/people/5", :method => :put) + params = request_path_params("/people/5", :method => :put) assert_equal("update", params[:action]) - params = set.recognize_path("/people/5", :method => :patch) + params = request_path_params("/people/5", :method => :patch) assert_equal("update", params[:action]) assert_raise(ActionController::UnknownHttpMethod) { - set.recognize_path("/people", :method => :bacon) + request_path_params("/people", :method => :bacon) } - params = set.recognize_path("/people/5", :method => :get) + params = request_path_params("/people/5", :method => :get) assert_equal("show", params[:action]) assert_equal("5", params[:id]) - params = set.recognize_path("/people/5", :method => :put) + params = request_path_params("/people/5", :method => :put) assert_equal("update", params[:action]) assert_equal("5", params[:id]) - params = set.recognize_path("/people/5", :method => :patch) + params = request_path_params("/people/5", :method => :patch) assert_equal("update", params[:action]) assert_equal("5", params[:id]) - params = set.recognize_path("/people/5", :method => :delete) + params = request_path_params("/people/5", :method => :delete) assert_equal("destroy", params[:action]) assert_equal("5", params[:id]) assert_raise(ActionController::RoutingError) { - set.recognize_path("/people/5", :method => :post) + request_path_params("/people/5", :method => :post) } end @@ -1143,11 +1184,11 @@ class RouteSetTest < ActiveSupport::TestCase root :to => "people#index" end - params = set.recognize_path("/people", :method => :get) + params = request_path_params("/people", :method => :get) assert_equal("people", params[:controller]) assert_equal("index", params[:action]) - params = set.recognize_path("/", :method => :get) + params = request_path_params("/", :method => :get) assert_equal("people", params[:controller]) assert_equal("index", params[:action]) end @@ -1158,7 +1199,7 @@ class RouteSetTest < ActiveSupport::TestCase :year => /\d{4}/, :day => /\d{1,2}/, :month => /\d{1,2}/ end - params = set.recognize_path("/articles/2005/11/05/a-very-interesting-article", :method => :get) + params = request_path_params("/articles/2005/11/05/a-very-interesting-article", :method => :get) assert_equal("permalink", params[:action]) assert_equal("2005", params[:year]) assert_equal("11", params[:month]) @@ -1172,7 +1213,7 @@ class RouteSetTest < ActiveSupport::TestCase get '/profile' => 'profile#index' end - set.recognize_path("/profile") rescue nil + request_path_params("/profile") rescue nil assert !Object.const_defined?("Profiler__"), "Profiler should not be loaded" end @@ -1185,17 +1226,17 @@ class RouteSetTest < ActiveSupport::TestCase get "people/:id(.:format)" => "people#show" end - params = set.recognize_path("/people/5", :method => :get) + params = request_path_params("/people/5", :method => :get) assert_equal("show", params[:action]) assert_equal("5", params[:id]) - params = set.recognize_path("/people/5", :method => :put) + params = request_path_params("/people/5", :method => :put) assert_equal("update", params[:action]) - params = set.recognize_path("/people/5", :method => :patch) + params = request_path_params("/people/5", :method => :patch) assert_equal("update", params[:action]) - params = set.recognize_path("/people/5.png", :method => :get) + params = request_path_params("/people/5.png", :method => :get) assert_equal("show", params[:action]) assert_equal("5", params[:id]) assert_equal("png", params[:format]) @@ -1214,7 +1255,7 @@ class RouteSetTest < ActiveSupport::TestCase def test_root_map set.draw { root :to => 'people#index' } - params = set.recognize_path("", :method => :get) + params = request_path_params("", :method => :get) assert_equal("people", params[:controller]) assert_equal("index", params[:action]) end @@ -1228,7 +1269,7 @@ class RouteSetTest < ActiveSupport::TestCase end - params = set.recognize_path("/api/inventory", :method => :get) + params = request_path_params("/api/inventory", :method => :get) assert_equal("api/products", params[:controller]) assert_equal("inventory", params[:action]) end @@ -1240,7 +1281,7 @@ class RouteSetTest < ActiveSupport::TestCase end end - params = set.recognize_path("/api", :method => :get) + params = request_path_params("/api", :method => :get) assert_equal("api/products", params[:controller]) assert_equal("index", params[:action]) end @@ -1252,7 +1293,7 @@ class RouteSetTest < ActiveSupport::TestCase end end - params = set.recognize_path("/prefix/inventory", :method => :get) + params = request_path_params("/prefix/inventory", :method => :get) assert_equal("api/products", params[:controller]) assert_equal("inventory", params[:action]) end @@ -1264,38 +1305,36 @@ class RouteSetTest < ActiveSupport::TestCase end end - params = set.recognize_path("/inventory", :method => :get) + params = request_path_params("/inventory", :method => :get) assert_equal("api/products", params[:controller]) assert_equal("inventory", params[:action]) end - def test_generate_changes_controller_module - set.draw { get ':controller/:action/:id' } - current = { :controller => "bling/bloop", :action => "bap", :id => 9 } - - assert_equal "/foo/bar/baz/7", - url_for(set, { :controller => "foo/bar", :action => "baz", :id => 7 }, current) - end - def test_id_is_sticky_when_it_ought_to_be + @set = make_set false + set.draw do get ':controller/:id/:action' end - url = url_for(set, { :action => "destroy" }, { :controller => "people", :action => "show", :id => "7" }) - assert_equal "/people/7/destroy", url + get URI('http://test.host/people/7/show') + + assert_equal "/people/7/destroy", controller.url_for(:action => 'destroy', :only_path => true) end def test_use_static_path_when_possible + @set = make_set false + set.draw do get 'about' => "welcome#about" get ':controller/:action/:id' end - url = url_for(set, { :controller => "welcome", :action => "about" }, - { :controller => "welcome", :action => "get", :id => "7" }) + get URI('http://test.host/welcom/get/7') - assert_equal "/about", url + assert_equal "/about", controller.url_for(:controller => 'welcome', + :action => 'about', + :only_path => true) end def test_generate @@ -1330,38 +1369,51 @@ class RouteSetTest < ActiveSupport::TestCase end def test_named_routes_are_never_relative_to_modules + @set = make_set false + set.draw do get "/connection/manage(/:action)" => 'connection/manage#index' get "/connection/connection" => "connection/connection#index" get '/connection' => 'connection#index', :as => 'family_connection' end - url = url_for(set, { :controller => "connection" }, { :controller => 'connection/manage' }) + assert_equal({ :controller => 'connection/manage', + :action => 'index', }, request_path_params('/connection/manage')) + + url = controller.url_for({ :controller => "connection", :only_path => true }) assert_equal "/connection/connection", url - url = url_for(set, { :use_route => :family_connection, :controller => "connection" }, { :controller => 'connection/manage' }) + url = controller.url_for({ :use_route => :family_connection, + :controller => "connection", :only_path => true }) assert_equal "/connection", url end def test_action_left_off_when_id_is_recalled + @set = make_set false + set.draw do get ':controller(/:action(/:id))' end - assert_equal '/books', url_for(set, - {:controller => 'books', :action => 'index'}, - {:controller => 'books', :action => 'show', :id => '10'} - ) + + get URI('http://test.host/books/show/10') + + assert_equal '/books', controller.url_for(:controller => 'books', + :only_path => true, + :action => 'index') end def test_query_params_will_be_shown_when_recalled + @set = make_set false + set.draw do get 'show_weblog/:parameter' => 'weblog#show' get ':controller(/:action(/:id))' end - assert_equal '/weblog/edit?parameter=1', url_for(set, - {:action => 'edit', :parameter => 1}, - {:controller => 'weblog', :action => 'show', :parameter => 1} - ) + + get URI('http://test.host/weblog/show/1') + + assert_equal '/weblog/edit?parameter=1', controller.url_for( + {:action => 'edit', :parameter => 1, :only_path => true}) end def test_format_is_not_inherit @@ -1369,22 +1421,30 @@ class RouteSetTest < ActiveSupport::TestCase get '/posts(.:format)' => 'posts#index' end - assert_equal '/posts', url_for(set, - {:controller => 'posts'}, - {:controller => 'posts', :action => 'index', :format => 'xml'} - ) + get URI('http://test.host/posts.xml') + assert_equal({:controller => 'posts', :action => 'index', :format => 'xml'}, + controller.request.path_parameters) - assert_equal '/posts.xml', url_for(set, - {:controller => 'posts', :format => 'xml'}, - {:controller => 'posts', :action => 'index', :format => 'xml'} - ) + assert_equal '/posts', controller.url_for( + {:controller => 'posts', :only_path => true}) + + assert_equal '/posts.xml', controller.url_for( + {:controller => 'posts', :format => 'xml', :only_path => true}) end def test_expiry_determination_should_consider_values_with_to_param + @set = make_set false + set.draw { get 'projects/:project_id/:controller/:action' } - assert_equal '/projects/1/weblog/show', url_for(set, - { :action => 'show', :project_id => 1 }, - { :controller => 'weblog', :action => 'show', :project_id => '1' }) + + get URI('http://test.host/projects/1/weblog/show') + + assert_equal( + { :controller => 'weblog', :action => 'show', :project_id => '1' }, + controller.request.path_parameters) + + assert_equal '/projects/1/weblog/show', + controller.url_for({ :action => 'show', :project_id => 1, :only_path => true }) end def test_named_route_in_nested_resource @@ -1587,7 +1647,6 @@ class RouteSetTest < ActiveSupport::TestCase end def test_slashes_are_implied - @set = nil set.draw { get("/:controller(/:action(/:id))") } assert_equal '/content', url_for(set, { :controller => 'content', :action => 'index' }) @@ -1685,7 +1744,43 @@ class RouteSetTest < ActiveSupport::TestCase assert_equal '/ibocorp', url_for(set, { :controller => 'ibocorp', :action => "show", :page => 1 }) end + include ActionDispatch::RoutingVerbs + + class TestSet < ROUTING::RouteSet + def initialize(block) + @block = block + super() + end + + class Dispatcher < ROUTING::RouteSet::Dispatcher + def initialize(defaults, set, block) + super(defaults) + @block = block + @set = set + end + + def controller_reference(controller_param) + block = @block + set = @set + Class.new(ActionController::Base) { + include set.url_helpers + define_method(:process) { |name| block.call(self) } + def to_a; [200, {}, []]; end + } + end + end + + def dispatcher defaults + TestSet::Dispatcher.new defaults, self, @block + end + end + + alias :routes :set + def test_generate_with_optional_params_recalls_last_request + controller = nil + @set = TestSet.new ->(c) { controller = c } + set.draw do get "blog/", :controller => "blog", :action => "index" @@ -1700,23 +1795,29 @@ class RouteSetTest < ActiveSupport::TestCase get "*anything", :controller => "blog", :action => "unknown_request" end - assert_equal({:controller => "blog", :action => "index"}, set.recognize_path("/blog")) - assert_equal({:controller => "blog", :action => "show", :id => "123"}, set.recognize_path("/blog/show/123")) - assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :day => nil, :month => nil }, set.recognize_path("/blog/2004")) - assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :month => "12", :day => nil }, set.recognize_path("/blog/2004/12")) - assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :month => "12", :day => "25"}, set.recognize_path("/blog/2004/12/25")) - assert_equal({:controller => "articles", :action => "edit", :id => "123"}, set.recognize_path("/blog/articles/edit/123")) - assert_equal({:controller => "articles", :action => "show_stats"}, set.recognize_path("/blog/articles/show_stats")) - assert_equal({:controller => "blog", :action => "unknown_request", :anything => "blog/wibble"}, set.recognize_path("/blog/wibble")) - assert_equal({:controller => "blog", :action => "unknown_request", :anything => "junk"}, set.recognize_path("/junk")) + recognize_path = ->(path) { + get(URI("http://example.org" + path)) + controller.request.path_parameters + } - last_request = set.recognize_path("/blog/2006/07/28").freeze - assert_equal({:controller => "blog", :action => "show_date", :year => "2006", :month => "07", :day => "28"}, last_request) - assert_equal("/blog/2006/07/25", url_for(set, { :day => 25 }, last_request)) - assert_equal("/blog/2005", url_for(set, { :year => 2005 }, last_request)) - assert_equal("/blog/show/123", url_for(set, { :action => "show" , :id => 123 }, last_request)) - assert_equal("/blog/2006", url_for(set, { :year => 2006 }, last_request)) - assert_equal("/blog/2006", url_for(set, { :year => 2006, :month => nil }, last_request)) + assert_equal({:controller => "blog", :action => "index"}, recognize_path.("/blog")) + assert_equal({:controller => "blog", :action => "show", :id => "123"}, recognize_path.("/blog/show/123")) + assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :day => nil, :month => nil }, recognize_path.("/blog/2004")) + assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :month => "12", :day => nil }, recognize_path.("/blog/2004/12")) + assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :month => "12", :day => "25"}, recognize_path.("/blog/2004/12/25")) + assert_equal({:controller => "articles", :action => "edit", :id => "123"}, recognize_path.("/blog/articles/edit/123")) + assert_equal({:controller => "articles", :action => "show_stats"}, recognize_path.("/blog/articles/show_stats")) + assert_equal({:controller => "blog", :action => "unknown_request", :anything => "blog/wibble"}, recognize_path.("/blog/wibble")) + assert_equal({:controller => "blog", :action => "unknown_request", :anything => "junk"}, recognize_path.("/junk")) + + get URI('http://example.org/blog/2006/07/28') + + assert_equal({:controller => "blog", :action => "show_date", :year => "2006", :month => "07", :day => "28"}, controller.request.path_parameters) + assert_equal("/blog/2006/07/25", controller.url_for({ :day => 25, :only_path => true })) + assert_equal("/blog/2005", controller.url_for({ :year => 2005, :only_path => true })) + assert_equal("/blog/show/123", controller.url_for({ :action => "show" , :id => 123, :only_path => true })) + assert_equal("/blog/2006", controller.url_for({ :year => 2006, :only_path => true })) + assert_equal("/blog/2006", controller.url_for({ :year => 2006, :month => nil, :only_path => true })) end private @@ -1789,6 +1890,9 @@ class RackMountIntegrationTests < ActiveSupport::TestCase root :to => "news#index" } + attr_reader :routes + attr_reader :controller + def setup @routes = ActionDispatch::Routing::RouteSet.new @routes.draw(&Mapping) diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb index 4df2f8b98d..c002cf4d8f 100644 --- a/actionpack/test/controller/send_file_test.rb +++ b/actionpack/test/controller/send_file_test.rb @@ -9,6 +9,7 @@ end class SendFileController < ActionController::Base include TestFileUtils + include ActionController::Testing layout "layouts/standard" # to make sure layouts don't interfere attr_writer :options @@ -30,11 +31,8 @@ class SendFileWithActionControllerLive < SendFileController end class SendFileTest < ActionController::TestCase - tests SendFileController include TestFileUtils - Mime::Type.register "image/png", :png unless defined? Mime::PNG - def setup @controller = SendFileController.new @request = ActionController::TestRequest.new @@ -201,8 +199,6 @@ class SendFileTest < ActionController::TestCase end end - tests SendFileWithActionControllerLive - def test_send_file_with_action_controller_live @controller = SendFileWithActionControllerLive.new @controller.options = { :content_type => "application/x-ruby" } diff --git a/actionpack/test/controller/show_exceptions_test.rb b/actionpack/test/controller/show_exceptions_test.rb index ff23b22040..f7eba1ef43 100644 --- a/actionpack/test/controller/show_exceptions_test.rb +++ b/actionpack/test/controller/show_exceptions_test.rb @@ -32,7 +32,7 @@ module ShowExceptions test 'show diagnostics from a local ip if show_detailed_exceptions? is set to request.local?' do @app = ShowExceptionsController.action(:boom) - ['127.0.0.1', '127.0.0.127', '::1', '0:0:0:0:0:0:0:1', '0:0:0:0:0:0:0:1%0'].each do |ip_address| + ['127.0.0.1', '127.0.0.127', '127.12.1.1', '::1', '0:0:0:0:0:0:0:1', '0:0:0:0:0:0:0:1%0'].each do |ip_address| self.remote_addr = ip_address get '/' assert_match(/boom/, body) diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index fbc10baf21..3b3b15c061 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -164,7 +164,7 @@ XML end class DefaultUrlOptionsCachingController < ActionController::Base - before_filter { @dynamic_opt = 'opt' } + before_action { @dynamic_opt = 'opt' } def test_url_options_reset render text: url_for(params) @@ -662,7 +662,7 @@ XML def test_id_converted_to_string get :test_params, :id => 20, :foo => Object.new - assert_kind_of String, @request.path_parameters['id'] + assert_kind_of String, @request.path_parameters[:id] end def test_array_path_parameter_handled_properly @@ -673,17 +673,17 @@ XML end get :test_params, :path => ['hello', 'world'] - assert_equal ['hello', 'world'], @request.path_parameters['path'] - assert_equal 'hello/world', @request.path_parameters['path'].to_param + assert_equal ['hello', 'world'], @request.path_parameters[:path] + assert_equal 'hello/world', @request.path_parameters[:path].to_param end end def test_assert_realistic_path_parameters get :test_params, :id => 20, :foo => Object.new - # All elements of path_parameters should use string keys + # All elements of path_parameters should use Symbol keys @request.path_parameters.keys.each do |key| - assert_kind_of String, key + assert_kind_of Symbol, key end end @@ -737,12 +737,12 @@ XML assert_equal "baz", @request.filtered_parameters[:foo] end - def test_symbolized_path_params_reset_after_request + def test_path_params_reset_after_request get :test_params, :id => "foo" - assert_equal "foo", @request.symbolized_path_parameters[:id] + assert_equal "foo", @request.path_parameters[:id] @request.recycle! get :test_params - assert_nil @request.symbolized_path_parameters[:id] + assert_nil @request.path_parameters[:id] end def test_request_protocol_is_reset_after_request diff --git a/actionpack/test/controller/url_for_integration_test.rb b/actionpack/test/controller/url_for_integration_test.rb index 6c2311e7a5..24a09222b1 100644 --- a/actionpack/test/controller/url_for_integration_test.rb +++ b/actionpack/test/controller/url_for_integration_test.rb @@ -6,6 +6,7 @@ require 'active_support/core_ext/object/with_options' module ActionPack class URLForIntegrationTest < ActiveSupport::TestCase include RoutingTestHelpers + include ActionDispatch::RoutingVerbs Model = Struct.new(:to_param) @@ -61,8 +62,11 @@ module ActionPack root :to => "news#index" } + attr_reader :routes + attr_accessor :controller + def setup - @routes = ActionDispatch::Routing::RouteSet.new + @routes = make_set false @routes.draw(&Mapping) end @@ -70,9 +74,9 @@ module ActionPack ['/admin/users',[ { :use_route => 'admin_users' }]], ['/admin/users',[ { :controller => 'admin/users' }]], ['/admin/users',[ { :controller => 'admin/users', :action => 'index' }]], - ['/admin/users',[ { :action => 'index' }, { :controller => 'admin/users' }]], - ['/admin/users',[ { :controller => 'users', :action => 'index' }, { :controller => 'admin/accounts' }]], - ['/people',[ { :controller => '/people', :action => 'index' }, { :controller => 'admin/accounts' }]], + ['/admin/users',[ { :action => 'index' }, { :controller => 'admin/users', :action => 'index' }, '/admin/users']], + ['/admin/users',[ { :controller => 'users', :action => 'index' }, { :controller => 'admin/accounts', :action => 'show', :id => '1' }, '/admin/accounts/show/1']], + ['/people',[ { :controller => '/people', :action => 'index' }, {:controller=>"admin/accounts", :action=>"foo", :id=>"bar"}, '/admin/accounts/foo/bar']], ['/admin/posts',[ { :controller => 'admin/posts' }]], ['/admin/posts/new',[ { :controller => 'admin/posts', :action => 'new' }]], @@ -86,11 +90,11 @@ module ActionPack ['/archive?year=january',[ { :controller => 'archive', :action => 'index', :year => 'january' }]], ['/people',[ { :controller => 'people', :action => 'index' }]], - ['/people',[ { :action => 'index' }, { :controller => 'people' }]], - ['/people',[ { :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }]], - ['/people',[ { :controller => 'people', :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }]], - ['/people',[ {}, { :controller => 'people', :action => 'index' }]], - ['/people/1',[ { :controller => 'people', :action => 'show' }, { :controller => 'people', :action => 'show', :id => '1' }]], + ['/people',[ { :action => 'index' }, { :controller => 'people', :action => 'index' }, '/people']], + ['/people',[ { :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']], + ['/people',[ { :controller => 'people', :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']], + ['/people',[ {}, { :controller => 'people', :action => 'index' }, '/people']], + ['/people/1',[ { :controller => 'people', :action => 'show' }, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']], ['/people/new',[ { :use_route => 'new_person' }]], ['/people/new',[ { :controller => 'people', :action => 'new' }]], ['/people/1',[ { :use_route => 'person', :id => '1' }]], @@ -98,11 +102,11 @@ module ActionPack ['/people/1.xml',[ { :controller => 'people', :action => 'show', :id => '1', :format => 'xml' }]], ['/people/1',[ { :controller => 'people', :action => 'show', :id => 1 }]], ['/people/1',[ { :controller => 'people', :action => 'show', :id => Model.new('1') }]], - ['/people/1',[ { :action => 'show', :id => '1' }, { :controller => 'people', :action => 'index' }]], - ['/people/1',[ { :action => 'show', :id => 1 }, { :controller => 'people', :action => 'show', :id => '1' }]], - ['/people',[ { :controller => 'people', :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }]], - ['/people/1',[ {}, { :controller => 'people', :action => 'show', :id => '1' }]], - ['/people/1',[ { :controller => 'people', :action => 'show' }, { :controller => 'people', :action => 'index', :id => '1' }]], + ['/people/1',[ { :action => 'show', :id => '1' }, { :controller => 'people', :action => 'index' }, '/people']], + ['/people/1',[ { :action => 'show', :id => 1 }, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']], + ['/people',[ { :controller => 'people', :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']], + ['/people/1',[ {}, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']], + ['/people/1',[ { :controller => 'people', :action => 'show' }, { :controller => 'people', :action => 'index', :id => '1' }, '/people/index/1']], ['/people/1/edit',[ { :controller => 'people', :action => 'edit', :id => '1' }]], ['/people/1/edit.xml',[ { :controller => 'people', :action => 'edit', :id => '1', :format => 'xml' }]], ['/people/1/edit',[ { :use_route => 'edit_person', :id => '1' }]], @@ -118,16 +122,15 @@ module ActionPack ['/project',[ { :controller => 'project', :action => 'index' }]], ['/projects/1',[ { :controller => 'project', :action => 'index', :project_id => '1' }]], - ['/projects/1',[ { :controller => 'project', :action => 'index'}, {:project_id => '1' }]], + ['/projects/1',[ { :controller => 'project', :action => 'index'}, {:project_id => '1', :controller => 'project', :action => 'index' }, '/projects/1']], ['/projects/1',[ { :use_route => 'project', :controller => 'project', :action => 'index', :project_id => '1' }]], - ['/projects/1',[ { :use_route => 'project', :controller => 'project', :action => 'index' }, { :project_id => '1' }]], + ['/projects/1',[ { :use_route => 'project', :controller => 'project', :action => 'index' }, { :controller => 'project', :action => 'index', :project_id => '1' }, '/projects/1']], ['/clients',[ { :controller => 'projects', :action => 'index' }]], ['/clients?project_id=1',[ { :controller => 'projects', :action => 'index', :project_id => '1' }]], - ['/clients',[ { :controller => 'projects', :action => 'index' }, { :project_id => '1' }]], - ['/clients',[ { :action => 'index' }, { :controller => 'projects', :action => 'index', :project_id => '1' }]], + ['/clients',[ { :controller => 'projects', :action => 'index' }, { :project_id => '1', :controller => 'project', :action => 'index' }, '/projects/1']], - ['/comment/20',[ { :id => 20 }, { :controller => 'comments', :action => 'show' }]], + ['/comment/20',[ { :id => 20 }, { :controller => 'comments', :action => 'show' }, '/comments/show']], ['/comment/20',[ { :controller => 'comments', :id => 20, :action => 'show' }]], ['/comments/boo',[ { :controller => 'comments', :action => 'boo' }]], @@ -144,24 +147,21 @@ module ActionPack ['/notes',[ { :page_id => nil, :controller => 'notes' }]], ['/notes',[ { :controller => 'notes' }]], ['/notes/print',[ { :controller => 'notes', :action => 'print' }]], - ['/notes/print',[ {}, { :controller => 'notes', :action => 'print' }]], - - ['/notes/index/1',[ { :controller => 'notes' }, { :controller => 'notes', :id => '1' }]], - ['/notes/index/1',[ { :controller => 'notes' }, { :controller => 'notes', :id => '1', :foo => 'bar' }]], - ['/notes/index/1',[ { :controller => 'notes' }, { :controller => 'notes', :id => '1' }]], - ['/notes/index/1',[ { :action => 'index' }, { :controller => 'notes', :id => '1' }]], - ['/notes/index/1',[ {}, { :controller => 'notes', :id => '1' }]], - ['/notes/show/1',[ {}, { :controller => 'notes', :action => 'show', :id => '1' }]], - ['/notes/index/1',[ { :controller => 'notes', :id => '1' }, { :foo => 'bar' }]], - ['/posts',[ { :controller => 'posts' }, { :controller => 'notes', :action => 'show', :id => '1' }]], - ['/notes/list',[ { :action => 'list' }, { :controller => 'notes', :action => 'show', :id => '1' }]], + ['/notes/print',[ {}, { :controller => 'notes', :action => 'print' }, '/notes/print']], + + ['/notes/index/1',[ { :controller => 'notes' }, { :controller => 'notes', :action => 'index', :id => '1' }, '/notes/index/1']], + ['/notes/index/1',[ { :controller => 'notes' }, { :controller => 'notes', :id => '1', :action => 'index' }, '/notes/index/1']], + ['/notes/index/1',[ { :action => 'index' }, { :controller => 'notes', :id => '1', :action => 'index' }, '/notes/index/1']], + ['/notes/index/1',[ {}, { :controller => 'notes', :id => '1', :action => 'index' }, '/notes/index/1']], + ['/notes/show/1',[ {}, { :controller => 'notes', :action => 'show', :id => '1' }, '/notes/show/1']], + ['/posts',[ { :controller => 'posts' }, { :controller => 'notes', :action => 'show', :id => '1' }, '/notes/show/1']], + ['/notes/list',[ { :action => 'list' }, { :controller => 'notes', :action => 'show', :id => '1' }, '/notes/show/1']], ['/posts/ping',[ { :controller => 'posts', :action => 'ping' }]], ['/posts/show/1',[ { :controller => 'posts', :action => 'show', :id => '1' }]], ['/posts',[ { :controller => 'posts' }]], ['/posts',[ { :controller => 'posts', :action => 'index' }]], - ['/posts',[ { :controller => 'posts' }, { :controller => 'posts', :action => 'index' }]], - ['/posts/create',[ { :action => 'create' }, { :controller => 'posts' }]], + ['/posts/create',[ { :action => 'create' }, {:day=>nil, :month=>nil, :controller=>"posts", :action=>"show_date"}, '/blog']], ['/posts?foo=bar',[ { :controller => 'posts', :foo => 'bar' }]], ['/posts?foo%5B%5D=bar&foo%5B%5D=baz', [{ :controller => 'posts', :foo => ['bar', 'baz'] }]], ['/posts?page=2', [{ :controller => 'posts', :page => 2 }]], @@ -169,9 +169,20 @@ module ActionPack ['/news.rss', [{ :controller => 'news', :action => 'index', :format => 'rss' }]], ].each_with_index do |(url, params), i| - define_method("test_#{url.gsub(/\W/, '_')}_#{i}") do - assert_equal url, url_for(@routes, *params), params.inspect - end + if params.length > 1 + hash, path_params, route = *params + hash[:only_path] = true + + define_method("test_#{url.gsub(/\W/, '_')}_#{i}") do + get URI('http://test.host' + route.to_s) + assert_equal path_params, controller.request.path_parameters + assert_equal url, controller.url_for(hash), params.inspect + end + else + define_method("test_#{url.gsub(/\W/, '_')}_#{i}") do + assert_equal url, url_for(@routes, params.first), params.inspect + end + end end end end diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb index a8035e5bd7..9f086af664 100644 --- a/actionpack/test/controller/url_for_test.rb +++ b/actionpack/test/controller/url_for_test.rb @@ -11,6 +11,26 @@ module AbstractController W.default_url_options.clear end + def test_nested_optional + klass = Class.new { + include ActionDispatch::Routing::RouteSet.new.tap { |r| + r.draw { + get "/foo/(:bar/(:baz))/:zot", :as => 'fun', + :controller => :articles, + :action => :index + } + }.url_helpers + self.default_url_options[:host] = 'example.com' + } + + path = klass.new.fun_path({:controller => :articles, + :baz => "baz", + :zot => "zot", + :only_path => true }) + # :bar key isn't provided + assert_equal '/foo/zot', path + end + def add_host! W.default_url_options[:host] = 'www.basecamphq.com' end @@ -75,7 +95,7 @@ module AbstractController end def test_subdomain_may_be_object - model = mock(:to_param => 'api') + model = Class.new { def self.to_param; 'api'; end } add_host! assert_equal('http://api.basecamphq.com/c/a/i', W.new.url_for(:subdomain => model, :controller => 'c', :action => 'a', :id => 'i') @@ -169,6 +189,18 @@ module AbstractController ) end + def test_without_protocol_and_with_port + add_host! + add_port! + + assert_equal('//www.basecamphq.com:3000/c/a/i', + W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => '//') + ) + assert_equal('//www.basecamphq.com:3000/c/a/i', + W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => false) + ) + end + def test_trailing_slash add_host! options = {:controller => 'foo', :trailing_slash => true, :action => 'bar', :id => '33'} @@ -255,12 +287,12 @@ module AbstractController # We need to create a new class in order to install the new named route. kls = Class.new { include set.url_helpers } controller = kls.new - assert controller.respond_to?(:home_url) + assert_respond_to controller, :home_url assert_equal '/brave/new/world', - controller.send(:url_for, :controller => 'brave', :action => 'new', :id => 'world', :only_path => true) + controller.url_for(:controller => 'brave', :action => 'new', :id => 'world', :only_path => true) - assert_equal("/home/sweet/home/alabama", controller.send(:home_url, :user => 'alabama', :host => 'unused', :only_path => true)) - assert_equal("/home/sweet/home/alabama", controller.send(:home_path, 'alabama')) + assert_equal("/home/sweet/home/alabama", controller.home_path(:user => 'alabama', :host => 'unused', :only_path => true)) + assert_equal("/home/sweet/home/alabama", controller.home_path('alabama')) end end diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb index 8660deb634..24526fb00e 100644 --- a/actionpack/test/dispatch/debug_exceptions_test.rb +++ b/actionpack/test/dispatch/debug_exceptions_test.rb @@ -10,6 +10,8 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest @closed = false end + # We're obliged to implement this (even though it doesn't actually + # get called here) to properly comply with the Rack SPEC def each end diff --git a/actionpack/test/dispatch/header_test.rb b/actionpack/test/dispatch/header_test.rb index 9e37b96951..e2b38c23bc 100644 --- a/actionpack/test/dispatch/header_test.rb +++ b/actionpack/test/dispatch/header_test.rb @@ -55,6 +55,8 @@ class HeaderTest < ActiveSupport::TestCase test "key?" do assert @headers.key?("CONTENT_TYPE") assert @headers.include?("CONTENT_TYPE") + assert @headers.key?("Content-Type") + assert @headers.include?("Content-Type") end test "fetch with block" do diff --git a/actionpack/test/dispatch/mapper_test.rb b/actionpack/test/dispatch/mapper_test.rb index 58457b0c28..3e554a9cf6 100644 --- a/actionpack/test/dispatch/mapper_test.rb +++ b/actionpack/test/dispatch/mapper_test.rb @@ -3,7 +3,7 @@ require 'abstract_unit' module ActionDispatch module Routing class MapperTest < ActiveSupport::TestCase - class FakeSet + class FakeSet < ActionDispatch::Routing::RouteSet attr_reader :routes alias :set :routes @@ -38,7 +38,7 @@ module ActionDispatch def test_mapping_requirements options = { :controller => 'foo', :action => 'bar', :via => :get } - m = Mapper::Mapping.new FakeSet.new, {}, '/store/:name(*rest)', options + m = Mapper::Mapping.build({}, FakeSet.new, '/store/:name(*rest)', options) _, _, requirements, _ = m.to_route assert_equal(/.+?/, requirements[:rest]) end @@ -72,7 +72,7 @@ module ActionDispatch mapper = Mapper.new fakeset mapper.get '/*path/foo/:bar', :to => 'pages#show' assert_equal '/*path/foo/:bar(.:format)', fakeset.conditions.first[:path_info] - assert_nil fakeset.requirements.first[:path] + assert_equal(/.+?/, fakeset.requirements.first[:path]) end def test_map_wildcard_with_multiple_wildcard @@ -80,7 +80,7 @@ module ActionDispatch mapper = Mapper.new fakeset mapper.get '/*foo/*bar', :to => 'pages#show' assert_equal '/*foo/*bar(.:format)', fakeset.conditions.first[:path_info] - assert_nil fakeset.requirements.first[:foo] + assert_equal(/.+?/, fakeset.requirements.first[:foo]) assert_equal(/.+?/, fakeset.requirements.first[:bar]) end diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb index 981cf2426e..d29cc8473e 100644 --- a/actionpack/test/dispatch/mime_type_test.rb +++ b/actionpack/test/dispatch/mime_type_test.rb @@ -1,8 +1,6 @@ require 'abstract_unit' class MimeTypeTest < ActiveSupport::TestCase - Mime::Type.register "image/png", :png unless defined? Mime::PNG - Mime::Type.register "application/pdf", :pdf unless defined? Mime::PDF test "parse single" do Mime::LOOKUP.keys.each do |mime_type| diff --git a/actionpack/test/dispatch/mount_test.rb b/actionpack/test/dispatch/mount_test.rb index cdf00d84fb..d5a4d8ee11 100644 --- a/actionpack/test/dispatch/mount_test.rb +++ b/actionpack/test/dispatch/mount_test.rb @@ -1,12 +1,18 @@ require 'abstract_unit' +require 'rails/engine' class TestRoutingMount < ActionDispatch::IntegrationTest Router = ActionDispatch::Routing::RouteSet.new - class FakeEngine + class AppWithRoutes < Rails::Engine def self.routes @routes ||= ActionDispatch::Routing::RouteSet.new end + end + + # Test for mounting apps that respond to routes, but aren't Rails-like apps. + class SinatraLikeApp + def self.routes; Object.new; end def self.call(env) [200, {"Content-Type" => "text/html"}, ["OK"]] @@ -21,20 +27,23 @@ class TestRoutingMount < ActionDispatch::IntegrationTest mount SprocketsApp, :at => "/sprockets" mount SprocketsApp => "/shorthand" - mount FakeEngine, :at => "/fakeengine", :as => :fake - mount FakeEngine, :at => "/getfake", :via => :get + mount SinatraLikeApp, :at => "/fakeengine", :as => :fake + mount SinatraLikeApp, :at => "/getfake", :via => :get scope "/its_a" do mount SprocketsApp, :at => "/sprocket" end resources :users do - mount FakeEngine, :at => "/fakeengine", :as => :fake_mounted_at_resource + mount AppWithRoutes, :at => "/fakeengine", :as => :fake_mounted_at_resource end + + mount SprocketsApp, :at => "/", :via => :get end + APP = RoutedRackApp.new Router def app - Router + APP end def test_app_name_is_properly_generated_when_engine_is_mounted_in_resources @@ -44,6 +53,11 @@ class TestRoutingMount < ActionDispatch::IntegrationTest "A named route should be defined with a parent's prefix" end + def test_mounting_at_root_path + get "/omg" + assert_equal " -- /omg", response.body + end + def test_mounting_sets_script_name get "/sprockets/omg" assert_equal "/sprockets -- /omg", response.body diff --git a/actionpack/test/dispatch/prefix_generation_test.rb b/actionpack/test/dispatch/prefix_generation_test.rb index 08501d19c0..c6e4eefa7a 100644 --- a/actionpack/test/dispatch/prefix_generation_test.rb +++ b/actionpack/test/dispatch/prefix_generation_test.rb @@ -1,5 +1,6 @@ require 'abstract_unit' require 'rack/test' +require 'rails/engine' module TestGenerationPrefix class Post @@ -15,12 +16,15 @@ module TestGenerationPrefix ActiveModel::Name.new(klass) end + + def to_model; self; end + def persisted?; true; end end class WithMountedEngine < ActionDispatch::IntegrationTest include Rack::Test::Methods - class BlogEngine + class BlogEngine < Rails::Engine def self.routes @routes ||= begin routes = ActionDispatch::Routing::RouteSet.new diff --git a/actionpack/test/dispatch/reloader_test.rb b/actionpack/test/dispatch/reloader_test.rb index ce9ccfcee8..62e8197e20 100644 --- a/actionpack/test/dispatch/reloader_test.rb +++ b/actionpack/test/dispatch/reloader_test.rb @@ -3,6 +3,11 @@ require 'abstract_unit' class ReloaderTest < ActiveSupport::TestCase Reloader = ActionDispatch::Reloader + teardown do + Reloader.reset_callbacks :prepare + Reloader.reset_callbacks :cleanup + end + def test_prepare_callbacks a = b = c = nil Reloader.to_prepare { |*args| a = b = c = 1 } diff --git a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb index 2a2f92b5b3..926472163e 100644 --- a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb @@ -8,7 +8,11 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest end def parse - self.class.last_request_parameters = request.request_parameters + self.class.last_request_parameters = begin + request.request_parameters + rescue EOFError + {} + end self.class.last_parameters = request.parameters head :ok end @@ -145,7 +149,7 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest test "does not raise EOFError on GET request with multipart content-type" do with_routing do |set| set.draw do - get ':action', to: 'multipart_params_parsing_test/test' + get ':action', controller: 'multipart_params_parsing_test/test' end headers = { "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x" } get "/parse", {}, headers @@ -174,7 +178,7 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest def with_test_routing with_routing do |set| set.draw do - post ':action', :to => 'multipart_params_parsing_test/test' + post ':action', :controller => 'multipart_params_parsing_test/test' end yield end diff --git a/actionpack/test/dispatch/request/query_string_parsing_test.rb b/actionpack/test/dispatch/request/query_string_parsing_test.rb index d82493140f..4e99c26e03 100644 --- a/actionpack/test/dispatch/request/query_string_parsing_test.rb +++ b/actionpack/test/dispatch/request/query_string_parsing_test.rb @@ -105,6 +105,7 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest end test "perform_deep_munge" do + old_perform_deep_munge = ActionDispatch::Request::Utils.perform_deep_munge ActionDispatch::Request::Utils.perform_deep_munge = false begin assert_parses({"action" => nil}, "action") @@ -115,7 +116,7 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest assert_parses({"action" => {"foo" => [{"bar" => nil}]}}, "action[foo][][bar]") assert_parses({"action" => ['1',nil]}, "action[]=1&action[]") ensure - ActionDispatch::Request::Utils.perform_deep_munge = true + ActionDispatch::Request::Utils.perform_deep_munge = old_perform_deep_munge end end diff --git a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb index 9a77454f30..1de05cbf09 100644 --- a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb @@ -130,10 +130,7 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest end test "ambiguous params returns a bad request" do - with_routing do |set| - set.draw do - post ':action', to: ::UrlEncodedParamsParsingTest::TestController - end + with_test_routing do post "/parse", "foo[]=bar&foo[4]=bar" assert_response :bad_request end diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index 6e21b4a258..6737609567 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -152,9 +152,12 @@ class RequestIP < BaseRequestTest request = stub_request 'HTTP_X_FORWARDED_FOR' => 'unknown,::1' assert_equal nil, request.remote_ip - request = stub_request 'HTTP_X_FORWARDED_FOR' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334, fe80:0000:0000:0000:0202:b3ff:fe1e:8329, ::1, fc00::' + request = stub_request 'HTTP_X_FORWARDED_FOR' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334, fe80:0000:0000:0000:0202:b3ff:fe1e:8329, ::1, fc00::, fc01::, fdff' assert_equal 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329', request.remote_ip + request = stub_request 'HTTP_X_FORWARDED_FOR' => 'FE00::, FDFF::' + assert_equal 'FE00::', request.remote_ip + request = stub_request 'HTTP_X_FORWARDED_FOR' => 'not_ip_address' assert_equal nil, request.remote_ip end @@ -525,6 +528,13 @@ class RequestCGI < BaseRequestTest end end +class LocalhostTest < BaseRequestTest + test "IPs that match localhost" do + request = stub_request("REMOTE_IP" => "127.1.1.1", "REMOTE_ADDR" => "127.1.1.1") + assert request.local? + end +end + class RequestCookie < BaseRequestTest test "cookie syntax resilience" do request = stub_request("HTTP_COOKIE" => "_session_id=c84ace84796670c052c6ceb2451fb0f2; is_admin=yes") @@ -629,12 +639,14 @@ class RequestProtocol < BaseRequestTest end class RequestMethod < BaseRequestTest - test "request methods" do - [:post, :get, :patch, :put, :delete].each do |method| - request = stub_request('REQUEST_METHOD' => method.to_s.upcase) + test "method returns environment's request method when it has not been + overriden by middleware".squish do + + ActionDispatch::Request::HTTP_METHODS.each do |method| + request = stub_request('REQUEST_METHOD' => method) - assert_equal method.to_s.upcase, request.method - assert_equal method, request.method_symbol + assert_equal method, request.method + assert_equal method.underscore.to_sym, request.method_symbol end end @@ -644,28 +656,18 @@ class RequestMethod < BaseRequestTest end end - test "allow method hacking on post" do - %w(GET OPTIONS PATCH PUT POST DELETE).each do |method| - request = stub_request 'REQUEST_METHOD' => method.to_s.upcase - - assert_equal(method == "HEAD" ? "GET" : method, request.method) - end + test "method returns original value of environment request method on POST" do + request = stub_request('rack.methodoverride.original_method' => 'POST') + assert_equal 'POST', request.method end - test "invalid method hacking on post raises exception" do + test "method raises exception on invalid HTTP method" do assert_raise(ActionController::UnknownHttpMethod) do - stub_request('REQUEST_METHOD' => '_RANDOM_METHOD').request_method + stub_request('rack.methodoverride.original_method' => '_RANDOM_METHOD').method end - end - test "restrict method hacking" do - [:get, :patch, :put, :delete].each do |method| - request = stub_request( - 'action_dispatch.request.request_parameters' => { :_method => 'put' }, - 'REQUEST_METHOD' => method.to_s.upcase - ) - - assert_equal method.to_s.upcase, request.method + assert_raise(ActionController::UnknownHttpMethod) do + stub_request('REQUEST_METHOD' => '_RANDOM_METHOD').method end end @@ -795,6 +797,12 @@ class RequestFormat < BaseRequestTest assert_not request.format.json? end + test "format does not throw exceptions when malformed parameters" do + request = stub_request("QUERY_STRING" => "x[y]=1&x[y][][w]=2") + assert request.formats + assert request.format.html? + end + test "formats with xhr request" do request = stub_request 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest" request.expects(:parameters).at_least_once.returns({}) @@ -802,6 +810,7 @@ class RequestFormat < BaseRequestTest end test "ignore_accept_header" do + old_ignore_accept_header = ActionDispatch::Request.ignore_accept_header ActionDispatch::Request.ignore_accept_header = true begin @@ -831,7 +840,7 @@ class RequestFormat < BaseRequestTest request.expects(:parameters).at_least_once.returns({:format => :json}) assert_equal [ Mime::JSON ], request.formats ensure - ActionDispatch::Request.ignore_accept_header = false + ActionDispatch::Request.ignore_accept_header = old_ignore_accept_header end end end @@ -889,15 +898,15 @@ class RequestParameters < BaseRequestTest assert_equal({"bar" => 2}, request.query_parameters) end - test "parameters still accessible after rack parse error" do + test "parameters not accessible after rack parse error" do request = stub_request("QUERY_STRING" => "x[y]=1&x[y][][w]=2") - assert_raises(ActionController::BadRequest) do - # rack will raise a TypeError when parsing this query string - request.parameters + 2.times do + assert_raises(ActionController::BadRequest) do + # rack will raise a TypeError when parsing this query string + request.parameters + end end - - assert_equal({}, request.parameters) end test "we have access to the original exception" do @@ -1062,7 +1071,7 @@ class RequestEtag < BaseRequestTest end end -class RequestVarient < BaseRequestTest +class RequestVariant < BaseRequestTest test "setting variant" do request = stub_request diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index 959a3bc5cd..187b9a2420 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -178,6 +178,7 @@ class ResponseTest < ActiveSupport::TestCase end test "read x_frame_options, x_content_type_options and x_xss_protection" do + original_default_headers = ActionDispatch::Response.default_headers begin ActionDispatch::Response.default_headers = { 'X-Frame-Options' => 'DENY', @@ -193,11 +194,12 @@ class ResponseTest < ActiveSupport::TestCase assert_equal('nosniff', resp.headers['X-Content-Type-Options']) assert_equal('1;', resp.headers['X-XSS-Protection']) ensure - ActionDispatch::Response.default_headers = nil + ActionDispatch::Response.default_headers = original_default_headers end end test "read custom default_header" do + original_default_headers = ActionDispatch::Response.default_headers begin ActionDispatch::Response.default_headers = { 'X-XX-XXXX' => 'Here is my phone number' @@ -209,7 +211,7 @@ class ResponseTest < ActiveSupport::TestCase assert_equal('Here is my phone number', resp.headers['X-XX-XXXX']) ensure - ActionDispatch::Response.default_headers = nil + ActionDispatch::Response.default_headers = original_default_headers end end diff --git a/actionpack/test/dispatch/routing/concerns_test.rb b/actionpack/test/dispatch/routing/concerns_test.rb index 9f37701656..7ef513b0c8 100644 --- a/actionpack/test/dispatch/routing/concerns_test.rb +++ b/actionpack/test/dispatch/routing/concerns_test.rb @@ -36,7 +36,8 @@ class RoutingConcernsTest < ActionDispatch::IntegrationTest end include Routes.url_helpers - def app; Routes end + APP = RoutedRackApp.new Routes + def app; APP end def test_accessing_concern_from_resources get "/posts/1/comments" diff --git a/actionpack/test/dispatch/routing/route_set_test.rb b/actionpack/test/dispatch/routing/route_set_test.rb index 0e488d2b88..c465d56bde 100644 --- a/actionpack/test/dispatch/routing/route_set_test.rb +++ b/actionpack/test/dispatch/routing/route_set_test.rb @@ -81,10 +81,6 @@ module ActionDispatch end private - def clear! - @set.clear! - end - def draw(&block) @set.draw(&block) end diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index b22a56bb27..269c7b4159 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -99,6 +99,16 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + def test_namespace_without_controller_segment + draw do + namespace :admin do + get 'hello/:controllers/:action' + end + end + get '/admin/hello/foo/new' + assert_equal 'foo', @request.params["controllers"] + end + def test_session_singleton_resource draw do resource :session do @@ -351,8 +361,8 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest draw do controller(:global) do get 'global/hide_notice' - get 'global/export', :to => :export, :as => :export_request - get '/export/:id/:file', :to => :export, :as => :export_download, :constraints => { :file => /.*/ } + get 'global/export', :action => :export, :as => :export_request + get '/export/:id/:file', :action => :export, :as => :export_download, :constraints => { :file => /.*/ } get 'global/:action' end end @@ -720,8 +730,8 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest draw do resources :replies do member do - put :answer, :to => :mark_as_answer - delete :answer, :to => :unmark_as_answer + put :answer, :action => :mark_as_answer + delete :answer, :action => :unmark_as_answer end end end @@ -1178,7 +1188,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest controller :articles do scope '/articles', :as => 'article' do scope :path => '/:title', :title => /[a-z]+/, :as => :with_title do - get '/:id', :to => :with_id, :as => "" + get '/:id', :action => :with_id, :as => "" end end end @@ -1425,7 +1435,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest def test_scoped_controller_with_namespace_and_action draw do namespace :account do - get ':action/callback', :action => /twitter|github/, :to => "callbacks", :as => :callback + get ':action/callback', :action => /twitter|github/, :controller => "callbacks", :as => :callback end end @@ -1482,7 +1492,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest def test_normalize_namespaced_matches draw do namespace :account do - get 'description', :to => :description, :as => "description" + get 'description', :action => :description, :as => "description" end end @@ -1723,7 +1733,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest get "whatever/:controller(/:action(/:id))" end - get 'whatever/foo/bar' + get '/whatever/foo/bar' assert_equal 'foo#bar', @response.body assert_equal 'http://www.example.com/whatever/foo/bar/1', @@ -1735,10 +1745,10 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest get "whatever/:controller(/:action(/:id))", :id => /\d+/ end - get 'whatever/foo/bar/show' + get '/whatever/foo/bar/show' assert_equal 'foo/bar#show', @response.body - get 'whatever/foo/bar/show/1' + get '/whatever/foo/bar/show/1' assert_equal 'foo/bar#show', @response.body assert_equal 'http://www.example.com/whatever/foo/bar/show', @@ -2012,6 +2022,28 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal '/blogs/1/posts/2/comments/new', new_blog_post_comment_path(:blog_id => 1, :post_id => 2) end + def test_direct_children_of_shallow_resources + draw do + resources :blogs do + resources :posts, shallow: true do + resources :comments + end + end + end + + post '/posts/1/comments' + assert_equal 'comments#create', @response.body + assert_equal '/posts/1/comments', post_comments_path('1') + + get '/posts/2/comments/new' + assert_equal 'comments#new', @response.body + assert_equal '/posts/2/comments/new', new_post_comment_path('2') + + get '/posts/1/comments' + assert_equal 'comments#index', @response.body + assert_equal '/posts/1/comments', post_comments_path('1') + end + def test_shallow_nested_resources_within_scope draw do scope '/hello' do @@ -2144,7 +2176,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end resources :invoices do get "outstanding" => "invoices#outstanding", :on => :collection - get "overdue", :to => :overdue, :on => :collection + get "overdue", :action => :overdue, :on => :collection get "print" => "invoices#print", :as => :print, :on => :member post "preview" => "invoices#preview", :as => :preview, :on => :new end @@ -2232,6 +2264,22 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal '/api/1.0/users/first.last.xml', api_user_path(:version => '1.0', :id => 'first.last', :format => :xml) end + def test_match_without_via + assert_raises(ArgumentError) do + draw do + match '/foo/bar', :to => 'files#show' + end + end + end + + def test_match_with_empty_via + assert_raises(ArgumentError) do + draw do + match '/foo/bar', :to => 'files#show', :via => [] + end + end + end + def test_glob_parameter_accepts_regexp draw do get '/:locale/*file.:format', :to => 'files#show', :file => /path\/to\/existing\/file/ @@ -2287,12 +2335,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest get "(/user/:username)/photos" => "photos#index" end - get 'user/bob/photos' + get '/user/bob/photos' assert_equal 'photos#index', @response.body assert_equal 'http://www.example.com/user/bob/photos', url_for(:controller => "photos", :action => "index", :username => "bob") - get 'photos' + get '/photos' assert_equal 'photos#index', @response.body assert_equal 'http://www.example.com/photos', url_for(:controller => "photos", :action => "index", :username => nil) @@ -2805,7 +2853,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end - def test_symbolized_path_parameters_is_not_stale + def test_path_parameters_is_not_stale draw do scope '/countries/:country', :constraints => lambda { |params, req| %w(all France).include?(params[:country]) } do get '/', :to => 'countries#index' @@ -2970,7 +3018,9 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end assert_raise(ArgumentError) do - draw { controller("/feeds") { get '/feeds/:service', :to => :show } } + assert_deprecated do + draw { controller("/feeds") { get '/feeds/:service', :to => :show } } + end end assert_raise(ArgumentError) do @@ -3120,7 +3170,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest get '/downloads/1/1.tar' assert_equal 'downloads#show', @response.body - assert_equal expected_params, @request.symbolized_path_parameters + assert_equal expected_params, @request.path_parameters assert_equal '/downloads/1/1.tar', download_path('1') assert_equal '/downloads/1/1.tar', download_path('1', '1') end @@ -3137,6 +3187,18 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal '/foo', foo_root_path end + def test_namespace_as_controller + draw do + namespace :foo do + get '/', to: '/bar#index', as: 'root' + end + end + + get '/foo' + assert_equal 'bar#index', @response.body + assert_equal '/foo', foo_root_path + end + def test_trailing_slash draw do resources :streams @@ -3217,6 +3279,58 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal '/admin/posts/1/comments', admin_post_comments_path('1') end + def test_mix_string_to_controller_action + draw do + get '/projects', controller: 'project_files', + action: 'index', + to: 'comments#index' + end + get '/projects' + assert_equal 'comments#index', @response.body + end + + def test_mix_string_to_controller + draw do + get '/projects', controller: 'project_files', + to: 'comments#index' + end + get '/projects' + assert_equal 'comments#index', @response.body + end + + def test_mix_string_to_action + draw do + get '/projects', action: 'index', + to: 'comments#index' + end + get '/projects' + assert_equal 'comments#index', @response.body + end + + def test_mix_symbol_to_controller_action + assert_deprecated do + draw do + get '/projects', controller: 'project_files', + action: 'index', + to: :show + end + end + get '/projects' + assert_equal 'project_files#show', @response.body + end + + def test_mix_string_to_controller_action_no_hash + assert_deprecated do + draw do + get '/projects', controller: 'project_files', + action: 'index', + to: 'show' + end + end + get '/projects' + assert_equal 'show#index', @response.body + end + def test_shallow_path_and_prefix_are_not_added_to_non_shallow_routes draw do scope shallow_path: 'projects', shallow_prefix: 'project' do @@ -3329,19 +3443,19 @@ private def draw(&block) self.class.stub_controllers do |routes| - @app = routes - @app.default_url_options = { host: 'www.example.com' } - @app.draw(&block) + routes.default_url_options = { host: 'www.example.com' } + routes.draw(&block) + @app = RoutedRackApp.new routes end end def url_for(options = {}) - @app.url_helpers.url_for(options) + @app.routes.url_helpers.url_for(options) end def method_missing(method, *args, &block) if method.to_s =~ /_(path|url)$/ - @app.url_helpers.send(method, *args, &block) + @app.routes.url_helpers.send(method, *args, &block) else super end @@ -3368,12 +3482,14 @@ end class TestAltApp < ActionDispatch::IntegrationTest class AltRequest + attr_accessor :path_parameters, :path_info, :script_name + attr_reader :env + def initialize(env) + @path_parameters = {} @env = env - end - - def path_info - "/" + @path_info = "/" + @script_name = "" end def request_method @@ -3407,8 +3523,10 @@ class TestAltApp < ActionDispatch::IntegrationTest get "/" => TestAltApp::AltApp.new end + APP = build_app AltRoutes + def app - AltRoutes + APP end def test_alt_request_without_header @@ -3435,15 +3553,16 @@ class TestAppendingRoutes < ActionDispatch::IntegrationTest def setup super s = self - @app = ActionDispatch::Routing::RouteSet.new - @app.append do + routes = ActionDispatch::Routing::RouteSet.new + routes.append do get '/hello' => s.simple_app('fail') get '/goodbye' => s.simple_app('goodbye') end - @app.draw do + routes.draw do get '/hello' => s.simple_app('hello') end + @app = self.class.build_app routes end def test_goodbye_should_be_available @@ -3472,8 +3591,38 @@ class TestNamespaceWithControllerOption < ActionDispatch::IntegrationTest end def draw(&block) - @app = ActionDispatch::Routing::RouteSet.new - @app.draw(&block) + routes = ActionDispatch::Routing::RouteSet.new + routes.draw(&block) + @app = self.class.build_app routes + end + + def test_missing_controller + ex = assert_raises(ArgumentError) { + draw do + get '/foo/bar', :action => :index + end + } + assert_match(/Missing :controller/, ex.message) + end + + def test_missing_action + ex = assert_raises(ArgumentError) { + assert_deprecated do + draw do + get '/foo/bar', :to => 'foo' + end + end + } + assert_match(/Missing :action/, ex.message) + end + + def test_missing_action_on_hash + ex = assert_raises(ArgumentError) { + draw do + get '/foo/bar', :to => 'foo#' + end + } + assert_match(/Missing :action/, ex.message) end def test_valid_controller_options_inside_namespace @@ -3492,7 +3641,7 @@ class TestNamespaceWithControllerOption < ActionDispatch::IntegrationTest resources :storage_files, :controller => 'admin/storage_files' end - get 'storage_files' + get '/storage_files' assert_equal "admin/storage_files#index", @response.body end @@ -3517,6 +3666,16 @@ class TestNamespaceWithControllerOption < ActionDispatch::IntegrationTest assert_match "'Admin::StorageFiles' is not a supported controller name", e.message end + + def test_warn_with_ruby_constant_syntax_no_colons + e = assert_raise(ArgumentError) do + draw do + resources :storage_files, :controller => 'Admin' + end + end + + assert_match "'Admin' is not a supported controller name", e.message + end end class TestDefaultScope < ActionDispatch::IntegrationTest @@ -3534,8 +3693,10 @@ class TestDefaultScope < ActionDispatch::IntegrationTest resources :posts end + APP = build_app DefaultScopeRoutes + def app - DefaultScopeRoutes + APP end include DefaultScopeRoutes.url_helpers @@ -3553,24 +3714,28 @@ class TestHttpMethods < ActionDispatch::IntegrationTest RFC3648 = %w(ORDERPATCH) RFC3744 = %w(ACL) RFC5323 = %w(SEARCH) + RFC4791 = %w(MKCALENDAR) RFC5789 = %w(PATCH) def simple_app(response) lambda { |env| [ 200, { 'Content-Type' => 'text/plain' }, [response] ] } end - setup do + attr_reader :app + + def setup s = self - @app = ActionDispatch::Routing::RouteSet.new + routes = ActionDispatch::Routing::RouteSet.new + @app = RoutedRackApp.new routes - @app.draw do - (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC5789).each do |method| + routes.draw do + (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789).each do |method| match '/' => s.simple_app(method), :via => method.underscore.to_sym end end end - (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC5789).each do |method| + (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789).each do |method| test "request method #{method.underscore} can be matched" do get '/', nil, 'REQUEST_METHOD' => method assert_equal method, @response.body @@ -3594,7 +3759,8 @@ class TestUriPathEscaping < ActionDispatch::IntegrationTest end include Routes.url_helpers - def app; Routes end + APP = build_app Routes + def app; APP end test 'escapes slash in generated path segment' do assert_equal '/a%20b%2Fc+d', segment_path(:segment => 'a b/c+d') @@ -3625,7 +3791,8 @@ class TestUnicodePaths < ActionDispatch::IntegrationTest end include Routes.url_helpers - def app; Routes end + APP = build_app Routes + def app; APP end test 'recognizes unicode path' do get "/#{Rack::Utils.escape("ほげ")}" @@ -3656,7 +3823,8 @@ class TestMultipleNestedController < ActionDispatch::IntegrationTest end include Routes.url_helpers - def app; Routes end + APP = build_app Routes + def app; APP end test "controller option which starts with '/' from multiple nested controller" do get "/foo/bar/baz" @@ -3675,7 +3843,8 @@ class TestTildeAndMinusPaths < ActionDispatch::IntegrationTest end include Routes.url_helpers - def app; Routes end + APP = build_app Routes + def app; APP end test 'recognizes tilde path' do get "/~user" @@ -3702,7 +3871,8 @@ class TestRedirectInterpolation < ActionDispatch::IntegrationTest end end - def app; Routes end + APP = build_app Routes + def app; APP end test "redirect escapes interpolated parameters with redirect proc" do get "/foo/1%3E" @@ -3744,7 +3914,8 @@ class TestConstraintsAccessingParameters < ActionDispatch::IntegrationTest end end - def app; Routes end + APP = build_app Routes + def app; APP end test "parameters are reset between constraint checks" do get "/bar" @@ -3764,7 +3935,8 @@ class TestGlobRoutingMapper < ActionDispatch::IntegrationTest end #include Routes.url_helpers - def app; Routes end + APP = build_app Routes + def app; APP end def test_glob_constraint get "/dummy" @@ -3796,7 +3968,8 @@ class TestOptimizedNamedRoutes < ActionDispatch::IntegrationTest end include Routes.url_helpers - def app; Routes end + APP = build_app Routes + def app; APP end test 'enabled when not mounted and default_url_options is empty' do assert Routes.url_helpers.optimize_routes_generation? @@ -3868,7 +4041,8 @@ class TestNamedRouteUrlHelpers < ActionDispatch::IntegrationTest end end - def app; Routes end + APP = build_app Routes + def app; APP end include Routes.url_helpers @@ -3903,7 +4077,8 @@ class TestUrlConstraints < ActionDispatch::IntegrationTest end include Routes.url_helpers - def app; Routes end + APP = build_app Routes + def app; APP end test "constraints are copied to defaults when using constraints method" do assert_equal 'http://admin.example.com/', admin_root_url @@ -3957,7 +4132,7 @@ class TestInvalidUrls < ActionDispatch::IntegrationTest set.draw do get "/bar/:id", :to => redirect("/foo/show/%{id}") get "/foo/show(/:id)", :to => "test_invalid_urls/foo#show" - get "/foo(/:action(/:id))", :to => "test_invalid_urls/foo" + get "/foo(/:action(/:id))", :controller => "test_invalid_urls/foo" get "/:controller(/:action(/:id))" end @@ -3984,8 +4159,9 @@ class TestOptionalRootSegments < ActionDispatch::IntegrationTest end end + APP = build_app Routes def app - Routes + APP end include Routes.url_helpers @@ -4016,7 +4192,8 @@ class TestPortConstraints < ActionDispatch::IntegrationTest end include Routes.url_helpers - def app; Routes end + APP = build_app Routes + def app; APP end def test_integer_port_constraints get 'http://www.example.com/integer' @@ -4064,7 +4241,8 @@ class TestFormatConstraints < ActionDispatch::IntegrationTest end include Routes.url_helpers - def app; Routes end + APP = build_app Routes + def app; APP end def test_string_format_constraints get 'http://www.example.com/string' @@ -4111,6 +4289,19 @@ class TestFormatConstraints < ActionDispatch::IntegrationTest end end +class TestCallableConstraintValidation < ActionDispatch::IntegrationTest + def test_constraint_with_object_not_callable + assert_raises(ArgumentError) do + ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } + get '/test', to: ok, constraints: Object.new + end + end + end + end +end + class TestRouteDefaults < ActionDispatch::IntegrationTest stub_controllers do |routes| Routes = routes @@ -4120,8 +4311,9 @@ class TestRouteDefaults < ActionDispatch::IntegrationTest end end + APP = build_app Routes def app - Routes + APP end include Routes.url_helpers @@ -4149,8 +4341,9 @@ class TestRackAppRouteGeneration < ActionDispatch::IntegrationTest end end + APP = build_app Routes def app - Routes + APP end include Routes.url_helpers @@ -4175,8 +4368,9 @@ class TestRedirectRouteGeneration < ActionDispatch::IntegrationTest end end + APP = build_app Routes def app - Routes + APP end include Routes.url_helpers @@ -4199,7 +4393,8 @@ class TestUrlGenerationErrors < ActionDispatch::IntegrationTest end end - def app; Routes end + APP = build_app Routes + def app; APP end include Routes.url_helpers diff --git a/actionpack/test/dispatch/session/mem_cache_store_test.rb b/actionpack/test/dispatch/session/mem_cache_store_test.rb index 92544230b2..f7a06cfed4 100644 --- a/actionpack/test/dispatch/session/mem_cache_store_test.rb +++ b/actionpack/test/dispatch/session/mem_cache_store_test.rb @@ -49,6 +49,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest assert_response :success assert_equal 'foo: "bar"', response.body end + rescue Dalli::RingError => ex + skip ex.message, ex.backtrace end def test_getting_nil_session_value @@ -57,6 +59,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest assert_response :success assert_equal 'foo: nil', response.body end + rescue Dalli::RingError => ex + skip ex.message, ex.backtrace end def test_getting_session_value_after_session_reset @@ -76,6 +80,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest assert_response :success assert_equal 'foo: nil', response.body, "data for this session should have been obliterated from memcached" end + rescue Dalli::RingError => ex + skip ex.message, ex.backtrace end def test_getting_from_nonexistent_session @@ -85,6 +91,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest assert_equal 'foo: nil', response.body assert_nil cookies['_session_id'], "should only create session on write, not read" end + rescue Dalli::RingError => ex + skip ex.message, ex.backtrace end def test_setting_session_value_after_session_reset @@ -106,6 +114,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest assert_response :success assert_not_equal session_id, response.body end + rescue Dalli::RingError => ex + skip ex.message, ex.backtrace end def test_getting_session_id @@ -119,6 +129,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest assert_response :success assert_equal session_id, response.body, "should be able to read session id without accessing the session hash" end + rescue Dalli::RingError => ex + skip ex.message, ex.backtrace end def test_deserializes_unloaded_class @@ -133,6 +145,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest assert_response :success end end + rescue Dalli::RingError => ex + skip ex.message, ex.backtrace end def test_doesnt_write_session_cookie_if_session_id_is_already_exists @@ -145,6 +159,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest assert_response :success assert_equal nil, headers['Set-Cookie'], "should not resend the cookie again if session_id cookie is already exists" end + rescue Dalli::RingError => ex + skip ex.message, ex.backtrace end def test_prevents_session_fixation @@ -160,6 +176,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest assert_response :success assert_not_equal session_id, cookies['_session_id'] end + rescue Dalli::RingError => ex + skip ex.message, ex.backtrace end rescue LoadError, RuntimeError, Dalli::DalliError $stderr.puts "Skipping MemCacheStoreTest tests. Start memcached and try again." diff --git a/actionpack/test/dispatch/show_exceptions_test.rb b/actionpack/test/dispatch/show_exceptions_test.rb index 38bd234f37..323fbc285e 100644 --- a/actionpack/test/dispatch/show_exceptions_test.rb +++ b/actionpack/test/dispatch/show_exceptions_test.rb @@ -37,7 +37,7 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest get "/", {}, {'action_dispatch.show_exceptions' => true} assert_response 500 assert_equal "500 error fixture\n", body - + get "/bad_params", {}, {'action_dispatch.show_exceptions' => true} assert_response 400 assert_equal "400 error fixture\n", body @@ -92,6 +92,7 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest exceptions_app = lambda do |env| assert_kind_of AbstractController::ActionNotFound, env["action_dispatch.exception"] assert_equal "/404", env["PATH_INFO"] + assert_equal "/not_found_original_exception", env["action_dispatch.original_path"] [404, { "Content-Type" => "text/plain" }, ["YOU FAILED BRO"]] end diff --git a/actionpack/test/dispatch/template_assertions_test.rb b/actionpack/test/dispatch/template_assertions_test.rb new file mode 100644 index 0000000000..3c393f937b --- /dev/null +++ b/actionpack/test/dispatch/template_assertions_test.rb @@ -0,0 +1,98 @@ +require 'abstract_unit' + +class AssertTemplateController < ActionController::Base + def render_with_partial + render partial: 'test/partial' + end + + def render_with_template + render 'test/hello_world' + end + + def render_with_layout + @variable_for_layout = nil + render 'test/hello_world', layout: "layouts/standard" + end + + def render_with_file + render file: 'README.rdoc' + end + + def render_nothing + head :ok + end +end + +class AssertTemplateControllerTest < ActionDispatch::IntegrationTest + def test_template_reset_between_requests + get '/assert_template/render_with_template' + assert_template 'test/hello_world' + + get '/assert_template/render_nothing' + assert_template nil + end + + def test_partial_reset_between_requests + get '/assert_template/render_with_partial' + assert_template partial: 'test/_partial' + + get '/assert_template/render_nothing' + assert_template partial: nil + end + + def test_layout_reset_between_requests + get '/assert_template/render_with_layout' + assert_template layout: 'layouts/standard' + + get '/assert_template/render_nothing' + assert_template layout: nil + end + + def test_file_reset_between_requests + get '/assert_template/render_with_file' + assert_template file: 'README.rdoc' + + get '/assert_template/render_nothing' + assert_template file: nil + end + + def test_template_reset_between_requests_when_opening_a_session + open_session do |session| + session.get '/assert_template/render_with_template' + session.assert_template 'test/hello_world' + + session.get '/assert_template/render_nothing' + session.assert_template nil + end + end + + def test_partial_reset_between_requests_when_opening_a_session + open_session do |session| + session.get '/assert_template/render_with_partial' + session.assert_template partial: 'test/_partial' + + session.get '/assert_template/render_nothing' + session.assert_template partial: nil + end + end + + def test_layout_reset_between_requests_when_opening_a_session + open_session do |session| + session.get '/assert_template/render_with_layout' + session.assert_template layout: 'layouts/standard' + + session.get '/assert_template/render_nothing' + session.assert_template layout: nil + end + end + + def test_file_reset_between_requests_when_opening_a_session + open_session do |session| + session.get '/assert_template/render_with_file' + session.assert_template file: 'README.rdoc' + + session.get '/assert_template/render_nothing' + session.assert_template file: nil + end + end +end diff --git a/actionpack/test/dispatch/uploaded_file_test.rb b/actionpack/test/dispatch/uploaded_file_test.rb index 9f6381f118..55ebbd5143 100644 --- a/actionpack/test/dispatch/uploaded_file_test.rb +++ b/actionpack/test/dispatch/uploaded_file_test.rb @@ -18,6 +18,12 @@ module ActionDispatch assert_equal "UTF-8", uf.original_filename.encoding.to_s end + def test_filename_should_always_be_in_utf_8 + uf = Http::UploadedFile.new(:filename => 'foo'.encode(Encoding::SHIFT_JIS), + :tempfile => Object.new) + assert_equal "UTF-8", uf.original_filename.encoding.to_s + end + def test_content_type uf = Http::UploadedFile.new(:type => 'foo', :tempfile => Object.new) assert_equal 'foo', uf.content_type diff --git a/actionpack/test/dispatch/url_generation_test.rb b/actionpack/test/dispatch/url_generation_test.rb index fdea27e2d2..8f79e7bf9a 100644 --- a/actionpack/test/dispatch/url_generation_test.rb +++ b/actionpack/test/dispatch/url_generation_test.rb @@ -15,15 +15,19 @@ module TestUrlGeneration Routes.draw do get "/foo", :to => "my_route_generating#index", :as => :foo + resources :bars + mount MyRouteGeneratingController.action(:index), at: '/bar' end + APP = build_app Routes + def _routes Routes end def app - Routes + APP end test "generating URLS normally" do @@ -64,18 +68,30 @@ module TestUrlGeneration test "port is extracted from the host" do assert_equal "http://www.example.com:8080/foo", foo_url(host: "www.example.com:8080", protocol: "http://") + assert_equal "//www.example.com:8080/foo", foo_url(host: "www.example.com:8080", protocol: "//") + assert_equal "//www.example.com:80/foo", foo_url(host: "www.example.com:80", protocol: "//") + end + + test "port option is used" do + assert_equal "http://www.example.com:8080/foo", foo_url(host: "www.example.com", protocol: "http://", port: 8080) + assert_equal "//www.example.com:8080/foo", foo_url(host: "www.example.com", protocol: "//", port: 8080) + assert_equal "//www.example.com:80/foo", foo_url(host: "www.example.com", protocol: "//", port: 80) end test "port option overrides the host" do assert_equal "http://www.example.com:8080/foo", foo_url(host: "www.example.com:8443", protocol: "http://", port: 8080) + assert_equal "//www.example.com:8080/foo", foo_url(host: "www.example.com:8443", protocol: "//", port: 8080) + assert_equal "//www.example.com:80/foo", foo_url(host: "www.example.com:443", protocol: "//", port: 80) end test "port option disables the host when set to nil" do assert_equal "http://www.example.com/foo", foo_url(host: "www.example.com:8443", protocol: "http://", port: nil) + assert_equal "//www.example.com/foo", foo_url(host: "www.example.com:8443", protocol: "//", port: nil) end test "port option disables the host when set to false" do assert_equal "http://www.example.com/foo", foo_url(host: "www.example.com:8443", protocol: "http://", port: false) + assert_equal "//www.example.com/foo", foo_url(host: "www.example.com:8443", protocol: "//", port: false) end test "keep subdomain when key is true" do @@ -97,6 +113,22 @@ module TestUrlGeneration test "omit subdomain when key is blank" do assert_equal "http://example.com/foo", foo_url(subdomain: "") end + + test "generating URLs with trailing slashes" do + assert_equal "/bars.json", bars_path( + trailing_slash: true, + format: 'json' + ) + end + + test "generating URLS with querystring and trailing slashes" do + assert_equal "/bars.json?a=b", bars_path( + trailing_slash: true, + a: 'b', + format: 'json' + ) + end + end end diff --git a/actionpack/test/fixtures/test/_changing_priority.html.erb b/actionpack/test/fixtures/test/_changing_priority.html.erb deleted file mode 100644 index 3225efc49a..0000000000 --- a/actionpack/test/fixtures/test/_changing_priority.html.erb +++ /dev/null @@ -1 +0,0 @@ -HTML
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_changing_priority.json.erb b/actionpack/test/fixtures/test/_changing_priority.json.erb deleted file mode 100644 index 7fa41dce66..0000000000 --- a/actionpack/test/fixtures/test/_changing_priority.json.erb +++ /dev/null @@ -1 +0,0 @@ -JSON
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_counter.html.erb b/actionpack/test/fixtures/test/_counter.html.erb deleted file mode 100644 index fd245bfc70..0000000000 --- a/actionpack/test/fixtures/test/_counter.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= counter_counter %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_customer.erb b/actionpack/test/fixtures/test/_customer.erb deleted file mode 100644 index d8220afeda..0000000000 --- a/actionpack/test/fixtures/test/_customer.erb +++ /dev/null @@ -1 +0,0 @@ -Hello: <%= customer.name rescue "Anonymous" %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_customer_counter.erb b/actionpack/test/fixtures/test/_customer_counter.erb deleted file mode 100644 index 3435979dba..0000000000 --- a/actionpack/test/fixtures/test/_customer_counter.erb +++ /dev/null @@ -1 +0,0 @@ -<%= customer_counter.name %><%= customer_counter_counter %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_customer_counter_with_as.erb b/actionpack/test/fixtures/test/_customer_counter_with_as.erb deleted file mode 100644 index 1241eb604d..0000000000 --- a/actionpack/test/fixtures/test/_customer_counter_with_as.erb +++ /dev/null @@ -1 +0,0 @@ -<%= client.name %><%= client_counter %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_customer_greeting.erb b/actionpack/test/fixtures/test/_customer_greeting.erb deleted file mode 100644 index 6acbcb20c4..0000000000 --- a/actionpack/test/fixtures/test/_customer_greeting.erb +++ /dev/null @@ -1 +0,0 @@ -<%= greeting %>: <%= customer_greeting.name %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_customer_with_var.erb b/actionpack/test/fixtures/test/_customer_with_var.erb deleted file mode 100644 index 00047dd20e..0000000000 --- a/actionpack/test/fixtures/test/_customer_with_var.erb +++ /dev/null @@ -1 +0,0 @@ -<%= customer.name %> <%= customer.name %> <%= customer.name %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_directory/_partial_with_locales.html.erb b/actionpack/test/fixtures/test/_directory/_partial_with_locales.html.erb deleted file mode 100644 index 1cc8d41475..0000000000 --- a/actionpack/test/fixtures/test/_directory/_partial_with_locales.html.erb +++ /dev/null @@ -1 +0,0 @@ -Hello <%= name %> diff --git a/actionpack/test/fixtures/test/_first_json_partial.json.erb b/actionpack/test/fixtures/test/_first_json_partial.json.erb deleted file mode 100644 index 790ee896db..0000000000 --- a/actionpack/test/fixtures/test/_first_json_partial.json.erb +++ /dev/null @@ -1 +0,0 @@ -<%= render :partial => "test/second_json_partial" %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_form.erb b/actionpack/test/fixtures/test/_form.erb deleted file mode 100644 index 01107f1cb2..0000000000 --- a/actionpack/test/fixtures/test/_form.erb +++ /dev/null @@ -1 +0,0 @@ -<%= form.label :title %> diff --git a/actionpack/test/fixtures/test/_hash_greeting.erb b/actionpack/test/fixtures/test/_hash_greeting.erb deleted file mode 100644 index fc54a36f2a..0000000000 --- a/actionpack/test/fixtures/test/_hash_greeting.erb +++ /dev/null @@ -1 +0,0 @@ -<%= greeting %>: <%= hash_greeting[:first_name] %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_hash_object.erb b/actionpack/test/fixtures/test/_hash_object.erb deleted file mode 100644 index 34a92c6a56..0000000000 --- a/actionpack/test/fixtures/test/_hash_object.erb +++ /dev/null @@ -1,2 +0,0 @@ -<%= hash_object[:first_name] %> -<%= hash_object[:first_name].reverse %> diff --git a/actionpack/test/fixtures/test/_hello.builder b/actionpack/test/fixtures/test/_hello.builder deleted file mode 100644 index ef52f632d1..0000000000 --- a/actionpack/test/fixtures/test/_hello.builder +++ /dev/null @@ -1 +0,0 @@ -xm.hello
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_json_change_priority.json.erb b/actionpack/test/fixtures/test/_json_change_priority.json.erb deleted file mode 100644 index e69de29bb2..0000000000 --- a/actionpack/test/fixtures/test/_json_change_priority.json.erb +++ /dev/null diff --git a/actionpack/test/fixtures/test/_labelling_form.erb b/actionpack/test/fixtures/test/_labelling_form.erb deleted file mode 100644 index 1b95763165..0000000000 --- a/actionpack/test/fixtures/test/_labelling_form.erb +++ /dev/null @@ -1 +0,0 @@ -<%= labelling_form.label :title %> diff --git a/actionpack/test/fixtures/test/_layout_for_partial.html.erb b/actionpack/test/fixtures/test/_layout_for_partial.html.erb deleted file mode 100644 index 666efadbb6..0000000000 --- a/actionpack/test/fixtures/test/_layout_for_partial.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -Before (<%= name %>) -<%= yield %> -After
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_partial_for_use_in_layout.html.erb b/actionpack/test/fixtures/test/_partial_for_use_in_layout.html.erb deleted file mode 100644 index 3a03a64e31..0000000000 --- a/actionpack/test/fixtures/test/_partial_for_use_in_layout.html.erb +++ /dev/null @@ -1 +0,0 @@ -Inside from partial (<%= name %>)
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_partial_html_erb.html.erb b/actionpack/test/fixtures/test/_partial_html_erb.html.erb deleted file mode 100644 index 4b54875782..0000000000 --- a/actionpack/test/fixtures/test/_partial_html_erb.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= "partial.html.erb" %> diff --git a/actionpack/test/fixtures/test/_partial_name_local_variable.erb b/actionpack/test/fixtures/test/_partial_name_local_variable.erb deleted file mode 100644 index cc3a91c89f..0000000000 --- a/actionpack/test/fixtures/test/_partial_name_local_variable.erb +++ /dev/null @@ -1 +0,0 @@ -<%= partial_name_local_variable %> diff --git a/actionpack/test/fixtures/test/_partial_only.erb b/actionpack/test/fixtures/test/_partial_only.erb deleted file mode 100644 index a44b3eed40..0000000000 --- a/actionpack/test/fixtures/test/_partial_only.erb +++ /dev/null @@ -1 +0,0 @@ -only partial
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_partial_only_html.html b/actionpack/test/fixtures/test/_partial_only_html.html deleted file mode 100644 index d2d630bd40..0000000000 --- a/actionpack/test/fixtures/test/_partial_only_html.html +++ /dev/null @@ -1 +0,0 @@ -only html partial
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_partial_with_partial.erb b/actionpack/test/fixtures/test/_partial_with_partial.erb deleted file mode 100644 index ee0d5037b6..0000000000 --- a/actionpack/test/fixtures/test/_partial_with_partial.erb +++ /dev/null @@ -1,2 +0,0 @@ -<%= render 'test/partial' %> -partial with partial diff --git a/actionpack/test/fixtures/test/_person.erb b/actionpack/test/fixtures/test/_person.erb deleted file mode 100644 index b2e5688956..0000000000 --- a/actionpack/test/fixtures/test/_person.erb +++ /dev/null @@ -1,2 +0,0 @@ -Second: <%= name %> -Third: <%= @name %> diff --git a/actionpack/test/fixtures/test/_raise_indentation.html.erb b/actionpack/test/fixtures/test/_raise_indentation.html.erb deleted file mode 100644 index f9a93728fe..0000000000 --- a/actionpack/test/fixtures/test/_raise_indentation.html.erb +++ /dev/null @@ -1,13 +0,0 @@ -<p>First paragraph</p> -<p>Second paragraph</p> -<p>Third paragraph</p> -<p>Fourth paragraph</p> -<p>Fifth paragraph</p> -<p>Sixth paragraph</p> -<p>Seventh paragraph</p> -<p>Eight paragraph</p> -<p>Ninth paragraph</p> -<p>Tenth paragraph</p> -<%= raise "error here!" %> -<p>Eleventh paragraph</p> -<p>Twelfth paragraph</p>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_second_json_partial.json.erb b/actionpack/test/fixtures/test/_second_json_partial.json.erb deleted file mode 100644 index 5ebb7f1afd..0000000000 --- a/actionpack/test/fixtures/test/_second_json_partial.json.erb +++ /dev/null @@ -1 +0,0 @@ -Third level
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/action_talk_to_layout.erb b/actionpack/test/fixtures/test/action_talk_to_layout.erb deleted file mode 100644 index 36e896daa8..0000000000 --- a/actionpack/test/fixtures/test/action_talk_to_layout.erb +++ /dev/null @@ -1,2 +0,0 @@ -<% @title = "Talking to the layout" -%> -Action was here!
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/calling_partial_with_layout.html.erb b/actionpack/test/fixtures/test/calling_partial_with_layout.html.erb deleted file mode 100644 index ac44bc0d81..0000000000 --- a/actionpack/test/fixtures/test/calling_partial_with_layout.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= render(:layout => "layout_for_partial", :partial => "partial_for_use_in_layout", :locals => { :name => "David" }) %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/capturing.erb b/actionpack/test/fixtures/test/capturing.erb deleted file mode 100644 index 1addaa40d9..0000000000 --- a/actionpack/test/fixtures/test/capturing.erb +++ /dev/null @@ -1,4 +0,0 @@ -<% days = capture do %> - Dreamy days -<% end %> -<%= days %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/change_priority.html.erb b/actionpack/test/fixtures/test/change_priority.html.erb deleted file mode 100644 index 5618977d05..0000000000 --- a/actionpack/test/fixtures/test/change_priority.html.erb +++ /dev/null @@ -1,2 +0,0 @@ -<%= render :partial => "test/json_change_priority", formats: :json %> -HTML Template, but <%= render :partial => "test/changing_priority" %> partial
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/content_for.erb b/actionpack/test/fixtures/test/content_for.erb deleted file mode 100644 index 1fb829f54c..0000000000 --- a/actionpack/test/fixtures/test/content_for.erb +++ /dev/null @@ -1 +0,0 @@ -<% content_for :title do -%>Putting stuff in the title!<% end -%>Great stuff!
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/content_for_concatenated.erb b/actionpack/test/fixtures/test/content_for_concatenated.erb deleted file mode 100644 index e65f629574..0000000000 --- a/actionpack/test/fixtures/test/content_for_concatenated.erb +++ /dev/null @@ -1,3 +0,0 @@ -<% content_for :title, "Putting stuff " - content_for :title, "in the title!" -%> -Great stuff!
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/content_for_with_parameter.erb b/actionpack/test/fixtures/test/content_for_with_parameter.erb deleted file mode 100644 index aeb6f73ce0..0000000000 --- a/actionpack/test/fixtures/test/content_for_with_parameter.erb +++ /dev/null @@ -1,2 +0,0 @@ -<% content_for :title, "Putting stuff in the title!" -%> -Great stuff!
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/formatted_html_erb.html.erb b/actionpack/test/fixtures/test/formatted_html_erb.html.erb deleted file mode 100644 index 1c64efabd8..0000000000 --- a/actionpack/test/fixtures/test/formatted_html_erb.html.erb +++ /dev/null @@ -1 +0,0 @@ -formatted html erb
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/greeting.html.erb b/actionpack/test/fixtures/test/greeting.html.erb deleted file mode 100644 index 62fb0293f0..0000000000 --- a/actionpack/test/fixtures/test/greeting.html.erb +++ /dev/null @@ -1 +0,0 @@ -<p>This is grand!</p> diff --git a/actionpack/test/fixtures/test/greeting.xml.erb b/actionpack/test/fixtures/test/greeting.xml.erb deleted file mode 100644 index 62fb0293f0..0000000000 --- a/actionpack/test/fixtures/test/greeting.xml.erb +++ /dev/null @@ -1 +0,0 @@ -<p>This is grand!</p> diff --git a/actionpack/test/fixtures/test/hello,world.erb b/actionpack/test/fixtures/test/hello,world.erb deleted file mode 100644 index bc8fa5e0ca..0000000000 --- a/actionpack/test/fixtures/test/hello,world.erb +++ /dev/null @@ -1 +0,0 @@ -Hello w*rld!
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/hello.builder b/actionpack/test/fixtures/test/hello.builder deleted file mode 100644 index a471553941..0000000000 --- a/actionpack/test/fixtures/test/hello.builder +++ /dev/null @@ -1,4 +0,0 @@ -xml.html do - xml.p "Hello #{@name}" - xml << render(:file => "test/greeting") -end
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/hello_world_container.builder b/actionpack/test/fixtures/test/hello_world_container.builder deleted file mode 100644 index e48d75c405..0000000000 --- a/actionpack/test/fixtures/test/hello_world_container.builder +++ /dev/null @@ -1,3 +0,0 @@ -xml.test do - render :partial => 'hello', :locals => { :xm => xml } -end
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/hello_world_from_rxml.builder b/actionpack/test/fixtures/test/hello_world_from_rxml.builder deleted file mode 100644 index 619a97ba96..0000000000 --- a/actionpack/test/fixtures/test/hello_world_from_rxml.builder +++ /dev/null @@ -1,3 +0,0 @@ -xml.html do - xml.p "Hello" -end diff --git a/actionpack/test/fixtures/test/hello_world_with_layout_false.erb b/actionpack/test/fixtures/test/hello_world_with_layout_false.erb deleted file mode 100644 index 6769dd60bd..0000000000 --- a/actionpack/test/fixtures/test/hello_world_with_layout_false.erb +++ /dev/null @@ -1 +0,0 @@ -Hello world!
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/html_template.html.erb b/actionpack/test/fixtures/test/html_template.html.erb deleted file mode 100644 index 1bbc2b7f09..0000000000 --- a/actionpack/test/fixtures/test/html_template.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= render :partial => "test/first_json_partial", formats: :json %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/hyphen-ated.erb b/actionpack/test/fixtures/test/hyphen-ated.erb deleted file mode 100644 index cd0875583a..0000000000 --- a/actionpack/test/fixtures/test/hyphen-ated.erb +++ /dev/null @@ -1 +0,0 @@ -Hello world! diff --git a/actionpack/test/fixtures/test/list.erb b/actionpack/test/fixtures/test/list.erb deleted file mode 100644 index 0a4bda58ee..0000000000 --- a/actionpack/test/fixtures/test/list.erb +++ /dev/null @@ -1 +0,0 @@ -<%= @test_unchanged = 'goodbye' %><%= render :partial => 'customer', :collection => @customers %><%= @test_unchanged %> diff --git a/actionpack/test/fixtures/test/non_erb_block_content_for.builder b/actionpack/test/fixtures/test/non_erb_block_content_for.builder deleted file mode 100644 index d539a425a4..0000000000 --- a/actionpack/test/fixtures/test/non_erb_block_content_for.builder +++ /dev/null @@ -1,4 +0,0 @@ -content_for :title do - 'Putting stuff in the title!' -end -xml << "Great stuff!" diff --git a/actionpack/test/fixtures/test/potential_conflicts.erb b/actionpack/test/fixtures/test/potential_conflicts.erb deleted file mode 100644 index a5e964e359..0000000000 --- a/actionpack/test/fixtures/test/potential_conflicts.erb +++ /dev/null @@ -1,4 +0,0 @@ -First: <%= @name %> -<%= render :partial => "person", :locals => { :name => "Stephan" } -%> -Fourth: <%= @name %> -Fifth: <%= name %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/proper_block_detection.erb b/actionpack/test/fixtures/test/proper_block_detection.erb deleted file mode 100644 index b55efbb25d..0000000000 --- a/actionpack/test/fixtures/test/proper_block_detection.erb +++ /dev/null @@ -1 +0,0 @@ -<%= @todo %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/render_file_from_template.html.erb b/actionpack/test/fixtures/test/render_file_from_template.html.erb deleted file mode 100644 index fde9f4bb64..0000000000 --- a/actionpack/test/fixtures/test/render_file_from_template.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= render :file => @path %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/render_file_with_locals_and_default.erb b/actionpack/test/fixtures/test/render_file_with_locals_and_default.erb deleted file mode 100644 index 9b4900acc5..0000000000 --- a/actionpack/test/fixtures/test/render_file_with_locals_and_default.erb +++ /dev/null @@ -1 +0,0 @@ -<%= secret ||= 'one' %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/render_implicit_html_template_from_xhr_request.da.html.erb b/actionpack/test/fixtures/test/render_implicit_html_template_from_xhr_request.da.html.erb deleted file mode 100644 index 0740b2d07c..0000000000 --- a/actionpack/test/fixtures/test/render_implicit_html_template_from_xhr_request.da.html.erb +++ /dev/null @@ -1 +0,0 @@ -Hey HTML! diff --git a/actionpack/test/fixtures/test/render_implicit_html_template_from_xhr_request.html.erb b/actionpack/test/fixtures/test/render_implicit_html_template_from_xhr_request.html.erb deleted file mode 100644 index 4a11845cfe..0000000000 --- a/actionpack/test/fixtures/test/render_implicit_html_template_from_xhr_request.html.erb +++ /dev/null @@ -1 +0,0 @@ -Hello HTML!
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/render_implicit_js_template_without_layout.js.erb b/actionpack/test/fixtures/test/render_implicit_js_template_without_layout.js.erb deleted file mode 100644 index 892ae5eca2..0000000000 --- a/actionpack/test/fixtures/test/render_implicit_js_template_without_layout.js.erb +++ /dev/null @@ -1 +0,0 @@ -alert('hello');
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/render_partial_inside_directory.html.erb b/actionpack/test/fixtures/test/render_partial_inside_directory.html.erb deleted file mode 100644 index 1461b95186..0000000000 --- a/actionpack/test/fixtures/test/render_partial_inside_directory.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= render partial: 'test/_directory/partial_with_locales', locals: {'name' => 'Jane'} %> diff --git a/actionpack/test/fixtures/test/render_to_string_test.erb b/actionpack/test/fixtures/test/render_to_string_test.erb deleted file mode 100644 index 6e267e8634..0000000000 --- a/actionpack/test/fixtures/test/render_to_string_test.erb +++ /dev/null @@ -1 +0,0 @@ -The value of foo is: ::<%= @foo %>:: diff --git a/actionpack/test/fixtures/test/render_two_partials.html.erb b/actionpack/test/fixtures/test/render_two_partials.html.erb deleted file mode 100644 index 3db6025860..0000000000 --- a/actionpack/test/fixtures/test/render_two_partials.html.erb +++ /dev/null @@ -1,2 +0,0 @@ -<%= render :partial => 'partial', :locals => {'first' => '1'} %> -<%= render :partial => 'partial', :locals => {'second' => '2'} %> diff --git a/actionpack/test/fixtures/test/using_layout_around_block.html.erb b/actionpack/test/fixtures/test/using_layout_around_block.html.erb deleted file mode 100644 index 3d6661df9a..0000000000 --- a/actionpack/test/fixtures/test/using_layout_around_block.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= render(:layout => "layout_for_partial", :locals => { :name => "David" }) do %>Inside from block<% end %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/with_html_partial.html.erb b/actionpack/test/fixtures/test/with_html_partial.html.erb deleted file mode 100644 index d84d909d64..0000000000 --- a/actionpack/test/fixtures/test/with_html_partial.html.erb +++ /dev/null @@ -1 +0,0 @@ -<strong><%= render :partial => "partial_only_html" %></strong> diff --git a/actionpack/test/fixtures/test/with_partial.html.erb b/actionpack/test/fixtures/test/with_partial.html.erb deleted file mode 100644 index 7502364cf5..0000000000 --- a/actionpack/test/fixtures/test/with_partial.html.erb +++ /dev/null @@ -1 +0,0 @@ -<strong><%= render :partial => "partial_only" %></strong> diff --git a/actionpack/test/fixtures/test/with_partial.text.erb b/actionpack/test/fixtures/test/with_partial.text.erb deleted file mode 100644 index 5f068ebf27..0000000000 --- a/actionpack/test/fixtures/test/with_partial.text.erb +++ /dev/null @@ -1 +0,0 @@ -**<%= render :partial => "partial_only" %>** diff --git a/actionpack/test/fixtures/test/with_xml_template.html.erb b/actionpack/test/fixtures/test/with_xml_template.html.erb deleted file mode 100644 index e54a7cd001..0000000000 --- a/actionpack/test/fixtures/test/with_xml_template.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= render :template => "test/greeting", :formats => :xml %> diff --git a/actionpack/test/journey/path/pattern_test.rb b/actionpack/test/journey/path/pattern_test.rb index ce02104181..9dfdfc23ed 100644 --- a/actionpack/test/journey/path/pattern_test.rb +++ b/actionpack/test/journey/path/pattern_test.rb @@ -18,7 +18,7 @@ module ActionDispatch '/:controller/*foo/bar' => %r{\A/(#{x})/(.+)/bar\Z}, }.each do |path, expected| define_method(:"test_to_regexp_#{path}") do - strexp = Router::Strexp.new( + strexp = Router::Strexp.build( path, { :controller => /.+/ }, ["/", ".", "?"] @@ -41,7 +41,7 @@ module ActionDispatch '/:controller/*foo/bar' => %r{\A/(#{x})/(.+)/bar}, }.each do |path, expected| define_method(:"test_to_non_anchored_regexp_#{path}") do - strexp = Router::Strexp.new( + strexp = Router::Strexp.build( path, { :controller => /.+/ }, ["/", ".", "?"], @@ -65,7 +65,7 @@ module ActionDispatch '/:controller/*foo/bar' => %w{ controller foo }, }.each do |path, expected| define_method(:"test_names_#{path}") do - strexp = Router::Strexp.new( + strexp = Router::Strexp.build( path, { :controller => /.+/ }, ["/", ".", "?"] @@ -75,12 +75,8 @@ module ActionDispatch end end - def test_to_raise_exception_with_bad_expression - assert_raise(ArgumentError, "Bad expression: []") { Pattern.new [] } - end - def test_to_regexp_with_extended_group - strexp = Router::Strexp.new( + strexp = Router::Strexp.build( '/page/:name', { :name => / #ROFL @@ -101,13 +97,13 @@ module ActionDispatch ['/:foo(/:bar)', %w{ bar }], ['/:foo(/:bar)/:lol(/:baz)', %w{ bar baz }], ].each do |pattern, list| - path = Pattern.new pattern + path = Pattern.from_string pattern assert_equal list.sort, path.optional_names.sort end end def test_to_regexp_match_non_optional - strexp = Router::Strexp.new( + strexp = Router::Strexp.build( '/:name', { :name => /\d+/ }, ["/", ".", "?"] @@ -118,7 +114,7 @@ module ActionDispatch end def test_to_regexp_with_group - strexp = Router::Strexp.new( + strexp = Router::Strexp.build( '/page/:name', { :name => /(tender|love)/ }, ["/", ".", "?"] @@ -131,7 +127,7 @@ module ActionDispatch def test_ast_sets_regular_expressions requirements = { :name => /(tender|love)/, :value => /./ } - strexp = Router::Strexp.new( + strexp = Router::Strexp.build( '/page/:name/:value', requirements, ["/", ".", "?"] @@ -148,7 +144,7 @@ module ActionDispatch end def test_match_data_with_group - strexp = Router::Strexp.new( + strexp = Router::Strexp.build( '/page/:name', { :name => /(tender|love)/ }, ["/", ".", "?"] @@ -160,7 +156,7 @@ module ActionDispatch end def test_match_data_with_multi_group - strexp = Router::Strexp.new( + strexp = Router::Strexp.build( '/page/:name/:id', { :name => /t(((ender|love)))()/ }, ["/", ".", "?"] @@ -175,7 +171,7 @@ module ActionDispatch def test_star_with_custom_re z = /\d+/ - strexp = Router::Strexp.new( + strexp = Router::Strexp.build( '/page/*foo', { :foo => z }, ["/", ".", "?"] @@ -185,7 +181,7 @@ module ActionDispatch end def test_insensitive_regexp_with_group - strexp = Router::Strexp.new( + strexp = Router::Strexp.build( '/page/:name/aaron', { :name => /(tender|love)/i }, ["/", ".", "?"] @@ -197,7 +193,7 @@ module ActionDispatch end def test_to_regexp_with_strexp - strexp = Router::Strexp.new('/:controller', { }, ["/", ".", "?"]) + strexp = Router::Strexp.build('/:controller', { }, ["/", ".", "?"]) path = Pattern.new strexp x = %r{\A/([^/.?]+)\Z} @@ -205,20 +201,20 @@ module ActionDispatch end def test_to_regexp_defaults - path = Pattern.new '/:controller(/:action(/:id))' + path = Pattern.from_string '/:controller(/:action(/:id))' expected = %r{\A/([^/.?]+)(?:/([^/.?]+)(?:/([^/.?]+))?)?\Z} assert_equal expected, path.to_regexp end def test_failed_match - path = Pattern.new '/:controller(/:action(/:id(.:format)))' + path = Pattern.from_string '/:controller(/:action(/:id(.:format)))' uri = 'content' assert_not path =~ uri end def test_match_controller - path = Pattern.new '/:controller(/:action(/:id(.:format)))' + path = Pattern.from_string '/:controller(/:action(/:id(.:format)))' uri = '/content' match = path =~ uri @@ -230,7 +226,7 @@ module ActionDispatch end def test_match_controller_action - path = Pattern.new '/:controller(/:action(/:id(.:format)))' + path = Pattern.from_string '/:controller(/:action(/:id(.:format)))' uri = '/content/list' match = path =~ uri @@ -242,7 +238,7 @@ module ActionDispatch end def test_match_controller_action_id - path = Pattern.new '/:controller(/:action(/:id(.:format)))' + path = Pattern.from_string '/:controller(/:action(/:id(.:format)))' uri = '/content/list/10' match = path =~ uri @@ -254,7 +250,7 @@ module ActionDispatch end def test_match_literal - path = Path::Pattern.new "/books(/:action(.:format))" + path = Path::Pattern.from_string "/books(/:action(.:format))" uri = '/books' match = path =~ uri @@ -264,7 +260,7 @@ module ActionDispatch end def test_match_literal_with_action - path = Path::Pattern.new "/books(/:action(.:format))" + path = Path::Pattern.from_string "/books(/:action(.:format))" uri = '/books/list' match = path =~ uri @@ -274,7 +270,7 @@ module ActionDispatch end def test_match_literal_with_action_and_format - path = Path::Pattern.new "/books(/:action(.:format))" + path = Path::Pattern.from_string "/books(/:action(.:format))" uri = '/books/list.rss' match = path =~ uri diff --git a/actionpack/test/journey/route_test.rb b/actionpack/test/journey/route_test.rb index cbe6284714..21d867aca0 100644 --- a/actionpack/test/journey/route_test.rb +++ b/actionpack/test/journey/route_test.rb @@ -5,7 +5,7 @@ module ActionDispatch class TestRoute < ActiveSupport::TestCase def test_initialize app = Object.new - path = Path::Pattern.new '/:controller(/:action(/:id(.:format)))' + path = Path::Pattern.from_string '/:controller(/:action(/:id(.:format)))' defaults = {} route = Route.new("name", app, path, {}, defaults) @@ -16,7 +16,7 @@ module ActionDispatch def test_route_adds_itself_as_memo app = Object.new - path = Path::Pattern.new '/:controller(/:action(/:id(.:format)))' + path = Path::Pattern.from_string '/:controller(/:action(/:id(.:format)))' defaults = {} route = Route.new("name", app, path, {}, defaults) @@ -26,21 +26,21 @@ module ActionDispatch end def test_ip_address - path = Path::Pattern.new '/messages/:id(.:format)' + path = Path::Pattern.from_string '/messages/:id(.:format)' route = Route.new("name", nil, path, {:ip => '192.168.1.1'}, { :controller => 'foo', :action => 'bar' }) assert_equal '192.168.1.1', route.ip end def test_default_ip - path = Path::Pattern.new '/messages/:id(.:format)' + path = Path::Pattern.from_string '/messages/:id(.:format)' route = Route.new("name", nil, path, {}, { :controller => 'foo', :action => 'bar' }) assert_equal(//, route.ip) end def test_format_with_star - path = Path::Pattern.new '/:controller/*extra' + path = Path::Pattern.from_string '/:controller/*extra' route = Route.new("name", nil, path, {}, { :controller => 'foo', :action => 'bar' }) assert_equal '/foo/himom', route.format({ @@ -50,7 +50,7 @@ module ActionDispatch end def test_connects_all_match - path = Path::Pattern.new '/:controller(/:action(/:id(.:format)))' + path = Path::Pattern.from_string '/:controller(/:action(/:id(.:format)))' route = Route.new("name", nil, path, {:action => 'bar'}, { :controller => 'foo' }) assert_equal '/foo/bar/10', route.format({ @@ -61,21 +61,21 @@ module ActionDispatch end def test_extras_are_not_included_if_optional - path = Path::Pattern.new '/page/:id(/:action)' + path = Path::Pattern.from_string '/page/:id(/:action)' route = Route.new("name", nil, path, { }, { :action => 'show' }) assert_equal '/page/10', route.format({ :id => 10 }) end def test_extras_are_not_included_if_optional_with_parameter - path = Path::Pattern.new '(/sections/:section)/pages/:id' + path = Path::Pattern.from_string '(/sections/:section)/pages/:id' route = Route.new("name", nil, path, { }, { :action => 'show' }) assert_equal '/pages/10', route.format({:id => 10}) end def test_extras_are_not_included_if_optional_parameter_is_nil - path = Path::Pattern.new '(/sections/:section)/pages/:id' + path = Path::Pattern.from_string '(/sections/:section)/pages/:id' route = Route.new("name", nil, path, { }, { :action => 'show' }) assert_equal '/pages/10', route.format({:id => 10, :section => nil}) @@ -85,10 +85,10 @@ module ActionDispatch constraints = {:required_defaults => [:controller, :action]} defaults = {:controller=>"pages", :action=>"show"} - path = Path::Pattern.new "/page/:id(/:action)(.:format)" + path = Path::Pattern.from_string "/page/:id(/:action)(.:format)" specific = Route.new "name", nil, path, constraints, defaults - path = Path::Pattern.new "/:controller(/:action(/:id))(.:format)" + path = Path::Pattern.from_string "/:controller(/:action(/:id))(.:format)" generic = Route.new "name", nil, path, constraints knowledge = {:id=>20, :controller=>"pages", :action=>"show"} diff --git a/actionpack/test/journey/router/strexp_test.rb b/actionpack/test/journey/router/strexp_test.rb deleted file mode 100644 index 7ccdfb7b4d..0000000000 --- a/actionpack/test/journey/router/strexp_test.rb +++ /dev/null @@ -1,32 +0,0 @@ -require 'abstract_unit' - -module ActionDispatch - module Journey - class Router - class TestStrexp < ActiveSupport::TestCase - def test_many_names - exp = Strexp.new( - "/:controller(/:action(/:id(.:format)))", - {:controller=>/.+?/}, - ["/", ".", "?"], - true) - - assert_equal ["controller", "action", "id", "format"], exp.names - end - - def test_names - { - "/bar(.:format)" => %w{ format }, - ":format" => %w{ format }, - ":format-" => %w{ format }, - ":format0" => %w{ format0 }, - ":format1,:format2" => %w{ format1 format2 }, - }.each do |string, expected| - exp = Strexp.new(string, {}, ["/", ".", "?"]) - assert_equal expected, exp.names - end - end - end - end - end -end diff --git a/actionpack/test/journey/router/utils_test.rb b/actionpack/test/journey/router/utils_test.rb index 584fd56a5c..9b2b85ec73 100644 --- a/actionpack/test/journey/router/utils_test.rb +++ b/actionpack/test/journey/router/utils_test.rb @@ -1,3 +1,4 @@ +# coding: utf-8 require 'abstract_unit' module ActionDispatch @@ -20,6 +21,10 @@ module ActionDispatch assert_equal "a/b c+d", Utils.unescape_uri("a%2Fb%20c+d") end + def test_uri_unescape_with_utf8_string + assert_equal "Šašinková", Utils.unescape_uri("%C5%A0a%C5%A1inkov%C3%A1".force_encoding(Encoding::US_ASCII)) + end + def test_normalize_path_not_greedy assert_equal "/foo%20bar%20baz", Utils.normalize_path("/foo%20bar%20baz") end diff --git a/actionpack/test/journey/router_test.rb b/actionpack/test/journey/router_test.rb index e54b64e0f3..2e7e8e1bea 100644 --- a/actionpack/test/journey/router_test.rb +++ b/actionpack/test/journey/router_test.rb @@ -4,24 +4,15 @@ require 'abstract_unit' module ActionDispatch module Journey class TestRouter < ActiveSupport::TestCase - # TODO : clean up routing tests so we don't need this hack - class StubDispatcher < Routing::RouteSet::Dispatcher; end - attr_reader :routes def setup - @app = StubDispatcher.new + @app = Routing::RouteSet::Dispatcher.new({}) @routes = Routes.new - @router = Router.new(@routes, {}) + @router = Router.new(@routes) @formatter = Formatter.new(@routes) end - def test_request_class_reader - klass = Object.new - router = Router.new(routes, :request_class => klass) - assert_equal klass, router.request_class - end - class FakeRequestFeeler < Struct.new(:env, :called) def new env self.env = env @@ -39,33 +30,33 @@ module ActionDispatch end def test_dashes - router = Router.new(routes, {}) + router = Router.new(routes) - exp = Router::Strexp.new '/foo-bar-baz', {}, ['/.?'] + exp = Router::Strexp.build '/foo-bar-baz', {}, ['/.?'] path = Path::Pattern.new exp routes.add_route nil, path, {}, {:id => nil}, {} env = rails_env 'PATH_INFO' => '/foo-bar-baz' called = false - router.recognize(env) do |r, _, params| + router.recognize(env) do |r, params| called = true end assert called end def test_unicode - router = Router.new(routes, {}) + router = Router.new(routes) #match the escaped version of /ほげ - exp = Router::Strexp.new '/%E3%81%BB%E3%81%92', {}, ['/.?'] + exp = Router::Strexp.build '/%E3%81%BB%E3%81%92', {}, ['/.?'] path = Path::Pattern.new exp routes.add_route nil, path, {}, {:id => nil}, {} env = rails_env 'PATH_INFO' => '/%E3%81%BB%E3%81%92' called = false - router.recognize(env) do |r, _, params| + router.recognize(env) do |r, params| called = true end assert called @@ -73,17 +64,17 @@ module ActionDispatch def test_request_class_and_requirements_success klass = FakeRequestFeeler.new nil - router = Router.new(routes, {:request_class => klass }) + router = Router.new(routes) requirements = { :hello => /world/ } - exp = Router::Strexp.new '/foo(/:id)', {}, ['/.?'] + exp = Router::Strexp.build '/foo(/:id)', {}, ['/.?'] path = Path::Pattern.new exp routes.add_route nil, path, requirements, {:id => nil}, {} - env = rails_env 'PATH_INFO' => '/foo/10' - router.recognize(env) do |r, _, params| + env = rails_env({'PATH_INFO' => '/foo/10'}, klass) + router.recognize(env) do |r, params| assert_equal({:id => '10'}, params) end @@ -93,17 +84,17 @@ module ActionDispatch def test_request_class_and_requirements_fail klass = FakeRequestFeeler.new nil - router = Router.new(routes, {:request_class => klass }) + router = Router.new(routes) requirements = { :hello => /mom/ } - exp = Router::Strexp.new '/foo(/:id)', {}, ['/.?'] + exp = Router::Strexp.build '/foo(/:id)', {}, ['/.?'] path = Path::Pattern.new exp router.routes.add_route nil, path, requirements, {:id => nil}, {} - env = rails_env 'PATH_INFO' => '/foo/10' - router.recognize(env) do |r, _, params| + env = rails_env({'PATH_INFO' => '/foo/10'}, klass) + router.recognize(env) do |r, params| flunk 'route should not be found' end @@ -111,24 +102,29 @@ module ActionDispatch assert_equal env.env, klass.env end - class CustomPathRequest < Router::NullReq + class CustomPathRequest < ActionDispatch::Request def path_info env['custom.path_info'] end + + def path_info=(x) + env['custom.path_info'] = x + end end def test_request_class_overrides_path_info - router = Router.new(routes, {:request_class => CustomPathRequest }) + router = Router.new(routes) - exp = Router::Strexp.new '/bar', {}, ['/.?'] + exp = Router::Strexp.build '/bar', {}, ['/.?'] path = Path::Pattern.new exp routes.add_route nil, path, {}, {}, {} - env = rails_env 'PATH_INFO' => '/foo', 'custom.path_info' => '/bar' + env = rails_env({'PATH_INFO' => '/foo', + 'custom.path_info' => '/bar'}, CustomPathRequest) recognized = false - router.recognize(env) do |r, _, params| + router.recognize(env) do |r, params| recognized = true end @@ -137,14 +133,14 @@ module ActionDispatch def test_regexp_first_precedence add_routes @router, [ - Router::Strexp.new("/whois/:domain", {:domain => /\w+\.[\w\.]+/}, ['/', '.', '?']), - Router::Strexp.new("/whois/:id(.:format)", {}, ['/', '.', '?']) + Router::Strexp.build("/whois/:domain", {:domain => /\w+\.[\w\.]+/}, ['/', '.', '?']), + Router::Strexp.build("/whois/:id(.:format)", {}, ['/', '.', '?']) ] env = rails_env 'PATH_INFO' => '/whois/example.com' list = [] - @router.recognize(env) do |r, _, params| + @router.recognize(env) do |r, params| list << r end assert_equal 2, list.length @@ -156,50 +152,50 @@ module ActionDispatch def test_required_parts_verified_are_anchored add_routes @router, [ - Router::Strexp.new("/foo/:id", { :id => /\d/ }, ['/', '.', '?'], false) + Router::Strexp.build("/foo/:id", { :id => /\d/ }, ['/', '.', '?'], false) ] assert_raises(ActionController::UrlGenerationError) do - @formatter.generate(:path_info, nil, { :id => '10' }, { }) + @formatter.generate(nil, { :id => '10' }, { }) end end def test_required_parts_are_verified_when_building add_routes @router, [ - Router::Strexp.new("/foo/:id", { :id => /\d+/ }, ['/', '.', '?'], false) + Router::Strexp.build("/foo/:id", { :id => /\d+/ }, ['/', '.', '?'], false) ] - path, _ = @formatter.generate(:path_info, nil, { :id => '10' }, { }) + path, _ = @formatter.generate(nil, { :id => '10' }, { }) assert_equal '/foo/10', path assert_raises(ActionController::UrlGenerationError) do - @formatter.generate(:path_info, nil, { :id => 'aa' }, { }) + @formatter.generate(nil, { :id => 'aa' }, { }) end end def test_only_required_parts_are_verified add_routes @router, [ - Router::Strexp.new("/foo(/:id)", {:id => /\d/}, ['/', '.', '?'], false) + Router::Strexp.build("/foo(/:id)", {:id => /\d/}, ['/', '.', '?'], false) ] - path, _ = @formatter.generate(:path_info, nil, { :id => '10' }, { }) + path, _ = @formatter.generate(nil, { :id => '10' }, { }) assert_equal '/foo/10', path - path, _ = @formatter.generate(:path_info, nil, { }, { }) + path, _ = @formatter.generate(nil, { }, { }) assert_equal '/foo', path - path, _ = @formatter.generate(:path_info, nil, { :id => 'aa' }, { }) + path, _ = @formatter.generate(nil, { :id => 'aa' }, { }) assert_equal '/foo/aa', path end def test_knows_what_parts_are_missing_from_named_route route_name = "gorby_thunderhorse" - pattern = Router::Strexp.new("/foo/:id", { :id => /\d+/ }, ['/', '.', '?'], false) + pattern = Router::Strexp.build("/foo/:id", { :id => /\d+/ }, ['/', '.', '?'], false) path = Path::Pattern.new pattern @router.routes.add_route nil, path, {}, {}, route_name error = assert_raises(ActionController::UrlGenerationError) do - @formatter.generate(:path_info, route_name, { }, { }) + @formatter.generate(route_name, { }, { }) end assert_match(/missing required keys: \[:id\]/, error.message) @@ -207,42 +203,43 @@ module ActionDispatch def test_X_Cascade add_routes @router, [ "/messages(.:format)" ] - resp = @router.call({ 'REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/lol' }) + resp = @router.serve(rails_env({ 'REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/lol' })) assert_equal ['Not Found'], resp.last assert_equal 'pass', resp[1]['X-Cascade'] assert_equal 404, resp.first end def test_clear_trailing_slash_from_script_name_on_root_unanchored_routes - strexp = Router::Strexp.new("/", {}, ['/', '.', '?'], false) - path = Path::Pattern.new strexp + route_set = Routing::RouteSet.new + mapper = Routing::Mapper.new route_set + app = lambda { |env| [200, {}, ['success!']] } - @router.routes.add_route(app, path, {}, {}, {}) + mapper.get '/weblog', :to => app env = rack_env('SCRIPT_NAME' => '', 'PATH_INFO' => '/weblog') - resp = @router.call(env) + resp = route_set.call env assert_equal ['success!'], resp.last assert_equal '', env['SCRIPT_NAME'] end def test_defaults_merge_correctly - path = Path::Pattern.new '/foo(/:id)' + path = Path::Pattern.from_string '/foo(/:id)' @router.routes.add_route nil, path, {}, {:id => nil}, {} env = rails_env 'PATH_INFO' => '/foo/10' - @router.recognize(env) do |r, _, params| + @router.recognize(env) do |r, params| assert_equal({:id => '10'}, params) end env = rails_env 'PATH_INFO' => '/foo' - @router.recognize(env) do |r, _, params| + @router.recognize(env) do |r, params| assert_equal({:id => nil}, params) end end def test_recognize_with_unbound_regexp add_routes @router, [ - Router::Strexp.new("/foo", { }, ['/', '.', '?'], false) + Router::Strexp.build("/foo", { }, ['/', '.', '?'], false) ] env = rails_env 'PATH_INFO' => '/foo/bar' @@ -255,7 +252,7 @@ module ActionDispatch def test_bound_regexp_keeps_path_info add_routes @router, [ - Router::Strexp.new("/foo", { }, ['/', '.', '?'], true) + Router::Strexp.build("/foo", { }, ['/', '.', '?'], true) ] env = rails_env 'PATH_INFO' => '/foo' @@ -287,14 +284,14 @@ module ActionDispatch def test_required_part_in_recall add_routes @router, [ "/messages/:a/:b" ] - path, _ = @formatter.generate(:path_info, nil, { :a => 'a' }, { :b => 'b' }) + path, _ = @formatter.generate(nil, { :a => 'a' }, { :b => 'b' }) assert_equal "/messages/a/b", path end def test_splat_in_recall add_routes @router, [ "/*path" ] - path, _ = @formatter.generate(:path_info, nil, { }, { :path => 'b' }) + path, _ = @formatter.generate(nil, { }, { :path => 'b' }) assert_equal "/b", path end @@ -304,35 +301,35 @@ module ActionDispatch "/messages/:id(.:format)" ] - path, _ = @formatter.generate(:path_info, nil, { :id => 10 }, { :action => 'index' }) + path, _ = @formatter.generate(nil, { :id => 10 }, { :action => 'index' }) assert_equal "/messages/index/10", path end def test_nil_path_parts_are_ignored - path = Path::Pattern.new "/:controller(/:action(.:format))" + path = Path::Pattern.from_string "/:controller(/:action(.:format))" @router.routes.add_route @app, path, {}, {}, {} params = { :controller => "tasks", :format => nil } extras = { :action => 'lol' } - path, _ = @formatter.generate(:path_info, nil, params, extras) + path, _ = @formatter.generate(nil, params, extras) assert_equal '/tasks', path end def test_generate_slash params = [ [:controller, "tasks"], [:action, "show"] ] - str = Router::Strexp.new("/", Hash[params], ['/', '.', '?'], true) + str = Router::Strexp.build("/", Hash[params], ['/', '.', '?'], true) path = Path::Pattern.new str @router.routes.add_route @app, path, {}, {}, {} - path, _ = @formatter.generate(:path_info, nil, Hash[params], {}) + path, _ = @formatter.generate(nil, Hash[params], {}) assert_equal '/', path end def test_generate_calls_param_proc - path = Path::Pattern.new '/:controller(/:action)' + path = Path::Pattern.from_string '/:controller(/:action)' @router.routes.add_route @app, path, {}, {}, {} parameterized = [] @@ -340,7 +337,6 @@ module ActionDispatch [:action, "show"] ] @formatter.generate( - :path_info, nil, Hash[params], {}, @@ -350,31 +346,31 @@ module ActionDispatch end def test_generate_id - path = Path::Pattern.new '/:controller(/:action)' + path = Path::Pattern.from_string '/:controller(/:action)' @router.routes.add_route @app, path, {}, {}, {} path, params = @formatter.generate( - :path_info, nil, {:id=>1, :controller=>"tasks", :action=>"show"}, {}) + nil, {:id=>1, :controller=>"tasks", :action=>"show"}, {}) assert_equal '/tasks/show', path assert_equal({:id => 1}, params) end def test_generate_escapes - path = Path::Pattern.new '/:controller(/:action)' + path = Path::Pattern.from_string '/:controller(/:action)' @router.routes.add_route @app, path, {}, {}, {} - path, _ = @formatter.generate(:path_info, - nil, { :controller => "tasks", + path, _ = @formatter.generate(nil, + { :controller => "tasks", :action => "a/b c+d", }, {}) assert_equal '/tasks/a%2Fb%20c+d', path end def test_generate_escapes_with_namespaced_controller - path = Path::Pattern.new '/:controller(/:action)' + path = Path::Pattern.from_string '/:controller(/:action)' @router.routes.add_route @app, path, {}, {}, {} - path, _ = @formatter.generate(:path_info, + path, _ = @formatter.generate( nil, { :controller => "admin/tasks", :action => "a/b c+d", }, {}) @@ -382,10 +378,10 @@ module ActionDispatch end def test_generate_extra_params - path = Path::Pattern.new '/:controller(/:action)' + path = Path::Pattern.from_string '/:controller(/:action)' @router.routes.add_route @app, path, {}, {}, {} - path, params = @formatter.generate(:path_info, + path, params = @formatter.generate( nil, { :id => 1, :controller => "tasks", :action => "show", @@ -396,10 +392,10 @@ module ActionDispatch end def test_generate_uses_recall_if_needed - path = Path::Pattern.new '/:controller(/:action(/:id))' + path = Path::Pattern.from_string '/:controller(/:action(/:id))' @router.routes.add_route @app, path, {}, {}, {} - path, params = @formatter.generate(:path_info, + path, params = @formatter.generate( nil, {:controller =>"tasks", :id => 10}, {:action =>"index"}) @@ -408,10 +404,10 @@ module ActionDispatch end def test_generate_with_name - path = Path::Pattern.new '/:controller(/:action)' + path = Path::Pattern.from_string '/:controller(/:action)' @router.routes.add_route @app, path, {}, {}, {} - path, params = @formatter.generate(:path_info, + path, params = @formatter.generate( "tasks", {:controller=>"tasks"}, {:controller=>"tasks", :action=>"index"}) @@ -425,14 +421,14 @@ module ActionDispatch '/content/show/10' => { :controller => 'content', :action => 'show', :id => "10" }, }.each do |request_path, expected| define_method("test_recognize_#{expected.keys.map(&:to_s).join('_')}") do - path = Path::Pattern.new "/:controller(/:action(/:id))" + path = Path::Pattern.from_string "/:controller(/:action(/:id))" app = Object.new route = @router.routes.add_route(app, path, {}, {}, {}) env = rails_env 'PATH_INFO' => request_path called = false - @router.recognize(env) do |r, _, params| + @router.recognize(env) do |r, params| assert_equal route, r assert_equal(expected, params) called = true @@ -447,14 +443,14 @@ module ActionDispatch :splat => ['/segment/a/b%20c+d', { :segment => 'segment', :splat => 'a/b c+d' }] }.each do |name, (request_path, expected)| define_method("test_recognize_#{name}") do - path = Path::Pattern.new '/:segment/*splat' + path = Path::Pattern.from_string '/:segment/*splat' app = Object.new route = @router.routes.add_route(app, path, {}, {}, {}) env = rails_env 'PATH_INFO' => request_path called = false - @router.recognize(env) do |r, _, params| + @router.recognize(env) do |r, params| assert_equal route, r assert_equal(expected, params) called = true @@ -465,7 +461,7 @@ module ActionDispatch end def test_namespaced_controller - strexp = Router::Strexp.new( + strexp = Router::Strexp.build( "/:controller(/:action(/:id))", { :controller => /.+?/ }, ["/", ".", "?"] @@ -482,7 +478,7 @@ module ActionDispatch :id => '10' } - @router.recognize(env) do |r, _, params| + @router.recognize(env) do |r, params| assert_equal route, r assert_equal(expected, params) called = true @@ -491,14 +487,14 @@ module ActionDispatch end def test_recognize_literal - path = Path::Pattern.new "/books(/:action(.:format))" + path = Path::Pattern.from_string "/books(/:action(.:format))" app = Object.new route = @router.routes.add_route(app, path, {}, {:controller => 'books'}) env = rails_env 'PATH_INFO' => '/books/list.rss' expected = { :controller => 'books', :action => 'list', :format => 'rss' } called = false - @router.recognize(env) do |r, _, params| + @router.recognize(env) do |r, params| assert_equal route, r assert_equal(expected, params) called = true @@ -508,7 +504,7 @@ module ActionDispatch end def test_recognize_head_request_as_get_route - path = Path::Pattern.new "/books(/:action(.:format))" + path = Path::Pattern.from_string "/books(/:action(.:format))" app = Object.new conditions = { :request_method => 'GET' @@ -519,7 +515,7 @@ module ActionDispatch "REQUEST_METHOD" => "HEAD" called = false - @router.recognize(env) do |r, _, params| + @router.recognize(env) do |r, params| called = true end @@ -527,7 +523,7 @@ module ActionDispatch end def test_recognize_cares_about_verbs - path = Path::Pattern.new "/books(/:action(.:format))" + path = Path::Pattern.from_string "/books(/:action(.:format))" app = Object.new conditions = { :request_method => 'GET' @@ -543,7 +539,7 @@ module ActionDispatch "REQUEST_METHOD" => "POST" called = false - @router.recognize(env) do |r, _, params| + @router.recognize(env) do |r, params| assert_equal post, r called = true end @@ -555,15 +551,17 @@ module ActionDispatch def add_routes router, paths paths.each do |path| - path = Path::Pattern.new path + if String === path + path = Path::Pattern.from_string path + else + path = Path::Pattern.new path + end router.routes.add_route @app, path, {}, {}, {} end end - RailsEnv = Struct.new(:env) - - def rails_env env - RailsEnv.new rack_env env + def rails_env env, klass = ActionDispatch::Request + klass.new env end def rack_env env diff --git a/actionpack/test/journey/routes_test.rb b/actionpack/test/journey/routes_test.rb index 25e0321d31..a4efc82b8c 100644 --- a/actionpack/test/journey/routes_test.rb +++ b/actionpack/test/journey/routes_test.rb @@ -5,7 +5,7 @@ module ActionDispatch class TestRoutes < ActiveSupport::TestCase def test_clear routes = Routes.new - exp = Router::Strexp.new '/foo(/:id)', {}, ['/.?'] + exp = Router::Strexp.build '/foo(/:id)', {}, ['/.?'] path = Path::Pattern.new exp requirements = { :hello => /world/ } @@ -18,7 +18,7 @@ module ActionDispatch def test_ast routes = Routes.new - path = Path::Pattern.new '/hello' + path = Path::Pattern.from_string '/hello' routes.add_route nil, path, {}, {}, {} ast = routes.ast @@ -28,7 +28,7 @@ module ActionDispatch def test_simulator_changes routes = Routes.new - path = Path::Pattern.new '/hello' + path = Path::Pattern.from_string '/hello' routes.add_route nil, path, {}, {}, {} sim = routes.simulator @@ -40,8 +40,8 @@ module ActionDispatch #def add_route app, path, conditions, defaults, name = nil routes = Routes.new - one = Path::Pattern.new '/hello' - two = Path::Pattern.new '/aaron' + one = Path::Pattern.from_string '/hello' + two = Path::Pattern.from_string '/aaron' routes.add_route nil, one, {}, {}, 'aaron' routes.add_route nil, two, {}, {}, 'aaron' diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 50ca64d536..3fc2ab178c 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,8 +1,107 @@ +* Fix that render layout: 'messages/layout' should also be added to the dependency tracker tree. + + *DHH* + +* Add `PartialIteration` object used when rendering collections. + + The iteration object is available as the local variable + `#{template_name}_iteration` when rendering partials with collections. + + It gives access to the `size` of the collection being iterated over, + the current `index` and two convenience methods `first?` and `last?`. + + *Joel Junström*, *Lucas Uyezu* + +* Return an absolute instead of relative path from an asset url in the case + of the `asset_host` proc returning nil + + *Jolyon Pawlyn* + +* Fix `html_escape_once` to properly handle hex escape sequences (e.g. ᨫ) + + *John F. Douthat* + +* Added String support for min and max properties for date field helpers. + + *Todd Bealmear* + +* The `highlight` helper now accepts a block to be used instead of the `highlighter` + option. + + *Lucas Mazza* + +* The `except` and `highlight` helpers now accept regular expressions. + + *Jan Szumiec* + +* Flatten the array parameter in `safe_join`, so it behaves consistently with + `Array#join`. + + *Paul Grayson* + +* Honor `html_safe` on array elements in tag values, as we do for plain string + values. + + *Paul Grayson* + +* Add `ActionView::Template::Handler.unregister_template_handler`. + + It performs the opposite of `ActionView::Template::Handler.register_template_handler`. + + *Zuhao Wan* + +* Bring `cache_digest` rake tasks up-to-date with the latest API changes + + *Jiri Pospisil* + +* Allow custom `:host` option to be passed to `asset_url` helper that + overwrites `config.action_controller.asset_host` for particular asset. + + *Hubert Łępicki* + +* Deprecate `AbstractController::Base.parent_prefixes`. + Override `AbstractController::Base.local_prefixes` when you want to change + where to find views. + + *Nick Sutterer* + +* Take label values into account when doing I18n lookups for model attributes. + + The following: + + # form.html.erb + <%= form_for @post do |f| %> + <%= f.label :type, value: "long" %> + <% end %> + + # en.yml + en: + activerecord: + attributes: + post/long: "Long-form Post" + + Used to simply return "long", but now it will return "Long-form + Post". + + *Joshua Cody* + +* Change `asset_path` to use File.join to create proper paths: + + Before: + + https://some.host.com//assets/some.js + + After: + + https://some.host.com/assets/some.js + + *Peter Schröder* + * Change `favicon_link_tag` default mimetype from `image/vnd.microsoft.icon` to `image/x-icon`. Before: - + #=> favicon_link_tag 'myicon.ico' <link href="/assets/myicon.ico" rel="shortcut icon" type="image/vnd.microsoft.icon" /> @@ -16,7 +115,7 @@ * Remove wrapping div with inline styles for hidden form fields. We are dropping HTML 4.01 and XHTML strict compliance since input tags directly - inside a form are valid HTML5, and the absense of inline styles help in validating + inside a form are valid HTML5, and the absence of inline styles help in validating for Content Security Policy. *Joost Baaij* @@ -57,5 +156,4 @@ *Piotr Chmolowski, Łukasz Strzałkowski* - Please check [4-1-stable](https://github.com/rails/rails/blob/4-1-stable/actionview/CHANGELOG.md) for previous changes. diff --git a/actionview/README.rdoc b/actionview/README.rdoc index 35f805346c..5bb62c7562 100644 --- a/actionview/README.rdoc +++ b/actionview/README.rdoc @@ -29,6 +29,11 @@ API documentation is at * http://api.rubyonrails.org -Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here: +Bug reports can be filed for the Ruby on Rails project here: * https://github.com/rails/rails/issues + +Feature requests should be discussed on the rails-core mailing list here: + +* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core + diff --git a/actionview/RUNNING_UNIT_TESTS.rdoc b/actionview/RUNNING_UNIT_TESTS.rdoc index 104b3e288d..6c4e5e983a 100644 --- a/actionview/RUNNING_UNIT_TESTS.rdoc +++ b/actionview/RUNNING_UNIT_TESTS.rdoc @@ -4,7 +4,7 @@ The easiest way to run the unit tests is through Rake. The default task runs the entire test suite for all classes. For more information, checkout the full array of rake tasks with "rake -T" -Rake can be found at http://rake.rubyforge.org +Rake can be found at http://docs.seattlerb.org/rake/. == Running by hand @@ -19,8 +19,8 @@ which can be further narrowed down to one test: == Dependency on Active Record and database setup 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, +activerecord and sqlite3 installed. If Active Record is not in +actionview/../activerecord directory, or the sqlite3 rubygem is not installed, these tests are skipped. Other tests are runnable from a fresh copy of actionview without any configuration. diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb index 455ce531ae..900f96255e 100644 --- a/actionview/lib/action_view/base.rb +++ b/actionview/lib/action_view/base.rb @@ -131,7 +131,8 @@ module ActionView #:nodoc: # end # end # - # More builder documentation can be found at http://builder.rubyforge.org. + # For more information on Builder please consult the [source + # code](https://github.com/jimweirich/builder). class Base include Helpers, ::ERB::Util, Context diff --git a/actionview/lib/action_view/dependency_tracker.rb b/actionview/lib/action_view/dependency_tracker.rb index 0ccf2515c5..e34bdd4a46 100644 --- a/actionview/lib/action_view/dependency_tracker.rb +++ b/actionview/lib/action_view/dependency_tracker.rb @@ -53,6 +53,12 @@ module ActionView \s* # followed by optional spaces /x + # Part of any hash containing the :layout key + LAYOUT_HASH_KEY = / + (?:\blayout:|:layout\s*=>) # layout key in either old or new style hash syntax + \s* # followed by optional spaces + /x + # Matches: # partial: "comments/comment", collection: @all_comments => "comments/comment" # (object: @single_comment, partial: "comments/comment") => "comments/comment" @@ -65,9 +71,9 @@ module ActionView # topics => "topics/topic" # (message.topics) => "topics/topic" RENDER_ARGUMENTS = /\A - (?:\s*\(?\s*) # optional opening paren surrounded by spaces - (?:.*?#{PARTIAL_HASH_KEY})? # optional hash, up to the partial key declaration - (?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest + (?:\s*\(?\s*) # optional opening paren surrounded by spaces + (?:.*?#{PARTIAL_HASH_KEY}|#{LAYOUT_HASH_KEY})? # optional hash, up to the partial or layout key declaration + (?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest /xm def self.call(name, template) @@ -85,8 +91,8 @@ module ActionView attr_reader :name, :template private :name, :template - private + private def source template.source end diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index 72d79735ae..1f103786cb 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -60,7 +60,7 @@ module ActionView def digest Digest::MD5.hexdigest("#{source}-#{dependency_digest}").tap do |digest| - logger.try :info, " Cache digest for #{template.inspect}: #{digest}" + logger.try :debug, " Cache digest for #{template.inspect}: #{digest}" end rescue ActionView::MissingTemplate logger.try :error, " Couldn't find template for digesting: #{name}" diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index 824cdaa45e..669050e7a7 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -7,7 +7,7 @@ module ActionView # = Action View Asset Tag Helpers module Helpers #:nodoc: # This module provides methods for generating HTML that links views to assets such - # as images, javascripts, stylesheets, and feeds. These methods do not verify + # as images, JavaScripts, stylesheets, and feeds. These methods do not verify # the assets exist before linking to them: # # image_tag("rails.png") @@ -258,19 +258,19 @@ module ActionView # ==== Examples # # video_tag("trailer") - # # => <video src="/videos/trailer" /> + # # => <video src="/videos/trailer"></video> # video_tag("trailer.ogg") - # # => <video src="/videos/trailer.ogg" /> + # # => <video src="/videos/trailer.ogg"></video> # video_tag("trailer.ogg", controls: true, autobuffer: true) - # # => <video autobuffer="autobuffer" controls="controls" src="/videos/trailer.ogg" /> + # # => <video autobuffer="autobuffer" controls="controls" src="/videos/trailer.ogg" ></video> # video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png") - # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/assets/screenshot.png" /> + # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/assets/screenshot.png"></video> # video_tag("/trailers/hd.avi", size: "16x16") - # # => <video src="/trailers/hd.avi" width="16" height="16" /> + # # => <video src="/trailers/hd.avi" width="16" height="16"></video> # video_tag("/trailers/hd.avi", size: "16") - # # => <video height="16" src="/trailers/hd.avi" width="16" /> + # # => <video height="16" src="/trailers/hd.avi" width="16"></video> # video_tag("/trailers/hd.avi", height: '32', width: '32') - # # => <video height="32" src="/trailers/hd.avi" width="32" /> + # # => <video height="32" src="/trailers/hd.avi" width="32"></video> # video_tag("trailer.ogg", "trailer.flv") # # => <video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video> # video_tag(["trailer.ogg", "trailer.flv"]) @@ -289,11 +289,11 @@ module ActionView # your public audios directory. # # audio_tag("sound") - # # => <audio src="/audios/sound" /> + # # => <audio src="/audios/sound"></audio> # audio_tag("sound.wav") - # # => <audio src="/audios/sound.wav" /> + # # => <audio src="/audios/sound.wav"></audio> # audio_tag("sound.wav", autoplay: true, controls: true) - # # => <audio autoplay="autoplay" controls="controls" src="/audios/sound.wav" /> + # # => <audio autoplay="autoplay" controls="controls" src="/audios/sound.wav"></audio> # audio_tag("sound.wav", "sound.mid") # # => <audio><source src="/audios/sound.wav" /><source src="/audios/sound.mid" /></audio> def audio_tag(*sources) diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb index c830ab23e3..9e8d005ec7 100644 --- a/actionview/lib/action_view/helpers/asset_url_helper.rb +++ b/actionview/lib/action_view/helpers/asset_url_helper.rb @@ -88,9 +88,12 @@ module ActionView # still sending assets for plain HTTP requests from asset hosts. If you don't # have SSL certificates for each of the asset hosts this technique allows you # to avoid warnings in the client about mixed media. + # Note that the request parameter might not be supplied, e.g. when the assets + # are precompiled via a Rake task. Make sure to use a Proc instead of a lambda, + # since a Proc allows missing parameters and sets them to nil. # # config.action_controller.asset_host = Proc.new { |source, request| - # if request.ssl? + # if request && request.ssl? # "#{request.protocol}#{request.host_with_port}" # else # "#{request.protocol}assets.example.com" @@ -113,13 +116,13 @@ module ActionView # # All other asset *_path helpers delegate through this method. # - # asset_path "application.js" # => /application.js - # asset_path "application", type: :javascript # => /javascripts/application.js - # asset_path "application", type: :stylesheet # => /stylesheets/application.css + # asset_path "application.js" # => /assets/application.js + # asset_path "application", type: :javascript # => /assets/application.js + # asset_path "application", type: :stylesheet # => /assets/application.css # asset_path "http://www.example.com/js/xmlhr.js" # => http://www.example.com/js/xmlhr.js def asset_path(source, options = {}) - source = source.to_s return "" unless source.present? + source = source.to_s return source if source =~ URI_REGEXP tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, '') @@ -134,11 +137,11 @@ module ActionView relative_url_root = defined?(config.relative_url_root) && config.relative_url_root if relative_url_root - source = "#{relative_url_root}#{source}" unless source.starts_with?("#{relative_url_root}/") + source = File.join(relative_url_root, source) unless source.starts_with?("#{relative_url_root}/") end if host = compute_asset_host(source, options) - source = "#{host}#{source}" + source = File.join(host, source) end "#{source}#{tail}" @@ -147,7 +150,14 @@ module ActionView # 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. + # will be the same. If :host options is set, it overwrites global + # +config.action_controller.asset_host+ setting. + # + # All other options provided are forwarded to +asset_path+ call. + # + # asset_url "application.js" # => http://example.com/assets/application.js + # asset_url "application.js", host: "http://cdn.example.com" # => http://cdn.example.com/assets/application.js + # def asset_url(source, options = {}) path_to_asset(source, options.merge(:protocol => :request)) end @@ -191,8 +201,8 @@ module ActionView # (proc or otherwise). def compute_asset_host(source = "", options = {}) request = self.request if respond_to?(:request) - host = config.asset_host if defined? config.asset_host - host ||= request.base_url if request && options[:protocol] == :request + host = options[:host] + host ||= config.asset_host if defined? config.asset_host if host.respond_to?(:call) arity = host.respond_to?(:arity) ? host.arity : host.method(:call).arity @@ -203,6 +213,7 @@ module ActionView host = host % (Zlib.crc32(source) % 4) end + host ||= request.base_url if request && options[:protocol] == :request return unless host if host =~ URI_REGEXP @@ -220,13 +231,13 @@ module ActionView end end - # Computes the path to a javascript asset in the public javascripts directory. + # Computes the path to a JavaScript asset in the public javascripts directory. # If the +source+ filename has no extension, .js will be appended (except for explicit URIs) # Full paths from the document root will be passed through. - # Used internally by javascript_include_tag to build the script path. + # Used internally by +javascript_include_tag+ to build the script path. # - # javascript_path "xmlhr" # => /javascripts/xmlhr.js - # javascript_path "dir/xmlhr.js" # => /javascripts/dir/xmlhr.js + # javascript_path "xmlhr" # => /assets/xmlhr.js + # javascript_path "dir/xmlhr.js" # => /assets/dir/xmlhr.js # javascript_path "/dir/xmlhr" # => /dir/xmlhr.js # javascript_path "http://www.example.com/js/xmlhr" # => http://www.example.com/js/xmlhr # javascript_path "http://www.example.com/js/xmlhr.js" # => http://www.example.com/js/xmlhr.js @@ -235,7 +246,7 @@ module ActionView end alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route - # Computes the full URL to a javascript asset in the public javascripts directory. + # Computes the full URL to a JavaScript asset in the public javascripts directory. # This will use +javascript_path+ internally, so most of their behaviors will be the same. def javascript_url(source, options = {}) url_to_asset(source, {type: :javascript}.merge!(options)) @@ -243,12 +254,12 @@ module ActionView alias_method :url_to_javascript, :javascript_url # aliased to avoid conflicts with a javascript_url named route # Computes the path to a stylesheet asset in the public stylesheets directory. - # If the +source+ filename has no extension, <tt>.css</tt> will be appended (except for explicit URIs). + # If the +source+ filename has no extension, .css will be appended (except for explicit URIs). # Full paths from the document root will be passed through. # Used internally by +stylesheet_link_tag+ to build the stylesheet path. # - # stylesheet_path "style" # => /stylesheets/style.css - # stylesheet_path "dir/style.css" # => /stylesheets/dir/style.css + # stylesheet_path "style" # => /assets/style.css + # stylesheet_path "dir/style.css" # => /assets/dir/style.css # stylesheet_path "/dir/style.css" # => /dir/style.css # stylesheet_path "http://www.example.com/css/style" # => http://www.example.com/css/style # stylesheet_path "http://www.example.com/css/style.css" # => http://www.example.com/css/style.css @@ -334,9 +345,9 @@ module ActionView # Computes the path to a font asset. # Full paths from the document root will be passed through. # - # font_path("font") # => /assets/font - # font_path("font.ttf") # => /assets/font.ttf - # font_path("dir/font.ttf") # => /assets/dir/font.ttf + # font_path("font") # => /fonts/font + # font_path("font.ttf") # => /fonts/font.ttf + # font_path("dir/font.ttf") # => /fonts/dir/font.ttf # font_path("/dir/font.ttf") # => /dir/font.ttf # font_path("http://www.example.com/dir/font.ttf") # => http://www.example.com/dir/font.ttf def font_path(source, options = {}) diff --git a/actionview/lib/action_view/helpers/capture_helper.rb b/actionview/lib/action_view/helpers/capture_helper.rb index 5afe435459..75d1634b2e 100644 --- a/actionview/lib/action_view/helpers/capture_helper.rb +++ b/actionview/lib/action_view/helpers/capture_helper.rb @@ -202,15 +202,6 @@ module ActionView ensure self.output_buffer = old_buffer end - - # Add the output buffer to the response body and start a new one. - def flush_output_buffer #:nodoc: - if output_buffer && !output_buffer.empty? - response.stream.write output_buffer - self.output_buffer = output_buffer.respond_to?(:clone_empty) ? output_buffer.clone_empty : output_buffer[0, 0] - nil - end - end end end end diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb index 2efb9612ac..27c7a26098 100644 --- a/actionview/lib/action_view/helpers/date_helper.rb +++ b/actionview/lib/action_view/helpers/date_helper.rb @@ -153,8 +153,8 @@ module ActionView # # Note that you cannot pass a <tt>Numeric</tt> value to <tt>time_ago_in_words</tt>. # - def time_ago_in_words(from_time, include_seconds_or_options = {}) - distance_of_time_in_words(from_time, Time.now, include_seconds_or_options) + def time_ago_in_words(from_time, options = {}) + distance_of_time_in_words(from_time, Time.now, options) end alias_method :distance_of_time_in_words_to_now, :time_ago_in_words diff --git a/actionview/lib/action_view/helpers/debug_helper.rb b/actionview/lib/action_view/helpers/debug_helper.rb index c29c1b1eea..ba47eee9ba 100644 --- a/actionview/lib/action_view/helpers/debug_helper.rb +++ b/actionview/lib/action_view/helpers/debug_helper.rb @@ -11,24 +11,20 @@ module ActionView # If the object cannot be converted to YAML using +to_yaml+, +inspect+ will be called instead. # Useful for inspecting an object at the time of rendering. # - # @user = User.new({ username: 'testing', password: 'xyz', age: 42}) %> + # @user = User.new({ username: 'testing', password: 'xyz', age: 42}) # debug(@user) # # => # <pre class='debug_dump'>--- !ruby/object:User # attributes: - # updated_at: - # username: testing - # - # age: 42 - # password: xyz - # created_at: - # attributes_cache: {} - # - # new_record: true + # updated_at: + # username: testing + # age: 42 + # password: xyz + # created_at: # </pre> def debug(object) Marshal::dump(object) - object = ERB::Util.html_escape(object.to_yaml).gsub(" ", " ").html_safe + object = ERB::Util.html_escape(object.to_yaml) content_tag(:pre, object, :class => "debug_dump") rescue Exception # errors from Marshal or YAML # Object couldn't be dumped, perhaps because of singleton methods -- this is the fallback diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 22bfd87d85..c6bc0c9e38 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -434,7 +434,8 @@ module ActionView output = capture(builder, &block) html_options[:multipart] ||= builder.multipart? - form_tag(options[:url] || {}, html_options) { output } + html_options = html_options_for_form(options[:url] || {}, html_options) + form_tag_with_body(html_options, output) end def apply_form_for_options!(record, object, options) #:nodoc: @@ -449,7 +450,11 @@ module ActionView method: method ) - options[:url] ||= polymorphic_path(record, format: options.delete(:format)) + options[:url] ||= if options.key?(:format) + polymorphic_path(record, format: options.delete(:format)) + else + polymorphic_path(record, {}) + end end private :apply_form_for_options! @@ -477,7 +482,7 @@ module ActionView # Admin? : <%= permission_fields.check_box :admin %> # <% end %> # - # <%= f.submit %> + # <%= person_form.submit %> # <% end %> # # In this case, the checkbox field will be represented by an HTML +input+ @@ -1008,6 +1013,18 @@ module ActionView # date_field("user", "born_on", value: "1984-05-12") # # => <input id="user_born_on" name="user[born_on]" type="date" value="1984-05-12" /> # + # You can create values for the "min" and "max" attributes by passing + # instances of Date or Time to the options hash. + # + # date_field("user", "born_on", min: Date.today) + # # => <input id="user_born_on" name="user[born_on]" type="date" min="2014-05-20" /> + # + # Alternatively, you can pass a String formatted as an ISO8601 date as the + # values for "min" and "max." + # + # date_field("user", "born_on", min: "2014-05-20") + # # => <input id="user_born_on" name="user[born_on]" type="date" min="2014-05-20" /> + # def date_field(object_name, method, options = {}) Tags::DateField.new(object_name, method, self, options).render end @@ -1025,6 +1042,18 @@ module ActionView # time_field("task", "started_at") # # => <input id="task_started_at" name="task[started_at]" type="time" /> # + # You can create values for the "min" and "max" attributes by passing + # instances of Date or Time to the options hash. + # + # time_field("task", "started_at", min: Time.now) + # # => <input id="task_started_at" name="task[started_at]" type="time" min="01:00:00.000" /> + # + # Alternatively, you can pass a String formatted as an ISO8601 time as the + # values for "min" and "max." + # + # time_field("task", "started_at", min: "01:00:00") + # # => <input id="task_started_at" name="task[started_at]" type="time" min="01:00:00.000" /> + # def time_field(object_name, method, options = {}) Tags::TimeField.new(object_name, method, self, options).render end @@ -1042,6 +1071,18 @@ module ActionView # datetime_field("user", "born_on") # # => <input id="user_born_on" name="user[born_on]" type="datetime" value="1984-01-12T00:00:00.000+0000" /> # + # You can create values for the "min" and "max" attributes by passing + # instances of Date or Time to the options hash. + # + # datetime_field("user", "born_on", min: Date.today) + # # => <input id="user_born_on" name="user[born_on]" type="datetime" min="2014-05-20T00:00:00.000+0000" /> + # + # Alternatively, you can pass a String formatted as an ISO8601 datetime + # with UTC offset as the values for "min" and "max." + # + # datetime_field("user", "born_on", min: "2014-05-20T00:00:00+0000") + # # => <input id="user_born_on" name="user[born_on]" type="datetime" min="2014-05-20T00:00:00.000+0000" /> + # def datetime_field(object_name, method, options = {}) Tags::DatetimeField.new(object_name, method, self, options).render end @@ -1059,6 +1100,18 @@ module ActionView # datetime_local_field("user", "born_on") # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" value="1984-01-12T00:00:00" /> # + # You can create values for the "min" and "max" attributes by passing + # instances of Date or Time to the options hash. + # + # datetime_local_field("user", "born_on", min: Date.today) + # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" min="2014-05-20T00:00:00.000" /> + # + # Alternatively, you can pass a String formatted as an ISO8601 datetime as + # the values for "min" and "max." + # + # datetime_local_field("user", "born_on", min: "2014-05-20T00:00:00") + # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" min="2014-05-20T00:00:00.000" /> + # def datetime_local_field(object_name, method, options = {}) Tags::DatetimeLocalField.new(object_name, method, self, options).render end diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb index 48f42947db..528e2828a1 100644 --- a/actionview/lib/action_view/helpers/form_options_helper.rb +++ b/actionview/lib/action_view/helpers/form_options_helper.rb @@ -152,11 +152,9 @@ module ActionView # To prevent this the helper generates an auxiliary hidden field before # every multiple select. The hidden field has the same name as multiple select and blank value. # - # This way, the client either sends only the hidden field (representing - # the deselected multiple select box), or both fields. Since the HTML specification - # says key/value pairs have to be sent in the same order they appear in the - # form, and parameters extraction gets the last occurrence of any repeated - # key in the query string, that works for ordinary forms. + # <b>Note:</b> The client either sends only the hidden field (representing + # the deselected multiple select box), or both fields. This means that the resulting array + # always contains a blank string. # # In case if you don't want the helper to generate this hidden field you can specify # <tt>include_hidden: false</tt> option. diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb index 8f10eb46ad..b18f578183 100644 --- a/actionview/lib/action_view/helpers/form_tag_helper.rb +++ b/actionview/lib/action_view/helpers/form_tag_helper.rb @@ -67,7 +67,7 @@ module ActionView def form_tag(url_for_options = {}, options = {}, &block) html_options = html_options_for_form(url_for_options, options) if block_given? - form_tag_in_block(html_options, &block) + form_tag_with_body(html_options, capture(&block)) else form_tag_html(html_options) end @@ -82,14 +82,18 @@ module ActionView # ==== Options # * <tt>:multiple</tt> - If set to true the selection will allow multiple choices. # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input. - # * <tt>:include_blank</tt> - If set to true, an empty option will be created. - # * <tt>:prompt</tt> - Create a prompt option with blank value and the text asking user to select something + # * <tt>:include_blank</tt> - If set to true, an empty option will be created. If set to a string, the string will be used as the option's content and the value will be empty. + # * <tt>:prompt</tt> - Create a prompt option with blank value and the text asking user to select something. + # * <tt>:selected</tt> - Provide a default selected value. It should be of the exact type as the provided options. # * Any other key creates standard HTML attributes for the tag. # # ==== Examples # select_tag "people", options_from_collection_for_select(@people, "id", "name") # # <select id="people" name="people"><option value="1">David</option></select> # + # select_tag "people", options_from_collection_for_select(@people, "id", "name"), selected: ["1", "David"] + # # <select id="people" name="people"><option value="1" selected="selected">David</option></select> + # # select_tag "people", "<option>David</option>".html_safe # # => <select id="people" name="people"><option>David</option></select> # @@ -105,13 +109,16 @@ module ActionView # # => <select id="locations" name="locations"><option>Home</option><option selected='selected'>Work</option> # # <option>Out</option></select> # - # select_tag "access", "<option>Read</option><option>Write</option>".html_safe, multiple: true, class: 'form_input' - # # => <select class="form_input" id="access" multiple="multiple" name="access[]"><option>Read</option> + # select_tag "access", "<option>Read</option><option>Write</option>".html_safe, multiple: true, class: 'form_input', id: 'unique_id' + # # => <select class="form_input" id="unique_id" multiple="multiple" name="access[]"><option>Read</option> # # <option>Write</option></select> # # select_tag "people", options_from_collection_for_select(@people, "id", "name"), include_blank: true # # => <select id="people" name="people"><option value=""></option><option value="1">David</option></select> # + # select_tag "people", options_from_collection_for_select(@people, "id", "name"), include_blank: "All" + # # => <select id="people" name="people"><option value="">All</option><option value="1">David</option></select> + # # select_tag "people", options_from_collection_for_select(@people, "id", "name"), prompt: "Select something" # # => <select id="people" name="people"><option value="">Select something</option><option value="1">David</option></select> # @@ -501,19 +508,19 @@ module ActionView # # ==== Examples # image_submit_tag("login.png") - # # => <input alt="Login" src="/images/login.png" type="image" /> + # # => <input alt="Login" src="/assets/login.png" type="image" /> # # image_submit_tag("purchase.png", disabled: true) - # # => <input alt="Purchase" disabled="disabled" src="/images/purchase.png" type="image" /> + # # => <input alt="Purchase" disabled="disabled" src="/assets/purchase.png" type="image" /> # # image_submit_tag("search.png", class: 'search_button', alt: 'Find') - # # => <input alt="Find" class="search_button" src="/images/search.png" type="image" /> + # # => <input alt="Find" class="search_button" src="/assets/search.png" type="image" /> # # image_submit_tag("agree.png", disabled: true, class: "agree_disagree_button") - # # => <input alt="Agree" class="agree_disagree_button" disabled="disabled" src="/images/agree.png" type="image" /> + # # => <input alt="Agree" class="agree_disagree_button" disabled="disabled" src="/assets/agree.png" type="image" /> # # image_submit_tag("save.png", data: { confirm: "Are you sure?" }) - # # => <input alt="Save" src="/images/save.png" data-confirm="Are you sure?" type="image" /> + # # => <input alt="Save" src="/assets/save.png" data-confirm="Are you sure?" type="image" /> def image_submit_tag(source, options = {}) options = options.stringify_keys tag :input, { "alt" => image_alt(source), "type" => "image", "src" => path_to_image(source) }.update(options) @@ -789,7 +796,10 @@ module ActionView # Creates the hidden UTF8 enforcer tag. Override this method in a helper # to customize the tag. def utf8_enforcer_tag - tag(:input, :type => "hidden", :name => "utf8", :value => "✓".html_safe) + # Use raw HTML to ensure the value is written as an HTML entity; it + # needs to be the right character regardless of which encoding the + # browser infers. + '<input name="utf8" type="hidden" value="✓" />'.html_safe end private @@ -844,8 +854,7 @@ module ActionView tag(:form, html_options, true) + extra_tags end - def form_tag_in_block(html_options, &block) - content = capture(&block) + def form_tag_with_body(html_options, content) output = form_tag_html(html_options) output << content output.safe_concat("</form>") diff --git a/actionview/lib/action_view/helpers/javascript_helper.rb b/actionview/lib/action_view/helpers/javascript_helper.rb index e475d5b018..629c447f3f 100644 --- a/actionview/lib/action_view/helpers/javascript_helper.rb +++ b/actionview/lib/action_view/helpers/javascript_helper.rb @@ -47,7 +47,13 @@ module ActionView # tag. # # javascript_tag "alert('All is good')", defer: 'defer' - # # => <script defer="defer">alert('All is good')</script> + # + # Returns: + # <script defer="defer"> + # //<![CDATA[ + # alert('All is good') + # //]]> + # </script> # # Instead of passing the content as an argument, you can also use a block # in which case, you pass your +html_options+ as the first parameter. diff --git a/actionview/lib/action_view/helpers/number_helper.rb b/actionview/lib/action_view/helpers/number_helper.rb index 7157a95146..7220bded3c 100644 --- a/actionview/lib/action_view/helpers/number_helper.rb +++ b/actionview/lib/action_view/helpers/number_helper.rb @@ -266,14 +266,8 @@ module ActionView # number_to_human_size(1234567, precision: 2) # => 1.2 MB # number_to_human_size(483989, precision: 2) # => 470 KB # number_to_human_size(1234567, precision: 2, separator: ',') # => 1,2 MB - # - # Non-significant zeros after the fractional separator are - # stripped out by default (set - # <tt>:strip_insignificant_zeros</tt> to +false+ to change - # that): - # - # number_to_human_size(1234567890123, precision: 5) # => "1.1229 TB" - # number_to_human_size(524288000, precision: 5) # => "500 MB" + # number_to_human_size(1234567890123, precision: 5) # => "1.1228 TB" + # number_to_human_size(524288000, precision: 5) # => "500 MB" def number_to_human_size(number, options = {}) delegate_number_helper_method(:number_to_human_size, number, options) end @@ -343,11 +337,15 @@ module ActionView # separator: ',', # significant: false) # => "1,2 Million" # + # number_to_human(500000000, precision: 5) # => "500 Million" + # number_to_human(12345012345, significant: false) # => "12.345 Billion" + # # Non-significant zeros after the decimal separator are stripped # out by default (set <tt>:strip_insignificant_zeros</tt> to # +false+ to change that): - # number_to_human(12345012345, significant_digits: 6) # => "12.345 Billion" - # number_to_human(500000000, precision: 5) # => "500 Million" + # + # number_to_human(12.00001) # => "12" + # number_to_human(12.00001, strip_insignificant_zeros: false) # => "12.0" # # ==== Custom Unit Quantifiers # diff --git a/actionview/lib/action_view/helpers/output_safety_helper.rb b/actionview/lib/action_view/helpers/output_safety_helper.rb index 60a4478c26..f03362d0f5 100644 --- a/actionview/lib/action_view/helpers/output_safety_helper.rb +++ b/actionview/lib/action_view/helpers/output_safety_helper.rb @@ -17,10 +17,10 @@ module ActionView #:nodoc: stringish.to_s.html_safe end - # This method returns a html safe string similar to what <tt>Array#join</tt> - # would return. All items in the array, including the supplied separator, are - # html escaped unless they are html safe, and the returned string is marked - # as html safe. + # This method returns an html safe string similar to what <tt>Array#join</tt> + # would return. The array is flattened, and all items, including + # the supplied separator, are html escaped unless they are html + # safe, and the returned string is marked as html safe. # # safe_join(["<p>foo</p>".html_safe, "<p>bar</p>"], "<br />") # # => "<p>foo</p><br /><p>bar</p>" @@ -29,9 +29,9 @@ module ActionView #:nodoc: # # => "<p>foo</p><br /><p>bar</p>" # def safe_join(array, sep=$,) - sep = ERB::Util.html_escape(sep) + sep = ERB::Util.unwrapped_html_escape(sep) - array.map { |i| ERB::Util.html_escape(i) }.join(sep).html_safe + array.flatten.map! { |i| ERB::Util.unwrapped_html_escape(i) }.join(sep).html_safe end end end diff --git a/actionview/lib/action_view/helpers/rendering_helper.rb b/actionview/lib/action_view/helpers/rendering_helper.rb index ebfc35a4c7..6cd6e858dd 100644 --- a/actionview/lib/action_view/helpers/rendering_helper.rb +++ b/actionview/lib/action_view/helpers/rendering_helper.rb @@ -13,13 +13,13 @@ module ActionView # * <tt>:inline</tt> - Renders an inline template similar to how it's done in the controller. # * <tt>:text</tt> - Renders the text passed in out. # * <tt>:plain</tt> - Renders the text passed in out. Setting the content - # type as <tt>text/plain</tt>. + # type as <tt>text/plain</tt>. # * <tt>:html</tt> - Renders the html safe string passed in out, otherwise - # performs html escape on the string first. Setting the content type as - # <tt>text/html</tt>. + # performs html escape on the string first. Setting the content type as + # <tt>text/html</tt>. # * <tt>:body</tt> - Renders the text passed in, and inherits the content - # type of <tt>text/html</tt> from <tt>ActionDispatch::Response</tt> - # object. + # type of <tt>text/html</tt> from <tt>ActionDispatch::Response</tt> + # object. # # If no options hash is passed or :update specified, the default is to render a partial and use the second parameter # as the locals hash. diff --git a/actionview/lib/action_view/helpers/sanitize_helper.rb b/actionview/lib/action_view/helpers/sanitize_helper.rb index e5cb843670..049af275b6 100644 --- a/actionview/lib/action_view/helpers/sanitize_helper.rb +++ b/actionview/lib/action_view/helpers/sanitize_helper.rb @@ -48,7 +48,7 @@ module ActionView # Change allowed default attributes # # class Application < Rails::Application - # config.action_view.sanitized_allowed_attributes = 'id', 'class', 'style' + # config.action_view.sanitized_allowed_attributes = ['id', 'class', 'style'] # end # # Please note that sanitizing user-provided text does not guarantee that the @@ -204,7 +204,7 @@ module ActionView # Adds to the Set of allowed HTML attributes for the +sanitize+ helper. # # class Application < Rails::Application - # config.action_view.sanitized_allowed_attributes = 'onclick', 'longdesc' + # config.action_view.sanitized_allowed_attributes = ['onclick', 'longdesc'] # end # def sanitized_allowed_attributes=(attributes) diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb index a9f3b0ffbc..268558669e 100644 --- a/actionview/lib/action_view/helpers/tag_helper.rb +++ b/actionview/lib/action_view/helpers/tag_helper.rb @@ -9,6 +9,7 @@ module ActionView module TagHelper extend ActiveSupport::Concern include CaptureHelper + include OutputSafetyHelper BOOLEAN_ATTRIBUTES = %w(disabled readonly multiple checked autobuffer autoplay controls loop selected hidden scoped async @@ -139,7 +140,7 @@ module ActionView def content_tag_string(name, content, options, escape = true) tag_options = tag_options(options, escape) if options - content = ERB::Util.h(content) if escape + content = ERB::Util.unwrapped_html_escape(content) if escape "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name.to_sym]}#{content}</#{name}>".html_safe end @@ -173,8 +174,11 @@ module ActionView end def tag_option(key, value, escape) - value = value.join(" ") if value.is_a?(Array) - value = ERB::Util.h(value) if escape + if value.is_a?(Array) + value = escape ? safe_join(value, " ") : value.join(" ") + else + value = escape ? ERB::Util.unwrapped_html_escape(value) : value + end %(#{key}="#{value}") end end diff --git a/actionview/lib/action_view/helpers/tags/datetime_field.rb b/actionview/lib/action_view/helpers/tags/datetime_field.rb index 25e7e05ec6..b2cee9d198 100644 --- a/actionview/lib/action_view/helpers/tags/datetime_field.rb +++ b/actionview/lib/action_view/helpers/tags/datetime_field.rb @@ -5,8 +5,8 @@ module ActionView def render options = @options.stringify_keys options["value"] ||= format_date(value(object)) - options["min"] = format_date(options["min"]) - options["max"] = format_date(options["max"]) + options["min"] = format_date(datetime_value(options["min"])) + options["max"] = format_date(datetime_value(options["max"])) @options = options super end @@ -16,6 +16,14 @@ module ActionView def format_date(value) value.try(:strftime, "%Y-%m-%dT%T.%L%z") end + + def datetime_value(value) + if value.is_a? String + DateTime.parse(value) rescue nil + else + value + end + end end end end diff --git a/actionview/lib/action_view/helpers/tags/label.rb b/actionview/lib/action_view/helpers/tags/label.rb index 6335e3dd4d..a5bcaf8153 100644 --- a/actionview/lib/action_view/helpers/tags/label.rb +++ b/actionview/lib/action_view/helpers/tags/label.rb @@ -35,9 +35,9 @@ module ActionView if block_given? content = @template_object.capture(&block) else + method_and_value = tag_value.present? ? "#{@method_name}.#{tag_value}" : @method_name content = if @content.blank? @object_name.gsub!(/\[(.*)_attributes\]\[\d+\]/, '.\1') - method_and_value = tag_value.present? ? "#{@method_name}.#{tag_value}" : @method_name if object.respond_to?(:to_model) key = object.class.model_name.i18n_key @@ -51,7 +51,7 @@ module ActionView end content ||= if object && object.class.respond_to?(:human_attribute_name) - object.class.human_attribute_name(@method_name) + object.class.human_attribute_name(method_and_value) end content ||= @method_name.humanize diff --git a/actionview/lib/action_view/helpers/tags/text_field.rb b/actionview/lib/action_view/helpers/tags/text_field.rb index e910879ebf..e0b80d81c2 100644 --- a/actionview/lib/action_view/helpers/tags/text_field.rb +++ b/actionview/lib/action_view/helpers/tags/text_field.rb @@ -7,7 +7,6 @@ module ActionView options["size"] = options["maxlength"] unless options.key?("size") options["type"] ||= field_type options["value"] = options.fetch("value") { value_before_type_cast(object) } unless field_type == "file" - options["value"] &&= ERB::Util.html_escape(options["value"]) add_default_name_and_id(options) tag("input", options) end diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb index 7cfbca5b6f..b859653bc9 100644 --- a/actionview/lib/action_view/helpers/text_helper.rb +++ b/actionview/lib/action_view/helpers/text_helper.rb @@ -103,11 +103,14 @@ module ActionView # Highlights one or more +phrases+ everywhere in +text+ by inserting it into # a <tt>:highlighter</tt> string. The highlighter can be specialized by passing <tt>:highlighter</tt> # as a single-quoted string with <tt>\1</tt> where the phrase is to be inserted (defaults to - # '<mark>\1</mark>') + # '<mark>\1</mark>') or passing a block that receives each matched term. # # highlight('You searched for: rails', 'rails') # # => You searched for: <mark>rails</mark> # + # highlight('You searched for: rails', /for|rails/) + # # => You searched <mark>for</mark>: <mark>rails</mark> + # # highlight('You searched for: ruby, rails, dhh', 'actionpack') # # => You searched for: ruby, rails, dhh # @@ -116,15 +119,25 @@ module ActionView # # highlight('You searched for: rails', 'rails', highlighter: '<a href="search?q=\1">\1</a>') # # => You searched for: <a href="search?q=rails">rails</a> + # + # highlight('You searched for: rails', 'rails') { |match| link_to(search_path(q: match, match)) } + # # => You searched for: <a href="search?q=rails">rails</a> def highlight(text, phrases, options = {}) text = sanitize(text) if options.fetch(:sanitize, true) if text.blank? || phrases.blank? text else - highlighter = options.fetch(:highlighter, '<mark>\1</mark>') - match = Array(phrases).map { |p| Regexp.escape(p) }.join('|') - text.gsub(/(#{match})(?![^<]*?>)/i, highlighter) + match = Array(phrases).map do |p| + Regexp === p ? p.to_s : Regexp.escape(p) + end.join('|') + + if block_given? + text.gsub(/(#{match})(?![^<]*?>)/i) { |found| yield found } + else + highlighter = options.fetch(:highlighter, '<mark>\1</mark>') + text.gsub(/(#{match})(?![^<]*?>)/i, highlighter) + end end.html_safe end @@ -155,9 +168,13 @@ module ActionView def excerpt(text, phrase, options = {}) return unless text && phrase - separator = options[:separator] || '' - phrase = Regexp.escape(phrase) - regex = /#{phrase}/i + separator = options.fetch(:separator, nil) || "" + case phrase + when Regexp + regex = phrase + else + regex = /#{Regexp.escape(phrase)}/i + end return unless matches = text.match(regex) phrase = matches[0] @@ -171,7 +188,7 @@ module ActionView end end - first_part, second_part = text.split(regex, 2) + first_part, second_part = text.split(phrase, 2) prefix, first_part = cut_excerpt_part(:first, first_part, separator, options) postfix, second_part = cut_excerpt_part(:second, second_part, separator, options) diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb index 0bc40874d9..17ec6a40bf 100644 --- a/actionview/lib/action_view/helpers/translation_helper.rb +++ b/actionview/lib/action_view/helpers/translation_helper.rb @@ -7,7 +7,7 @@ module ActionView module TranslationHelper # Delegates to <tt>I18n#translate</tt> but also performs three additional functions. # - # First, it will ensure that any thrown +MissingTranslation+ messages will be turned + # First, it will ensure that any thrown +MissingTranslation+ messages will be turned # into inline spans that: # # * have a "translation-missing" class set, @@ -34,6 +34,7 @@ module ActionView # naming convention helps to identify translations that include HTML tags so that # you know what kind of output to expect when you call translate in a template. def translate(key, options = {}) + options = options.dup options[:default] = wrap_translate_defaults(options[:default]) if options[:default] # If the user has specified rescue_format then pass it all through, otherwise use diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index 894616a449..c3be47133c 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -462,8 +462,6 @@ module ActionView # <strong>Email me:</strong> <span>me@domain.com</span> # </a> def mail_to(email_address, name = nil, html_options = {}, &block) - email_address = ERB::Util.html_escape(email_address) - html_options, name = name, nil if block_given? html_options = (html_options || {}).stringify_keys @@ -471,11 +469,11 @@ module ActionView option = html_options.delete(item) || next "#{item}=#{Rack::Utils.escape_path(option)}" }.compact - extras = extras.empty? ? '' : '?' + ERB::Util.html_escape(extras.join('&')) + extras = extras.empty? ? '' : '?' + extras.join('&') - html_options["href"] = "mailto:#{email_address}#{extras}".html_safe + html_options["href"] = "mailto:#{email_address}#{extras}" - content_tag(:a, name || email_address.html_safe, html_options, &block) + content_tag(:a, name || email_address, html_options, &block) end # True if the current request URI was generated by the given +options+. diff --git a/actionview/lib/action_view/log_subscriber.rb b/actionview/lib/action_view/log_subscriber.rb index 6c8d9cb5bf..9047dbdd85 100644 --- a/actionview/lib/action_view/log_subscriber.rb +++ b/actionview/lib/action_view/log_subscriber.rb @@ -13,11 +13,11 @@ module ActionView end def render_template(event) - return unless logger.info? - message = " Rendered #{from_rails_root(event.payload[:identifier])}" - message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout] - message << " (#{event.duration.round(1)}ms)" - info(message) + info do + message = " Rendered #{from_rails_root(event.payload[:identifier])}" + message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout] + message << " (#{event.duration.round(1)}ms)" + end end alias :render_partial :render_template alias :render_collection :render_template diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb index 855fed0190..ea687d9cca 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -66,10 +66,7 @@ module ActionView def self.get(details) if details[:formats] details = details.dup - syms = Set.new Mime::SET.symbols - details[:formats] = details[:formats].select { |v| - syms.include? v - } + details[:formats] &= Mime::SET.symbols end @details_keys[details] ||= new end @@ -114,7 +111,7 @@ module ActionView module ViewPaths attr_reader :view_paths, :html_fallback_for_js - # Whenever setting view paths, makes a copy so we can manipulate then in + # Whenever setting view paths, makes a copy so that we can manipulate them in # instance objects as we wish. def view_paths=(paths) @view_paths = ActionView::PathSet.new(Array(paths)) @@ -134,7 +131,8 @@ module ActionView end alias :template_exists? :exists? - # Add fallbacks to the view paths. Useful in cases you are rendering a :file. + # Adds fallbacks to the view paths. Useful in cases when you are rendering + # a :file. def with_fallbacks added_resolvers = 0 self.class.fallbacks.each do |resolver| @@ -227,7 +225,7 @@ module ActionView end # Overload locale= to also set the I18n.locale. If the current I18n.config object responds - # to original_config, it means that it's has a copy of the original I18n configuration and it's + # to original_config, it means that it has a copy of the original I18n configuration and it's # acting as proxy, which we need to skip. def locale=(value) if value @@ -238,7 +236,7 @@ module ActionView super(@skip_default_locale ? I18n.locale : default_locale) end - # A method which only uses the first format in the formats array for layout lookup. + # Uses the first format in the formats array for layout lookup. def with_layout_format if formats.size == 1 yield diff --git a/actionview/lib/action_view/renderer/abstract_renderer.rb b/actionview/lib/action_view/renderer/abstract_renderer.rb index 73c19a0ae2..1f122f9bc6 100644 --- a/actionview/lib/action_view/renderer/abstract_renderer.rb +++ b/actionview/lib/action_view/renderer/abstract_renderer.rb @@ -29,8 +29,9 @@ module ActionView def extract_details(options) @lookup_context.registered_details.each_with_object({}) do |key, details| - next unless value = options[key] - details[key] = Array(value) + value = options[key] + + details[key] = Array(value) if value end end @@ -41,6 +42,7 @@ module ActionView def prepend_formats(formats) formats = Array(formats) return if formats.empty? || @lookup_context.html_fallback_for_js + @lookup_context.formats = formats | @lookup_context.formats end end diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb index 36f17f01fd..0407632435 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -1,6 +1,33 @@ require 'thread_safe' module ActionView + class PartialIteration + # The number of iterations that will be done by the partial. + attr_reader :size + + # The current iteration of the partial. + attr_reader :index + + def initialize(size) + @size = size + @index = 0 + end + + # Check if this is the first iteration of the partial. + def first? + index == 0 + end + + # Check if this is the last iteration of the partial. + def last? + index == size - 1 + end + + def iterate! # :nodoc: + @index += 1 + end + end + # = Action View Partials # # There's also a convenience method for rendering sub templates within the current controller that depends on a @@ -56,8 +83,12 @@ module ActionView # <%= render partial: "ad", collection: @advertisements %> # # This will render "advertiser/_ad.html.erb" and pass the local variable +ad+ to the template for display. An - # iteration counter will automatically be made available to the template with a name of the form - # +partial_name_counter+. In the case of the example above, the template would be fed +ad_counter+. + # iteration object will automatically be made available to the template with a name of the form + # +partial_name_iteration+. The iteration object has knowledge about which index the current object has in + # the collection and the total size of the collection. The iteration object also has two convenience methods, + # +first?+ and +last?+. In the case of the example above, the template would be fed +ad_iteration+. + # For backwards compatibility the +partial_name_counter+ is still present and is mapped to the iteration's + # +index+ method. # # The <tt>:as</tt> option may be used when rendering partials. # @@ -281,6 +312,8 @@ module ActionView end end + private + def render_collection return nil if @collection.blank? @@ -322,25 +355,27 @@ module ActionView # respond to +to_partial_path+ in order to setup the path. def setup(context, options, block) @view = context - partial = options[:partial] - @options = options - @locals = options[:locals] || {} @block = block + + @locals = options[:locals] || {} @details = extract_details(options) prepend_formats(options[:formats]) + partial = options[:partial] + if String === partial @object = options[:object] + @collection = collection_from_options @path = partial - @collection = collection else @object = partial + @collection = collection_from_object || collection_from_options - if @collection = collection_from_object || collection + if @collection paths = @collection_data = @collection.map { |o| partial_path(o) } - @path = paths.uniq.size == 1 ? paths.first : nil + @path = paths.uniq.one? ? paths.first : nil else @path = partial_path end @@ -352,7 +387,7 @@ module ActionView end if @path - @variable, @variable_counter = retrieve_variable(@path, as) + @variable, @variable_counter, @variable_iteration = retrieve_variable(@path, as) @template_keys = retrieve_template_keys else paths.map! { |path| retrieve_variable(path, as).unshift(path) } @@ -361,7 +396,7 @@ module ActionView self end - def collection + def collection_from_options if @options.key?(:collection) collection = @options[:collection] collection.respond_to?(:to_ary) ? collection.to_ary : [] @@ -373,9 +408,7 @@ module ActionView end def find_partial - if path = @path - find_template(path, @template_keys) - end + find_template(@path, @template_keys) if @path end def find_template(path, locals) @@ -385,19 +418,22 @@ module ActionView def collection_with_template view, locals, template = @view, @locals, @template - as, counter = @variable, @variable_counter + as, counter, iteration = @variable, @variable_counter, @variable_iteration if layout = @options[:layout] layout = find_template(layout, @template_keys) end - index = -1 + partial_iteration = PartialIteration.new(@collection.size) + locals[iteration] = partial_iteration + @collection.map do |object| - locals[as] = object - locals[counter] = (index += 1) + locals[as] = object + locals[counter] = partial_iteration.index content = template.render(view, locals) content = layout.render(view, locals) { content } if layout + partial_iteration.iterate! content end end @@ -407,16 +443,20 @@ module ActionView cache = {} keys = @locals.keys - index = -1 + partial_iteration = PartialIteration.new(@collection.size) + @collection.map do |object| - index += 1 - path, as, counter = collection_data[index] + index = partial_iteration.index + path, as, counter, iteration = collection_data[index] - locals[as] = object - locals[counter] = index + locals[as] = object + locals[counter] = index + locals[iteration] = partial_iteration template = (cache[path] ||= find_template(path, keys + [as, counter])) - template.render(view, locals) + content = template.render(view, locals) + partial_iteration.iterate! + content end end @@ -466,8 +506,11 @@ module ActionView def retrieve_template_keys keys = @locals.keys - keys << @variable if @object || @collection - keys << @variable_counter if @collection + keys << @variable if @object || @collection + if @collection + keys << @variable_counter + keys << @variable_iteration + end keys end @@ -477,8 +520,11 @@ module ActionView raise_invalid_identifier(path) unless base =~ /\A_?([a-z]\w*)(\.\w+)*\z/ $1.to_sym end - variable_counter = :"#{variable}_counter" if @collection - [variable, variable_counter] + if @collection + variable_counter = :"#{variable}_counter" + variable_iteration = :"#{variable}_iteration" + end + [variable, variable_counter, variable_iteration] end IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " + diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb index be17097428..f3a48ecfa0 100644 --- a/actionview/lib/action_view/renderer/template_renderer.rb +++ b/actionview/lib/action_view/renderer/template_renderer.rb @@ -6,19 +6,18 @@ module ActionView @view = context @details = extract_details(options) template = determine_template(options) - context = @lookup_context prepend_formats(template.formats) - unless context.rendered_format - context.rendered_format = template.formats.first || formats.first - end + @lookup_context.rendered_format ||= (template.formats.first || formats.first) render_template(template, options[:layout], options[:locals]) end + private + # Determine the template to be rendered using the given options. - def determine_template(options) #:nodoc: + def determine_template(options) keys = options.fetch(:locals, {}).keys if options.key?(:body) diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb index 017302d40f..c92d090cce 100644 --- a/actionview/lib/action_view/rendering.rb +++ b/actionview/lib/action_view/rendering.rb @@ -62,8 +62,8 @@ module ActionView # # The view class must have the following methods: # View.new[lookup_context, assigns, controller] - # Create a new ActionView instance for a controller - # View#render[options] + # Create a new ActionView instance for a controller and we can also pass the arguments. + # View#render(option) # Returns String with the rendered template # # Override this method in a module to change the default behavior. diff --git a/actionview/lib/action_view/routing_url_for.rb b/actionview/lib/action_view/routing_url_for.rb index b9e4b590e7..881a123572 100644 --- a/actionview/lib/action_view/routing_url_for.rb +++ b/actionview/lib/action_view/routing_url_for.rb @@ -1,3 +1,5 @@ +require 'action_dispatch/routing/polymorphic_routes' + module ActionView module RoutingUrlFor @@ -83,10 +85,14 @@ module ActionView super({ :only_path => options[:host].nil? }.merge!(options.symbolize_keys)) when :back _back_url + when Symbol + ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_string_call self, options when Array polymorphic_path(options, options.extract_options!) + when Class + ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_class_call self, options else - polymorphic_path(options) + ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_model_call self, options end end diff --git a/actionview/lib/action_view/tasks/dependencies.rake b/actionview/lib/action_view/tasks/dependencies.rake index 1b9426c0e5..b39f7d583b 100644 --- a/actionview/lib/action_view/tasks/dependencies.rake +++ b/actionview/lib/action_view/tasks/dependencies.rake @@ -2,16 +2,20 @@ namespace :cache_digests do desc 'Lookup nested dependencies for TEMPLATE (like messages/show or comments/_comment.html)' task :nested_dependencies => :environment do abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present? - template, format = ENV['TEMPLATE'].split(".") - format ||= :html - puts JSON.pretty_generate ActionView::Digestor.new(template, format, ApplicationController.new.lookup_context).nested_dependencies + puts JSON.pretty_generate ActionView::Digestor.new(name: template_name, finder: finder).nested_dependencies end desc 'Lookup first-level dependencies for TEMPLATE (like messages/show or comments/_comment.html)' task :dependencies => :environment do abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present? - template, format = ENV['TEMPLATE'].split(".") - format ||= :html - puts JSON.pretty_generate ActionView::Digestor.new(template, format, ApplicationController.new.lookup_context).dependencies + puts JSON.pretty_generate ActionView::Digestor.new(name: template_name, finder: finder).dependencies + end + + def template_name + ENV['TEMPLATE'].split('.', 2).first + end + + def finder + ApplicationController.new.lookup_context end end diff --git a/actionview/lib/action_view/template/handlers.rb b/actionview/lib/action_view/template/handlers.rb index d9cddc0040..33bfcb458c 100644 --- a/actionview/lib/action_view/template/handlers.rb +++ b/actionview/lib/action_view/template/handlers.rb @@ -32,6 +32,15 @@ module ActionView #:nodoc: @@template_extensions = nil end + # Opposite to register_template_handler. + def unregister_template_handler(*extensions) + extensions.each do |extension| + handler = @@template_handlers.delete extension.to_sym + @@default_template_handlers = nil if @@default_template_handlers == handler + end + @@template_extensions = nil + end + def template_handler_extensions @@template_handlers.keys.map {|key| key.to_s }.sort end diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb index 05f0c301e7..d29d020c17 100644 --- a/actionview/lib/action_view/template/resolver.rb +++ b/actionview/lib/action_view/template/resolver.rb @@ -181,13 +181,7 @@ module ActionView def query(path, details, formats) query = build_query(path, details) - # deals with case-insensitive file systems. - sanitizer = Hash.new { |h,dir| h[dir] = Dir["#{dir}/*"] } - - template_paths = Dir[query].reject { |filename| - File.directory?(filename) || - !sanitizer[File.dirname(filename)].include?(filename) - } + template_paths = find_template_paths query template_paths.map { |template| handler, format, variant = extract_handler_and_format_and_variant(template, formats) @@ -202,6 +196,26 @@ module ActionView } end + if RUBY_VERSION >= '2.2.0' + def find_template_paths(query) + Dir[query].reject { |filename| + File.directory?(filename) || + # deals with case-insensitive file systems. + !File.fnmatch(query, filename, File::FNM_EXTGLOB) + } + end + else + def find_template_paths(query) + # deals with case-insensitive file systems. + sanitizer = Hash.new { |h,dir| h[dir] = Dir["#{dir}/*"] } + + Dir[query].reject { |filename| + File.directory?(filename) || + !sanitizer[File.dirname(filename)].include?(filename) + } + end + end + # Helper for building query glob string based on resolver's pattern. def build_query(path, details) query = @pattern.dup diff --git a/actionview/lib/action_view/view_paths.rb b/actionview/lib/action_view/view_paths.rb index 6c349feb1d..80a41f2418 100644 --- a/actionview/lib/action_view/view_paths.rb +++ b/actionview/lib/action_view/view_paths.rb @@ -14,27 +14,38 @@ module ActionView :locale, :locale=, :to => :lookup_context module ClassMethods - def parent_prefixes - @parent_prefixes ||= begin - parent_controller = superclass - prefixes = [] - - until parent_controller.abstract? - prefixes << parent_controller.controller_path - parent_controller = parent_controller.superclass - end + def _prefixes # :nodoc: + @_prefixes ||= begin + deprecated_prefixes = handle_deprecated_parent_prefixes + if deprecated_prefixes + deprecated_prefixes + else + return local_prefixes if superclass.abstract? - prefixes + local_prefixes + superclass._prefixes + end end end + + private + + # Override this method in your controller if you want to change paths prefixes for finding views. + # Prefixes defined here will still be added to parents' <tt>._prefixes</tt>. + def local_prefixes + [controller_path] + end + + def handle_deprecated_parent_prefixes # TODO: remove in 4.3/5.0. + return unless respond_to?(:parent_prefixes) + + ActiveSupport::Deprecation.warn "Overriding ActionController::Base::parent_prefixes is deprecated, override .local_prefixes instead." + local_prefixes + parent_prefixes + end end # The prefixes used in render "foo" shortcuts. - def _prefixes - @_prefixes ||= begin - parent_prefixes = self.class.parent_prefixes - parent_prefixes.dup.unshift(controller_path) - end + def _prefixes # :nodoc: + self.class._prefixes end # LookupContext is the object responsible to hold all information required to lookup diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb index 9eae3a4fbd..d60712255b 100644 --- a/actionview/test/abstract_unit.rb +++ b/actionview/test/abstract_unit.rb @@ -274,7 +274,6 @@ ActionView::RoutingUrlFor.send(:include, ActionDispatch::Routing::UrlFor) module ActionController class Base - include ActionController::Testing # This stub emulates the Railtie including the URL helpers from a Rails application include SharedTestRoutes.url_helpers include SharedTestRoutes.mounted_helpers @@ -339,3 +338,5 @@ end def jruby_skip(message = '') skip message if defined?(JRUBY_VERSION) end + +require 'mocha/setup' # FIXME: stop using mocha diff --git a/actionview/test/actionpack/abstract/abstract_controller_test.rb b/actionview/test/actionpack/abstract/abstract_controller_test.rb index 40d3b17131..e653b12d32 100644 --- a/actionview/test/actionpack/abstract/abstract_controller_test.rb +++ b/actionview/test/actionpack/abstract/abstract_controller_test.rb @@ -150,6 +150,54 @@ module AbstractController end end + class OverridingLocalPrefixes < AbstractController::Base + include AbstractController::Rendering + include ActionView::Rendering + append_view_path File.expand_path(File.join(File.dirname(__FILE__), "views")) + + def index + render + end + + def self.local_prefixes + # this would usually return "abstract_controller/testing/overriding_local_prefixes" + super + ["abstract_controller/testing/me3"] + end + + class Inheriting < self + end + end + + class OverridingLocalPrefixesTest < ActiveSupport::TestCase # TODO: remove me in 5.0/4.3. + test "overriding .local_prefixes adds prefix" do + @controller = OverridingLocalPrefixes.new + @controller.process(:index) + assert_equal "Hello from me3/index.erb", @controller.response_body + end + + test ".local_prefixes is inherited" do + @controller = OverridingLocalPrefixes::Inheriting.new + @controller.process(:index) + assert_equal "Hello from me3/index.erb", @controller.response_body + end + end + + class DeprecatedParentPrefixes < OverridingLocalPrefixes + def self.parent_prefixes + ["abstract_controller/testing/me3"] + end + end + + class DeprecatedParentPrefixesTest < ActiveSupport::TestCase # TODO: remove me in 5.0/4.3. + test "overriding .parent_prefixes is deprecated" do + @controller = DeprecatedParentPrefixes.new + assert_deprecated do + @controller.process(:index) + end + assert_equal "Hello from me3/index.erb", @controller.response_body + end + end + # Test rendering with layouts # ==== # self._layout is used when defined diff --git a/actionview/test/actionpack/abstract/render_test.rb b/actionview/test/actionpack/abstract/render_test.rb index f9d8c916d9..d09f91c1e2 100644 --- a/actionview/test/actionpack/abstract/render_test.rb +++ b/actionview/test/actionpack/abstract/render_test.rb @@ -60,42 +60,42 @@ module AbstractController end def test_render_template - @controller.process(:template) + assert_equal "With Template", @controller.process(:template) assert_equal "With Template", @controller.response_body end def test_render_file - @controller.process(:file) + assert_equal "With File", @controller.process(:file) assert_equal "With File", @controller.response_body end def test_render_inline - @controller.process(:inline) + assert_equal "With Inline", @controller.process(:inline) assert_equal "With Inline", @controller.response_body end def test_render_text - @controller.process(:text) + assert_equal "With Text", @controller.process(:text) assert_equal "With Text", @controller.response_body end def test_render_default - @controller.process(:default) + assert_equal "With Default", @controller.process(:default) assert_equal "With Default", @controller.response_body end def test_render_string - @controller.process(:string) + assert_equal "With String", @controller.process(:string) assert_equal "With String", @controller.response_body end def test_render_symbol - @controller.process(:symbol) + assert_equal "With Symbol", @controller.process(:symbol) assert_equal "With Symbol", @controller.response_body end def test_render_string_with_path - @controller.process(:string_with_path) + assert_equal "With String With Path", @controller.process(:string_with_path) assert_equal "With String With Path", @controller.response_body end end diff --git a/actionview/test/actionpack/controller/layout_test.rb b/actionview/test/actionpack/controller/layout_test.rb index b44f57a23e..bd345fe873 100644 --- a/actionview/test/actionpack/controller/layout_test.rb +++ b/actionview/test/actionpack/controller/layout_test.rb @@ -6,9 +6,6 @@ require 'active_support/core_ext/array/extract_options' # method has access to the view_paths array when looking for a layout to automatically assign. old_load_paths = ActionController::Base.view_paths -ActionView::Template::register_template_handler :mab, - lambda { |template| template.source.inspect } - ActionController::Base.view_paths = [ File.dirname(__FILE__) + '/../../fixtures/actionpack/layout_tests/' ] class LayoutTest < ActionController::Base @@ -17,6 +14,15 @@ class LayoutTest < ActionController::Base self.view_paths = ActionController::Base.view_paths.dup end +module TemplateHandlerHelper + def with_template_handler(*extensions, handler) + ActionView::Template.register_template_handler(*extensions, handler) + yield + ensure + ActionView::Template.unregister_template_handler(*extensions) + end +end + # Restore view_paths to previous value ActionController::Base.view_paths = old_load_paths @@ -39,6 +45,8 @@ class MultipleExtensions < LayoutTest end class LayoutAutoDiscoveryTest < ActionController::TestCase + include TemplateHandlerHelper + def setup super @request.host = "www.nextangle.com" @@ -57,10 +65,12 @@ class LayoutAutoDiscoveryTest < ActionController::TestCase end def test_third_party_template_library_auto_discovers_layout - @controller = ThirdPartyTemplateLibraryController.new - get :hello - assert_response :success - assert_equal 'layouts/third_party_template_library.mab', @response.body + with_template_handler :mab, lambda { |template| template.source.inspect } do + @controller = ThirdPartyTemplateLibraryController.new + get :hello + assert_response :success + assert_equal 'layouts/third_party_template_library.mab', @response.body + end end def test_namespaced_controllers_auto_detect_layouts1 @@ -135,6 +145,7 @@ end class LayoutSetInResponseTest < ActionController::TestCase include ActionView::Template::Handlers + include TemplateHandlerHelper def test_layout_set_when_using_default_layout @controller = DefaultLayoutController.new @@ -191,9 +202,11 @@ class LayoutSetInResponseTest < ActionController::TestCase end def test_layout_set_when_using_render - @controller = SetsLayoutInRenderController.new - get :hello - assert_template :layout => "layouts/third_party_template_library" + with_template_handler :mab, lambda { |template| template.source.inspect } do + @controller = SetsLayoutInRenderController.new + get :hello + assert_template :layout => "layouts/third_party_template_library" + end end def test_layout_is_not_set_when_none_rendered diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb index 45b8049b83..b3b51ae583 100644 --- a/actionview/test/actionpack/controller/render_test.rb +++ b/actionview/test/actionpack/controller/render_test.rb @@ -536,6 +536,14 @@ class TestController < ApplicationController render :partial => "customer_with_var", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => :customer end + def partial_collection_with_iteration + render partial: "customer_iteration", collection: [ Customer.new("david"), Customer.new("mary"), Customer.new('christine') ] + end + + def partial_collection_with_as_and_iteration + render partial: "customer_iteration_with_as", collection: [ Customer.new("david"), Customer.new("mary"), Customer.new('christine') ], as: :client + end + def partial_collection_with_counter render :partial => "customer_counter", :collection => [ Customer.new("david"), Customer.new("mary") ] end @@ -720,6 +728,11 @@ class RenderTest < ActionController::TestCase assert_equal "Elastica", @response.body end + def test_render_process + get :render_action_hello_world_as_string + assert_equal ["Hello world!"], @controller.process(:render_action_hello_world_as_string) + end + # :ported: def test_render_from_variable get :render_hello_world_from_variable @@ -834,7 +847,7 @@ class RenderTest < ActionController::TestCase def test_render_text_with_nil get :render_text_with_nil assert_response 200 - assert_equal ' ', @response.body + assert_equal '', @response.body end # :ported: @@ -1022,7 +1035,7 @@ class RenderTest < ActionController::TestCase def test_rendering_nothing_on_layout get :rendering_nothing_on_layout - assert_equal " ", @response.body + assert_equal '', @response.body end def test_render_to_string_doesnt_break_assigns @@ -1232,6 +1245,16 @@ class RenderTest < ActionController::TestCase assert_equal "david david davidmary mary mary", @response.body end + def test_partial_collection_with_iteration + get :partial_collection_with_iteration + assert_equal "3-0: david-first3-1: mary3-2: christine-last", @response.body + end + + def test_partial_collection_with_as_and_iteration + get :partial_collection_with_as_and_iteration + assert_equal "3-0: david-first3-1: mary3-2: christine-last", @response.body + end + def test_partial_collection_with_counter get :partial_collection_with_counter assert_equal "david0mary1", @response.body @@ -1332,4 +1355,3 @@ class RenderTest < ActionController::TestCase assert_equal "Before (Anthony)\nInside from partial (Anthony)\nAfter\nBefore (David)\nInside from partial (David)\nAfter\nBefore (Ramm)\nInside from partial (Ramm)\nAfter", @response.body end end - diff --git a/actionview/test/active_record_unit.rb b/actionview/test/active_record_unit.rb index 95fbb112c0..cca55c9af4 100644 --- a/actionview/test/active_record_unit.rb +++ b/actionview/test/active_record_unit.rb @@ -57,7 +57,7 @@ class ActiveRecordTestConnector end end - # Load actionpack sqlite tables + # Load actionpack sqlite3 tables def load_schema File.read(File.dirname(__FILE__) + "/fixtures/db_definitions/sqlite.sql").split(';').each do |sql| ActiveRecord::Base.connection.execute(sql) unless sql.blank? diff --git a/actionview/test/activerecord/polymorphic_routes_test.rb b/actionview/test/activerecord/polymorphic_routes_test.rb index afb714484b..fef27ef492 100644 --- a/actionview/test/activerecord/polymorphic_routes_test.rb +++ b/actionview/test/activerecord/polymorphic_routes_test.rb @@ -74,25 +74,77 @@ class PolymorphicRoutesTest < ActionController::TestCase @blog_blog = Blog::Blog.new end + def assert_url(url, args) + host = self.class.default_url_options[:host] + + assert_equal url.sub(/http:\/\/#{host}/, ''), polymorphic_path(args) + assert_equal url, polymorphic_url(args) + assert_equal url, url_for(args) + end + + def test_string + with_test_routes do + # FIXME: why are these different? Symbol case passes through to + # `polymorphic_url`, but the String case doesn't. + assert_equal "http://example.com/projects", polymorphic_url("projects") + assert_equal "projects", url_for("projects") + end + end + + def test_string_with_options + with_test_routes do + assert_equal "http://example.com/projects?id=10", polymorphic_url("projects", :id => 10) + end + end + + def test_symbol + with_test_routes do + assert_url "http://example.com/projects", :projects + end + end + + def test_symbol_with_options + with_test_routes do + assert_equal "http://example.com/projects?id=10", polymorphic_url(:projects, :id => 10) + end + end + def test_passing_routes_proxy with_namespaced_routes(:blog) do proxy = ActionDispatch::Routing::RoutesProxy.new(_routes, self) @blog_post.save - assert_equal "http://example.com/posts/#{@blog_post.id}", polymorphic_url([proxy, @blog_post]) + assert_url "http://example.com/posts/#{@blog_post.id}", [proxy, @blog_post] end end def test_namespaced_model with_namespaced_routes(:blog) do @blog_post.save - assert_equal "http://example.com/posts/#{@blog_post.id}", polymorphic_url(@blog_post) + assert_url "http://example.com/posts/#{@blog_post.id}", @blog_post end end def test_namespaced_model_with_name_the_same_as_namespace with_namespaced_routes(:blog) do @blog_blog.save - assert_equal "http://example.com/blogs/#{@blog_blog.id}", polymorphic_url(@blog_blog) + assert_url "http://example.com/blogs/#{@blog_blog.id}", @blog_blog + end + end + + def test_polymorphic_url_with_2_objects + with_namespaced_routes(:blog) do + @blog_blog.save + @blog_post.save + assert_equal "http://example.com/blogs/#{@blog_blog.id}/posts/#{@blog_post.id}", polymorphic_url([@blog_blog, @blog_post]) + end + end + + def test_polymorphic_url_with_3_objects + with_namespaced_routes(:blog) do + @blog_blog.save + @blog_post.save + @fax.save + assert_equal "http://example.com/blogs/#{@blog_blog.id}/posts/#{@blog_post.id}/faxes/#{@fax.id}", polymorphic_url([@blog_blog, @blog_post, @fax]) end end @@ -100,7 +152,7 @@ class PolymorphicRoutesTest < ActionController::TestCase with_namespaced_routes(:blog) do @blog_post.save @blog_blog.save - assert_equal "http://example.com/blogs/#{@blog_blog.id}/posts/#{@blog_post.id}", polymorphic_url([@blog_blog, @blog_post]) + assert_url "http://example.com/blogs/#{@blog_blog.id}/posts/#{@blog_post.id}", [@blog_blog, @blog_post] end end @@ -112,29 +164,88 @@ class PolymorphicRoutesTest < ActionController::TestCase end end + def test_with_empty_list + with_test_routes do + assert_raise ArgumentError, "Nil location provided. Can't build URI." do + polymorphic_url([]) + end + end + end + + def test_with_nil_id + with_test_routes do + assert_raise ArgumentError, "Nil location provided. Can't build URI." do + polymorphic_url({ :id => nil }) + end + end + end + + def test_with_nil_in_list + with_test_routes do + assert_raise ArgumentError, "Nil location provided. Can't build URI." do + @series.save + polymorphic_url([nil, @series]) + end + end + end + def test_with_record with_test_routes do @project.save - assert_equal "http://example.com/projects/#{@project.id}", polymorphic_url(@project) + assert_url "http://example.com/projects/#{@project.id}", @project end end def test_with_class with_test_routes do - assert_equal "http://example.com/projects", polymorphic_url(@project.class) + assert_url "http://example.com/projects", @project.class + end + end + + def test_with_class_list_of_one + with_test_routes do + assert_url "http://example.com/projects", [@project.class] + end + end + + def test_class_with_options + with_test_routes do + assert_equal "http://example.com/projects?foo=bar", polymorphic_url(@project.class, { :foo => :bar }) + assert_equal "/projects?foo=bar", polymorphic_path(@project.class, { :foo => :bar }) end end def test_with_new_record with_test_routes do - assert_equal "http://example.com/projects", polymorphic_url(@project) + assert_url "http://example.com/projects", @project + end + end + + def test_new_record_arguments + params = nil + + with_test_routes do + extend Module.new { + define_method("projects_url") { |*args| + params = args + super(*args) + } + + define_method("projects_path") { |*args| + params = args + super(*args) + } + } + + assert_url "http://example.com/projects", @project + assert_equal [], params end end def test_with_destroyed_record with_test_routes do @project.destroy - assert_equal "http://example.com/projects", polymorphic_url(@project) + assert_url "http://example.com/projects", @project end end @@ -196,14 +307,14 @@ class PolymorphicRoutesTest < ActionController::TestCase with_test_routes do @project.save @task.save - assert_equal "http://example.com/projects/#{@project.id}/tasks/#{@task.id}", polymorphic_url([@project, @task]) + assert_url "http://example.com/projects/#{@project.id}/tasks/#{@task.id}", [@project, @task] end end def test_with_nested_unsaved with_test_routes do @project.save - assert_equal "http://example.com/projects/#{@project.id}/tasks", polymorphic_url([@project, @task]) + assert_url "http://example.com/projects/#{@project.id}/tasks", [@project, @task] end end @@ -211,20 +322,20 @@ class PolymorphicRoutesTest < ActionController::TestCase with_test_routes do @project.save @task.destroy - assert_equal "http://example.com/projects/#{@project.id}/tasks", polymorphic_url([@project, @task]) + assert_url "http://example.com/projects/#{@project.id}/tasks", [@project, @task] end end def test_with_nested_class with_test_routes do @project.save - assert_equal "http://example.com/projects/#{@project.id}/tasks", polymorphic_url([@project, @task.class]) + assert_url "http://example.com/projects/#{@project.id}/tasks", [@project, @task.class] end end def test_class_with_array_and_namespace with_admin_test_routes do - assert_equal "http://example.com/admin/projects", polymorphic_url([:admin, @project.class]) + assert_url "http://example.com/admin/projects", [:admin, @project.class] end end @@ -236,14 +347,14 @@ class PolymorphicRoutesTest < ActionController::TestCase def test_unsaved_with_array_and_namespace with_admin_test_routes do - assert_equal "http://example.com/admin/projects", polymorphic_url([:admin, @project]) + assert_url "http://example.com/admin/projects", [:admin, @project] end end def test_nested_unsaved_with_array_and_namespace with_admin_test_routes do @project.save - assert_equal "http://example.com/admin/projects/#{@project.id}/tasks", polymorphic_url([:admin, @project, @task]) + assert_url "http://example.com/admin/projects/#{@project.id}/tasks", [:admin, @project, @task] end end @@ -251,7 +362,7 @@ class PolymorphicRoutesTest < ActionController::TestCase with_admin_test_routes do @project.save @task.save - assert_equal "http://example.com/admin/projects/#{@project.id}/tasks/#{@task.id}", polymorphic_url([:admin, @project, @task]) + assert_url "http://example.com/admin/projects/#{@project.id}/tasks/#{@task.id}", [:admin, @project, @task] end end @@ -260,14 +371,14 @@ class PolymorphicRoutesTest < ActionController::TestCase @project.save @task.save @step.save - assert_equal "http://example.com/admin/projects/#{@project.id}/site/tasks/#{@task.id}/steps/#{@step.id}", polymorphic_url([:admin, @project, :site, @task, @step]) + assert_url "http://example.com/admin/projects/#{@project.id}/site/tasks/#{@task.id}/steps/#{@step.id}", [:admin, @project, :site, @task, @step] end end def test_nesting_with_array_ending_in_singleton_resource with_test_routes do @project.save - assert_equal "http://example.com/projects/#{@project.id}/bid", polymorphic_url([@project, :bid]) + assert_url "http://example.com/projects/#{@project.id}/bid", [@project, :bid] end end @@ -275,7 +386,7 @@ class PolymorphicRoutesTest < ActionController::TestCase with_test_routes do @project.save @task.save - assert_equal "http://example.com/projects/#{@project.id}/bid/tasks/#{@task.id}", polymorphic_url([@project, :bid, @task]) + assert_url "http://example.com/projects/#{@project.id}/bid/tasks/#{@task.id}", [@project, :bid, @task] end end @@ -291,34 +402,40 @@ class PolymorphicRoutesTest < ActionController::TestCase with_admin_test_routes do @project.save @task.save - assert_equal "http://example.com/admin/projects/#{@project.id}/bid/tasks/#{@task.id}", polymorphic_url([:admin, @project, :bid, @task]) + assert_url "http://example.com/admin/projects/#{@project.id}/bid/tasks/#{@task.id}", [:admin, @project, :bid, @task] end end - def test_nesting_with_array_containing_nil + def test_nesting_with_array with_test_routes do @project.save - assert_equal "http://example.com/projects/#{@project.id}/bid", polymorphic_url([@project, nil, :bid]) + assert_url "http://example.com/projects/#{@project.id}/bid", [@project, :bid] end end def test_with_array_containing_single_object with_test_routes do @project.save - assert_equal "http://example.com/projects/#{@project.id}", polymorphic_url([nil, @project]) + assert_url "http://example.com/projects/#{@project.id}", [@project] end end def test_with_array_containing_single_name with_test_routes do @project.save - assert_equal "http://example.com/projects", polymorphic_url([:projects]) + assert_url "http://example.com/projects", [:projects] + end + end + + def test_with_array_containing_single_string_name + with_test_routes do + assert_url "http://example.com/projects", ["projects"] end end def test_with_array_containing_symbols with_test_routes do - assert_equal "http://example.com/series/new", polymorphic_url([:new, :series]) + assert_url "http://example.com/series/new", [:new, :series] end end @@ -353,26 +470,26 @@ class PolymorphicRoutesTest < ActionController::TestCase def test_with_irregular_plural_record with_test_routes do @tax.save - assert_equal "http://example.com/taxes/#{@tax.id}", polymorphic_url(@tax) + assert_url "http://example.com/taxes/#{@tax.id}", @tax end end def test_with_irregular_plural_class with_test_routes do - assert_equal "http://example.com/taxes", polymorphic_url(@tax.class) + assert_url "http://example.com/taxes", @tax.class end end def test_with_irregular_plural_new_record with_test_routes do - assert_equal "http://example.com/taxes", polymorphic_url(@tax) + assert_url "http://example.com/taxes", @tax end end def test_with_irregular_plural_destroyed_record with_test_routes do @tax.destroy - assert_equal "http://example.com/taxes", polymorphic_url(@tax) + assert_url "http://example.com/taxes", @tax end end @@ -406,7 +523,7 @@ class PolymorphicRoutesTest < ActionController::TestCase def test_with_nested_unsaved_irregular_plurals with_test_routes do @tax.save - assert_equal "http://example.com/taxes/#{@tax.id}/faxes", polymorphic_url([@tax, @fax]) + assert_url "http://example.com/taxes/#{@tax.id}/faxes", [@tax, @fax] end end @@ -418,34 +535,34 @@ class PolymorphicRoutesTest < ActionController::TestCase def test_class_with_irregular_plural_array_and_namespace with_admin_test_routes do - assert_equal "http://example.com/admin/taxes", polymorphic_url([:admin, @tax.class]) + assert_url "http://example.com/admin/taxes", [:admin, @tax.class] end end def test_unsaved_with_irregular_plural_array_and_namespace with_admin_test_routes do - assert_equal "http://example.com/admin/taxes", polymorphic_url([:admin, @tax]) + assert_url "http://example.com/admin/taxes", [:admin, @tax] end end def test_nesting_with_irregular_plurals_and_array_ending_in_singleton_resource with_test_routes do @tax.save - assert_equal "http://example.com/taxes/#{@tax.id}/bid", polymorphic_url([@tax, :bid]) + assert_url "http://example.com/taxes/#{@tax.id}/bid", [@tax, :bid] end end def test_with_array_containing_single_irregular_plural_object with_test_routes do @tax.save - assert_equal "http://example.com/taxes/#{@tax.id}", polymorphic_url([nil, @tax]) + assert_url "http://example.com/taxes/#{@tax.id}", [@tax] end end def test_with_array_containing_single_name_irregular_plural with_test_routes do @tax.save - assert_equal "http://example.com/taxes", polymorphic_url([:taxes]) + assert_url "http://example.com/taxes", [:taxes] end end @@ -453,15 +570,15 @@ class PolymorphicRoutesTest < ActionController::TestCase def test_uncountable_resource with_test_routes do @series.save - assert_equal "http://example.com/series/#{@series.id}", polymorphic_url(@series) - assert_equal "http://example.com/series", polymorphic_url(Series.new) + assert_url "http://example.com/series/#{@series.id}", @series + assert_url "http://example.com/series", Series.new end end def test_routing_a_to_model_delegate with_test_routes do @delegator.save - assert_equal "http://example.com/model_delegates/overridden", polymorphic_url(@delegator) + assert_url "http://example.com/model_delegates/overridden", @delegator end end @@ -470,13 +587,15 @@ class PolymorphicRoutesTest < ActionController::TestCase set.draw do scope(:module => name) do resources :blogs do - resources :posts + resources :posts do + resources :faxes + end end resources :posts end end - self.class.send(:include, @routes.url_helpers) + extend @routes.url_helpers yield end end @@ -498,7 +617,7 @@ class PolymorphicRoutesTest < ActionController::TestCase resources :model_delegates end - self.class.send(:include, @routes.url_helpers) + extend @routes.url_helpers yield end end @@ -520,7 +639,7 @@ class PolymorphicRoutesTest < ActionController::TestCase end end - self.class.send(:include, @routes.url_helpers) + extend @routes.url_helpers yield end end @@ -539,8 +658,21 @@ class PolymorphicRoutesTest < ActionController::TestCase end end - self.class.send(:include, @routes.url_helpers) + extend @routes.url_helpers yield end end end + +class PolymorphicPathRoutesTest < PolymorphicRoutesTest + include ActionView::RoutingUrlFor + include ActionView::Context + + attr_accessor :controller + + def assert_url(url, args) + host = self.class.default_url_options[:host] + + assert_equal url.sub(/http:\/\/#{host}/, ''), url_for(args) + end +end diff --git a/actionview/test/fixtures/actionpack/test/_customer_iteration.erb b/actionview/test/fixtures/actionpack/test/_customer_iteration.erb new file mode 100644 index 0000000000..fb530b04a7 --- /dev/null +++ b/actionview/test/fixtures/actionpack/test/_customer_iteration.erb @@ -0,0 +1 @@ +<%= customer_iteration_iteration.size %>-<%= customer_iteration_iteration.index %>: <%= customer_iteration.name %><%= '-first' if customer_iteration_iteration.first? %><%= '-last' if customer_iteration_iteration.last? %>
\ No newline at end of file diff --git a/actionview/test/fixtures/actionpack/test/_customer_iteration_with_as.erb b/actionview/test/fixtures/actionpack/test/_customer_iteration_with_as.erb new file mode 100644 index 0000000000..57297d0564 --- /dev/null +++ b/actionview/test/fixtures/actionpack/test/_customer_iteration_with_as.erb @@ -0,0 +1 @@ +<%= client_iteration.size %>-<%= client_iteration.index %>: <%= client.name %><%= '-first' if client_iteration.first? %><%= '-last' if client_iteration.last? %>
\ No newline at end of file diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb index 651978ed80..d789a5ca27 100644 --- a/actionview/test/template/asset_tag_helper_test.rb +++ b/actionview/test/template/asset_tag_helper_test.rb @@ -309,6 +309,14 @@ class AssetTagHelperTest < ActionView::TestCase AssetPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end + def test_asset_path_tag_to_not_create_duplicate_slashes + @controller.config.asset_host = "host/" + assert_dom_equal('http://host/foo', asset_path("foo")) + + @controller.config.relative_url_root = '/some/root/' + assert_dom_equal('http://host/some/root/foo', asset_path("foo")) + end + def test_compute_asset_public_path assert_equal "/robots.txt", compute_asset_path("robots.txt") assert_equal "/robots.txt", compute_asset_path("/robots.txt") @@ -538,6 +546,14 @@ class AssetTagHelperTest < ActionView::TestCase assert_equal "http://cdn.example.com/images/file.png", image_path("file.png") end + def test_image_url_with_asset_host_proc_returning_nil + @controller.config.asset_host = Proc.new { nil } + @controller.request = Struct.new(:base_url, :script_name).new("http://www.example.com", nil) + + assert_equal "/images/rails.png", image_path("rails.png") + assert_equal "http://www.example.com/images/rails.png", image_url("rails.png") + end + def test_caching_image_path_with_caching_and_proc_asset_host_using_request @controller.config.asset_host = Proc.new do |source, request| if request.ssl? @@ -588,6 +604,10 @@ class AssetTagHelperNonVhostTest < ActionView::TestCase assert_equal "gopher://www.example.com", compute_asset_host("foo", :protocol => :request) end + def test_should_return_custom_host_if_passed_in_options + assert_equal "http://custom.example.com", compute_asset_host("foo", :host => "http://custom.example.com") + end + def test_should_ignore_relative_root_path_on_complete_url assert_dom_equal(%(http://www.example.com/images/xml.png), image_path("http://www.example.com/images/xml.png")) end @@ -751,4 +771,15 @@ class AssetUrlHelperEmptyModuleTest < ActionView::TestCase assert @module.config.asset_host assert_equal "http://www.example.com/foo", @module.asset_url("foo") end + + def test_asset_url_with_custom_asset_host + @module.instance_eval do + def config + Struct.new(:asset_host).new("http://www.example.com") + end + end + + assert @module.config.asset_host + assert_equal "http://custom.example.com/foo", @module.asset_url("foo", :host => "http://custom.example.com") + end end diff --git a/actionview/test/template/capture_helper_test.rb b/actionview/test/template/capture_helper_test.rb index 938f1c3e54..f213da5934 100644 --- a/actionview/test/template/capture_helper_test.rb +++ b/actionview/test/template/capture_helper_test.rb @@ -207,29 +207,6 @@ class CaptureHelperTest < ActionView::TestCase assert_equal "", @av.with_output_buffer {} end - def test_flush_output_buffer_concats_output_buffer_to_response - view = view_with_controller - assert_equal [], view.response.body_parts - - view.output_buffer << 'OMG' - view.flush_output_buffer - assert_equal ['OMG'], view.response.body_parts - assert_equal '', view.output_buffer - - view.output_buffer << 'foobar' - view.flush_output_buffer - assert_equal ['OMG', 'foobar'], view.response.body_parts - assert_equal '', view.output_buffer - end - - def test_flush_output_buffer_preserves_the_encoding_of_the_output_buffer - view = view_with_controller - alt_encoding = alt_encoding(view.output_buffer) - view.output_buffer.force_encoding(alt_encoding) - flush_output_buffer - assert_equal alt_encoding, view.output_buffer.encoding - end - def alt_encoding(output_buffer) output_buffer.encoding == Encoding::US_ASCII ? Encoding::UTF_8 : Encoding::US_ASCII end diff --git a/actionview/test/template/compiled_templates_test.rb b/actionview/test/template/compiled_templates_test.rb index 2336321f3e..b84aca6746 100644 --- a/actionview/test/template/compiled_templates_test.rb +++ b/actionview/test/template/compiled_templates_test.rb @@ -1,15 +1,8 @@ require 'abstract_unit' class CompiledTemplatesTest < ActiveSupport::TestCase - def setup - # Clean up any details key cached to expose failures - # that otherwise would appear just on isolated tests + teardown do ActionView::LookupContext::DetailsKey.clear - - @compiled_templates = ActionView::CompiledTemplates - @compiled_templates.instance_methods.each do |m| - @compiled_templates.send(:remove_method, m) if m =~ /^_render_template_/ - end end def test_template_gets_recompiled_when_using_different_keys_in_local_assigns diff --git a/actionview/test/template/debug_helper_test.rb b/actionview/test/template/debug_helper_test.rb index 42d06bd9ff..5609694cd5 100644 --- a/actionview/test/template/debug_helper_test.rb +++ b/actionview/test/template/debug_helper_test.rb @@ -3,6 +3,6 @@ require 'active_record_unit' class DebugHelperTest < ActionView::TestCase def test_debug company = Company.new(name: "firebase") - assert_match " name: firebase", debug(company) + assert_match "name: firebase", debug(company) end end diff --git a/actionview/test/template/dependency_tracker_test.rb b/actionview/test/template/dependency_tracker_test.rb index df3a0602d1..bb375076c6 100644 --- a/actionview/test/template/dependency_tracker_test.rb +++ b/actionview/test/template/dependency_tracker_test.rb @@ -31,6 +31,7 @@ class DependencyTrackerTest < ActionView::TestCase end def teardown + ActionView::Template.unregister_template_handler :neckbeard tracker.remove_tracker(:neckbeard) end @@ -59,6 +60,21 @@ class ERBTrackerTest < Minitest::Test assert_equal ["messages/message123"], tracker.dependencies end + def test_dependency_of_template_partial_with_layout + skip # FIXME: Needs to be fixed properly, right now we can only match one dependency per line. Need multiple! + template = FakeTemplate.new("<%# render partial: 'messages/show', layout: 'messages/layout' %>", :erb) + tracker = make_tracker("multiple/_dependencies", template) + + assert_equal ["messages/layout", "messages/show"], tracker.dependencies + end + + def test_dependency_of_template_layout_standalone + template = FakeTemplate.new("<%# render layout: 'messages/layout' do %>", :erb) + tracker = make_tracker("messages/layout", template) + + assert_equal ["messages/layout"], tracker.dependencies + end + def test_finds_dependency_in_correct_directory template = FakeTemplate.new("<%# render(message.topic) %>", :erb) tracker = make_tracker("messages/_message", template) diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb index 47e1f6a6e5..c2b8439df3 100644 --- a/actionview/test/template/digestor_test.rb +++ b/actionview/test/template/digestor_test.rb @@ -233,6 +233,7 @@ class TemplateDigestorTest < ActionView::TestCase assert_digest_difference("messages/edit", true) do change_template("comments/_comment") end + ensure ActionView::Resolver.caching = resolver_before end diff --git a/actionview/test/template/erb_util_test.rb b/actionview/test/template/erb_util_test.rb index 9bacbba908..3bb84cbc50 100644 --- a/actionview/test/template/erb_util_test.rb +++ b/actionview/test/template/erb_util_test.rb @@ -92,6 +92,7 @@ class ErbUtilTest < ActiveSupport::TestCase def test_html_escape_once assert_equal '1 <>&"' 2 & 3', html_escape_once('1 <>&"\' 2 & 3') + assert_equal " ' ' λ λ " ' < > ", html_escape_once(" ' ' λ λ \" ' < > ") end def test_html_escape_once_returns_unsafe_strings_when_passed_unsafe_strings diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb index 0ad0ae6b4b..a9f137aec6 100644 --- a/actionview/test/template/form_helper_test.rb +++ b/actionview/test/template/form_helper_test.rb @@ -10,15 +10,20 @@ class FormHelperTest < ActionView::TestCase @output_buffer = super end - def setup - super + teardown do + I18n.backend.reload! + end + setup do # Create "label" locale for testing I18n label helpers I18n.backend.store_translations 'label', { activemodel: { attributes: { post: { cost: "Total cost" + }, + :"post/language" => { + spanish: "Espanol" } } }, @@ -154,6 +159,12 @@ class FormHelperTest < ActionView::TestCase end end + def test_label_with_human_attribute_name_and_options + with_locale :label do + assert_dom_equal('<label for="post_language_spanish">Espanol</label>', label(:post, :language, value: "spanish")) + end + end + def test_label_with_locales_symbols with_locale :label do assert_dom_equal('<label for="post_body">Write entire text here</label>', label(:post, :body)) @@ -683,6 +694,13 @@ class FormHelperTest < ActionView::TestCase ) end + def test_text_area_with_value_before_type_cast + assert_dom_equal( + %{<textarea id="post_id" name="post[id]">\n123</textarea>}, + text_area("post", "id") + ) + end + def test_text_area_with_html_entities @post.body = "The HTML Entity for & is &" assert_dom_equal( @@ -765,6 +783,22 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal(expected, date_field("post", "written_on")) end + def test_date_field_with_string_values_for_min_and_max + expected = %{<input id="post_written_on" max="2010-08-15" min="2000-06-15" name="post[written_on]" type="date" value="2004-06-15" />} + @post.written_on = DateTime.new(2004, 6, 15) + min_value = "2000-06-15" + max_value = "2010-08-15" + assert_dom_equal(expected, date_field("post", "written_on", min: min_value, max: max_value)) + end + + def test_date_field_with_invalid_string_values_for_min_and_max + expected = %{<input id="post_written_on" name="post[written_on]" type="date" value="2004-06-15" />} + @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) + min_value = "foo" + max_value = "bar" + assert_dom_equal(expected, date_field("post", "written_on", min: min_value, max: max_value)) + end + def test_time_field expected = %{<input id="post_written_on" name="post[written_on]" type="time" value="00:00:00.000" />} assert_dom_equal(expected, time_field("post", "written_on")) @@ -800,6 +834,22 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal(expected, time_field("post", "written_on")) end + def test_time_field_with_string_values_for_min_and_max + expected = %{<input id="post_written_on" max="10:25:00.000" min="20:45:30.000" name="post[written_on]" type="time" value="01:02:03.000" />} + @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) + min_value = "20:45:30.000" + max_value = "10:25:00.000" + assert_dom_equal(expected, time_field("post", "written_on", min: min_value, max: max_value)) + end + + def test_time_field_with_invalid_string_values_for_min_and_max + expected = %{<input id="post_written_on" name="post[written_on]" type="time" value="01:02:03.000" />} + @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) + min_value = "foo" + max_value = "bar" + assert_dom_equal(expected, time_field("post", "written_on", min: min_value, max: max_value)) + end + def test_datetime_field expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T00:00:00.000+0000" />} assert_dom_equal(expected, datetime_field("post", "written_on")) @@ -841,6 +891,22 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal(expected, datetime_field("post", "written_on")) end + def test_datetime_field_with_string_values_for_min_and_max + expected = %{<input id="post_written_on" max="2010-08-15T10:25:00.000+0000" min="2000-06-15T20:45:30.000+0000" name="post[written_on]" type="datetime" value="2004-06-15T01:02:03.000+0000" />} + @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) + min_value = "2000-06-15T20:45:30.000+0000" + max_value = "2010-08-15T10:25:00.000+0000" + assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value)) + end + + def test_datetime_field_with_invalid_string_values_for_min_and_max + expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T01:02:03.000+0000" />} + @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) + min_value = "foo" + max_value = "bar" + assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value)) + end + def test_datetime_local_field expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2004-06-15T00:00:00" />} assert_dom_equal(expected, datetime_local_field("post", "written_on")) @@ -876,6 +942,22 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal(expected, datetime_local_field("post", "written_on")) end + def test_datetime_local_field_with_string_values_for_min_and_max + expected = %{<input id="post_written_on" max="2010-08-15T10:25:00" min="2000-06-15T20:45:30" name="post[written_on]" type="datetime-local" value="2004-06-15T01:02:03" />} + @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) + min_value = "2000-06-15T20:45:30" + max_value = "2010-08-15T10:25:00" + assert_dom_equal(expected, datetime_local_field("post", "written_on", min: min_value, max: max_value)) + end + + def test_datetime_local_field_with_invalid_string_values_for_min_and_max + expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2004-06-15T01:02:03" />} + @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) + min_value = "foo" + max_value = "bar" + assert_dom_equal(expected, datetime_local_field("post", "written_on", min: min_value, max: max_value)) + end + def test_month_field expected = %{<input id="post_written_on" name="post[written_on]" type="month" value="2004-06" />} assert_dom_equal(expected, month_field("post", "written_on")) diff --git a/actionview/test/template/javascript_helper_test.rb b/actionview/test/template/javascript_helper_test.rb index 4703111741..9ba7f64ad1 100644 --- a/actionview/test/template/javascript_helper_test.rb +++ b/actionview/test/template/javascript_helper_test.rb @@ -12,14 +12,14 @@ class JavaScriptHelperTest < ActionView::TestCase yield if block_given? end - def setup - super + setup do + @old_escape_html_entities_in_json = ActiveSupport.escape_html_entities_in_json ActiveSupport.escape_html_entities_in_json = true @template = self end def teardown - ActiveSupport.escape_html_entities_in_json = false + ActiveSupport.escape_html_entities_in_json = @old_escape_html_entities_in_json end def test_escape_javascript diff --git a/actionview/test/template/number_helper_test.rb b/actionview/test/template/number_helper_test.rb index adb888319d..b59883b760 100644 --- a/actionview/test/template/number_helper_test.rb +++ b/actionview/test/template/number_helper_test.rb @@ -48,6 +48,7 @@ class NumberHelperTest < ActionView::TestCase assert_equal "-111.235", number_with_precision(-111.2346) assert_equal "111.00", number_with_precision(111, precision: 2) assert_equal "0.00100", number_with_precision(0.001, precision: 5) + assert_equal "3.33", number_with_precision(Rational(10, 3), precision: 2) end def test_number_to_human_size @@ -113,6 +114,8 @@ class NumberHelperTest < ActionView::TestCase I18n.backend.store_translations 'ts', :custom_units_for_number_to_human => {:mili => "mm", :centi => "cm", :deci => "dm", :unit => "m", :ten => "dam", :hundred => "hm", :thousand => "km"} assert_equal "1.01 cm", number_to_human(0.0101, :locale => 'ts', :units => :custom_units_for_number_to_human) + ensure + I18n.reload! end def test_number_helpers_outputs_are_html_safe diff --git a/actionview/test/template/output_buffer_test.rb b/actionview/test/template/output_buffer_test.rb deleted file mode 100644 index eb0df3d1ab..0000000000 --- a/actionview/test/template/output_buffer_test.rb +++ /dev/null @@ -1,59 +0,0 @@ -require 'abstract_unit' - -class OutputBufferTest < ActionController::TestCase - class TestController < ActionController::Base - def index - render :text => 'foo' - end - end - - tests TestController - - def setup - @vc = @controller.view_context - get :index - assert_equal ['foo'], body_parts - end - - test 'output buffer is nil after rendering' do - assert_nil output_buffer - end - - test 'flushing ignores nil output buffer' do - @controller.view_context.flush_output_buffer - assert_nil output_buffer - assert_equal ['foo'], body_parts - end - - test 'flushing ignores empty output buffer' do - @vc.output_buffer = '' - @vc.flush_output_buffer - assert_equal '', output_buffer - assert_equal ['foo'], body_parts - end - - test 'flushing appends the output buffer to the body parts' do - @vc.output_buffer = 'bar' - @vc.flush_output_buffer - assert_equal '', output_buffer - assert_equal ['foo', 'bar'], body_parts - end - - test 'flushing preserves output buffer encoding' do - original_buffer = ' '.force_encoding(Encoding::EUC_JP) - @vc.output_buffer = original_buffer - @vc.flush_output_buffer - assert_equal ['foo', original_buffer], body_parts - assert_not_equal original_buffer, output_buffer - assert_equal Encoding::EUC_JP, output_buffer.encoding - end - - protected - def output_buffer - @vc.output_buffer - end - - def body_parts - @controller.response.body_parts - end -end diff --git a/actionview/test/template/output_safety_helper_test.rb b/actionview/test/template/output_safety_helper_test.rb index 76c71c9e6d..a1bf0e1a5f 100644 --- a/actionview/test/template/output_safety_helper_test.rb +++ b/actionview/test/template/output_safety_helper_test.rb @@ -25,4 +25,11 @@ class OutputSafetyHelperTest < ActionView::TestCase assert_equal "<p>foo</p><br /><p>bar</p>", joined end -end
\ No newline at end of file + test "safe_join should work recursively similarly to Array.join" do + joined = safe_join(['a',['b','c']], ':') + assert_equal 'a:b:c', joined + + joined = safe_join(['"a"',['<b>','<c>']], ' <br/> ') + assert_equal '"a" <br/> <b> <br/> <c>', joined + end +end diff --git a/actionview/test/template/partial_iteration_test.rb b/actionview/test/template/partial_iteration_test.rb new file mode 100644 index 0000000000..695f9f1bef --- /dev/null +++ b/actionview/test/template/partial_iteration_test.rb @@ -0,0 +1,33 @@ +require 'abstract_unit' +require 'action_view/renderer/partial_renderer' + +class PartialIterationTest < ActiveSupport::TestCase + def test_has_size_and_index + iteration = ActionView::PartialIteration.new 3 + assert_equal 0, iteration.index, "should be at the first index" + assert_equal 3, iteration.size, "should have the size" + end + + def test_first_is_true_when_current_is_at_the_first_index + iteration = ActionView::PartialIteration.new 3 + assert iteration.first?, "first when current is 0" + end + + def test_first_is_false_unless_current_is_at_the_first_index + iteration = ActionView::PartialIteration.new 3 + iteration.iterate! + assert !iteration.first?, "not first when current is 1" + end + + def test_last_is_true_when_current_is_at_the_last_index + iteration = ActionView::PartialIteration.new 3 + iteration.iterate! + iteration.iterate! + assert iteration.last?, "last when current is 2" + end + + def test_last_is_false_unless_current_is_at_the_last_index + iteration = ActionView::PartialIteration.new 3 + assert !iteration.last?, "not last when current is 0" + end +end diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb index ca508abfb8..85817119ba 100644 --- a/actionview/test/template/render_test.rb +++ b/actionview/test/template/render_test.rb @@ -12,7 +12,6 @@ module RenderTestCases @controller_view = TestController.new.view_context # Reload and register danish language for testing - I18n.reload! I18n.backend.store_translations 'da', {} I18n.backend.store_translations 'pt-BR', {} @@ -257,7 +256,7 @@ module RenderTestCases end def test_render_partial_collection_without_as - assert_equal "local_inspector,local_inspector_counter", + assert_equal "local_inspector,local_inspector_counter,local_inspector_iteration", @view.render(:partial => "test/local_inspector", :collection => [ Customer.new("mary") ]) end @@ -325,11 +324,16 @@ module RenderTestCases @controller_view.render(customers, :greeting => "Hello") end + def test_render_partial_using_collection_without_path + assert_equal "hi good customer: david0", @controller_view.render([ GoodCustomer.new("david") ], greeting: "hi") + end + def test_render_partial_without_object_or_collection_does_not_generate_partial_name_local_variable exception = assert_raises ActionView::Template::Error do @controller_view.render("partial_name_local_variable") end - assert_match "undefined local variable or method `partial_name_local_variable'", exception.message + assert_instance_of NameError, exception.original_exception + assert_equal :partial_name_local_variable, exception.original_exception.name end # TODO: The reason for this test is unclear, improve documentation @@ -369,23 +373,48 @@ module RenderTestCases def test_render_inline_with_render_from_to_proc ActionView::Template.register_template_handler :ruby_handler, :source.to_proc - assert_equal '3', @view.render(:inline => "(1 + 2).to_s", :type => :ruby_handler) + assert_equal '3', @view.render(inline: "(1 + 2).to_s", type: :ruby_handler) + ensure + ActionView::Template.unregister_template_handler :ruby_handler end def test_render_inline_with_compilable_custom_type ActionView::Template.register_template_handler :foo, CustomHandler - assert_equal 'source: "Hello, World!"', @view.render(:inline => "Hello, World!", :type => :foo) + assert_equal 'source: "Hello, World!"', @view.render(inline: "Hello, World!", type: :foo) + ensure + ActionView::Template.unregister_template_handler :foo end def test_render_inline_with_locals_and_compilable_custom_type ActionView::Template.register_template_handler :foo, CustomHandler - assert_equal 'source: "Hello, <%= name %>!"', @view.render(:inline => "Hello, <%= name %>!", :locals => { :name => "Josh" }, :type => :foo) + assert_equal 'source: "Hello, <%= name %>!"', @view.render(inline: "Hello, <%= name %>!", locals: { name: "Josh" }, type: :foo) + ensure + ActionView::Template.unregister_template_handler :foo + end + + def test_render_body + assert_equal 'some body', @view.render(body: 'some body') + end + + def test_render_plain + assert_equal 'some plaintext', @view.render(plain: 'some plaintext') end def test_render_knows_about_types_registered_when_extensions_are_checked_earlier_in_initialization ActionView::Template::Handlers.extensions ActionView::Template.register_template_handler :foo, CustomHandler assert ActionView::Template::Handlers.extensions.include?(:foo) + ensure + ActionView::Template.unregister_template_handler :foo + end + + def test_render_does_not_use_unregistered_extension_and_template_handler + ActionView::Template.register_template_handler :foo, CustomHandler + ActionView::Template.unregister_template_handler :foo + assert_not ActionView::Template::Handlers.extensions.include?(:foo) + assert_equal "Hello, World!", @view.render(inline: "Hello, World!", type: :foo) + ensure + ActionView::Template::Handlers.class_variable_get(:@@template_handlers).delete(:foo) end def test_render_ignores_templates_with_malformed_template_handlers @@ -474,7 +503,9 @@ module RenderTestCases def test_render_with_passing_couple_extensions_to_one_register_template_handler_function_call ActionView::Template.register_template_handler :foo1, :foo2, CustomHandler - assert_equal @view.render(:inline => "Hello, World!", :type => :foo1), @view.render(:inline => "Hello, World!", :type => :foo2) + assert_equal @view.render(inline: "Hello, World!", type: :foo1), @view.render(inline: "Hello, World!", type: :foo2) + ensure + ActionView::Template.unregister_template_handler :foo1, :foo2 end def test_render_throws_exception_when_no_extensions_passed_to_register_template_handler_function_call @@ -494,6 +525,7 @@ class CachedViewRenderTest < ActiveSupport::TestCase def teardown GC.start + I18n.reload! end end @@ -511,6 +543,7 @@ class LazyViewRenderTest < ActiveSupport::TestCase def teardown GC.start + I18n.reload! end def test_render_utf8_template_with_magic_comment diff --git a/actionview/test/template/sanitize_helper_test.rb b/actionview/test/template/sanitize_helper_test.rb index 12d5260a9d..f7c8f36b78 100644 --- a/actionview/test/template/sanitize_helper_test.rb +++ b/actionview/test/template/sanitize_helper_test.rb @@ -1,6 +1,6 @@ require 'abstract_unit' -# The exhaustive tests are in test/controller/html/sanitizer_test.rb. +# The exhaustive tests are in test/template/html-scanner/sanitizer_test.rb # This tests the that the helpers hook up correctly to the sanitizer classes. class SanitizeHelperTest < ActionView::TestCase tests ActionView::Helpers::SanitizeHelper diff --git a/actionview/test/template/tag_helper_test.rb b/actionview/test/template/tag_helper_test.rb index fb016a52de..0ea669b3d0 100644 --- a/actionview/test/template/tag_helper_test.rb +++ b/actionview/test/template/tag_helper_test.rb @@ -80,11 +80,27 @@ class TagHelperTest < ActionView::TestCase str = content_tag('p', "limelight", :class => ["song", "play"]) assert_equal "<p class=\"song play\">limelight</p>", str + + str = content_tag('p', "limelight", :class => ["song", ["play"]]) + assert_equal "<p class=\"song play\">limelight</p>", str end def test_content_tag_with_unescaped_array_class str = content_tag('p', "limelight", {:class => ["song", "play>"]}, false) assert_equal "<p class=\"song play>\">limelight</p>", str + + str = content_tag('p', "limelight", {:class => ["song", ["play>"]]}, false) + assert_equal "<p class=\"song play>\">limelight</p>", str + end + + def test_content_tag_with_empty_array_class + str = content_tag('p', 'limelight', {:class => []}) + assert_equal '<p class="">limelight</p>', str + end + + def test_content_tag_with_unescaped_empty_array_class + str = content_tag('p', 'limelight', {:class => []}, false) + assert_equal '<p class="">limelight</p>', str end def test_content_tag_with_data_attributes @@ -107,6 +123,7 @@ class TagHelperTest < ActionView::TestCase def test_escape_once assert_equal '1 < 2 & 3', escape_once('1 < 2 & 3') + assert_equal " ' ' λ λ " ' < > ", escape_once(" ' ' λ λ \" ' < > ") end def test_tag_honors_html_safe_for_param_values @@ -115,6 +132,14 @@ class TagHelperTest < ActionView::TestCase end end + def test_tag_honors_html_safe_with_escaped_array_class + str = tag('p', :class => ['song>', 'play>'.html_safe]) + assert_equal '<p class="song> play>" />', str + + str = tag('p', :class => ['song>'.html_safe, 'play>']) + assert_equal '<p class="song> play>" />', str + end + def test_skip_invalid_escaped_attributes ['&1;', 'dfa3;', '& #123;'].each do |escaped| assert_equal %(<a href="#{escaped.gsub(/&/, '&')}" />), tag('a', :href => escaped) diff --git a/actionview/test/template/test_case_test.rb b/actionview/test/template/test_case_test.rb index 4ee0930341..4582fa13ee 100644 --- a/actionview/test/template/test_case_test.rb +++ b/actionview/test/template/test_case_test.rb @@ -1,4 +1,5 @@ require 'abstract_unit' +require 'rails/engine' module ActionView @@ -223,7 +224,7 @@ module ActionView test "is able to use mounted routes" do with_routing do |set| - app = Class.new do + app = Class.new(Rails::Engine) do def self.routes @routes ||= ActionDispatch::Routing::RouteSet.new end diff --git a/actionview/test/template/test_test.rb b/actionview/test/template/test_test.rb index 108a674d95..88bac85039 100644 --- a/actionview/test/template/test_test.rb +++ b/actionview/test/template/test_test.rb @@ -37,10 +37,22 @@ class PeopleHelperTest < ActionView::TestCase def test_link_to_person with_test_route_set do - person = mock(:name => "David") - person.class.extend ActiveModel::Naming - expects(:mocha_mock_path).with(person).returns("/people/1") + person = Struct.new(:name) { + extend ActiveModel::Naming + def to_model; self; end + def persisted?; true; end + def self.name; 'Mocha::Mock'; end + }.new "David" + + the_model = nil + extend Module.new { + define_method(:mocha_mock_path) { |model, *args| + the_model = model + "/people/1" + } + } assert_equal '<a href="/people/1">David</a>', link_to_person(person) + assert_equal person, the_model end end diff --git a/actionview/test/template/text_helper_test.rb b/actionview/test/template/text_helper_test.rb index a514bba83d..db416a8de4 100644 --- a/actionview/test/template/text_helper_test.rb +++ b/actionview/test/template/text_helper_test.rb @@ -222,6 +222,11 @@ class TextHelperTest < ActionView::TestCase ) end + def test_highlight_accepts_regexp + assert_equal("This day was challenging for judge <mark>Allen</mark> and his colleagues.", + highlight("This day was challenging for judge Allen and his colleagues.", /\ballen\b/i)) + end + def test_highlight_with_multiple_phrases_in_one_pass assert_equal %(<em>wow</em> <em>em</em>), highlight('wow em', %w(wow em), :highlighter => '<em>\1</em>') end @@ -260,6 +265,13 @@ class TextHelperTest < ActionView::TestCase assert_equal options, passed_options end + def test_highlight_with_block + assert_equal( + "<b>one</b> <b>two</b> <b>three</b>", + highlight("one two three", ["one", "two", "three"]) { |word| "<b>#{word}</b>" } + ) + end + def test_excerpt assert_equal("...is a beautiful morn...", excerpt("This is a beautiful morning", "beautiful", :radius => 5)) assert_equal("This is a...", excerpt("This is a beautiful morning", "this", :radius => 5)) @@ -267,6 +279,16 @@ class TextHelperTest < ActionView::TestCase assert_nil excerpt("This is a beautiful morning", "day") end + def test_excerpt_with_regex + assert_equal('...is a beautiful! mor...', excerpt('This is a beautiful! morning', 'beautiful', :radius => 5)) + assert_equal('...is a beautiful? mor...', excerpt('This is a beautiful? morning', 'beautiful', :radius => 5)) + assert_equal('...is a beautiful? mor...', excerpt('This is a beautiful? morning', /\bbeau\w*\b/i, :radius => 5)) + assert_equal('...is a beautiful? mor...', excerpt('This is a beautiful? morning', /\b(beau\w*)\b/i, :radius => 5)) + assert_equal("...udge Allen and...", excerpt("This day was challenging for judge Allen and his colleagues.", /\ballen\b/i, :radius => 5)) + assert_equal("...judge Allen and...", excerpt("This day was challenging for judge Allen and his colleagues.", /\ballen\b/i, :radius => 1, :separator => ' ')) + assert_equal("...was challenging for...", excerpt("This day was challenging for judge Allen and his colleagues.", /\b(\w*allen\w*)\b/i, :radius => 5)) + end + def test_excerpt_should_not_be_html_safe assert !excerpt('This is a beautiful! morning', 'beautiful', :radius => 5).html_safe? end @@ -288,11 +310,6 @@ class TextHelperTest < ActionView::TestCase assert_equal("...abc...", excerpt("z abc d", "b", :radius => 1)) end - def test_excerpt_with_regex - assert_equal('...is a beautiful! mor...', excerpt('This is a beautiful! morning', 'beautiful', :radius => 5)) - assert_equal('...is a beautiful? mor...', excerpt('This is a beautiful? morning', 'beautiful', :radius => 5)) - end - def test_excerpt_with_omission assert_equal("[...]is a beautiful morn[...]", excerpt("This is a beautiful morning", "beautiful", :omission => "[...]",:radius => 5)) assert_equal( diff --git a/actionview/test/template/translation_helper_test.rb b/actionview/test/template/translation_helper_test.rb index c4770840fb..41f6770f23 100644 --- a/actionview/test/template/translation_helper_test.rb +++ b/actionview/test/template/translation_helper_test.rb @@ -6,7 +6,7 @@ class TranslationHelperTest < ActiveSupport::TestCase attr_reader :request, :view - def setup + setup do I18n.backend.store_translations(:en, :translations => { :templates => { @@ -30,6 +30,10 @@ class TranslationHelperTest < ActiveSupport::TestCase @view = ::ActionView::Base.new(ActionController::Base.view_paths, {}) end + teardown do + I18n.backend.reload! + end + def test_delegates_to_i18n_setting_the_rescue_format_option_to_html I18n.expects(:translate).with(:foo, :locale => 'en', :raise=>true).returns("") translate :foo, :locale => 'en' @@ -151,4 +155,10 @@ class TranslationHelperTest < ActiveSupport::TestCase translation = translate(:'translations.missing', default: ['A Generic String', 'Second generic string']) assert_equal 'A Generic String', translation end + + def test_translate_does_not_change_options + options = {} + translate(:'translations.missing', options) + assert_equal({}, options) + end end diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 0db220ab8f..8d22e3ac46 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,6 +1,51 @@ +* Validate options passed to `ActiveModel::Validations.validate`. + + Preventing, in many cases, the simple mistake of using `validate` instead of `validates`. + + *Sonny Michaud* + +* Deprecate `reset_#{attribute}` in favor of `restore_#{attribute}`. + + These methods may cause confusion with the `reset_changes` that behaves differently + of them. + +* Deprecate `ActiveModel::Dirty#reset_changes` in favor of `#clear_changes_information`. + + This method name is causing confusion with the `reset_#{attribute}` + methods. While `reset_name` set the value of the name attribute for the + previous value `reset_changes` only discard the changes and previous + changes. + +* Added `restore_attributes` method to `ActiveModel::Dirty` API to restore all the + changed values to the previous data. + + *Igor G.* + +* Allow proc and symbol as values for `only_integer` of `NumericalityValidator` + + *Robin Mehner* + +* `has_secure_password` now verifies that the given password is less than 72 + characters if validations are enabled. + + Fixes #14591. + + *Akshay Vishnoi* + +* Remove deprecated `Validator#setup` without replacement. + + See #10716. + + *Kuldeep Aggarwal* + +* Add plural and singular form for length validator's default messages. + + *Abd ar-Rahman Hamid* + * Introduce `validate` as an alias for `valid?`. - This is more intuitive when you want to run validations but don't care about the return value. + This is more intuitive when you want to run validations but don't care about + the return value. *Henrik Nyh* diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc index 500be2a04a..f6beff14e1 100644 --- a/activemodel/README.rdoc +++ b/activemodel/README.rdoc @@ -147,7 +147,7 @@ behavior out of the box: extend ActiveModel::Naming end - NamedPerson.model_name # => "NamedPerson" + NamedPerson.model_name.name # => "NamedPerson" NamedPerson.model_name.human # => "Named person" {Learn more}[link:classes/ActiveModel/Naming.html] @@ -262,6 +262,11 @@ API documentation is at * http://api.rubyonrails.org -Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here: +Bug reports can be filed for the Ruby on Rails project here: * https://github.com/rails/rails/issues + +Feature requests should be discussed on the rails-core mailing list here: + +* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core + diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb index 374265f0d8..9c9b6f4a77 100644 --- a/activemodel/lib/active_model/conversion.rb +++ b/activemodel/lib/active_model/conversion.rb @@ -40,13 +40,15 @@ module ActiveModel self end - # Returns an Enumerable of all key attributes if any is set, regardless if + # Returns an Array of all key attributes if any is set, regardless if # the object is persisted or not. Returns +nil+ if there are no key attributes. # - # class Person < ActiveRecord::Base + # class Person + # include ActiveModel::Conversion + # attr_accessor :id # end # - # person = Person.create + # person = Person.create(id: 1) # person.to_key # => [1] def to_key key = respond_to?(:id) && id @@ -56,10 +58,15 @@ module ActiveModel # Returns a +string+ representing the object's key suitable for use in URLs, # or +nil+ if <tt>persisted?</tt> is +false+. # - # class Person < ActiveRecord::Base + # class Person + # include ActiveModel::Conversion + # attr_accessor :id + # def persisted? + # true + # end # end # - # person = Person.create + # person = Person.create(id: 1) # person.to_param # => "1" def to_param (persisted? && key = to_key) ? key.join('-') : nil diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index 98ffffeb10..d11243c4c0 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -15,8 +15,9 @@ module ActiveModel # * Call <tt>attr_name_will_change!</tt> before each change to the tracked # attribute. # * Call <tt>changes_applied</tt> after the changes are persisted. - # * Call <tt>reset_changes</tt> when you want to reset the changes + # * Call <tt>clear_changes_information</tt> when you want to reset the changes # information. + # * Call <tt>restore_attributes</tt> when you want to restore previous data. # # A minimal implementation could be: # @@ -36,11 +37,18 @@ module ActiveModel # # def save # # do persistence work + # # changes_applied # end # # def reload! - # reset_changes + # # get the values from the persistence layer + # + # clear_changes_information + # end + # + # def rollback! + # restore_attributes # end # end # @@ -72,6 +80,13 @@ module ActiveModel # person.reload! # person.previous_changes # => {} # + # Rollback the changes: + # + # person.name = "Uncle Bob" + # person.rollback! + # person.name # => "Bill" + # person.name_changed? # => false + # # Assigning the same value leaves the attribute unchanged: # # person.name = 'Bill' @@ -84,9 +99,11 @@ module ActiveModel # person.changed # => ["name"] # person.changes # => {"name" => ["Bill", "Bob"]} # - # If an attribute is modified in-place then make use of <tt>[attribute_name]_will_change!</tt> - # to mark that the attribute is changing. Otherwise ActiveModel can't track - # changes to in-place attributes. + # If an attribute is modified in-place then make use of + # +[attribute_name]_will_change!+ to mark that the attribute is changing. + # Otherwise Active Model can't track changes to in-place attributes. Note + # that Active Record can detect in-place modifications automatically. You do + # not need to call +[attribute_name]_will_change!+ on Active Record models. # # person.name_will_change! # person.name_change # => ["Bill", "Bill"] @@ -99,6 +116,7 @@ module ActiveModel included do attribute_method_suffix '_changed?', '_change', '_will_change!', '_was' attribute_method_affix prefix: 'reset_', suffix: '!' + attribute_method_affix prefix: 'restore_', suffix: '!' end # Returns +true+ if any attribute have unsaved changes, +false+ otherwise. @@ -162,20 +180,30 @@ module ActiveModel attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr) end + # Restore all previous data of the provided attributes. + def restore_attributes(attributes = changed) + attributes.each { |attr| restore_attribute! attr } + end + private # Removes current changes and makes them accessible through +previous_changes+. - def changes_applied + def changes_applied # :doc: @previously_changed = changes @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new end - # Removes all dirty data: current changes and previous changes - def reset_changes + # Clear all dirty data: current changes and previous changes. + def clear_changes_information # :doc: @previously_changed = ActiveSupport::HashWithIndifferentAccess.new @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new end + def reset_changes + ActiveSupport::Deprecation.warn "#reset_changes is deprecated and will be removed on Rails 5. Please use #clear_changes_information instead." + clear_changes_information + end + # Handle <tt>*_change</tt> for +method_missing+. def attribute_change(attr) [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr) @@ -196,6 +224,13 @@ module ActiveModel # Handle <tt>reset_*!</tt> for +method_missing+. def reset_attribute!(attr) + ActiveSupport::Deprecation.warn "#reset_#{attr}! is deprecated and will be removed on Rails 5. Please use #restore_#{attr}! instead." + + restore_attribute!(attr) + end + + # Handle <tt>restore_*!</tt> for +method_missing+. + def restore_attribute!(attr) if attribute_changed?(attr) __send__("#{attr}=", changed_attributes[attr]) changed_attributes.delete(attr) diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 917d3b9142..1d025beeef 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -23,7 +23,7 @@ module ActiveModel # attr_reader :errors # # def validate! - # errors.add(:name, "cannot be nil") if name == nil + # errors.add(:name, "cannot be nil") if name.nil? # end # # # The following methods are needed to be minimally implemented diff --git a/activemodel/lib/active_model/locale/en.yml b/activemodel/lib/active_model/locale/en.yml index 540e8132d3..bf07945fe1 100644 --- a/activemodel/lib/active_model/locale/en.yml +++ b/activemodel/lib/active_model/locale/en.yml @@ -14,9 +14,15 @@ en: empty: "can't be empty" blank: "can't be blank" present: "must be blank" - too_long: "is too long (maximum is %{count} characters)" - too_short: "is too short (minimum is %{count} characters)" - wrong_length: "is the wrong length (should be %{count} characters)" + too_long: + one: "is too long (maximum is 1 character)" + other: "is too long (maximum is %{count} characters)" + too_short: + one: "is too short (minimum is 1 character)" + other: "is too short (minimum is %{count} characters)" + wrong_length: + one: "is the wrong length (should be 1 character)" + other: "is the wrong length (should be %{count} characters)" not_a_number: "is not a number" not_an_integer: "must be an integer" greater_than: "must be greater than %{count}" diff --git a/activemodel/lib/active_model/model.rb b/activemodel/lib/active_model/model.rb index 63716eebb1..640024eaa1 100644 --- a/activemodel/lib/active_model/model.rb +++ b/activemodel/lib/active_model/model.rb @@ -16,8 +16,8 @@ module ActiveModel # end # # person = Person.new(name: 'bob', age: '18') - # person.name # => 'bob' - # person.age # => 18 + # person.name # => "bob" + # person.age # => "18" # # Note that, by default, <tt>ActiveModel::Model</tt> implements <tt>persisted?</tt> # to return +false+, which is the most common case. You may want to override @@ -74,7 +74,7 @@ module ActiveModel # # person = Person.new(name: 'bob', age: '18') # person.name # => "bob" - # person.age # => 18 + # person.age # => "18" def initialize(params={}) params.each do |attr, value| self.public_send("#{attr}=", value) diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index 11ebfe6cc0..86f5c96af9 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -204,7 +204,7 @@ module ActiveModel # extend ActiveModel::Naming # end # - # BookCover.model_name # => "BookCover" + # BookCover.model_name.name # => "BookCover" # BookCover.model_name.human # => "Book cover" # # BookCover.model_name.i18n_key # => :book_cover @@ -214,14 +214,22 @@ module ActiveModel # is required to pass the Active Model Lint test. So either extending the # provided method below, or rolling your own is required. module Naming + def self.extended(base) #:nodoc: + base.class_eval do + remove_possible_method(:model_name) + delegate :model_name, to: :class + end + end + # Returns an ActiveModel::Name object for module. It can be # used to retrieve all kinds of naming-related information # (See ActiveModel::Name for more information). # - # class Person < ActiveModel::Model + # class Person + # include ActiveModel::Model # end # - # Person.model_name # => Person + # Person.model_name.name # => "Person" # Person.model_name.class # => ActiveModel::Name # Person.model_name.singular # => "person" # Person.model_name.plural # => "people" diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index 826e89bf9d..7e179cf4b7 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -2,6 +2,11 @@ module ActiveModel module SecurePassword extend ActiveSupport::Concern + # BCrypt hash function can handle maximum 72 characters, and if we pass + # password of length more than 72 characters it ignores extra characters. + # Hence need to put a restriction on password length. + MAX_PASSWORD_LENGTH_ALLOWED = 72 + class << self attr_accessor :min_cost # :nodoc: end @@ -11,16 +16,20 @@ module ActiveModel # Adds methods to set and authenticate against a BCrypt password. # This mechanism requires you to have a +password_digest+ attribute. # - # Validations for presence of password on create, confirmation of password - # (using a +password_confirmation+ attribute) are automatically added. If - # you wish to turn off validations, pass <tt>validations: false</tt> as an - # argument. You can add more validations by hand if need be. + # The following validations are added automatically: + # * Password must be present on creation + # * Password length should be less than or equal to 72 characters + # * Confirmation of password (using a +password_confirmation+ attribute) + # + # If password confirmation validation is not needed, simply leave out the + # value for +password_confirmation+ (i.e. don't provide a form field for + # it). When this attribute has a +nil+ value, the validation will not be + # triggered. # - # If you don't need the confirmation validation, just don't set any - # value to the password_confirmation attribute and the validation - # will not be triggered. + # For further customizability, it is possible to supress the default + # validations by passing <tt>validations: false</tt> as an argument. # - # You need to add bcrypt (~> 3.1.7) to Gemfile to use #has_secure_password: + # Add bcrypt (~> 3.1.7) to Gemfile to use #has_secure_password: # # gem 'bcrypt', '~> 3.1.7' # @@ -52,11 +61,11 @@ module ActiveModel raise end - attr_reader :password - include InstanceMethodsOnActivation if options.fetch(:validations, true) + include ActiveModel::Validations + # This ensures the model has a password by checking whether the password_digest # is present, so that this works with both new and existing records. However, # when there is an error, the message is added to the password attribute instead @@ -65,9 +74,11 @@ module ActiveModel record.errors.add(:password, :blank) unless record.password_digest.present? end + validates_length_of :password, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED validates_confirmation_of :password, if: ->{ password.present? } end + # This code is necessary as long as the protected_attributes gem is supported. if respond_to?(:attributes_protected_by_default) def self.attributes_protected_by_default #:nodoc: super + ['password_digest'] @@ -91,6 +102,8 @@ module ActiveModel BCrypt::Password.new(password_digest) == unencrypted_password && self end + attr_reader :password + # Encrypts the password into the +password_digest+ attribute, only if the # new password is not blank. # diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index 36a6c00290..976f50b13e 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -4,7 +4,7 @@ require 'active_support/core_ext/hash/slice' module ActiveModel # == Active \Model \Serialization # - # Provides a basic serialization to a serializable_hash for your object. + # Provides a basic serialization to a serializable_hash for your objects. # # A minimal implementation could be: # @@ -25,14 +25,14 @@ module ActiveModel # person.name = "Bob" # person.serializable_hash # => {"name"=>"Bob"} # - # You need to declare an attributes hash which contains the attributes you - # want to serialize. Attributes must be strings, not symbols. When called, - # serializable hash will use instance methods that match the name of the - # attributes hash's keys. In order to override this behavior, take a look at - # the private method +read_attribute_for_serialization+. + # An +attributes+ hash must be defined and should contain any attributes you + # need to be serialized. Attributes must be strings, not symbols. + # When called, serializable hash will use instance methods that match the name + # of the attributes hash's keys. In order to override this behavior, take a look + # at the private method +read_attribute_for_serialization+. # - # Most of the time though, you will want to include the JSON or XML - # serializations. Both of these modules automatically include the + # Most of the time though, either the JSON or XML serializations are needed. + # Both of these modules automatically include the # <tt>ActiveModel::Serialization</tt> module, so there is no need to # explicitly include it. # diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index cf97f45dba..f67a3be5c1 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -141,6 +141,11 @@ module ActiveModel # value. def validate(*args, &block) options = args.extract_options! + + if args.all? { |arg| arg.is_a?(Symbol) } + options.assert_valid_keys([:on, :if, :unless]) + end + if options.key?(:on) options = options.dup options[:if] = Array(options[:if]) @@ -148,6 +153,7 @@ module ActiveModel Array(options[:on]).include?(o.validation_context) } end + args << options set_callback(:validate, *args, &block) end diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index a9fb9804d4..5bd162433d 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -30,7 +30,7 @@ module ActiveModel return end - if options[:only_integer] + if allow_only_integer?(record) unless value = parse_raw_value_as_an_integer(raw_value) record.errors.add(attr_name, :not_an_integer, filtered_options(raw_value)) return @@ -75,6 +75,17 @@ module ActiveModel filtered[:value] = value filtered end + + def allow_only_integer?(record) + case options[:only_integer] + when Symbol + record.send(options[:only_integer]) + when Proc + options[:only_integer].call(record) + else + options[:only_integer] + end + end end module HelperMethods @@ -121,6 +132,7 @@ module ActiveModel # * <tt>:equal_to</tt> # * <tt>:less_than</tt> # * <tt>:less_than_or_equal_to</tt> + # * <tt>:only_integer</tt> # # For example: # diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index bddacc8c45..0116de68ab 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -79,7 +79,7 @@ module ActiveModel # include ActiveModel::Validations # attr_accessor :title # - # validates :title, presence: true + # validates :title, presence: true, title: true # end # # It can be useful to access the class that is using that validator when there are prerequisites such @@ -106,7 +106,6 @@ module ActiveModel # Accepts options that will be made available through the +options+ reader. def initialize(options = {}) @options = options.except(:class).freeze - deprecated_setup(options) end # Returns the kind for this validator. @@ -122,21 +121,6 @@ module ActiveModel def validate(record) raise NotImplementedError, "Subclasses must implement a validate(record) method." end - - private - def deprecated_setup(options) # TODO: remove me in 4.2. - return unless respond_to?(:setup) - ActiveSupport::Deprecation.warn "The `Validator#setup` instance method is deprecated and will be removed on Rails 4.2. Do your setup in the constructor instead: - -class MyValidator < ActiveModel::Validator - def initialize(options={}) - super - options[:class].send :attr_accessor, :custom_attribute - end -end -" - setup(options[:class]) - end end # +EachValidator+ is a validator which iterates through the attributes given diff --git a/activemodel/test/cases/conversion_test.rb b/activemodel/test/cases/conversion_test.rb index c5cfbf909d..800cad6d9a 100644 --- a/activemodel/test/cases/conversion_test.rb +++ b/activemodel/test/cases/conversion_test.rb @@ -24,6 +24,10 @@ class ConversionTest < ActiveModel::TestCase assert_equal "1", Contact.new(id: 1).to_param end + test "to_param returns the string joined by '-'" do + assert_equal "abc-xyz", Contact.new(id: ["abc", "xyz"]).to_param + end + test "to_param returns nil if to_key is nil" do klass = Class.new(Contact) do def persisted? diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb index 2853476c91..db2cd885e2 100644 --- a/activemodel/test/cases/dirty_test.rb +++ b/activemodel/test/cases/dirty_test.rb @@ -43,6 +43,10 @@ class DirtyTest < ActiveModel::TestCase end def reload + clear_changes_information + end + + def deprecated_reload reset_changes end end @@ -107,7 +111,7 @@ class DirtyTest < ActiveModel::TestCase test "resetting attribute" do @model.name = "Bob" - @model.reset_name! + @model.restore_name! assert_nil @model.name assert !@model.name_changed? end @@ -176,4 +180,49 @@ class DirtyTest < ActiveModel::TestCase assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.previous_changes assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.changed_attributes end + + test "reset_changes is deprecated" do + @model.name = 'Dmitry' + @model.name_changed? + @model.save + @model.name = 'Bob' + + assert_equal [nil, 'Dmitry'], @model.previous_changes['name'] + assert_equal 'Dmitry', @model.changed_attributes['name'] + + assert_deprecated do + @model.deprecated_reload + end + + assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.previous_changes + assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.changed_attributes + end + + test "restore_attributes should restore all previous data" do + @model.name = 'Dmitry' + @model.color = 'Red' + @model.save + @model.name = 'Bob' + @model.color = 'White' + + @model.restore_attributes + + assert_not @model.changed? + assert_equal 'Dmitry', @model.name + assert_equal 'Red', @model.color + end + + test "restore_attributes can restore only some attributes" do + @model.name = 'Dmitry' + @model.color = 'Red' + @model.save + @model.name = 'Bob' + @model.color = 'White' + + @model.restore_attributes(['name']) + + assert @model.changed? + assert_equal 'Dmitry', @model.name + assert_equal 'White', @model.color + end end diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb index 522a7cebb4..804e0c24f6 100644 --- a/activemodel/test/cases/helper.rb +++ b/activemodel/test/cases/helper.rb @@ -11,3 +11,5 @@ ActiveSupport::Deprecation.debug = true I18n.enforce_available_locales = false require 'active_support/testing/autorun' + +require 'mocha/setup' # FIXME: stop using mocha diff --git a/activemodel/test/cases/naming_test.rb b/activemodel/test/cases/naming_test.rb index aa683f4152..7b8287edbf 100644 --- a/activemodel/test/cases/naming_test.rb +++ b/activemodel/test/cases/naming_test.rb @@ -272,3 +272,9 @@ class NameWithAnonymousClassTest < ActiveModel::TestCase assert_equal "Anonymous", model_name end end + +class NamingMethodDelegationTest < ActiveModel::TestCase + def test_model_name + assert_equal Blog::Post.model_name, Blog::Post.new.model_name + end +end diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb index bcd1e04a0f..6b21bc68fa 100644 --- a/activemodel/test/cases/secure_password_test.rb +++ b/activemodel/test/cases/secure_password_test.rb @@ -20,15 +20,24 @@ class SecurePasswordTest < ActiveModel::TestCase ActiveModel::SecurePassword.min_cost = @original_min_cost end - test "create and updating without validations" do - assert @visitor.valid?(:create), 'visitor should be valid' - assert @visitor.valid?(:update), 'visitor should be valid' + test "automatically include ActiveModel::Validations when validations are enabled" do + assert_respond_to @user, :valid? + end - @visitor.password = '123' - @visitor.password_confirmation = '456' + test "don't include ActiveModel::Validations when validations are disabled" do + assert_not_respond_to @visitor, :valid? + end - assert @visitor.valid?(:create), 'visitor should be valid' - assert @visitor.valid?(:update), 'visitor should be valid' + test "create a new user with validations and valid password/confirmation" do + @user.password = 'password' + @user.password_confirmation = 'password' + + assert @user.valid?(:create), 'user should be valid' + + @user.password = 'a' * 72 + @user.password_confirmation = 'a' * 72 + + assert @user.valid?(:create), 'user should be valid' end test "create a new user with validation and a blank password" do @@ -45,6 +54,14 @@ class SecurePasswordTest < ActiveModel::TestCase assert_equal ["can't be blank"], @user.errors[:password] end + test 'create a new user with validation and password length greater than 72' do + @user.password = 'a' * 73 + @user.password_confirmation = 'a' * 73 + assert !@user.valid?(:create), 'user should be invalid' + assert_equal 1, @user.errors.count + assert_equal ["is too long (maximum is 72 characters)"], @user.errors[:password] + end + test "create a new user with validation and a blank password confirmation" do @user.password = 'password' @user.password_confirmation = '' @@ -67,15 +84,19 @@ class SecurePasswordTest < ActiveModel::TestCase assert_equal ["doesn't match Password"], @user.errors[:password_confirmation] end - test "create a new user with validation and a correct password confirmation" do - @user.password = 'password' - @user.password_confirmation = 'something else' - assert !@user.valid?(:create), 'user should be invalid' - assert_equal 1, @user.errors.count - assert_equal ["doesn't match Password"], @user.errors[:password_confirmation] + test "update an existing user with validation and no change in password" do + assert @existing_user.valid?(:update), 'user should be valid' end - test "update an existing user with validation and no change in password" do + test "update an existing user with validations and valid password/confirmation" do + @existing_user.password = 'password' + @existing_user.password_confirmation = 'password' + + assert @existing_user.valid?(:update), 'user should be valid' + + @existing_user.password = 'a' * 72 + @existing_user.password_confirmation = 'a' * 72 + assert @existing_user.valid?(:update), 'user should be valid' end @@ -97,6 +118,14 @@ class SecurePasswordTest < ActiveModel::TestCase assert_equal ["can't be blank"], @existing_user.errors[:password] end + test 'updating an existing user with validation and password length greater than 72' do + @existing_user.password = 'a' * 73 + @existing_user.password_confirmation = 'a' * 73 + assert !@existing_user.valid?(:update), 'user should be invalid' + assert_equal 1, @existing_user.errors.count + assert_equal ["is too long (maximum is 72 characters)"], @existing_user.errors[:password] + end + test "updating an existing user with validation and a blank password confirmation" do @existing_user.password = 'password' @existing_user.password_confirmation = '' @@ -119,14 +148,6 @@ class SecurePasswordTest < ActiveModel::TestCase assert_equal ["doesn't match Password"], @existing_user.errors[:password_confirmation] end - test "updating an existing user with validation and a correct password confirmation" do - @existing_user.password = 'password' - @existing_user.password_confirmation = 'something else' - assert !@existing_user.valid?(:update), 'user should be invalid' - assert_equal 1, @existing_user.errors.count - assert_equal ["doesn't match Password"], @existing_user.errors[:password_confirmation] - end - test "updating an existing user with validation and a blank password digest" do @existing_user.password_digest = '' assert !@existing_user.valid?(:update), 'user should be invalid' diff --git a/activemodel/test/cases/serializers/json_serialization_test.rb b/activemodel/test/cases/serializers/json_serialization_test.rb index 60414a6570..734656b749 100644 --- a/activemodel/test/cases/serializers/json_serialization_test.rb +++ b/activemodel/test/cases/serializers/json_serialization_test.rb @@ -1,6 +1,5 @@ require 'cases/helper' require 'models/contact' -require 'models/automobile' require 'active_support/core_ext/object/instance_variables' class Contact diff --git a/activemodel/test/cases/validations/absence_validation_test.rb b/activemodel/test/cases/validations/absence_validation_test.rb index 795ce16d28..ebfe1cf4e4 100644 --- a/activemodel/test/cases/validations/absence_validation_test.rb +++ b/activemodel/test/cases/validations/absence_validation_test.rb @@ -11,7 +11,7 @@ class AbsenceValidationTest < ActiveModel::TestCase CustomReader.clear_validators! end - def test_validate_absences + def test_validates_absence_of Topic.validates_absence_of(:title, :content) t = Topic.new t.title = "foo" @@ -23,11 +23,12 @@ class AbsenceValidationTest < ActiveModel::TestCase t.content = "something" assert t.invalid? assert_equal ["must be blank"], t.errors[:content] + assert_equal [], t.errors[:title] t.content = "" assert t.valid? end - def test_accepts_array_arguments + def test_validates_absence_of_with_array_arguments Topic.validates_absence_of %w(title content) t = Topic.new t.title = "foo" @@ -37,7 +38,7 @@ class AbsenceValidationTest < ActiveModel::TestCase assert_equal ["must be blank"], t.errors[:content] end - def test_validates_acceptance_of_with_custom_error_using_quotes + def test_validates_absence_of_with_custom_error_using_quotes Person.validates_absence_of :karma, message: "This string contains 'single' and \"double\" quotes" p = Person.new p.karma = "good" diff --git a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb index 93600c587a..3eeb80a48b 100644 --- a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb @@ -72,28 +72,40 @@ class I18nGenerateMessageValidationTest < ActiveModel::TestCase end # validates_length_of: generate_message(attr, :too_long, message: custom_message, count: option_value.end) - def test_generate_message_too_long_with_default_message + def test_generate_message_too_long_with_default_message_plural assert_equal "is too long (maximum is 10 characters)", @person.errors.generate_message(:title, :too_long, count: 10) end + def test_generate_message_too_long_with_default_message_singular + assert_equal "is too long (maximum is 1 character)", @person.errors.generate_message(:title, :too_long, count: 1) + end + def test_generate_message_too_long_with_custom_message assert_equal 'custom message 10', @person.errors.generate_message(:title, :too_long, message: 'custom message %{count}', count: 10) end # validates_length_of: generate_message(attr, :too_short, default: custom_message, count: option_value.begin) - def test_generate_message_too_short_with_default_message + def test_generate_message_too_short_with_default_message_plural assert_equal "is too short (minimum is 10 characters)", @person.errors.generate_message(:title, :too_short, count: 10) end + def test_generate_message_too_short_with_default_message_singular + assert_equal "is too short (minimum is 1 character)", @person.errors.generate_message(:title, :too_short, count: 1) + end + def test_generate_message_too_short_with_custom_message assert_equal 'custom message 10', @person.errors.generate_message(:title, :too_short, message: 'custom message %{count}', count: 10) end # validates_length_of: generate_message(attr, :wrong_length, message: custom_message, count: option_value) - def test_generate_message_wrong_length_with_default_message + def test_generate_message_wrong_length_with_default_message_plural assert_equal "is the wrong length (should be 10 characters)", @person.errors.generate_message(:title, :wrong_length, count: 10) end + def test_generate_message_wrong_length_with_default_message_singular + assert_equal "is the wrong length (should be 1 character)", @person.errors.generate_message(:title, :wrong_length, count: 1) + end + def test_generate_message_wrong_length_with_custom_message assert_equal 'custom message 10', @person.errors.generate_message(:title, :wrong_length, message: 'custom message %{count}', count: 10) end diff --git a/activemodel/test/cases/validations/numericality_validation_test.rb b/activemodel/test/cases/validations/numericality_validation_test.rb index e1657407cf..3834d327ea 100644 --- a/activemodel/test/cases/validations/numericality_validation_test.rb +++ b/activemodel/test/cases/validations/numericality_validation_test.rb @@ -50,6 +50,21 @@ class NumericalityValidationTest < ActiveModel::TestCase valid!(NIL + INTEGERS) end + def test_validates_numericality_of_with_integer_only_and_symbol_as_value + Topic.validates_numericality_of :approved, only_integer: :condition_is_true_but_its_not + + invalid!(NIL + BLANK + JUNK) + valid!(FLOATS + INTEGERS + BIGDECIMAL + INFINITY) + end + + def test_validates_numericality_of_with_integer_only_and_proc_as_value + Topic.send(:define_method, :allow_only_integers?, lambda { false }) + Topic.validates_numericality_of :approved, only_integer: Proc.new {|topic| topic.allow_only_integers? } + + invalid!(NIL + BLANK + JUNK) + valid!(FLOATS + INTEGERS + BIGDECIMAL + INFINITY) + end + def test_validates_numericality_with_greater_than Topic.validates_numericality_of :approved, greater_than: 10 diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb index 6a74ee353d..38e54b7f17 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -167,6 +167,13 @@ class ValidationsTest < ActiveModel::TestCase end end + def test_invalid_options_to_validate + assert_raises(ArgumentError) do + # A common mistake -- we meant to call 'validates' + Topic.validate :title, presence: true + end + end + def test_errors_conversions Topic.validates_presence_of %w(title content) t = Topic.new @@ -378,25 +385,4 @@ class ValidationsTest < ActiveModel::TestCase assert topic.invalid? assert duped.valid? end - - # validator test: - def test_setup_is_deprecated_but_still_receives_klass # TODO: remove me in 4.2. - validator_class = Class.new(ActiveModel::Validator) do - def setup(klass) - @old_klass = klass - end - - def validate(*) - @old_klass == Topic or raise "#setup didn't work" - end - end - - assert_deprecated do - Topic.validates_with validator_class - end - - t = Topic.new - t.valid? - end - end diff --git a/activemodel/test/models/user.rb b/activemodel/test/models/user.rb index cbe259b1ad..1ec6001c48 100644 --- a/activemodel/test/models/user.rb +++ b/activemodel/test/models/user.rb @@ -1,6 +1,5 @@ class User extend ActiveModel::Callbacks - include ActiveModel::Validations include ActiveModel::SecurePassword define_model_callbacks :create diff --git a/activemodel/test/models/visitor.rb b/activemodel/test/models/visitor.rb index 4d7f4be097..22ad1a3c3d 100644 --- a/activemodel/test/models/visitor.rb +++ b/activemodel/test/models/visitor.rb @@ -1,6 +1,5 @@ class Visitor extend ActiveModel::Callbacks - include ActiveModel::Validations include ActiveModel::SecurePassword define_model_callbacks :create diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 7568773aad..b679d64472 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,4 +1,621 @@ -* Give ActiveRecord::PredicateBuilder private methods the privacy they deserve +* No verbose backtrace by db:drop when database does not exist. + + Fixes #16295. + + *Kenn Ejima* + +* Add support for Postgresql JSONB. + + Example: + + create_table :posts do |t| + t.jsonb :meta_data + end + + *Philippe Creux*, *Chris Teague* + +* `db:purge` with MySQL respects `Rails.env`. + + *Yves Senn* + +* `change_column_default :table, :column, nil` with PostgreSQL will issue a + `DROP DEFAULT` instead of a `DEFAULT NULL` query. + + Fixes #16261. + + *Matthew Draper*, *Yves Senn* + +* Allow to specify a type for the foreign key column in `references` + and `add_reference`. + + Example: + + change_table :vehicle do |t| + t.references :station, type: :uuid + end + + *Andrey Novikov*, *Łukasz Sarnacki* + +* `create_join_table` removes a common prefix when generating the join table. + This matches the existing behavior of HABTM associations. + + Fixes #13683. + + *Stefan Kanev* + +* Dont swallow errors on compute_type when having a bad alias_method on + a class. + + *arthurnn* + +* PostgreSQL invalid `uuid` are convert to nil. + + *Abdelkader Boudih* + +* Restore 4.0 behavior for using serialize attributes with `JSON` as coder. + + With 4.1.x, `serialize` started returning a string when `JSON` was passed as + the second attribute. It will now return a hash as per previous versions. + + Example: + + class Post < ActiveRecord::Base + serialize :comment, JSON + end + + class Comment + include ActiveModel::Model + attr_accessor :category, :text + end + + post = Post.create! + post.comment = Comment.new(category: "Animals", text: "This is a comment about squirrels.") + post.save! + + # 4.0 + post.comment # => {"category"=>"Animals", "text"=>"This is a comment about squirrels."} + + # 4.1 before + post.comment # => "#<Comment:0x007f80ab48ff98>" + + # 4.1 after + post.comment # => {"category"=>"Animals", "text"=>"This is a comment about squirrels."} + + When using `JSON` as the coder in `serialize`, Active Record will use the + new `ActiveRecord::Coders::JSON` coder which delegates its `dump/load` to + `ActiveSupport::JSON.encode/decode`. This ensures special objects are dumped + correctly using the `#as_json` hook. + + To keep the previous behaviour, supply a custom coder instead + ([example](https://gist.github.com/jenncoop/8c4142bbe59da77daa63)). + + Fixes #15594. + + *Jenn Cooper* + +* Do not use `RENAME INDEX` syntax for MariaDB 10.0. + + Fixes #15931. + + *Jeff Browning* + +* Calling `#empty?` on a `has_many` association would use the value from the + counter cache if one exists. + + *David Verhasselt* + +* Fix the schema dump generated for tables without constraints and with + primary key with default value of custom PostgreSQL function result. + + Fixes #16111. + + *Andrey Novikov* + +* Fix the SQL generated when a `delete_all` is run on an association to not + produce an `IN` statements. + + Before: + + UPDATE "categorizations" SET "category_id" = NULL WHERE + "categorizations"."category_id" = 1 AND "categorizations"."id" IN (1, 2) + + After: + + UPDATE "categorizations" SET "category_id" = NULL WHERE + "categorizations"."category_id" = 1 + + *Eileen M. Uchitelle, Aaron Patterson* + +* Avoid type casting boolean and ActiveSupport::Duration values to numeric + values for string columns. Otherwise, in some database, the string column + values will be coerced to a numeric allowing false or 0.seconds match any + string starting with a non-digit. + + Example: + + App.where(apikey: false) # => SELECT * FROM users WHERE apikey = '0' + + *Dylan Thacker-Smith* + +* Add a `:required` option to singular associations, providing a nicer + API for presence validations on associations. + + *Sean Griffin* + +* Fixed error in `reset_counters` when associations have `select` scope. + (Call to `count` generates invalid SQL.) + + *Cade Truitt* + +* After a successful `reload`, `new_record?` is always false. + + Fixes #12101. + + *Matthew Draper* + +* PostgreSQL renaming table doesn't attempt to rename non existent sequences. + + *Abdelkader Boudih* + +* Move 'dependent: :destroy' handling for 'belongs_to' + from 'before_destroy' to 'after_destroy' callback chain + + Fixes #12380. + + *Ivan Antropov* + +* Detect in-place modifications on String attributes. + + Before this change user have to mark the attribute as changed to it be persisted + in the database. Now it is not required anymore. + + Before: + + user = User.first + user.name << ' Griffin' + user.name_will_change! + user.save + user.reload.name # => "Sean Griffin" + + After: + + user = User.first + user.name << ' Griffin' + user.save + user.reload.name # => "Sean Griffin" + + *Sean Griffin* + +* Add `ActiveRecord::Base#validate!` that raises `RecordInvalid` if the record + is invalid. + + *Bogdan Gusiev*, *Marc Schütz* + +* Support for adding and removing foreign keys. Foreign keys are now + a part of `schema.rb`. This is supported by Mysql2Adapter, MysqlAdapter + and PostgreSQLAdapter. + + Many thanks to *Matthew Higgins* for laying the foundation with his work on + [foreigner](https://github.com/matthuhiggins/foreigner). + + Example: + + # within your migrations: + add_foreign_key :articles, :authors + remove_foreign_key :articles, :authors + + *Yves Senn* + +* Fix subtle bugs regarding attribute assignment on models with no primary + key. `'id'` will no longer be part of the attributes hash. + + *Sean Griffin* + +* Deprecate automatic counter caches on `has_many :through`. The behavior was + broken and inconsistent. + + *Sean Griffin* + +* `preload` preserves readonly flag for associations. + + See #15853. + + *Yves Senn* + +* Assume numeric types have changed if they were assigned to a value that + would fail numericality validation, regardless of the old value. Previously + this would only occur if the old value was 0. + + Example: + + model = Model.create!(number: 5) + model.number = '5wibble' + model.number_changed? # => true + + Fixes #14731. + + *Sean Griffin* + +* `reload` no longer merges with the existing attributes. + The attribute hash is fully replaced. The record is put into the same state + as it would be with `Model.find(model.id)`. + + *Sean Griffin* + +* The object returned from `select_all` must respond to `column_types`. + If this is not the case a `NoMethodError` is raised. + + *Sean Griffin* + +* `has_many :through` associations will no longer save the through record + twice when added in an `after_create` callback defined before the + associations. + + Fixes #3798. + + *Sean Griffin* + +* Detect in-place modifications of PG array types + + *Sean Griffin* + +* Add `bin/rake db:purge` task to empty the current database. + + *Yves Senn* + +* Deprecate `serialized_attributes` without replacement. + + *Sean Griffin* + +* Correctly extract IPv6 addresses from `DATABASE_URI`: the square brackets + are part of the URI structure, not the actual host. + + Fixes #15705. + + *Andy Bakun*, *Aaron Stone* + +* Ensure both parent IDs are set on join records when both sides of a + through association are new. + + *Sean Griffin* + +* `ActiveRecord::Dirty` now detects in-place changes to mutable values. + Serialized attributes on ActiveRecord models will no longer save when + unchanged. + + Fixes #8328. + + *Sean Griffin* + +* Pluck now works when selecting columns from different tables with the same + name. + + Fixes #15649. + + *Sean Griffin* + +* Remove `cache_attributes` and friends. All attributes are cached. + + *Sean Griffin* + +* Remove deprecated method `ActiveRecord::Base.quoted_locking_column`. + + *Akshay Vishnoi* + +* `ActiveRecord::FinderMethods.find` with block can handle proc parameter as + `Enumerable#find` does. + + Fixes #15382. + + *James Yang* + +* Make timezone aware attributes work with PostgreSQL array columns. + + Fixes #13402. + + *Kuldeep Aggarwal*, *Sean Griffin* + +* `ActiveRecord::SchemaMigration` has no primary key regardless of the + `primary_key_prefix_type` configuration. + + Fixes #15051. + + *JoseLuis Torres*, *Yves Senn* + +* `rake db:migrate:status` works with legacy migration numbers like `00018_xyz.rb`. + + Fixes #15538. + + *Yves Senn* + +* Baseclass becomes! subclass. + + Before this change, a record which changed its STI type, could not be + updated. + + Fixes #14785. + + *Matthew Draper*, *Earl St Sauver*, *Edo Balvers* + +* Remove deprecated `ActiveRecord::Migrator.proper_table_name`. Use the + `proper_table_name` instance method on `ActiveRecord::Migration` instead. + + *Akshay Vishnoi* + +* Fix regression on eager loading association based on SQL query rather than + existing column. + + Fixes #15480. + + *Lauro Caetano*, *Carlos Antonio da Silva* + +* Deprecate returning `nil` from `column_for_attribute` when no column exists. + It will return a null object in Rails 5.0 + + *Sean Griffin* + +* Implemented ActiveRecord::Base#pretty_print to work with PP. + + *Ethan* + +* Preserve type when dumping PostgreSQL point, bit, bit varying and money + columns. + + *Yves Senn* + +* New records remain new after YAML serialization. + + *Sean Griffin* + +* PostgreSQL support default values for enum types. Fixes #7814. + + *Yves Senn* + +* PostgreSQL `default_sequence_name` respects schema. Fixes #7516. + + *Yves Senn* + +* Fixed `columns_for_distinct` of postgresql adapter to work correctly + with orders without sort direction modifiers. + + *Nikolay Kondratyev* + +* PostgreSQL `reset_pk_sequence!` respects schemas. Fixes #14719. + + *Yves Senn* + +* Keep PostgreSQL `hstore` and `json` attributes as `Hash` in `@attributes`. + Fixes duplication in combination with `store_accessor`. + + Fixes #15369. + + *Yves Senn* + +* `rake railties:install:migrations` respects the order of railties. + + *Arun Agrawal* + +* Fix redefine a has_and_belongs_to_many inside inherited class + Fixing regression case, where redefining the same has_an_belongs_to_many + definition into a subclass would raise. + + Fixes #14983. + + *arthurnn* + +* Fix has_and_belongs_to_many public reflection. + When defining a has_and_belongs_to_many, internally we convert that to two has_many. + But as `reflections` is a public API, people expect to see the right macro. + + Fixes #14682. + + *arthurnn* + +* Fixed serialization for records with an attribute named `format`. + + Fixes #15188. + + *Godfrey Chan* + +* When a `group` is set, `sum`, `size`, `average`, `minimum` and `maximum` + on a NullRelation should return a Hash. + + *Kuldeep Aggarwal* + +* Fixed serialized fields returning serialized data after being updated with + `update_column`. + + *Simon Hørup Eskildsen* + +* Fixed polymorphic eager loading when using a String as foreign key. + + Fixes #14734. + + *Lauro Caetano* + +* Change belongs_to touch to be consistent with timestamp updates + + If a model is set up with a belongs_to: touch relationship the parent + record will only be touched if the record was modified. This makes it + consistent with timestamp updating on the record itself. + + *Brock Trappitt* + +* Fixed the inferred table name of a has_and_belongs_to_many auxiliar + table inside a schema. + + Fixes #14824. + + *Eric Chahin* + +* Remove unused `:timestamp` type. Transparently alias it to `:datetime` + in all cases. Fixes inconsistencies when column types are sent outside of + `ActiveRecord`, such as for XML Serialization. + + *Sean Griffin* + +* Fix bug that added `table_name_prefix` and `table_name_suffix` to + extension names in PostgreSQL when migrating. + + *Joao Carlos* + +* The `:index` option in migrations, which previously was only available for + `references`, now works with any column types. + + *Marc Schütz* + +* Add support for counter name to be passed as parameter on `CounterCache::ClassMethods#reset_counters`. + + *jnormore* + +* Restrict deletion of record when using `delete_all` with `uniq`, `group`, `having` + or `offset`. + + In these cases the generated query ignored them and that caused unintended + records to be deleted. + + Fixes #11985. + + *Leandro Facchinetti* + +* Floats with limit >= 25 that get turned into doubles in MySQL no longer have + their limit dropped from the schema. + + Fixes #14135. + + *Aaron Nelson* + +* Fix how to calculate associated class name when using namespaced has_and_belongs_to_many + association. + + Fixes #14709. + + *Kassio Borges* + +* `ActiveRecord::Relation::Merger#filter_binds` now compares equivalent symbols and + strings in column names as equal. + + This fixes a rare case in which more bind values are passed than there are + placeholders for them in the generated SQL statement, which can make PostgreSQL + throw a `StatementInvalid` exception. + + *Nat Budin* + +* Fix `stored_attributes` to correctly merge the details of stored + attributes defined in parent classes. + + Fixes #14672. + + *Brad Bennett*, *Jessica Yao*, *Lakshmi Parthasarathy* + +* `change_column_default` allows `[]` as argument to `change_column_default`. + + Fixes #11586. + + *Yves Senn* + +* Handle `name` and `"char"` column types in the PostgreSQL adapter. + + `name` and `"char"` are special character types used internally by + PostgreSQL and are used by internal system catalogs. These field types + can sometimes show up in structure-sniffing queries that feature internal system + structures or with certain PostgreSQL extensions. + + *J Smith*, *Yves Senn* + +* Fix `PostgreSQLAdapter::OID::Float#type_cast` to convert Infinity and + NaN PostgreSQL values into a native Ruby `Float::INFINITY` and `Float::NAN` + + Before: + + Point.create(value: 1.0/0) + Point.last.value # => 0.0 + + After: + + Point.create(value: 1.0/0) + Point.last.value # => Infinity + + *Innokenty Mikhailov* + +* Allow the PostgreSQL adapter to handle bigserial primary key types again. + + Fixes #10410. + + *Patrick Robertson* + +* Deprecate joining, eager loading and preloading of instance dependent + associations without replacement. These operations happen before instances + are created. The current behavior is unexpected and can result in broken + behavior. + + Fixes #15024. + + *Yves Senn* + +* Fixed has_and_belongs_to_many's CollectionAssociation size calculation. + + has_and_belongs_to_many should fall back to using the normal CollectionAssociation's + size calculation if the collection is not cached or loaded. + + Fixes #14913, #14914. + + *Fred Wu* + +* Return a non zero status when running `rake db:migrate:status` and migration table does + not exist. + + *Paul B.* + +* Add support for module-level `table_name_suffix` in models. + + This makes `table_name_suffix` work the same way as `table_name_prefix` when + using namespaced models. + + *Jenner LaFave* + +* Revert the behaviour of `ActiveRecord::Relation#join` changed through 4.0 => 4.1 to 4.0. + + In 4.1.0 `Relation#join` is delegated to `Arel#SelectManager`. + In 4.0 series it is delegated to `Array#join`. + + *Bogdan Gusiev* + +* Log nil binary column values correctly. + + When an object with a binary column is updated with a nil value + in that column, the SQL logger would throw an exception when trying + to log that nil value. This only occurs when updating a record + that already has a non-nil value in that column since an initial nil + value isn't included in the SQL anyway (at least, when dirty checking + is enabled.) The column's new value will now be logged as `<NULL binary data>` + to parallel the existing `<N bytes of binary data>` for non-nil values. + + *James Coleman* + +* Rails will now pass a custom validation context through to autosave associations + in order to validate child associations with the same context. + + Fixes #13854. + + *Eric Chahin*, *Aaron Nelson*, *Kevin Casey* + +* Stringify all variables keys of MySQL connection configuration. + + When `sql_mode` variable for MySQL adapters set in configuration as `String` + was ignored and overwritten by strict mode option. + + Fixes #14895. + + *Paul Nikitochkin* + +* Ensure SQLite3 statements are closed on errors. + + Fixes #13631. + + *Timur Alperovich* + +* Give ActiveRecord::PredicateBuilder private methods the privacy they deserve. *Hector Satre* @@ -66,7 +683,7 @@ *Eric Chahin* -* `sanitize_sql_like` helper method to escape a string for safe use in a SQL +* `sanitize_sql_like` helper method to escape a string for safe use in an SQL LIKE statement. Example: @@ -102,7 +719,7 @@ *Lauro Caetano* * Calling `delete_all` on an unloaded `CollectionProxy` no longer - generates a SQL statement containing each id of the collection: + generates an SQL statement containing each id of the collection: Before: @@ -143,7 +760,7 @@ * Auto-generate stable fixture UUIDs on PostgreSQL. - Fixes: #11524 + Fixes #11524. *Roderick van Domburg* @@ -227,10 +844,6 @@ *Luke Steensen* -* Make possible to change `record_timestamps` inside Callbacks. - - *Tieg Zaharia* - * Fixed error where .persisted? throws SystemStackError for an unsaved model with a custom primary key that didn't save due to validation error. @@ -261,12 +874,6 @@ *Cody Cutrer*, *Steve Rice*, *Rafael Mendonça Franca* -* Save `has_one` association even if the record doesn't changed. - - Fixes #14407. - - *Rafael Mendonça França* - * Use singular table name in generated migrations when `ActiveRecord::Base.pluralize_table_names` is `false`. @@ -326,12 +933,11 @@ *arthurnn* -* Passing an Active Record object to `find` is now deprecated. Call `.id` - on the object first. - * Passing an Active Record object to `find` or `exists?` is now deprecated. Call `.id` on the object first. + *Aaron Patterson* + * Only use BINARY for MySQL case sensitive uniqueness check when column has a case insensitive collation. *Ryuta Kamizono* @@ -345,6 +951,12 @@ *Troy Kruthoff*, *Lachlan Sylvester* +* Only save has_one associations if record has changes. + Previously after save related callbacks, such as `#after_commit`, were triggered when the has_one + object did not get saved to the db. + + *Alan Kennedy* + * Allow strings to specify the `#order` value. Example: @@ -374,7 +986,7 @@ *Vilius Luneckas* *Ahmed AbouElhamayed* * `before_add` callbacks are fired before the record is saved on - `has_and_belongs_to_many` assocations *and* on `has_many :through` + `has_and_belongs_to_many` associations *and* on `has_many :through` associations. Before this change, `before_add` callbacks would be fired before the record was saved on `has_and_belongs_to_many` associations, but *not* on `has_many :through` associations. diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc index e04abe9b37..f4777919d3 100644 --- a/activerecord/README.rdoc +++ b/activerecord/README.rdoc @@ -1,4 +1,4 @@ -= Active Record -- Object-relational mapping put on rails += Active Record -- Object-relational mapping in Rails Active Record connects classes to relational database tables to establish an almost zero-configuration persistence layer for applications. The library @@ -20,8 +20,10 @@ A short rundown of some of the major features: class Product < ActiveRecord::Base end - The Product class is automatically mapped to the table named "products", - which might look like this: + {Learn more}[link:classes/ActiveRecord/Base.html] + +The Product class is automatically mapped to the table named "products", +which might look like this: CREATE TABLE products ( id int(11) NOT NULL auto_increment, @@ -29,10 +31,8 @@ A short rundown of some of the major features: PRIMARY KEY (id) ); - This would also define the following accessors: `Product#name` and - `Product#name=(new_name)` - - {Learn more}[link:classes/ActiveRecord/Base.html] +This would also define the following accessors: `Product#name` and +`Product#name=(new_name)`. * Associations between objects defined by simple class methods. @@ -130,7 +130,7 @@ A short rundown of some of the major features: SQLite3[link:classes/ActiveRecord/ConnectionAdapters/SQLite3Adapter.html]. -* Logging support for Log4r[http://log4r.rubyforge.org] and Logger[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc]. +* Logging support for Log4r[https://github.com/colbygk/log4r] and Logger[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc]. ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT) ActiveRecord::Base.logger = Log4r::Logger.new('Application Log') @@ -208,6 +208,11 @@ API documentation is at: * http://api.rubyonrails.org -Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here: +Bug reports can be filed for the Ruby on Rails project here: * https://github.com/rails/rails/issues + +Feature requests should be discussed on the rails-core mailing list here: + +* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core + diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 6f8948f987..7769966a22 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -38,33 +38,36 @@ namespace :test do end end +desc 'Build MySQL and PostgreSQL test databases' namespace :db do - 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'] + task :create => ['db:mysql:build', 'db:postgresql:build'] + task :drop => ['db:mysql:drop', 'db:postgresql:drop'] end -%w( mysql mysql2 postgresql sqlite3 sqlite3_mem firebird db2 oracle sybase openbase frontbase jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ).each do |adapter| - Rake::TestTask.new("test_#{adapter}") { |t| - adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/] - t.libs << 'test' - t.test_files = (Dir.glob( "test/cases/**/*_test.rb" ).reject { - |x| x =~ /\/adapters\// - } + Dir.glob("test/cases/adapters/#{adapter_short}/**/*_test.rb")).sort - - t.warning = true - t.verbose = true - } - - task "isolated_test_#{adapter}" do - adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/] - puts [adapter, adapter_short].inspect - (Dir["test/cases/**/*_test.rb"].reject { - |x| x =~ /\/adapters\// - } + Dir["test/cases/adapters/#{adapter_short}/**/*_test.rb"]).all? do |file| - sh(Gem.ruby, '-w' ,"-Itest", file) - end or raise "Failures" +%w( mysql mysql2 postgresql sqlite3 sqlite3_mem db2 oracle jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ).each do |adapter| + namespace :test do + Rake::TestTask.new(adapter => "#{adapter}:env") { |t| + adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/] + t.libs << 'test' + t.test_files = (Dir.glob( "test/cases/**/*_test.rb" ).reject { + |x| x =~ /\/adapters\// + } + Dir.glob("test/cases/adapters/#{adapter_short}/**/*_test.rb")).sort + + t.warning = true + t.verbose = true + } + + namespace :isolated do + task adapter => "#{adapter}:env" do + adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/] + puts [adapter, adapter_short].inspect + (Dir["test/cases/**/*_test.rb"].reject { + |x| x =~ /\/adapters\// + } + Dir["test/cases/adapters/#{adapter_short}/**/*_test.rb"]).all? do |file| + sh(Gem.ruby, '-w' ,"-Itest", file) + end or raise "Failures" + end + end end namespace adapter do @@ -76,8 +79,8 @@ end end # Make sure the adapter test evaluates the env setting task - task "test_#{adapter}" => "#{adapter}:env" - task "isolated_test_#{adapter}" => "#{adapter}:env" + task "test_#{adapter}" => ["#{adapter}:env", "test:#{adapter}"] + task "isolated_test_#{adapter}" => ["#{adapter}:env", "test:isolated:#{adapter}"] end rule '.sqlite3' do |t| @@ -89,109 +92,58 @@ task :test_sqlite3 => [ 'test/fixtures/fixture_database_2.sqlite3' ] -namespace :mysql do - desc 'Build the MySQL test databases' - task :build_databases do - config = ARTest.config['connections']['mysql'] - %x( mysql --user=#{config['arunit']['username']} -e "create DATABASE #{config['arunit']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ") - %x( mysql --user=#{config['arunit2']['username']} -e "create DATABASE #{config['arunit2']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ") - end - - desc 'Drop the MySQL test databases' - task :drop_databases do - config = ARTest.config['connections']['mysql'] - %x( mysqladmin --user=#{config['arunit']['username']} -f drop #{config['arunit']['database']} ) - %x( mysqladmin --user=#{config['arunit2']['username']} -f drop #{config['arunit2']['database']} ) - end - - desc 'Rebuild the MySQL test databases' - task :rebuild_databases => [:drop_databases, :build_databases] -end - -task :build_mysql_databases => 'mysql:build_databases' -task :drop_mysql_databases => 'mysql:drop_databases' -task :rebuild_mysql_databases => 'mysql:rebuild_databases' - - -namespace :postgresql do - desc 'Build the PostgreSQL test databases' - task :build_databases do - config = ARTest.config['connections']['postgresql'] - %x( createdb -E UTF8 -T template0 #{config['arunit']['database']} ) - %x( createdb -E UTF8 -T template0 #{config['arunit2']['database']} ) +namespace :db do + namespace :mysql do + desc 'Build the MySQL test databases' + task :build do + config = ARTest.config['connections']['mysql'] + %x( mysql --user=#{config['arunit']['username']} -e "create DATABASE #{config['arunit']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ") + %x( mysql --user=#{config['arunit2']['username']} -e "create DATABASE #{config['arunit2']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ") + end - # notify about preparing hstore - if %x( createdb --version ).strip.gsub(/(.*)(\d\.\d\.\d)$/, "\\2") < "9.1.0" - puts "Please prepare hstore data type. See http://www.postgresql.org/docs/9.0/static/hstore.html" + desc 'Drop the MySQL test databases' + task :drop do + config = ARTest.config['connections']['mysql'] + %x( mysqladmin --user=#{config['arunit']['username']} -f drop #{config['arunit']['database']} ) + %x( mysqladmin --user=#{config['arunit2']['username']} -f drop #{config['arunit2']['database']} ) end - end - desc 'Drop the PostgreSQL test databases' - task :drop_databases do - config = ARTest.config['connections']['postgresql'] - %x( dropdb #{config['arunit']['database']} ) - %x( dropdb #{config['arunit2']['database']} ) + desc 'Rebuild the MySQL test databases' + task :rebuild => [:drop, :build] end - desc 'Rebuild the PostgreSQL test databases' - task :rebuild_databases => [:drop_databases, :build_databases] -end - -task :build_postgresql_databases => 'postgresql:build_databases' -task :drop_postgresql_databases => 'postgresql:drop_databases' -task :rebuild_postgresql_databases => 'postgresql:rebuild_databases' - - -namespace :frontbase do - desc 'Build the FrontBase test databases' - task :build_databases => :rebuild_frontbase_databases - - desc 'Rebuild the FrontBase test databases' - task :rebuild_databases do - build_frontbase_database = Proc.new do |db_name, sql_definition_file| - %( - STOP DATABASE #{db_name}; - DELETE DATABASE #{db_name}; - CREATE DATABASE #{db_name}; + namespace :postgresql do + desc 'Build the PostgreSQL test databases' + task :build do + config = ARTest.config['connections']['postgresql'] + %x( createdb -E UTF8 -T template0 #{config['arunit']['database']} ) + %x( createdb -E UTF8 -T template0 #{config['arunit2']['database']} ) - CONNECT TO #{db_name} AS SESSION_NAME USER _SYSTEM; - SET COMMIT FALSE; - - CREATE USER RAILS; - CREATE SCHEMA RAILS AUTHORIZATION RAILS; - COMMIT; - - SET SESSION AUTHORIZATION RAILS; - SCRIPT '#{sql_definition_file}'; - - COMMIT; - - DISCONNECT ALL; - ) + # prepare hstore + if %x( createdb --version ).strip.gsub(/(.*)(\d\.\d\.\d)$/, "\\2") < "9.1.0" + puts "Please prepare hstore data type. See http://www.postgresql.org/docs/9.0/static/hstore.html" + end end - config = ARTest.config['connections']['frontbase'] - create_activerecord_unittest = build_frontbase_database[config['arunit']['database'], File.join(SCHEMA_ROOT, 'frontbase.sql')] - create_activerecord_unittest2 = build_frontbase_database[config['arunit2']['database'], File.join(SCHEMA_ROOT, 'frontbase2.sql')] - execute_frontbase_sql = Proc.new do |sql| - system(<<-SHELL) - /Library/FrontBase/bin/sql92 <<-SQL - #{sql} - SQL - SHELL + + desc 'Drop the PostgreSQL test databases' + task :drop do + config = ARTest.config['connections']['postgresql'] + %x( dropdb #{config['arunit']['database']} ) + %x( dropdb #{config['arunit2']['database']} ) end - execute_frontbase_sql[create_activerecord_unittest] - execute_frontbase_sql[create_activerecord_unittest2] + + desc 'Rebuild the PostgreSQL test databases' + task :rebuild => [:drop, :build] end end -task :build_frontbase_databases => 'frontbase:build_databases' -task :rebuild_frontbase_databases => 'frontbase:rebuild_databases' +task :build_mysql_databases => 'db:mysql:build' +task :drop_mysql_databases => 'db:mysql:drop' +task :rebuild_mysql_databases => 'db:mysql:rebuild' -spec = eval(File.read('activerecord.gemspec')) - -Gem::PackageTask.new(spec) do |p| - p.gem_spec = spec -end +task :build_postgresql_databases => 'db:postgresql:build' +task :drop_postgresql_databases => 'db:postgresql:drop' +task :rebuild_postgresql_databases => 'db:postgresql:rebuild' task :lines do lines, codelines, total_lines, total_codelines = 0, 0, 0, 0 @@ -217,6 +169,11 @@ task :lines do puts "Total: Lines #{total_lines}, LOC #{total_codelines}" end +spec = eval(File.read('activerecord.gemspec')) + +Gem::PackageTask.new(spec) do |p| + p.gem_spec = spec +end # Publishing ------------------------------------------------------ diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index d397c9e016..8075008574 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', '~> 5.0.0' + s.add_dependency 'arel', '~> 6.0.0' end diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index f856c482d6..9028970a3d 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -27,10 +27,12 @@ require 'active_model' require 'arel' require 'active_record/version' +require 'active_record/attribute_set' module ActiveRecord extend ActiveSupport::Autoload + autoload :Attribute autoload :Base autoload :Callbacks autoload :Core @@ -95,6 +97,7 @@ module ActiveRecord module Coders autoload :YAMLColumn, 'active_record/coders/yaml_column' + autoload :JSON, 'active_record/coders/json' end module AttributeMethods diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 0d5313956b..e576ec4d40 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -129,10 +129,10 @@ module ActiveRecord # is an instance of the value class. Specifying a custom converter allows the new value to be automatically # converted to an instance of value class if necessary. # - # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that - # should be aggregated using the NetAddr::CIDR value class (http://netaddr.rubyforge.org). The constructor - # for the value class is called +create+ and it expects a CIDR address string as a parameter. New - # values can be assigned to the value object using either another NetAddr::CIDR object, a string + # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that should be + # aggregated using the NetAddr::CIDR value class (http://www.ruby-doc.org/gems/docs/n/netaddr-1.5.0/NetAddr/CIDR.html). + # The constructor for the value class is called +create+ and it expects a CIDR address string as a parameter. + # New values can be assigned to the value object using either another NetAddr::CIDR object, a string # or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet # these requirements: # @@ -230,8 +230,8 @@ module ActiveRecord private def reader_method(name, class_name, mapping, allow_nil, constructor) define_method(name) do - if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? }) - attrs = mapping.collect {|pair| read_attribute(pair.first)} + if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|key, _| !read_attribute(key).nil? }) + attrs = mapping.collect {|key, _| read_attribute(key)} object = constructor.respond_to?(:call) ? constructor.call(*attrs) : class_name.constantize.send(constructor, *attrs) @@ -244,15 +244,19 @@ module ActiveRecord def writer_method(name, class_name, mapping, allow_nil, converter) define_method("#{name}=") do |part| klass = class_name.constantize + if part.is_a?(Hash) + part = klass.new(*part.values) + end + unless part.is_a?(klass) || converter.nil? || part.nil? part = converter.respond_to?(:call) ? converter.call(part) : klass.send(converter, part) end if part.nil? && allow_nil - mapping.each { |pair| self[pair.first] = nil } + mapping.each { |key, _| self[key] = nil } @aggregation_cache[name] = nil else - mapping.each { |pair| self[pair.first] = part.send(pair.last) } + mapping.each { |key, value| self[key] = part.send(value) } @aggregation_cache[name] = part.freeze end end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 265bad7bc2..ec78d10124 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -50,7 +50,7 @@ module ActiveRecord def initialize(reflection) through_reflection = reflection.through_reflection source_reflection_names = reflection.source_reflection_names - source_associations = reflection.through_reflection.klass.reflect_on_all_associations.collect { |a| a.name.inspect } + source_associations = reflection.through_reflection.klass._reflections.keys super("Could not find the source association(s) #{source_reflection_names.collect{ |a| a.inspect }.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?") end end @@ -151,7 +151,7 @@ module ActiveRecord association = association_instance_get(name) if association.nil? - raise AssociationNotFoundError.new(self, name) unless reflection = self.class.reflect_on_association(name) + raise AssociationNotFoundError.new(self, name) unless reflection = self.class._reflect_on_association(name) association = reflection.association_class.new(self, reflection) association_instance_set(name, association) end @@ -202,12 +202,13 @@ module ActiveRecord # For instance, +attributes+ and +connection+ would be bad choices for association names. # # == Auto-generated methods + # See also Instance Public methods below for more details. # # === Singular associations (one-to-one) # | | belongs_to | # generated methods | belongs_to | :polymorphic | has_one # ----------------------------------+------------+--------------+--------- - # other | X | X | X + # other(force_reload=false) | X | X | X # other=(other) | X | X | X # build_other(attributes={}) | X | | X # create_other(attributes={}) | X | | X @@ -217,7 +218,7 @@ module ActiveRecord # | | | has_many # generated methods | habtm | has_many | :through # ----------------------------------+-------+----------+---------- - # others | X | X | X + # others(force_reload=false) | X | X | X # others=(other,other,...) | X | X | X # other_ids | X | X | X # other_ids=(id,id,...) | X | X | X @@ -419,6 +420,10 @@ module ActiveRecord # has_many :birthday_events, ->(user) { where starts_on: user.birthday }, class_name: 'Event' # end # + # Note: Joining, eager loading and preloading of these associations is not fully possible. + # These operations happen before instance creation and the scope will be called with a +nil+ argument. + # This can lead to unexpected behavior and is deprecated. + # # == Association callbacks # # Similar to the normal callbacks that hook into the life cycle of an Active Record object, @@ -712,9 +717,9 @@ module ActiveRecord # == Eager loading of associations # # Eager loading is a way to find objects of a certain class and a number of named associations. - # This is one of the easiest ways of to prevent the dreaded 1+N problem in which fetching 100 + # This is one of the easiest ways of to prevent the dreaded N+1 problem in which fetching 100 # posts that each need to display their author triggers 101 database queries. Through the - # use of eager loading, the 101 queries can be reduced to 2. + # use of eager loading, the number of queries will be reduced from 101 to 2. # # class Post < ActiveRecord::Base # belongs_to :author @@ -774,16 +779,15 @@ module ActiveRecord # In the above example posts with no approved comments are not returned at all, because # the conditions apply to the SQL statement as a whole and not just to the association. # + # You must disambiguate column references for this fallback to happen, for example + # <tt>order: "author.name DESC"</tt> will work but <tt>order: "name DESC"</tt> will not. + # # If you want to load all posts (including posts with no approved comments) then write # your own LEFT OUTER JOIN query using ON # - # Post.joins('LEFT OUTER JOIN comments ON comments.post_id = posts.id AND comments.approved = true') + # Post.joins("LEFT OUTER JOIN comments ON comments.post_id = posts.id AND comments.approved = '1'") # - # You must disambiguate column references for this fallback to happen, for example - # <tt>order: "author.name DESC"</tt> will work but <tt>order: "name DESC"</tt> will not. - # - # If you do want eager load only some members of an association it is usually more natural - # to include an association which has conditions defined on it: + # In this case it is usually more natural to include an association which has conditions defined on it: # # class Post < ActiveRecord::Base # has_many :approved_comments, -> { where approved: true }, class_name: 'Comment' @@ -1305,6 +1309,10 @@ module ActiveRecord # that is the inverse of this <tt>has_one</tt> association. Does not work in combination # with <tt>:through</tt> or <tt>:as</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. + # [:required] + # When set to +true+, the association will also have its presence validated. + # This will validate the association itself, not the id. You can use + # +:inverse_of+ to avoid an extra query during validation. # # Option examples: # has_one :credit_card, dependent: :destroy # destroys the associated credit card @@ -1316,6 +1324,7 @@ module ActiveRecord # has_one :boss, readonly: :true # has_one :club, through: :membership # has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable + # has_one :credit_card, required: true def has_one(name, scope = nil, options = {}) reflection = Builder::HasOne.build(self, name, scope, options) Reflection.add_reflection self, name, reflection @@ -1409,7 +1418,7 @@ module ActiveRecord # # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>. # [:touch] - # If true, the associated object will be touched (the updated_at/on attributes set to now) + # If true, the associated object will be touched (the updated_at/on attributes set to current time) # when this record is either saved or destroyed. If you specify a symbol, that attribute # will be updated with the current time in addition to the updated_at/on attribute. # [:inverse_of] @@ -1417,6 +1426,10 @@ module ActiveRecord # object that is the inverse of this <tt>belongs_to</tt> association. Does not work in # combination with the <tt>:polymorphic</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. + # [:required] + # When set to +true+, the association will also have its presence validated. + # This will validate the association itself, not the id. You can use + # +:inverse_of+ to avoid an extra query during validation. # # Option examples: # belongs_to :firm, foreign_key: "client_of" @@ -1429,6 +1442,7 @@ module ActiveRecord # belongs_to :post, counter_cache: true # belongs_to :company, touch: true # belongs_to :company, touch: :employees_last_updated_at + # belongs_to :company, required: true def belongs_to(name, scope = nil, options = {}) reflection = Builder::BelongsTo.build(self, name, scope, options) Reflection.add_reflection self, name, reflection @@ -1573,14 +1587,22 @@ module ActiveRecord scope = nil end + habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self) + builder = Builder::HasAndBelongsToMany.new name, self, options join_model = builder.through_model + # FIXME: we should move this to the internal constants. Also people + # should never directly access this constant so I'm not happy about + # setting it. + const_set join_model.name, join_model + middle_reflection = builder.middle_reflection join_model Builder::HasMany.define_callbacks self, middle_reflection Reflection.add_reflection self, middle_reflection.name, middle_reflection + middle_reflection.parent_reflection = [name.to_s, habtm_reflection] include Module.new { class_eval <<-RUBY, __FILE__, __LINE__ + 1 @@ -1601,6 +1623,7 @@ module ActiveRecord end has_many name, scope, hm_options, &extension + self._reflections[name.to_s].parent_reflection = [name.to_s, habtm_reflection] end end end diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 85109aee6c..a6a1947148 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -32,8 +32,18 @@ module ActiveRecord join.left.downcase.scan( /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/ ).size - else + elsif join.respond_to? :left join.left.table_name == name ? 1 : 0 + else + # this branch is reached by two tests: + # + # activerecord/test/cases/associations/cascaded_eager_loading_test.rb:37 + # with :posts + # + # activerecord/test/cases/associations/eager_test.rb:1133 + # with :comments + # + 0 end end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 9ad2d2fb12..f1c36cd047 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -160,7 +160,7 @@ module ActiveRecord def marshal_load(data) reflection_name, ivars = data ivars.each { |name, val| instance_variable_set(name, val) } - @reflection = @owner.class.reflect_on_association(reflection_name) + @reflection = @owner.class._reflect_on_association(reflection_name) end def initialize_attributes(record) #:nodoc: @@ -179,7 +179,7 @@ module ActiveRecord def creation_attributes attributes = {} - if (reflection.macro == :has_one || reflection.macro == :has_many) && !options[:through] + if (reflection.has_one? || reflection.collection?) && !options[:through] attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key] if reflection.options[:as] diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index f1a3b23d5a..519d4d8651 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -47,15 +47,8 @@ module ActiveRecord def self.get_bind_values(owner, chain) bvs = [] chain.each_with_index do |reflection, i| - if reflection.source_macro == :belongs_to - foreign_key = reflection.foreign_key - else - foreign_key = reflection.active_record_primary_key - end - if reflection == chain.last - bvs << owner[foreign_key] - + bvs << reflection.join_id_for(owner) if reflection.type bvs << owner.class.base_class.name end @@ -112,18 +105,9 @@ module ActiveRecord chain.each_with_index do |reflection, i| table, foreign_table = tables.shift, tables.first - if reflection.source_macro == :belongs_to - if reflection.options[:polymorphic] - key = reflection.association_primary_key(assoc_klass) - else - key = reflection.association_primary_key - end - - foreign_key = reflection.foreign_key - else - key = reflection.foreign_key - foreign_key = reflection.active_record_primary_key - end + join_keys = reflection.join_keys(assoc_klass) + key = join_keys.key + foreign_key = join_keys.foreign_key if reflection == chain.last bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key], tracker diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 1edd4fa3aa..81fdd681de 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -92,7 +92,7 @@ module ActiveRecord # has_one associations. def invertible_for?(record) inverse = inverse_reflection_for(record) - inverse && inverse.macro == :has_one + inverse && inverse.has_one? end def target_id diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index f085fd1cfd..947d61ee7b 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -36,6 +36,7 @@ module ActiveRecord::Associations::Builder reflection = builder.build(model) define_accessors model, reflection define_callbacks model, reflection + define_validations model, reflection builder.define_extensions model reflection end @@ -85,7 +86,11 @@ module ActiveRecord::Associations::Builder end def self.define_callbacks(model, reflection) - add_before_destroy_callbacks(model, reflection) if reflection.options[:dependent] + if dependent = reflection.options[:dependent] + check_dependent_options(dependent) + add_destroy_callbacks(model, reflection) + end + Association.extensions.each do |extension| extension.build model, reflection end @@ -120,17 +125,23 @@ module ActiveRecord::Associations::Builder CODE end + def self.define_validations(model, reflection) + # noop + end + def self.valid_dependent_options raise NotImplementedError end private - 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]}" + def self.check_dependent_options(dependent) + unless valid_dependent_options.include? dependent + raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{dependent}" end + end + def self.add_destroy_callbacks(model, reflection) name = reflection.name model.before_destroy lambda { |o| o.association(name).handle_dependency } end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 47cc1f4b34..954ea3878a 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -103,9 +103,14 @@ module ActiveRecord::Associations::Builder BelongsTo.touch_record(record, foreign_key, n, touch) } - model.after_save callback + model.after_save callback, if: :changed? model.after_touch callback model.after_destroy callback end + + def self.add_destroy_callbacks(model, reflection) + name = reflection.name + model.after_destroy lambda { |o| o.association(name).handle_dependency } + end end end 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 e472277374..34a555dfd4 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 @@ -11,11 +11,14 @@ module ActiveRecord::Associations::Builder end def join_table - @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_") + @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 + + def klass + @lhs_class.send(:compute_type, @rhs_class_name) + end end def self.build(lhs_class, name, options) @@ -60,13 +63,13 @@ module ActiveRecord::Associations::Builder def self.add_left_association(name, options) belongs_to name, options - self.left_reflection = reflect_on_association(name) + 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) + self.right_reflection = _reflect_on_association(rhs_name) end } diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index f359efd496..c194c8ae9a 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -5,7 +5,7 @@ module ActiveRecord::Associations::Builder end def valid_options - valid = super + [:order, :as] + valid = super + [:as] valid += [:through, :source, :source_type] if options[:through] valid end @@ -16,7 +16,7 @@ module ActiveRecord::Associations::Builder private - def self.add_before_destroy_callbacks(model, reflection) + def self.add_destroy_callbacks(model, reflection) super unless reflection.options[:through] 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 e655c389a6..6e6dd7204c 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -3,7 +3,7 @@ module ActiveRecord::Associations::Builder class SingularAssociation < Association #:nodoc: def valid_options - super + [:remote, :dependent, :primary_key, :inverse_of] + super + [:dependent, :primary_key, :inverse_of, :required] end def self.define_accessors(model, reflection) @@ -27,5 +27,12 @@ module ActiveRecord::Associations::Builder end CODE end + + def self.define_validations(model, reflection) + super + if reflection.options[:required] + model.validates_presence_of reflection.name + end + end end end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 1c84973920..065a2cff01 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -55,9 +55,9 @@ module ActiveRecord # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items def ids_writer(ids) - pk_column = reflection.primary_key_column + pk_type = reflection.primary_key_type ids = Array(ids).reject { |id| id.blank? } - ids.map! { |i| pk_column.type_cast(i) } + ids.map! { |i| pk_type.type_cast_from_user(i) } replace(klass.find(ids).index_by { |r| r.id }.values_at(*ids)) end @@ -194,7 +194,7 @@ module ActiveRecord options[:dependent] end - delete_all_with_dependency(dependent).tap do + delete_or_nullify_all_records(dependent).tap do reset loaded! end @@ -244,6 +244,7 @@ module ActiveRecord # are actually removed from the database, that depends precisely on # +delete_records+. They are in any case removed from the collection. def delete(*records) + return if records.empty? _options = records.extract_options! dependent = _options[:dependent] || options[:dependent] @@ -251,20 +252,13 @@ module ActiveRecord delete_or_destroy(records, dependent) end - def delete_all_with_dependency(dependent) - if dependent == :destroy - delete_or_destroy(load_target, dependent) - else - delete_records(:all, dependent) - end - end - # Deletes the +records+ and removes them from this association calling # +before_remove+ , +after_remove+ , +before_destroy+ and +after_destroy+ callbacks. # # Note that this method removes records from the database ignoring the # +:dependent+ option. def destroy(*records) + return if records.empty? records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) } delete_or_destroy(records, :destroy) end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index aac85a36c8..2a97d0ed31 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -41,6 +41,14 @@ module ActiveRecord end end + def empty? + if has_cached_counter? + size.zero? + else + super + end + end + private # Returns the number of records in this collection. @@ -80,9 +88,20 @@ module ActiveRecord end def update_counter(difference, reflection = reflection()) + update_counter_in_database(difference, reflection) + update_counter_in_memory(difference, reflection) + end + + def update_counter_in_database(difference, reflection = reflection()) if has_cached_counter?(reflection) counter = cached_counter_attribute_name(reflection) owner.class.update_counters(owner.id, counter => difference) + end + end + + def update_counter_in_memory(difference, reflection = reflection()) + if has_cached_counter?(reflection) + counter = cached_counter_attribute_name(reflection) owner[counter] += difference owner.changed_attributes.delete(counter) # eww end @@ -100,28 +119,37 @@ module ActiveRecord # Hence this method. def inverse_updates_counter_cache?(reflection = reflection()) counter_name = cached_counter_attribute_name(reflection) - reflection.klass.reflect_on_all_associations(:belongs_to).any? { |inverse_reflection| + inverse_updates_counter_named?(counter_name, reflection) + end + + def inverse_updates_counter_named?(counter_name, reflection = reflection()) + reflection.klass._reflections.values.any? { |inverse_reflection| + :belongs_to == inverse_reflection.macro && inverse_reflection.counter_cache_column == counter_name } end + def delete_count(method, scope) + if method == :delete_all + scope.delete_all + else + scope.update_all(reflection.foreign_key => nil) + end + end + + def delete_or_nullify_all_records(method) + count = delete_count(method, self.scope) + update_counter(-count) + end + # Deletes the records according to the <tt>:dependent</tt> option. def delete_records(records, method) if method == :destroy records.each(&:destroy!) update_counter(-records.length) unless inverse_updates_counter_cache? else - if records == :all || !reflection.klass.primary_key - scope = self.scope - else - scope = self.scope.where(reflection.klass.primary_key => records) - end - - if method == :delete_all - update_counter(-scope.delete_all) - else - update_counter(-scope.update_all(reflection.foreign_key => nil)) - end + scope = self.scope.where(reflection.klass.primary_key => records) + update_counter(-delete_count(method, scope)) end end @@ -132,6 +160,25 @@ module ActiveRecord false end end + + def concat_records(records, *) + update_counter_if_success(super, records.length) + end + + def _create_record(attributes, *) + if attributes.is_a?(Array) + super + else + update_counter_if_success(super, 1) + end + end + + def update_counter_if_success(saved_successfully, difference) + if saved_successfully + update_counter_in_memory(difference) + end + saved_successfully + 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 f3af8605cd..007e3bc555 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -12,17 +12,18 @@ module ActiveRecord @through_association = nil end - # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been - # loaded and calling collection.size if it has. If it's more likely than not that the collection does - # have a size larger than zero, and you need to fetch that collection afterwards, it'll take one fewer - # SELECT query if you use #length. + # Returns the size of the collection by executing a SELECT COUNT(*) query + # if the collection hasn't been loaded, and by calling collection.size if + # it has. If the collection will likely have a size greater than zero, + # and if fetching the collection will be needed afterwards, one less + # SELECT query will be generated by using #length instead. def size if has_cached_counter? owner.read_attribute cached_counter_attribute_name(reflection) elsif loaded? target.size else - count + super end end @@ -62,7 +63,15 @@ module ActiveRecord end save_through_record(record) - update_counter(1) + if has_cached_counter? && !through_reflection_updates_counter_cache? + ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) + Automatic updating of counter caches on through associations has been + deprecated, and will be removed in Rails 5.0. Instead, please set the + appropriate counter_cache options on the has_many and belongs_to for + your associations to #{through_reflection.name}. + MESSAGE + update_counter_in_database(1) + end record end @@ -72,25 +81,29 @@ module ActiveRecord @through_association ||= owner.association(through_reflection.name) end - # We temporarily cache through record that has been build, because if we build a - # through record in build_record and then subsequently call insert_record, then we - # want to use the exact same object. + # The through record (built with build_record) is temporarily cached + # so that it may be reused if insert_record is subsequently called. # - # However, after insert_record has been called, we clear the cache entry because - # we want it to be possible to have multiple instances of the same record in an - # association + # However, after insert_record has been called, the cache is cleared in + # order to allow multiple instances of the same record in an association. def build_through_record(record) @through_records[record.object_id] ||= begin ensure_mutable - through_record = through_association.build through_scope_attributes + through_record = through_association.build(*options_for_through_record) through_record.send("#{source_reflection.name}=", record) through_record end end + def options_for_through_record + [through_scope_attributes] + end + def through_scope_attributes - scope.where_values_hash(through_association.reflection.name.to_s) + scope.where_values_hash(through_association.reflection.name.to_s). + except!(through_association.reflection.foreign_key, + through_association.reflection.klass.inheritance_column) end def save_through_record(record) @@ -106,9 +119,9 @@ module ActiveRecord inverse = source_reflection.inverse_of if inverse - if inverse.macro == :has_many + if inverse.collection? record.send(inverse.name) << build_through_record(record) - elsif inverse.macro == :has_one + elsif inverse.has_one? record.send("#{inverse.name}=", build_through_record(record)) end end @@ -117,7 +130,7 @@ module ActiveRecord end def target_reflection_has_associated_record? - !(through_reflection.macro == :belongs_to && owner[through_reflection.foreign_key].blank?) + !(through_reflection.belongs_to? && owner[through_reflection.foreign_key].blank?) end def update_through_counter?(method) @@ -131,13 +144,13 @@ module ActiveRecord end end + def delete_or_nullify_all_records(method) + delete_records(load_target, method) + end + def delete_records(records, method) ensure_not_nested - # This is unoptimised; it will load all the target records - # even when we just want to delete everything. - records = load_target if records == :all - scope = through_association.scope scope.where! construct_join_attributes(*records) @@ -171,7 +184,7 @@ module ActiveRecord klass.decrement_counter counter, records.map(&:id) end - if through_reflection.macro == :has_many && update_through_counter?(method) + if through_reflection.collection? && update_through_counter?(method) update_counter(-count, through_reflection) end @@ -181,14 +194,18 @@ module ActiveRecord def through_records_for(record) attributes = construct_join_attributes(record) candidates = Array.wrap(through_association.target) - candidates.find_all { |c| c.attributes.slice(*attributes.keys) == attributes } + candidates.find_all do |c| + attributes.all? do |key, value| + c.public_send(key) == value + end + end end def delete_through_records(records) records.each do |record| through_records = through_records_for(record) - if through_reflection.macro == :has_many + if through_reflection.collection? through_records.each { |r| through_association.target.delete(r) } else if through_records.include?(through_association.target) @@ -209,6 +226,11 @@ module ActiveRecord def invertible_for?(record) false end + + def through_reflection_updates_counter_cache? + counter_name = cached_counter_attribute_name + inverse_updates_counter_named?(counter_name, through_reflection) + end end end end diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index b7dc037a65..ec5c189cd3 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -131,7 +131,6 @@ module ActiveRecord 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| @@ -144,8 +143,7 @@ module ActiveRecord column_aliases = aliases.column_aliases join_root result_set.each { |row_hash| - primary_id = type_caster.type_cast row_hash[primary_key] - parent = parents[primary_id] ||= join_root.instantiate(row_hash, column_aliases) + parent = parents[row_hash[primary_key]] ||= join_root.instantiate(row_hash, column_aliases) construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases) } @@ -207,7 +205,7 @@ module ActiveRecord end def find_reflection(klass, name) - klass.reflect_on_association(name) or + klass._reflect_on_association(name) or raise ConfigurationError, "Association named '#{ name }' was not found on #{ klass.name }; perhaps you misspelled it?" end @@ -215,8 +213,9 @@ module ActiveRecord associations.map do |name, right| reflection = find_reflection base_klass, name reflection.check_validity! + reflection.check_eager_loadable! - if reflection.options[:polymorphic] + if reflection.polymorphic? raise EagerLoadPolymorphicError.new(reflection) end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index a0e83c0a02..719eff9acc 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -95,7 +95,7 @@ module ActiveRecord # end # # If I execute `Physician.joins(:appointments).to_a` then - # reflection # => #<ActiveRecord::Reflection::AssociationReflection @macro=:has_many ...> + # reflection # => #<ActiveRecord::Reflection::HasManyReflection ...> # table # => #<Arel::Table @name="appointments" ...> # key # => physician_id # foreign_table # => #<Arel::Table @name="physicians" ...> diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 311684d886..7519fec10a 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -112,13 +112,14 @@ module ActiveRecord end def preloaders_for_hash(association, records, scope) - parent, child = association.to_a.first # hash should only be of length 1 + association.flat_map { |parent, child| + loaders = preloaders_for_one parent, records, scope - 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 + recs = loaders.flat_map(&:preloaded_records).uniq + loaders.concat Array.wrap(child).flat_map { |assoc| + preloaders_on assoc, recs, scope + } + loaders } end @@ -142,6 +143,7 @@ module ActiveRecord def grouped_records(association, records) h = {} records.each do |record| + next unless record assoc = record.association(association) klasses = h[assoc.reflection] ||= {} (klasses[assoc.klass] ||= []) << record @@ -175,6 +177,7 @@ module ActiveRecord if owners.first.association(reflection.name).loaded? return AlreadyLoaded end + reflection.check_preloadable! case reflection.macro when :has_many diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index bf461070e0..c0639742be 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -57,9 +57,15 @@ module ActiveRecord end def owners_by_key - @owners_by_key ||= owners.group_by do |owner| - owner[owner_key_name] - end + @owners_by_key ||= if key_conversion_required? + owners.group_by do |owner| + owner[owner_key_name].to_s + end + else + owners.group_by do |owner| + owner[owner_key_name] + end + end end def options @@ -93,13 +99,28 @@ module ActiveRecord records_by_owner end + def key_conversion_required? + association_key_type != owner_key_type + end + + def association_key_type + @klass.type_for_attribute(association_key_name.to_s).type + end + + def owner_key_type + @model.type_for_attribute(owner_key_name.to_s).type + end + def load_slices(slices) @preloaded_records = slices.flat_map { |slice| records_for(slice) } @preloaded_records.map { |record| - [record, record[association_key_name]] + key = record[association_key_name] + key = key.to_s if key_conversion_required? + + [record, key] } end @@ -130,6 +151,10 @@ module ActiveRecord end end + if preload_values[:readonly] || values[:readonly] + scope.readonly! + 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/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index f8a85b8a6f..f00fef8b9e 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -41,12 +41,16 @@ module ActiveRecord def construct_join_attributes(*records) ensure_mutable - join_attributes = { - source_reflection.foreign_key => - records.map { |record| - record.send(source_reflection.association_primary_key(reflection.klass)) - } - } + if source_reflection.association_primary_key(reflection.klass) == reflection.klass.primary_key + join_attributes = { source_reflection.name => records } + else + join_attributes = { + source_reflection.foreign_key => + records.map { |record| + record.send(source_reflection.association_primary_key(reflection.klass)) + } + } + end if options[:source_type] join_attributes[source_reflection.foreign_type] = @@ -63,14 +67,13 @@ module ActiveRecord # Note: this does not capture all cases, for example it would be crazy to try to # properly support stale-checking for nested associations. def stale_state - if through_reflection.macro == :belongs_to + if through_reflection.belongs_to? owner[through_reflection.foreign_key] && owner[through_reflection.foreign_key].to_s end end def foreign_key_present? - through_reflection.macro == :belongs_to && - !owner[through_reflection.foreign_key].nil? + through_reflection.belongs_to? && !owner[through_reflection.foreign_key].nil? end def ensure_mutable diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb new file mode 100644 index 0000000000..6d38224830 --- /dev/null +++ b/activerecord/lib/active_record/attribute.rb @@ -0,0 +1,120 @@ +module ActiveRecord + class Attribute # :nodoc: + class << self + def from_database(name, value, type) + FromDatabase.new(name, value, type) + end + + def from_user(name, value, type) + FromUser.new(name, value, type) + end + + def null(name) + Null.new(name) + end + + def uninitialized(name, type) + Uninitialized.new(name, type) + end + end + + attr_reader :name, :value_before_type_cast, :type + + # This method should not be called directly. + # Use #from_database or #from_user + def initialize(name, value_before_type_cast, type) + @name = name + @value_before_type_cast = value_before_type_cast + @type = type + end + + def value + # `defined?` is cheaper than `||=` when we get back falsy values + @value = type_cast(value_before_type_cast) unless defined?(@value) + @value + end + + def value_for_database + type.type_cast_for_database(value) + end + + def changed_from?(old_value) + type.changed?(old_value, value, value_before_type_cast) + end + + def changed_in_place_from?(old_value) + type.changed_in_place?(old_value, value) + end + + def with_value_from_user(value) + self.class.from_user(name, value, type) + end + + def with_value_from_database(value) + self.class.from_database(name, value, type) + end + + def type_cast + raise NotImplementedError + end + + def initialized? + true + end + + protected + + def initialize_dup(other) + if defined?(@value) && @value.duplicable? + @value = @value.dup + end + end + + class FromDatabase < Attribute # :nodoc: + def type_cast(value) + type.type_cast_from_database(value) + end + end + + class FromUser < Attribute # :nodoc: + def type_cast(value) + type.type_cast_from_user(value) + end + end + + class Null < Attribute # :nodoc: + def initialize(name) + super(name, nil, Type::Value.new) + end + + def value + nil + end + + def with_value_from_database(value) + raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`" + end + alias_method :with_value_from_user, :with_value_from_database + end + + class Uninitialized < Attribute # :nodoc: + def initialize(name, type) + super(name, nil, type) + end + + def value + if block_given? + yield name + end + end + + def value_for_database + end + + def initialized? + false + end + end + private_constant :FromDatabase, :FromUser, :Null, :Uninitialized + end +end diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index 30fa2c8ba5..2887db3bf7 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -11,6 +11,15 @@ module ActiveRecord # If the passed hash responds to <tt>permitted?</tt> method and the return value # of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt> # exception is raised. + # + # cat = Cat.new(name: "Gorby", status: "yawning") + # cat.attributes # => { "name" => "Gorby", "status" => "yawning", "created_at" => nil, "updated_at" => nil} + # cat.assign_attributes(status: "sleeping") + # cat.attributes # => { "name" => "Gorby", "status" => "sleeping", "created_at" => nil, "updated_at" => nil } + # + # New attributes will be persisted in the database when the object is saved. + # + # Aliased to <tt>attributes=</tt>. 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." @@ -106,7 +115,7 @@ module ActiveRecord end class MultiparameterAttribute #:nodoc: - attr_reader :object, :name, :values, :column + attr_reader :object, :name, :values, :cast_type def initialize(object, name, values) @object = object @@ -117,22 +126,22 @@ module ActiveRecord def read_value return if values.values.compact.empty? - @column = object.class.reflect_on_aggregation(name.to_sym) || object.column_for_attribute(name) - klass = column.klass + @cast_type = object.type_for_attribute(name) + klass = cast_type.klass if klass == Time read_time elsif klass == Date read_date else - read_other(klass) + read_other end end private def instantiate_time_object(set_values) - if object.class.send(:create_time_zone_conversion_attribute?, name, column) + if object.class.send(:create_time_zone_conversion_attribute?, name, cast_type) Time.zone.local(*set_values) else Time.send(object.class.default_timezone, *set_values) @@ -140,9 +149,9 @@ module ActiveRecord end def read_time - # If column is a :time (and not :date or :timestamp) there is no need to validate if + # If column is a :time (and not :date or :datetime) there is no need to validate if # there are year/month/day fields - if column.type == :time + if cast_type.type == :time # if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil { 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value| values[key] ||= value @@ -172,13 +181,12 @@ module ActiveRecord end end - def read_other(klass) + def read_other max_position = extract_max_param positions = (1..max_position) validate_required_parameters!(positions) - set_values = values.values_at(*positions) - klass.new(*set_values) + values.slice(*positions) end # Checks whether some blank date parameter exists. Note that this is different diff --git a/activerecord/lib/active_record/attribute_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb new file mode 100644 index 0000000000..5b96623b6e --- /dev/null +++ b/activerecord/lib/active_record/attribute_decorators.rb @@ -0,0 +1,66 @@ +module ActiveRecord + module AttributeDecorators # :nodoc: + extend ActiveSupport::Concern + + included do + class_attribute :attribute_type_decorations, instance_accessor: false # :internal: + self.attribute_type_decorations = TypeDecorator.new + end + + module ClassMethods # :nodoc: + def decorate_attribute_type(column_name, decorator_name, &block) + matcher = ->(name, _) { name == column_name.to_s } + key = "_#{column_name}_#{decorator_name}" + decorate_matching_attribute_types(matcher, key, &block) + end + + def decorate_matching_attribute_types(matcher, decorator_name, &block) + clear_caches_calculated_from_columns + decorator_name = decorator_name.to_s + + # Create new hashes so we don't modify parent classes + self.attribute_type_decorations = attribute_type_decorations.merge(decorator_name => [matcher, block]) + end + + private + + def add_user_provided_columns(*) + super.map do |column| + decorated_type = attribute_type_decorations.apply(column.name, column.cast_type) + column.with_type(decorated_type) + end + end + end + + class TypeDecorator # :nodoc: + delegate :clear, to: :@decorations + + def initialize(decorations = {}) + @decorations = decorations + end + + def merge(*args) + TypeDecorator.new(@decorations.merge(*args)) + end + + def apply(name, type) + decorations = decorators_for(name, type) + decorations.inject(type) do |new_type, block| + block.call(new_type) + end + end + + private + + def decorators_for(name, type) + matching(name, type).map(&:last) + end + + def matching(name, type) + @decorations.values.select do |(matcher, _)| + matcher.call(name, type) + end + end + end + end +end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 4b1733619a..a2bb78dfcc 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -18,6 +18,8 @@ module ActiveRecord include TimeZoneConversion include Dirty include Serialization + + delegate :column_for_attribute, to: :class end AttrNames = Module.new { @@ -48,7 +50,11 @@ module ActiveRecord end private - def method_body; raise NotImplementedError; end + + # Override this method in the subclasses for method body. + def method_body(method_name, const_name) + raise NotImplementedError, "Subclasses must implement a method_body(method_name, const_name) method." + end end module ClassMethods @@ -66,7 +72,8 @@ module ActiveRecord # Generates all the attribute related methods for columns in the database # accessors, mutators and query methods. def define_attribute_methods # :nodoc: - # Use a mutex; we don't want two thread simultaneously trying to define + return false if @attribute_methods_generated + # Use a mutex; we don't want two threads simultaneously trying to define # attribute methods. generated_attribute_methods.synchronize do return false if @attribute_methods_generated @@ -149,16 +156,6 @@ module ActiveRecord end end - def find_generated_attribute_method(method_name) # :nodoc: - klass = self - until klass == Base - gen_methods = klass.generated_attribute_methods - return gen_methods.instance_method(method_name) if method_defined_within?(method_name, gen_methods, Object) - klass = klass.superclass - end - nil - end - # Returns +true+ if +attribute+ is an attribute method and table exists, # +false+ otherwise. # @@ -187,23 +184,29 @@ module ActiveRecord [] end end - end - # 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: - self.class.define_attribute_methods - if respond_to_without_attributes?(method) - # make sure to invoke the correct attribute method, as we might have gotten here via a `super` - # call in a overwritten attribute method - if attribute_method = self.class.find_generated_attribute_method(method) - # this is probably horribly slow, but should only happen at most once for a given AR class - attribute_method.bind(self).call(*args, &block) - else - send(method, *args, &block) + # Returns the column object for the named attribute. + # Returns nil if the named attribute does not exist. + # + # class Person < ActiveRecord::Base + # end + # + # person = Person.new + # person.column_for_attribute(:name) # the result depends on the ConnectionAdapter + # # => #<ActiveRecord::ConnectionAdapters::Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...> + # + # person.column_for_attribute(:nothing) + # # => nil + def column_for_attribute(name) + column = columns_hash[name.to_s] + if column.nil? + ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) + `column_for_attribute` will return a null object for non-existent columns + in Rails 5.0. Use `has_attribute?` if you need to check for an + attribute's existence. + MESSAGE end - else - super + column end end @@ -224,18 +227,14 @@ module ActiveRecord # person.respond_to('age?') # => true # person.respond_to(:nothing) # => false def respond_to?(name, include_private = false) + return false unless super name = name.to_s - self.class.define_attribute_methods - result = super - - # If the result is false the answer is false. - return false unless result # If the result is true then check for the select case. # For queries selecting a subset of columns, return false for unselected columns. # We check defined?(@attributes) not to issue warnings if called on objects that # have been allocated but not yet initialized. - if defined?(@attributes) && @attributes.any? && self.class.column_names.include?(name) + if defined?(@attributes) && self.class.column_names.include?(name) return has_attribute?(name) end @@ -252,7 +251,7 @@ module ActiveRecord # person.has_attribute?('age') # => true # person.has_attribute?(:nothing) # => false def has_attribute?(attr_name) - @attributes.has_key?(attr_name.to_s) + @attributes.key?(attr_name.to_s) end # Returns an array of names for the attributes available on this object. @@ -276,14 +275,7 @@ module ActiveRecord # person.attributes # # => {"id"=>3, "created_at"=>Sun, 21 Oct 2012 04:53:04, "updated_at"=>Sun, 21 Oct 2012 04:53:04, "name"=>"Francesco", "age"=>22} def attributes - attribute_names.each_with_object({}) { |name, attrs| - attrs[name] = read_attribute(name) - } - end - - # Placeholder so it can be overriden when needed by serialization - def attributes_for_coder # :nodoc: - attributes + @attributes.to_hash end # Returns an <tt>#inspect</tt>-like string for the value of the @@ -326,39 +318,24 @@ module ActiveRecord # class Task < ActiveRecord::Base # end # - # person = Task.new(title: '', is_done: false) - # person.attribute_present?(:title) # => false - # person.attribute_present?(:is_done) # => true - # person.name = 'Francesco' - # person.is_done = true - # person.attribute_present?(:title) # => true - # person.attribute_present?(:is_done) # => true + # task = Task.new(title: '', is_done: false) + # task.attribute_present?(:title) # => false + # task.attribute_present?(:is_done) # => true + # task.title = 'Buy milk' + # task.is_done = true + # task.attribute_present?(:title) # => true + # task.attribute_present?(:is_done) # => true def attribute_present?(attribute) value = read_attribute(attribute) !value.nil? && !(value.respond_to?(:empty?) && value.empty?) end - # Returns the column object for the named attribute. Returns +nil+ if the - # named attribute not exists. - # - # class Person < ActiveRecord::Base - # end - # - # person = Person.new - # person.column_for_attribute(:name) # the result depends on the ConnectionAdapter - # # => #<ActiveRecord::ConnectionAdapters::SQLite3Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...> - # - # person.column_for_attribute(:nothing) - # # => nil - def column_for_attribute(name) - # FIXME: should this return a null object for columns that don't exist? - self.class.columns_hash[name.to_s] - end - # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, # "2004-12-12" in a date column is cast to a date object, like Date.new(2004, 12, 12)). It raises # <tt>ActiveModel::MissingAttributeError</tt> if the identified attribute is missing. # + # Note: +:id+ is always present. + # # Alias for the <tt>read_attribute</tt> method. # # class Person < ActiveRecord::Base @@ -392,13 +369,6 @@ module ActiveRecord protected - def clone_attributes(reader_method = :read_attribute, attributes = {}) # :nodoc: - attribute_names.each do |name| - attributes[name] = clone_attribute_value(reader_method, name) - end - attributes - end - def clone_attribute_value(reader_method, attribute_name) # :nodoc: value = send(reader_method, attribute_name) value.duplicable? ? value.clone : value @@ -416,7 +386,7 @@ module ActiveRecord def attribute_method?(attr_name) # :nodoc: # We check defined? because Syck calls respond_to? before actually calling initialize. - defined?(@attributes) && @attributes.include?(attr_name) + defined?(@attributes) && @attributes.key?(attr_name) end private @@ -435,16 +405,16 @@ module ActiveRecord # Filters the primary keys and readonly attributes from the attribute names. def attributes_for_update(attribute_names) - attribute_names.select do |name| - column_for_attribute(name) && !readonly_attribute?(name) + attribute_names.reject do |name| + readonly_attribute?(name) end end # Filters out the primary keys, from the attribute names, when the primary # key is to be generated (e.g. the id attribute has no value). def attributes_for_create(attribute_names) - attribute_names.select do |name| - column_for_attribute(name) && !(pk_attribute?(name) && id.nil?) + attribute_names.reject do |name| + pk_attribute?(name) && id.nil? end end @@ -453,13 +423,10 @@ module ActiveRecord end def pk_attribute?(name) - column_for_attribute(name).primary + name == self.class.primary_key end def typecasted_attribute_value(name) - # FIXME: we need @attributes to be used consistently. - # If the values stored in @attributes were already typecasted, this code - # could be simplified read_attribute(name) end end diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb index f596a8b02e..fd61febd57 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -43,7 +43,7 @@ module ActiveRecord # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21" # task.read_attribute_before_type_cast(:completed_on) # => "2012-10-21" def read_attribute_before_type_cast(attr_name) - @attributes[attr_name.to_s] + @attributes[attr_name.to_s].value_before_type_cast end # Returns a hash of attributes before typecasting and deserialization. @@ -57,7 +57,7 @@ module ActiveRecord # task.attributes_before_type_cast # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil} def attributes_before_type_cast - @attributes + @attributes.values_before_type_cast end private diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 99070f127b..b58295a106 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -34,28 +34,47 @@ module ActiveRecord # <tt>reload</tt> the record and clears changed attributes. def reload(*) super.tap do - reset_changes + clear_changes_information end end - def initialize_dup(other) # :nodoc: - super - init_changed_attributes - end + def initialize_dup(other) # :nodoc: + super + calculate_changes_from_defaults + end + + def changed? + super || changed_in_place.any? + end + + def changed + super | changed_in_place + end - private - def initialize_internals_callback + def attribute_changed?(attr_name, options = {}) + result = super + # We can't change "from" something in place. Only setters can define + # "from" and "to" + result ||= changed_in_place?(attr_name) unless options.key?(:from) + result + end + + def changes_applied + super + store_original_raw_attributes + end + + def clear_changes_information super - init_changed_attributes + original_raw_attributes.clear end - def init_changed_attributes + private + + def calculate_changes_from_defaults @changed_attributes = nil - # Intentionally avoid using #column_defaults since overridden defaults (as is done in - # 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]) + self.class.column_defaults.each do |attr, orig_value| + changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value) end end @@ -63,19 +82,35 @@ module ActiveRecord def write_attribute(attr, value) attr = attr.to_s - save_changed_attribute(attr, value) + old_value = old_attribute_value(attr) - super(attr, value) + result = super + store_original_raw_attribute(attr) + save_changed_attribute(attr, old_value) + result end - def save_changed_attribute(attr, value) - # The attribute already has an unsaved change. + def raw_write_attribute(attr, value) + attr = attr.to_s + + result = super + original_raw_attributes[attr] = value + result + end + + def save_changed_attribute(attr, old_value) + if attribute_changed?(attr) + changed_attributes.delete(attr) unless _field_changed?(attr, old_value) + else + changed_attributes[attr] = old_value if _field_changed?(attr, old_value) + end + end + + def old_attribute_value(attr) if attribute_changed?(attr) - old = changed_attributes[attr] - changed_attributes.delete(attr) unless _field_changed?(attr, old, value) + changed_attributes[attr] else - old = clone_attribute_value(:read_attribute, attr) - changed_attributes[attr] = old if _field_changed?(attr, old, value) + clone_attribute_value(:read_attribute, attr) end end @@ -93,34 +128,39 @@ module ActiveRecord changed end - def _field_changed?(attr, old, value) - if column = column_for_attribute(attr) - if column.number? && (changes_from_nil_to_empty_string?(column, old, value) || - changes_from_zero_to_string?(old, value)) - value = nil - else - value = column.type_cast(value) - end + def _field_changed?(attr, old_value) + @attributes[attr].changed_from?(old_value) + end + + def changed_in_place + self.class.attribute_names.select do |attr_name| + changed_in_place?(attr_name) end + end - old != value + def changed_in_place?(attr_name) + old_value = original_raw_attribute(attr_name) + @attributes[attr_name].changed_in_place_from?(old_value) end - def changes_from_nil_to_empty_string?(column, old, value) - # For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values. - # Hence we don't record it as a change if the value changes from nil to ''. - # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll - # be typecast back to 0 (''.to_i => 0) - column.null && (old.nil? || old == 0) && value.blank? + def original_raw_attribute(attr_name) + original_raw_attributes.fetch(attr_name) do + read_attribute_before_type_cast(attr_name) + end end - def changes_from_zero_to_string?(old, value) - # For columns with old 0 and value non-empty string - old == 0 && value.is_a?(String) && value.present? && non_zero?(value) + def original_raw_attributes + @original_raw_attributes ||= {} end - def non_zero?(value) - value !~ /\A0+(\.0+)?\z/ + def store_original_raw_attribute(attr_name) + original_raw_attributes[attr_name] = @attributes[attr_name].value_for_database + end + + def store_original_raw_attributes + attribute_names.each do |attr| + store_original_raw_attribute(attr) + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 931209b07b..cadad60ddd 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -15,8 +15,10 @@ module ActiveRecord # Returns the primary key value. def id - sync_with_transaction_state - read_attribute(self.class.primary_key) + if pk = self.class.primary_key + sync_with_transaction_state + read_attribute(pk) + end end # Sets the primary key value. @@ -81,12 +83,9 @@ module ActiveRecord end def get_primary_key(base_name) #:nodoc: - return 'id' if base_name.blank? - - case primary_key_prefix_type - when :table_name + if base_name && primary_key_prefix_type == :table_name base_name.foreign_key(false) - when :table_name_with_underscore + elsif base_name && primary_key_prefix_type == :table_name_with_underscore base_name.foreign_key else if ActiveRecord::Base != self && table_exists? diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index d01e9aea59..10869dfc1e 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -22,7 +22,7 @@ module ActiveRecord # the attribute name. Using a constant means that we do not have # to allocate an object on each call to the attribute method. # Making it frozen means that it doesn't get duped when used to - # key the @attributes_cache in read_attribute. + # key the @attributes in read_attribute. def method_body(method_name, const_name) <<-EOMETHOD def #{method_name} @@ -35,35 +35,22 @@ module ActiveRecord extend ActiveSupport::Concern - ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date] - - included do - class_attribute :attribute_types_cached_by_default, instance_writer: false - self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT - end - module ClassMethods - # +cache_attributes+ allows you to declare which converted attribute - # values should be cached. Usually caching only pays off for attributes - # with expensive conversion methods, like time related columns (e.g. - # +created_at+, +updated_at+). - def cache_attributes(*attribute_names) - cached_attributes.merge attribute_names.map { |attr| attr.to_s } + [:cache_attributes, :cached_attributes, :cache_attribute?].each do |method_name| + define_method method_name do |*| + cached_attributes_deprecation_warning(method_name) + true + end end - # Returns the attributes which are cached. By default time related columns - # with datatype <tt>:datetime, :timestamp, :time, :date</tt> are cached. - def cached_attributes - @cached_attributes ||= columns.select { |c| cacheable_column?(c) }.map { |col| col.name }.to_set - end + protected - # Returns +true+ if the provided attribute is being cached. - def cache_attribute?(attr_name) - cached_attributes.include?(attr_name) + def cached_attributes_deprecation_warning(method_name) + ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) + Calling `#{method_name}` is no longer necessary. All attributes are cached. + MESSAGE end - protected - if Module.methods_transplantable? def define_method_attribute(name) method = ReaderMethodCache[name] @@ -89,45 +76,15 @@ module ActiveRecord end end end - - private - - def cacheable_column?(column) - if attribute_types_cached_by_default == ATTRIBUTE_TYPES_CACHED_BY_DEFAULT - ! serialized_attributes.include? column.name - else - attribute_types_cached_by_default.include?(column.type) - end - end end # Returns the value of the attribute identified by <tt>attr_name</tt> after # it has been typecast (for example, "2004-12-12" in a date column is cast # to a date object, like Date.new(2004, 12, 12)). - def read_attribute(attr_name) - # If it's cached, just return it - # We use #[] first as a perf optimization for non-nil values. See https://gist.github.com/jonleighton/3552829. + def read_attribute(attr_name, &block) name = attr_name.to_s - @attributes_cache[name] || @attributes_cache.fetch(name) { - 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 - } - - if self.class.cache_attribute?(name) - @attributes_cache[name] = column.type_cast(value) - else - column.type_cast value - end - } + name = self.class.primary_key if name == 'id' + @attributes.fetch_value(name, &block) end private diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index c3466153d6..264ce2bdfa 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -3,20 +3,7 @@ module ActiveRecord module Serialization extend ActiveSupport::Concern - included do - # Returns a hash of all the attributes that have been specified for - # serialization as keys and their class restriction as values. - class_attribute :serialized_attributes, instance_accessor: false - self.serialized_attributes = {} - end - module ClassMethods - ## - # :method: serialized_attributes - # - # Returns a hash of all the attributes that have been specified for - # serialization as keys and their class restriction as values. - # If you have an attribute that needs to be saved to the database as an # object, and retrieved as the same object, then specify the name of that # attribute using this method and it will be handled automatically. The @@ -50,140 +37,32 @@ module ActiveRecord # serialize :preferences, Hash # end def serialize(attr_name, class_name_or_coder = Object) - include Behavior - - coder = if [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) } + # When ::JSON is used, force it to go through the Active Support JSON encoder + # to ensure special objects (e.g. Active Record models) are dumped correctly + # using the #as_json hook. + coder = if class_name_or_coder == ::JSON + Coders::JSON + elsif [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) } class_name_or_coder else Coders::YAMLColumn.new(class_name_or_coder) end - # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy - # has its own hash of own serialized attributes - self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder) - end - end - - class Type # :nodoc: - def initialize(column) - @column = column - end - - def type_cast(value) - if value.state == :serialized - value.unserialized_value @column.type_cast value.value - else - value.unserialized_value - end - end - - def type - @column.type - end - - def accessor - ActiveRecord::Store::IndifferentHashAccessor - end - end - - class Attribute < Struct.new(:coder, :value, :state) # :nodoc: - def unserialized_value(v = value) - state == :serialized ? unserialize(v) : value - end - - def serialized_value - state == :unserialized ? serialize : value - end - - def unserialize(v) - self.state = :unserialized - self.value = coder.load(v) - end - - def serialize - self.state = :serialized - self.value = coder.dump(value) - end - end - - # This is only added to the model when serialize is called, which - # ensures we do not make things slower when serialization is not used. - module Behavior # :nodoc: - extend ActiveSupport::Concern - - module ClassMethods # :nodoc: - def initialize_attributes(attributes, options = {}) - serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized - super(attributes, options) - - serialized_attributes.each do |key, coder| - if attributes.key?(key) - attributes[key] = Attribute.new(coder, attributes[key], serialized) - end - end - - attributes + decorate_attribute_type(attr_name, :serialize) do |type| + Type::Serialized.new(type, coder) end end - def should_record_timestamps? - super || (self.record_timestamps && (attributes.keys & self.class.serialized_attributes.keys).present?) - end - - def keys_for_partial_write - super | (attributes.keys & self.class.serialized_attributes.keys) - end - - def type_cast_attribute_for_write(column, value) - if column && coder = self.class.serialized_attributes[column.name] - Attribute.new(coder, value, :unserialized) - else - super - end - end - - def _field_changed?(attr, old, value) - if self.class.serialized_attributes.include?(attr) - old != value - else - super - end - end - - def read_attribute_before_type_cast(attr_name) - if self.class.serialized_attributes.include?(attr_name) - super.unserialized_value - else - super - end - end - - def attributes_before_type_cast - super.dup.tap do |attributes| - self.class.serialized_attributes.each_key do |key| - if attributes.key?(key) - attributes[key] = attributes[key].unserialized_value - end - end - end - end - - def typecasted_attribute_value(name) - if self.class.serialized_attributes.include?(name) - @attributes[name].serialized_value - else - super - end - end - - def attributes_for_coder - attribute_names.each_with_object({}) do |name, attrs| - attrs[name] = if self.class.serialized_attributes.include?(name) - @attributes[name].serialized_value - else - read_attribute(name) - end - end + def serialized_attributes + ActiveSupport::Deprecation.warn(<<-WARNING.strip_heredoc) + `serialized_attributes` is deprecated without replacement, and will + be removed in Rails 5.0. + WARNING + @serialized_attributes ||= Hash[ + columns.select { |t| t.cast_type.is_a?(Type::Serialized) }.map { |c| + [c.name, c.cast_type.coder] + } + ] end end end diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index f168282ea3..f439bd1ffe 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -1,18 +1,27 @@ module ActiveRecord module AttributeMethods module TimeZoneConversion - class Type # :nodoc: - def initialize(column) - @column = column + class TimeZoneConverter < SimpleDelegator # :nodoc: + def type_cast_from_database(value) + convert_time_to_time_zone(super) end - def type_cast(value) - value = @column.type_cast(value) - value.acts_like?(:time) ? value.in_time_zone : value + def type_cast_from_user(value) + if value.is_a?(Array) + value.map { |v| type_cast_from_user(v) } + elsif value.respond_to?(:in_time_zone) + value.in_time_zone + end end - def type - @column.type + def convert_time_to_time_zone(value) + if value.is_a?(Array) + value.map { |v| convert_time_to_time_zone(v) } + elsif value.acts_like?(:time) + value.in_time_zone + else + value + end end end @@ -27,31 +36,26 @@ module ActiveRecord end module ClassMethods - protected - # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. - # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone. - def define_method_attribute=(attr_name) - if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) - method_body, line = <<-EOV, __LINE__ + 1 - def #{attr_name}=(time) - time_with_zone = time.respond_to?(:in_time_zone) ? time.in_time_zone : nil - previous_time = attribute_changed?("#{attr_name}") ? changed_attributes["#{attr_name}"] : read_attribute(:#{attr_name}) - write_attribute(:#{attr_name}, time) - #{attr_name}_will_change! if previous_time != time_with_zone - @attributes_cache["#{attr_name}"] = time_with_zone - end - EOV - generated_attribute_methods.module_eval(method_body, __FILE__, line) - else - super + private + + def inherited(subclass) + # We need to apply this decorator here, rather than on module inclusion. The closure + # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the + # sub class being decorated. As such, changes to `time_zone_aware_attributes`, or + # `skip_time_zone_conversion_for_attributes` would not be picked up. + subclass.class_eval do + matcher = ->(name, type) { create_time_zone_conversion_attribute?(name, type) } + decorate_matching_attribute_types(matcher, :_time_zone_conversion) do |type| + TimeZoneConverter.new(type) + end end + super end - private - def create_time_zone_conversion_attribute?(name, column) + def create_time_zone_conversion_attribute?(name, cast_type) time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && - (:datetime == column.type || :timestamp == column.type) + (:datetime == cast_type.type) end end end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index c853fc0917..b3c8209a74 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -26,8 +26,6 @@ module ActiveRecord protected if Module.methods_transplantable? - # See define_method_attribute in read.rb for an explanation of - # this code. def define_method_attribute=(name) method = WriterMethodCache[name] generated_attribute_methods.module_eval { @@ -55,24 +53,12 @@ module ActiveRecord # specified +value+. Empty strings for fixnum and float columns are # turned into +nil+. def write_attribute(attr_name, value) - attr_name = attr_name.to_s - attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key - @attributes_cache.delete(attr_name) - column = column_for_attribute(attr_name) - - # If we're dealing with a binary column, write the data to the cache - # so we don't attempt to typecast multiple times. - if column && column.binary? - @attributes_cache[attr_name] = value - end + write_attribute_with_type_cast(attr_name, value, true) + end - if column || @attributes.has_key?(attr_name) - @attributes[attr_name] = type_cast_attribute_for_write(column, value) - else - raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'" - end + def raw_write_attribute(attr_name, value) + write_attribute_with_type_cast(attr_name, value, false) end - alias_method :raw_write_attribute, :write_attribute private # Handle *= for method_missing. @@ -80,10 +66,17 @@ module ActiveRecord write_attribute(attribute_name, value) end - def type_cast_attribute_for_write(column, value) - return value unless column + def write_attribute_with_type_cast(attr_name, value, should_type_cast) + attr_name = attr_name.to_s + attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key + + if should_type_cast + @attributes.write_from_user(attr_name, value) + else + @attributes.write_from_database(attr_name, value) + end - column.type_cast_for_write value + value end end end diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb new file mode 100644 index 0000000000..98ac63c7e1 --- /dev/null +++ b/activerecord/lib/active_record/attribute_set.rb @@ -0,0 +1,77 @@ +require 'active_record/attribute_set/builder' + +module ActiveRecord + class AttributeSet # :nodoc: + delegate :keys, to: :initialized_attributes + + def initialize(attributes) + @attributes = attributes + end + + def [](name) + attributes[name] || Attribute.null(name) + end + + def values_before_type_cast + attributes.transform_values(&:value_before_type_cast) + end + + def to_hash + initialized_attributes.transform_values(&:value) + end + alias_method :to_h, :to_hash + + def key?(name) + attributes.key?(name) && self[name].initialized? + end + + def fetch_value(name, &block) + self[name].value(&block) + end + + def write_from_database(name, value) + attributes[name] = self[name].with_value_from_database(value) + end + + def write_from_user(name, value) + attributes[name] = self[name].with_value_from_user(value) + end + + def freeze + @attributes.freeze + super + end + + def initialize_dup(_) + @attributes = attributes.transform_values(&:dup) + super + end + + def initialize_clone(_) + @attributes = attributes.clone + super + end + + def reset(key) + if key?(key) + write_from_database(key, nil) + end + end + + def ensure_initialized(key) + unless self[key].initialized? + write_from_database(key, nil) + end + end + + protected + + attr_reader :attributes + + private + + def initialized_attributes + attributes.select { |_, attr| attr.initialized? } + end + end +end diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb new file mode 100644 index 0000000000..1e146a07da --- /dev/null +++ b/activerecord/lib/active_record/attribute_set/builder.rb @@ -0,0 +1,32 @@ +module ActiveRecord + class AttributeSet # :nodoc: + class Builder # :nodoc: + attr_reader :types + + def initialize(types) + @types = types + end + + def build_from_database(values = {}, additional_types = {}) + attributes = build_attributes_from_values(values, additional_types) + add_uninitialized_attributes(attributes) + AttributeSet.new(attributes) + end + + private + + def build_attributes_from_values(values, additional_types) + values.each_with_object({}) do |(name, value), hash| + type = additional_types.fetch(name, types[name]) + hash[name] = Attribute.from_database(name, value, type) + end + end + + def add_uninitialized_attributes(attributes) + types.except(*attributes.keys).each do |name, type| + attributes[name] = Attribute.uninitialized(name, type) + end + end + end + end +end diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb new file mode 100644 index 0000000000..890a1314d9 --- /dev/null +++ b/activerecord/lib/active_record/attributes.rb @@ -0,0 +1,122 @@ +module ActiveRecord + module Attributes # :nodoc: + extend ActiveSupport::Concern + + Type = ActiveRecord::Type + + included do + class_attribute :user_provided_columns, instance_accessor: false # :internal: + self.user_provided_columns = {} + end + + module ClassMethods # :nodoc: + # Defines or overrides a attribute on this model. This allows customization of + # Active Record's type casting behavior, as well as adding support for user defined + # types. + # + # +name+ The name of the methods to define attribute methods for, and the column which + # this will persist to. + # + # +cast_type+ A type object that contains information about how to type cast the value. + # See the examples section for more information. + # + # ==== Options + # The options hash accepts the following options: + # + # +default+ is the default value that the column should use on a new record. + # + # ==== Examples + # + # The type detected by Active Record can be overridden. + # + # # db/schema.rb + # create_table :store_listings, force: true do |t| + # t.decimal :price_in_cents + # end + # + # # app/models/store_listing.rb + # class StoreListing < ActiveRecord::Base + # end + # + # store_listing = StoreListing.new(price_in_cents: '10.1') + # + # # before + # store_listing.price_in_cents # => BigDecimal.new(10.1) + # + # class StoreListing < ActiveRecord::Base + # attribute :price_in_cents, Type::Integer.new + # end + # + # # after + # store_listing.price_in_cents # => 10 + # + # Users may also define their own custom types, as long as they respond to the methods + # defined on the value type. The `type_cast` method on your type object will be called + # with values both from the database, and from your controllers. See + # `ActiveRecord::Attributes::Type::Value` for the expected API. It is recommended that your + # type objects inherit from an existing type, or the base value type. + # + # class MoneyType < ActiveRecord::Type::Integer + # def type_cast(value) + # if value.include?('$') + # price_in_dollars = value.gsub(/\$/, '').to_f + # price_in_dollars * 100 + # else + # value.to_i + # end + # end + # end + # + # class StoreListing < ActiveRecord::Base + # attribute :price_in_cents, MoneyType.new + # end + # + # store_listing = StoreListing.new(price_in_cents: '$10.00') + # store_listing.price_in_cents # => 1000 + def attribute(name, cast_type, options = {}) + name = name.to_s + clear_caches_calculated_from_columns + # Assign a new hash to ensure that subclasses do not share a hash + self.user_provided_columns = user_provided_columns.merge(name => connection.new_column(name, options[:default], cast_type)) + end + + # Returns an array of column objects for the table associated with this class. + def columns + @columns ||= add_user_provided_columns(connection.schema_cache.columns(table_name)) + end + + # Returns a hash of column objects for the table associated with this class. + def columns_hash + @columns_hash ||= Hash[columns.map { |c| [c.name, c] }] + end + + def reset_column_information # :nodoc: + super + clear_caches_calculated_from_columns + end + + private + + def add_user_provided_columns(schema_columns) + existing_columns = schema_columns.map do |column| + user_provided_columns[column.name] || column + end + + existing_column_names = existing_columns.map(&:name) + new_columns = user_provided_columns.except(*existing_column_names).values + + existing_columns + new_columns + end + + def clear_caches_calculated_from_columns + @attributes_builder = nil + @column_names = nil + @column_types = nil + @columns = nil + @columns_hash = nil + @content_columns = nil + @default_attributes = nil + end + end + end +end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index f149d8f127..dd92e29199 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -147,6 +147,7 @@ module ActiveRecord private def define_non_cyclic_method(name, &block) + return if method_defined?(name) define_method(name) do |*args| result = true; @_already_called ||= {} # Loop prevention for validation of associations @@ -179,30 +180,26 @@ module ActiveRecord 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 + if collection + before_save :before_save_collection_association + + define_non_cyclic_method(save_method) { save_collection_association(reflection) } + after_save save_method + elsif reflection.has_one? + define_method(save_method) { save_has_one_association(reflection) } unless method_defined?(save_method) + # Configures two callbacks instead of a single after_save so that + # the model may rely on their execution order relative to its + # 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 if reflection.validate? && !method_defined?(validation_method) @@ -273,9 +270,11 @@ module ActiveRecord # 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? } + self.class._reflections.values.any? do |reflection| + if reflection.options[:autosave] + association = association_instance_get(reflection.name) + association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? } + end end end @@ -304,7 +303,8 @@ module ActiveRecord def association_valid?(reflection, record) return true if record.destroyed? || record.marked_for_destruction? - unless valid = record.valid? + validation_context = self.validation_context unless [:create, :update].include?(self.validation_context) + unless valid = record.valid?(validation_context) if reflection.options[:autosave] record.errors.each do |attribute, message| attribute = "#{reflection.name}.#{attribute}" @@ -362,6 +362,7 @@ module ActiveRecord raise ActiveRecord::Rollback unless saved end + @new_record_before_save = false end # reconstruct the scope now that we know the owner's id @@ -380,15 +381,16 @@ module ActiveRecord 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 + elsif autosave != false key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id - if autosave != false && (autosave || new_record? || record_changed?(reflection, record, key)) + if (autosave && record.changed_for_autosave?) || new_record? || record_changed?(reflection, record, key) unless reflection.through_reflection record[reflection.foreign_key] = key end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index db4d5f0129..f978fbd0a4 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -9,16 +9,19 @@ require 'active_support/core_ext/class/delegating_attributes' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/hash/deep_merge' require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/hash/transform_values' require 'active_support/core_ext/string/behavior' require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/module/introspection' require 'active_support/core_ext/object/duplicable' require 'active_support/core_ext/class/subclasses' require 'arel' +require 'active_record/attribute_decorators' require 'active_record/errors' require 'active_record/log_subscriber' require 'active_record/explain_subscriber' require 'active_record/relation/delegation' +require 'active_record/attributes' module ActiveRecord #:nodoc: # = Active Record @@ -138,6 +141,7 @@ module ActiveRecord #:nodoc: # # In addition to the basic accessors, query methods are also automatically available on the Active Record object. # Query methods allow you to test whether an attribute value is present. + # For numeric values, present is defined as non-zero. # # For example, an Active Record User with the <tt>name</tt> attribute has a <tt>name?</tt> method that you can call # to determine whether the user has a name: @@ -217,25 +221,9 @@ module ActiveRecord #:nodoc: # # == Single table inheritance # - # Active Record allows inheritance by storing the name of the class in a column that by - # default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>). - # This means that an inheritance looking like this: - # - # class Company < ActiveRecord::Base; end - # class Firm < Company; end - # class Client < Company; end - # class PriorityClient < Client; end - # - # When you do <tt>Firm.create(name: "37signals")</tt>, this record will be saved in - # the companies table with type = "Firm". You can then fetch this row again using - # <tt>Company.where(name: '37signals').first</tt> and it will return a Firm object. - # - # If you don't have a type column defined in your table, single-table inheritance won't - # be triggered. In that case, it'll work just like normal subclasses with no special magic - # for differentiating between them or reloading the right type with find. - # - # Note, all the attributes for all the cases are kept in the same table. Read more: - # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html + # Active Record allows inheritance by storing the name of the class in a + # column that is named "type" by default. See ActiveRecord::Inheritance for + # more details. # # == Connection to multiple databases in different models # @@ -306,11 +294,13 @@ module ActiveRecord #:nodoc: include Integration include Validations include CounterCache + include Attributes + include AttributeDecorators include Locking::Optimistic include Locking::Pessimistic include AttributeMethods - include Timestamp include Callbacks + include Timestamp include Associations include ActiveModel::SecurePassword include AutosaveAssociation diff --git a/activerecord/lib/active_record/coders/json.rb b/activerecord/lib/active_record/coders/json.rb new file mode 100644 index 0000000000..75d3bfe625 --- /dev/null +++ b/activerecord/lib/active_record/coders/json.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module Coders # :nodoc: + class JSON # :nodoc: + def self.dump(obj) + ActiveSupport::JSON.encode(obj) + end + + def self.load(json) + ActiveSupport::JSON.decode(json) unless json.nil? + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index db80c0faee..cb75070e3a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -364,7 +364,7 @@ module ActiveRecord conn.expire end - release conn, owner + release owner @available.add conn end @@ -377,7 +377,7 @@ module ActiveRecord @connections.delete conn @available.delete conn - release conn, conn.owner + release conn.owner @available.add checkout_new_connection if @available.any_waiting? end @@ -425,7 +425,7 @@ module ActiveRecord end end - def release(conn, owner) + def release(owner) thread_id = owner.object_id @reserved_connections.delete thread_id 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 bc47412405..98e96099cb 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -193,7 +193,7 @@ module ActiveRecord # * You are creating a nested (savepoint) transaction # # The mysql, mysql2 and postgresql adapters support setting the transaction - # isolation level. However, support is disabled for mysql versions below 5, + # isolation level. However, support is disabled for MySQL versions below 5, # because they are affected by a bug[http://bugs.mysql.com/bug.php?id=39170] # which means the isolation level gets persisted outside the transaction. def transaction(options = {}) @@ -203,58 +203,30 @@ module ActiveRecord if options[:isolation] raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction" end - yield else - within_new_transaction(options) { yield } + transaction_manager.within_new_transaction(options) { yield } end rescue ActiveRecord::Rollback # rollbacks are silently swallowed end - def within_new_transaction(options = {}) #:nodoc: - transaction = begin_transaction(options) - yield - rescue Exception => error - rollback_transaction if transaction - raise - ensure - begin - commit_transaction unless error - rescue Exception - rollback_transaction - raise - end - end + attr_reader :transaction_manager #:nodoc: - def current_transaction #:nodoc: - @transaction - end + delegate :within_new_transaction, :open_transactions, :current_transaction, :begin_transaction, :commit_transaction, :rollback_transaction, to: :transaction_manager def transaction_open? - @transaction.open? - end - - def begin_transaction(options = {}) #:nodoc: - @transaction = @transaction.begin(options) - end - - def commit_transaction #:nodoc: - @transaction = @transaction.commit - end - - def rollback_transaction #:nodoc: - @transaction = @transaction.rollback + current_transaction.open? end def reset_transaction #:nodoc: - @transaction = ClosedTransaction.new(self) + @transaction_manager = TransactionManager.new(self) end # Register a record with the current transaction so that its after_commit and after_rollback callbacks # can be called. def add_transaction_record(record) - @transaction.add_record(record) + current_transaction.add_record(record) end # Begins the transaction (and turns off auto-committing). @@ -334,8 +306,8 @@ module ActiveRecord end # The default strategy for an UPDATE with joins is to use a subquery. This doesn't work - # on mysql (even when aliasing the tables), but mysql allows using JOIN directly in - # an UPDATE statement, so in the mysql adapters we redefine this to do that. + # on MySQL (even when aliasing the tables), but MySQL allows using JOIN directly in + # an UPDATE statement, so in the MySQL adapters we redefine this to do that. def join_to_update(update, select) #:nodoc: key = update.key subselect = subquery_for(key, select) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 75501852ed..eb88845913 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -9,34 +9,11 @@ module ActiveRecord # records are quoted as their primary key return value.quoted_id if value.respond_to?(:quoted_id) - case value - when String, ActiveSupport::Multibyte::Chars - value = value.to_s - return "'#{quote_string(value)}'" unless column - - case column.type - when :integer then value.to_i.to_s - when :float then value.to_f.to_s - else - "'#{quote_string(value)}'" - end - - when true, false - if column && column.type == :integer - value ? '1' : '0' - else - value ? quoted_true : quoted_false - end - # BigDecimals need to be put in a non-normalized form and quoted. - when nil then "NULL" - when BigDecimal then value.to_s('F') - when Numeric, ActiveSupport::Duration then value.to_s - when Date, Time then "'#{quoted_date(value)}'" - when Symbol then "'#{quote_string(value.to_s)}'" - when Class then "'#{value.to_s}'" - else - "'#{quote_string(YAML.dump(value))}'" + if column + value = column.cast_type.type_cast_for_database(value) end + + _quote(value) end # Cast a +value+ to a type that the database understands. For example, @@ -47,34 +24,14 @@ module ActiveRecord return value.id end - case value - when String, ActiveSupport::Multibyte::Chars - value = value.to_s - return value unless column - - case column.type - when :integer then value.to_i - when :float then value.to_f - else - value - end - - when true, false - if column && column.type == :integer - value ? 1 : 0 - else - value ? 't' : 'f' - end - # BigDecimals need to be put in a non-normalized form and quoted. - when nil then nil - when BigDecimal then value.to_s('F') - when Numeric then value - when Date, Time then quoted_date(value) - when Symbol then value.to_s - else - to_type = column ? " to #{column.type}" : "" - raise TypeError, "can't cast #{value.class}#{to_type}" + if column + value = column.cast_type.type_cast_for_database(value) end + + _type_cast(value) + rescue TypeError + to_type = column ? " to #{column.type}" : "" + raise TypeError, "can't cast #{value.class}#{to_type}" end # Quotes a string, escaping any ' (single quote) and \ (backslash) @@ -99,7 +56,7 @@ module ActiveRecord # This works for mysql and mysql2 where table.column can be used to # resolve ambiguity. # - # We override this in the sqlite and postgresql adapters to use only + # We override this in the sqlite3 and postgresql adapters to use only # the column name (as per syntax requirements). def quote_table_name_for_assignment(table, attr) quote_table_name("#{table}.#{attr}") @@ -109,10 +66,18 @@ module ActiveRecord "'t'" end + def unquoted_true + 't' + end + def quoted_false "'f'" end + def unquoted_false + 'f' + end + def quoted_date(value) if value.acts_like?(:time) zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal @@ -124,6 +89,45 @@ module ActiveRecord value.to_s(:db) end + + private + + def types_which_need_no_typecasting + [nil, Numeric, String] + end + + def _quote(value) + case value + when String, ActiveSupport::Multibyte::Chars, Type::Binary::Data + "'#{quote_string(value.to_s)}'" + when true then quoted_true + when false then quoted_false + when nil then "NULL" + # BigDecimals need to be put in a non-normalized form and quoted. + when BigDecimal then value.to_s('F') + when Numeric, ActiveSupport::Duration then value.to_s + when Date, Time then "'#{quoted_date(value)}'" + when Symbol then "'#{quote_string(value.to_s)}'" + when Class then "'#{value.to_s}'" + else + "'#{quote_string(YAML.dump(value))}'" + end + end + + def _type_cast(value) + case value + when Symbol, ActiveSupport::Multibyte::Chars, Type::Binary::Data + value.to_s + when true then unquoted_true + when false then unquoted_false + # BigDecimals need to be put in a non-normalized form and quoted. + when BigDecimal then value.to_s('F') + when Date, Time then quoted_date(value) + when *types_which_need_no_typecasting + value + else raise TypeError + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb index 47fe501752..adad6cd542 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -23,6 +23,8 @@ module ActiveRecord def visit_AlterTable(o) sql = "ALTER TABLE #{quote_table_name(o.name)} " sql << o.adds.map { |col| visit_AddColumn col }.join(' ') + sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(' ') + sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(' ') end def visit_ColumnDefinition(o) @@ -41,6 +43,21 @@ module ActiveRecord create_sql end + def visit_AddForeignKey(o) + sql = <<-SQL.strip_heredoc + ADD CONSTRAINT #{quote_column_name(o.name)} + FOREIGN KEY (#{quote_column_name(o.column)}) + REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)}) + SQL + sql << " #{action_sql('DELETE', o.on_delete)}" if o.on_delete + sql << " #{action_sql('UPDATE', o.on_update)}" if o.on_update + sql + end + + def visit_DropForeignKey(name) + "DROP CONSTRAINT #{quote_column_name(name)}" + end + def column_options(o) column_options = {} column_options[:null] = o.null unless o.null.nil? @@ -77,6 +94,7 @@ module ActiveRecord def quote_value(value, column) column.sql_type ||= type_to_sql(column.type, column.limit, column.precision, column.scale) + column.cast_type ||= type_for_column(column) @conn.quote(value, column) end @@ -84,6 +102,23 @@ module ActiveRecord def options_include_default?(options) options.include?(:default) && !(options[:null] == false && options[:default].nil?) end + + def action_sql(action, dependency) + case dependency + when :nullify then "ON #{action} SET NULL" + when :cascade then "ON #{action} CASCADE" + when :restrict then "ON #{action} RESTRICT" + else + raise ArgumentError, <<-MSG.strip_heredoc + '#{dependency}' is not supported for :on_update or :on_delete. + Supported values are: :nullify, :cascade, :restrict + MSG + end + end + + def type_for_column(column) + @conn.lookup_cast_type(column.sql_type) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 71c3a4378b..e44ccb7d81 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -15,7 +15,7 @@ module ActiveRecord # are typically created by methods in TableDefinition, and added to the # +columns+ attribute of said TableDefinition object, in order to be used # for generating a number of table creation or table changing SQL statements. - class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :primary_key, :sql_type) #:nodoc: + class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :primary_key, :sql_type, :cast_type) #:nodoc: def primary_key? primary_key || type.to_sym == :primary_key @@ -25,6 +25,37 @@ module ActiveRecord class ChangeColumnDefinition < Struct.new(:column, :type, :options) #:nodoc: end + class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc: + def name + options[:name] + end + + def column + options[:column] + end + + def primary_key + options[:primary_key] || default_primary_key + end + + def on_delete + options[:on_delete] + end + + def on_update + options[:on_update] + end + + def custom_primary_key? + options[:primary_key] != default_primary_key + end + + private + def default_primary_key + "id" + end + end + # Represents the schema of an SQL table in an abstract way. This class # provides methods for manipulating the schema representation. # @@ -99,9 +130,11 @@ module ActiveRecord # Specifies the precision for a <tt>:decimal</tt> column. # * <tt>:scale</tt> - # Specifies the scale for a <tt>:decimal</tt> column. + # * <tt>:index</tt> - + # Create an index for the column. Can be either <tt>true</tt> or an options hash. # - # For clarity's sake: the precision is the number of significant digits, - # while the scale is the number of digits that can be stored following + # Note: The precision is the total number of significant digits + # and the scale is the number of digits that can be stored following # the decimal point. For example, the number 123.45 has a precision of 5 # and a scale of 2. A decimal with a precision of 5 and a scale of 2 can # range from -999.99 to 999.99. @@ -123,17 +156,8 @@ module ActiveRecord # Default is (38,0). # * DB2: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..62]. # Default unknown. - # * Firebird: <tt>:precision</tt> [1..18], <tt>:scale</tt> [0..18]. - # Default (9,0). Internal types NUMERIC and DECIMAL have different - # storage rules, decimal being better. - # * FrontBase?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38]. - # Default (38,0). WARNING Max <tt>:precision</tt>/<tt>:scale</tt> for - # NUMERIC is 19, and DECIMAL is 38. # * SqlServer?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38]. # Default (38,0). - # * Sybase: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38]. - # Default (38,0). - # * OpenBase?: Documentation unclear. Claims storage in <tt>double</tt>. # # This method returns <tt>self</tt>. # @@ -172,18 +196,21 @@ module ActiveRecord # What can be written like this with the regular calls to column: # # create_table :products do |t| - # t.column :shop_id, :integer - # t.column :creator_id, :integer - # t.column :name, :string, default: "Untitled" - # t.column :value, :string, default: "Untitled" - # t.column :created_at, :datetime - # t.column :updated_at, :datetime + # t.column :shop_id, :integer + # t.column :creator_id, :integer + # t.column :item_number, :string + # t.column :name, :string, default: "Untitled" + # t.column :value, :string, default: "Untitled" + # t.column :created_at, :datetime + # t.column :updated_at, :datetime # end + # add_index :products, :item_number # # can also be written as follows using the short-hand: # # create_table :products do |t| # t.integer :shop_id, :creator_id + # t.string :item_number, index: true # t.string :name, :value, default: "Untitled" # t.timestamps # end @@ -219,6 +246,8 @@ module ActiveRecord raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table." end + index_options = options.delete(:index) + index(name, index_options.is_a?(Hash) ? index_options : {}) if index_options @columns_hash[name] = new_column_definition(name, type, options) self end @@ -251,12 +280,22 @@ module ActiveRecord column(:updated_at, :datetime, options) end + # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. + # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. The reference column will be an +integer+ + # by default, the <tt>:type</tt> option can be used to specify a different type. + # + # t.references(:user) + # t.references(:user, type: "string") + # t.belongs_to(:supplier, polymorphic: true) + # + # See SchemaStatements#add_reference def references(*args) options = args.extract_options! polymorphic = options.delete(:polymorphic) index_options = options.delete(:index) + type = options.delete(:type) || :integer args.each do |col| - column("#{col}_id", :integer, options) + column("#{col}_id", type, options) column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic index(polymorphic ? %w(id type).map { |t| "#{col}_#{t}" } : "#{col}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options end @@ -264,6 +303,7 @@ module ActiveRecord alias :belongs_to :references def new_column_definition(name, type, options) # :nodoc: + type = aliased_types[type] || type column = create_column_definition name, type limit = options.fetch(:limit) do native[type][:limit] if native[type].is_a?(Hash) @@ -294,18 +334,36 @@ module ActiveRecord def native @native end + + def aliased_types + HashWithIndifferentAccess.new( + timestamp: :datetime, + ) + end end class AlterTable # :nodoc: attr_reader :adds + attr_reader :foreign_key_adds + attr_reader :foreign_key_drops def initialize(td) @td = td @adds = [] + @foreign_key_adds = [] + @foreign_key_drops = [] end def name; @td.name; end + def add_foreign_key(to_table, options) + @foreign_key_adds << ForeignKeyDefinition.new(name, to_table, options) + end + + def drop_foreign_key(name) + @foreign_key_drops << name + end + def add_column(name, type, options) name = name.to_s type = type.to_sym @@ -452,11 +510,14 @@ module ActiveRecord end # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. - # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. + # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. The reference column will be an +integer+ + # by default, the <tt>:type</tt> option can be used to specify a different type. # # t.references(:user) + # t.references(:user, type: "string") # t.belongs_to(:supplier, polymorphic: true) # + # See SchemaStatements#add_reference def references(*args) options = args.extract_options! args.each do |ref_name| @@ -471,6 +532,7 @@ module ActiveRecord # t.remove_references(:user) # t.remove_belongs_to(:supplier, polymorphic: true) # + # See SchemaStatements#remove_reference def remove_references(*args) options = args.extract_options! args.each do |ref_name| diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index cdf0cbe218..9bd0401e40 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -1,5 +1,3 @@ -require 'ipaddr' - module ActiveRecord module ConnectionAdapters # :nodoc: # The goal of this module is to move Adapter specific column @@ -20,19 +18,13 @@ module ActiveRecord def prepare_column_options(column, types) spec = {} spec[:name] = column.name.inspect - - # AR has an optimization which handles zero-scale decimals as integers. This - # code ensures that the dumper still dumps the column as a decimal. - spec[:type] = if column.type == :integer && /^(numeric|decimal)/ =~ column.sql_type - 'decimal' - else - column.type.to_s - end - spec[:limit] = column.limit.inspect if column.limit != types[column.type][:limit] && spec[:type] != 'decimal' + spec[:type] = column.type.to_s + spec[:limit] = column.limit.inspect if column.limit != types[column.type][:limit] spec[:precision] = column.precision.inspect if column.precision spec[:scale] = column.scale.inspect if column.scale spec[:null] = 'false' unless column.null - spec[:default] = default_string(column.default) if column.has_default? + spec[:default] = schema_default(column) if column.has_default? + spec.delete(:default) if spec[:default].nil? spec end @@ -43,28 +35,12 @@ module ActiveRecord private - def default_string(value) - case value - when BigDecimal - value.to_s - when Date, DateTime, Time - "'#{value.to_s(:db)}'" - when Range - # infinity dumps as Infinity, which causes uninitialized constant error - value.inspect.gsub('Infinity', '::Float::INFINITY') - when IPAddr - subnet_mask = value.instance_variable_get(:@mask_addr) - - # If the subnet mask is equal to /32, don't output it - if subnet_mask == (2**32 - 1) - "\"#{value.to_s}\"" - else - "\"#{value.to_s}/#{subnet_mask.to_s(2).count('1')}\"" - end - else - value.inspect - end + def schema_default(column) + default = column.type_cast_from_database(column.default) + unless default.nil? + column.type_cast_for_schema(default) end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index aa99822389..10753defc2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -71,7 +71,8 @@ module ActiveRecord # column_exists?(:suppliers, :tax, :decimal, precision: 8, scale: 2) # def column_exists?(table_name, column_name, type = nil, options = {}) - columns(table_name).any?{ |c| c.name == column_name.to_s && + column_name = column_name.to_s + columns(table_name).any?{ |c| c.name == column_name && (!type || c.type == type) && (!options.key?(:limit) || c.limit == options[:limit]) && (!options.key?(:precision) || c.precision == options[:precision]) && @@ -601,12 +602,18 @@ module ActiveRecord end # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. + # The reference column is an +integer+ by default, the <tt>:type</tt> option can be used to specify + # a different type. # <tt>add_reference</tt> and <tt>add_belongs_to</tt> are acceptable. # - # ====== Create a user_id column + # ====== Create a user_id integer column # # add_reference(:products, :user) # + # ====== Create a user_id string column + # + # add_reference(:products, :user, type: :string) + # # ====== Create a supplier_id and supplier_type columns # # add_belongs_to(:products, :supplier, polymorphic: true) @@ -618,7 +625,8 @@ module ActiveRecord def add_reference(table_name, ref_name, options = {}) polymorphic = options.delete(:polymorphic) index_options = options.delete(:index) - add_column(table_name, "#{ref_name}_id", :integer, options) + type = options.delete(:type) || :integer + add_column(table_name, "#{ref_name}_id", type, options) add_column(table_name, "#{ref_name}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic add_index(table_name, polymorphic ? %w[id type].map{ |t| "#{ref_name}_#{t}" } : "#{ref_name}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options end @@ -641,6 +649,115 @@ module ActiveRecord end alias :remove_belongs_to :remove_reference + # Returns an array of foreign keys for the given table. + # The foreign keys are represented as +ForeignKeyDefinition+ objects. + def foreign_keys(table_name) + raise NotImplementedError, "foreign_keys is not implemented" + end + + # Adds a new foreign key. +from_table+ is the table with the key column, + # +to_table+ contains the referenced primary key. + # + # The foreign key will be named after the following pattern: <tt>fk_rails_<identifier></tt>. + # +identifier+ is a 10 character long random string. A custom name can be specified with + # the <tt>:name</tt> option. + # + # ====== Creating a simple foreign key + # + # add_foreign_key :articles, :authors + # + # generates: + # + # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id") + # + # ====== Creating a foreign key on a specific column + # + # add_foreign_key :articles, :users, column: :author_id, primary_key: "lng_id" + # + # generates: + # + # ALTER TABLE "articles" ADD CONSTRAINT fk_rails_58ca3d3a82 FOREIGN KEY ("author_id") REFERENCES "users" ("lng_id") + # + # ====== Creating a cascading foreign key + # + # add_foreign_key :articles, :authors, on_delete: :cascade + # + # generates: + # + # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id") ON DELETE CASCADE + # + # The +options+ hash can include the following keys: + # [<tt>:column</tt>] + # The foreign key column name on +from_table+. Defaults to <tt>to_table.singularize + "_id"</tt> + # [<tt>:primary_key</tt>] + # The primary key column name on +to_table+. Defaults to +id+. + # [<tt>:name</tt>] + # The constraint name. Defaults to <tt>fk_rails_<identifier></tt>. + # [<tt>:on_delete</tt>] + # Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+ + # [<tt>:on_update</tt>] + # Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+ + def add_foreign_key(from_table, to_table, options = {}) + return unless supports_foreign_keys? + + options[:column] ||= foreign_key_column_for(to_table) + + options = { + column: options[:column], + primary_key: options[:primary_key], + name: foreign_key_name(from_table, options), + on_delete: options[:on_delete], + on_update: options[:on_update] + } + at = create_alter_table from_table + at.add_foreign_key to_table, options + + execute schema_creation.accept(at) + end + + # Removes the given foreign key from the table. + # + # Removes the foreign key on +accounts.branch_id+. + # + # remove_foreign_key :accounts, :branches + # + # Removes the foreign key on +accounts.owner_id+. + # + # remove_foreign_key :accounts, column: :owner_id + # + # Removes the foreign key named +special_fk_name+ on the +accounts+ table. + # + # remove_foreign_key :accounts, name: :special_fk_name + # + def remove_foreign_key(from_table, options_or_to_table = {}) + return unless supports_foreign_keys? + + if options_or_to_table.is_a?(Hash) + options = options_or_to_table + else + options = { column: foreign_key_column_for(options_or_to_table) } + end + + fk_name_to_delete = options.fetch(:name) do + fk_to_delete = foreign_keys(from_table).detect {|fk| fk.column == options[:column] } + + if fk_to_delete + fk_to_delete.name + else + raise ArgumentError, "Table '#{from_table}' has no foreign key on column '#{options[:column]}'" + end + end + + at = create_alter_table from_table + at.drop_foreign_key fk_name_to_delete + + execute schema_creation.accept(at) + end + + def foreign_key_column_for(table_name) # :nodoc: + "#{table_name.to_s.singularize}_id" + end + def dump_schema_information #:nodoc: sm_table = ActiveRecord::Migrator.schema_migrations_table_name @@ -787,7 +904,7 @@ module ActiveRecord return option_strings end - # Overridden by the mysql adapter for supporting index lengths + # Overridden by the MySQL adapter for supporting index lengths def quoted_columns_for_index(column_names, options = {}) option_strings = Hash[column_names.map {|name| [name, '']}] @@ -851,6 +968,12 @@ module ActiveRecord def create_alter_table(name) AlterTable.new create_table_definition(name, false, {}) end + + def foreign_key_name(table_name, options) # :nodoc: + options.fetch(:name) do + "fk_rails_#{SecureRandom.hex(5)}" + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index bc4884b538..3a266512a9 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -1,5 +1,59 @@ module ActiveRecord module ConnectionAdapters + class TransactionManager #:nodoc: + def initialize(connection) + @stack = [] + @connection = connection + end + + def begin_transaction(options = {}) + transaction_class = @stack.empty? ? RealTransaction : SavepointTransaction + transaction = transaction_class.new(@connection, current_transaction, options) + + @stack.push(transaction) + transaction + end + + def commit_transaction + @stack.pop.commit + end + + def rollback_transaction + @stack.pop.rollback + end + + def within_new_transaction(options = {}) + transaction = begin_transaction options + yield + rescue Exception => error + transaction.rollback if transaction + raise + ensure + begin + transaction.commit unless error + rescue Exception + transaction.rollback + raise + ensure + @stack.pop if transaction + end + end + + def open_transactions + @stack.size + end + + def current_transaction + @stack.last || closed_transaction + end + + private + + def closed_transaction + @closed_transaction ||= ClosedTransaction.new(@connection) + end + end + class Transaction #:nodoc: attr_reader :connection @@ -11,6 +65,10 @@ module ActiveRecord def state @state end + + def savepoint_name + nil + end end class TransactionState @@ -78,45 +136,28 @@ module ActiveRecord @parent = parent @records = [] - @finishing = false @joinable = options.fetch(:joinable, true) end - # This state is necessary so that we correctly handle stuff that might - # happen in a commit/rollback. But it's kinda distasteful. Maybe we can - # find a better way to structure it in the future. - def finishing? - @finishing - end def joinable? - @joinable && !finishing? + @joinable end def number - if finishing? - parent.number - else - parent.number + 1 - end + parent.number + 1 end def begin(options = {}) - if finishing? - parent.begin - else - SavepointTransaction.new(connection, self, options) - end + SavepointTransaction.new(connection, self, options) end def rollback - @finishing = true perform_rollback parent end def commit - @finishing = true perform_commit parent end @@ -183,24 +224,29 @@ module ActiveRecord end class SavepointTransaction < OpenTransaction #:nodoc: + attr_reader :savepoint_name + def initialize(connection, parent, options = {}) if options[:isolation] raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction" end super - connection.create_savepoint + + # Savepoint name only counts the Savepoint transactions, so we need to subtract 1 + @savepoint_name = "active_record_#{number - 1}" + connection.create_savepoint(@savepoint_name) end def perform_rollback - connection.rollback_to_savepoint + connection.rollback_to_savepoint(@savepoint_name) rollback_records end def perform_commit @state.set_state(:committed) @state.parent = parent.state - connection.release_savepoint + connection.release_savepoint(@savepoint_name) 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 78343cf4f5..99c728814a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -1,6 +1,7 @@ require 'date' require 'bigdecimal' require 'bigdecimal/util' +require 'active_record/type' require 'active_support/core_ext/benchmark' require 'active_record/connection_adapters/schema_cache' require 'active_record/connection_adapters/abstract/schema_dumper' @@ -13,7 +14,10 @@ module ActiveRecord module ConnectionAdapters # :nodoc: extend ActiveSupport::Autoload - autoload :Column + autoload_at 'active_record/connection_adapters/column' do + autoload :Column + autoload :NullColumn + end autoload :ConnectionSpecification autoload_at 'active_record/connection_adapters/abstract/schema_definitions' do @@ -41,6 +45,7 @@ module ActiveRecord end autoload_at 'active_record/connection_adapters/abstract/transaction' do + autoload :TransactionManager autoload :ClosedTransaction autoload :RealTransaction autoload :SavepointTransaction @@ -120,7 +125,7 @@ module ActiveRecord end def collector - if @prepared_statements + if prepared_statements SQLString.new else BindCollector.new @@ -229,6 +234,11 @@ module ActiveRecord false end + # Does this adapter support creating foreign key constraints? + def supports_foreign_keys? + false + end + # This is meant to be implemented by the adapters that support extensions def disable_extension(name) end @@ -324,10 +334,6 @@ module ActiveRecord @connection end - def open_transactions - @transaction.number - end - def create_savepoint(name = nil) end @@ -352,7 +358,7 @@ module ActiveRecord end def current_savepoint_name - "active_record_#{open_transactions}" + current_transaction.savepoint_name end # Check the connection back in to the connection pool @@ -360,8 +366,80 @@ module ActiveRecord pool.checkin self end + def type_map # :nodoc: + @type_map ||= Type::TypeMap.new.tap do |mapping| + initialize_type_map(mapping) + end + end + + def new_column(name, default, cast_type, sql_type = nil, null = true) + Column.new(name, default, cast_type, sql_type, null) + end + + def lookup_cast_type(sql_type) # :nodoc: + type_map.lookup(sql_type) + end + protected + def initialize_type_map(m) # :nodoc: + register_class_with_limit m, %r(boolean)i, Type::Boolean + register_class_with_limit m, %r(char)i, Type::String + register_class_with_limit m, %r(binary)i, Type::Binary + register_class_with_limit m, %r(text)i, Type::Text + register_class_with_limit m, %r(date)i, Type::Date + register_class_with_limit m, %r(time)i, Type::Time + register_class_with_limit m, %r(datetime)i, Type::DateTime + register_class_with_limit m, %r(float)i, Type::Float + register_class_with_limit m, %r(int)i, Type::Integer + + m.alias_type %r(blob)i, 'binary' + m.alias_type %r(clob)i, 'text' + m.alias_type %r(timestamp)i, 'datetime' + m.alias_type %r(numeric)i, 'decimal' + m.alias_type %r(number)i, 'decimal' + m.alias_type %r(double)i, 'float' + + m.register_type(%r(decimal)i) do |sql_type| + scale = extract_scale(sql_type) + precision = extract_precision(sql_type) + + if scale == 0 + # FIXME: Remove this class as well + Type::DecimalWithoutScale.new(precision: precision) + else + Type::Decimal.new(precision: precision, scale: scale) + end + end + end + + def reload_type_map # :nodoc: + type_map.clear + initialize_type_map(type_map) + end + + def register_class_with_limit(mapping, key, klass) # :nodoc: + mapping.register_type(key) do |*args| + limit = extract_limit(args.last) + klass.new(limit: limit) + end + end + + def extract_scale(sql_type) # :nodoc: + case sql_type + when /\((\d+)\)/ then 0 + when /\((\d+)(,(\d+))\)/ then $3.to_i + end + end + + def extract_precision(sql_type) # :nodoc: + $1.to_i if sql_type =~ /\((\d+)(,\d+)?\)/ + end + + def extract_limit(sql_type) # :nodoc: + $1.to_i if sql_type =~ /\((.*)\)/ + end + def translate_exception_class(e, sql) message = "#{e.class.name}: #{e.message}: #{sql}" @logger.error message if @logger @@ -388,7 +466,13 @@ module ActiveRecord end def without_prepared_statement?(binds) - !@prepared_statements || binds.empty? + !prepared_statements || binds.empty? + end + + def column_for(table_name, column_name) # :nodoc: + column_name = column_name.to_s + columns(table_name).detect { |c| c.name == column_name } || + raise(ActiveRecordError, "No such column: #{table_name}.#{column_name}") 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 75c58ac7d9..e5417a9556 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -12,6 +12,10 @@ module ActiveRecord private + def visit_DropForeignKey(name) + "DROP FOREIGN KEY #{name}" + end + def visit_TableDefinition(o) name = o.name create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(name)} " @@ -56,29 +60,25 @@ module ActiveRecord class Column < ConnectionAdapters::Column # :nodoc: attr_reader :collation, :strict, :extra - def initialize(name, default, sql_type = nil, null = true, collation = nil, strict = false, extra = "") + def initialize(name, default, cast_type, sql_type = nil, null = true, collation = nil, strict = false, extra = "") @strict = strict @collation = collation @extra = extra - super(name, default, sql_type, null) + super(name, default, cast_type, sql_type, null) + assert_valid_default(default) + extract_default end - def extract_default(default) + def extract_default if blob_or_text_column? - if default.blank? - null || strict ? nil : '' - else - raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" - end - elsif missing_default_forged_as_empty_string?(default) - nil - else - super + @default = null || strict ? nil : '' + elsif missing_default_forged_as_empty_string?(@default) + @default = nil end end def has_default? - return false if blob_or_text_column? #mysql forbids defaults on blob and text columns + return false if blob_or_text_column? # MySQL forbids defaults on blob and text columns super end @@ -86,54 +86,12 @@ module ActiveRecord sql_type =~ /blob/i || type == :text end - # Must return the relevant concrete adapter - def adapter - raise NotImplementedError - end - def case_sensitive? collation && !collation.match(/_ci$/) end private - def simplified_type(field_type) - return :boolean if adapter.emulate_booleans && field_type.downcase.index("tinyint(1)") - - case field_type - when /enum/i, /set/i then :string - when /year/i then :integer - when /bit/i then :binary - else - super - end - end - - def extract_limit(sql_type) - case sql_type - when /^enum\((.+)\)/i - $1.split(',').map{|enum| enum.strip.length - 2}.max - when /blob|text/i - case sql_type - when /tiny/i - 255 - when /medium/i - 16777215 - when /long/i - 2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases - else - super # we could return 65535 here, but we leave it undecorated by default - end - when /^bigint/i; 8 - when /^int/i; 4 - when /^mediumint/i; 3 - when /^smallint/i; 2 - when /^tinyint/i; 1 - else - super - end - end - # MySQL misreports NOT NULL column default when none is given. # We can't detect this for columns which may have a legitimate '' # default (string) but we can for others (integer, datetime, boolean, @@ -144,6 +102,12 @@ module ActiveRecord def missing_default_forged_as_empty_string?(default) type != :string && !null && default == '' end + + def assert_valid_default(default) + if blob_or_text_column? && default.present? + raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" + end + end end ## @@ -173,7 +137,6 @@ module ActiveRecord :float => { :name => "float" }, :decimal => { :name => "decimal" }, :datetime => { :name => "datetime" }, - :timestamp => { :name => "datetime" }, :time => { :name => "time" }, :date => { :name => "date" }, :binary => { :name => "blob" }, @@ -221,17 +184,6 @@ module ActiveRecord true end - def type_cast(value, column) - case value - when TrueClass - 1 - when FalseClass - 0 - else - super - end - end - # MySQL 4 technically support transaction isolation, but it is affected by a bug # where the transaction level gets persisted for the whole session: # @@ -244,6 +196,10 @@ module ActiveRecord true end + def supports_foreign_keys? + true + end + def native_database_types NATIVE_DATABASE_TYPES end @@ -260,12 +216,11 @@ module ActiveRecord raise NotImplementedError end - # Overridden by the adapters to instantiate their specific Column type. - def new_column(field, default, type, null, collation, extra = "") # :nodoc: - Column.new(field, default, type, null, collation, extra) + def new_column(field, default, cast_type, sql_type = nil, null = true, collation = "", extra = "") # :nodoc: + Column.new(field, default, cast_type, sql_type, null, collation, strict_mode?, extra) end - # Must return the Mysql error number from the exception, if the exception has an + # Must return the MySQL error number from the exception, if the exception has an # error number. def error_number(exception) # :nodoc: raise NotImplementedError @@ -273,12 +228,9 @@ module ActiveRecord # QUOTING ================================================== - def quote(value, column = nil) - if value.kind_of?(String) && column && column.type == :binary - s = value.unpack("H*")[0] - "x'#{s}'" - elsif value.kind_of?(BigDecimal) - value.to_s("F") + def _quote(value) # :nodoc: + if value.is_a?(Type::Binary::Data) + "x'#{value.hex}'" else super end @@ -296,10 +248,18 @@ module ActiveRecord QUOTED_TRUE end + def unquoted_true + 1 + end + def quoted_false QUOTED_FALSE end + def unquoted_false + 0 + end + # REFERENTIAL INTEGRITY ==================================== def disable_referential_integrity #:nodoc: @@ -315,6 +275,11 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== + def clear_cache! + super + reload_type_map + end + # Executes the SQL statement in the context of this connection. def execute(sql, name = nil) log(sql, name) { @connection.query(sql) } @@ -424,7 +389,7 @@ module ActiveRecord end def table_exists?(name) - return false unless name + return false unless name.present? return true if tables(nil, nil, name).any? name = name.to_s @@ -468,7 +433,9 @@ module ActiveRecord execute_and_free(sql, 'SCHEMA') do |result| each_hash(result).map do |field| field_name = set_field_encoding(field[:Field]) - new_column(field_name, field[:Default], field[:Type], field[:Null] == "YES", field[:Collation], field[:Extra]) + sql_type = field[:Type] + cast_type = lookup_cast_type(sql_type) + new_column(field_name, field[:Default], cast_type, sql_type, field[:Null] == "YES", field[:Collation], field[:Extra]) end end end @@ -506,7 +473,7 @@ module ActiveRecord end def rename_index(table_name, old_name, new_name) - if (version[0] == 5 && version[1] >= 7) || version[0] >= 6 + if supports_rename_index? execute "ALTER TABLE #{quote_table_name(table_name)} RENAME INDEX #{quote_table_name(old_name)} TO #{quote_table_name(new_name)}" else super @@ -542,6 +509,34 @@ module ActiveRecord execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options} #{index_algorithm}" end + def foreign_keys(table_name) + fk_info = select_all <<-SQL.strip_heredoc + SELECT fk.referenced_table_name as 'to_table' + ,fk.referenced_column_name as 'primary_key' + ,fk.column_name as 'column' + ,fk.constraint_name as 'name' + FROM information_schema.key_column_usage fk + WHERE fk.referenced_column_name is not null + AND fk.table_schema = '#{@config[:database]}' + AND fk.table_name = '#{table_name}' + SQL + + create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] + + fk_info.map do |row| + options = { + column: row['column'], + name: row['name'], + primary_key: row['primary_key'] + } + + options[:on_update] = extract_foreign_key_action(create_table_info, row['name'], "UPDATE") + options[:on_delete] = extract_foreign_key_action(create_table_info, row['name'], "DELETE") + + ForeignKeyDefinition.new(table_name, row['to_table'], options) + end + end + # Maps logical Rails types to MySQL-specific data types. def type_to_sql(type, limit = nil, precision = nil, scale = nil) case type.to_s @@ -642,6 +637,34 @@ module ActiveRecord protected + def initialize_type_map(m) # :nodoc: + super + m.register_type(%r(enum)i) do |sql_type| + limit = sql_type[/^enum\((.+)\)/i, 1] + .split(',').map{|enum| enum.strip.length - 2}.max + Type::String.new(limit: limit) + end + + m.register_type %r(tinytext)i, Type::Text.new(limit: 255) + m.register_type %r(tinyblob)i, Type::Binary.new(limit: 255) + m.register_type %r(mediumtext)i, Type::Text.new(limit: 16777215) + m.register_type %r(mediumblob)i, Type::Binary.new(limit: 16777215) + m.register_type %r(longtext)i, Type::Text.new(limit: 2147483647) + m.register_type %r(longblob)i, Type::Binary.new(limit: 2147483647) + m.register_type %r(^bigint)i, Type::Integer.new(limit: 8) + m.register_type %r(^int)i, Type::Integer.new(limit: 4) + m.register_type %r(^mediumint)i, Type::Integer.new(limit: 3) + m.register_type %r(^smallint)i, Type::Integer.new(limit: 2) + m.register_type %r(^tinyint)i, Type::Integer.new(limit: 1) + m.register_type %r(^float)i, Type::Float.new(limit: 24) + m.register_type %r(^double)i, Type::Float.new(limit: 53) + + m.alias_type %r(tinyint\(1\))i, 'boolean' if emulate_booleans + m.alias_type %r(set)i, 'varchar' + m.alias_type %r(year)i, 'integer' + m.alias_type %r(bit)i, 'binary' + end + # MySQL is too stupid to create a temporary table for use subquery, so we have # to give it some prompting in the form of a subsubquery. Ugh! def subquery_for(key, select) @@ -711,15 +734,13 @@ module ActiveRecord end def rename_column_sql(table_name, column_name, new_column_name) - options = { name: new_column_name } - - if column = columns(table_name).find { |c| c.name == column_name.to_s } - options[:default] = column.default - options[:null] = column.null - options[:auto_increment] = (column.extra == "auto_increment") - else - raise ActiveRecordError, "No such column: #{table_name}.#{column_name}" - end + column = column_for(table_name, column_name) + options = { + name: new_column_name, + default: column.default, + null: column.null, + auto_increment: column.extra == "auto_increment" + } current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["Type"] schema_creation.accept ChangeColumnDefinition.new column, current_type, options @@ -753,34 +774,39 @@ module ActiveRecord private + def version + @version ||= full_version.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } + end + + def mariadb? + full_version =~ /mariadb/i + end + def supports_views? version[0] >= 5 end - def column_for(table_name, column_name) - unless column = columns(table_name).find { |c| c.name == column_name.to_s } - raise "No such column: #{table_name}.#{column_name}" - end - column + def supports_rename_index? + mariadb? ? false : (version[0] == 5 && version[1] >= 7) || version[0] >= 6 end def configure_connection - variables = @config[:variables] || {} + variables = @config.fetch(:variables, {}).stringify_keys # By default, MySQL 'where id is null' selects the last inserted id. # Turn this off. http://dev.rubyonrails.org/ticket/6778 - variables[:sql_auto_is_null] = 0 + variables['sql_auto_is_null'] = 0 # Increase timeout so the server doesn't disconnect us. wait_timeout = @config[:wait_timeout] wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum) - variables[:wait_timeout] = self.class.type_cast_config_to_integer(wait_timeout) + variables['wait_timeout'] = self.class.type_cast_config_to_integer(wait_timeout) # Make MySQL reject illegal values rather than truncating or blanking them, see # http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html#sqlmode_strict_all_tables # If the user has provided another value for sql_mode, don't replace it. - if strict_mode? && !variables.has_key?(:sql_mode) - variables[:sql_mode] = 'STRICT_ALL_TABLES' + unless variables.has_key?('sql_mode') + variables['sql_mode'] = strict_mode? ? 'STRICT_ALL_TABLES' : '' end # NAMES does not have an equals sign, see @@ -801,6 +827,15 @@ module ActiveRecord # ...and send them all in one query @connection.query "SET #{encoding} #{variable_assignments}" end + + def extract_foreign_key_action(structure, name, action) # :nodoc: + if structure =~ /CONSTRAINT #{quote_column_name(name)} FOREIGN KEY .* REFERENCES .* ON #{action} (CASCADE|SET NULL|RESTRICT)/ + case $1 + when 'CASCADE'; :cascade + when 'SET NULL'; :nullify + end + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 187eefb9e4..1f1e2c46f4 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -13,107 +13,36 @@ 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, :default_function - attr_accessor :primary, :coder + attr_reader :name, :cast_type, :null, :sql_type, :default, :default_function - alias :encoded? :coder + delegate :type, :precision, :scale, :limit, :klass, :accessor, + :text?, :number?, :binary?, :changed?, + :type_cast_from_user, :type_cast_from_database, :type_cast_for_database, + :type_cast_for_schema, + to: :cast_type # Instantiates a new column in the table. # # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>. # +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>. + # +cast_type+ is the object used for type casting and type information. # +sql_type+ is used to extract the column's length, if necessary. For example +60+ in # <tt>company_name varchar(60)</tt>. # 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) + def initialize(name, default, cast_type, sql_type = nil, null = true) @name = name + @cast_type = cast_type @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 = default @default_function = nil - @primary = nil - @coder = nil - end - - # Returns +true+ if the column is either of type string or text. - def text? - type == :string || type == :text - end - - # Returns +true+ if the column is either of type integer, float or decimal. - def number? - type == :integer || type == :float || type == :decimal end def has_default? !default.nil? end - # Returns the Ruby class that corresponds to the abstract data type. - def klass - case type - when :integer then Fixnum - when :float then Float - when :decimal then BigDecimal - when :datetime, :timestamp, :time then Time - when :date then Date - when :text, :string, :binary then String - when :boolean then Object - end - end - - def binary? - type == :binary - end - - # Casts a Ruby value to something appropriate for writing to the database. - def type_cast_for_write(value) - return value unless number? - - case value - when FalseClass - 0 - when TrueClass - 1 - when String - value.presence - else - value - end - end - - # Casts value to an appropriate instance. - def type_cast(value) - return nil if value.nil? - return coder.load(value) if encoded? - - klass = self.class - - case type - when :string, :text - case value - when TrueClass; "1" - when FalseClass; "0" - else - value.to_s - end - when :integer then klass.value_to_integer(value) - when :float then value.to_f - when :decimal then klass.value_to_decimal(value) - when :datetime, :timestamp then klass.string_to_time(value) - when :time then klass.string_to_dummy_time(value) - when :date then klass.value_to_date(value) - when :binary then klass.binary_to_string(value) - when :boolean then klass.value_to_boolean(value) - else value - end - end - # Returns the human name of the column name. # # ===== Examples @@ -122,177 +51,11 @@ module ActiveRecord Base.human_attribute_name(@name) end - def extract_default(default) - type_cast(default) - end - - class << self - # Used to convert from BLOBs to Strings - def binary_to_string(value) - value + def with_type(type) + dup.tap do |clone| + clone.instance_variable_set('@cast_type', type) end - - def value_to_date(value) - if value.is_a?(String) - return nil if value.empty? - fast_string_to_date(value) || fallback_string_to_date(value) - elsif value.respond_to?(:to_date) - value.to_date - else - value - end - end - - def string_to_time(string) - return string unless string.is_a?(String) - return nil if string.empty? - - fast_string_to_time(string) || fallback_string_to_time(string) - end - - def string_to_dummy_time(string) - return string unless string.is_a?(String) - return nil if string.empty? - - dummy_time_string = "2000-01-01 #{string}" - - fast_string_to_time(dummy_time_string) || begin - time_hash = Date._parse(dummy_time_string) - return nil if time_hash[:hour].nil? - new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) - end - end - - # convert something to a boolean - def value_to_boolean(value) - if value.is_a?(String) && value.empty? - nil - else - TRUE_VALUES.include?(value) - end - end - - # Used to convert values to integer. - # handle the case when an integer column is used to store boolean values - def value_to_integer(value) - case value - when TrueClass, FalseClass - value ? 1 : 0 - else - value.to_i rescue nil - end - end - - # convert something to a BigDecimal - def value_to_decimal(value) - # Using .class is faster than .is_a? and - # subclasses of BigDecimal will be handled - # in the else clause - if value.class == BigDecimal - value - elsif value.respond_to?(:to_d) - value.to_d - else - value.to_s.to_d - end - end - - protected - # '0.123456' -> 123456 - # '1.123456' -> 123456 - def microseconds(time) - time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0 - end - - def new_date(year, mon, mday) - if year && year != 0 - Date.new(year, mon, mday) rescue nil - end - end - - 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) - - 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) - if string =~ Format::ISO_DATE - new_date $1.to_i, $2.to_i, $3.to_i - end - end - - # Doesn't handle time zones. - def fast_string_to_time(string) - if string =~ Format::ISO_DATETIME - microsec = ($7.to_r * 1_000_000).to_i - new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec - end - end - - def fallback_string_to_date(string) - new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday)) - end - - def fallback_string_to_time(string) - 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, :offset)) - end end - - private - def extract_limit(sql_type) - $1.to_i if sql_type =~ /\((.*)\)/ - end - - def extract_precision(sql_type) - $2.to_i if sql_type =~ /^(numeric|decimal|number)\((\d+)(,\d+)?\)/i - end - - def extract_scale(sql_type) - case sql_type - when /^(numeric|decimal|number)\((\d+)\)/i then 0 - when /^(numeric|decimal|number)\((\d+)(,(\d+))\)/i then $4.to_i - end - end - - def simplified_type(field_type) - case field_type - when /int/i - :integer - when /float|double/i - :float - when /decimal|numeric|number/i - extract_scale(field_type) == 0 ? :integer : :decimal - when /datetime/i - :datetime - when /timestamp/i - :timestamp - when /time/i - :time - when /date/i - :date - when /clob/i, /text/i - :text - when /blob/i, /binary/i - :binary - when /char/i - :string - when /boolean/i - :boolean - end - end end end # :startdoc: diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index b79d1a4458..5693031053 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -85,7 +85,7 @@ module ActiveRecord "password" => uri.password, "port" => uri.port, "database" => database_from_path, - "host" => uri.host }) + "host" => uri.hostname }) end end @@ -160,7 +160,7 @@ module ActiveRecord # config = { "production" => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } } # spec = Resolver.new(config).spec(:production) # spec.adapter_method - # # => "sqlite3" + # # => "sqlite3_connection" # spec.config # # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } # @@ -250,7 +250,7 @@ module ActiveRecord # Connection details inside of the "url" key win any merge conflicts def resolve_hash_connection(spec) if spec["url"] && spec["url"] !~ /^jdbc:/ - connection_hash = resolve_string_connection(spec.delete("url")) + connection_hash = resolve_url_connection(spec.delete("url")) spec.merge!(connection_hash) end spec diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 233af252d6..39d52e6349 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -29,13 +29,6 @@ module ActiveRecord module ConnectionAdapters class Mysql2Adapter < AbstractMysqlAdapter - - class Column < AbstractMysqlAdapter::Column # :nodoc: - def adapter - Mysql2Adapter - end - end - ADAPTER_NAME = 'Mysql2' def initialize(connection, logger, connection_options, config) @@ -69,10 +62,6 @@ module ActiveRecord end end - def new_column(field, default, type, null, collation, extra = "") # :nodoc: - Column.new(field, default, type, null, collation, strict_mode?, extra) - end - def error_number(exception) exception.error_number if exception.respond_to?(:error_number) end @@ -280,8 +269,8 @@ module ActiveRecord super end - def version - @version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } + def full_version + @full_version ||= @connection.info[:version] end def set_field_encoding field_name diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index e6aa2ba921..a03bc28744 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -66,35 +66,6 @@ module ActiveRecord # * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection. # class MysqlAdapter < AbstractMysqlAdapter - - class Column < AbstractMysqlAdapter::Column #:nodoc: - def self.string_to_time(value) - return super unless Mysql::Time === value - new_time( - value.year, - value.month, - value.day, - value.hour, - value.minute, - value.second, - value.second_part) - end - - def self.string_to_dummy_time(v) - return super unless Mysql::Time === v - new_time(2000, 01, 01, v.hour, v.minute, v.second, v.second_part) - end - - def self.string_to_date(v) - return super unless Mysql::Time === v - new_date(v.year, v.month, v.day) - end - - def adapter - MysqlAdapter - end - end - ADAPTER_NAME = 'MySQL' class StatementPool < ConnectionAdapters::StatementPool @@ -156,10 +127,6 @@ module ActiveRecord end end - def new_column(field, default, type, null, collation, extra = "") # :nodoc: - Column.new(field, default, type, null, collation, strict_mode?, extra) - end - def error_number(exception) # :nodoc: exception.errno if exception.respond_to?(:errno) end @@ -222,6 +189,7 @@ module ActiveRecord # Clears the prepared statements cache. def clear_cache! + super @statements.clear end @@ -294,126 +262,70 @@ module ActiveRecord @connection.insert_id end - module Fields - class Type - def type; end - - def type_cast_for_write(value) - value - end - end - - class Identity < Type - def type_cast(value); value; end - end - - class Integer < Type - def type_cast(value) - return if value.nil? - - value.to_i rescue value ? 1 : 0 - end - end - - class Date < Type - def type; :date; end - - def type_cast(value) - return if value.nil? - - # FIXME: probably we can improve this since we know it is mysql - # specific - ConnectionAdapters::Column.value_to_date value - end - end - - class DateTime < Type - def type; :datetime; end - - def type_cast(value) - return if value.nil? - - # FIXME: probably we can improve this since we know it is mysql - # specific - ConnectionAdapters::Column.string_to_time value - end - end - - class Time < Type - def type; :time; end - - def type_cast(value) - return if value.nil? - - # FIXME: probably we can improve this since we know it is mysql - # specific - ConnectionAdapters::Column.string_to_dummy_time value - end - end - - class Float < Type - def type; :float; end - - def type_cast(value) - return if value.nil? - - value.to_f + module Fields # :nodoc: + class DateTime < Type::DateTime # :nodoc: + def cast_value(value) + if Mysql::Time === value + new_time( + value.year, + value.month, + value.day, + value.hour, + value.minute, + value.second, + value.second_part) + else + super + end end end - class Decimal < Type - def type_cast(value) - return if value.nil? - - ConnectionAdapters::Column.value_to_decimal value + class Time < Type::Time # :nodoc: + def cast_value(value) + if Mysql::Time === value + new_time( + 2000, + 01, + 01, + value.hour, + value.minute, + value.second, + value.second_part) + else + super + end end end - class Boolean < Type - def type_cast(value) - return if value.nil? + class << self + TYPES = Type::HashLookupTypeMap.new # :nodoc: - ConnectionAdapters::Column.value_to_boolean value - end - end - - TYPES = {} - - # Register an MySQL +type_id+ with a typecasting object in - # +type+. - def self.register_type(type_id, type) - TYPES[type_id] = type - end + delegate :register_type, :alias_type, to: :TYPES - def self.alias_type(new, old) - TYPES[new] = TYPES[old] - end - - def self.find_type(field) - if field.type == Mysql::Field::TYPE_TINY && field.length > 1 - TYPES[Mysql::Field::TYPE_LONG] - else - TYPES.fetch(field.type) { Fields::Identity.new } + def find_type(field) + if field.type == Mysql::Field::TYPE_TINY && field.length > 1 + TYPES.lookup(Mysql::Field::TYPE_LONG) + else + TYPES.lookup(field.type) + end end end - register_type Mysql::Field::TYPE_TINY, Fields::Boolean.new - register_type Mysql::Field::TYPE_LONG, Fields::Integer.new + register_type Mysql::Field::TYPE_TINY, Type::Boolean.new + register_type Mysql::Field::TYPE_LONG, Type::Integer.new alias_type Mysql::Field::TYPE_LONGLONG, Mysql::Field::TYPE_LONG alias_type Mysql::Field::TYPE_NEWDECIMAL, Mysql::Field::TYPE_LONG - register_type Mysql::Field::TYPE_VAR_STRING, Fields::Identity.new - register_type Mysql::Field::TYPE_BLOB, Fields::Identity.new - register_type Mysql::Field::TYPE_DATE, Fields::Date.new + register_type Mysql::Field::TYPE_DATE, Type::Date.new register_type Mysql::Field::TYPE_DATETIME, Fields::DateTime.new register_type Mysql::Field::TYPE_TIME, Fields::Time.new - register_type Mysql::Field::TYPE_FLOAT, Fields::Float.new + register_type Mysql::Field::TYPE_FLOAT, Type::Float.new + end - Mysql::Field.constants.grep(/TYPE/).map { |class_name| - Mysql::Field.const_get class_name - }.reject { |const| TYPES.key? const }.each do |const| - register_type const, Fields::Identity.new - end + def initialize_type_map(m) # :nodoc: + super + m.register_type %r(datetime)i, Fields::DateTime.new + m.register_type %r(time)i, Fields::Time.new end def exec_without_stmt(sql, name = 'SQL') # :nodoc: @@ -431,7 +343,7 @@ module ActiveRecord fields << field_name if field.decimals > 0 - types[field_name] = Fields::Decimal.new + types[field_name] = Type::Decimal.new else types[field_name] = Fields.find_type field end @@ -447,7 +359,7 @@ module ActiveRecord end end - def execute_and_free(sql, name = nil) + def execute_and_free(sql, name = nil) # :nodoc: result = execute(sql, name) ret = yield result result.free @@ -460,7 +372,7 @@ module ActiveRecord end alias :create :insert_sql - def exec_delete(sql, name, binds) + def exec_delete(sql, name, binds) # :nodoc: affected_rows = 0 exec_query(sql, name, binds) do |n| @@ -497,7 +409,7 @@ module ActiveRecord 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 + # place when an error occurs. To support older MySQL versions, we # need to close the statement and delete the statement from the # cache. stmt.close @@ -558,9 +470,9 @@ module ActiveRecord rows end - # Returns the version of the connected MySQL server. - def version - @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } + # Returns the full version of the connected MySQL server. + def full_version + @full_version ||= @connection.server_info end def set_field_encoding field_name diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb index 0b218f2bfd..1b74c039ce 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters - class PostgreSQLColumn < Column - module ArrayParser + module PostgreSQL + module ArrayParser # :nodoc: DOUBLE_QUOTE = '"' BACKSLASH = "\\" @@ -9,35 +9,23 @@ module ActiveRecord BRACKET_OPEN = '{' BRACKET_CLOSE = '}' - private - # Loads pg_array_parser if available. String parsing can be - # performed quicker by a native extension, which will not create - # a large amount of Ruby objects that will need to be garbage - # collected. pg_array_parser has a C and Java extension - begin - require 'pg_array_parser' - include PgArrayParser - rescue LoadError - def parse_pg_array(string) - parse_data(string) + def parse_pg_array(string) # :nodoc: + local_index = 0 + array = [] + while(local_index < string.length) + case string[local_index] + when BRACKET_OPEN + local_index,array = parse_array_contents(array, string, local_index + 1) + when BRACKET_CLOSE + return array end + local_index += 1 end - def parse_data(string) - local_index = 0 - array = [] - while(local_index < string.length) - case string[local_index] - when BRACKET_OPEN - local_index,array = parse_array_contents(array, string, local_index + 1) - when BRACKET_CLOSE - return array - end - local_index += 1 - end + array + end - array - end + private def parse_array_contents(array, string, index) is_escaping = false diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb deleted file mode 100644 index 551a9289c3..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ /dev/null @@ -1,168 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - class PostgreSQLColumn < Column - module Cast - def point_to_string(point) - "(#{point[0]},#{point[1]})" - end - - def string_to_point(string) - if string[0] == '(' && string[-1] == ')' - string = string[1...-1] - end - string.split(',').map{ |v| Float(v) } - end - - def string_to_time(string) - return string unless String === string - - case string - when 'infinity'; Float::INFINITY - when '-infinity'; -Float::INFINITY - when / BC$/ - super("-" + string.sub(/ BC$/, "")) - else - super - end - end - - def string_to_bit(value) - case value - when /^0x/i - value[2..-1].hex.to_s(2) # Hexadecimal notation - else - value # Bit-string notation - end - end - - def hstore_to_string(object, array_member = false) - if Hash === object - string = object.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(',') - string = escape_hstore(string) if array_member - string - else - object - end - end - - def string_to_hstore(string) - if string.nil? - nil - elsif String === string - Hash[string.scan(HstorePair).map { |k, v| - v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') - k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') - [k, v] - }] - else - string - end - end - - def json_to_string(object) - if Hash === object || Array === object - ActiveSupport::JSON.encode(object) - else - object - end - end - - def array_to_string(value, column, adapter) - casted_values = value.map do |val| - if String === val - if val == "NULL" - "\"#{val}\"" - else - quote_and_escape(adapter.type_cast(val, column, true)) - end - else - adapter.type_cast(val, column, true) - end - end - "{#{casted_values.join(',')}}" - end - - def range_to_string(object) - from = object.begin.respond_to?(:infinite?) && object.begin.infinite? ? '' : object.begin - to = object.end.respond_to?(:infinite?) && object.end.infinite? ? '' : object.end - "[#{from},#{to}#{object.exclude_end? ? ')' : ']'}" - end - - def string_to_json(string) - if String === string - ActiveSupport::JSON.decode(string) - else - string - end - end - - def string_to_cidr(string) - if string.nil? - nil - elsif String === string - begin - IPAddr.new(string) - rescue ArgumentError - nil - end - else - string - end - end - - def cidr_to_string(object) - if IPAddr === object - "#{object.to_s}/#{object.instance_variable_get(:@mask_addr).to_s(2).count('1')}" - else - object - end - end - - def string_to_array(string, oid) - parse_pg_array(string).map {|val| type_cast_array(oid, val)} - end - - private - - HstorePair = begin - quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/ - unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/ - /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/ - end - - def escape_hstore(value) - if value.nil? - 'NULL' - else - if value == "" - '""' - else - '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1') - end - end - end - - ARRAY_ESCAPE = "\\" * 2 * 2 # escape the backslash twice for PG arrays - - def quote_and_escape(value) - case value - when "NULL", Numeric - value - else - value = value.gsub(/\\/, ARRAY_ESCAPE) - value.gsub!(/"/,"\\\"") - "\"#{value}\"" - end - end - - def type_cast_array(oid, value) - if ::Array === value - value.map {|item| type_cast_array(oid, item)} - else - oid.type_cast value - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb index 2cbcd5fd50..37e5c3859c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -4,155 +4,17 @@ module ActiveRecord class PostgreSQLColumn < Column #:nodoc: attr_accessor :array - def initialize(name, default, oid_type, sql_type = nil, null = true) - @oid_type = oid_type - default_value = self.class.extract_value_from_default(default) - + def initialize(name, default, cast_type, sql_type = nil, null = true, default_function = nil) if sql_type =~ /\[\]$/ @array = true - super(name, default_value, sql_type[0..sql_type.length - 3], null) + super(name, default, cast_type, sql_type[0..sql_type.length - 3], null) else @array = false - super(name, default_value, sql_type, null) - end - - @default_function = default if has_default_function?(default_value, default) - end - - def number? - !array && super - end - - def text? - !array && super - end - - # :stopdoc: - class << self - include ConnectionAdapters::PostgreSQLColumn::Cast - include ConnectionAdapters::PostgreSQLColumn::ArrayParser - attr_accessor :money_precision - end - # :startdoc: - - # Extracts the value from a PostgreSQL column default definition. - def self.extract_value_from_default(default) - # This is a performance optimization for Ruby 1.9.2 in development. - # If the value is nil, we return nil straight away without checking - # the regular expressions. If we check each regular expression, - # Regexp#=== will call NilClass#to_str, which will trigger - # method_missing (defined by whiny nil in ActiveSupport) which - # makes this method very very slow. - return default unless default - - case default - when /\A'(.*)'::(num|date|tstz|ts|int4|int8)range\z/m - $1 - # Numeric types - when /\A\(?(-?\d+(\.\d*)?\)?(::bigint)?)\z/ - $1 - # Character types - when /\A\(?'(.*)'::.*\b(?:character varying|bpchar|text)\z/m - $1.gsub(/''/, "'") - # Binary data types - when /\A'(.*)'::bytea\z/m - $1 - # Date/time types - when /\A'(.+)'::(?:time(?:stamp)? with(?:out)? time zone|date)\z/ - $1 - when /\A'(.*)'::interval\z/ - $1 - # Boolean type - when 'true' - true - when 'false' - false - # Geometric types - when /\A'(.*)'::(?:point|line|lseg|box|"?path"?|polygon|circle)\z/ - $1 - # Network address types - when /\A'(.*)'::(?:cidr|inet|macaddr)\z/ - $1 - # Bit string types - when /\AB'(.*)'::"?bit(?: varying)?"?\z/ - $1 - # XML type - when /\A'(.*)'::xml\z/m - $1 - # Arrays - when /\A'(.*)'::"?\D+"?\[\]\z/ - $1 - # Hstore - when /\A'(.*)'::hstore\z/ - $1 - # JSON - when /\A'(.*)'::json\z/ - $1 - # Object identifier types - when /\A-?\d+\z/ - $1 - else - # Anything else is blank, some user type, or some function - # and we can't know the value of that, so return nil. - nil + super(name, default, cast_type, sql_type, null) end - end - def type_cast_for_write(value) - if @oid_type.respond_to?(:type_cast_for_write) - @oid_type.type_cast_for_write(value) - else - super - end + @default_function = default_function end - - def type_cast(value) - return if value.nil? - return super if encoded? - - @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 - when /^smallint/i; 2 - when /^timestamp/i; nil - else super - end - end - - # Extracts the scale from PostgreSQL-specific data types. - def extract_scale(sql_type) - # Money type has a fixed scale of 2. - sql_type =~ /^money/ ? 2 : super - end - - # Extracts the precision from PostgreSQL-specific data types. - def extract_precision(sql_type) - if sql_type == 'money' - self.class.money_precision - elsif sql_type =~ /timestamp/i - $1.to_i if sql_type =~ /\((\d+)\)/ - else - super - end - end - - # Maps PostgreSQL-specific data types to logical Rails types. - def simplified_type(field_type) - @oid_type.simplified_type(field_type) || super - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index 168b08ba75..89a7257d77 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -1,6 +1,6 @@ module ActiveRecord module ConnectionAdapters - class PostgreSQLAdapter < AbstractAdapter + module PostgreSQL module DatabaseStatements def explain(arel, binds = []) sql = "EXPLAIN #{to_sql(arel, binds)}" @@ -94,6 +94,11 @@ module ActiveRecord super.insert end + # The internal PostgreSQL identifier of the money data type. + MONEY_COLUMN_TYPE_OID = 790 #:nodoc: + # The internal PostgreSQL identifier of the BYTEA data type. + BYTEA_COLUMN_TYPE_OID = 17 #:nodoc: + # create a 2D array representing the result set def result_as_array(res) #:nodoc: # check if we have any binary column and if they need escaping diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 540b3694b5..d28a2b4fa0 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -1,440 +1,35 @@ -require 'active_record/connection_adapters/abstract_adapter' +require 'active_record/connection_adapters/postgresql/oid/infinity' + +require 'active_record/connection_adapters/postgresql/oid/array' +require 'active_record/connection_adapters/postgresql/oid/bit' +require 'active_record/connection_adapters/postgresql/oid/bit_varying' +require 'active_record/connection_adapters/postgresql/oid/bytea' +require 'active_record/connection_adapters/postgresql/oid/cidr' +require 'active_record/connection_adapters/postgresql/oid/date' +require 'active_record/connection_adapters/postgresql/oid/date_time' +require 'active_record/connection_adapters/postgresql/oid/decimal' +require 'active_record/connection_adapters/postgresql/oid/enum' +require 'active_record/connection_adapters/postgresql/oid/float' +require 'active_record/connection_adapters/postgresql/oid/hstore' +require 'active_record/connection_adapters/postgresql/oid/inet' +require 'active_record/connection_adapters/postgresql/oid/integer' +require 'active_record/connection_adapters/postgresql/oid/json' +require 'active_record/connection_adapters/postgresql/oid/jsonb' +require 'active_record/connection_adapters/postgresql/oid/money' +require 'active_record/connection_adapters/postgresql/oid/point' +require 'active_record/connection_adapters/postgresql/oid/range' +require 'active_record/connection_adapters/postgresql/oid/specialized_string' +require 'active_record/connection_adapters/postgresql/oid/time' +require 'active_record/connection_adapters/postgresql/oid/uuid' +require 'active_record/connection_adapters/postgresql/oid/vector' +require 'active_record/connection_adapters/postgresql/oid/xml' + +require 'active_record/connection_adapters/postgresql/oid/type_map_initializer' module ActiveRecord module ConnectionAdapters - class PostgreSQLAdapter < AbstractAdapter - module OID - class Type - def type; end - def simplified_type(sql_type); type end - - def infinity(options = {}) - ::Float::INFINITY * (options[:negative] ? -1 : 1) - end - end - - class Identity < Type - def type_cast(value) - value - end - end - - class String < Type - def type; :string end - - def type_cast(value) - return if value.nil? - - value.to_s - end - end - - class SpecializedString < OID::String - def type; @type end - - def initialize(type) - @type = type - end - end - - class Text < OID::String - def type; :text end - end - - class Bit < Type - def type; :string end - - def type_cast(value) - if ::String === value - ConnectionAdapters::PostgreSQLColumn.string_to_bit value - else - value - end - end - end - - class Bytea < Type - def type; :binary end - - def type_cast(value) - return if value.nil? - PGconn.unescape_bytea value - end - end - - class Money < Type - def type; :decimal end - - def type_cast(value) - return if value.nil? - return value unless ::String === value - - # Because money output is formatted according to the locale, there are two - # cases to consider (note the decimal separators): - # (1) $12,345,678.12 - # (2) $12.345.678,12 - # Negative values are represented as follows: - # (3) -$2.55 - # (4) ($2.55) - - value.sub!(/^\((.+)\)$/, '-\1') # (4) - case value - when /^-?\D+[\d,]+\.\d{2}$/ # (1) - value.gsub!(/[^-\d.]/, '') - when /^-?\D+[\d.]+,\d{2}$/ # (2) - value.gsub!(/[^-\d,]/, '').sub!(/,/, '.') - end - - ConnectionAdapters::Column.value_to_decimal value - end - end - - class Vector < Type - attr_reader :delim, :subtype - - # +delim+ corresponds to the `typdelim` column in the pg_types - # table. +subtype+ is derived from the `typelem` column in the - # pg_types table. - def initialize(delim, subtype) - @delim = delim - @subtype = subtype - end - - # FIXME: this should probably split on +delim+ and use +subtype+ - # to cast the values. Unfortunately, the current Rails behavior - # is to just return the string. - def type_cast(value) - value - end - end - - class Point < Type - def type; :string end - - def type_cast(value) - if ::String === value - ConnectionAdapters::PostgreSQLColumn.string_to_point value - else - value - end - end - end - - class Array < Type - def type; @subtype.type end - - attr_reader :subtype - def initialize(subtype) - @subtype = subtype - end - - def type_cast(value) - if ::String === value - ConnectionAdapters::PostgreSQLColumn.string_to_array value, @subtype - else - value - end - end - end - - class Range < Type - attr_reader :subtype - def simplified_type(sql_type); sql_type.to_sym end - - def initialize(subtype) - @subtype = subtype - end - - def extract_bounds(value) - from, to = value[1..-2].split(',') - { - from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from, - to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to, - exclude_start: (value[0] == '('), - exclude_end: (value[-1] == ')') - } - end - - def infinity?(value) - value.respond_to?(:infinite?) && value.infinite? - end - - def type_cast_single(value) - infinity?(value) ? value : @subtype.type_cast(value) - end - - def type_cast(value) - return if value.nil? || value == 'empty' - return value if value.is_a?(::Range) - - extracted = extract_bounds(value) - from = type_cast_single extracted[:from] - to = type_cast_single extracted[:to] - - if !infinity?(from) && extracted[:exclude_start] - if from.respond_to?(:succ) - from = from.succ - ActiveSupport::Deprecation.warn <<-MESSAGE -Excluding the beginning of a Range is only partialy supported through `#succ`. -This is not reliable and will be removed in the future. - MESSAGE - else - raise ArgumentError, "The Ruby Range object does not support excluding the beginning of a Range. (unsupported value: '#{value}')" - end - end - ::Range.new(from, to, extracted[:exclude_end]) - end - end - - class Integer < Type - def type; :integer end - - def type_cast(value) - return if value.nil? - - ConnectionAdapters::Column.value_to_integer value - end - end - - class Boolean < Type - def type; :boolean end - - def type_cast(value) - return if value.nil? - - ConnectionAdapters::Column.value_to_boolean value - end - end - - class Timestamp < Type - def type; :timestamp; end - def simplified_type(sql_type) - :datetime - end - - def type_cast(value) - return if value.nil? - - # FIXME: probably we can improve this since we know it is PG - # specific - ConnectionAdapters::PostgreSQLColumn.string_to_time value - end - end - - class Date < Type - def type; :date; end - - def type_cast(value) - return if value.nil? - - # FIXME: probably we can improve this since we know it is PG - # specific - ConnectionAdapters::Column.value_to_date value - end - end - - class Time < Type - def type; :time end - - def type_cast(value) - return if value.nil? - - # FIXME: probably we can improve this since we know it is PG - # specific - ConnectionAdapters::Column.string_to_dummy_time value - end - end - - class Float < Type - def type; :float end - - def type_cast(value) - return if value.nil? - - value.to_f - end - end - - class Decimal < Type - def type; :decimal end - - def type_cast(value) - return if value.nil? - - ConnectionAdapters::Column.value_to_decimal value - end - - def infinity(options = {}) - BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1) - end - end - - class Enum < Type - def type; :enum end - - def type_cast(value) - value.to_s - end - end - - class Hstore < Type - def type; :hstore end - - def type_cast_for_write(value) - ConnectionAdapters::PostgreSQLColumn.hstore_to_string value - end - - def type_cast(value) - return if value.nil? - - ConnectionAdapters::PostgreSQLColumn.string_to_hstore value - end - - def accessor - ActiveRecord::Store::StringKeyedHashAccessor - end - end - - class Cidr < Type - def type; :cidr end - def type_cast(value) - return if value.nil? - - ConnectionAdapters::PostgreSQLColumn.string_to_cidr value - end - end - class Inet < Cidr - def type; :inet end - end - - class Json < Type - def type; :json end - - 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 Uuid < Type - def type; :uuid end - def type_cast(value) - value.presence - end - end - - class TypeMap - def initialize - @mapping = {} - end - - def []=(oid, type) - @mapping[oid] = type - end - - def [](oid) - @mapping[oid] - end - - def clear - @mapping.clear - end - - def key?(oid) - @mapping.key? oid - end - - def fetch(ftype, fmod) - # The type for the numeric depends on the width of the field, - # so we'll do something special here. - # - # When dealing with decimal columns: - # - # places after decimal = fmod - 4 & 0xffff - # places before decimal = (fmod - 4) >> 16 & 0xffff - if ftype == 1700 && (fmod - 4 & 0xffff).zero? - ftype = 23 - end - - @mapping.fetch(ftype) { |oid| yield oid, fmod } - end - end - - # When the PG adapter connects, the pg_type table is queried. The - # key of this hash maps to the `typname` column from the table. - # type_map is then dynamically built with oids as the key and type - # objects as values. - NAMES = Hash.new { |h,k| # :nodoc: - h[k] = OID::Identity.new - } - - # Register an OID type named +name+ with a typecasting object in - # +type+. +name+ should correspond to the `typname` column in - # the `pg_type` table. - def self.register_type(name, type) - NAMES[name] = type - end - - # Alias the +old+ type to the +new+ type. - def self.alias_type(new, old) - NAMES[new] = NAMES[old] - end - - # Is +name+ a registered type? - def self.registered_type?(name) - NAMES.key? name - end - - register_type 'int2', OID::Integer.new - alias_type 'int4', 'int2' - alias_type 'int8', 'int2' - alias_type 'oid', 'int2' - register_type 'numeric', OID::Decimal.new - register_type 'float4', OID::Float.new - alias_type 'float8', 'float4' - register_type 'text', OID::Text.new - register_type 'varchar', OID::String.new - alias_type 'char', 'varchar' - alias_type 'bpchar', 'varchar' - register_type 'bool', OID::Boolean.new - register_type 'bit', OID::Bit.new - alias_type 'varbit', 'bit' - register_type 'timestamp', OID::Timestamp.new - alias_type 'timestamptz', 'timestamp' - register_type 'date', OID::Date.new - register_type 'time', OID::Time.new - - register_type 'money', OID::Money.new - register_type 'bytea', OID::Bytea.new - register_type 'point', OID::Point.new - register_type 'hstore', OID::Hstore.new - register_type 'json', OID::Json.new - register_type 'cidr', OID::Cidr.new - register_type 'inet', OID::Inet.new - register_type 'uuid', OID::Uuid.new - register_type 'xml', SpecializedString.new(:xml) - register_type 'tsvector', SpecializedString.new(:tsvector) - register_type 'macaddr', SpecializedString.new(:macaddr) - register_type 'citext', SpecializedString.new(:citext) - register_type 'ltree', SpecializedString.new(:ltree) - - # FIXME: why are we keeping these types as strings? - alias_type 'interval', 'varchar' - alias_type 'path', 'varchar' - alias_type 'line', 'varchar' - alias_type 'polygon', 'varchar' - alias_type 'circle', 'varchar' - alias_type 'lseg', 'varchar' - alias_type 'box', 'varchar' + module PostgreSQL + module OID # :nodoc: end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb new file mode 100644 index 0000000000..cd5efe2bb8 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -0,0 +1,96 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Array < Type::Value # :nodoc: + include Type::Mutable + + # Loads pg_array_parser if available. String parsing can be + # performed quicker by a native extension, which will not create + # a large amount of Ruby objects that will need to be garbage + # collected. pg_array_parser has a C and Java extension + begin + require 'pg_array_parser' + include PgArrayParser + rescue LoadError + require 'active_record/connection_adapters/postgresql/array_parser' + include PostgreSQL::ArrayParser + end + + attr_reader :subtype, :delimiter + delegate :type, to: :subtype + + def initialize(subtype, delimiter = ',') + @subtype = subtype + @delimiter = delimiter + end + + def type_cast_from_database(value) + if value.is_a?(::String) + type_cast_array(parse_pg_array(value), :type_cast_from_database) + else + super + end + end + + def type_cast_from_user(value) + type_cast_array(value, :type_cast_from_user) + end + + def type_cast_for_database(value) + if value.is_a?(::Array) + cast_value_for_database(value) + else + super + end + end + + private + + def type_cast_array(value, method) + if value.is_a?(::Array) + value.map { |item| type_cast_array(item, method) } + else + @subtype.public_send(method, value) + end + end + + def cast_value_for_database(value) + if value.is_a?(::Array) + casted_values = value.map { |item| cast_value_for_database(item) } + "{#{casted_values.join(delimiter)}}" + else + quote_and_escape(subtype.type_cast_for_database(value)) + end + end + + ARRAY_ESCAPE = "\\" * 2 * 2 # escape the backslash twice for PG arrays + + def quote_and_escape(value) + case value + when ::String + if string_requires_quoting?(value) + value = value.gsub(/\\/, ARRAY_ESCAPE) + value.gsub!(/"/,"\\\"") + %("#{value}") + else + value + end + when nil then "NULL" + else value + end + end + + # See http://www.postgresql.org/docs/9.2/static/arrays.html#ARRAYS-IO + # for a list of all cases in which strings will be quoted. + def string_requires_quoting?(string) + string.empty? || + string == "NULL" || + string =~ /[\{\}"\\\s]/ || + string.include?(delimiter) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb new file mode 100644 index 0000000000..1dbb40ca1d --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb @@ -0,0 +1,52 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Bit < Type::Value # :nodoc: + def type + :bit + end + + def type_cast(value) + if ::String === value + case value + when /^0x/i + value[2..-1].hex.to_s(2) # Hexadecimal notation + else + value # Bit-string notation + end + else + value + end + end + + def type_cast_for_database(value) + Data.new(super) if value + end + + class Data + def initialize(value) + @value = value + end + + def to_s + value + end + + def binary? + /\A[01]*\Z/ === value + end + + def hex? + /\A[0-9A-F]*\Z/i === value + end + + protected + + attr_reader :value + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb new file mode 100644 index 0000000000..4c21097d48 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class BitVarying < OID::Bit # :nodoc: + def type + :bit_varying + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb new file mode 100644 index 0000000000..997613d7be --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb @@ -0,0 +1,14 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Bytea < Type::Binary # :nodoc: + def type_cast_from_database(value) + return if value.nil? + PGconn.unescape_bytea(super) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb new file mode 100644 index 0000000000..a53b4ee8e2 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb @@ -0,0 +1,46 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Cidr < Type::Value # :nodoc: + def type + :cidr + end + + def type_cast_for_schema(value) + subnet_mask = value.instance_variable_get(:@mask_addr) + + # If the subnet mask is equal to /32, don't output it + if subnet_mask == (2**32 - 1) + "\"#{value.to_s}\"" + else + "\"#{value.to_s}/#{subnet_mask.to_s(2).count('1')}\"" + end + end + + def type_cast_for_database(value) + if IPAddr === value + "#{value.to_s}/#{value.instance_variable_get(:@mask_addr).to_s(2).count('1')}" + else + value + end + end + + def cast_value(value) + if value.nil? + nil + elsif String === value + begin + IPAddr.new(value) + rescue ArgumentError + nil + end + else + value + end + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb new file mode 100644 index 0000000000..1d8d264530 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb @@ -0,0 +1,11 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Date < Type::Date # :nodoc: + include Infinity + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb new file mode 100644 index 0000000000..b9e7894e5c --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb @@ -0,0 +1,27 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class DateTime < Type::DateTime # :nodoc: + include Infinity + + def cast_value(value) + if value.is_a?(::String) + case value + when 'infinity' then ::Float::INFINITY + when '-infinity' then -::Float::INFINITY + when / BC$/ + astronomical_year = format("%04d", -value[/^\d+/].to_i + 1) + super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year)) + else + super + end + else + value + end + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb new file mode 100644 index 0000000000..43d22c8daf --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Decimal < Type::Decimal # :nodoc: + def infinity(options = {}) + BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb new file mode 100644 index 0000000000..77d5038efd --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb @@ -0,0 +1,17 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Enum < Type::Value # :nodoc: + def type + :enum + end + + def type_cast(value) + value.to_s + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb new file mode 100644 index 0000000000..78ef94b912 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb @@ -0,0 +1,21 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Float < Type::Float # :nodoc: + include Infinity + + def cast_value(value) + case value + when ::Float then value + when 'Infinity' then ::Float::INFINITY + when '-Infinity' then -::Float::INFINITY + when 'NaN' then ::Float::NAN + else value.to_f + end + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb new file mode 100644 index 0000000000..be4525c94f --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb @@ -0,0 +1,59 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Hstore < Type::Value # :nodoc: + include Type::Mutable + + def type + :hstore + end + + def type_cast_from_database(value) + if value.is_a?(::String) + ::Hash[value.scan(HstorePair).map { |k, v| + v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') + k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') + [k, v] + }] + else + value + end + end + + def type_cast_for_database(value) + if value.is_a?(::Hash) + value.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(', ') + else + value + end + end + + def accessor + ActiveRecord::Store::StringKeyedHashAccessor + end + + private + + HstorePair = begin + quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/ + unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/ + /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/ + end + + def escape_hstore(value) + if value.nil? + 'NULL' + else + if value == "" + '""' + else + '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1') + end + end + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb new file mode 100644 index 0000000000..96486fa65b --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Inet < Cidr # :nodoc: + def type + :inet + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb new file mode 100644 index 0000000000..e47780399a --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + module Infinity # :nodoc: + def infinity(options = {}) + options[:negative] ? -::Float::INFINITY : ::Float::INFINITY + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb new file mode 100644 index 0000000000..59abdc0009 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb @@ -0,0 +1,11 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Integer < Type::Integer # :nodoc: + include Infinity + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb new file mode 100644 index 0000000000..e12ddd9901 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb @@ -0,0 +1,35 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Json < Type::Value # :nodoc: + include Type::Mutable + + def type + :json + end + + def type_cast_from_database(value) + if value.is_a?(::String) + ::ActiveSupport::JSON.decode(value) + else + super + end + end + + def type_cast_for_database(value) + if value.is_a?(::Array) || value.is_a?(::Hash) + ::ActiveSupport::JSON.encode(value) + else + super + end + end + + def accessor + ActiveRecord::Store::StringKeyedHashAccessor + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb new file mode 100644 index 0000000000..34ed32ad35 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb @@ -0,0 +1,23 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Jsonb < Json # :nodoc: + def type + :jsonb + end + + def changed_in_place?(raw_old_value, new_value) + # Postgres does not preserve insignificant whitespaces when + # roundtripping jsonb columns. This causes some false positives for + # the comparison here. Therefore, we need to parse and re-dump the + # raw value here to ensure the insignificant whitespaces are + # consitent with our encoder's output. + raw_old_value = type_cast_for_database(type_cast_from_database(raw_old_value)) + super(raw_old_value, new_value) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb new file mode 100644 index 0000000000..df890c2ed6 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb @@ -0,0 +1,43 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Money < Type::Decimal # :nodoc: + include Infinity + + class_attribute :precision + + def type + :money + end + + def scale + 2 + end + + def cast_value(value) + return value unless ::String === value + + # Because money output is formatted according to the locale, there are two + # cases to consider (note the decimal separators): + # (1) $12,345,678.12 + # (2) $12.345.678,12 + # Negative values are represented as follows: + # (3) -$2.55 + # (4) ($2.55) + + value.sub!(/^\((.+)\)$/, '-\1') # (4) + case value + when /^-?\D+[\d,]+\.\d{2}$/ # (1) + value.gsub!(/[^-\d.]/, '') + when /^-?\D+[\d.]+,\d{2}$/ # (2) + value.gsub!(/[^-\d,]/, '').sub!(/,/, '.') + end + + super(value) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb new file mode 100644 index 0000000000..bac8b01d6b --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb @@ -0,0 +1,43 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Point < Type::Value # :nodoc: + include Type::Mutable + + def type + :point + end + + def type_cast(value) + case value + when ::String + if value[0] == '(' && value[-1] == ')' + value = value[1...-1] + end + type_cast(value.split(',')) + when ::Array + value.map { |v| Float(v) } + else + value + end + end + + def type_cast_for_database(value) + if value.is_a?(::Array) + "(#{number_for_point(value[0])},#{number_for_point(value[1])})" + else + super + end + end + + private + + def number_for_point(number) + number.to_s.gsub(/\.0$/, '') + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb new file mode 100644 index 0000000000..ae967d5167 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -0,0 +1,76 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Range < Type::Value # :nodoc: + attr_reader :subtype, :type + + def initialize(subtype, type) + @subtype = subtype + @type = type + end + + def type_cast_for_schema(value) + value.inspect.gsub('Infinity', '::Float::INFINITY') + end + + def cast_value(value) + return if value == 'empty' + return value if value.is_a?(::Range) + + extracted = extract_bounds(value) + from = type_cast_single extracted[:from] + to = type_cast_single extracted[:to] + + if !infinity?(from) && extracted[:exclude_start] + if from.respond_to?(:succ) + from = from.succ + ActiveSupport::Deprecation.warn <<-MESSAGE +Excluding the beginning of a Range is only partialy supported through `#succ`. +This is not reliable and will be removed in the future. + MESSAGE + else + raise ArgumentError, "The Ruby Range object does not support excluding the beginning of a Range. (unsupported value: '#{value}')" + end + end + ::Range.new(from, to, extracted[:exclude_end]) + end + + def type_cast_for_database(value) + if value.is_a?(::Range) + from = type_cast_single_for_database(value.begin) + to = type_cast_single_for_database(value.end) + "[#{from},#{to}#{value.exclude_end? ? ')' : ']'}" + else + super + end + end + + private + + def type_cast_single(value) + infinity?(value) ? value : @subtype.type_cast_from_database(value) + end + + def type_cast_single_for_database(value) + infinity?(value) ? '' : @subtype.type_cast_for_database(value) + end + + def extract_bounds(value) + from, to = value[1..-2].split(',') + { + from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from, + to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to, + exclude_start: (value[0] == '('), + exclude_end: (value[-1] == ')') + } + end + + def infinity?(value) + value.respond_to?(:infinite?) && value.infinite? + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb new file mode 100644 index 0000000000..2d2fede4e8 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb @@ -0,0 +1,15 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class SpecializedString < Type::String # :nodoc: + attr_reader :type + + def initialize(type) + @type = type + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb new file mode 100644 index 0000000000..8f0246eddb --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb @@ -0,0 +1,11 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Time < Type::Time # :nodoc: + include Infinity + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb new file mode 100644 index 0000000000..e396ff4a1e --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb @@ -0,0 +1,85 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + # This class uses the data from PostgreSQL pg_type table to build + # the OID -> Type mapping. + # - OID is and integer representing the type. + # - Type is an OID::Type object. + # This class has side effects on the +store+ passed during initialization. + class TypeMapInitializer # :nodoc: + def initialize(store) + @store = store + end + + def run(records) + nodes = records.reject { |row| @store.key? row['oid'].to_i } + mapped, nodes = nodes.partition { |row| @store.key? row['typname'] } + ranges, nodes = nodes.partition { |row| row['typtype'] == 'r' } + enums, nodes = nodes.partition { |row| row['typtype'] == 'e' } + domains, nodes = nodes.partition { |row| row['typtype'] == 'd' } + arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' } + composites, nodes = nodes.partition { |row| row['typelem'] != '0' } + + mapped.each { |row| register_mapped_type(row) } + enums.each { |row| register_enum_type(row) } + domains.each { |row| register_domain_type(row) } + arrays.each { |row| register_array_type(row) } + ranges.each { |row| register_range_type(row) } + composites.each { |row| register_composite_type(row) } + end + + private + def register_mapped_type(row) + alias_type row['oid'], row['typname'] + end + + def register_enum_type(row) + register row['oid'], OID::Enum.new + end + + def register_array_type(row) + if subtype = @store.lookup(row['typelem'].to_i) + register row['oid'], OID::Array.new(subtype, row['typdelim']) + end + end + + def register_range_type(row) + if subtype = @store.lookup(row['rngsubtype'].to_i) + register row['oid'], OID::Range.new(subtype, row['typname'].to_sym) + end + end + + def register_domain_type(row) + if base_type = @store.lookup(row["typbasetype"].to_i) + register row['oid'], base_type + else + warn "unknown base type (OID: #{row["typbasetype"]}) for domain #{row["typname"]}." + end + end + + def register_composite_type(row) + if subtype = @store.lookup(row['typelem'].to_i) + register row['oid'], OID::Vector.new(row['typdelim'], subtype) + end + end + + def register(oid, oid_type) + oid = assert_valid_registration(oid, oid_type) + @store.register_type(oid, oid_type) + end + + def alias_type(oid, target) + oid = assert_valid_registration(oid, target) + @store.alias_type(oid, target) + end + + def assert_valid_registration(oid, oid_type) + raise ArgumentError, "can't register nil type for OID #{oid}" if oid_type.nil? + oid.to_i + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb new file mode 100644 index 0000000000..dd97393eac --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb @@ -0,0 +1,26 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Uuid < Type::Value # :nodoc: + RFC_4122 = %r{\A\{?[a-fA-F0-9]{4}-? + [a-fA-F0-9]{4}-? + [a-fA-F0-9]{4}-? + [1-5][a-fA-F0-9]{3}-? + [8-Bab][a-fA-F0-9]{3}-? + [a-fA-F0-9]{4}-? + [a-fA-F0-9]{4}-? + [a-fA-F0-9]{4}-?\}?\z}x + + def type + :uuid + end + + def type_cast(value) + value.to_s[RFC_4122, 0] + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb new file mode 100644 index 0000000000..de4187b028 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb @@ -0,0 +1,26 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Vector < Type::Value # :nodoc: + attr_reader :delim, :subtype + + # +delim+ corresponds to the `typdelim` column in the pg_types + # table. +subtype+ is derived from the `typelem` column in the + # pg_types table. + def initialize(delim, subtype) + @delim = delim + @subtype = subtype + end + + # FIXME: this should probably split on +delim+ and use +subtype+ + # to cast the values. Unfortunately, the current Rails behavior + # is to just return the string. + def type_cast(value) + value + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb new file mode 100644 index 0000000000..7323f12763 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb @@ -0,0 +1,32 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Xml < Type::String # :nodoc: + def type + :xml + end + + def text? + false + end + + def type_cast_for_database(value) + return unless value + Data.new(super) + end + + class Data # :nodoc: + def initialize(value) + @value = value + end + + def to_s + @value + end + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 403e37fde9..cf5c8d288e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -1,6 +1,6 @@ module ActiveRecord module ConnectionAdapters - class PostgreSQLAdapter < AbstractAdapter + module PostgreSQL module Quoting # Escapes binary strings for bytea input to the database. def escape_bytea(value) @@ -18,124 +18,18 @@ module ActiveRecord def quote(value, column = nil) #:nodoc: return super unless column - sql_type = type_to_sql(column.type, column.limit, column.precision, column.scale) - case value - when Range - if /range$/ =~ sql_type - "'#{PostgreSQLColumn.range_to_string(value)}'::#{sql_type}" - else - super - end - when Array - case sql_type - when 'point' then super(PostgreSQLColumn.point_to_string(value)) - when 'json' then super(PostgreSQLColumn.json_to_string(value)) - else - if column.array - "'#{PostgreSQLColumn.array_to_string(value, column, self).gsub(/'/, "''")}'" - else - super - end - end - when Hash - case sql_type - when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column) - when 'json' then super(PostgreSQLColumn.json_to_string(value), column) - else super - end - when IPAddr - case sql_type - when 'inet', 'cidr' then super(PostgreSQLColumn.cidr_to_string(value), column) - else super - end when Float - if value.infinite? && column.type == :datetime - "'#{value.to_s.downcase}'" - elsif value.infinite? || value.nan? + if value.infinite? || value.nan? "'#{value.to_s}'" else super end - when Numeric - if sql_type == 'money' || [:string, :text].include?(column.type) - # Not truly string input, so doesn't require (or allow) escape string syntax. - "'#{value}'" - else - super - end - when String - case sql_type - when 'bytea' then "'#{escape_bytea(value)}'" - when 'xml' then "xml '#{quote_string(value)}'" - when /^bit/ - case value - when /^[01]*$/ then "B'#{value}'" # Bit-string notation - when /^[0-9A-F]*$/i then "X'#{value}'" # Hexadecimal notation - end - else - super - end else super end end - def type_cast(value, column, array_member = false) - return super(value, column) unless column - - case value - when Range - if /range$/ =~ column.sql_type - PostgreSQLColumn.range_to_string(value) - else - super(value, column) - end - when NilClass - if column.array && array_member - 'NULL' - elsif column.array - value - else - super(value, column) - end - when Array - case column.sql_type - when 'point' then PostgreSQLColumn.point_to_string(value) - when 'json' then PostgreSQLColumn.json_to_string(value) - else - if column.array - PostgreSQLColumn.array_to_string(value, column, self) - else - super(value, column) - end - end - when String - if 'bytea' == column.sql_type - # Return a bind param hash with format as binary. - # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc - # for more information - { value: value, format: 1 } - else - super(value, column) - end - when Hash - case column.sql_type - when 'hstore' then PostgreSQLColumn.hstore_to_string(value, array_member) - when 'json' then PostgreSQLColumn.json_to_string(value) - else super(value, column) - end - when IPAddr - if %w(inet cidr).include? column.sql_type - PostgreSQLColumn.cidr_to_string(value) - else - super(value, column) - end - else - super(value, column) - end - end - # Quotes strings for use in SQL input. def quote_string(s) #:nodoc: @connection.escape(s) @@ -150,14 +44,7 @@ module ActiveRecord # - "schema.name".table_name # - "schema.name"."table.name" def quote_table_name(name) - schema, name_part = extract_pg_identifier_from_name(name.to_s) - - unless name_part - quote_column_name(schema) - else - table_name, name_part = extract_pg_identifier_from_name(name_part) - "#{quote_column_name(schema)}.#{quote_column_name(table_name)}" - end + Utils.extract_schema_qualified_name(name.to_s).quoted end def quote_table_name_for_assignment(table, attr) @@ -177,8 +64,9 @@ module ActiveRecord result = "#{result}.#{sprintf("%06d", value.usec)}" end - if value.year < 0 - result = result.sub(/^-/, "") + " BC" + if value.year <= 0 + bce_year = format("%04d", -value.year + 1) + result = result.sub(/^-?\d+/, bce_year) + " BC" end result end @@ -187,8 +75,41 @@ module ActiveRecord def quote_default_value(value, column) #:nodoc: if column.type == :uuid && value =~ /\(\)/ value - else - quote(value) + else + quote(value, column) + end + end + + private + + def _quote(value) + case value + when Type::Binary::Data + "'#{escape_bytea(value.to_s)}'" + when OID::Xml::Data + "xml '#{quote_string(value.to_s)}'" + when OID::Bit::Data + if value.binary? + "B'#{value}'" + elsif value.hex? + "X'#{value}'" + end + else + super + end + end + + def _type_cast(value) + case value + when Type::Binary::Data + # Return a bind param hash with format as binary. + # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc + # for more information + { value: value.to_s, format: 1 } + when OID::Xml::Data, OID::Bit::Data + value.to_s + else + super end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb index bc775394a6..52b307c432 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb @@ -1,12 +1,12 @@ module ActiveRecord module ConnectionAdapters - class PostgreSQLAdapter < AbstractAdapter - module ReferentialIntegrity - def supports_disable_referential_integrity? #:nodoc: + module PostgreSQL + module ReferentialIntegrity # :nodoc: + def supports_disable_referential_integrity? # :nodoc: true end - def disable_referential_integrity #:nodoc: + def disable_referential_integrity # :nodoc: if supports_disable_referential_integrity? begin execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";")) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb new file mode 100644 index 0000000000..83554bbf74 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -0,0 +1,154 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module ColumnMethods + def xml(*args) + options = args.extract_options! + column(args[0], :xml, options) + end + + def tsvector(*args) + options = args.extract_options! + column(args[0], :tsvector, options) + end + + def int4range(name, options = {}) + column(name, :int4range, options) + end + + def int8range(name, options = {}) + column(name, :int8range, options) + end + + def tsrange(name, options = {}) + column(name, :tsrange, options) + end + + def tstzrange(name, options = {}) + column(name, :tstzrange, options) + end + + def numrange(name, options = {}) + column(name, :numrange, options) + end + + def daterange(name, options = {}) + column(name, :daterange, options) + end + + def hstore(name, options = {}) + column(name, :hstore, options) + end + + def ltree(name, options = {}) + column(name, :ltree, options) + end + + def inet(name, options = {}) + column(name, :inet, options) + end + + def cidr(name, options = {}) + column(name, :cidr, options) + end + + def macaddr(name, options = {}) + column(name, :macaddr, options) + end + + def uuid(name, options = {}) + column(name, :uuid, options) + end + + def json(name, options = {}) + column(name, :json, options) + end + + def jsonb(name, options = {}) + column(name, :jsonb, options) + end + + def citext(name, options = {}) + column(name, :citext, options) + end + + def point(name, options = {}) + column(name, :point, options) + end + + def bit(name, options) + column(name, :bit, options) + end + + def bit_varying(name, options) + column(name, :bit_varying, options) + end + + def money(name, options) + column(name, :money, options) + end + end + + class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition + attr_accessor :array + end + + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition + include ColumnMethods + + # Defines the primary key field. + # Use of the native PostgreSQL UUID type is supported, and can be used + # by defining your tables as such: + # + # create_table :stuffs, id: :uuid do |t| + # t.string :content + # t.timestamps + # end + # + # By default, this will use the +uuid_generate_v4()+ function from the + # +uuid-ossp+ extension, which MUST be enabled on your database. To enable + # the +uuid-ossp+ extension, you can use the +enable_extension+ method in your + # migrations. To use a UUID primary key without +uuid-ossp+ enabled, you can + # set the +:default+ option to +nil+: + # + # create_table :stuffs, id: false do |t| + # t.primary_key :id, :uuid, default: nil + # t.uuid :foo_id + # t.timestamps + # end + # + # You may also pass a different UUID generation function from +uuid-ossp+ + # or another library. + # + # Note that setting the UUID primary key default value to +nil+ will + # require you to assure that you always provide a UUID value before saving + # a record (as primary keys cannot be +nil+). This might be done via the + # +SecureRandom.uuid+ method and a +before_save+ callback, for instance. + def primary_key(name, type = :primary_key, options = {}) + return super unless type == :uuid + options[:default] = options.fetch(:default, 'uuid_generate_v4()') + options[:primary_key] = true + column name, type, options + end + + def column(name, type = nil, options = {}) + super + column = self[name] + column.array = options[:array] + + self + end + + private + + def create_column_definition(name, type) + PostgreSQL::ColumnDefinition.new name, type + end + end + + class Table < ActiveRecord::ConnectionAdapters::Table + include ColumnMethods + end + end + end +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 1dc7a6f0fd..7042817672 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -1,18 +1,18 @@ module ActiveRecord module ConnectionAdapters - class PostgreSQLAdapter < AbstractAdapter + module PostgreSQL class SchemaCreation < AbstractAdapter::SchemaCreation private def visit_AddColumn(o) - sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale) + sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale) sql = "ADD COLUMN #{quote_column_name(o.name)} #{sql_type}" add_column_options!(sql, column_options(o)) end def visit_ColumnDefinition(o) sql = super - if o.primary_key? && o.type == :uuid + if o.primary_key? && o.type != :primary_key sql << " PRIMARY KEY " add_column_options!(sql, column_options(o)) end @@ -31,10 +31,14 @@ module ActiveRecord super end end - end - def schema_creation - SchemaCreation.new self + def type_for_column(column) + if column.array + @conn.lookup_cast_type("#{column.sql_type}[]") + else + super + end + end end module SchemaStatements @@ -101,16 +105,16 @@ module ActiveRecord # If the schema is not specified as part of +name+ then it will only find tables within # the current schema search path (regardless of permissions to access tables in other schemas) def table_exists?(name) - schema, table = Utils.extract_schema_and_table(name.to_s) - return false unless table + name = Utils.extract_schema_qualified_name(name.to_s) + return false unless name.identifier exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 SELECT COUNT(*) FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind IN ('r','v','m') -- (r)elation/table, (v)iew, (m)aterialized view - AND c.relname = '#{table.gsub(/(^"|"$)/,'')}' - AND n.nspname = #{schema ? "'#{schema}'" : 'ANY (current_schemas(false))'} + AND c.relname = '#{name.identifier}' + AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'} SQL end @@ -182,13 +186,15 @@ module ActiveRecord def columns(table_name) # Limit, precision, and scale are all handled by the superclass. column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod| - oid = get_oid_type(oid.to_i, fmod.to_i, column_name) - PostgreSQLColumn.new(column_name, default, oid, type, notnull == 'f') + oid = get_oid_type(oid.to_i, fmod.to_i, column_name, type) + default_value = extract_value_from_default(oid, default) + default_function = extract_default_function(default_value, default) + new_column(column_name, default_value, oid, type, notnull == 'f', default_function) end end - def column_for(table_name, column_name) #:nodoc: - columns(table_name).detect { |c| c.name == column_name.to_s } + def new_column(name, default, cast_type, sql_type = nil, null = true, default_function = nil) # :nodoc: + PostgreSQLColumn.new(name, default, cast_type, sql_type, null, default_function) end # Returns the current database name. @@ -275,9 +281,9 @@ module ActiveRecord def default_sequence_name(table_name, pk = nil) #:nodoc: result = serial_sequence(table_name, pk || 'id') return nil unless result - result.split('.').last + Utils.extract_schema_qualified_name(result) rescue ActiveRecord::StatementInvalid - "#{table_name}_#{pk || 'id'}_seq" + PostgreSQL::Name.new(nil, "#{table_name}_#{pk || 'id'}_seq") end def serial_sequence(table, column) @@ -314,17 +320,19 @@ module ActiveRecord # First try looking for a sequence with a dependency on the # given table's primary key. result = query(<<-end_sql, 'SCHEMA')[0] - SELECT attr.attname, seq.relname + SELECT attr.attname, nsp.nspname, seq.relname FROM pg_class seq, pg_attribute attr, pg_depend dep, - pg_constraint cons + pg_constraint cons, + pg_namespace nsp WHERE seq.oid = dep.objid AND seq.relkind = 'S' AND attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid AND attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1] + AND seq.relnamespace = nsp.oid AND cons.contype = 'p' AND dep.classid = 'pg_class'::regclass AND dep.refobjid = '#{quote_table_name(table)}'::regclass @@ -332,7 +340,7 @@ module ActiveRecord if result.nil? or result.empty? result = query(<<-end_sql, 'SCHEMA')[0] - SELECT attr.attname, + SELECT attr.attname, nsp.nspname, CASE WHEN pg_get_expr(def.adbin, def.adrelid) !~* 'nextval' THEN NULL WHEN split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2) ~ '.' THEN @@ -344,13 +352,19 @@ module ActiveRecord JOIN pg_attribute attr ON (t.oid = attrelid) JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum) JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1]) + JOIN pg_namespace nsp ON (t.relnamespace = nsp.oid) WHERE t.oid = '#{quote_table_name(table)}'::regclass AND cons.contype = 'p' AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval|uuid_generate' end_sql end - [result.first, result.last] + pk = result.shift + if result.last + [pk, PostgreSQL::Name.new(*result)] + else + [pk, nil] + end rescue nil end @@ -369,8 +383,8 @@ module ActiveRecord end # Renames a table. - # Also renames a table's primary key sequence if the sequence name matches the - # Active Record default. + # Also renames a table's primary key sequence if the sequence name exists and + # matches the Active Record default. # # Example: # rename_table('octopuses', 'octopi') @@ -378,7 +392,7 @@ module ActiveRecord clear_cache! execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}" pk, seq = pk_and_sequence_for(new_name) - if seq == "#{table_name}_#{pk}_seq" + if seq && seq.identifier == "#{table_name}_#{pk}_seq" new_seq = "#{new_name}_#{pk}_seq" execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}" end @@ -409,7 +423,16 @@ module ActiveRecord def change_column_default(table_name, column_name, default) clear_cache! column = column_for(table_name, column_name) - execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote_default_value(default, column)}" if column + return unless column + + alter_column_query = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} %s" + if default.nil? + # <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will + # cast the default to the columns type, which leaves us with a default like "default NULL::character varying". + execute alter_column_query % "DROP DEFAULT" + else + execute alter_column_query % "SET DEFAULT #{quote_default_value(default, column)}" + end end def change_column_null(table_name, column_name, null, default = nil) @@ -441,6 +464,42 @@ module ActiveRecord execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}" end + def foreign_keys(table_name) + fk_info = select_all <<-SQL.strip_heredoc + SELECT t2.relname AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete + FROM pg_constraint c + JOIN pg_class t1 ON c.conrelid = t1.oid + JOIN pg_class t2 ON c.confrelid = t2.oid + JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid + JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid + JOIN pg_namespace t3 ON c.connamespace = t3.oid + WHERE c.contype = 'f' + AND t1.relname = #{quote(table_name)} + AND t3.nspname = ANY (current_schemas(false)) + ORDER BY c.conname + SQL + + fk_info.map do |row| + options = { + column: row['column'], + name: row['name'], + primary_key: row['primary_key'] + } + + options[:on_delete] = extract_foreign_key_action(row['on_delete']) + options[:on_update] = extract_foreign_key_action(row['on_update']) + ForeignKeyDefinition.new(table_name, row['to_table'], options) + end + end + + def extract_foreign_key_action(specifier) # :nodoc: + case specifier + when 'c'; :cascade + when 'n'; :nullify + when 'r'; :restrict + end + end + def index_name_length 63 end @@ -490,7 +549,7 @@ module ActiveRecord # Convert Arel node to string s = s.to_sql unless s.is_a?(String) # Remove any ASC/DESC modifiers - s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '') + s.gsub(/\s+(?:ASC|DESC)?\s*(?:NULLS\s+(?:FIRST|LAST)\s*)?/i, '') }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" } [super, *order_columns].join(', ') diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb new file mode 100644 index 0000000000..0290bcb48c --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb @@ -0,0 +1,66 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + # Value Object to hold a schema qualified name. + # This is usually the name of a PostgreSQL relation but it can also represent + # schema qualified type names. +schema+ and +identifier+ are unquoted to prevent + # double quoting. + class Name # :nodoc: + SEPARATOR = "." + attr_reader :schema, :identifier + + def initialize(schema, identifier) + @schema, @identifier = unquote(schema), unquote(identifier) + end + + def to_s + parts.join SEPARATOR + end + + def quoted + parts.map { |p| PGconn.quote_ident(p) }.join SEPARATOR + end + + def ==(o) + o.class == self.class && o.parts == parts + end + alias_method :eql?, :== + + def hash + parts.hash + end + + protected + def unquote(part) + return unless part + part.gsub(/(^"|"$)/,'') + end + + def parts + @parts ||= [@schema, @identifier].compact + end + end + + module Utils # :nodoc: + extend self + + # Returns an instance of <tt>ActiveRecord::ConnectionAdapters::PostgreSQL::Name</tt> + # extracted from +string+. + # +schema+ is nil if not specified in +string+. + # +schema+ and +identifier+ exclude surrounding quotes (regardless of whether provided in +string+) + # +string+ supports the range of schema/table references understood by PostgreSQL, for example: + # + # * <tt>table_name</tt> + # * <tt>"table.name"</tt> + # * <tt>schema_name.table_name</tt> + # * <tt>schema_name."table.name"</tt> + # * <tt>"schema_name".table_name</tt> + # * <tt>"schema.name"."table name"</tt> + def extract_schema_qualified_name(string) + table, schema = string.scan(/[^".\s]+|"[^"]*"/)[0..1].reverse + PostgreSQL::Name.new(schema, table) + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 764cb576d9..eede374678 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,13 +1,15 @@ require 'active_record/connection_adapters/abstract_adapter' require 'active_record/connection_adapters/statement_pool' + +require 'active_record/connection_adapters/postgresql/utils' +require 'active_record/connection_adapters/postgresql/column' require 'active_record/connection_adapters/postgresql/oid' -require 'active_record/connection_adapters/postgresql/cast' -require 'active_record/connection_adapters/postgresql/array_parser' require 'active_record/connection_adapters/postgresql/quoting' +require 'active_record/connection_adapters/postgresql/referential_integrity' +require 'active_record/connection_adapters/postgresql/schema_definitions' require 'active_record/connection_adapters/postgresql/schema_statements' require 'active_record/connection_adapters/postgresql/database_statements' -require 'active_record/connection_adapters/postgresql/referential_integrity' -require 'active_record/connection_adapters/postgresql/column' + require 'arel/visitors/bind_visitor' # Make sure we're using pg high enough for PGResult#values @@ -72,139 +74,6 @@ module ActiveRecord # In addition, default connection parameters of libpq can be set per environment variables. # See http://www.postgresql.org/docs/9.1/static/libpq-envars.html . class PostgreSQLAdapter < AbstractAdapter - class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition - attr_accessor :array - end - - module ColumnMethods - def xml(*args) - options = args.extract_options! - column(args[0], 'xml', options) - end - - def tsvector(*args) - options = args.extract_options! - column(args[0], 'tsvector', options) - end - - def int4range(name, options = {}) - column(name, 'int4range', options) - end - - def int8range(name, options = {}) - column(name, 'int8range', options) - end - - def tsrange(name, options = {}) - column(name, 'tsrange', options) - end - - def tstzrange(name, options = {}) - column(name, 'tstzrange', options) - end - - def numrange(name, options = {}) - column(name, 'numrange', options) - end - - def daterange(name, options = {}) - column(name, 'daterange', options) - end - - def hstore(name, options = {}) - column(name, 'hstore', options) - end - - def ltree(name, options = {}) - column(name, 'ltree', options) - end - - def inet(name, options = {}) - column(name, 'inet', options) - end - - def cidr(name, options = {}) - column(name, 'cidr', options) - end - - def macaddr(name, options = {}) - column(name, 'macaddr', options) - end - - def uuid(name, options = {}) - column(name, 'uuid', options) - end - - def json(name, options = {}) - column(name, 'json', options) - end - - def citext(name, options = {}) - column(name, 'citext', options) - end - end - - class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition - include ColumnMethods - - # Defines the primary key field. - # Use of the native PostgreSQL UUID type is supported, and can be used - # by defining your tables as such: - # - # create_table :stuffs, id: :uuid do |t| - # t.string :content - # t.timestamps - # end - # - # By default, this will use the +uuid_generate_v4()+ function from the - # +uuid-ossp+ extension, which MUST be enabled on your database. To enable - # the +uuid-ossp+ extension, you can use the +enable_extension+ method in your - # migrations. To use a UUID primary key without +uuid-ossp+ enabled, you can - # set the +:default+ option to +nil+: - # - # create_table :stuffs, id: false do |t| - # t.primary_key :id, :uuid, default: nil - # t.uuid :foo_id - # t.timestamps - # end - # - # You may also pass a different UUID generation function from +uuid-ossp+ - # or another library. - # - # Note that setting the UUID primary key default value to +nil+ will - # require you to assure that you always provide a UUID value before saving - # a record (as primary keys cannot be +nil+). This might be done via the - # +SecureRandom.uuid+ method and a +before_save+ callback, for instance. - def primary_key(name, type = :primary_key, options = {}) - return super unless type == :uuid - options[:default] = options.fetch(:default, 'uuid_generate_v4()') - options[:primary_key] = true - column name, type, options - end - - def citext(name, options = {}) - column(name, 'citext', options) - end - - def column(name, type = nil, options = {}) - super - column = self[name] - column.array = options[:array] - - self - end - - private - - def create_column_definition(name, type) - ColumnDefinition.new name, type - end - end - - class Table < ActiveRecord::ConnectionAdapters::Table - include ColumnMethods - end - ADAPTER_NAME = 'PostgreSQL' NATIVE_DATABASE_TYPES = { @@ -215,7 +84,6 @@ module ActiveRecord float: { name: "float" }, decimal: { name: "decimal" }, datetime: { name: "timestamp" }, - timestamp: { name: "timestamp" }, time: { name: "time" }, date: { name: "date" }, daterange: { name: "daterange" }, @@ -235,13 +103,19 @@ module ActiveRecord uuid: { name: "uuid" }, json: { name: "json" }, ltree: { name: "ltree" }, - citext: { name: "citext" } + citext: { name: "citext" }, + point: { name: "point" }, + bit: { name: "bit" }, + bit_varying: { name: "bit varying" }, + money: { name: "money" }, } - include Quoting - include ReferentialIntegrity - include SchemaStatements - include DatabaseStatements + OID = PostgreSQL::OID #:nodoc: + + include PostgreSQL::Quoting + include PostgreSQL::ReferentialIntegrity + include PostgreSQL::SchemaStatements + include PostgreSQL::DatabaseStatements include Savepoints # Returns 'PostgreSQL' as adapter name for identification purposes. @@ -249,9 +123,13 @@ module ActiveRecord ADAPTER_NAME end + def schema_creation # :nodoc: + PostgreSQL::SchemaCreation.new self + end + # Adds `:array` option to the default set provided by the # AbstractAdapter - def prepare_column_options(column, types) + def prepare_column_options(column, types) # :nodoc: spec = super spec[:array] = 'true' if column.respond_to?(:array) && column.array spec[:default] = "\"#{column.default_function}\"" if column.default_function @@ -281,6 +159,10 @@ module ActiveRecord true end + def supports_foreign_keys? + true + end + def index_algorithms { concurrently: 'CONCURRENTLY' } end @@ -363,7 +245,7 @@ module ActiveRecord raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!" end - @type_map = OID::TypeMap.new + @type_map = Type::HashLookupTypeMap.new initialize_type_map(type_map) @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"] @use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true @@ -428,10 +310,6 @@ module ActiveRecord self.client_min_messages = old end - def supports_insert_with_returning? - true - end - def supports_ddl_transactions? true end @@ -470,14 +348,13 @@ module ActiveRecord if supports_extensions? res = exec_query "SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled", 'SCHEMA' - res.column_types['enabled'].type_cast res.rows.first.first + res.cast_values.first end end def extensions if supports_extensions? - res = exec_query "SELECT extname from pg_extension", "SCHEMA" - res.rows.map { |r| res.column_types['extname'].type_cast r.first } + exec_query("SELECT extname from pg_extension", "SCHEMA").cast_values else super end @@ -494,25 +371,6 @@ module ActiveRecord exec_query "SET SESSION AUTHORIZATION #{user}" end - module Utils - extend self - - # Returns an array of <tt>[schema_name, table_name]</tt> extracted from +name+. - # +schema_name+ is nil if not specified in +name+. - # +schema_name+ and +table_name+ exclude surrounding quotes (regardless of whether provided in +name+) - # +name+ supports the range of schema/table references understood by PostgreSQL, for example: - # - # * <tt>table_name</tt> - # * <tt>"table.name"</tt> - # * <tt>schema_name.table_name</tt> - # * <tt>schema_name."table.name"</tt> - # * <tt>"schema.name"."table name"</tt> - def extract_schema_and_table(name) - table, schema = name.scan(/[^".\s]+|"[^"]*"/)[0..1].collect{|m| m.gsub(/(^"|"$)/,'') }.reverse - [schema, table] - end - end - def use_insert_returning? @use_insert_returning end @@ -522,7 +380,12 @@ module ActiveRecord end def update_table_definition(table_name, base) #:nodoc: - Table.new(table_name, base) + PostgreSQL::Table.new(table_name, base) + end + + def lookup_cast_type(sql_type) # :nodoc: + oid = execute("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").first['oid'].to_i + super(oid) end protected @@ -551,46 +414,129 @@ module ActiveRecord private - def type_map - @type_map - end - - def get_oid_type(oid, fmod, column_name) + def get_oid_type(oid, fmod, column_name, sql_type = '') # :nodoc: if !type_map.key?(oid) - initialize_type_map(type_map, [oid]) + load_additional_types(type_map, [oid]) end - type_map.fetch(oid, fmod) { + type_map.fetch(oid, fmod, sql_type) { warn "unknown OID #{oid}: failed to recognize type of '#{column_name}'. It will be treated as String." - type_map[oid] = OID::Identity.new + Type::Value.new.tap do |cast_type| + type_map.register_type(oid, cast_type) + end } end - def reload_type_map - type_map.clear - initialize_type_map(type_map) - end - - def add_oid(row, records_by_oid, type_map) - return type_map if type_map.key? row['type_elem'].to_i + def initialize_type_map(m) # :nodoc: + register_class_with_limit m, 'int2', OID::Integer + m.alias_type 'int4', 'int2' + m.alias_type 'int8', 'int2' + m.alias_type 'oid', 'int2' + m.register_type 'float4', OID::Float.new + m.alias_type 'float8', 'float4' + m.register_type 'text', Type::Text.new + register_class_with_limit m, 'varchar', Type::String + m.alias_type 'char', 'varchar' + m.alias_type 'name', 'varchar' + m.alias_type 'bpchar', 'varchar' + m.register_type 'bool', Type::Boolean.new + register_class_with_limit m, 'bit', OID::Bit + register_class_with_limit m, 'varbit', OID::BitVarying + m.alias_type 'timestamptz', 'timestamp' + m.register_type 'date', OID::Date.new + m.register_type 'time', OID::Time.new + + m.register_type 'money', OID::Money.new + m.register_type 'bytea', OID::Bytea.new + m.register_type 'point', OID::Point.new + m.register_type 'hstore', OID::Hstore.new + m.register_type 'json', OID::Json.new + m.register_type 'jsonb', OID::Jsonb.new + m.register_type 'cidr', OID::Cidr.new + m.register_type 'inet', OID::Inet.new + m.register_type 'uuid', OID::Uuid.new + m.register_type 'xml', OID::Xml.new + m.register_type 'tsvector', OID::SpecializedString.new(:tsvector) + m.register_type 'macaddr', OID::SpecializedString.new(:macaddr) + m.register_type 'citext', OID::SpecializedString.new(:citext) + m.register_type 'ltree', OID::SpecializedString.new(:ltree) + + # FIXME: why are we keeping these types as strings? + m.alias_type 'interval', 'varchar' + m.alias_type 'path', 'varchar' + m.alias_type 'line', 'varchar' + m.alias_type 'polygon', 'varchar' + m.alias_type 'circle', 'varchar' + m.alias_type 'lseg', 'varchar' + m.alias_type 'box', 'varchar' + + m.register_type 'timestamp' do |_, _, sql_type| + precision = extract_precision(sql_type) + OID::DateTime.new(precision: precision) + end - if OID.registered_type? row['typname'] - # this composite type is explicitly registered - vector = OID::NAMES[row['typname']] - else - # use the default for composite types - unless type_map.key? row['typelem'].to_i - add_oid records_by_oid[row['typelem']], records_by_oid, type_map + m.register_type 'numeric' do |_, fmod, sql_type| + precision = extract_precision(sql_type) + scale = extract_scale(sql_type) + + # The type for the numeric depends on the width of the field, + # so we'll do something special here. + # + # When dealing with decimal columns: + # + # places after decimal = fmod - 4 & 0xffff + # places before decimal = (fmod - 4) >> 16 & 0xffff + if fmod && (fmod - 4 & 0xffff).zero? + # FIXME: Remove this class, and the second argument to + # lookups on PG + Type::DecimalWithoutScale.new(precision: precision) + else + OID::Decimal.new(precision: precision, scale: scale) end + end - vector = OID::Vector.new row['typdelim'], type_map[row['typelem'].to_i] + load_additional_types(m) + end + + def extract_limit(sql_type) # :nodoc: + case sql_type + when /^bigint/i; 8 + when /^smallint/i; 2 + else super + end + end + + # Extracts the value from a PostgreSQL column default definition. + def extract_value_from_default(oid, default) # :nodoc: + case default + # Quoted types + when /\A[\(B]?'(.*)'::/m + $1.gsub(/''/, "'") + # Boolean types + when 'true', 'false' + default + # Numeric types + when /\A\(?(-?\d+(\.\d*)?\)?(::bigint)?)\z/ + $1 + # Object identifier types + when /\A-?\d+\z/ + $1 + else + # Anything else is blank, some user type, or some function + # and we can't know the value of that, so return nil. + nil end + end - type_map[row['oid'].to_i] = vector - type_map + def extract_default_function(default_value, default) # :nodoc: + default if has_default_function?(default_value, default) end - def initialize_type_map(type_map, oids = nil) + def has_default_function?(default_value, default) # :nodoc: + !default_value && (%r{\w+\(.*\)} === default) + end + + def load_additional_types(type_map, oids = nil) # :nodoc: if supports_ranges? query = <<-SQL SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype @@ -608,52 +554,9 @@ module ActiveRecord query += "WHERE t.oid::integer IN (%s)" % oids.join(", ") end - result = execute(query, 'SCHEMA') - ranges, nodes = result.partition { |row| row['typtype'] == 'r' } - enums, nodes = nodes.partition { |row| row['typtype'] == 'e' } - domains, nodes = nodes.partition { |row| row['typtype'] == 'd' } - arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' } - leaves, nodes = nodes.partition { |row| row['typelem'] == '0' } - - # populate the enum types - enums.each do |row| - type_map[row['oid'].to_i] = OID::Enum.new - end - - # populate the base types - leaves.find_all { |row| OID.registered_type? row['typname'] }.each do |row| - type_map[row['oid'].to_i] = OID::NAMES[row['typname']] - end - - records_by_oid = result.group_by { |row| row['oid'] } - - # populate composite types - nodes.each do |row| - add_oid row, records_by_oid, type_map - end - - # populate array types - arrays.find_all { |row| type_map.key? row['typelem'].to_i }.each do |row| - array = OID::Array.new type_map[row['typelem'].to_i] - type_map[row['oid'].to_i] = array - end - - # populate range types - ranges.find_all { |row| type_map.key? row['rngsubtype'].to_i }.each do |row| - subtype = type_map[row['rngsubtype'].to_i] - range = OID::Range.new subtype - type_map[row['oid'].to_i] = range - end - - # populate domain types - domains.each do |row| - base_type_oid = row["typbasetype"].to_i - if base_type = type_map[base_type_oid] - type_map[row['oid'].to_i] = base_type - else - warn "unknown base type (OID: #{base_type_oid}) for domain #{row["typname"]}." - end - end + initializer = OID::TypeMapInitializer.new(type_map) + records = execute(query, 'SCHEMA') + initializer.run(records) end FEATURE_NOT_SUPPORTED = "0A000" #:nodoc: @@ -667,7 +570,7 @@ module ActiveRecord end def exec_no_cache(sql, name, binds) - log(sql, name, binds) { @connection.async_exec(sql) } + log(sql, name, binds) { @connection.async_exec(sql, []) } end def exec_cache(sql, name, binds) @@ -725,11 +628,6 @@ module ActiveRecord @statements[sql_key] end - # The internal PostgreSQL identifier of the money data type. - MONEY_COLUMN_TYPE_OID = 790 #:nodoc: - # The internal PostgreSQL identifier of the BYTEA data type. - BYTEA_COLUMN_TYPE_OID = 17 #:nodoc: - # Connects to a PostgreSQL server and sets up the adapter depending on the # connected server's characteristics. def connect @@ -738,7 +636,7 @@ module ActiveRecord # Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of # PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision # should know about this but can't detect it there, so deal with it here. - PostgreSQLColumn.money_precision = (postgresql_version >= 80300) ? 19 : 10 + OID::Money.precision = (postgresql_version >= 80300) ? 19 : 10 configure_connection rescue ::PG::Error => error @@ -820,7 +718,7 @@ module ActiveRecord # Query implementation notes: # - format_type includes the column size constraint, e.g. varchar(50) # - ::regclass is a function that gives the id for a table name - def column_definitions(table_name) #:nodoc: + def column_definitions(table_name) # :nodoc: exec_query(<<-end_sql, 'SCHEMA').rows SELECT a.attname, format_type(a.atttypid, a.atttypmod), pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod @@ -832,23 +730,13 @@ module ActiveRecord end_sql end - def extract_pg_identifier_from_name(name) - match_data = name.start_with?('"') ? name.match(/\"([^\"]+)\"/) : name.match(/([^\.]+)/) - - if match_data - rest = name[match_data[0].length, name.length] - rest = rest[1, rest.length] if rest.start_with? "." - [match_data[1], (rest.length > 0 ? rest : nil)] - end - end - - def extract_table_ref_from_insert_sql(sql) + def extract_table_ref_from_insert_sql(sql) # :nodoc: sql[/into\s+([^\(]*).*values\s*\(/im] $1.strip if $1 end - def create_table_definition(name, temporary, options, as = nil) - TableDefinition.new native_database_types, name, temporary, options, as + def create_table_definition(name, temporary, options, as = nil) # :nodoc: + PostgreSQL::TableDefinition.new native_database_types, name, temporary, options, as end end end diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index e5c9f6f54a..4d8afcf16a 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -12,11 +12,10 @@ module ActiveRecord @columns_hash = {} @primary_keys = {} @tables = {} - prepare_default_proc end def primary_keys(table_name) - @primary_keys[table_name] + @primary_keys[table_name] ||= table_exists?(table_name) ? connection.primary_key(table_name) : nil end # A cached lookup for table existence. @@ -29,9 +28,9 @@ module ActiveRecord # Add internal cache for table with +table_name+. def add(table_name) if table_exists?(table_name) - @primary_keys[table_name] - @columns[table_name] - @columns_hash[table_name] + primary_keys(table_name) + columns(table_name) + columns_hash(table_name) end end @@ -40,14 +39,16 @@ module ActiveRecord end # Get the columns for a table - def columns(table) - @columns[table] + def columns(table_name) + @columns[table_name] ||= connection.columns(table_name) end # Get the columns for a table as a hash, key is the column name # value is the column object. - def columns_hash(table) - @columns_hash[table] + def columns_hash(table_name) + @columns_hash[table_name] ||= Hash[columns(table_name).map { |col| + [col.name, col] + }] end # Clears out internal caches @@ -76,32 +77,11 @@ module ActiveRecord def marshal_dump # if we get current version during initialization, it happens stack over flow. @version = ActiveRecord::Migrator.current_version - [@version] + [@columns, @columns_hash, @primary_keys, @tables].map { |val| - Hash[val] - } + [@version, @columns, @columns_hash, @primary_keys, @tables] end def marshal_load(array) @version, @columns, @columns_hash, @primary_keys, @tables = array - prepare_default_proc - end - - private - - def prepare_default_proc - @columns.default_proc = Proc.new do |h, table_name| - h[table_name] = connection.columns(table_name) - end - - @columns_hash.default_proc = Proc.new do |h, table_name| - h[table_name] = Hash[columns(table_name).map { |col| - [col.name, col] - }] - end - - @primary_keys.default_proc = Proc.new do |h, table_name| - h[table_name] = table_exists?(table_name) ? connection.primary_key(table_name) : nil - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index dd4261cec7..faf1cdc686 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -14,9 +14,9 @@ module ActiveRecord raise ArgumentError, "No database file specified. Missing argument: database" end - # Allow database path relative to Rails.root, but only if - # the database path is not the special path that tells - # Sqlite to build a database only in memory. + # Allow database path relative to Rails.root, but only if the database + # path is not the special path that tells sqlite to build a database only + # in memory. if ':memory:' != config[:database] config[:database] = File.expand_path(config[:database], Rails.root) if defined?(Rails.root) dirname = File.dirname(config[:database]) @@ -41,13 +41,21 @@ module ActiveRecord end module ConnectionAdapters #:nodoc: - class SQLite3Column < Column #:nodoc: - class << self - def binary_to_string(value) - if value.encoding != Encoding::ASCII_8BIT - value = value.force_encoding(Encoding::ASCII_8BIT) - end - value + class SQLite3Binary < Type::Binary # :nodoc: + def cast_value(value) + if value.encoding != Encoding::ASCII_8BIT + value = value.force_encoding(Encoding::ASCII_8BIT) + end + value + end + end + + class SQLite3String < Type::String # :nodoc: + def type_cast_for_database(value) + if value.is_a?(::String) && value.encoding == Encoding::ASCII_8BIT + value.encode(Encoding::UTF_8) + else + super end end end @@ -69,7 +77,6 @@ module ActiveRecord float: { name: "float" }, decimal: { name: "decimal" }, datetime: { name: "datetime" }, - timestamp: { name: "datetime" }, time: { name: "time" }, date: { name: "date" }, binary: { name: "blob" }, @@ -222,10 +229,19 @@ module ActiveRecord # QUOTING ================================================== - def quote(value, column = nil) - if value.kind_of?(String) && column && column.type == :binary - s = value.unpack("H*")[0] - "x'#{s}'" + def _quote(value) # :nodoc: + case value + when Type::Binary::Data + "x'#{value.hex}'" + else + super + end + end + + def _type_cast(value) # :nodoc: + case value + when BigDecimal + value.to_f else super end @@ -253,19 +269,6 @@ module ActiveRecord end end - def type_cast(value, column) # :nodoc: - return value.to_f if BigDecimal === value - return super unless String === value - return super unless column && value - - value = super - if column.type == :string && value.encoding == Encoding::ASCII_8BIT - logger.error "Binary data inserted for `string` type on column `#{column.name}`" if logger - value = value.encode Encoding::UTF_8 - end - value - end - # DATABASE STATEMENTS ====================================== def explain(arel, binds = []) @@ -296,9 +299,12 @@ module ActiveRecord # 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 - stmt.close + begin + cols = stmt.columns + records = stmt.to_a + ensure + stmt.close + end stmt = records else cache = @statements[sql] ||= { @@ -379,7 +385,7 @@ module ActiveRecord table_name && tables(nil, table_name).any? end - # Returns an array of +SQLite3Column+ objects for the table specified by +table_name+. + # Returns an array of +Column+ objects for the table specified by +table_name+. def columns(table_name) #:nodoc: table_structure(table_name).map do |field| case field["dflt_value"] @@ -391,7 +397,9 @@ module ActiveRecord field["dflt_value"] = $1.gsub('""', '"') end - SQLite3Column.new(field['name'], field['dflt_value'], field['type'], field['notnull'].to_i == 0) + sql_type = field['type'] + cast_type = lookup_cast_type(sql_type) + new_column(field['name'], field['dflt_value'], cast_type, sql_type, field['notnull'].to_i == 0) end end @@ -492,14 +500,19 @@ module ActiveRecord end def rename_column(table_name, column_name, new_column_name) #:nodoc: - unless columns(table_name).detect{|c| c.name == column_name.to_s } - raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}" - end - alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s}) - rename_column_indexes(table_name, column_name, new_column_name) + column = column_for(table_name, column_name) + alter_table(table_name, rename: {column.name => new_column_name.to_s}) + rename_column_indexes(table_name, column.name, new_column_name) end protected + + def initialize_type_map(m) + super + m.register_type(/binary/i, SQLite3Binary.new) + register_class_with_limit m, %r(char)i, SQLite3String + end + def select(sql, name = nil, binds = []) #:nodoc: exec_query(sql, name, binds) end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 4571cc0786..d22806fbdf 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -16,7 +16,6 @@ module ActiveRecord mattr_accessor :logger, instance_writer: false ## - # :singleton-method: # Contains the database configuration - as is typically stored in config/database.yml - # as a Hash. # @@ -108,6 +107,11 @@ module ActiveRecord end module ClassMethods + def allocate + define_attribute_methods + super + end + def initialize_find_by_cache self.find_by_statement_cache = {}.extend(Mutex_m) end @@ -249,16 +253,12 @@ module ActiveRecord # # Instantiates a single new object # User.new(first_name: 'Jamie') 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) - @column_types_override = nil - @column_types = self.class.column_types + @attributes = self.class.default_attributes.dup init_internals initialize_internals_callback + self.class.define_attribute_methods # +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 @@ -278,13 +278,13 @@ module ActiveRecord # post.init_with('attributes' => { 'title' => 'hello world' }) # post.title # => 'hello world' def init_with(coder) - @attributes = self.class.initialize_attributes(coder['attributes']) - @column_types_override = coder['column_types'] - @column_types = self.class.column_types + @attributes = coder['attributes'] init_internals - @new_record = false + @new_record = coder['new_record'] + + self.class.define_attribute_methods run_callbacks :find run_callbacks :initialize @@ -320,17 +320,13 @@ module ActiveRecord ## def initialize_dup(other) # :nodoc: - cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) - self.class.initialize_attributes(cloned_attributes, :serialized => false) - - @attributes = cloned_attributes - @attributes[self.class.primary_key] = nil + @attributes = @attributes.dup + @attributes.reset(self.class.primary_key) run_callbacks(:initialize) unless _initialize_callbacks.empty? @aggregation_cache = {} @association_cache = {} - @attributes_cache = {} @new_record = true @destroyed = false @@ -351,7 +347,10 @@ module ActiveRecord # Post.new.encode_with(coder) # coder # => {"attributes" => {"id" => nil, ... }} def encode_with(coder) - coder['attributes'] = attributes_for_coder + # FIXME: Remove this when we better serialize attributes + coder['raw_attributes'] = attributes_before_type_cast + coder['attributes'] = @attributes + coder['new_record'] = new_record? end # Returns true if +comparison_object+ is the same exact object, or +comparison_object+ @@ -374,7 +373,11 @@ module ActiveRecord # Delegates to id in order to allow two records of the same type and id to work with something like: # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ] def hash - id.hash + if id + id.hash + else + super + end end # Clone and freeze the attributes hash such that associations are still @@ -430,6 +433,29 @@ module ActiveRecord "#<#{self.class} #{inspection}>" end + # Takes a PP and prettily prints this record to it, allowing you to get a nice result from `pp record` + # when pp is required. + def pretty_print(pp) + pp.object_address_group(self) do + if defined?(@attributes) && @attributes + column_names = self.class.column_names.select { |name| has_attribute?(name) || new_record? } + pp.seplist(column_names, proc { pp.text ',' }) do |column_name| + column_value = read_attribute(column_name) + pp.breakable ' ' + pp.group(1) do + pp.text column_name + pp.text ':' + pp.breakable + pp.pp column_value + end + end + else + pp.breakable ' ' + pp.text 'not initialized' + end + end + end + # Returns a hash of the given methods with their names as keys and returned values as values. def slice(*methods) Hash[methods.map! { |method| [method, public_send(method)] }].with_indifferent_access @@ -493,12 +519,10 @@ module ActiveRecord end def init_internals - pk = self.class.primary_key - @attributes[pk] = nil unless @attributes.key?(pk) + @attributes.ensure_initialized(self.class.primary_key) @aggregation_cache = {} @association_cache = {} - @attributes_cache = {} @readonly = false @destroyed = false @marked_for_destruction = false @@ -518,5 +542,11 @@ module ActiveRecord def init_attributes(attributes, options) assign_attributes(attributes) end + + def thaw + if frozen? + @attributes = @attributes.dup + end + end end end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index b7b790322a..a33c7c64a7 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -11,7 +11,7 @@ module ActiveRecord # ==== Parameters # # * +id+ - The id of the object you wish to reset a counter on. - # * +counters+ - One or more association counters to reset + # * +counters+ - One or more association counters to reset. Association name or counter name can be given. # # ==== Examples # @@ -19,9 +19,14 @@ module ActiveRecord # Post.reset_counters(1, :comments) def reset_counters(id, *counters) object = find(id) - counters.each do |association| - has_many_association = reflect_on_association(association.to_sym) - raise ArgumentError, "'#{self.name}' has no association called '#{association}'" unless has_many_association + counters.each do |counter_association| + has_many_association = _reflect_on_association(counter_association.to_sym) + unless has_many_association + has_many = reflect_on_all_associations(:has_many) + has_many_association = has_many.find { |association| association.counter_cache_column && association.counter_cache_column.to_sym == counter_association.to_sym } + counter_association = has_many_association.plural_name if has_many_association + end + raise ArgumentError, "'#{self.name}' has no association called '#{counter_association}'" unless has_many_association if has_many_association.is_a? ActiveRecord::Reflection::ThroughReflection has_many_association = has_many_association.through_reflection @@ -29,12 +34,11 @@ module ActiveRecord foreign_key = has_many_association.foreign_key.to_s child_class = has_many_association.klass - belongs_to = child_class.reflect_on_all_associations(:belongs_to) - reflection = belongs_to.find { |e| e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? } + reflection = child_class._reflections.values.find { |e| :belongs_to == e.macro && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? } counter_name = reflection.counter_cache_column stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({ - arel_table[counter_name] => object.send(association).count + arel_table[counter_name] => object.send(counter_association).count(:all) }, primary_key) connection.update stmt end @@ -162,7 +166,7 @@ module ActiveRecord end def each_counter_cached_associations - reflections.each do |name, reflection| + _reflections.each do |name, reflection| yield association(name) if reflection.belongs_to? && reflection.counter_cache_column end end diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 18f1ca26de..f0ee433d0b 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -27,8 +27,10 @@ module ActiveRecord # conversation.status # => nil # # Scopes based on the allowed values of the enum field will be provided - # as well. With the above example, it will create an +active+ and +archived+ - # scope. + # as well. With the above example: + # + # Conversation.active + # Conversation.archived # # You can set the default value from the database declaration, like: # @@ -67,12 +69,12 @@ module ActiveRecord # # Where conditions on an enum attribute must use the ordinal value of an enum. module Enum - def self.extended(base) + def self.extended(base) # :nodoc: base.class_attribute(:defined_enums) base.defined_enums = {} end - def inherited(base) + def inherited(base) # :nodoc: base.defined_enums = defined_enums.deep_dup super end @@ -138,17 +140,14 @@ module ActiveRecord @_enum_methods_module ||= begin mod = Module.new do private - def save_changed_attribute(attr_name, value) + def save_changed_attribute(attr_name, old) if (mapping = self.class.defined_enums[attr_name.to_s]) + value = read_attribute(attr_name) if attribute_changed?(attr_name) - old = changed_attributes[attr_name] - if mapping[old] == value changed_attributes.delete(attr_name) end else - old = clone_attribute_value(:read_attribute, attr_name) - if old != value changed_attributes[attr_name] = mapping.key old end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 71efbb8f93..52c70977ef 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -30,17 +30,18 @@ module ActiveRecord class SerializationTypeMismatch < ActiveRecordError end - # Raised when adapter not specified on connection (or configuration file <tt>config/database.yml</tt> - # misses adapter field). + # Raised when adapter not specified on connection (or configuration file + # +config/database.yml+ misses adapter field). class AdapterNotSpecified < ActiveRecordError end - # Raised when Active Record cannot find database adapter specified in <tt>config/database.yml</tt> or programmatically. + # Raised when Active Record cannot find database adapter specified in + # +config/database.yml+ or programmatically. class AdapterNotFound < ActiveRecordError end - # Raised when connection to the database could not been established (for example when <tt>connection=</tt> - # is given a nil object). + # Raised when connection to the database could not been established (for + # example when +connection=+ is given a nil object). class ConnectionNotEstablished < ActiveRecordError end @@ -82,19 +83,17 @@ module ActiveRecord class InvalidForeignKey < WrappedDatabaseException end - # Raised when number of bind variables in statement given to <tt>:condition</tt> key (for example, - # when using +find+ method) - # does not match number of expected variables. + # Raised when number of bind variables in statement given to +:condition+ key + # (for example, when using +find+ method) does not match number of expected + # values supplied. # - # For example, in + # For example, when there are two placeholders with only one value supplied: # # Location.where("lat = ? AND lng = ?", 53.7362) - # - # two placeholders are given but only one variable to fill them. class PreparedStatementInvalid < ActiveRecordError end - # Raised when a given database does not exist + # Raised when a given database does not exist. class NoDatabaseError < StatementInvalid end @@ -102,7 +101,8 @@ module ActiveRecord # instantiation, for example, when two users edit the same wiki page and one starts editing and saves # the page before the other. # - # Read more about optimistic locking in ActiveRecord::Locking module RDoc. + # Read more about optimistic locking in ActiveRecord::Locking module + # documentation. class StaleObjectError < ActiveRecordError attr_reader :record, :attempted_action @@ -114,8 +114,9 @@ module ActiveRecord end - # Raised when association is being configured improperly or - # user tries to use offset and limit together with has_many or has_and_belongs_to_many associations. + # Raised when association is being configured improperly or user tries to use + # offset and limit together with +has_many+ or +has_and_belongs_to_many+ + # associations. class ConfigurationError < ActiveRecordError end @@ -153,7 +154,8 @@ module ActiveRecord class Rollback < ActiveRecordError end - # Raised when attribute has a name reserved by Active Record (when attribute has name of one of Active Record instance methods). + # Raised when attribute has a name reserved by Active Record (when attribute + # has name of one of Active Record instance methods). class DangerousAttributeError < ActiveRecordError end @@ -171,7 +173,7 @@ module ActiveRecord end # Raised when an error occurred while doing a mass assignment to an attribute through the - # <tt>attributes=</tt> method. The exception has an +attribute+ property that is the name of the + # +attributes=+ method. The exception has an +attribute+ property that is the name of the # offending attribute. class AttributeAssignmentError < ActiveRecordError attr_reader :exception, :attribute @@ -217,6 +219,13 @@ module ActiveRecord class ImmutableRelation < ActiveRecordError end + # TransactionIsolationError will be raised under the following conditions: + # + # * The adapter does not support setting the isolation level + # * You are joining an existing open transaction + # * You are creating a nested (savepoint) transaction + # + # The mysql, mysql2 and postgresql adapters support setting the transaction isolation level. class TransactionIsolationError < ActiveRecordError end end diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index e65dab07ba..727a9befc1 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -27,7 +27,7 @@ module ActiveRecord end.join("\n") end.join("\n") - # Overriding inspect to be more human readable, specially in the console. + # Overriding inspect to be more human readable, especially in the console. def str.inspect self end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 47d32fae05..4306b36ae1 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -2,7 +2,7 @@ require 'erb' require 'yaml' require 'zlib' require 'active_support/dependencies' -require 'active_support/core_ext/securerandom' +require 'active_support/core_ext/digest/uuid' require 'active_record/fixture_set/file' require 'active_record/errors' @@ -15,9 +15,10 @@ module ActiveRecord # They are stored in YAML files, one file per model, which are placed in the directory # appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically # configured for Rails, so you can just put your files in <tt><your-rails-app>/test/fixtures/</tt>). - # The fixture file ends with the <tt>.yml</tt> file extension (Rails example: - # <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>). The format of a fixture file looks - # like this: + # The fixture file ends with the +.yml+ file extension, for example: + # <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>). + # + # The format of a fixture file looks like this: # # rubyonrails: # id: 1 @@ -33,7 +34,7 @@ module ActiveRecord # is followed by an indented list of key/value pairs in the "key: value" format. Records are # separated by a blank line for your viewing pleasure. # - # Note that fixtures are unordered. If you want ordered fixtures, use the omap YAML type. + # Note: Fixtures are unordered. If you want ordered fixtures, use the omap YAML type. # See http://yaml.org/type/omap.html # for the specification. You will need ordered fixtures when you have foreign key constraints # on keys in the same table. This is commonly needed for tree structures. Example: @@ -61,8 +62,8 @@ module ActiveRecord # end # end # - # By default, <tt>test_helper.rb</tt> will load all of your fixtures into your test database, - # so this test will succeed. + # By default, +test_helper.rb+ will load all of your fixtures into your test + # database, so this test will succeed. # # The testing environment will automatically load the all fixtures into the database before each # test. To ensure consistent data, the environment deletes the fixtures before running the load. @@ -374,8 +375,9 @@ module ActiveRecord # # == Support for YAML defaults # - # You probably already know how to use YAML to set and reuse defaults in - # your <tt>database.yml</tt> file. You can use the same technique in your fixtures: + # You can set and reuse defaults in your fixtures YAML file. + # This is the same technique used in the +database.yml+ file to specify + # defaults: # # DEFAULTS: &DEFAULTS # created_on: <%= 3.weeks.ago.to_s(:db) %> @@ -391,7 +393,8 @@ module ActiveRecord # Any fixture labeled "DEFAULTS" is safely ignored. class FixtureSet #-- - # An instance of FixtureSet is normally stored in a single YAML file and possibly in a folder with the same name. + # An instance of FixtureSet is normally stored in a single YAML file and + # possibly in a folder with the same name. #++ MAX_ID = 2 ** 30 - 1 @@ -461,13 +464,7 @@ module ActiveRecord @config = config # Remove string values that aren't constants or subclasses of AR - @class_names.delete_if { |k,klass| - unless klass.is_a? Class - klass = klass.safe_constantize - ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name to `set_fixture_class` will be removed in Rails 4.2. Use the class itself instead.") - end - !insert_class(@class_names, k, klass) - } + @class_names.delete_if { |klass_name, klass| !insert_class(@class_names, klass_name, klass) } end def [](fs_name) @@ -554,7 +551,7 @@ module ActiveRecord # Integer identifiers are values less than 2^30. UUIDs are RFC 4122 version 5 SHA-1 hashes. def self.identify(label, column_type = :integer) if column_type == :uuid - SecureRandom.uuid_v5(SecureRandom::UUID_OID_NAMESPACE, label.to_s) + Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, label.to_s) else Zlib.crc32(label.to_s) % MAX_ID end @@ -573,10 +570,6 @@ module ActiveRecord @config = config @model_class = nil - if class_name.is_a?(String) - ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name to `FixtureSet.new` will be removed in Rails 4.2. Use the class itself instead.") - end - if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any? @model_class = class_name else @@ -649,14 +642,14 @@ module ActiveRecord model_class end - reflection_class.reflect_on_all_associations.each do |association| + reflection_class._reflections.values.each do |association| case association.macro when :belongs_to # Do not replace association name with association foreign key if they are named the same fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s if association.name.to_s != fk_name && value = row.delete(association.name.to_s) - if association.options[:polymorphic] && value.sub!(/\s*\(([^\)]*)\)\s*$/, "") + if association.polymorphic? && value.sub!(/\s*\(([^\)]*)\)\s*$/, "") # support polymorphic belongs_to as "label (Type)" row[association.foreign_type] = $1 end diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 08fc91c9df..251d682a02 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -1,6 +1,37 @@ require 'active_support/core_ext/hash/indifferent_access' module ActiveRecord + # == Single table inheritance + # + # Active Record allows inheritance by storing the name of the class in a column that by + # default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>). + # This means that an inheritance looking like this: + # + # class Company < ActiveRecord::Base; end + # class Firm < Company; end + # class Client < Company; end + # class PriorityClient < Client; end + # + # When you do <tt>Firm.create(name: "37signals")</tt>, this record will be saved in + # the companies table with type = "Firm". You can then fetch this row again using + # <tt>Company.where(name: '37signals').first</tt> and it will return a Firm object. + # + # Be aware that because the type column is an attribute on the record every new + # subclass will instantly be marked as dirty and the type column will be included + # in the list of changed attributes on the record. This is different from non + # STI classes: + # + # Company.new.changed? # => false + # Firm.new.changed? # => true + # Firm.new.changes # => {"type"=>["","Firm"]} + # + # If you don't have a type column defined in your table, single-table inheritance won't + # be triggered. In that case, it'll work just like normal subclasses with no special magic + # for differentiating between them or reloading the right type with find. + # + # Note, all the attributes for all the cases are kept in the same table. Read more: + # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html + # module Inheritance extend ActiveSupport::Concern @@ -120,14 +151,8 @@ module ActiveRecord candidates << type_name candidates.each do |candidate| - begin - constant = ActiveSupport::Dependencies.constantize(candidate) - return constant if candidate == constant.to_s - # We don't want to swallow NoMethodError < NameError errors - rescue NoMethodError - raise - rescue NameError - end + constant = ActiveSupport::Dependencies.safe_constantize(candidate) + return constant if candidate == constant.to_s end raise NameError.new("uninitialized constant #{candidates.first}", candidates.first) diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 4d63b04d9f..52eeb8ae1f 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -66,7 +66,7 @@ module ActiveRecord send(lock_col + '=', previous_lock_value + 1) end - def _update_record(attribute_names = @attributes.keys) #:nodoc: + def _update_record(attribute_names = self.attribute_names) #:nodoc: return super unless locking_enabled? return 0 if attribute_names.empty? @@ -141,7 +141,7 @@ module ActiveRecord # Set the column to use for optimistic locking. Defaults to +lock_version+. def locking_column=(value) - @column_defaults = nil + clear_caches_calculated_from_columns @locking_column = value.to_s end @@ -151,12 +151,6 @@ module ActiveRecord @locking_column end - # Quote the column name used for optimistic locking. - def quoted_locking_column - ActiveSupport::Deprecation.warn "ActiveRecord::Base.quoted_locking_column is deprecated and will be removed in Rails 4.2 or later." - connection.quote_column_name(locking_column) - end - # Reset the column used for optimistic locking back to the +lock_version+ default. def reset_locking_column self.locking_column = DEFAULT_LOCKING_COLUMN @@ -169,18 +163,42 @@ module ActiveRecord super end - def column_defaults - @column_defaults ||= begin - defaults = super - - if defaults.key?(locking_column) && lock_optimistically - defaults[locking_column] ||= 0 + private + + # We need to apply this decorator here, rather than on module inclusion. The closure + # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the + # sub class being decorated. As such, changes to `lock_optimistically`, or + # `locking_column` would not be picked up. + def inherited(subclass) + subclass.class_eval do + is_lock_column = ->(name, _) { lock_optimistically && name == locking_column } + decorate_matching_attribute_types(is_lock_column, :_optimistic_locking) do |type| + LockingType.new(type) end - - defaults end + super end end end + + class LockingType < SimpleDelegator # :nodoc: + def type_cast_from_database(value) + # `nil` *should* be changed to 0 + super.to_i + end + + def changed?(old_value, *) + # Ensure we save if the default was `nil` + super || old_value == 0 + end + + def init_with(coder) + __setobj__(coder['subtype']) + end + + def encode_with(coder) + coder['subtype'] = __getobj__ + end + end end end diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 654ef21b07..eb64d197f0 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -25,7 +25,7 @@ module ActiveRecord 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>" + value = value ? "<#{value.bytesize} bytes of binary data>" : "<NULL binary data>" end [column.name, value] diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index b6b02322d7..e94b6ae9eb 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -195,7 +195,7 @@ module ActiveRecord # == Database support # # Migrations are currently supported in MySQL, PostgreSQL, SQLite, - # SQL Server, Sybase, and Oracle (all supported databases except DB2). + # SQL Server, and Oracle (all supported databases except DB2). # # == More examples # @@ -372,13 +372,21 @@ module ActiveRecord end def call(env) - mtime = ActiveRecord::Migrator.last_migration.mtime.to_i - if @last_check < mtime - ActiveRecord::Migration.check_pending! - @last_check = mtime + if connection.supports_migrations? + mtime = ActiveRecord::Migrator.last_migration.mtime.to_i + if @last_check < mtime + ActiveRecord::Migration.check_pending!(connection) + @last_check = mtime + end end @app.call(env) end + + private + + def connection + ActiveRecord::Base.connection + end end class << self @@ -640,9 +648,11 @@ module ActiveRecord say_with_time "#{method}(#{arg_list})" do unless @connection.respond_to? :revert - unless arguments.empty? || method == :execute + unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method) arguments[0] = proper_table_name(arguments.first, table_name_options) - arguments[1] = proper_table_name(arguments.second, table_name_options) if method == :rename_table + if [:rename_table, :add_foreign_key].include?(method) + arguments[1] = proper_table_name(arguments.second, table_name_options) + end end end return super unless connection.respond_to?(method) @@ -711,7 +721,7 @@ module ActiveRecord if ActiveRecord::Base.timestamped_migrations [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % number].max else - "%.3d" % number + SchemaMigration.normalize_migration_number(number) end end @@ -851,19 +861,6 @@ module ActiveRecord migrations(migrations_paths).last || NullMigration.new end - def proper_table_name(name, options = {}) - ActiveSupport::Deprecation.warn "ActiveRecord::Migrator.proper_table_name is deprecated and will be removed in Rails 4.2. Use the proper_table_name instance method on ActiveRecord::Migration instead" - options = { - table_name_prefix: ActiveRecord::Base.table_name_prefix, - table_name_suffix: ActiveRecord::Base.table_name_suffix - }.merge(options) - if name.respond_to? :table_name - name.table_name - else - "#{options[:table_name_prefix]}#{name}#{options[:table_name_suffix]}" - end - end - def migrations_paths @migrations_paths ||= ['db/migrate'] # just to not break things if someone uses: migration_path = some_string diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index 5337106001..36256415df 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -74,7 +74,9 @@ module ActiveRecord :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column_default, :add_reference, :remove_reference, :transaction, :drop_join_table, :drop_table, :execute_block, :enable_extension, - :change_column, :execute, :remove_columns, :change_column_null # irreversible methods need to be here too + :change_column, :execute, :remove_columns, :change_column_null, + :add_foreign_key, :remove_foreign_key + # irreversible methods need to be here too ].each do |method| class_eval <<-EOV, __FILE__, __LINE__ + 1 def #{method}(*args, &block) # def create_table(*args, &block) @@ -167,6 +169,21 @@ module ActiveRecord [:change_column_null, args] end + def invert_add_foreign_key(args) + from_table, to_table, add_options = args + add_options ||= {} + + if add_options[:name] + options = { name: add_options[:name] } + elsif add_options[:column] + options = { column: add_options[:column] } + else + options = to_table + end + + [:remove_foreign_key, [from_table, options]] + end + # Forwards any missing method call to the \target. def method_missing(method, *args, &block) if @delegate.respond_to?(method) diff --git a/activerecord/lib/active_record/migration/join_table.rb b/activerecord/lib/active_record/migration/join_table.rb index ebf64cbcdc..05569fadbd 100644 --- a/activerecord/lib/active_record/migration/join_table.rb +++ b/activerecord/lib/active_record/migration/join_table.rb @@ -8,7 +8,7 @@ module ActiveRecord end def join_table_name(table_1, table_2) - [table_1.to_s, table_2.to_s].sort.join("_").to_sym + ModelSchema.derive_join_table_name(table_1, table_2).to_sym end end end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index dc5ff02882..850220babd 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -29,6 +29,10 @@ module ActiveRecord # :singleton-method: # Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp", # "people_basecamp"). By default, the suffix is the empty string. + # + # If you are organising your models within modules, you can add a suffix to the models within + # a namespace by defining a singleton method in the parent module called table_name_suffix which + # returns your chosen suffix. class_attribute :table_name_suffix, instance_writer: false self.table_name_suffix = "" @@ -47,6 +51,19 @@ module ActiveRecord self.pluralize_table_names = true self.inheritance_column = 'type' + + delegate :type_for_attribute, to: :class + end + + # Derives the join table name for +first_table+ and +second_table+. The + # table names appear in alphabetical order. A common prefix is removed + # (useful for namespaced models like Music::Artist and Music::Record): + # + # artists, records => artists_records + # records, artists => artists_records + # music_artists, music_records => music_artists_records + def self.derive_join_table_name(first_table, second_table) # :nodoc: + [first_table.to_s, second_table.to_s].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_") end module ClassMethods @@ -153,6 +170,10 @@ module ActiveRecord (parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix end + def full_table_name_suffix #:nodoc: + (parents.detect {|p| p.respond_to?(:table_name_suffix) } || self).table_name_suffix + end + # Defines the name of the table column which will store the class name on single-table # inheritance situations. # @@ -190,7 +211,7 @@ module ActiveRecord # given block. This is required for Oracle and is useful for any # database which relies on sequences for primary key generation. # - # If a sequence name is not explicitly set when using Oracle or Firebird, + # If a sequence name is not explicitly set when using Oracle, # it will default to the commonly used pattern of: #{table_name}_seq # # If a sequence name is not explicitly set when using PostgreSQL, it @@ -209,50 +230,29 @@ module ActiveRecord connection.schema_cache.table_exists?(table_name) end - # Returns an array of column objects for the table associated with this class. - def columns - @columns ||= connection.schema_cache.columns(table_name).map do |col| - col = col.dup - col.primary = (col.name == primary_key) - col - end - end - - # Returns a hash of column objects for the table associated with this class. - def columns_hash - @columns_hash ||= Hash[columns.map { |c| [c.name, c] }] + def attributes_builder # :nodoc: + @attributes_builder ||= AttributeSet::Builder.new(column_types) end def column_types # :nodoc: - @column_types ||= decorate_columns(columns_hash.dup) - end - - def decorate_columns(columns_hash) # :nodoc: - return if columns_hash.empty? - - @serialized_column_names ||= self.columns_hash.keys.find_all do |name| - serialized_attributes.key?(name) - end - - @serialized_column_names.each do |name| - columns_hash[name] = AttributeMethods::Serialization::Type.new(columns_hash[name]) - end - - @time_zone_column_names ||= self.columns_hash.find_all do |name, col| - create_time_zone_conversion_attribute?(name, col) - end.map!(&:first) - - @time_zone_column_names.each do |name| - columns_hash[name] = AttributeMethods::TimeZoneConversion::Type.new(columns_hash[name]) + @column_types ||= columns_hash.transform_values(&:cast_type).tap do |h| + h.default = Type::Value.new end + end - columns_hash + def type_for_attribute(attr_name) # :nodoc: + column_types[attr_name] end # Returns a hash where the keys are column names and the values are # default values when instantiating the AR object for this table. def column_defaults - @column_defaults ||= Hash[columns.map { |c| [c.name, c.default] }] + default_attributes.to_hash + end + + def default_attributes # :nodoc: + @default_attributes ||= attributes_builder.build_from_database( + columns_hash.transform_values(&:default)) end # Returns an array of column names as strings. @@ -263,7 +263,7 @@ module ActiveRecord # Returns an array of column objects where the primary id, all columns ending in "_id" or "_count", # and columns used for single table inheritance have been removed. def content_columns - @content_columns ||= columns.reject { |c| c.primary || c.name =~ /(_id|_count)$/ || c.name == inheritance_column } + @content_columns ||= columns.reject { |c| c.name == primary_key || c.name =~ /(_id|_count)$/ || c.name == inheritance_column } end # Resets all the cached information about columns, which will cause them @@ -298,27 +298,17 @@ module ActiveRecord connection.schema_cache.clear_table_cache!(table_name) if table_exists? @arel_engine = nil - @column_defaults = nil @column_names = nil - @columns = nil - @columns_hash = nil @column_types = nil @content_columns = nil + @default_attributes = nil @dynamic_methods_hash = nil @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column @relation = nil - @serialized_column_names = nil @time_zone_column_names = nil @cached_time_zone = nil end - # This is a hook for use by modules that need to do extra stuff to - # attributes when they are initialized. (e.g. attribute - # serialization) - def initialize_attributes(attributes, options = {}) #:nodoc: - attributes - end - private # Guesses the table name, but does not decorate it with prefix and suffix information. @@ -337,7 +327,8 @@ module ActiveRecord contained = contained.singularize if parent.pluralize_table_names contained += '_' end - "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{table_name_suffix}" + + "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{full_table_name_suffix}" else # STI subclasses always use their superclass' table. base.table_name diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index e6195e48a5..8a2a06f2ca 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -305,7 +305,7 @@ module ActiveRecord options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank attr_names.each do |association_name| - if reflection = reflect_on_association(association_name) + if reflection = _reflect_on_association(association_name) reflection.autosave = true add_autosave_association_callbacks(reflection) @@ -516,7 +516,7 @@ module ActiveRecord # Determines if a hash contains a truthy _destroy key. def has_destroy_flag?(hash) - ConnectionAdapters::Column.value_to_boolean(hash['_destroy']) + Type::Boolean.new.type_cast_from_user(hash['_destroy']) end # Determines if a new record should be rejected by checking @@ -542,7 +542,7 @@ module ActiveRecord end def raise_nested_attributes_record_not_found!(association_name, record_id) - raise RecordNotFound, "Couldn't find #{self.class.reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}" + raise RecordNotFound, "Couldn't find #{self.class._reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}" end end end diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index 05d0c41678..807c301596 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -23,7 +23,7 @@ module ActiveRecord end def size - 0 + calculate :size, nil end def empty? @@ -47,14 +47,28 @@ module ActiveRecord end def sum(*) - 0 + calculate :sum, nil + end + + def average(*) + calculate :average, nil + end + + def minimum(*) + calculate :minimum, nil + end + + def maximum(*) + calculate :maximum, nil end def calculate(operation, _column_name, _options = {}) # TODO: Remove _options argument as soon we remove support to # activerecord-deprecated_finders. - if operation == :count + if [:count, :sum, :size].include? operation group_values.any? ? Hash.new : 0 + elsif [:average, :minimum, :maximum].include?(operation) && group_values.any? + Hash.new else nil end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 13d7432773..96e44c2f59 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -37,7 +37,7 @@ module ActiveRecord end # Given an attributes hash, +instantiate+ returns a new instance of - # the appropriate class. + # the appropriate class. Accepts only keys as strings. # # For example, +Post.all+ may return Comments, Messages, and Emails # by storing the record's subclass in a +type+ attribute. By calling @@ -46,10 +46,10 @@ module ActiveRecord # # See +ActiveRecord::Inheritance#discriminate_class_for_record+ to see # how this "single-table" inheritance mapping is implemented. - def instantiate(record, column_types = {}) - klass = discriminate_class_for_record(record) - column_types = klass.decorate_columns(column_types.dup) - klass.allocate.init_with('attributes' => record, 'column_types' => column_types) + def instantiate(attributes, column_types = {}) + klass = discriminate_class_for_record(attributes) + attributes = klass.attributes_builder.build_from_database(attributes, column_types) + klass.allocate.init_with('attributes' => attributes, 'new_record' => false) end private @@ -180,7 +180,6 @@ module ActiveRecord def becomes(klass) became = klass.new became.instance_variable_set("@attributes", @attributes) - became.instance_variable_set("@attributes_cache", @attributes_cache) became.instance_variable_set("@changed_attributes", @changed_attributes) if defined?(@changed_attributes) became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) @@ -396,11 +395,8 @@ module ActiveRecord self.class.unscoped { self.class.find(id) } end - @attributes.update(fresh_object.instance_variable_get('@attributes')) - - @column_types = self.class.column_types - @column_types_override = fresh_object.instance_variable_get('@column_types_override') - @attributes_cache = {} + @attributes = fresh_object.instance_variable_get('@attributes') + @new_record = false self end @@ -490,7 +486,7 @@ module ActiveRecord # Updates the associated record with values matching those of the instance attributes. # Returns the number of affected rows. - def _update_record(attribute_names = @attributes.keys) + def _update_record(attribute_names = self.attribute_names) attributes_values = arel_attributes_with_values_for_update(attribute_names) if attributes_values.empty? 0 @@ -501,7 +497,7 @@ module ActiveRecord # Creates a record with values matching those of the instance attributes # and returns its id. - def _create_record(attribute_names = @attributes.keys) + def _create_record(attribute_names = self.attribute_names) attributes_values = arel_attributes_with_values_for_create(attribute_names) new_id = self.class.unscoped.insert attributes_values diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index df8654e5c1..16ad942912 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -29,9 +29,10 @@ module ActiveRecord end def call(env) - enabled = ActiveRecord::Base.connection.query_cache_enabled + connection = ActiveRecord::Base.connection + enabled = connection.query_cache_enabled connection_id = ActiveRecord::Base.connection_id - ActiveRecord::Base.connection.enable_query_cache! + connection.enable_query_cache! response = @app.call(env) response[2] = Rack::BodyProxy.new(response[2]) do diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index ef138c6f80..a9ddd9141f 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -37,14 +37,7 @@ module ActiveRecord # Post.find_by_sql ["SELECT body FROM comments WHERE author = :user_id OR approved_by = :user_id", { :user_id => user_id }] def find_by_sql(sql, binds = []) result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds) - column_types = {} - - if result_set.respond_to? :column_types - column_types = result_set.column_types - else - ActiveSupport::Deprecation.warn "the object returned from `select_all` must respond to `column_types`" - end - + column_types = result_set.column_types.except(*columns_hash.keys) result_set.map { |record| instantiate(record, column_types) } end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 6b0459ea37..fa94df7a52 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -28,6 +28,17 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.drop_current end + namespace :purge do + task :all => :load_config do + ActiveRecord::Tasks::DatabaseTasks.purge_all + end + end + + # desc "Empty the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV it defaults to purging the development and test databases." + task :purge => [:load_config] do + ActiveRecord::Tasks::DatabaseTasks.purge_current + end + desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." task :migrate => [:environment, :load_config] do ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true @@ -82,22 +93,21 @@ db_namespace = namespace :db do desc 'Display status of migrations' task :status => [:environment, :load_config] do - unless ActiveRecord::Base.connection.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name) - puts 'Schema migrations table does not exist yet.' - next # means "return" for rake task + unless ActiveRecord::SchemaMigration.table_exists? + abort 'Schema migrations table does not exist yet.' end - db_list = ActiveRecord::Base.connection.select_values("SELECT version FROM #{ActiveRecord::Migrator.schema_migrations_table_name}") - db_list.map! { |version| "%.3d" % version } - file_list = [] - ActiveRecord::Migrator.migrations_paths.each do |path| - Dir.foreach(path) do |file| - # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern - if match_data = /^(\d{3,})_(.+)\.rb$/.match(file) - status = db_list.delete(match_data[1]) ? 'up' : 'down' - file_list << [status, match_data[1], match_data[2].humanize] + db_list = ActiveRecord::SchemaMigration.normalized_versions + + file_list = + ActiveRecord::Migrator.migrations_paths.flat_map do |path| + # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern + Dir.foreach(path).grep(/^(\d{3,})_(.+)\.rb$/) do + version = ActiveRecord::SchemaMigration.normalize_migration_number($1) + status = db_list.delete(version) ? 'up' : 'down' + [status, version, $2.humanize] + end end - end - end + db_list.map! do |version| ['up', version, '********** NO FILE **********'] end @@ -105,8 +115,8 @@ db_namespace = namespace :db do puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n" puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name" puts "-" * 50 - (db_list + file_list).sort_by {|migration| migration[1]}.each do |migration| - puts "#{migration[0].center(8)} #{migration[1].ljust(14)} #{migration[2]}" + (db_list + file_list).sort_by { |_, version, _| version }.each do |status, version, name| + puts "#{status.center(8)} #{version.ljust(14)} #{name}" end puts end @@ -178,17 +188,21 @@ db_namespace = namespace :db do task :load => [:environment, :load_config] do require 'active_record/fixtures' - base_dir = if ENV['FIXTURES_PATH'] - File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten - else - ActiveRecord::Tasks::DatabaseTasks.fixtures_path - end + base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path - fixtures_dir = File.join [base_dir, ENV['FIXTURES_DIR']].compact + fixtures_dir = if ENV['FIXTURES_DIR'] + File.join base_dir, ENV['FIXTURES_DIR'] + else + base_dir + end - (ENV['FIXTURES'] ? ENV['FIXTURES'].split(',') : Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file| - ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_file) - end + fixture_files = if ENV['FIXTURES'] + ENV['FIXTURES'].split(',') + else + Pathname.glob("#{fixtures_dir}/**/*.yml").map {|f| f.basename.sub_ext('').to_s } + end + + ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_files) end # desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." @@ -200,12 +214,7 @@ db_namespace = namespace :db do puts %Q(The fixture ID for "#{label}" is #{ActiveRecord::FixtureSet.identify(label)}.) if label - base_dir = if ENV['FIXTURES_PATH'] - File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten - else - ActiveRecord::Tasks::DatabaseTasks.fixtures_path - end - + base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path Dir["#{base_dir}/**/*.yml"].each do |file| if data = YAML::load(ERB.new(IO.read(file)).result) @@ -278,7 +287,7 @@ db_namespace = namespace :db do db_namespace['structure:dump'].reenable end - # desc "Recreate the databases from the structure.sql file" + desc "Recreate the databases from the structure.sql file" task :load => [:environment, :load_config] do ActiveRecord::Tasks::DatabaseTasks.load_schema(:sql, ENV['DB_STRUCTURE']) end @@ -367,7 +376,7 @@ namespace :railties do task :migrations => :'db:load_config' do to_load = ENV['FROM'].blank? ? :all : ENV['FROM'].split(",").map {|n| n.strip } railties = {} - Rails.application.railties.each do |railtie| + Rails.application.migration_railties.each do |railtie| next unless to_load == :all || to_load.include?(railtie.railtie_name) if railtie.respond_to?(:paths) && (path = railtie.paths['db/migrate'].first) diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 95485ddada..1672128aa3 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -6,25 +6,32 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :reflections + class_attribute :_reflections class_attribute :aggregate_reflections - self.reflections = {} + self._reflections = {} self.aggregate_reflections = {} end def self.create(macro, name, scope, options, ar) - case macro - when :has_many, :belongs_to, :has_one - klass = options[:through] ? ThroughReflection : AssociationReflection - when :composed_of - klass = AggregateReflection - end - - klass.new(macro, name, scope, options, ar) + klass = case macro + when :composed_of + AggregateReflection + when :has_many + HasManyReflection + when :has_one + HasOneReflection + when :belongs_to + BelongsToReflection + else + raise "Unsupported Macro: #{macro}" + end + + reflection = klass.new(name, scope, options, ar) + options[:through] ? ThroughReflection.new(reflection) : reflection end def self.add_reflection(ar, name, reflection) - ar.reflections = ar.reflections.merge(name.to_s => reflection) + ar._reflections = ar._reflections.merge(name.to_s => reflection) end def self.add_aggregate_reflection(ar, name, reflection) @@ -53,6 +60,24 @@ module ActiveRecord aggregate_reflections[aggregation.to_s] end + # Returns a Hash of name of the reflection as the key and a AssociationReflection as the value. + # + # Account.reflections # => {balance: AggregateReflection} + # + # @api public + def reflections + ref = {} + _reflections.each do |name, reflection| + parent_name, parent_reflection = reflection.parent_reflection + if parent_name + ref[parent_name] = parent_reflection + else + ref[name] = reflection + end + end + ref + end + # Returns an array of AssociationReflection objects for all the # associations in the class. If you only want to reflect on a certain # association type, pass in the symbol (<tt>:has_many</tt>, <tt>:has_one</tt>, @@ -63,6 +88,7 @@ module ActiveRecord # Account.reflect_on_all_associations # returns an array of all associations # Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations # + # @api public def reflect_on_all_associations(macro = nil) association_reflections = reflections.values macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections @@ -73,36 +99,87 @@ module ActiveRecord # Account.reflect_on_association(:owner) # returns the owner AssociationReflection # Invoice.reflect_on_association(:line_items).macro # returns :has_many # + # @api public def reflect_on_association(association) reflections[association.to_s] end + # @api private + def _reflect_on_association(association) #:nodoc: + _reflections[association.to_s] + end + # Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled. + # + # @api public def reflect_on_all_autosave_associations reflections.values.select { |reflection| reflection.options[:autosave] } end end + # Holds all the methods that are shared between MacroReflection, AssociationReflection + # and ThroughReflection + class AbstractReflection # :nodoc: + def table_name + klass.table_name + end + + # Returns a new, unsaved instance of the associated class. +attributes+ will + # be passed to the class's constructor. + def build_association(attributes, &block) + klass.new(attributes, &block) + end + + def quoted_table_name + klass.quoted_table_name + end + + def primary_key_type + klass.type_for_attribute(klass.primary_key) + end + + # Returns the class name for the macro. + # + # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt> + # <tt>has_many :clients</tt> returns <tt>'Client'</tt> + def class_name + @class_name ||= (options[:class_name] || derive_class_name).to_s + end + + JoinKeys = Struct.new(:key, :foreign_key) # :nodoc: + + def join_keys(assoc_klass) + if source_macro == :belongs_to + if polymorphic? + reflection_key = association_primary_key(assoc_klass) + else + reflection_key = association_primary_key + end + reflection_foreign_key = foreign_key + else + reflection_foreign_key = active_record_primary_key + reflection_key = foreign_key + end + JoinKeys.new(reflection_key, reflection_foreign_key) + end + end # Base class for AggregateReflection and AssociationReflection. Objects of # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. # # MacroReflection - # AggregateReflection # AssociationReflection - # ThroughReflection - class MacroReflection + # AggregateReflection + # HasManyReflection + # HasOneReflection + # BelongsToReflection + # ThroughReflection + class MacroReflection < AbstractReflection # Returns the name of the macro. # # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:balance</tt> # <tt>has_many :clients</tt> returns <tt>:clients</tt> attr_reader :name - # Returns the macro type. - # - # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:composed_of</tt> - # <tt>has_many :clients</tt> returns <tt>:has_many</tt> - attr_reader :macro - attr_reader :scope # Returns the hash of options used for the macro. @@ -115,8 +192,7 @@ module ActiveRecord attr_reader :plural_name # :nodoc: - def initialize(macro, name, scope, options, active_record) - @macro = macro + def initialize(name, scope, options, active_record) @name = name @scope = scope @options = options @@ -129,6 +205,10 @@ module ActiveRecord def autosave=(autosave) @automatic_inverse_of = false @options[:autosave] = autosave + _, parent_reflection = self.parent_reflection + if parent_reflection + parent_reflection.autosave = autosave + end end # Returns the class for the macro. @@ -136,15 +216,11 @@ module ActiveRecord # <tt>composed_of :balance, class_name: 'Money'</tt> returns the Money class # <tt>has_many :clients</tt> returns the Client class def klass - @klass ||= class_name.constantize + @klass ||= compute_class(class_name) end - # Returns the class name for the macro. - # - # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt> - # <tt>has_many :clients</tt> returns <tt>'Client'</tt> - def class_name - @class_name ||= (options[:class_name] || derive_class_name).to_s + def compute_class(name) + name.constantize end # Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute, @@ -189,14 +265,18 @@ module ActiveRecord # a new association object. Use +build_association+ or +create_association+ # instead. This allows plugins to hook into association object creation. def klass - @klass ||= active_record.send(:compute_type, class_name) + @klass ||= compute_class(class_name) + end + + def compute_class(name) + active_record.send(:compute_type, name) end attr_reader :type, :foreign_type + attr_accessor :parent_reflection # [:name, Reflection] - def initialize(macro, name, scope, options, active_record) + def initialize(name, scope, options, active_record) super - @collection = :has_many == macro @automatic_inverse_of = nil @type = options[:as] && "#{options[:as]}_type" @foreign_type = options[:foreign_type] || "#{name}_type" @@ -207,7 +287,7 @@ module ActiveRecord def association_scope_cache(conn, owner) key = conn.prepared_statements - if options[:polymorphic] + if polymorphic? key = [key, owner.read_attribute(@foreign_type)] end @association_scope_cache[key] ||= @scope_lock.synchronize { @@ -215,24 +295,10 @@ module ActiveRecord } end - # Returns a new, unsaved instance of the associated class. +attributes+ will - # be passed to the class's constructor. - def build_association(attributes, &block) - klass.new(attributes, &block) - end - def constructable? # :nodoc: @constructable end - def table_name - klass.table_name - end - - def quoted_table_name - klass.quoted_table_name - end - def join_table @join_table ||= options[:join_table] || derive_join_table end @@ -241,10 +307,6 @@ module ActiveRecord @foreign_key ||= options[:foreign_key] || derive_foreign_key end - def primary_key_column - klass.columns_hash[klass.primary_key] - end - def association_foreign_key @association_foreign_key ||= options[:association_foreign_key] || class_name.foreign_key end @@ -271,13 +333,32 @@ module ActiveRecord end def check_validity_of_inverse! - unless options[:polymorphic] + unless polymorphic? if has_inverse? && inverse_of.nil? raise InverseOfAssociationNotFoundError.new(self) end end end + def check_preloadable! + return unless scope + + if scope.arity > 0 + ActiveSupport::Deprecation.warn <<-WARNING +The association scope '#{name}' is instance dependent (the scope block takes an argument). +Preloading happens before the individual instances are created. This means that there is no instance +being passed to the association scope. This will most likely result in broken or incorrect behavior. +Joining, Preloading and eager loading of these associations is deprecated and will be removed in the future. + WARNING + end + end + alias :check_eager_loadable! :check_preloadable! + + def join_id_for(owner) #:nodoc: + key = (source_macro == :belongs_to) ? foreign_key : active_record_primary_key + owner[key] + end + def through_reflection nil end @@ -302,7 +383,7 @@ module ActiveRecord scope ? [[scope]] : [[]] end - alias :source_macro :macro + def source_macro; macro; end def has_inverse? inverse_name @@ -311,12 +392,12 @@ module ActiveRecord def inverse_of return unless inverse_name - @inverse_of ||= klass.reflect_on_association inverse_name + @inverse_of ||= klass._reflect_on_association inverse_name end def polymorphic_inverse_of(associated_class) if has_inverse? - if inverse_relationship = associated_class.reflect_on_association(options[:inverse_of]) + if inverse_relationship = associated_class._reflect_on_association(options[:inverse_of]) inverse_relationship else raise InverseOfAssociationNotFoundError.new(self, associated_class) @@ -324,11 +405,16 @@ module ActiveRecord end end + # Returns the macro type. + # + # <tt>has_many :clients</tt> returns <tt>:has_many</tt> + def macro; raise NotImplementedError; end + # Returns whether or not this association reflection is for a collection # association. Returns +true+ if the +macro+ is either +has_many+ or # +has_and_belongs_to_many+, +false+ otherwise. def collection? - @collection + false end # Returns whether or not the association should be validated as part of @@ -341,7 +427,7 @@ module ActiveRecord # * you use autosave; <tt>autosave: true</tt> # * the association is a +has_many+ association def validate? - !options[:validate].nil? ? options[:validate] : (options[:autosave] == true || macro == :has_many) + !options[:validate].nil? ? options[:validate] : (options[:autosave] == true || collection?) end # Returns +true+ if +self+ is a +belongs_to+ reflection. @@ -349,10 +435,15 @@ module ActiveRecord macro == :belongs_to end + # Returns +true+ if +self+ is a +has_one+ reflection. + def has_one? + macro == :has_one + end + def association_class case macro when :belongs_to - if options[:polymorphic] + if polymorphic? Associations::BelongsToPolymorphicAssociation else Associations::BelongsToAssociation @@ -373,7 +464,7 @@ module ActiveRecord end def polymorphic? - options.key? :polymorphic + options[:polymorphic] end VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to] @@ -390,7 +481,7 @@ module ActiveRecord def calculate_constructable(macro, options) case macro when :belongs_to - !options[:polymorphic] + !polymorphic? when :has_one !options[:through] else @@ -414,10 +505,10 @@ module ActiveRecord # returns either nil or the inverse association name that it finds. def automatic_inverse_of if can_find_inverse_of_automatically?(self) - inverse_name = ActiveSupport::Inflector.underscore(active_record.name).to_sym + inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name).to_sym begin - reflection = klass.reflect_on_association(inverse_name) + reflection = klass._reflect_on_association(inverse_name) rescue NameError # Give up: we couldn't compute the klass type so we won't be able # to find any associations either. @@ -479,7 +570,7 @@ module ActiveRecord end def derive_join_table - [active_record.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_") + ModelSchema.derive_join_table_name active_record.table_name, klass.table_name end def primary_key(klass) @@ -487,15 +578,61 @@ module ActiveRecord end end + class HasManyReflection < AssociationReflection #:nodoc: + def initialize(name, scope, options, active_record) + super(name, scope, options, active_record) + end + + def macro; :has_many; end + + def collection? + true + end + end + + class HasOneReflection < AssociationReflection #:nodoc: + def initialize(name, scope, options, active_record) + super(name, scope, options, active_record) + end + + def macro; :has_one; end + end + + class BelongsToReflection < AssociationReflection #:nodoc: + def initialize(name, scope, options, active_record) + super(name, scope, options, active_record) + end + + def macro; :belongs_to; end + end + + class HasAndBelongsToManyReflection < AssociationReflection #:nodoc: + def initialize(name, scope, options, active_record) + super + end + + def macro; :has_and_belongs_to_many; end + + def collection? + true + end + end + # Holds all the meta-data about a :through association as it was specified # in the Active Record class. - class ThroughReflection < AssociationReflection #:nodoc: + class ThroughReflection < AbstractReflection #:nodoc: + attr_reader :delegate_reflection delegate :foreign_key, :foreign_type, :association_foreign_key, :active_record_primary_key, :type, :to => :source_reflection - def initialize(macro, name, scope, options, active_record) - super - @source_reflection_name = options[:source] + def initialize(delegate_reflection) + @delegate_reflection = delegate_reflection + @klass = delegate_reflection.options[:class] + @source_reflection_name = delegate_reflection.options[:source] + end + + def klass + @klass ||= delegate_reflection.compute_class(class_name) end # Returns the source of the through reflection. It checks both a singularized @@ -513,10 +650,10 @@ module ActiveRecord # # tags_reflection = Post.reflect_on_association(:tags) # tags_reflection.source_reflection - # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:belongs_to, @name=:tag, @active_record=Tagging, @plural_name="tags"> + # # => <ActiveRecord::Reflection::BelongsToReflection: @name=:tag, @active_record=Tagging, @plural_name="tags"> # def source_reflection - through_reflection.klass.reflect_on_association(source_reflection_name) + through_reflection.klass._reflect_on_association(source_reflection_name) end # Returns the AssociationReflection object specified in the <tt>:through</tt> option @@ -529,10 +666,10 @@ module ActiveRecord # # tags_reflection = Post.reflect_on_association(:tags) # tags_reflection.through_reflection - # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @active_record=Post, @plural_name="taggings"> + # # => <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @active_record=Post, @plural_name="taggings"> # def through_reflection - active_record.reflect_on_association(options[:through]) + active_record._reflect_on_association(options[:through]) end # Returns an array of reflections which are involved in this association. Each item in the @@ -549,8 +686,8 @@ module ActiveRecord # # tags_reflection = Post.reflect_on_association(:tags) # tags_reflection.chain - # # => [<ActiveRecord::Reflection::ThroughReflection: @macro=:has_many, @name=:tags, @options={:through=>:taggings}, @active_record=Post>, - # <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @options={}, @active_record=Post>] + # # => [<ActiveRecord::Reflection::ThroughReflection: @delegate_reflection=#<ActiveRecord::Reflection::HasManyReflection: @name=:tags...>, + # <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @options={}, @active_record=Post>] # def chain @chain ||= begin @@ -639,7 +776,7 @@ module ActiveRecord names = [name.to_s.singularize, name].collect { |n| n.to_sym }.uniq names = names.find_all { |n| - through_reflection.klass.reflect_on_association(n) + through_reflection.klass._reflect_on_association(n) } if names.length > 1 @@ -672,7 +809,7 @@ directive on your declaration like: raise HasManyThroughAssociationNotFoundError.new(active_record.name, self) end - if through_reflection.options[:polymorphic] + if through_reflection.polymorphic? raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self) end @@ -680,15 +817,15 @@ directive on your declaration like: raise HasManyThroughSourceAssociationNotFoundError.new(self) end - if options[:source_type] && source_reflection.options[:polymorphic].nil? + if options[:source_type] && !source_reflection.polymorphic? raise HasManyThroughAssociationPointlessSourceTypeError.new(active_record.name, self, source_reflection) end - if source_reflection.options[:polymorphic] && options[:source_type].nil? + if source_reflection.polymorphic? && options[:source_type].nil? raise HasManyThroughAssociationPolymorphicSourceError.new(active_record.name, self, source_reflection) end - if macro == :has_one && through_reflection.collection? + if has_one? && through_reflection.collection? raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection) end @@ -697,15 +834,25 @@ directive on your declaration like: protected - def actual_source_reflection # FIXME: this is a horrible name - source_reflection.actual_source_reflection - end + def actual_source_reflection # FIXME: this is a horrible name + source_reflection.send(:actual_source_reflection) + end + + def primary_key(klass) + klass.primary_key || raise(UnknownPrimaryKey.new(klass)) + end private def derive_class_name # get the class_name of the belongs_to association of the through reflection options[:source_type] || source_reflection.class_name end + + delegate_methods = AssociationReflection.public_instance_methods - + public_instance_methods + + delegate(*delegate_methods, to: :delegate_reflection) + end end end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 24b33ab0a8..ad54d84665 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -12,6 +12,7 @@ module ActiveRecord SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering, :reverse_order, :distinct, :create_with, :uniq] + INVALID_METHODS_FOR_DELETE_ALL = [:limit, :distinct, :offset, :group, :having] VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS @@ -73,7 +74,14 @@ module ActiveRecord def _update_record(values, id, id_was) # :nodoc: substitutes, binds = substitute_values values - um = @klass.unscoped.where(@klass.arel_table[@klass.primary_key].eq(id_was || id)).arel.compile_update(substitutes, @klass.primary_key) + + scope = @klass.unscoped + + if @klass.finder_needs_type_condition? + scope.unscope!(where: @klass.inheritance_column) + end + + um = scope.where(@klass.arel_table[@klass.primary_key].eq(id_was || id)).arel.compile_update(substitutes, @klass.primary_key) @klass.connection.update( um, @@ -234,6 +242,11 @@ module ActiveRecord @records end + # Serializes the relation objects Array. + def encode_with(coder) + coder.represent_seq(nil, to_a) + end + def as_json(options = nil) #:nodoc: to_a.as_json(options) end @@ -430,12 +443,21 @@ module ActiveRecord # If you need to destroy dependent associations or call your <tt>before_*</tt> or # +after_destroy+ callbacks, use the +destroy_all+ method instead. # - # If a limit scope is supplied, +delete_all+ raises an ActiveRecord error: + # If an invalid method is supplied, +delete_all+ raises an ActiveRecord error: # # Post.limit(100).delete_all - # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit scope + # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit def delete_all(conditions = nil) - raise ActiveRecordError.new("delete_all doesn't support limit scope") if self.limit_value + invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select { |method| + if MULTI_VALUE_METHODS.include?(method) + send("#{method}_values").any? + else + send("#{method}_value") + end + } + if invalid_methods.any? + raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}") + end if conditions where(conditions).delete_all diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 0b56430b34..90e99957f6 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -19,6 +19,22 @@ module ActiveRecord # # Person.group(:city).count # # => { 'Rome' => 5, 'Paris' => 3 } + # + # If +count+ is used with +group+ for multiple columns, it returns a Hash whose + # keys are an array containing the individual values of each column and the value + # of each key would be the +count+. + # + # Article.group(:status, :category).count + # # => {["draft", "business"]=>10, ["draft", "technology"]=>4, + # ["published", "business"]=>0, ["published", "technology"]=>2} + # + # If +count+ is used with +select+, it will count the selected columns: + # + # Person.select(:age).count + # # => counts the number of different age values + # + # Note: not all valid +select+ expressions are valid +count+ expressions. The specifics differ + # between databases. In invalid cases, an error from the database is thrown. def count(column_name = nil, options = {}) # TODO: Remove options argument as soon we remove support to # activerecord-deprecated_finders. @@ -162,18 +178,7 @@ module ActiveRecord columns_hash.key?(cn) ? arel_table[cn] : cn } result = klass.connection.select_all(relation.arel, nil, bind_values) - columns = result.columns.map do |key| - klass.column_types.fetch(key) { - result.column_types.fetch(key) { result.identity_type } - } - end - - result = result.map do |attributes| - values = klass.initialize_attributes(attributes).values - - columns.zip(values).map { |column, value| column.type_cast value } - end - columns.one? ? result.map!(&:first) : result + result.cast_values(klass.column_types) end end @@ -242,7 +247,7 @@ module ActiveRecord return 0 if relation.limit_value == 0 query_builder = build_count_subquery(relation, column_name, distinct) - bind_values = relation.bind_values + bind_values = query_builder.bind_values + relation.bind_values else column = aggregate_column(column_name) @@ -259,7 +264,7 @@ module ActiveRecord row = result.first value = row && row.values.first column = result.column_types.fetch(column_alias) do - column_for(column_name) + type_for(column_name) end type_cast_calculated_value(value, column, operation) @@ -269,8 +274,8 @@ module ActiveRecord group_attrs = group_values if group_attrs.first.respond_to?(:to_sym) - association = @klass.reflect_on_association(group_attrs.first.to_sym) - associated = group_attrs.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations + association = @klass._reflect_on_association(group_attrs.first.to_sym) + associated = group_attrs.size == 1 && association && association.belongs_to? # only count belongs_to associations group_fields = Array(associated ? association.foreign_key : group_attrs) else group_fields = group_attrs @@ -322,14 +327,14 @@ module ActiveRecord Hash[calculated_data.map do |row| key = group_columns.map { |aliaz, col_name| column = calculated_data.column_types.fetch(aliaz) do - column_for(col_name) + type_for(col_name) end type_cast_calculated_value(row[aliaz], column) } key = key.first if key.size == 1 key = key_records[key] if associated - column_type = calculated_data.column_types.fetch(aggregate_alias) { column_for(column_name) } + column_type = calculated_data.column_types.fetch(aggregate_alias) { type_for(column_name) } [key, type_cast_calculated_value(row[aggregate_alias], column_type, operation)] end] end @@ -356,24 +361,20 @@ module ActiveRecord @klass.connection.table_alias_for(table_name) end - def column_for(field) + def type_for(field) field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last - @klass.columns_hash[field_name] + @klass.type_for_attribute(field_name) end - def type_cast_calculated_value(value, column, operation = nil) + def type_cast_calculated_value(value, type, operation = nil) case operation when 'count' then value.to_i - when 'sum' then type_cast_using_column(value || 0, column) + when 'sum' then type.type_cast_from_database(value || 0) when 'average' then value.respond_to?(:to_d) ? value.to_d : value - else type_cast_using_column(value, column) + else type.type_cast_from_database(value) end end - def type_cast_using_column(value, column) - column ? column.type_cast(value) : value - end - # TODO: refactor to allow non-string `select_values` (eg. Arel nodes). def select_for_count if select_values.present? @@ -389,9 +390,11 @@ module ActiveRecord aliased_column = aggregate_column(column_name == :all ? 1 : column_name).as(column_alias) relation.select_values = [aliased_column] - subquery = relation.arel.as(subquery_alias) + arel = relation.arel + subquery = arel.as(subquery_alias) sm = Arel::SelectManager.new relation.engine + sm.bind_values = arel.bind_values select_value = operation_over_aggregate_column(column_alias, 'count', distinct) sm.project(select_value).from(subquery) end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 9c666dcd3b..50f4d5c7ab 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -43,7 +43,7 @@ module ActiveRecord :keep_if, :pop, :shift, :delete_at, :compact, :select! ].to_set # :nodoc: - delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, to: :to_a + delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, to: :to_a delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, :connection, :columns_hash, :to => :klass diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index db32ae12a8..0c9c761f97 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -65,7 +65,7 @@ module ActiveRecord # # returns an Array of the required fields, available since Rails 3.1. def find(*args) if block_given? - to_a.find { |*block_args| yield(*block_args) } + to_a.find(*args) { |*block_args| yield(*block_args) } else find_with_ids(*args) end @@ -336,7 +336,16 @@ module ActiveRecord end def find_with_associations - join_dependency = construct_join_dependency + # NOTE: the JoinDependency constructed here needs to know about + # any joins already present in `self`, so pass them in + # + # failing to do so means that in cases like activerecord/test/cases/associations/inner_join_association_test.rb:136 + # incorrect SQL is generated. In that case, the join dependency for + # SpecialCategorizations is constructed without knowledge of the + # preexisting join in joins_values to categorizations (by way of + # the `has_many :through` for categories). + # + join_dependency = construct_join_dependency(joins_values) aliases = join_dependency.aliases relation = select aliases.columns diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index fcb28a18f6..ac41d0aa80 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -156,7 +156,7 @@ module ActiveRecord def filter_binds(lhs_binds, removed_wheres) return lhs_binds if removed_wheres.empty? - set = Set.new removed_wheres.map { |x| x.left.name } + set = Set.new removed_wheres.map { |x| x.left.name.to_s } lhs_binds.dup.delete_if { |col,_| set.include? col.name } end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index d40f276968..eff5c8f09c 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -26,7 +26,7 @@ module ActiveRecord queries << '1=0' else table = Arel::Table.new(column, default_table.engine) - association = klass.reflect_on_association(column.to_sym) + association = klass._reflect_on_association(column.to_sym) value.each do |k, v| queries.concat expand(association && association.klass, table, k, v) @@ -55,7 +55,7 @@ module ActiveRecord # # For polymorphic relationships, find the foreign key and type: # PriceEstimate.where(estimate_of: treasure) - if klass && reflection = klass.reflect_on_association(column.to_sym) + if klass && reflection = klass._reflect_on_association(column.to_sym) if reflection.polymorphic? && base_class = polymorphic_base_class_from_value(value) queries << build(table[reflection.foreign_type], base_class) end diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb index 2f6c34ac08..78dba8be06 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb @@ -2,28 +2,33 @@ module ActiveRecord class PredicateBuilder class ArrayHandler # :nodoc: def call(attribute, value) + return attribute.in([]) if value.empty? + values = value.map { |x| x.is_a?(Base) ? x.id : x } ranges, values = values.partition { |v| v.is_a?(Range) } + nils, values = values.partition(&:nil?) - values_predicate = if values.include?(nil) - values = values.compact - + values_predicate = case values.length - when 0 - attribute.eq(nil) - when 1 - attribute.eq(values.first).or(attribute.eq(nil)) - else - attribute.in(values).or(attribute.eq(nil)) + when 0 then NullPredicate + when 1 then attribute.eq(values.first) + else attribute.in(values) end - else - attribute.in(values) + + unless nils.empty? + values_predicate = values_predicate.or(attribute.eq(nil)) end array_predicates = ranges.map { |range| attribute.in(range) } array_predicates << values_predicate array_predicates.inject { |composite, predicate| composite.or(predicate) } end + + module NullPredicate + def self.or(other) + other + end + end end end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 416f2305d2..1262b2c291 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -573,15 +573,11 @@ WARNING end end - def where!(opts = :chain, *rest) # :nodoc: - if opts == :chain - WhereChain.new(self) - else - references!(PredicateBuilder.references(opts)) if Hash === opts + def where!(opts, *rest) # :nodoc: + references!(PredicateBuilder.references(opts)) if Hash === opts - self.where_values += build_where(opts, rest) - self - end + self.where_values += build_where(opts, rest) + self end # Allows you to change a previously set where condition for a given attribute, instead of appending to that condition. @@ -950,7 +946,6 @@ WARNING [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] when Hash opts = PredicateBuilder.resolve_column_aliases(klass, opts) - attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts) bv_len = bind_values.length tmp_opts, bind_values = create_binds(opts, bv_len) diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 228b2aa60f..8405fdaeb9 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -31,7 +31,7 @@ module ActiveRecord class Result include Enumerable - IDENTITY_TYPE = Class.new { def type_cast(v); v; end }.new # :nodoc: + IDENTITY_TYPE = Type::Value.new # :nodoc: attr_reader :columns, :rows, :column_types @@ -42,14 +42,6 @@ module ActiveRecord @column_types = column_types end - def identity_type # :nodoc: - IDENTITY_TYPE - end - - def column_type(name) - @column_types[name] || identity_type - end - def each if block_given? hash_rows.each { |row| yield row } @@ -82,6 +74,15 @@ module ActiveRecord hash_rows.last end + def cast_values(type_overrides = {}) # :nodoc: + types = columns.map { |name| column_type(name, type_overrides) } + result = rows.map do |values| + types.zip(values).map { |type, value| type.type_cast_from_database(value) } + end + + columns.one? ? result.map!(&:first) : result + end + def initialize_copy(other) @columns = columns.dup @rows = rows.dup @@ -91,6 +92,12 @@ module ActiveRecord private + def column_type(name, type_overrides = {}) + type_overrides.fetch(name) do + column_types.fetch(name, IDENTITY_TYPE) + end + end + def hash_rows @hash_rows ||= begin diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 1aa93ffbb3..ff70cbed0f 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -107,7 +107,7 @@ module ActiveRecord end.join(', ') end - # Sanitizes a +string+ so that it is safe to use within a sql + # Sanitizes a +string+ so that it is safe to use within an SQL # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%" def sanitize_sql_like(string, escape_character = "\\") pattern = Regexp.union(escape_character, "%", "_") diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index e055d571ab..fae6427ea1 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -91,16 +91,17 @@ HEADER end def tables(stream) - @connection.tables.sort.each do |tbl| - next if ['schema_migrations', ignore_tables].flatten.any? do |ignored| - case ignored - when String; remove_prefix_and_suffix(tbl) == ignored - when Regexp; remove_prefix_and_suffix(tbl) =~ ignored - else - raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.' - end + sorted_tables = @connection.tables.sort + + sorted_tables.each do |table_name| + table(table_name, stream) unless ignored?(table_name) + end + + # dump foreign keys at the end to make sure all dependent tables exist. + if @connection.supports_foreign_keys? + sorted_tables.each do |tbl| + foreign_keys(tbl, stream) end - table(tbl, stream) end end @@ -112,7 +113,8 @@ HEADER # first dump primary key column if @connection.respond_to?(:pk_and_sequence_for) pk, _ = @connection.pk_and_sequence_for(table) - elsif @connection.respond_to?(:primary_key) + end + if !pk && @connection.respond_to?(:primary_key) pk = @connection.primary_key(table) end @@ -212,8 +214,49 @@ HEADER end end + def foreign_keys(table, stream) + if (foreign_keys = @connection.foreign_keys(table)).any? + add_foreign_key_statements = foreign_keys.map do |foreign_key| + parts = [ + 'add_foreign_key ' + remove_prefix_and_suffix(foreign_key.from_table).inspect, + remove_prefix_and_suffix(foreign_key.to_table).inspect, + ] + + if foreign_key.column != @connection.foreign_key_column_for(foreign_key.to_table) + parts << ('column: ' + foreign_key.column.inspect) + end + + if foreign_key.custom_primary_key? + parts << ('primary_key: ' + foreign_key.primary_key.inspect) + end + + if foreign_key.name !~ /^fk_rails_[0-9a-f]{10}$/ + parts << ('name: ' + foreign_key.name.inspect) + end + + parts << ('on_update: ' + foreign_key.on_update.inspect) if foreign_key.on_update + parts << ('on_delete: ' + foreign_key.on_delete.inspect) if foreign_key.on_delete + + ' ' + parts.join(', ') + end + + stream.puts add_foreign_key_statements.sort.join("\n") + end + end + def remove_prefix_and_suffix(table) table.gsub(/^(#{@options[:table_name_prefix]})(.+)(#{@options[:table_name_suffix]})$/, "\\2") end + + def ignored?(table_name) + ['schema_migrations', ignore_tables].flatten.any? do |ignored| + case ignored + when String; remove_prefix_and_suffix(table_name) == ignored + when Regexp; remove_prefix_and_suffix(table_name) =~ ignored + else + raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.' + end + end + end end end diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index a9d164e366..b5038104ac 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -5,6 +5,9 @@ require 'active_record/base' module ActiveRecord class SchemaMigration < ActiveRecord::Base class << self + def primary_key + nil + end def table_name "#{table_name_prefix}#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}" @@ -36,6 +39,14 @@ module ActiveRecord connection.drop_table(table_name) end end + + def normalize_migration_number(number) + "%.3d" % number.to_i + end + + def normalized_versions + pluck(:version).map { |v| normalize_migration_number v } + end end def version diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index 1a766093d0..c2484d02ed 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -180,13 +180,9 @@ module ActiveRecord #:nodoc: class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc: def compute_type klass = @serializable.class - type = if klass.serialized_attributes.key?(name) - super - elsif klass.columns_hash.key?(name) - klass.columns_hash[name].type - else - NilClass - end + column = klass.columns_hash[name] || Type::Value.new + + type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] || column.type { :text => :string, :time => :datetime }[type] || type diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 79a6ccbda0..3c291f28e3 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -66,8 +66,9 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :stored_attributes, instance_accessor: false - self.stored_attributes = {} + class << self + attr_accessor :local_stored_attributes + end end module ClassMethods @@ -93,18 +94,26 @@ module ActiveRecord # 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 + self.local_stored_attributes ||= {} + self.local_stored_attributes[store_attribute] ||= [] + self.local_stored_attributes[store_attribute] |= keys end - def _store_accessors_module + def _store_accessors_module # :nodoc: @_store_accessors_module ||= begin mod = Module.new include mod mod end end + + def stored_attributes + parent = superclass.respond_to?(:stored_attributes) ? superclass.stored_attributes : {} + if self.local_stored_attributes + parent.merge!(self.local_stored_attributes) { |k, a, b| a | b } + end + parent + end end protected @@ -120,10 +129,10 @@ module ActiveRecord private def store_accessor_for(store_attribute) - @column_types[store_attribute.to_s].accessor + type_for_attribute(store_attribute.to_s).accessor end - class HashAccessor + class HashAccessor # :nodoc: def self.read(object, attribute, key) prepare(object, attribute) object.public_send(attribute)[key] @@ -142,7 +151,7 @@ module ActiveRecord end end - class StringKeyedHashAccessor < HashAccessor + class StringKeyedHashAccessor < HashAccessor # :nodoc: def self.read(object, attribute, key) super object, attribute, key.to_s end @@ -152,7 +161,7 @@ module ActiveRecord end end - class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor + class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor # :nodoc: def self.prepare(object, store_attribute) attribute = object.send(store_attribute) unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess) diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 168b338b97..b7315ed4b3 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -6,7 +6,7 @@ module ActiveRecord # <tt>ActiveRecord::Tasks::DatabaseTasks</tt> is a utility class, which encapsulates # logic behind common tasks used to manage database and migrations. # - # The tasks defined here are used in rake tasks provided by Active Record. + # The tasks defined here are used with Rake tasks provided by Active Record. # # In order to use DatabaseTasks, a few config values need to be set. All the needed # config values are set by Rails already, so it's necessary to do it only if you @@ -14,7 +14,6 @@ module ActiveRecord # (in such case after configuring the database tasks, you can also use the rake tasks # defined in Active Record). # - # # The possible config values are: # # * +env+: current environment (like Rails.env). @@ -59,7 +58,11 @@ module ActiveRecord end def fixtures_path - @fixtures_path ||= File.join(root, 'test', 'fixtures') + @fixtures_path ||= if ENV['FIXTURES_PATH'] + File.join(root, ENV['FIXTURES_PATH']) + else + File.join(root, 'test', 'fixtures') + end end def root @@ -107,6 +110,8 @@ module ActiveRecord def drop(*arguments) configuration = arguments.first class_for_adapter(configuration['adapter']).new(*arguments).drop + rescue ActiveRecord::NoDatabaseError + $stderr.puts "Database '#{configuration['database']}' does not exist" rescue Exception => error $stderr.puts error, *(error.backtrace) $stderr.puts "Couldn't drop #{configuration['database']}" @@ -144,6 +149,18 @@ module ActiveRecord class_for_adapter(configuration['adapter']).new(configuration).purge end + def purge_all + each_local_configuration { |configuration| + purge configuration + } + end + + def purge_current(environment = env) + each_current_configuration(environment) { |configuration| + purge configuration + } + end + def structure_dump(*arguments) configuration = arguments.first filename = arguments.delete_at 1 diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index c755831e6d..d890196f47 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -42,7 +42,7 @@ module ActiveRecord end def purge - establish_connection :test + establish_connection configuration connection.recreate_database configuration['database'], creation_options end @@ -124,7 +124,7 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION; end def root_password - $stdout.print "Please provide the root password for your mysql installation\n>" + $stdout.print "Please provide the root password for your MySQL installation\n>" $stdin.gets.strip end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 6c30ccab72..e2e37e7c00 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -99,9 +99,11 @@ module ActiveRecord end def max_updated_column_timestamp(timestamp_names = timestamp_attributes_for_update) - if (timestamps = timestamp_names.map { |attr| self[attr] }.compact).present? - timestamps.map { |ts| ts.to_time }.max - end + timestamp_names + .map { |attr| self[attr] } + .compact + .map(&:to_time) + .max end def current_time_from_proper_timezone diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 17f76b63b3..7e4dc4c895 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -1,5 +1,3 @@ -require 'thread' - module ActiveRecord # See ActiveRecord::Transactions::ClassMethods for documentation. module Transactions @@ -295,7 +293,7 @@ module ActiveRecord def committed! #:nodoc: run_callbacks :commit if destroyed? || persisted? ensure - @_start_transaction_state.clear + force_clear_transaction_record_state end # Call the +after_rollback+ callbacks. The +force_restore_state+ argument indicates if the record @@ -328,7 +326,7 @@ module ActiveRecord begin status = yield rescue ActiveRecord::Rollback - @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 + clear_transaction_record_state status = nil end @@ -341,7 +339,7 @@ module ActiveRecord # Save the new record state and id of a record so it can be restored later if a transaction fails. def remember_transaction_record_state #:nodoc: - @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key) + @_start_transaction_state[:id] = id unless @_start_transaction_state.include?(:new_record) @_start_transaction_state[:new_record] = @new_record end @@ -349,13 +347,18 @@ module ActiveRecord @_start_transaction_state[:destroyed] = @destroyed end @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 - @_start_transaction_state[:frozen?] = @attributes.frozen? + @_start_transaction_state[:frozen?] = frozen? end # Clear the new record state and id of a record. def clear_transaction_record_state #:nodoc: @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 - @_start_transaction_state.clear if @_start_transaction_state[:level] < 1 + force_clear_transaction_record_state if @_start_transaction_state[:level] < 1 + end + + # Force to clear the transaction record state. + def force_clear_transaction_record_state #:nodoc: + @_start_transaction_state.clear end # Restore the new record state and id of a record that was previously saved by a call to save_record_state. @@ -364,17 +367,10 @@ module ActiveRecord transaction_level = (@_start_transaction_state[:level] || 0) - 1 if transaction_level < 1 || force restore_state = @_start_transaction_state - was_frozen = restore_state[:frozen?] - @attributes = @attributes.dup if @attributes.frozen? + thaw unless restore_state[:frozen?] @new_record = restore_state[:new_record] @destroyed = restore_state[:destroyed] - if restore_state.has_key?(:id) - write_attribute(self.class.primary_key, restore_state[:id]) - else - @attributes.delete(self.class.primary_key) - @attributes_cache.delete(self.class.primary_key) - end - @attributes.freeze if was_frozen + write_attribute(self.class.primary_key, restore_state[:id]) end end end diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb new file mode 100644 index 0000000000..f1384e0bb2 --- /dev/null +++ b/activerecord/lib/active_record/type.rb @@ -0,0 +1,20 @@ +require 'active_record/type/mutable' +require 'active_record/type/numeric' +require 'active_record/type/time_value' +require 'active_record/type/value' + +require 'active_record/type/binary' +require 'active_record/type/boolean' +require 'active_record/type/date' +require 'active_record/type/date_time' +require 'active_record/type/decimal' +require 'active_record/type/decimal_without_scale' +require 'active_record/type/float' +require 'active_record/type/integer' +require 'active_record/type/serialized' +require 'active_record/type/string' +require 'active_record/type/text' +require 'active_record/type/time' + +require 'active_record/type/type_map' +require 'active_record/type/hash_lookup_type_map' diff --git a/activerecord/lib/active_record/type/binary.rb b/activerecord/lib/active_record/type/binary.rb new file mode 100644 index 0000000000..d29ff4e494 --- /dev/null +++ b/activerecord/lib/active_record/type/binary.rb @@ -0,0 +1,40 @@ +module ActiveRecord + module Type + class Binary < Value # :nodoc: + def type + :binary + end + + def binary? + true + end + + def type_cast(value) + if value.is_a?(Data) + value.to_s + else + super + end + end + + def type_cast_for_database(value) + return if value.nil? + Data.new(super) + end + + class Data # :nodoc: + def initialize(value) + @value = value.to_s + end + + def to_s + @value + end + + def hex + @value.unpack('H*')[0] + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/boolean.rb b/activerecord/lib/active_record/type/boolean.rb new file mode 100644 index 0000000000..06dd17ed28 --- /dev/null +++ b/activerecord/lib/active_record/type/boolean.rb @@ -0,0 +1,19 @@ +module ActiveRecord + module Type + class Boolean < Value # :nodoc: + def type + :boolean + end + + private + + def cast_value(value) + if value == '' + nil + else + ConnectionAdapters::Column::TRUE_VALUES.include?(value) + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/date.rb b/activerecord/lib/active_record/type/date.rb new file mode 100644 index 0000000000..d90a6069b7 --- /dev/null +++ b/activerecord/lib/active_record/type/date.rb @@ -0,0 +1,46 @@ +module ActiveRecord + module Type + class Date < Value # :nodoc: + def type + :date + end + + def klass + ::Date + end + + def type_cast_for_schema(value) + "'#{value.to_s(:db)}'" + end + + private + + def cast_value(value) + if value.is_a?(::String) + return if value.empty? + fast_string_to_date(value) || fallback_string_to_date(value) + elsif value.respond_to?(:to_date) + value.to_date + else + value + end + end + + def fast_string_to_date(string) + if string =~ ConnectionAdapters::Column::Format::ISO_DATE + new_date $1.to_i, $2.to_i, $3.to_i + end + end + + def fallback_string_to_date(string) + new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday)) + end + + def new_date(year, mon, mday) + if year && year != 0 + ::Date.new(year, mon, mday) rescue nil + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb new file mode 100644 index 0000000000..5f19608a33 --- /dev/null +++ b/activerecord/lib/active_record/type/date_time.rb @@ -0,0 +1,43 @@ +module ActiveRecord + module Type + class DateTime < Value # :nodoc: + include TimeValue + + def type + :datetime + end + + def type_cast_for_database(value) + zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal + + if value.acts_like?(:time) + value.send(zone_conversion_method) + else + super + end + end + + private + + def cast_value(string) + return string unless string.is_a?(::String) + return if string.empty? + + fast_string_to_time(string) || fallback_string_to_time(string) + end + + # '0.123456' -> 123456 + # '1.123456' -> 123456 + def microseconds(time) + time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0 + end + + def fallback_string_to_time(string) + 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, :offset)) + end + end + end +end diff --git a/activerecord/lib/active_record/type/decimal.rb b/activerecord/lib/active_record/type/decimal.rb new file mode 100644 index 0000000000..ba5d244729 --- /dev/null +++ b/activerecord/lib/active_record/type/decimal.rb @@ -0,0 +1,27 @@ +module ActiveRecord + module Type + class Decimal < Value # :nodoc: + include Numeric + + def type + :decimal + end + + def type_cast_for_schema(value) + value.to_s + end + + private + + def cast_value(value) + if value.is_a?(::Numeric) || value.is_a?(::String) + BigDecimal(value, precision.to_i) + elsif value.respond_to?(:to_d) + value.to_d + else + cast_value(value.to_s) + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/decimal_without_scale.rb b/activerecord/lib/active_record/type/decimal_without_scale.rb new file mode 100644 index 0000000000..cabdcecdd7 --- /dev/null +++ b/activerecord/lib/active_record/type/decimal_without_scale.rb @@ -0,0 +1,11 @@ +require 'active_record/type/integer' + +module ActiveRecord + module Type + class DecimalWithoutScale < Integer # :nodoc: + def type + :decimal + end + end + end +end diff --git a/activerecord/lib/active_record/type/float.rb b/activerecord/lib/active_record/type/float.rb new file mode 100644 index 0000000000..42eb44b9a9 --- /dev/null +++ b/activerecord/lib/active_record/type/float.rb @@ -0,0 +1,19 @@ +module ActiveRecord + module Type + class Float < Value # :nodoc: + include Numeric + + def type + :float + end + + alias type_cast_for_database type_cast + + private + + def cast_value(value) + value.to_f + end + end + end +end diff --git a/activerecord/lib/active_record/type/hash_lookup_type_map.rb b/activerecord/lib/active_record/type/hash_lookup_type_map.rb new file mode 100644 index 0000000000..bf92680268 --- /dev/null +++ b/activerecord/lib/active_record/type/hash_lookup_type_map.rb @@ -0,0 +1,19 @@ +module ActiveRecord + module Type + class HashLookupTypeMap < TypeMap # :nodoc: + delegate :key?, to: :@mapping + + def lookup(type, *args) + @mapping.fetch(type, proc { default_value }).call(type, *args) + end + + def fetch(type, *args, &block) + @mapping.fetch(type, block).call(type, *args) + end + + def alias_type(type, alias_type) + register_type(type) { |_, *args| lookup(alias_type, *args) } + end + end + end +end diff --git a/activerecord/lib/active_record/type/integer.rb b/activerecord/lib/active_record/type/integer.rb new file mode 100644 index 0000000000..08477d1303 --- /dev/null +++ b/activerecord/lib/active_record/type/integer.rb @@ -0,0 +1,23 @@ +module ActiveRecord + module Type + class Integer < Value # :nodoc: + include Numeric + + def type + :integer + end + + alias type_cast_for_database type_cast + + private + + def cast_value(value) + case value + when true then 1 + when false then 0 + else value.to_i rescue nil + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/mutable.rb b/activerecord/lib/active_record/type/mutable.rb new file mode 100644 index 0000000000..066617ea59 --- /dev/null +++ b/activerecord/lib/active_record/type/mutable.rb @@ -0,0 +1,16 @@ +module ActiveRecord + module Type + module Mutable # :nodoc: + def type_cast_from_user(value) + type_cast_from_database(type_cast_for_database(value)) + end + + # +raw_old_value+ will be the `_before_type_cast` version of the + # value (likely a string). +new_value+ will be the current, type + # cast value. + def changed_in_place?(raw_old_value, new_value) + raw_old_value != type_cast_for_database(new_value) + end + end + end +end diff --git a/activerecord/lib/active_record/type/numeric.rb b/activerecord/lib/active_record/type/numeric.rb new file mode 100644 index 0000000000..fa43266504 --- /dev/null +++ b/activerecord/lib/active_record/type/numeric.rb @@ -0,0 +1,36 @@ +module ActiveRecord + module Type + module Numeric # :nodoc: + def number? + true + end + + def type_cast(value) + value = case value + when true then 1 + when false then 0 + when ::String then value.presence + else value + end + super(value) + end + + def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc: + super || number_to_non_number?(old_value, new_value_before_type_cast) + end + + private + + def number_to_non_number?(old_value, new_value_before_type_cast) + old_value != nil && non_numeric_string?(new_value_before_type_cast) + end + + def non_numeric_string?(value) + # 'wibble'.to_i will give zero, we want to make sure + # that we aren't marking int zero to string zero as + # changed. + value.to_s !~ /\A\d+\.?\d*\z/ + end + end + end +end diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb new file mode 100644 index 0000000000..42bbed7103 --- /dev/null +++ b/activerecord/lib/active_record/type/serialized.rb @@ -0,0 +1,51 @@ +module ActiveRecord + module Type + class Serialized < SimpleDelegator # :nodoc: + include Mutable + + attr_reader :subtype, :coder + + def initialize(subtype, coder) + @subtype = subtype + @coder = coder + super(subtype) + end + + def type_cast_from_database(value) + if is_default_value?(value) + value + else + coder.load(super) + end + end + + def type_cast_for_database(value) + return if value.nil? + unless is_default_value?(value) + super coder.dump(value) + end + end + + def accessor + ActiveRecord::Store::IndifferentHashAccessor + end + + def init_with(coder) + @subtype = coder['subtype'] + @coder = coder['coder'] + __setobj__(@subtype) + end + + def encode_with(coder) + coder['subtype'] = @subtype + coder['coder'] = @coder + end + + private + + def is_default_value?(value) + value == coder.load(nil) + end + end + end +end diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb new file mode 100644 index 0000000000..150defb106 --- /dev/null +++ b/activerecord/lib/active_record/type/string.rb @@ -0,0 +1,36 @@ +module ActiveRecord + module Type + class String < Value # :nodoc: + def type + :string + end + + def changed_in_place?(raw_old_value, new_value) + if new_value.is_a?(::String) + raw_old_value != new_value + end + end + + def type_cast_for_database(value) + case value + when ::Numeric, ActiveSupport::Duration then value.to_s + when ::String then ::String.new(value) + when true then "1" + when false then "0" + else super + end + end + + private + + def cast_value(value) + case value + when true then "1" + when false then "0" + # String.new is slightly faster than dup + else ::String.new(value.to_s) + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/text.rb b/activerecord/lib/active_record/type/text.rb new file mode 100644 index 0000000000..26f980f060 --- /dev/null +++ b/activerecord/lib/active_record/type/text.rb @@ -0,0 +1,11 @@ +require 'active_record/type/string' + +module ActiveRecord + module Type + class Text < String # :nodoc: + def type + :text + end + end + end +end diff --git a/activerecord/lib/active_record/type/time.rb b/activerecord/lib/active_record/type/time.rb new file mode 100644 index 0000000000..41f7d97f0c --- /dev/null +++ b/activerecord/lib/active_record/type/time.rb @@ -0,0 +1,26 @@ +module ActiveRecord + module Type + class Time < Value # :nodoc: + include TimeValue + + def type + :time + end + + private + + def cast_value(value) + return value unless value.is_a?(::String) + return if value.empty? + + dummy_time_value = "2000-01-01 #{value}" + + fast_string_to_time(dummy_time_value) || begin + time_hash = ::Date._parse(dummy_time_value) + return if time_hash[:hour].nil? + new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/time_value.rb b/activerecord/lib/active_record/type/time_value.rb new file mode 100644 index 0000000000..d611d72dd4 --- /dev/null +++ b/activerecord/lib/active_record/type/time_value.rb @@ -0,0 +1,38 @@ +module ActiveRecord + module Type + module TimeValue # :nodoc: + def klass + ::Time + end + + def type_cast_for_schema(value) + "'#{value.to_s(:db)}'" + end + + private + + def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil) + # Treat 0000-00-00 00:00:00 as nil. + return if year.nil? || (year == 0 && mon == 0 && mday == 0) + + if offset + time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil + return 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 + + # Doesn't handle time zones. + def fast_string_to_time(string) + if string =~ ConnectionAdapters::Column::Format::ISO_DATETIME + microsec = ($7.to_r * 1_000_000).to_i + new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/type_map.rb b/activerecord/lib/active_record/type/type_map.rb new file mode 100644 index 0000000000..88c5f9c497 --- /dev/null +++ b/activerecord/lib/active_record/type/type_map.rb @@ -0,0 +1,48 @@ +module ActiveRecord + module Type + class TypeMap # :nodoc: + def initialize + @mapping = {} + end + + def lookup(lookup_key, *args) + matching_pair = @mapping.reverse_each.detect do |key, _| + key === lookup_key + end + + if matching_pair + matching_pair.last.call(lookup_key, *args) + else + default_value + end + end + + def register_type(key, value = nil, &block) + raise ::ArgumentError unless value || block + + if block + @mapping[key] = block + else + @mapping[key] = proc { value } + end + end + + def alias_type(key, target_key) + register_type(key) do |sql_type, *args| + metadata = sql_type[/\(.*\)/, 0] + lookup("#{target_key}#{metadata}", *args) + end + end + + def clear + @mapping.clear + end + + private + + def default_value + @default_value ||= Value.new + end + end + end +end diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb new file mode 100644 index 0000000000..e0a783fb45 --- /dev/null +++ b/activerecord/lib/active_record/type/value.rb @@ -0,0 +1,94 @@ +module ActiveRecord + module Type + class Value # :nodoc: + attr_reader :precision, :scale, :limit + + # Valid options are +precision+, +scale+, and +limit+. They are only + # used when dumping schema. + def initialize(options = {}) + options.assert_valid_keys(:precision, :scale, :limit) + @precision = options[:precision] + @scale = options[:scale] + @limit = options[:limit] + end + + # The simplified type that this object represents. Returns a symbol such + # as +:string+ or +:integer+ + def type; end + + # Type casts a string from the database into the appropriate ruby type. + # Classes which do not need separate type casting behavior for database + # and user provided values should override +cast_value+ instead. + def type_cast_from_database(value) + type_cast(value) + end + + # Type casts a value from user input (e.g. from a setter). This value may + # be a string from the form builder, or an already type cast value + # provided manually to a setter. + # + # Classes which do not need separate type casting behavior for database + # and user provided values should override +type_cast+ or +cast_value+ + # instead. + def type_cast_from_user(value) + type_cast(value) + end + + # Cast a value from the ruby type to a type that the database knows how + # to understand. The returned value from this method should be a + # +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or + # +nil+ + def type_cast_for_database(value) + value + end + + # Type cast a value for schema dumping. This method is private, as we are + # hoping to remove it entirely. + def type_cast_for_schema(value) # :nodoc: + value.inspect + end + + # These predicates are not documented, as I need to look further into + # their use, and see if they can be removed entirely. + def number? # :nodoc: + false + end + + def binary? # :nodoc: + false + end + + def klass # :nodoc: + end + + # Determines whether a value has changed for dirty checking. +old_value+ + # and +new_value+ will always be type-cast. Types should not need to + # override this method. + def changed?(old_value, new_value, _new_value_before_type_cast) + old_value != new_value + end + + # Determines whether the mutable value has been modified since it was + # read. Returns +false+ by default. This method should not need to be + # overriden directly. Types which return a mutable value should include + # +Type::Mutable+, which will define this method. + def changed_in_place?(*) + false + end + + private + + def type_cast(value) + cast_value(value) unless value.nil? + end + + # Convenience method for types which do not need separate type casting + # behavior for user and database inputs. Called by + # `type_cast_from_database` and `type_cast_from_user` for all values + # except `nil`. + def cast_value(value) # :doc: + value + end + end + end +end diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 9999624fcf..b4b33804de 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -54,7 +54,7 @@ module ActiveRecord # Attempts to save the record just like Base#save but will raise a +RecordInvalid+ # exception instead of returning +false+ if the record is not valid. def save!(options={}) - perform_validations(options) ? super : raise(RecordInvalid.new(self)) + perform_validations(options) ? super : raise_record_invalid end # Runs all the validations within the specified context. Returns +true+ if @@ -75,8 +75,24 @@ module ActiveRecord alias_method :validate, :valid? + # Runs all the validations within the specified context. Returns +true+ if + # no errors are found, raises +RecordInvalid+ otherwise. + # + # If the argument is +false+ (default is +nil+), the context is set to <tt>:create</tt> if + # <tt>new_record?</tt> is +true+, and to <tt>:update</tt> if it is not. + # + # Validations with no <tt>:on</tt> option will run no matter the context. Validations with + # some <tt>:on</tt> option will only run in the specified context. + def validate!(context = nil) + valid?(context) || raise_record_invalid + end + protected + def raise_record_invalid + raise(RecordInvalid.new(self)) + end + def perform_validations(options={}) # :nodoc: options[:validate] == false || valid?(options[:context]) end diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb index 9a19483da3..e586744818 100644 --- a/activerecord/lib/active_record/validations/presence.rb +++ b/activerecord/lib/active_record/validations/presence.rb @@ -4,7 +4,7 @@ module ActiveRecord def validate(record) super attributes.each do |attribute| - next unless record.class.reflect_on_association(attribute) + next unless record.class._reflect_on_association(attribute) associated_records = Array.wrap(record.send(attribute)) # Superclass validates presence. Ensure present records aren't about to be destroyed. diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index ee080451a9..2a34969a8c 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -14,7 +14,6 @@ module ActiveRecord finder_class = find_finder_class_for(record) table = finder_class.arel_table value = map_enum_attribute(finder_class, attribute, value) - value = deserialize_attribute(record, attribute, value) relation = build_relation(finder_class, table, attribute, value) relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.id)) if record.persisted? @@ -47,9 +46,9 @@ module ActiveRecord end def build_relation(klass, table, attribute, value) #:nodoc: - if reflection = klass.reflect_on_association(attribute) + if reflection = klass._reflect_on_association(attribute) attribute = reflection.foreign_key - value = value.attributes[reflection.primary_key_column.name] unless value.nil? + value = value.attributes[reflection.klass.primary_key] unless value.nil? end attribute_name = attribute.to_s @@ -62,9 +61,11 @@ module ActiveRecord column = klass.columns_hash[attribute_name] value = klass.connection.type_cast(value, column) - value = value.to_s[0, column.limit] if value && column.limit && column.text? + if value.is_a?(String) && column.limit + value = value.to_s[0, column.limit] + end - if !options[:case_sensitive] && value && column.text? + if !options[:case_sensitive] && value.is_a?(String) # will use SQL LOWER function before comparison, unless it detects a case insensitive collation klass.connection.case_insensitive_comparison(table, attribute, column, value) else @@ -74,7 +75,7 @@ module ActiveRecord def scope_relation(record, table, relation) Array(options[:scope]).each do |scope_item| - if reflection = record.class.reflect_on_association(scope_item) + if reflection = record.class._reflect_on_association(scope_item) scope_value = record.send(reflection.foreign_key) scope_item = reflection.foreign_key else @@ -86,12 +87,6 @@ module ActiveRecord relation end - def deserialize_attribute(record, attribute, value) - coder = record.class.serialized_attributes[attribute.to_s] - value = coder.dump value if value && coder - value - end - def map_enum_attribute(klass, attribute, value) mapping = klass.defined_enums[attribute.to_s] value = mapping[value] if value && mapping diff --git a/activerecord/test/active_record/connection_adapters/fake_adapter.rb b/activerecord/test/active_record/connection_adapters/fake_adapter.rb index 59324c4857..64cde143a1 100644 --- a/activerecord/test/active_record/connection_adapters/fake_adapter.rb +++ b/activerecord/test/active_record/connection_adapters/fake_adapter.rb @@ -29,6 +29,7 @@ module ActiveRecord @columns[table_name] << ActiveRecord::ConnectionAdapters::Column.new( name.to_s, options[:default], + lookup_cast_type(sql_type.to_s), sql_type.to_s, options[:null]) end diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 90953ce6cd..6f84bae432 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -46,9 +46,7 @@ module ActiveRecord @connection.add_index :accounts, :firm_id, :name => idx_name indexes = @connection.indexes("accounts") assert_equal "accounts", indexes.first.table - # OpenBase does not have the concept of a named index - # Indexes are merely properties of columns. - assert_equal idx_name, indexes.first.name unless current_adapter?(:OpenBaseAdapter) + assert_equal idx_name, indexes.first.name assert !indexes.first.unique assert_equal ["firm_id"], indexes.first.columns else @@ -127,14 +125,12 @@ module ActiveRecord assert_equal 1, Movie.create(:name => 'fight club').id end - if ActiveRecord::Base.connection.adapter_name != "FrontBase" - def test_reset_table_with_non_integer_pk - Subscriber.delete_all - Subscriber.connection.reset_pk_sequence! 'subscribers' - sub = Subscriber.new(:name => 'robert drake') - sub.id = 'bob drake' - assert_nothing_raised { sub.save! } - end + def test_reset_table_with_non_integer_pk + Subscriber.delete_all + Subscriber.connection.reset_pk_sequence! 'subscribers' + sub = Subscriber.new(:name => 'robert drake') + sub.id = 'bob drake' + assert_nothing_raised { sub.save! } end end @@ -144,7 +140,7 @@ module ActiveRecord @connection.execute "INSERT INTO subscribers(nick) VALUES('me')" end end - + unless current_adapter?(:SQLite3Adapter) def test_foreign_key_violations_are_translated_to_specific_exception assert_raises(ActiveRecord::InvalidForeignKey) do @@ -157,7 +153,7 @@ module ActiveRecord end end end - + def test_foreign_key_violations_are_translated_to_specific_exception_with_validate_false klass_has_fk = Class.new(ActiveRecord::Base) do self.table_name = 'fk_test_has_fk' @@ -196,7 +192,7 @@ module ActiveRecord def test_select_methods_passing_a_association_relation author = Author.create!(name: 'john') Post.create!(author: author, title: 'foo', body: 'bar') - query = author.posts.select(:title) + query = author.posts.where(title: 'foo').select(:title) assert_equal({"title" => "foo"}, @connection.select_one(query.arel, nil, query.bind_values)) assert_equal({"title" => "foo"}, @connection.select_one(query)) assert @connection.select_all(query).is_a?(ActiveRecord::Result) diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb index 412efa22ff..b0759dffde 100644 --- a/activerecord/test/cases/adapters/mysql/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql/connection_test.rb @@ -134,12 +134,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase assert_equal [["STRICT_ALL_TABLES"]], result.rows end - def test_mysql_strict_mode_disabled_dont_override_global_sql_mode + def test_mysql_strict_mode_disabled run_without_connection do |orig_connection| ActiveRecord::Base.establish_connection(orig_connection.merge({:strict => false})) - global_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.sql_mode" - session_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode" - assert_equal global_sql_mode.rows, session_sql_mode.rows + result = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode" + assert_equal [['']], result.rows end end @@ -151,6 +150,14 @@ class MysqlConnectionTest < ActiveRecord::TestCase end end + def test_mysql_sql_mode_variable_overides_strict_mode + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { 'sql_mode' => 'ansi' })) + result = ActiveRecord::Base.connection.exec_query 'SELECT @@SESSION.sql_mode' + assert_not_equal [['STRICT_ALL_TABLES']], result.rows + end + end + def test_mysql_set_session_variable_to_default run_without_connection do |orig_connection| ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => :default}})) diff --git a/activerecord/test/cases/adapters/mysql/consistency_test.rb b/activerecord/test/cases/adapters/mysql/consistency_test.rb new file mode 100644 index 0000000000..083d533bb2 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql/consistency_test.rb @@ -0,0 +1,48 @@ +require "cases/helper" + +class MysqlConsistencyTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class Consistency < ActiveRecord::Base + self.table_name = "mysql_consistency" + end + + setup do + @old_emulate_booleans = ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans + ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false + + @connection = ActiveRecord::Base.connection + @connection.create_table("mysql_consistency") do |t| + t.boolean "a_bool" + t.string "a_string" + end + Consistency.reset_column_information + Consistency.create! + end + + teardown do + ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = @old_emulate_booleans + @connection.drop_table "mysql_consistency" + end + + test "boolean columns with random value type cast to 0 when emulate_booleans is false" do + with_new = Consistency.new + with_last = Consistency.last + with_new.a_bool = 'wibble' + with_last.a_bool = 'wibble' + + assert_equal 0, with_new.a_bool + assert_equal 0, with_last.a_bool + end + + test "string columns call #to_s" do + with_new = Consistency.new + with_last = Consistency.last + thing = Object.new + with_new.a_string = thing + with_last.a_string = thing + + assert_equal thing.to_s, with_new.a_string + assert_equal thing.to_s, with_last.a_string + end +end diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb index 1699380eb3..28106d3772 100644 --- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb @@ -105,7 +105,7 @@ module ActiveRecord result = @conn.exec_query('SELECT status FROM ex') - assert_equal 2, result.column_types['status'].type_cast(result.last['status']) + assert_equal 2, result.column_types['status'].type_cast_from_database(result.last['status']) end end diff --git a/activerecord/test/cases/adapters/mysql/quoting_test.rb b/activerecord/test/cases/adapters/mysql/quoting_test.rb index 3d1330efb8..d8a954efa8 100644 --- a/activerecord/test/cases/adapters/mysql/quoting_test.rb +++ b/activerecord/test/cases/adapters/mysql/quoting_test.rb @@ -9,13 +9,13 @@ module ActiveRecord end def test_type_cast_true - c = Column.new(nil, 1, 'boolean') + c = Column.new(nil, 1, Type::Boolean.new) assert_equal 1, @conn.type_cast(true, nil) assert_equal 1, @conn.type_cast(true, c) end def test_type_cast_false - c = Column.new(nil, 1, 'boolean') + c = Column.new(nil, 1, Type::Boolean.new) assert_equal 0, @conn.type_cast(false, nil) assert_equal 0, @conn.type_cast(false, c) end diff --git a/activerecord/test/cases/adapters/mysql/schema_test.rb b/activerecord/test/cases/adapters/mysql/schema_test.rb index 807a7a155e..87c5277e64 100644 --- a/activerecord/test/cases/adapters/mysql/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/schema_test.rb @@ -17,6 +17,44 @@ module ActiveRecord self.table_name = "#{db}.#{table}" def self.name; 'Post'; end end + + @connection.create_table "mysql_doubles" + end + + teardown do + @connection.execute "drop table if exists mysql_doubles" + end + + class MysqlDouble < ActiveRecord::Base + self.table_name = "mysql_doubles" + end + + def test_float_limits + @connection.add_column :mysql_doubles, :float_no_limit, :float + @connection.add_column :mysql_doubles, :float_short, :float, limit: 5 + @connection.add_column :mysql_doubles, :float_long, :float, limit: 53 + + @connection.add_column :mysql_doubles, :float_23, :float, limit: 23 + @connection.add_column :mysql_doubles, :float_24, :float, limit: 24 + @connection.add_column :mysql_doubles, :float_25, :float, limit: 25 + MysqlDouble.reset_column_information + + column_no_limit = MysqlDouble.columns.find { |c| c.name == 'float_no_limit' } + column_short = MysqlDouble.columns.find { |c| c.name == 'float_short' } + column_long = MysqlDouble.columns.find { |c| c.name == 'float_long' } + + column_23 = MysqlDouble.columns.find { |c| c.name == 'float_23' } + column_24 = MysqlDouble.columns.find { |c| c.name == 'float_24' } + column_25 = MysqlDouble.columns.find { |c| c.name == 'float_25' } + + # Mysql floats are precision 0..24, Mysql doubles are precision 25..53 + assert_equal 24, column_no_limit.limit + assert_equal 24, column_short.limit + assert_equal 53, column_long.limit + + assert_equal 24, column_23.limit + assert_equal 24, column_24.limit + assert_equal 53, column_25.limit end def test_schema diff --git a/activerecord/test/cases/adapters/mysql2/boolean_test.rb b/activerecord/test/cases/adapters/mysql2/boolean_test.rb index 267aa232d9..f3c711a64b 100644 --- a/activerecord/test/cases/adapters/mysql2/boolean_test.rb +++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb @@ -47,7 +47,7 @@ class Mysql2BooleanTest < ActiveRecord::TestCase assert_equal "1", attributes["published"] assert_equal 1, @connection.type_cast(true, boolean_column) - assert_equal 1, @connection.type_cast(true, string_column) + assert_equal "1", @connection.type_cast(true, string_column) end test "test type casting without emulated booleans" do @@ -60,7 +60,7 @@ class Mysql2BooleanTest < ActiveRecord::TestCase assert_equal "1", attributes["published"] assert_equal 1, @connection.type_cast(true, boolean_column) - assert_equal 1, @connection.type_cast(true, string_column) + assert_equal "1", @connection.type_cast(true, string_column) end test "with booleans stored as 1 and 0" do diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 182d9409c7..3b35e69e0d 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -60,12 +60,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase assert_equal [["STRICT_ALL_TABLES"]], result.rows end - def test_mysql_strict_mode_disabled_dont_override_global_sql_mode + def test_mysql_strict_mode_disabled run_without_connection do |orig_connection| ActiveRecord::Base.establish_connection(orig_connection.merge({:strict => false})) - global_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.sql_mode" - session_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode" - assert_equal global_sql_mode.rows, session_sql_mode.rows + result = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode" + assert_equal [['']], result.rows end end @@ -77,6 +76,14 @@ class MysqlConnectionTest < ActiveRecord::TestCase end end + def test_mysql_sql_mode_variable_overides_strict_mode + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { 'sql_mode' => 'ansi' })) + result = ActiveRecord::Base.connection.exec_query 'SELECT @@SESSION.sql_mode' + assert_not_equal [['STRICT_ALL_TABLES']], result.rows + end + end + def test_mysql_set_session_variable_to_default run_without_connection do |orig_connection| ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => :default}})) diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb index ec73ec35aa..9c49599d34 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb @@ -6,12 +6,12 @@ module ActiveRecord class SchemaMigrationsTest < ActiveRecord::TestCase def test_renaming_index_on_foreign_key connection.add_index "engines", "car_id" - connection.execute "ALTER TABLE engines ADD CONSTRAINT fk_engines_cars FOREIGN KEY (car_id) REFERENCES cars(id)" + connection.add_foreign_key :engines, :cars, name: "fk_engines_cars" connection.rename_index("engines", "index_engines_on_car_id", "idx_renamed") assert_equal ["idx_renamed"], connection.indexes("engines").map(&:name) ensure - connection.execute "ALTER TABLE engines DROP FOREIGN KEY fk_engines_cars" + connection.remove_foreign_key :engines, name: "fk_engines_cars" end def test_initializes_schema_migrations_for_encoding_utf8mb4 diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index 18dd4a6de8..8df1b7d18c 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -1,22 +1,33 @@ # encoding: utf-8 require "cases/helper" -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' class PostgresqlArrayTest < ActiveRecord::TestCase + include InTimeZone + OID = ActiveRecord::ConnectionAdapters::PostgreSQL::OID + class PgArray < ActiveRecord::Base self.table_name = 'pg_arrays' end def setup @connection = ActiveRecord::Base.connection + + unless @connection.extension_enabled?('hstore') + @connection.enable_extension 'hstore' + @connection.commit_db_transaction + end + + @connection.reconnect! + @connection.transaction do @connection.create_table('pg_arrays') do |t| t.string 'tags', array: true t.integer 'ratings', array: true + t.datetime :datetimes, array: true + t.hstore :hstores, array: true end end - @column = PgArray.columns.find { |c| c.name == 'tags' } + @column = PgArray.columns_hash['tags'] end teardown do @@ -27,7 +38,6 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert_equal :string, @column.type assert_equal "character varying", @column.sql_type assert @column.array - assert_not @column.text? assert_not @column.number? assert_not @column.binary? @@ -40,9 +50,8 @@ class PostgresqlArrayTest < ActiveRecord::TestCase def test_default @connection.add_column 'pg_arrays', 'score', :integer, array: true, default: [4, 4, 2] PgArray.reset_column_information - column = PgArray.columns_hash["score"] - assert_equal([4, 4, 2], column.default) + assert_equal([4, 4, 2], PgArray.column_defaults['score']) assert_equal([4, 4, 2], PgArray.new.score) ensure PgArray.reset_column_information @@ -51,9 +60,8 @@ class PostgresqlArrayTest < ActiveRecord::TestCase def test_default_strings @connection.add_column 'pg_arrays', 'names', :string, array: true, default: ["foo", "bar"] PgArray.reset_column_information - column = PgArray.columns_hash["names"] - assert_equal(["foo", "bar"], column.default) + assert_equal(["foo", "bar"], PgArray.column_defaults['names']) assert_equal(["foo", "bar"], PgArray.new.names) ensure PgArray.reset_column_information @@ -61,13 +69,13 @@ class PostgresqlArrayTest < ActiveRecord::TestCase def test_change_column_with_array @connection.add_column :pg_arrays, :snippets, :string, array: true, default: [] - @connection.change_column :pg_arrays, :snippets, :text, array: true, default: "{}" + @connection.change_column :pg_arrays, :snippets, :text, array: true, default: [] PgArray.reset_column_information - column = PgArray.columns.find { |c| c.name == 'snippets' } + column = PgArray.columns_hash['snippets'] assert_equal :text, column.type - assert_equal [], column.default + assert_equal [], PgArray.column_defaults['snippets'] assert column.array end @@ -80,38 +88,56 @@ class PostgresqlArrayTest < ActiveRecord::TestCase end end - def test_type_cast_array - data = '{1,2,3}' - oid_type = @column.instance_variable_get('@oid_type').subtype - # we are getting the instance variable in this test, but in the - # normal use of string_to_array, it's called from the OID::Array - # class and will have the OID instance that will provide the type - # casting - array = @column.class.string_to_array data, oid_type - assert_equal(['1', '2', '3'], array) - assert_equal(['1', '2', '3'], @column.type_cast(data)) + def test_change_column_default_with_array + @connection.change_column_default :pg_arrays, :tags, [] - assert_equal([], @column.type_cast('{}')) - assert_equal([nil], @column.type_cast('{NULL}')) + PgArray.reset_column_information + assert_equal [], PgArray.column_defaults['tags'] + end + + def test_type_cast_array + assert_equal(['1', '2', '3'], @column.type_cast_from_database('{1,2,3}')) + assert_equal([], @column.type_cast_from_database('{}')) + assert_equal([nil], @column.type_cast_from_database('{NULL}')) end def test_type_cast_integers x = PgArray.new(ratings: ['1', '2']) - assert x.save! - assert_equal(['1', '2'], x.ratings) + + assert_equal([1, 2], x.ratings) + + x.save! + x.reload + + assert_equal([1, 2], x.ratings) end - def test_rewrite + def test_select_with_strings @connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')" x = PgArray.first - x.tags = ['1','2','3','4'] - assert x.save! + assert_equal(['1','2','3'], x.tags) end - def test_select + def test_rewrite_with_strings @connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')" x = PgArray.first - assert_equal(['1','2','3'], x.tags) + x.tags = ['1','2','3','4'] + x.save! + assert_equal ['1','2','3','4'], x.reload.tags + end + + def test_select_with_integers + @connection.execute "insert into pg_arrays (ratings) VALUES ('{1,2,3}')" + x = PgArray.first + assert_equal([1, 2, 3], x.ratings) + end + + def test_rewrite_with_integers + @connection.execute "insert into pg_arrays (ratings) VALUES ('{1,2,3}')" + x = PgArray.first + x.ratings = [2, '3', 4] + x.save! + assert_equal [2, 3, 4], x.reload.ratings end def test_multi_dimensional_with_strings @@ -165,14 +191,71 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]", record.attribute_for_inspect(:ratings)) end - def test_update_all - pg_array = PgArray.create! tags: ["one", "two", "three"] + def test_escaping + unknown = 'foo\\",bar,baz,\\' + tags = ["hello_#{unknown}"] + ar = PgArray.create!(tags: tags) + ar.reload + assert_equal tags, ar.tags + end - PgArray.update_all tags: ["four", "five"] - assert_equal ["four", "five"], pg_array.reload.tags + def test_string_quoting_rules_match_pg_behavior + tags = ["", "one{", "two}", %(three"), "four\\", "five ", "six\t", "seven\n", "eight,", "nine", "ten\r", "NULL"] + x = PgArray.create!(tags: tags) + x.reload - PgArray.update_all tags: [] - assert_equal [], pg_array.reload.tags + assert_equal x.tags_before_type_cast, PgArray.columns_hash['tags'].type_cast_for_database(tags) + end + + def test_quoting_non_standard_delimiters + strings = ["hello,", "world;"] + comma_delim = OID::Array.new(ActiveRecord::Type::String.new, ',') + semicolon_delim = OID::Array.new(ActiveRecord::Type::String.new, ';') + + assert_equal %({"hello,",world;}), comma_delim.type_cast_for_database(strings) + assert_equal %({hello,;"world;"}), semicolon_delim.type_cast_for_database(strings) + end + + def test_mutate_array + x = PgArray.create!(tags: %w(one two)) + + x.tags << "three" + x.save! + x.reload + + assert_equal %w(one two three), x.tags + assert_not x.changed? + end + + def test_mutate_value_in_array + x = PgArray.create!(hstores: [{ a: 'a' }, { b: 'b' }]) + + x.hstores.first['a'] = 'c' + x.save! + x.reload + + assert_equal [{ 'a' => 'c' }, { 'b' => 'b' }], x.hstores + assert_not x.changed? + end + + def test_datetime_with_timezone_awareness + tz = "Pacific Time (US & Canada)" + + in_time_zone tz do + PgArray.reset_column_information + time_string = Time.current.to_s + time = Time.zone.parse(time_string) + + record = PgArray.new(datetimes: [time_string]) + assert_equal [time], record.datetimes + assert_equal ActiveSupport::TimeZone[tz], record.datetimes.first.time_zone + + record.save! + record.reload + + assert_equal [time], record.datetimes + assert_equal ActiveSupport::TimeZone[tz], record.datetimes.first.time_zone + end end private diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb new file mode 100644 index 0000000000..72222c01fd --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +require "cases/helper" +require 'support/connection_helper' +require 'support/schema_dumping_helper' + +class PostgresqlBitStringTest < ActiveRecord::TestCase + include ConnectionHelper + include SchemaDumpingHelper + + class PostgresqlBitString < ActiveRecord::Base; end + + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table('postgresql_bit_strings', :force => true) do |t| + t.bit :a_bit, default: "00000011", limit: 8 + t.bit_varying :a_bit_varying, default: "0011", limit: 4 + end + end + + def teardown + return unless @connection + @connection.execute 'DROP TABLE IF EXISTS postgresql_bit_strings' + end + + def test_bit_string_column + column = PostgresqlBitString.columns_hash["a_bit"] + assert_equal :bit, column.type + assert_equal "bit(8)", column.sql_type + assert_not column.number? + assert_not column.binary? + assert_not column.array + end + + def test_bit_string_varying_column + column = PostgresqlBitString.columns_hash["a_bit_varying"] + assert_equal :bit_varying, column.type + assert_equal "bit varying(4)", column.sql_type + assert_not column.number? + assert_not column.binary? + assert_not column.array + end + + def test_default + assert_equal "00000011", PostgresqlBitString.column_defaults['a_bit'] + assert_equal "00000011", PostgresqlBitString.new.a_bit + + assert_equal "0011", PostgresqlBitString.column_defaults['a_bit_varying'] + assert_equal "0011", PostgresqlBitString.new.a_bit_varying + end + + def test_schema_dumping + output = dump_table_schema("postgresql_bit_strings") + assert_match %r{t\.bit\s+"a_bit",\s+limit: 8,\s+default: "00000011"$}, output + assert_match %r{t\.bit_varying\s+"a_bit_varying",\s+limit: 4,\s+default: "0011"$}, output + end + + def test_assigning_invalid_hex_string_raises_exception + assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit: "FF" } + assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit_varying: "FF" } + end + + def test_roundtrip + PostgresqlBitString.create! a_bit: "00001010", a_bit_varying: "0101" + record = PostgresqlBitString.first + assert_equal "00001010", record.a_bit + assert_equal "0101", record.a_bit_varying + + record.a_bit = "11111111" + record.a_bit_varying = "0xF" + record.save! + + assert record.reload + assert_equal "11111111", record.a_bit + assert_equal "1111", record.a_bit_varying + end +end diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb index e3478856c8..7872f91943 100644 --- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb @@ -1,8 +1,5 @@ # encoding: utf-8 - require "cases/helper" -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' class PostgresqlByteaTest < ActiveRecord::TestCase class ByteaDataType < ActiveRecord::Base @@ -19,7 +16,7 @@ class PostgresqlByteaTest < ActiveRecord::TestCase end end end - @column = ByteaDataType.columns.find { |c| c.name == 'payload' } + @column = ByteaDataType.columns_hash['payload'] assert(@column.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLColumn)) end @@ -36,16 +33,16 @@ class PostgresqlByteaTest < ActiveRecord::TestCase data = "\u001F\x8B" assert_equal('UTF-8', data.encoding.name) - assert_equal('ASCII-8BIT', @column.type_cast(data).encoding.name) + assert_equal('ASCII-8BIT', @column.type_cast_from_database(data).encoding.name) end def test_type_cast_binary_value data = "\u001F\x8B".force_encoding("BINARY") - assert_equal(data, @column.type_cast(data)) + assert_equal(data, @column.type_cast_from_database(data)) end def test_type_case_nil - assert_equal(nil, @column.type_cast(nil)) + assert_equal(nil, @column.type_cast_from_database(nil)) end def test_read_value diff --git a/activerecord/test/cases/adapters/postgresql/citext_test.rb b/activerecord/test/cases/adapters/postgresql/citext_test.rb index 948bf49a54..2acb64f81c 100644 --- a/activerecord/test/cases/adapters/postgresql/citext_test.rb +++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb @@ -1,8 +1,5 @@ # encoding: utf-8 - require 'cases/helper' -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' if ActiveRecord::Base.connection.supports_extensions? class PostgresqlCitextTest < ActiveRecord::TestCase @@ -38,7 +35,6 @@ if ActiveRecord::Base.connection.supports_extensions? column = Citext.columns_hash['cival'] assert_equal :citext, column.type assert_equal 'citext', column.sql_type - assert_not column.text? assert_not column.number? assert_not column.binary? assert_not column.array @@ -50,7 +46,7 @@ if ActiveRecord::Base.connection.supports_extensions? t.citext 'username' end Citext.reset_column_information - column = Citext.columns.find { |c| c.name == 'username' } + column = Citext.columns_hash['username'] assert_equal :citext, column.type raise ActiveRecord::Rollback # reset the schema change diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb index 224b1b770b..cfab5ca902 100644 --- a/activerecord/test/cases/adapters/postgresql/composite_test.rb +++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb @@ -1,19 +1,17 @@ # -*- coding: utf-8 -*- require "cases/helper" -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' +require 'support/connection_helper' + +module PostgresqlCompositeBehavior + include ConnectionHelper -class PostgresqlCompositeTest < ActiveRecord::TestCase class PostgresqlComposite < ActiveRecord::Base self.table_name = "postgresql_composites" end - teardown do - @connection.execute 'DROP TABLE IF EXISTS postgresql_composites' - @connection.execute 'DROP TYPE IF EXISTS full_address' - end - def setup + super + @connection = ActiveRecord::Base.connection @connection.transaction do @connection.execute <<-SQL @@ -29,18 +27,37 @@ class PostgresqlCompositeTest < ActiveRecord::TestCase end end + def teardown + super + + @connection.execute 'DROP TABLE IF EXISTS postgresql_composites' + @connection.execute 'DROP TYPE IF EXISTS full_address' + reset_connection + PostgresqlComposite.reset_column_information + end +end + +# Composites are mapped to `OID::Identity` by default. The user is informed by a warning like: +# "unknown OID 5653508: failed to recognize type of 'address'. It will be treated as String." +# To take full advantage of composite types, we suggest you register your own +OID::Type+. +# See PostgresqlCompositeWithCustomOIDTest +class PostgresqlCompositeTest < ActiveRecord::TestCase + include PostgresqlCompositeBehavior + def test_column + ensure_warning_is_issued + column = PostgresqlComposite.columns_hash["address"] - # TODO: Composite columns should have a type assert_nil column.type assert_equal "full_address", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end def test_composite_mapping + ensure_warning_is_issued + @connection.execute "INSERT INTO postgresql_composites VALUES (1, ROW('Paris', 'Champs-Élysées'));" composite = PostgresqlComposite.first assert_equal "(Paris,Champs-Élysées)", composite.address @@ -50,4 +67,65 @@ class PostgresqlCompositeTest < ActiveRecord::TestCase assert_equal '(Paris,"Rue Basse")', composite.reload.address end + + private + def ensure_warning_is_issued + warning = capture(:stderr) do + PostgresqlComposite.columns_hash + end + assert_match(/unknown OID \d+: failed to recognize type of 'address'\. It will be treated as String\./, warning) + end +end + +class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase + include PostgresqlCompositeBehavior + + class FullAddressType < ActiveRecord::Type::Value + def type; :full_address end + + def type_cast_from_database(value) + if value =~ /\("?([^",]*)"?,"?([^",]*)"?\)/ + FullAddress.new($1, $2) + end + end + + def type_cast_from_user(value) + value + end + + def type_cast_for_database(value) + return if value.nil? + "(#{value.city},#{value.street})" + end + end + + FullAddress = Struct.new(:city, :street) + + def setup + super + + @connection.type_map.register_type "full_address", FullAddressType.new + end + + def test_column + column = PostgresqlComposite.columns_hash["address"] + assert_equal :full_address, column.type + assert_equal "full_address", column.sql_type + assert_not column.number? + assert_not column.binary? + assert_not column.array + end + + def test_composite_mapping + @connection.execute "INSERT INTO postgresql_composites VALUES (1, ROW('Paris', 'Champs-Élysées'));" + composite = PostgresqlComposite.first + assert_equal "Paris", composite.address.city + assert_equal "Champs-Élysées", composite.address.street + + composite.address = FullAddress.new("Paris", "Rue Basse") + composite.save! + + assert_equal 'Paris', composite.reload.address.city + assert_equal 'Rue Basse', composite.reload.address.street + end end diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index 5f84c893c0..d26cda46fa 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -121,7 +121,7 @@ module ActiveRecord name = @subscriber.payloads.last[:statement_name] assert name res = @connection.exec_query("EXPLAIN (FORMAT JSON) EXECUTE #{name}(#{bindval})") - plan = res.column_types['QUERY PLAN'].type_cast res.rows.first.first + plan = res.column_types['QUERY PLAN'].type_cast_from_database res.rows.first.first assert_operator plan.length, :>, 0 end diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index e7dda1a1af..a0a34e4b87 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -1,13 +1,6 @@ require "cases/helper" +require 'support/ddl_helper' -class PostgresqlArray < ActiveRecord::Base -end - -class PostgresqlTsvector < ActiveRecord::Base -end - -class PostgresqlMoney < ActiveRecord::Base -end class PostgresqlNumber < ActiveRecord::Base end @@ -15,18 +8,9 @@ end class PostgresqlTime < ActiveRecord::Base end -class PostgresqlNetworkAddress < ActiveRecord::Base -end - -class PostgresqlBitString < ActiveRecord::Base -end - class PostgresqlOid < ActiveRecord::Base end -class PostgresqlTimestampWithZone < ActiveRecord::Base -end - class PostgresqlLtree < ActiveRecord::Base end @@ -35,62 +19,23 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection - @connection.execute("set lc_monetary = 'C'") - - @connection.execute("INSERT INTO postgresql_arrays (id, commission_by_quarter, nicknames) VALUES (1, '{35000,21000,18000,17000}', '{foo,bar,baz}')") - @first_array = PostgresqlArray.find(1) - - @connection.execute("INSERT INTO postgresql_tsvectors (id, text_vector) VALUES (1, ' ''text'' ''vector'' ')") - - @first_tsvector = PostgresqlTsvector.find(1) - - @connection.execute("INSERT INTO postgresql_moneys (id, wealth) VALUES (1, '567.89'::money)") - @connection.execute("INSERT INTO postgresql_moneys (id, wealth) VALUES (2, '-567.89'::money)") - @first_money = PostgresqlMoney.find(1) - @second_money = PostgresqlMoney.find(2) @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (1, 123.456, 123456.789)") + @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (2, '-Infinity', 'Infinity')") + @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (3, 123.456, 'NaN')") @first_number = PostgresqlNumber.find(1) + @second_number = PostgresqlNumber.find(2) + @third_number = PostgresqlNumber.find(3) @connection.execute("INSERT INTO postgresql_times (id, time_interval, scaled_time_interval) VALUES (1, '1 year 2 days ago', '3 weeks ago')") @first_time = PostgresqlTime.find(1) - @connection.execute("INSERT INTO postgresql_network_addresses (id, cidr_address, inet_address, mac_address) VALUES(1, '192.168.0/24', '172.16.1.254/32', '01:23:45:67:89:0a')") - @first_network_address = PostgresqlNetworkAddress.find(1) - - @connection.execute("INSERT INTO postgresql_bit_strings (id, bit_string, bit_string_varying) VALUES (1, B'00010101', X'15')") - @first_bit_string = PostgresqlBitString.find(1) - @connection.execute("INSERT INTO postgresql_oids (id, obj_id) VALUES (1, 1234)") @first_oid = PostgresqlOid.find(1) - - @connection.execute("INSERT INTO postgresql_timestamp_with_zones (id, time) VALUES (1, '2010-01-01 10:00:00-1')") end teardown do - [PostgresqlArray, PostgresqlTsvector, PostgresqlMoney, PostgresqlNumber, PostgresqlTime, PostgresqlNetworkAddress, - PostgresqlBitString, PostgresqlOid, PostgresqlTimestampWithZone].each(&:delete_all) - end - - def test_array_escaping - unknown = %(foo\\",bar,baz,\\) - nicknames = ["hello_#{unknown}"] - ar = PostgresqlArray.create!(nicknames: nicknames, id: 100) - ar.reload - assert_equal nicknames, ar.nicknames - end - - def test_data_type_of_array_types - assert_equal :integer, @first_array.column_for_attribute(:commission_by_quarter).type - assert_equal :text, @first_array.column_for_attribute(:nicknames).type - end - - def test_data_type_of_tsvector_types - assert_equal :tsvector, @first_tsvector.column_for_attribute(:text_vector).type - end - - def test_data_type_of_money_types - assert_equal :decimal, @first_money.column_for_attribute(:wealth).type + [PostgresqlNumber, PostgresqlTime, PostgresqlOid].each(&:delete_all) end def test_data_type_of_number_types @@ -103,57 +48,16 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal :string, @first_time.column_for_attribute(:scaled_time_interval).type end - def test_data_type_of_network_address_types - assert_equal :cidr, @first_network_address.column_for_attribute(:cidr_address).type - assert_equal :inet, @first_network_address.column_for_attribute(:inet_address).type - assert_equal :macaddr, @first_network_address.column_for_attribute(:mac_address).type - end - - def test_data_type_of_bit_string_types - assert_equal :string, @first_bit_string.column_for_attribute(:bit_string).type - assert_equal :string, @first_bit_string.column_for_attribute(:bit_string_varying).type - end - def test_data_type_of_oid_types assert_equal :integer, @first_oid.column_for_attribute(:obj_id).type end - def test_array_values - assert_equal [35000,21000,18000,17000], @first_array.commission_by_quarter - assert_equal ['foo','bar','baz'], @first_array.nicknames - end - - def test_tsvector_values - assert_equal "'text' 'vector'", @first_tsvector.text_vector - end - - def test_money_values - assert_equal 567.89, @first_money.wealth - assert_equal(-567.89, @second_money.wealth) - end - - def test_money_type_cast - column = PostgresqlMoney.columns.find { |c| c.name == 'wealth' } - assert_equal(12345678.12, column.type_cast("$12,345,678.12")) - assert_equal(12345678.12, column.type_cast("$12.345.678,12")) - assert_equal(-1.15, column.type_cast("-$1.15")) - assert_equal(-2.25, column.type_cast("($2.25)")) - end - - def test_update_tsvector - new_text_vector = "'new' 'text' 'vector'" - @first_tsvector.text_vector = new_text_vector - assert @first_tsvector.save - assert @first_tsvector.reload - @first_tsvector.text_vector = new_text_vector - assert @first_tsvector.save - assert @first_tsvector.reload - assert_equal new_text_vector, @first_tsvector.text_vector - end - def test_number_values assert_equal 123.456, @first_number.single assert_equal 123456.789, @first_number.double + assert_equal(-::Float::INFINITY, @second_number.single) + assert_equal ::Float::INFINITY, @second_number.double + assert_same ::Float::NAN, @third_number.double end def test_time_values @@ -161,56 +65,10 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal '-21 days', @first_time.scaled_time_interval end - def test_network_address_values_ipaddr - cidr_address = IPAddr.new '192.168.0.0/24' - inet_address = IPAddr.new '172.16.1.254' - - assert_equal cidr_address, @first_network_address.cidr_address - assert_equal inet_address, @first_network_address.inet_address - assert_equal '01:23:45:67:89:0a', @first_network_address.mac_address - end - - def test_bit_string_values - assert_equal '00010101', @first_bit_string.bit_string - assert_equal '00010101', @first_bit_string.bit_string_varying - end - def test_oid_values assert_equal 1234, @first_oid.obj_id end - def test_update_integer_array - new_value = [32800,95000,29350,17000] - @first_array.commission_by_quarter = new_value - assert @first_array.save - assert @first_array.reload - assert_equal new_value, @first_array.commission_by_quarter - @first_array.commission_by_quarter = new_value - assert @first_array.save - assert @first_array.reload - assert_equal new_value, @first_array.commission_by_quarter - end - - def test_update_text_array - new_value = ['robby','robert','rob','robbie'] - @first_array.nicknames = new_value - assert @first_array.save - assert @first_array.reload - assert_equal new_value, @first_array.nicknames - @first_array.nicknames = new_value - assert @first_array.save - assert @first_array.reload - assert_equal new_value, @first_array.nicknames - end - - def test_update_money - new_value = BigDecimal.new('123.45') - @first_money.wealth = new_value - assert @first_money.save - assert @first_money.reload - assert_equal new_value, @first_money.wealth - end - def test_update_number new_single = 789.012 new_double = 789012.345 @@ -229,51 +87,6 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal '2 years 00:03:00', @first_time.time_interval end - def test_update_network_address - new_inet_address = '10.1.2.3/32' - new_cidr_address = '10.0.0.0/8' - new_mac_address = 'bc:de:f0:12:34:56' - @first_network_address.cidr_address = new_cidr_address - @first_network_address.inet_address = new_inet_address - @first_network_address.mac_address = new_mac_address - assert @first_network_address.save - assert @first_network_address.reload - assert_equal @first_network_address.cidr_address, new_cidr_address - assert_equal @first_network_address.inet_address, new_inet_address - assert_equal @first_network_address.mac_address, new_mac_address - end - - def test_update_bit_string - new_bit_string = '11111111' - new_bit_string_varying = '0xFF' - @first_bit_string.bit_string = new_bit_string - @first_bit_string.bit_string_varying = new_bit_string_varying - assert @first_bit_string.save - assert @first_bit_string.reload - assert_equal new_bit_string, @first_bit_string.bit_string - assert_equal @first_bit_string.bit_string, @first_bit_string.bit_string_varying - end - - def test_invalid_hex_string - new_bit_string = 'FF' - @first_bit_string.bit_string = new_bit_string - assert_raise(ActiveRecord::StatementInvalid) { assert @first_bit_string.save } - end - - def test_invalid_network_address - @first_network_address.cidr_address = 'invalid addr' - assert_nil @first_network_address.cidr_address - assert_equal 'invalid addr', @first_network_address.cidr_address_before_type_cast - assert @first_network_address.save - - @first_network_address.reload - - @first_network_address.inet_address = 'invalid addr' - assert_nil @first_network_address.inet_address - assert_equal 'invalid addr', @first_network_address.inet_address_before_type_cast - assert @first_network_address.save - end - def test_update_oid new_value = 567890 @first_oid.obj_id = new_value @@ -281,30 +94,26 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert @first_oid.reload assert_equal new_value, @first_oid.obj_id end +end - def test_timestamp_with_zone_values_with_rails_time_zone_support - with_timezone_config default: :utc, aware_attributes: true do - @connection.reconnect! +class PostgresqlInternalDataTypeTest < ActiveRecord::TestCase + include DdlHelper - @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 - @connection.reconnect! + setup do + @connection = ActiveRecord::Base.connection end - def test_timestamp_with_zone_values_without_rails_time_zone_support - 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') + def test_name_column_type + with_example_table @connection, 'ex', 'data name' do + column = @connection.columns('ex').find { |col| col.name == 'data' } + assert_equal :string, column.type + end + end - @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 + def test_char_column_type + with_example_table @connection, 'ex', 'data "char"' do + column = @connection.columns('ex').find { |col| col.name == 'data' } + assert_equal :string, column.type end - ensure - @connection.reconnect! end end diff --git a/activerecord/test/cases/adapters/postgresql/domain_test.rb b/activerecord/test/cases/adapters/postgresql/domain_test.rb index 5286a847a4..1500adb42d 100644 --- a/activerecord/test/cases/adapters/postgresql/domain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/domain_test.rb @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- require "cases/helper" require 'support/connection_helper' -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' class PostgresqlDomainTest < ActiveRecord::TestCase include ConnectionHelper @@ -32,7 +30,6 @@ class PostgresqlDomainTest < ActiveRecord::TestCase assert_equal :decimal, column.type assert_equal "custom_money", column.sql_type assert column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb index 4146b117f6..d99c4a292e 100644 --- a/activerecord/test/cases/adapters/postgresql/enum_test.rb +++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb @@ -34,11 +34,20 @@ class PostgresqlEnumTest < ActiveRecord::TestCase assert_equal :enum, column.type assert_equal "mood", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end + def test_enum_defaults + @connection.add_column 'postgresql_enums', 'good_mood', :mood, default: 'happy' + PostgresqlEnum.reset_column_information + + assert_equal "happy", PostgresqlEnum.column_defaults['good_mood'] + assert_equal "happy", PostgresqlEnum.new.good_mood + ensure + PostgresqlEnum.reset_column_information + end + def test_enum_mapping @connection.execute "INSERT INTO postgresql_enums VALUES (1, 'sad');" enum = PostgresqlEnum.first diff --git a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb new file mode 100644 index 0000000000..7b99fcdda0 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb @@ -0,0 +1,63 @@ +require "cases/helper" + +class PostgresqlExtensionMigrationTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class EnableHstore < ActiveRecord::Migration + def change + enable_extension "hstore" + end + end + + class DisableHstore < ActiveRecord::Migration + def change + disable_extension "hstore" + end + end + + def setup + super + + @connection = ActiveRecord::Base.connection + + unless @connection.supports_extensions? + return skip("no extension support") + end + + @old_schema_migration_tabel_name = ActiveRecord::SchemaMigration.table_name + @old_tabel_name_prefix = ActiveRecord::Base.table_name_prefix + @old_tabel_name_suffix = ActiveRecord::Base.table_name_suffix + + ActiveRecord::Base.table_name_prefix = "p_" + ActiveRecord::Base.table_name_suffix = "_s" + ActiveRecord::SchemaMigration.delete_all rescue nil + ActiveRecord::SchemaMigration.table_name = "p_schema_migrations_s" + ActiveRecord::Migration.verbose = false + end + + def teardown + ActiveRecord::Base.table_name_prefix = @old_tabel_name_prefix + ActiveRecord::Base.table_name_suffix = @old_tabel_name_suffix + ActiveRecord::SchemaMigration.delete_all rescue nil + ActiveRecord::Migration.verbose = true + ActiveRecord::SchemaMigration.table_name = @old_schema_migration_tabel_name + + super + end + + def test_enable_extension_migration_ignores_prefix_and_suffix + @connection.disable_extension("hstore") + + migrations = [EnableHstore.new(nil, 1)] + ActiveRecord::Migrator.new(:up, migrations).migrate + assert @connection.extension_enabled?("hstore"), "extension hstore should be enabled" + end + + def test_disable_extension_migration_ignores_prefix_and_suffix + @connection.enable_extension("hstore") + + migrations = [DisableHstore.new(nil, 1)] + ActiveRecord::Migrator.new(:up, migrations).migrate + assert_not @connection.extension_enabled?("hstore"), "extension hstore should not be enabled" + end +end diff --git a/activerecord/test/cases/adapters/postgresql/full_text_test.rb b/activerecord/test/cases/adapters/postgresql/full_text_test.rb new file mode 100644 index 0000000000..9dadb177ca --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/full_text_test.rb @@ -0,0 +1,26 @@ +# encoding: utf-8 +require "cases/helper" + +class PostgresqlFullTextTest < ActiveRecord::TestCase + class PostgresqlTsvector < ActiveRecord::Base; end + + def test_tsvector_column + column = PostgresqlTsvector.columns_hash["text_vector"] + assert_equal :tsvector, column.type + assert_equal "tsvector", column.sql_type + assert_not column.number? + assert_not column.binary? + assert_not column.array + end + + def test_update_tsvector + PostgresqlTsvector.create text_vector: "'text' 'vector'" + tsvector = PostgresqlTsvector.first + assert_equal "'text' 'vector'", tsvector.text_vector + + tsvector.text_vector = "'new' 'text' 'vector'" + tsvector.save! + assert tsvector.reload + assert_equal "'new' 'text' 'vector'", tsvector.text_vector + end +end diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb new file mode 100644 index 0000000000..6c0adbbeaa --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +require "cases/helper" +require 'support/connection_helper' +require 'support/schema_dumping_helper' + +class PostgresqlPointTest < ActiveRecord::TestCase + include ConnectionHelper + include SchemaDumpingHelper + + class PostgresqlPoint < ActiveRecord::Base; end + + def setup + @connection = ActiveRecord::Base.connection + @connection.transaction do + @connection.create_table('postgresql_points') do |t| + t.point :x + t.point :y, default: [12.2, 13.3] + t.point :z, default: "(14.4,15.5)" + end + end + end + + teardown do + @connection.execute 'DROP TABLE IF EXISTS postgresql_points' + end + + def test_column + column = PostgresqlPoint.columns_hash["x"] + assert_equal :point, column.type + assert_equal "point", column.sql_type + assert_not column.number? + assert_not column.binary? + assert_not column.array + end + + def test_default + assert_equal [12.2, 13.3], PostgresqlPoint.column_defaults['y'] + assert_equal [12.2, 13.3], PostgresqlPoint.new.y + + assert_equal [14.4, 15.5], PostgresqlPoint.column_defaults['z'] + assert_equal [14.4, 15.5], PostgresqlPoint.new.z + end + + def test_schema_dumping + output = dump_table_schema("postgresql_points") + assert_match %r{t\.point\s+"x"$}, output + assert_match %r{t\.point\s+"y",\s+default: \[12\.2, 13\.3\]$}, output + assert_match %r{t\.point\s+"z",\s+default: \[14\.4, 15\.5\]$}, output + end + + def test_roundtrip + PostgresqlPoint.create! x: [10, 25.2] + record = PostgresqlPoint.first + assert_equal [10, 25.2], record.x + + record.x = [1.1, 2.2] + record.save! + assert record.reload + assert_equal [1.1, 2.2], record.x + end + + def test_mutation + p = PostgresqlPoint.create! x: [10, 20] + + p.x[1] = 25 + p.save! + p.reload + + assert_equal [10.0, 25.0], p.x + assert_not p.changed? + end +end diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index c24c4b0d56..1296eb72c0 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -28,7 +28,7 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase t.hstore 'settings' end end - @column = Hstore.columns.find { |c| c.name == 'tags' } + @column = Hstore.columns_hash['tags'] end teardown do @@ -56,7 +56,6 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase assert_equal :hstore, @column.type assert_equal "hstore", @column.sql_type assert_not @column.number? - assert_not @column.text? assert_not @column.binary? assert_not @column.array end @@ -64,9 +63,8 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase def test_default @connection.add_column 'hstores', 'permissions', :hstore, default: '"users"=>"read", "articles"=>"write"' Hstore.reset_column_information - column = Hstore.columns_hash["permissions"] - assert_equal({"users"=>"read", "articles"=>"write"}, column.default) + assert_equal({"users"=>"read", "articles"=>"write"}, Hstore.column_defaults['permissions']) assert_equal({"users"=>"read", "articles"=>"write"}, Hstore.new.permissions) ensure Hstore.reset_column_information @@ -78,7 +76,7 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase t.hstore 'users', default: '' end Hstore.reset_column_information - column = Hstore.columns.find { |c| c.name == 'users' } + column = Hstore.columns_hash['users'] assert_equal :hstore, column.type raise ActiveRecord::Rollback # reset the schema change @@ -106,22 +104,17 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase def test_cast_value_on_write x = Hstore.new tags: {"bool" => true, "number" => 5} + assert_equal({"bool" => true, "number" => 5}, x.tags_before_type_cast) assert_equal({"bool" => "true", "number" => "5"}, x.tags) x.save assert_equal({"bool" => "true", "number" => "5"}, x.reload.tags) end 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)) - - assert_equal({}, @column.type_cast("")) - assert_equal({'key'=>nil}, @column.type_cast('key => NULL')) - assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast(%q(c=>"}", "\"a\""=>"b \"a b"))) + assert_equal({'1' => '2'}, @column.type_cast_from_database("\"1\"=>\"2\"")) + assert_equal({}, @column.type_cast_from_database("")) + assert_equal({'key'=>nil}, @column.type_cast_from_database('key => NULL')) + assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast_from_database(%q(c=>"}", "\"a\""=>"b \"a b"))) end def test_with_store_accessors @@ -142,48 +135,78 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase assert_equal "GMT", x.timezone end + def test_duplication_with_store_accessors + x = Hstore.new(language: "fr", timezone: "GMT") + assert_equal "fr", x.language + assert_equal "GMT", x.timezone + + y = x.dup + assert_equal "fr", y.language + assert_equal "GMT", y.timezone + end + + def test_yaml_round_trip_with_store_accessors + x = Hstore.new(language: "fr", timezone: "GMT") + assert_equal "fr", x.language + assert_equal "GMT", x.timezone + + y = YAML.load(YAML.dump(x)) + assert_equal "fr", y.language + assert_equal "GMT", y.timezone + end + + def test_changes_in_place + hstore = Hstore.create!(settings: { 'one' => 'two' }) + hstore.settings['three'] = 'four' + hstore.save! + hstore.reload + + assert_equal 'four', hstore.settings['three'] + assert_not hstore.changed? + end + def test_gen1 - assert_equal(%q(" "=>""), @column.class.hstore_to_string({' '=>''})) + assert_equal(%q(" "=>""), @column.cast_type.type_cast_for_database({' '=>''})) end def test_gen2 - assert_equal(%q(","=>""), @column.class.hstore_to_string({','=>''})) + assert_equal(%q(","=>""), @column.cast_type.type_cast_for_database({','=>''})) end def test_gen3 - assert_equal(%q("="=>""), @column.class.hstore_to_string({'='=>''})) + assert_equal(%q("="=>""), @column.cast_type.type_cast_for_database({'='=>''})) end def test_gen4 - assert_equal(%q(">"=>""), @column.class.hstore_to_string({'>'=>''})) + assert_equal(%q(">"=>""), @column.cast_type.type_cast_for_database({'>'=>''})) end def test_parse1 - assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @column.type_cast('a=>null,b=>NuLl,c=>"NuLl",null=>c')) + assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @column.type_cast_from_database('a=>null,b=>NuLl,c=>"NuLl",null=>c')) end def test_parse2 - assert_equal({" " => " "}, @column.type_cast("\\ =>\\ ")) + assert_equal({" " => " "}, @column.type_cast_from_database("\\ =>\\ ")) end def test_parse3 - assert_equal({"=" => ">"}, @column.type_cast("==>>")) + assert_equal({"=" => ">"}, @column.type_cast_from_database("==>>")) end def test_parse4 - assert_equal({"=a"=>"q=w"}, @column.type_cast('\=a=>q=w')) + assert_equal({"=a"=>"q=w"}, @column.type_cast_from_database('\=a=>q=w')) end def test_parse5 - assert_equal({"=a"=>"q=w"}, @column.type_cast('"=a"=>q\=w')) + assert_equal({"=a"=>"q=w"}, @column.type_cast_from_database('"=a"=>q\=w')) end def test_parse6 - assert_equal({"\"a"=>"q>w"}, @column.type_cast('"\"a"=>q>w')) + assert_equal({"\"a"=>"q>w"}, @column.type_cast_from_database('"\"a"=>q>w')) end def test_parse7 - assert_equal({"\"a"=>"q\"w"}, @column.type_cast('\"a=>q"w')) + assert_equal({"\"a"=>"q\"w"}, @column.type_cast_from_database('\"a=>q"w')) end def test_rewrite @@ -265,14 +288,32 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase assert_cycle("a\nb" => "c\nd") end - def test_update_all - hstore = Hstore.create! tags: { "one" => "two" } + class TagCollection + def initialize(hash); @hash = hash end + def to_hash; @hash end + def self.load(hash); new(hash) end + def self.dump(object); object.to_hash end + end - Hstore.update_all tags: { "three" => "four" } - assert_equal({ "three" => "four" }, hstore.reload.tags) + class HstoreWithSerialize < Hstore + serialize :tags, TagCollection + end + + def test_hstore_with_serialized_attributes + HstoreWithSerialize.create! tags: TagCollection.new({"one" => "two"}) + record = HstoreWithSerialize.first + assert_instance_of TagCollection, record.tags + assert_equal({"one" => "two"}, record.tags.to_hash) + record.tags = TagCollection.new("three" => "four") + record.save! + assert_equal({"three" => "four"}, HstoreWithSerialize.first.tags.to_hash) + end - Hstore.update_all tags: { } - assert_equal({ }, hstore.reload.tags) + def test_clone_hstore_with_serialized_attributes + HstoreWithSerialize.create! tags: TagCollection.new({"one" => "two"}) + record = HstoreWithSerialize.first + dupe = record.dup + assert_equal({"one" => "two"}, dupe.tags.to_hash) end end diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb new file mode 100644 index 0000000000..22e8873333 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb @@ -0,0 +1,44 @@ +require "cases/helper" + +class PostgresqlInfinityTest < ActiveRecord::TestCase + class PostgresqlInfinity < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:postgresql_infinities) do |t| + t.float :float + t.datetime :datetime + end + end + + teardown do + @connection.execute("DROP TABLE IF EXISTS postgresql_infinities") + end + + test "type casting infinity on a float column" do + record = PostgresqlInfinity.create!(float: Float::INFINITY) + record.reload + assert_equal Float::INFINITY, record.float + end + + test "update_all with infinity on a float column" do + record = PostgresqlInfinity.create! + PostgresqlInfinity.update_all(float: Float::INFINITY) + record.reload + assert_equal Float::INFINITY, record.float + end + + test "type casting infinity on a datetime column" do + record = PostgresqlInfinity.create!(datetime: Float::INFINITY) + record.reload + assert_equal Float::INFINITY, record.datetime + end + + test "update_all with infinity on a datetime column" do + record = PostgresqlInfinity.create! + PostgresqlInfinity.update_all(datetime: Float::INFINITY) + record.reload + assert_equal Float::INFINITY, record.datetime + end +end diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index ee793ffff2..86ba849445 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -4,7 +4,7 @@ require "cases/helper" require 'active_record/base' require 'active_record/connection_adapters/postgresql_adapter' -class PostgresqlJSONTest < ActiveRecord::TestCase +module PostgresqlJSONSharedTestCases class JsonDataType < ActiveRecord::Base self.table_name = 'json_data_type' @@ -16,36 +16,34 @@ class PostgresqlJSONTest < ActiveRecord::TestCase begin @connection.transaction do @connection.create_table('json_data_type') do |t| - t.json 'payload', :default => {} - t.json 'settings' + t.public_send column_type, 'payload', default: {} # t.json 'payload', default: {} + t.public_send column_type, 'settings' # t.json 'settings' end end rescue ActiveRecord::StatementInvalid skip "do not test on PG without json" end - @column = JsonDataType.columns.find { |c| c.name == 'payload' } + @column = JsonDataType.columns_hash['payload'] end - teardown do + def teardown @connection.execute 'drop table if exists json_data_type' end def test_column column = JsonDataType.columns_hash["payload"] - assert_equal :json, column.type - assert_equal "json", column.sql_type + assert_equal column_type, column.type + assert_equal column_type.to_s, column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end def test_default - @connection.add_column 'json_data_type', 'permissions', :json, default: '{"users": "read", "posts": ["read", "write"]}' + @connection.add_column 'json_data_type', 'permissions', column_type, default: '{"users": "read", "posts": ["read", "write"]}' JsonDataType.reset_column_information - column = JsonDataType.columns_hash["permissions"] - assert_equal({"users"=>"read", "posts"=>["read", "write"]}, column.default) + assert_equal({"users"=>"read", "posts"=>["read", "write"]}, JsonDataType.column_defaults['permissions']) assert_equal({"users"=>"read", "posts"=>["read", "write"]}, JsonDataType.new.permissions) ensure JsonDataType.reset_column_information @@ -54,11 +52,11 @@ class PostgresqlJSONTest < ActiveRecord::TestCase def test_change_table_supports_json @connection.transaction do @connection.change_table('json_data_type') do |t| - t.json 'users', default: '{}' + t.public_send column_type, 'users', default: '{}' # t.json 'users', default: '{}' end JsonDataType.reset_column_information - column = JsonDataType.columns.find { |c| c.name == 'users' } - assert_equal :json, column.type + column = JsonDataType.columns_hash['users'] + assert_equal column_type, column.type raise ActiveRecord::Rollback # reset the schema change end @@ -68,6 +66,7 @@ class PostgresqlJSONTest < ActiveRecord::TestCase def test_cast_value_on_write x = JsonDataType.new payload: {"string" => "foo", :symbol => :bar} + assert_equal({"string" => "foo", :symbol => :bar}, x.payload_before_type_cast) assert_equal({"string" => "foo", "symbol" => "bar"}, x.payload) x.save assert_equal({"string" => "foo", "symbol" => "bar"}, x.reload.payload) @@ -77,13 +76,13 @@ class PostgresqlJSONTest < ActiveRecord::TestCase column = JsonDataType.columns_hash["payload"] data = "{\"a_key\":\"a_value\"}" - hash = column.class.string_to_json data + hash = column.type_cast_from_database(data) assert_equal({'a_key' => 'a_value'}, hash) - assert_equal({'a_key' => 'a_value'}, column.type_cast(data)) + assert_equal({'a_key' => 'a_value'}, column.type_cast_from_database(data)) - assert_equal({}, column.type_cast("{}")) - assert_equal({'key'=>nil}, column.type_cast('{"key": null}')) - assert_equal({'c'=>'}','"a"'=>'b "a b'}, column.type_cast(%q({"c":"}", "\"a\"":"b \"a b"}))) + assert_equal({}, column.type_cast_from_database("{}")) + assert_equal({'key'=>nil}, column.type_cast_from_database('{"key": null}')) + assert_equal({'c'=>'}','"a"'=>'b "a b'}, column.type_cast_from_database(%q({"c":"}", "\"a\"":"b \"a b"}))) end def test_rewrite @@ -139,13 +138,56 @@ class PostgresqlJSONTest < ActiveRecord::TestCase assert_equal "640×1136", x.resolution end - def test_update_all - json = JsonDataType.create! payload: { "one" => "two" } + def test_duplication_with_store_accessors + x = JsonDataType.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + y = x.dup + assert_equal "320×480", y.resolution + end + + def test_yaml_round_trip_with_store_accessors + x = JsonDataType.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + y = YAML.load(YAML.dump(x)) + assert_equal "320×480", y.resolution + end + + def test_changes_in_place + json = JsonDataType.new + assert_not json.changed? + + json.payload = { 'one' => 'two' } + assert json.changed? + assert json.payload_changed? + + json.save! + assert_not json.changed? + + json.payload['three'] = 'four' + assert json.payload_changed? + + json.save! + json.reload + + assert_equal({ 'one' => 'two', 'three' => 'four' }, json.payload) + assert_not json.changed? + end +end + +class PostgresqlJSONTest < ActiveRecord::TestCase + include PostgresqlJSONSharedTestCases + + def column_type + :json + end +end - JsonDataType.update_all payload: { "three" => "four" } - assert_equal({ "three" => "four" }, json.reload.payload) +class PostgresqlJSONBTest < ActiveRecord::TestCase + include PostgresqlJSONSharedTestCases - JsonDataType.update_all payload: { } - assert_equal({ }, json.reload.payload) + def column_type + :jsonb end end diff --git a/activerecord/test/cases/adapters/postgresql/ltree_test.rb b/activerecord/test/cases/adapters/postgresql/ltree_test.rb index 718f37a380..889e369bd6 100644 --- a/activerecord/test/cases/adapters/postgresql/ltree_test.rb +++ b/activerecord/test/cases/adapters/postgresql/ltree_test.rb @@ -1,7 +1,5 @@ # encoding: utf-8 require "cases/helper" -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' class PostgresqlLtreeTest < ActiveRecord::TestCase class Ltree < ActiveRecord::Base @@ -33,7 +31,6 @@ class PostgresqlLtreeTest < ActiveRecord::TestCase assert_equal :ltree, column.type assert_equal "ltree", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb new file mode 100644 index 0000000000..87183174f2 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/money_test.rb @@ -0,0 +1,96 @@ +# encoding: utf-8 +require "cases/helper" +require 'support/schema_dumping_helper' + +class PostgresqlMoneyTest < ActiveRecord::TestCase + include SchemaDumpingHelper + + class PostgresqlMoney < ActiveRecord::Base; end + + setup do + @connection = ActiveRecord::Base.connection + @connection.execute("set lc_monetary = 'C'") + @connection.create_table('postgresql_moneys') do |t| + t.column "wealth", "money" + t.column "depth", "money", default: "150.55" + end + end + + teardown do + @connection.execute 'DROP TABLE IF EXISTS postgresql_moneys' + end + + def test_column + column = PostgresqlMoney.columns_hash["wealth"] + assert_equal :money, column.type + assert_equal "money", column.sql_type + assert_equal 2, column.scale + assert column.number? + assert_not column.binary? + assert_not column.array + end + + def test_default + assert_equal BigDecimal.new("150.55"), PostgresqlMoney.column_defaults['depth'] + assert_equal BigDecimal.new("150.55"), PostgresqlMoney.new.depth + end + + def test_money_values + @connection.execute("INSERT INTO postgresql_moneys (id, wealth) VALUES (1, '567.89'::money)") + @connection.execute("INSERT INTO postgresql_moneys (id, wealth) VALUES (2, '-567.89'::money)") + + first_money = PostgresqlMoney.find(1) + second_money = PostgresqlMoney.find(2) + assert_equal 567.89, first_money.wealth + assert_equal(-567.89, second_money.wealth) + end + + def test_money_type_cast + column = PostgresqlMoney.columns_hash['wealth'] + assert_equal(12345678.12, column.type_cast_from_user("$12,345,678.12")) + assert_equal(12345678.12, column.type_cast_from_user("$12.345.678,12")) + assert_equal(-1.15, column.type_cast_from_user("-$1.15")) + assert_equal(-2.25, column.type_cast_from_user("($2.25)")) + end + + def test_schema_dumping + output = dump_table_schema("postgresql_moneys") + assert_match %r{t\.money\s+"wealth",\s+scale: 2$}, output + assert_match %r{t\.money\s+"depth",\s+scale: 2,\s+default: 150.55$}, output + end + + def test_create_and_update_money + money = PostgresqlMoney.create(wealth: "987.65") + assert_equal 987.65, money.wealth + + new_value = BigDecimal.new('123.45') + money.wealth = new_value + money.save! + money.reload + assert_equal new_value, money.wealth + end + + def test_update_all_with_money_string + money = PostgresqlMoney.create! + PostgresqlMoney.update_all(wealth: "987.65") + money.reload + + assert_equal 987.65, money.wealth + end + + def test_update_all_with_money_big_decimal + money = PostgresqlMoney.create! + PostgresqlMoney.update_all(wealth: '123.45'.to_d) + money.reload + + assert_equal 123.45, money.wealth + end + + def test_update_all_with_money_numeric + money = PostgresqlMoney.create! + PostgresqlMoney.update_all(wealth: 123.45) + money.reload + + assert_equal 123.45, money.wealth + end +end diff --git a/activerecord/test/cases/adapters/postgresql/network_test.rb b/activerecord/test/cases/adapters/postgresql/network_test.rb new file mode 100644 index 0000000000..4f4c1103fa --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/network_test.rb @@ -0,0 +1,71 @@ +# encoding: utf-8 +require "cases/helper" + +class PostgresqlNetworkTest < ActiveRecord::TestCase + class PostgresqlNetworkAddress < ActiveRecord::Base + end + + def test_cidr_column + column = PostgresqlNetworkAddress.columns_hash["cidr_address"] + assert_equal :cidr, column.type + assert_equal "cidr", column.sql_type + assert_not column.number? + assert_not column.binary? + assert_not column.array + end + + def test_inet_column + column = PostgresqlNetworkAddress.columns_hash["inet_address"] + assert_equal :inet, column.type + assert_equal "inet", column.sql_type + assert_not column.number? + assert_not column.binary? + assert_not column.array + end + + def test_macaddr_column + column = PostgresqlNetworkAddress.columns_hash["mac_address"] + assert_equal :macaddr, column.type + assert_equal "macaddr", column.sql_type + assert_not column.number? + assert_not column.binary? + assert_not column.array + end + + def test_network_types + PostgresqlNetworkAddress.create(cidr_address: '192.168.0.0/24', + inet_address: '172.16.1.254/32', + mac_address: '01:23:45:67:89:0a') + + address = PostgresqlNetworkAddress.first + assert_equal IPAddr.new('192.168.0.0/24'), address.cidr_address + assert_equal IPAddr.new('172.16.1.254'), address.inet_address + assert_equal '01:23:45:67:89:0a', address.mac_address + + address.cidr_address = '10.1.2.3/32' + address.inet_address = '10.0.0.0/8' + address.mac_address = 'bc:de:f0:12:34:56' + + address.save! + assert address.reload + assert_equal IPAddr.new('10.1.2.3/32'), address.cidr_address + assert_equal IPAddr.new('10.0.0.0/8'), address.inet_address + assert_equal 'bc:de:f0:12:34:56', address.mac_address + end + + def test_invalid_network_address + invalid_address = PostgresqlNetworkAddress.new(cidr_address: 'invalid addr', + inet_address: 'invalid addr') + assert_nil invalid_address.cidr_address + assert_nil invalid_address.inet_address + assert_equal 'invalid addr', invalid_address.cidr_address_before_type_cast + assert_equal 'invalid addr', invalid_address.inet_address_before_type_cast + assert invalid_address.save + + invalid_address.reload + assert_nil invalid_address.cidr_address + assert_nil invalid_address.inet_address + assert_nil invalid_address.cidr_address_before_type_cast + assert_nil invalid_address.inet_address_before_type_cast + end +end diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index b7791078db..cfff1f980b 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -134,18 +134,18 @@ module ActiveRecord end def test_default_sequence_name - assert_equal 'accounts_id_seq', + assert_equal PostgreSQL::Name.new('public', 'accounts_id_seq'), @connection.default_sequence_name('accounts', 'id') - assert_equal 'accounts_id_seq', + assert_equal PostgreSQL::Name.new('public', 'accounts_id_seq'), @connection.default_sequence_name('accounts') end def test_default_sequence_name_bad_table - assert_equal 'zomg_id_seq', + assert_equal PostgreSQL::Name.new(nil, 'zomg_id_seq'), @connection.default_sequence_name('zomg', 'id') - assert_equal 'zomg_id_seq', + assert_equal PostgreSQL::Name.new(nil, 'zomg_id_seq'), @connection.default_sequence_name('zomg') end @@ -216,7 +216,7 @@ module ActiveRecord ) seq = @connection.pk_and_sequence_for('ex').last - assert_equal 'ex_id_seq', seq + assert_equal PostgreSQL::Name.new("public", "ex_id_seq"), seq @connection.exec_query( "DELETE FROM pg_depend WHERE objid = 'ex2_id_seq'::regclass AND refobjid = 'ex'::regclass AND deptype = 'a'" @@ -353,6 +353,17 @@ module ActiveRecord assert_equal "posts.title, posts.updater_id AS alias_0", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls last"]) end + def test_columns_for_distinct_without_order_specifiers + assert_equal "posts.title, posts.updater_id AS alias_0", + @connection.columns_for_distinct("posts.title", ["posts.updater_id"]) + + assert_equal "posts.title, posts.updater_id AS alias_0", + @connection.columns_for_distinct("posts.title", ["posts.updater_id nulls last"]) + + assert_equal "posts.title, posts.updater_id AS alias_0", + @connection.columns_for_distinct("posts.title", ["posts.updater_id nulls first"]) + end + def test_raise_error_when_cannot_translate_exception assert_raise TypeError do @connection.send(:log, nil) { @connection.execute(nil) } @@ -396,6 +407,23 @@ module ActiveRecord reset_connection end + def test_unparsed_defaults_are_at_least_set_when_saving + with_example_table "id SERIAL PRIMARY KEY, number INTEGER NOT NULL DEFAULT (4 + 4) * 2 / 4" do + number_klass = Class.new(ActiveRecord::Base) do + self.table_name = 'ex' + end + column = number_klass.columns_hash["number"] + assert_nil column.default + assert_nil column.default_function + + first_number = number_klass.new + assert_nil first_number.number + + first_number.save! + assert_equal 4, first_number.reload.number + end + end + private def insert(ctx, data) binds = data.map { |name, value| diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index 51846e22d9..11d5173d37 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -10,13 +10,13 @@ module ActiveRecord end def test_type_cast_true - c = PostgreSQLColumn.new(nil, 1, OID::Boolean.new, 'boolean') + c = PostgreSQLColumn.new(nil, 1, Type::Boolean.new, 'boolean') assert_equal 't', @conn.type_cast(true, nil) assert_equal 't', @conn.type_cast(true, c) end def test_type_cast_false - c = PostgreSQLColumn.new(nil, 1, OID::Boolean.new, 'boolean') + c = PostgreSQLColumn.new(nil, 1, Type::Boolean.new, 'boolean') assert_equal 'f', @conn.type_cast(false, nil) assert_equal 'f', @conn.type_cast(false, c) end @@ -47,9 +47,9 @@ module ActiveRecord def test_quote_cast_numeric fixnum = 666 - c = PostgreSQLColumn.new(nil, nil, OID::String.new, 'varchar') + c = PostgreSQLColumn.new(nil, nil, Type::String.new, 'varchar') assert_equal "'666'", @conn.quote(fixnum, c) - c = PostgreSQLColumn.new(nil, nil, OID::Text.new, 'text') + c = PostgreSQLColumn.new(nil, nil, Type::Text.new, 'text') assert_equal "'666'", @conn.quote(fixnum, c) end @@ -57,6 +57,17 @@ module ActiveRecord assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0)) assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0).to_datetime) end + + def test_quote_range + range = "1,2]'; SELECT * FROM users; --".."a" + c = PostgreSQLColumn.new(nil, nil, OID::Range.new(Type::Integer.new, :int8range)) + assert_equal "'[1,0]'", @conn.quote(range, c) + end + + def test_quote_bit_string + c = PostgreSQLColumn.new(nil, 1, OID::Bit.new) + assert_equal nil, @conn.quote("'); SELECT * FROM users; /*\n01\n*/--", c) + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb index 060b17d071..d812cd01c4 100644 --- a/activerecord/test/cases/adapters/postgresql/range_test.rb +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -1,7 +1,5 @@ require "cases/helper" require 'support/connection_helper' -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' if ActiveRecord::Base.connection.supports_ranges? class PostgresqlRange < ActiveRecord::Base @@ -158,7 +156,7 @@ _SQL assert_equal 0.5..0.7, @first_range.float_range assert_equal 0.5...0.7, @second_range.float_range assert_equal 0.5...Float::INFINITY, @third_range.float_range - assert_equal (-Float::INFINITY...Float::INFINITY), @fourth_range.float_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.float_range) assert_nil @empty_range.float_range end @@ -264,6 +262,23 @@ _SQL assert_raises(ArgumentError) { PostgresqlRange.create!(float_range: "(0.5, 0.7]") } end + def test_update_all_with_ranges + PostgresqlRange.create! + + PostgresqlRange.update_all(int8_range: 1..100) + + assert_equal 1...101, PostgresqlRange.first.int8_range + end + + def test_ranges_correctly_escape_input + range = "-1,2]'; DROP TABLE postgresql_ranges; --".."a" + PostgresqlRange.update_all(int8_range: range) + + assert_nothing_raised do + PostgresqlRange.first + end + end + private def assert_equal_round_trip(range, attribute, value) round_trip(range, attribute, value) diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 11ec7599a3..9e5fd17dc4 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -50,6 +50,16 @@ class SchemaTest < ActiveRecord::TestCase self.table_name = 'things' end + class Song < ActiveRecord::Base + self.table_name = "music.songs" + has_and_belongs_to_many :albums + end + + class Album < ActiveRecord::Base + self.table_name = "music.albums" + has_and_belongs_to_many :songs + end + def setup @connection = ActiveRecord::Base.connection @connection.execute "CREATE SCHEMA #{SCHEMA_NAME} CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})" @@ -109,6 +119,22 @@ class SchemaTest < ActiveRecord::TestCase assert !@connection.schema_names.include?("test_schema3") end + def test_habtm_table_name_with_schema + ActiveRecord::Base.connection.execute <<-SQL + DROP SCHEMA IF EXISTS music CASCADE; + CREATE SCHEMA music; + CREATE TABLE music.albums (id serial primary key); + CREATE TABLE music.songs (id serial primary key); + CREATE TABLE music.albums_songs (album_id integer, song_id integer); + SQL + + song = Song.create + Album.create + assert_equal song, Song.includes(:albums).references(:albums).first + ensure + ActiveRecord::Base.connection.execute "DROP SCHEMA music CASCADE;" + end + def test_raise_drop_schema_with_nonexisting_schema assert_raises(ActiveRecord::StatementInvalid) do @connection.drop_schema "test_schema3" @@ -305,14 +331,15 @@ class SchemaTest < ActiveRecord::TestCase end def test_pk_and_sequence_for_with_schema_specified + pg_name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name [ %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}"), %("#{SCHEMA_NAME}"."#{UNMATCHED_PK_TABLE_NAME}") ].each do |given| pk, seq = @connection.pk_and_sequence_for(given) assert_equal 'id', pk, "primary key should be found when table referenced as #{given}" - assert_equal "#{PK_TABLE_NAME}_id_seq", seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}") - assert_equal "#{UNMATCHED_SEQUENCE_NAME}", seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{UNMATCHED_PK_TABLE_NAME}") + assert_equal pg_name.new(SCHEMA_NAME, "#{PK_TABLE_NAME}_id_seq"), seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}") + assert_equal pg_name.new(SCHEMA_NAME, UNMATCHED_SEQUENCE_NAME), seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{UNMATCHED_PK_TABLE_NAME}") end end @@ -352,6 +379,14 @@ class SchemaTest < ActiveRecord::TestCase end end + def test_reset_pk_sequence + sequence_name = "#{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}" + @connection.execute "SELECT setval('#{sequence_name}', 123)" + assert_equal "124", @connection.select_value("SELECT nextval('#{sequence_name}')") + @connection.reset_pk_sequence!("#{SCHEMA_NAME}.#{UNMATCHED_PK_TABLE_NAME}") + assert_equal "1", @connection.select_value("SELECT nextval('#{sequence_name}')") + end + private def columns(table_name) @connection.send(:column_definitions, table_name).map do |name, type, default| diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb index 4d29a20e66..3614b29190 100644 --- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb +++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb @@ -2,6 +2,47 @@ require 'cases/helper' require 'models/developer' require 'models/topic' +class PostgresqlTimestampTest < ActiveRecord::TestCase + class PostgresqlTimestampWithZone < ActiveRecord::Base; end + + self.use_transactional_fixtures = false + + setup do + @connection = ActiveRecord::Base.connection + @connection.execute("INSERT INTO postgresql_timestamp_with_zones (id, time) VALUES (1, '2010-01-01 10:00:00-1')") + end + + teardown do + PostgresqlTimestampWithZone.delete_all + end + + def test_timestamp_with_zone_values_with_rails_time_zone_support + with_timezone_config default: :utc, aware_attributes: true do + @connection.reconnect! + + timestamp = PostgresqlTimestampWithZone.find(1) + assert_equal Time.utc(2010,1,1, 11,0,0), timestamp.time + assert_instance_of Time, timestamp.time + end + ensure + @connection.reconnect! + end + + def test_timestamp_with_zone_values_without_rails_time_zone_support + 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') + + timestamp = PostgresqlTimestampWithZone.find(1) + assert_equal Time.utc(2010,1,1, 11,0,0), timestamp.time + assert_instance_of Time, timestamp.time + end + ensure + @connection.reconnect! + end +end + class TimestampTest < ActiveRecord::TestCase fixtures :topics @@ -82,20 +123,32 @@ class TimestampTest < ActiveRecord::TestCase assert_equal date, Developer.find_by_name("aaron").updated_at end + def test_bc_timestamp_leap_year + date = Time.utc(-4, 2, 29) + Developer.create!(:name => "taihou", :updated_at => date) + assert_equal date, Developer.find_by_name("taihou").updated_at + end + + def test_bc_timestamp_year_zero + date = Time.utc(0, 4, 7) + Developer.create!(:name => "yahagi", :updated_at => date) + assert_equal date, Developer.find_by_name("yahagi").updated_at + end + private - def pg_datetime_precision(table_name, column_name) - results = ActiveRecord::Base.connection.execute("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name ='#{table_name}'") - result = results.find do |result_hash| - result_hash["column_name"] == column_name - end - result && result["datetime_precision"] + def pg_datetime_precision(table_name, column_name) + results = ActiveRecord::Base.connection.execute("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name ='#{table_name}'") + result = results.find do |result_hash| + result_hash["column_name"] == column_name end + result && result["datetime_precision"] + end - def activerecord_column_option(tablename, column_name, option) - result = ActiveRecord::Base.connection.columns(tablename).find do |column| - column.name == column_name - end - result && result.send(option) + def activerecord_column_option(tablename, column_name, option) + result = ActiveRecord::Base.connection.columns(tablename).find do |column| + column.name == column_name end + result && result.send(option) + end end diff --git a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb new file mode 100644 index 0000000000..23817198b1 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb @@ -0,0 +1,15 @@ +require 'cases/helper' + +class PostgresqlTypeLookupTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + end + + test "array delimiters are looked up correctly" do + box_array = @connection.type_map.lookup(1020) + int_array = @connection.type_map.lookup(1007) + + assert_equal ';', box_array.delimiter + assert_equal ',', int_array.delimiter + end +end diff --git a/activerecord/test/cases/adapters/postgresql/utils_test.rb b/activerecord/test/cases/adapters/postgresql/utils_test.rb index 9e7b08ef34..3fdb6888d9 100644 --- a/activerecord/test/cases/adapters/postgresql/utils_test.rb +++ b/activerecord/test/cases/adapters/postgresql/utils_test.rb @@ -1,9 +1,10 @@ require 'cases/helper' class PostgreSQLUtilsTest < ActiveSupport::TestCase - include ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::Utils + Name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name + include ActiveRecord::ConnectionAdapters::PostgreSQL::Utils - def test_extract_schema_and_table + def test_extract_schema_qualified_name { %(table_name) => [nil,'table_name'], %("table.name") => [nil,'table.name'], @@ -14,7 +15,47 @@ class PostgreSQLUtilsTest < ActiveSupport::TestCase %("even spaces".table) => ['even spaces','table'], %(schema."table.name") => ['schema', 'table.name'] }.each do |given, expect| - assert_equal expect, extract_schema_and_table(given) + assert_equal Name.new(*expect), extract_schema_qualified_name(given) end end end + +class PostgreSQLNameTest < ActiveSupport::TestCase + Name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name + + test "represents itself as schema.name" do + obj = Name.new("public", "articles") + assert_equal "public.articles", obj.to_s + end + + test "without schema, represents itself as name only" do + obj = Name.new(nil, "articles") + assert_equal "articles", obj.to_s + end + + test "quoted returns a string representation usable in a query" do + assert_equal %("articles"), Name.new(nil, "articles").quoted + assert_equal %("public"."articles"), Name.new("public", "articles").quoted + end + + test "prevents double quoting" do + name = Name.new('"quoted_schema"', '"quoted_table"') + assert_equal "quoted_schema.quoted_table", name.to_s + assert_equal %("quoted_schema"."quoted_table"), name.quoted + end + + test "equality based on state" do + assert_equal Name.new("access", "users"), Name.new("access", "users") + assert_equal Name.new(nil, "users"), Name.new(nil, "users") + assert_not_equal Name.new(nil, "users"), Name.new("access", "users") + assert_not_equal Name.new("access", "users"), Name.new("public", "users") + assert_not_equal Name.new("public", "users"), Name.new("public", "articles") + end + + test "can be used as hash key" do + hash = {Name.new("schema", "article_seq") => "success"} + assert_equal "success", hash[Name.new("schema", "article_seq")] + assert_equal nil, hash[Name.new("schema", "articles")] + assert_equal nil, hash[Name.new("public", "article_seq")] + end +end diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index bdf8e15e3e..66006d718f 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -9,15 +9,6 @@ module PostgresqlUUIDHelper @connection ||= ActiveRecord::Base.connection end - def enable_uuid_ossp - unless connection.extension_enabled?('uuid-ossp') - connection.enable_extension 'uuid-ossp' - connection.commit_db_transaction - end - - connection.reconnect! - end - def drop_table(name) connection.execute "drop table if exists #{name}" end @@ -43,14 +34,16 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase def test_change_column_default @connection.add_column :uuid_data_type, :thingy, :uuid, null: false, default: "uuid_generate_v1()" UUIDType.reset_column_information - column = UUIDType.columns.find { |c| c.name == 'thingy' } + column = UUIDType.columns_hash['thingy'] assert_equal "uuid_generate_v1()", column.default_function - + @connection.change_column :uuid_data_type, :thingy, :uuid, null: false, default: "uuid_generate_v4()" - + UUIDType.reset_column_information - column = UUIDType.columns.find { |c| c.name == 'thingy' } + column = UUIDType.columns_hash['thingy'] assert_equal "uuid_generate_v4()", column.default_function + ensure + UUIDType.reset_column_information end def test_data_type_of_uuid_types @@ -58,7 +51,6 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase assert_equal :uuid, column.type assert_equal "uuid", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end @@ -68,6 +60,43 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase assert_equal(nil, UUIDType.last.guid) end + def test_treat_invalid_uuid_as_nil + uuid = UUIDType.create! guid: 'foobar' + assert_equal(nil, uuid.guid) + end + + def test_invalid_uuid_dont_modify_before_type_cast + uuid = UUIDType.new guid: 'foobar' + assert_equal 'foobar', uuid.guid_before_type_cast + end + + def test_rfc_4122_regex + # Valid uuids + ['A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11', + '{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}', + 'a0eebc999c0b4ef8bb6d6bb9bd380a11', + 'a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11', + '{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}'].each do |valid_uuid| + uuid = UUIDType.new guid: valid_uuid + assert_not_nil uuid.guid + end + + # Invalid uuids + [['A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11'], + Hash.new, + 0, + 0.0, + true, + 'Z0000C99-9C0B-4EF8-BB6D-6BB9BD380A11', + '{a0eebc99-9c0b-4ef8-fb6d-6bb9bd380a11}', + 'a0eebc999r0b4ef8ab6d6bb9bd380a11', + 'a0ee-bc99------4ef8-bb6d-6bb9-bd38-0a11', + '{a0eebc99-bb6d6bb9-bd380a11}'].each do |invalid_uuid| + uuid = UUIDType.new guid: invalid_uuid + assert_nil uuid.guid + end + end + def test_uuid_formats ["A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11", "{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}", @@ -89,16 +118,32 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase end setup do - enable_uuid_ossp + enable_uuid_ossp!(connection) 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 + + # Create custom PostgreSQL function to generate UUIDs + # to test dumping tables which columns have defaults with custom functions + connection.execute <<-SQL + CREATE OR REPLACE FUNCTION my_uuid_generator() RETURNS uuid + AS $$ SELECT * FROM uuid_generate_v4() $$ + LANGUAGE SQL VOLATILE; + SQL + + # Create such a table with custom function as default value generator + connection.create_table('pg_uuids_2', id: :uuid, default: 'my_uuid_generator()') do |t| + t.string 'name' + t.uuid 'other_uuid_2', default: 'my_uuid_generator()' + end end teardown do drop_table "pg_uuids" + drop_table 'pg_uuids_2' + connection.execute 'DROP FUNCTION IF EXISTS my_uuid_generator();' end if ActiveRecord::Base.connection.supports_extensions? @@ -130,6 +175,13 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase 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 + + def test_schema_dumper_for_uuid_primary_key_with_custom_default + schema = StringIO.new + ActiveRecord::SchemaDumper.dump(connection, schema) + assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: "my_uuid_generator\(\)"/, schema.string) + assert_match(/t\.uuid "other_uuid_2", default: "my_uuid_generator\(\)"/, schema.string) + end end end @@ -137,7 +189,7 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase include PostgresqlUUIDHelper setup do - enable_uuid_ossp + enable_uuid_ossp!(connection) connection.create_table('pg_uuids', id: false) do |t| t.primary_key :id, :uuid, default: nil @@ -174,14 +226,14 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::TestCase end setup do - enable_uuid_ossp + enable_uuid_ossp!(connection) 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.references :uuid_post, type: :uuid t.string 'content' end end diff --git a/activerecord/test/cases/adapters/postgresql/xml_test.rb b/activerecord/test/cases/adapters/postgresql/xml_test.rb index ae299697b1..4165dd5ac9 100644 --- a/activerecord/test/cases/adapters/postgresql/xml_test.rb +++ b/activerecord/test/cases/adapters/postgresql/xml_test.rb @@ -1,8 +1,5 @@ # encoding: utf-8 - require 'cases/helper' -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' class PostgresqlXMLTest < ActiveRecord::TestCase class XmlDataType < ActiveRecord::Base @@ -14,13 +11,13 @@ class PostgresqlXMLTest < ActiveRecord::TestCase begin @connection.transaction do @connection.create_table('xml_data_type') do |t| - t.xml 'payload', default: {} + t.xml 'payload' end end rescue ActiveRecord::StatementInvalid skip "do not test on PG without xml" end - @column = XmlDataType.columns.find { |c| c.name == 'payload' } + @column = XmlDataType.columns_hash['payload'] end teardown do @@ -35,4 +32,17 @@ class PostgresqlXMLTest < ActiveRecord::TestCase @connection.execute %q|insert into xml_data_type (payload) VALUES(null)| assert_nil XmlDataType.first.payload end + + def test_round_trip + data = XmlDataType.new(payload: "<foo>bar</foo>") + assert_equal "<foo>bar</foo>", data.payload + data.save! + assert_equal "<foo>bar</foo>", data.reload.payload + end + + def test_update_all + data = XmlDataType.create! + XmlDataType.update_all(payload: "<bar>baz</bar>") + assert_equal "<bar>baz</bar>", data.reload.payload + end end diff --git a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb index b478db749d..13b754d226 100644 --- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb @@ -60,7 +60,6 @@ class CopyTableTest < ActiveRecord::TestCase assert_equal original_id.type, copied_id.type assert_equal original_id.sql_type, copied_id.sql_type assert_equal original_id.limit, copied_id.limit - assert_equal original_id.primary, copied_id.primary end end diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index ba89487838..ac8332e2fa 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -15,7 +15,7 @@ module ActiveRecord def test_type_cast_binary_encoding_without_logger @conn.extend(Module.new { def logger; end }) - column = Struct.new(:type, :name).new(:string, "foo") + column = Column.new(nil, nil, Type::String.new) binary = SecureRandom.hex expected = binary.dup.encode!(Encoding::UTF_8) assert_equal expected, @conn.type_cast(binary, column) @@ -47,13 +47,13 @@ module ActiveRecord end def test_type_cast_true - c = Column.new(nil, 1, 'int') + c = Column.new(nil, 1, Type::Integer.new) assert_equal 't', @conn.type_cast(true, nil) assert_equal 1, @conn.type_cast(true, c) end def test_type_cast_false - c = Column.new(nil, 1, 'int') + c = Column.new(nil, 1, Type::Integer.new) assert_equal 'f', @conn.type_cast(false, nil) assert_equal 0, @conn.type_cast(false, c) end @@ -61,16 +61,16 @@ module ActiveRecord def test_type_cast_string assert_equal '10', @conn.type_cast('10', nil) - c = Column.new(nil, 1, 'int') + c = Column.new(nil, 1, Type::Integer.new) assert_equal 10, @conn.type_cast('10', c) - c = Column.new(nil, 1, 'float') + c = Column.new(nil, 1, Type::Float.new) assert_equal 10.1, @conn.type_cast('10.1', c) - c = Column.new(nil, 1, 'binary') + c = Column.new(nil, 1, Type::Binary.new) assert_equal '10.1', @conn.type_cast('10.1', c) - c = Column.new(nil, 1, 'date') + c = Column.new(nil, 1, Type::Date.new) assert_equal '10.1', @conn.type_cast('10.1', c) end @@ -84,7 +84,7 @@ module ActiveRecord assert_raise(TypeError) { @conn.type_cast(obj, nil) } end - def test_quoted_id + def test_type_cast_object_which_responds_to_quoted_id quoted_id_obj = Class.new { def quoted_id "'zomg'" @@ -100,9 +100,16 @@ module ActiveRecord def quoted_id "'zomg'" end - } + }.new assert_raise(TypeError) { @conn.type_cast(quoted_id_obj, nil) } end + + def test_quoting_binary_strings + value = "hello".encode('ascii-8bit') + column = Column.new(nil, 1, SQLite3String.new) + + assert_equal "'hello'", @conn.quote(value, column) + end 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 2630a0f3a4..b2bf9480dd 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -57,10 +57,11 @@ module ActiveRecord end 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 - # even if the type is not valid. + # sqlite3 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 even if the + # type is not valid. def test_invalid_column assert @conn.valid_type?(:foobar) end @@ -339,7 +340,7 @@ module ActiveRecord column = @conn.columns('ex').find { |x| x.name == 'number' } - assert_equal 10, column.default + assert_equal '10', column.default end end @@ -416,6 +417,21 @@ module ActiveRecord assert @conn.respond_to?(:disable_extension) end + def test_statement_closed + db = SQLite3::Database.new(ActiveRecord::Base. + configurations['arunit']['database']) + statement = SQLite3::Statement.new(db, + 'CREATE TABLE statement_test (number integer not null)') + statement.stubs(:step).raises(SQLite3::BusyException, 'busy') + statement.stubs(:columns).once.returns([]) + statement.expects(:close).once + SQLite3::Statement.stubs(:new).returns(statement) + + assert_raises ActiveRecord::StatementInvalid do + @conn.exec_query 'select * from statement_test' + end + end + private def assert_logged logs diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index 811695938e..8700b20dee 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -17,6 +17,20 @@ if ActiveRecord::Base.connection.supports_migrations? ActiveRecord::SchemaMigration.delete_all rescue nil end + def test_has_no_primary_key + old_primary_key_prefix_type = ActiveRecord::Base.primary_key_prefix_type + ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore + assert_nil ActiveRecord::SchemaMigration.primary_key + + ActiveRecord::SchemaMigration.create_table + assert_difference "ActiveRecord::SchemaMigration.count", 1 do + ActiveRecord::SchemaMigration.create version: 12 + end + ensure + ActiveRecord::SchemaMigration.drop_table + ActiveRecord::Base.primary_key_prefix_type = old_primary_key_prefix_type + end + def test_schema_define ActiveRecord::Schema.define(:version => 7) do create_table :fruits do |t| @@ -34,6 +48,7 @@ if ActiveRecord::Base.connection.supports_migrations? def test_schema_define_w_table_name_prefix table_name = ActiveRecord::SchemaMigration.table_name + old_table_name_prefix = ActiveRecord::Base.table_name_prefix ActiveRecord::Base.table_name_prefix = "nep_" ActiveRecord::SchemaMigration.table_name = "nep_#{table_name}" ActiveRecord::Schema.define(:version => 7) do @@ -46,7 +61,7 @@ if ActiveRecord::Base.connection.supports_migrations? end assert_equal 7, ActiveRecord::Migrator::current_version ensure - ActiveRecord::Base.table_name_prefix = "" + ActiveRecord::Base.table_name_prefix = old_table_name_prefix ActiveRecord::SchemaMigration.table_name = table_name end @@ -66,5 +81,12 @@ if ActiveRecord::Base.connection.supports_migrations? end assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" } end + + def test_normalize_version + assert_equal "118", ActiveRecord::SchemaMigration.normalize_migration_number("0000118") + assert_equal "002", ActiveRecord::SchemaMigration.normalize_migration_number("2") + assert_equal "017", ActiveRecord::SchemaMigration.normalize_migration_number("0017") + assert_equal "20131219224947", ActiveRecord::SchemaMigration.normalize_migration_number("20131219224947") + end end end diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 3b484a0d64..25555bd75c 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -369,6 +369,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_queries(2) { line_item.update amount: 10 } end + def test_belongs_to_with_touch_option_on_empty_update + line_item = LineItem.create! + Invoice.create!(line_items: [line_item]) + + assert_queries(0) { line_item.save } + end + def test_belongs_to_with_touch_option_on_destroy line_item = LineItem.create! Invoice.create!(line_items: [line_item]) @@ -563,6 +570,19 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert companies(:first_client).readonly_firm.readonly? end + def test_test_polymorphic_assignment_foreign_key_type_string + comment = Comment.first + comment.author = Author.first + comment.resource = Member.first + comment.save + + assert_equal Comment.all.to_a, + Comment.includes(:author).to_a + + assert_equal Comment.all.to_a, + Comment.includes(:resource).to_a + end + def test_polymorphic_assignment_foreign_type_field_updating # should update when assigning a saved record sponsor = Sponsor.new @@ -767,8 +787,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase post = posts(:welcome) comment = comments(:greetings) - assert_difference lambda { post.reload.taggings_count }, -1 do - assert_difference 'comment.reload.taggings_count', +1 do + assert_difference lambda { post.reload.tags_count }, -1 do + assert_difference 'comment.reload.tags_count', +1 do tagging.taggable = comment end end @@ -915,3 +935,14 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 1, Column.count end end + +class BelongsToWithForeignKeyTest < ActiveRecord::TestCase + fixtures :authors, :author_addresses + + def test_destroy_linked_models + address = AuthorAddress.create! + author = Author.create! name: "Author", author_address_id: address.id + + author.destroy! + end +end diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb index 968f36e92c..5b7e462f64 100644 --- a/activerecord/test/cases/associations/callbacks_test.rb +++ b/activerecord/test/cases/associations/callbacks_test.rb @@ -159,7 +159,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase activerecord.reload assert activerecord.developers_with_callbacks.size == 2 end - log_array = activerecord.developers_with_callbacks.flat_map {|d| ["before_removing#{d.id}","after_removing#{d.id}"]}.sort + activerecord.developers_with_callbacks.flat_map {|d| ["before_removing#{d.id}","after_removing#{d.id}"]}.sort assert activerecord.developers_with_callbacks.clear assert_predicate activerecord.developers_log, :empty? end diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index 71c0609df5..51d8e0523e 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -35,9 +35,9 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase def test_eager_association_loading_with_hmt_does_not_table_name_collide_when_joining_associations assert_nothing_raised do - Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).to_a + Author.joins(:posts).eager_load(:comments).where(:posts => {:tags_count => 1}).to_a end - authors = Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).to_a + authors = Author.joins(:posts).eager_load(:comments).where(:posts => {:tags_count => 1}).to_a assert_equal 1, assert_no_queries { authors.size } assert_equal 10, assert_no_queries { authors[0].comments.size } end diff --git a/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb b/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb new file mode 100644 index 0000000000..48f7ddbe83 --- /dev/null +++ b/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb @@ -0,0 +1,26 @@ +require "cases/helper" + +class DeprecatedCounterCacheOnHasManyThroughTest < ActiveRecord::TestCase + class Post < ActiveRecord::Base + has_many :taggings, as: :taggable + has_many :tags, through: :taggings + end + + class Tagging < ActiveRecord::Base + belongs_to :taggable, polymorphic: true + belongs_to :tag + end + + class Tag < ActiveRecord::Base + end + + test "counter caches are updated in the database if the belongs_to association doesn't specify a counter cache" do + post = Post.create!(title: 'Hello', body: 'World!') + assert_deprecated { post.tags << Tag.create!(name: 'whatever') } + + assert_equal 1, post.tags.size + assert_equal 1, post.tags_count + assert_equal 1, post.reload.tags.size + assert_equal 1, post.reload.tags_count + end +end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 7eaa5adc86..21912fdf0f 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -534,21 +534,13 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_has_many_and_limit_and_conditions - if current_adapter?(:OpenBaseAdapter) - posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => "FETCHBLOB(posts.body) = 'hello'", :order => "posts.id").to_a - else - posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => "posts.body = 'hello'", :order => "posts.id").to_a - end + posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => "posts.body = 'hello'", :order => "posts.id").to_a assert_equal 2, posts.size assert_equal [4,5], posts.collect { |p| p.id } end def test_eager_with_has_many_and_limit_and_conditions_array - if current_adapter?(:OpenBaseAdapter) - posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => [ "FETCHBLOB(posts.body) = ?", 'hello' ], :order => "posts.id").to_a - else - posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => [ "posts.body = ?", 'hello' ], :order => "posts.id").to_a - end + posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => [ "posts.body = ?", 'hello' ], :order => "posts.id").to_a assert_equal 2, posts.size assert_equal [4,5], posts.collect { |p| p.id } end @@ -826,11 +818,15 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_preload_with_interpolation - post = Post.includes(:comments_with_interpolated_conditions).find(posts(:welcome).id) - assert_equal [comments(:greetings)], post.comments_with_interpolated_conditions + assert_deprecated do + post = Post.includes(:comments_with_interpolated_conditions).find(posts(:welcome).id) + assert_equal [comments(:greetings)], post.comments_with_interpolated_conditions + end - post = Post.joins(:comments_with_interpolated_conditions).find(posts(:welcome).id) - assert_equal [comments(:greetings)], post.comments_with_interpolated_conditions + assert_deprecated do + post = Post.joins(:comments_with_interpolated_conditions).find(posts(:welcome).id) + assert_equal [comments(:greetings)], post.comments_with_interpolated_conditions + end end def test_polymorphic_type_condition @@ -936,13 +932,7 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_count_with_include - if current_adapter?(:SybaseAdapter) - assert_equal 3, authors(:david).posts_with_comments.where("len(comments.body) > 15").references(:comments).count - elsif current_adapter?(:OpenBaseAdapter) - assert_equal 3, authors(:david).posts_with_comments.where("length(FETCHBLOB(comments.body)) > 15").references(:comments).count - else - assert_equal 3, authors(:david).posts_with_comments.where("length(comments.body) > 15").references(:comments).count - end + assert_equal 3, authors(:david).posts_with_comments.where("length(comments.body) > 15").references(:comments).count end def test_load_with_sti_sharing_association @@ -1177,6 +1167,13 @@ class EagerAssociationTest < ActiveRecord::TestCase ) end + test "deep preload" do + post = Post.preload(author: :posts, comments: :post).first + + assert_predicate post.author.association(:posts), :loaded? + assert_predicate post.comments.first.association(:post), :loaded? + end + test "preloading does not cache has many association subset when preloaded with a through association" do author = Author.includes(:comments_with_order_and_conditions, :posts).first assert_no_queries { assert_equal 2, author.comments_with_order_and_conditions.size } @@ -1206,6 +1203,15 @@ class EagerAssociationTest < ActiveRecord::TestCase end end + test "preloading the same association twice works" do + Member.create! + members = Member.preload(:current_membership).includes(current_membership: :club).all.to_a + assert_no_queries { + members_with_membership = members.select(&:current_membership) + assert_equal 3, members_with_membership.map(&:current_membership).map(&:club).size + } + end + test "preloading with a polymorphic association and using the existential predicate" do assert_equal authors(:david), authors(:david).essays.includes(:writer).first.writer @@ -1232,4 +1238,56 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal 2, author.posts.size } end + + test "including association based on sql condition and no database column" do + assert_equal pets(:parrot), Owner.including_last_pet.first.last_pet + end + + test "include instance dependent associations is deprecated" do + message = "association scope 'posts_with_signature' is" + assert_deprecated message do + begin + Author.includes(:posts_with_signature).to_a + rescue NoMethodError + # it's expected that preloading of this association fails + end + end + + assert_deprecated message do + Author.preload(:posts_with_signature).to_a rescue NoMethodError + end + + assert_deprecated message do + Author.eager_load(:posts_with_signature).to_a + end + end + + test "preloading readonly association" do + # has-one + firm = Firm.where(id: "1").preload(:readonly_account).first! + assert firm.readonly_account.readonly? + + # has_and_belongs_to_many + project = Project.where(id: "2").preload(:readonly_developers).first! + assert project.readonly_developers.first.readonly? + + # has-many :through + david = Author.where(id: "1").preload(:readonly_comments).first! + assert david.readonly_comments.first.readonly? + end + + test "eager-loading readonly association" do + skip "eager_load does not yet preserve readonly associations" + # has-one + firm = Firm.where(id: "1").eager_load(:readonly_account).first! + assert firm.readonly_account.readonly? + + # has_and_belongs_to_many + project = Project.where(id: "2").eager_load(:readonly_developers).first! + assert project.readonly_developers.first.readonly? + + # has-many :through + david = Author.where(id: "1").eager_load(:readonly_comments).first! + assert david.readonly_comments.first.readonly? + 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 5d33634da2..cc58a4a1a2 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 @@ -22,6 +22,9 @@ require 'models/sponsor' require 'models/country' require 'models/treaty' require 'models/vertex' +require 'models/publisher' +require 'models/publisher/article' +require 'models/publisher/magazine' require 'active_support/core_ext/string/conversions' class ProjectWithAfterCreateHook < ActiveRecord::Base @@ -67,6 +70,14 @@ class DeveloperWithSymbolsForKeys < ActiveRecord::Base :foreign_key => "developer_id" end +class SubDeveloper < Developer + self.table_name = 'developers' + has_and_belongs_to_many :special_projects, + :join_table => 'developers_projects', + :foreign_key => "project_id", + :association_foreign_key => "developer_id" +end + class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects, :parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings @@ -83,6 +94,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase country.treaties << treaty end + def test_marshal_dump + post = posts :welcome + preloaded = Post.includes(:categories).find post.id + assert_equal preloaded, Marshal.load(Marshal.dump(preloaded)) + end + def test_should_property_quote_string_primary_keys setup_data_for_habtm_case @@ -217,6 +234,24 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal developers(:poor_jamis, :jamis, :david), projects(:active_record).developers end + def test_habtm_collection_size_from_build + devel = Developer.create("name" => "Fred Wu") + devel.projects << Project.create("name" => "Grimetime") + devel.projects.build + + assert_equal 2, devel.projects.size + end + + def test_habtm_collection_size_from_params + devel = Developer.new({ + projects_attributes: { + '0' => {} + } + }) + + assert_equal 1, devel.projects.size + end + def test_build devel = Developer.find(1) proj = assert_no_queries { devel.projects.build("name" => "Projekt") } @@ -787,7 +822,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal [], Pirate.where(id: redbeard.id) end - test "has and belongs to many associations on new records use null relations" do + def test_has_and_belongs_to_many_associations_on_new_records_use_null_relations projects = Developer.new.projects assert_no_queries do assert_equal [], projects @@ -824,4 +859,28 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_custom_join_table assert_equal 'edges', Vertex.reflect_on_association(:sources).join_table end + + def test_has_and_belongs_to_many_in_a_namespaced_model_pointing_to_a_namespaced_model + magazine = Publisher::Magazine.create + article = Publisher::Article.create + magazine.articles << article + magazine.save + + assert_includes magazine.articles, article + end + + def test_has_and_belongs_to_many_in_a_namespaced_model_pointing_to_a_non_namespaced_model + article = Publisher::Article.create + tag = Tag.create + article.tags << tag + article.save + + assert_includes article.tags, tag + end + + def test_redefine_habtm + child = SubDeveloper.new("name" => "Aredridel") + child.special_projects << SpecialProject.new("name" => "Special Project") + assert child.save, 'child object should be saved' + end end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 2f5c9d6e1b..fe961e871c 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -26,6 +26,8 @@ require 'models/reference' require 'models/job' require 'models/college' require 'models/student' +require 'models/pirate' +require 'models/ship' class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCase fixtures :authors, :posts, :comments @@ -34,7 +36,7 @@ class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCa author = authors(:david) # this can fail on adapters which require ORDER BY expressions to be included in the SELECT expression # if the reorder clauses are not correctly handled - assert author.posts_with_comments_sorted_by_comment_id.where('comments.id > 0').reorder('posts.comments_count DESC', 'posts.taggings_count DESC').last + assert author.posts_with_comments_sorted_by_comment_id.where('comments.id > 0').reorder('posts.comments_count DESC', 'posts.tags_count DESC').last end end @@ -43,12 +45,18 @@ class HasManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :categories, :companies, :developers, :projects, :developers_projects, :topics, :authors, :comments, :people, :posts, :readers, :taggings, :cars, :essays, - :categorizations, :jobs + :categorizations, :jobs, :tags def setup Client.destroyed_client_ids.clear end + def test_sti_subselect_count + tag = Tag.first + len = Post.tagged_with(tag.id).limit(10).size + assert_operator len, :>, 0 + end + def test_anonymous_has_many developer = Class.new(ActiveRecord::Base) { self.table_name = 'developers' @@ -764,6 +772,36 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal topic.replies.to_a.size, topic.replies_count end + def test_counter_cache_updates_in_memory_after_concat + topic = Topic.create title: "Zoom-zoom-zoom" + + topic.replies << Reply.create(title: "re: zoom", content: "speedy quick!") + assert_equal 1, topic.replies_count + assert_equal 1, topic.replies.size + assert_equal 1, topic.reload.replies.size + end + + def test_counter_cache_updates_in_memory_after_create + topic = Topic.create title: "Zoom-zoom-zoom" + + topic.replies.create!(title: "re: zoom", content: "speedy quick!") + assert_equal 1, topic.replies_count + assert_equal 1, topic.replies.size + assert_equal 1, topic.reload.replies.size + end + + def test_counter_cache_updates_in_memory_after_create_with_array + topic = Topic.create title: "Zoom-zoom-zoom" + + topic.replies.create!([ + { title: "re: zoom", content: "speedy quick!" }, + { title: "re: zoom 2", content: "OMG lol!" }, + ]) + assert_equal 2, topic.replies_count + assert_equal 2, topic.replies.size + assert_equal 2, topic.reload.replies.size + end + def test_pushing_association_updates_counter_cache topic = Topic.order("id ASC").first reply = Reply.create! @@ -776,14 +814,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_deleting_updates_counter_cache_without_dependent_option post = posts(:welcome) - assert_difference "post.reload.taggings_count", -1 do + assert_difference "post.reload.tags_count", -1 do post.taggings.delete(post.taggings.first) end end def test_deleting_updates_counter_cache_with_dependent_delete_all post = posts(:welcome) - post.update_columns(taggings_with_delete_all_count: post.taggings_count) + post.update_columns(taggings_with_delete_all_count: post.tags_count) assert_difference "post.reload.taggings_with_delete_all_count", -1 do post.taggings_with_delete_all.delete(post.taggings_with_delete_all.first) @@ -792,13 +830,20 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_deleting_updates_counter_cache_with_dependent_destroy post = posts(:welcome) - post.update_columns(taggings_with_destroy_count: post.taggings_count) + post.update_columns(taggings_with_destroy_count: post.tags_count) assert_difference "post.reload.taggings_with_destroy_count", -1 do post.taggings_with_destroy.delete(post.taggings_with_destroy.first) end end + def test_calling_empty_with_counter_cache + post = posts(:welcome) + assert_queries(0) do + assert_not post.comments.empty? + end + end + def test_custom_named_counter_cache topic = topics(:first) @@ -1877,4 +1922,23 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end end + + test 'passes custom context validation to validate children' do + pirate = FamousPirate.new + pirate.famous_ships << ship = FamousShip.new + + assert pirate.valid? + assert_not pirate.valid?(:conference) + assert_equal "can't be blank", ship.errors[:name].first + end + + test 'association with instance dependent scope' do + bob = authors(:bob) + Post.create!(title: "signed post by bob", body: "stuff", author: authors(:bob)) + Post.create!(title: "anonymous post", body: "more stuff", author: authors(:bob)) + assert_equal ["misc post by bob", "other post by bob", + "signed post by bob"], bob.posts_with_signature.map(&:title).sort + + assert_equal [], authors(:david).posts_with_signature.map(&:title) + 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 2e62189e7a..a85e020f0c 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -330,6 +330,19 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert post.single_people.include?(person) end + def test_both_parent_ids_set_when_saving_new + post = Post.new(title: 'Hello', body: 'world') + person = Person.new(first_name: 'Sean') + + post.people = [person] + post.save + + assert post.id + assert person.id + assert_equal post.id, post.readers.first.post_id + assert_equal person.id, post.readers.first.person_id + end + def test_delete_association assert_queries(2){posts(:welcome);people(:michael); } @@ -476,7 +489,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase post = posts(:welcome) tag = post.tags.create!(:name => 'doomed') - assert_difference ['post.reload.taggings_count', 'post.reload.tags_count'], -1 do + assert_difference ['post.reload.tags_count'], -1 do posts(:welcome).tags.delete(tag) end end @@ -486,7 +499,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase tag = post.tags.create!(:name => 'doomed') post.update_columns(tags_with_destroy_count: post.tags.count) - assert_difference ['post.reload.taggings_count', 'post.reload.tags_with_destroy_count'], -1 do + assert_difference ['post.reload.tags_with_destroy_count'], -1 do posts(:welcome).tags_with_destroy.delete(tag) end end @@ -496,7 +509,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase tag = post.tags.create!(:name => 'doomed') post.update_columns(tags_with_nullify_count: post.tags.count) - assert_no_difference 'post.reload.taggings_count' do + assert_no_difference 'post.reload.tags_count' do assert_difference 'post.reload.tags_with_nullify_count', -1 do posts(:welcome).tags_with_nullify.delete(tag) end @@ -511,14 +524,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase tag.tagged_posts = [] post.reload - assert_equal(post.taggings.count, post.taggings_count) + assert_equal(post.taggings.count, post.tags_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 + assert_difference 'post.reload.tags_count', -1 do tag.tagged_posts.destroy(post) end end @@ -1126,4 +1139,31 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal 2, post.lazy_readers_unscope_skimmers.to_a.size assert_equal 2, post.lazy_people_unscope_skimmers.to_a.size end + + def test_has_many_through_add_with_sti_middle_relation + club = SuperClub.create!(name: 'Fight Club') + member = Member.create!(name: 'Tyler Durden') + + club.members << member + assert_equal 1, SuperMembership.where(member_id: member.id, club_id: club.id).count + end + + class ClubWithCallbacks < ActiveRecord::Base + self.table_name = 'clubs' + after_create :add_a_member + + has_many :memberships, inverse_of: :club, foreign_key: :club_id + has_many :members, through: :memberships + + def add_a_member + members << Member.last + end + end + + def test_has_many_with_callback_before_association + Member.create! + club = ClubWithCallbacks.create! + + assert_equal 1, club.reload.memberships.count + end end diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index a2725441b3..089cb0a3a2 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -45,6 +45,20 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase assert_equal clubs(:moustache_club), new_member.club end + def test_creating_association_sets_both_parent_ids_for_new + member = Member.new(name: 'Sean Griffin') + club = Club.new(name: 'Da Club') + + member.club = club + + member.save! + + assert member.id + assert club.id + assert_equal member.id, member.current_membership.member_id + assert_equal club.id, member.current_membership.club_id + end + def test_replace_target_record new_club = Club.create(:name => "Marx Bros") @member.club = new_club diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index b23517b2f9..07cf65a760 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -126,4 +126,14 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase categories = author.categories.includes(:special_categorizations).references(:special_categorizations).to_a assert_equal 2, categories.size end + + test "the correct records are loaded when including an aliased association" do + author = Author.create! name: "Jon" + author.categories.create! name: 'Not Special' + author.special_categories.create! name: 'Special' + + categories = author.categories.eager_load(:special_categorizations).order(:name).to_a + assert_equal 0, categories.first.special_categorizations.size + assert_equal 1, categories.second.special_categorizations.size + end end diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 893030345f..60df4e14dd 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -100,6 +100,17 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase assert_respond_to club_reflection, :has_inverse? assert !club_reflection.has_inverse?, "A has_many_through association should not find an inverse automatically" end + + def test_polymorphic_relationships_should_still_not_have_inverses_when_non_polymorphic_relationship_has_the_same_name + man_reflection = Man.reflect_on_association(:polymorphic_face_without_inverse) + face_reflection = Face.reflect_on_association(:man) + + assert_respond_to face_reflection, :has_inverse? + assert face_reflection.has_inverse?, "For this test, the non-polymorphic association must have an inverse" + + assert_respond_to man_reflection, :has_inverse? + assert !man_reflection.has_inverse?, "The target of a polymorphic association should not find an inverse automatically" + end end class InverseAssociationTests < ActiveRecord::TestCase @@ -333,7 +344,7 @@ class InverseHasManyTests < ActiveRecord::TestCase def test_parent_instance_should_be_shared_within_create_block_of_new_child man = Man.first - interest = man.interests.build do |i| + interest = man.interests.create do |i| assert i.man.equal?(man), "Man of child should be the same instance as a parent" end assert interest.man.equal?(man), "Man of the child should still be the same instance as a parent" diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index aabeea025f..cace7ba142 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -326,11 +326,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_belongs_to_polymorphic_with_counter_cache - assert_equal 1, posts(:welcome)[:taggings_count] + assert_equal 1, posts(:welcome)[:tags_count] tagging = posts(:welcome).taggings.create(:tag => tags(:general)) - assert_equal 2, posts(:welcome, :reload)[:taggings_count] + assert_equal 2, posts(:welcome, :reload)[:tags_count] tagging.destroy - assert_equal 1, posts(:welcome, :reload)[:taggings_count] + assert_equal 1, posts(:welcome, :reload)[:tags_count] end def test_unavailable_through_reflection @@ -489,7 +489,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase message = "Expected a Tag in tags collection, got #{wrong.class}.") assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, message = "Expected a Tagging in taggings collection, got #{wrong.class}.") - assert_equal(count + 1, post_thinking.tags.size) + assert_equal(count + 1, post_thinking.reload.tags.size) assert_equal(count + 1, post_thinking.tags(true).size) assert_kind_of Tag, post_thinking.tags.create!(:name => 'foo') @@ -497,7 +497,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase message = "Expected a Tag in tags collection, got #{wrong.class}.") assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, message = "Expected a Tagging in taggings collection, got #{wrong.class}.") - assert_equal(count + 2, post_thinking.tags.size) + assert_equal(count + 2, post_thinking.reload.tags.size) assert_equal(count + 2, post_thinking.tags(true).size) assert_nothing_raised { post_thinking.tags.concat(Tag.create!(:name => 'abc'), Tag.create!(:name => 'def')) } @@ -505,7 +505,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase message = "Expected a Tag in tags collection, got #{wrong.class}.") assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, message = "Expected a Tagging in taggings collection, got #{wrong.class}.") - assert_equal(count + 4, post_thinking.tags.size) + assert_equal(count + 4, post_thinking.reload.tags.size) assert_equal(count + 4, post_thinking.tags(true).size) # Raises if the wrong reflection name is used to set the Edge belongs_to @@ -554,34 +554,35 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_delete_associate_when_deleting_from_has_many_through count = posts(:thinking).tags.count - tags_before = posts(:thinking).tags + tags_before = posts(:thinking).tags.sort tag = Tag.create!(:name => 'doomed') post_thinking = posts(:thinking) post_thinking.tags << tag assert_equal(count + 1, post_thinking.taggings(true).size) - assert_equal(count + 1, post_thinking.tags(true).size) + assert_equal(count + 1, post_thinking.reload.tags(true).size) + assert_not_equal(tags_before, post_thinking.tags.sort) assert_nothing_raised { post_thinking.tags.delete(tag) } assert_equal(count, post_thinking.tags.size) assert_equal(count, post_thinking.tags(true).size) assert_equal(count, post_thinking.taggings(true).size) - assert_equal(tags_before.sort, post_thinking.tags.sort) + assert_equal(tags_before, post_thinking.tags.sort) end def test_delete_associate_when_deleting_from_has_many_through_with_multiple_tags count = posts(:thinking).tags.count - tags_before = posts(:thinking).tags + tags_before = posts(:thinking).tags.sort doomed = Tag.create!(:name => 'doomed') doomed2 = Tag.create!(:name => 'doomed2') quaked = Tag.create!(:name => 'quaked') post_thinking = posts(:thinking) post_thinking.tags << doomed << doomed2 - assert_equal(count + 2, post_thinking.tags(true).size) + assert_equal(count + 2, post_thinking.reload.tags(true).size) assert_nothing_raised { post_thinking.tags.delete(doomed, doomed2, quaked) } assert_equal(count, post_thinking.tags.size) assert_equal(count, post_thinking.tags(true).size) - assert_equal(tags_before.sort, post_thinking.tags.sort) + assert_equal(tags_before, post_thinking.tags.sort) end def test_deleting_junk_from_has_many_through_should_raise_type_mismatch diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index 8ef351cda8..31b68c940e 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -130,7 +130,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase def test_has_many_through_has_one_through_with_has_one_source_reflection_preload members = assert_queries(4) { Member.includes(:nested_sponsors).to_a } mustache = sponsors(:moustache_club_sponsor_for_groucho) - assert_no_queries do + assert_no_queries(ignore_none: false) do assert_equal [mustache], members.first.nested_sponsors end end @@ -153,6 +153,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase end def test_has_many_through_has_one_with_has_many_through_source_reflection_preload + ActiveRecord::Base.connection.table_alias_length # preheat cache members = assert_queries(4) { Member.includes(:organization_member_details).to_a.sort_by(&:id) } groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy) diff --git a/activerecord/test/cases/associations/required_test.rb b/activerecord/test/cases/associations/required_test.rb new file mode 100644 index 0000000000..a6934a056e --- /dev/null +++ b/activerecord/test/cases/associations/required_test.rb @@ -0,0 +1,82 @@ +require "cases/helper" + +class RequiredAssociationsTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class Parent < ActiveRecord::Base + end + + class Child < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table :parents, force: true + @connection.create_table :children, force: true do |t| + t.belongs_to :parent + end + end + + teardown do + @connection.execute("DROP TABLE IF EXISTS parents") + @connection.execute("DROP TABLE IF EXISTS children") + end + + test "belongs_to associations are not required by default" do + model = subclass_of(Child) do + belongs_to :parent, inverse_of: false, + class_name: "RequiredAssociationsTest::Parent" + end + + assert model.new.save + assert model.new(parent: Parent.new).save + end + + test "required belongs_to associations have presence validated" do + model = subclass_of(Child) do + belongs_to :parent, required: true, inverse_of: false, + class_name: "RequiredAssociationsTest::Parent" + end + + record = model.new + assert_not record.save + assert_equal ["Parent can't be blank"], record.errors.full_messages + + record.parent = Parent.new + assert record.save + end + + test "has_one associations are not required by default" do + model = subclass_of(Parent) do + has_one :child, inverse_of: false, + class_name: "RequiredAssociationsTest::Child" + end + + assert model.new.save + assert model.new(child: Child.new).save + end + + test "required has_one associations have presence validated" do + model = subclass_of(Parent) do + has_one :child, required: true, inverse_of: false, + class_name: "RequiredAssociationsTest::Child" + end + + record = model.new + assert_not record.save + assert_equal ["Child can't be blank"], record.errors.full_messages + + record.child = Child.new + assert record.save + end + + private + + def subclass_of(klass, &block) + subclass = Class.new(klass, &block) + def subclass.name + superclass.name + end + subclass + end +end diff --git a/activerecord/test/cases/attribute_decorators_test.rb b/activerecord/test/cases/attribute_decorators_test.rb new file mode 100644 index 0000000000..cbc2c4e5d7 --- /dev/null +++ b/activerecord/test/cases/attribute_decorators_test.rb @@ -0,0 +1,124 @@ +require 'cases/helper' + +module ActiveRecord + class AttributeDecoratorsTest < ActiveRecord::TestCase + class Model < ActiveRecord::Base + self.table_name = 'attribute_decorators_model' + end + + class StringDecorator < SimpleDelegator + def initialize(delegate, decoration = "decorated!") + @decoration = decoration + super(delegate) + end + + def type_cast_from_user(value) + "#{super} #{@decoration}" + end + + alias type_cast_from_database type_cast_from_user + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table :attribute_decorators_model, force: true do |t| + t.string :a_string + end + end + + teardown do + return unless @connection + @connection.execute 'DROP TABLE IF EXISTS attribute_decorators_model' + Model.attribute_type_decorations.clear + Model.reset_column_information + end + + test "attributes can be decorated" do + model = Model.new(a_string: 'Hello') + assert_equal 'Hello', model.a_string + + Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } + + model = Model.new(a_string: 'Hello') + assert_equal 'Hello decorated!', model.a_string + end + + test "decoration does not eagerly load existing columns" do + assert_no_queries do + Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } + end + end + + test "undecorated columns are not touched" do + Model.attribute :another_string, Type::String.new, default: 'something or other' + Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } + + assert_equal 'something or other', Model.new.another_string + end + + test "decorators can be chained" do + Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } + Model.decorate_attribute_type(:a_string, :other) { |t| StringDecorator.new(t) } + + model = Model.new(a_string: 'Hello!') + + assert_equal 'Hello! decorated! decorated!', model.a_string + end + + test "decoration of the same type multiple times is idempotent" do + Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } + Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } + + model = Model.new(a_string: 'Hello') + assert_equal 'Hello decorated!', model.a_string + end + + test "decorations occur in order of declaration" do + Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } + Model.decorate_attribute_type(:a_string, :other) do |type| + StringDecorator.new(type, 'decorated again!') + end + + model = Model.new(a_string: 'Hello!') + + assert_equal 'Hello! decorated! decorated again!', model.a_string + end + + test "decorating attributes does not modify parent classes" do + Model.attribute :another_string, Type::String.new, default: 'whatever' + Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } + child_class = Class.new(Model) + child_class.decorate_attribute_type(:another_string, :test) { |t| StringDecorator.new(t) } + child_class.decorate_attribute_type(:a_string, :other) { |t| StringDecorator.new(t) } + + model = Model.new(a_string: 'Hello!') + child = child_class.new(a_string: 'Hello!') + + assert_equal 'Hello! decorated!', model.a_string + assert_equal 'whatever', model.another_string + assert_equal 'Hello! decorated! decorated!', child.a_string + assert_equal 'whatever decorated!', child.another_string + end + + class Multiplier < SimpleDelegator + def type_cast_from_user(value) + return if value.nil? + value * 2 + end + alias type_cast_from_database type_cast_from_user + end + + test "decorating with a proc" do + Model.attribute :an_int, Type::Integer.new + type_is_integer = proc { |_, type| type.type == :integer } + Model.decorate_matching_attribute_types type_is_integer, :multiplier do |type| + Multiplier.new(type) + end + + model = Model.new(a_string: 'whatever', an_int: 1) + + assert_equal 'whatever', model.a_string + assert_equal 2, model.an_int + end + end +end diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb index c0659fddef..4741ee8799 100644 --- a/activerecord/test/cases/attribute_methods/read_test.rb +++ b/activerecord/test/cases/attribute_methods/read_test.rb @@ -12,6 +12,7 @@ module ActiveRecord @klass = Class.new do def self.superclass; Base; end def self.base_class; self; end + def self.decorate_matching_attribute_types(*); end include ActiveRecord::AttributeMethods diff --git a/activerecord/test/cases/attribute_methods/serialization_test.rb b/activerecord/test/cases/attribute_methods/serialization_test.rb deleted file mode 100644 index 75de773961..0000000000 --- a/activerecord/test/cases/attribute_methods/serialization_test.rb +++ /dev/null @@ -1,29 +0,0 @@ -require "cases/helper" - -module ActiveRecord - module AttributeMethods - class SerializationTest < ActiveSupport::TestCase - class FakeColumn < Struct.new(:name) - def type; :integer; end - def type_cast(s); "#{s}!"; end - end - - class NullCoder - def load(v); v; end - end - - def test_type_cast_serialized_value - value = Serialization::Attribute.new(NullCoder.new, "Hello world", :serialized) - type = Serialization::Type.new(FakeColumn.new) - assert_equal "Hello world!", type.type_cast(value) - end - - def test_type_cast_unserialized_value - value = Serialization::Attribute.new(nil, "Hello world", :unserialized) - type = Serialization::Type.new(FakeColumn.new) - type.type_cast(value) - assert_equal "Hello world", type.type_cast(value) - end - end - end -end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 38e93288e4..ab67cf4085 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -143,7 +143,11 @@ class AttributeMethodsTest < ActiveRecord::TestCase # Syck calls respond_to? before actually calling initialize def test_respond_to_with_allocated_object - topic = Topic.allocate + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'topics' + end + + topic = klass.allocate assert !topic.respond_to?("nothingness") assert !topic.respond_to?(:nothingness) assert_respond_to topic, "title" @@ -253,6 +257,15 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal @loaded_fixtures['computers']['workstation'].to_hash, Computer.first.attributes end + def test_attributes_without_primary_key + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'developers_projects' + end + + assert_equal klass.column_names, klass.new.attributes.keys + assert_not klass.new.attributes.key?('id') + end + def test_hashes_not_mangled new_topic = { :title => "New Topic" } new_topic_values = { :title => "AnotherTopic" } @@ -299,6 +312,8 @@ class AttributeMethodsTest < ActiveRecord::TestCase computer = Computer.select('id').first assert_raises(ActiveModel::MissingAttributeError) { computer[:developer] } assert_raises(ActiveModel::MissingAttributeError) { computer[:extendedWarranty] } + assert_raises(ActiveModel::MissingAttributeError) { computer[:no_column_exists] = 'Hello!' } + assert_nothing_raised { computer[:developer] = 'Hello!' } end def test_read_attribute_when_false @@ -449,10 +464,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase end def test_declared_suffixed_attribute_method_affects_respond_to_and_method_missing - topic = @target.new(:title => 'Budget') %w(_default _title_default _it! _candidate= able?).each do |suffix| @target.class_eval "def attribute#{suffix}(*args) args end" @target.attribute_method_suffix suffix + topic = @target.new(:title => 'Budget') meth = "title#{suffix}" assert topic.respond_to?(meth) @@ -463,10 +478,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase end def test_declared_affixed_attribute_method_affects_respond_to_and_method_missing - topic = @target.new(:title => 'Budget') [['mark_', '_for_update'], ['reset_', '!'], ['default_', '_value?']].each do |prefix, suffix| @target.class_eval "def #{prefix}attribute#{suffix}(*args) args end" @target.attribute_method_affix({ :prefix => prefix, :suffix => suffix }) + topic = @target.new(:title => 'Budget') meth = "#{prefix}title#{suffix}" assert topic.respond_to?(meth) @@ -515,43 +530,17 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - def test_only_time_related_columns_are_meant_to_be_cached_by_default - expected = %w(datetime timestamp time date).sort - assert_equal expected, ActiveRecord::Base.attribute_types_cached_by_default.map(&:to_s).sort - end - - def test_declaring_attributes_as_cached_adds_them_to_the_attributes_cached_by_default - default_attributes = Topic.cached_attributes - Topic.cache_attributes :replies_count - expected = default_attributes + ["replies_count"] - assert_equal expected.sort, Topic.cached_attributes.sort - Topic.instance_variable_set "@cached_attributes", nil - end - - def test_cacheable_columns_are_actually_cached - assert_equal cached_columns.sort, Topic.cached_attributes.sort - end - - def test_accessing_cached_attributes_caches_the_converted_values_and_nothing_else - t = topics(:first) - cache = t.instance_variable_get "@attributes_cache" - - assert_not_nil cache - assert cache.empty? + def test_deprecated_cache_attributes + assert_deprecated do + Topic.cache_attributes :replies_count + end - all_columns = Topic.columns.map(&:name) - uncached_columns = all_columns - cached_columns + assert_deprecated do + Topic.cached_attributes + end - all_columns.each do |attr_name| - attribute_gets_cached = Topic.cache_attribute?(attr_name) - val = t.send attr_name unless attr_name == "type" - if attribute_gets_cached - assert cached_columns.include?(attr_name) - assert_equal val, cache[attr_name] - else - assert uncached_columns.include?(attr_name) - assert !cache.include?(attr_name) - end + assert_deprecated do + Topic.cache_attribute? :replies_count end end @@ -843,6 +832,49 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal !real_topic.title?, klass.find(real_topic.id).title? end + def test_calling_super_when_parent_does_not_define_method_raises_error + klass = new_topic_like_ar_class do + def some_method_that_is_not_on_super + super + end + end + + assert_raise(NoMethodError) do + klass.new.some_method_that_is_not_on_super + end + end + + def test_attribute_method? + assert @target.attribute_method?(:title) + assert @target.attribute_method?(:title=) + assert_not @target.attribute_method?(:wibble) + end + + def test_attribute_method_returns_false_if_table_does_not_exist + @target.table_name = 'wibble' + assert_not @target.attribute_method?(:title) + end + + def test_attribute_names_on_new_record + model = @target.new + + assert_equal @target.column_names, model.attribute_names + end + + def test_attribute_names_on_queried_record + model = @target.last! + + assert_equal @target.column_names, model.attribute_names + end + + def test_attribute_names_with_custom_select + model = @target.select('id').last! + + assert_equal ['id'], model.attribute_names + # Sanity check, make sure other columns exist + assert_not_equal ['id'], @target.column_names + end + private def new_topic_like_ar_class(&block) @@ -856,7 +888,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase end def cached_columns - Topic.columns.map(&:name) - Topic.serialized_attributes.keys + Topic.columns.map(&:name) end def time_related_columns_on_topic diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb new file mode 100644 index 0000000000..dc20c3c676 --- /dev/null +++ b/activerecord/test/cases/attribute_set_test.rb @@ -0,0 +1,165 @@ +require 'cases/helper' + +module ActiveRecord + class AttributeSetTest < ActiveRecord::TestCase + test "building a new set from raw attributes" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) + attributes = builder.build_from_database(foo: '1.1', bar: '2.2') + + assert_equal 1, attributes[:foo].value + assert_equal 2.2, attributes[:bar].value + assert_equal :foo, attributes[:foo].name + assert_equal :bar, attributes[:bar].name + end + + test "building with custom types" do + builder = AttributeSet::Builder.new(foo: Type::Float.new) + attributes = builder.build_from_database({ foo: '3.3', bar: '4.4' }, { bar: Type::Integer.new }) + + assert_equal 3.3, attributes[:foo].value + assert_equal 4, attributes[:bar].value + end + + test "[] returns a null object" do + builder = AttributeSet::Builder.new(foo: Type::Float.new) + attributes = builder.build_from_database(foo: '3.3') + + assert_equal '3.3', attributes[:foo].value_before_type_cast + assert_equal nil, attributes[:bar].value_before_type_cast + assert_equal :bar, attributes[:bar].name + end + + test "duping creates a new hash and dups each attribute" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new) + attributes = builder.build_from_database(foo: 1, bar: 'foo') + + # Ensure the type cast value is cached + attributes[:foo].value + attributes[:bar].value + + duped = attributes.dup + duped.write_from_database(:foo, 2) + duped[:bar].value << 'bar' + + assert_equal 1, attributes[:foo].value + assert_equal 2, duped[:foo].value + assert_equal 'foo', attributes[:bar].value + assert_equal 'foobar', duped[:bar].value + end + + test "freezing cloned set does not freeze original" do + attributes = AttributeSet.new({}) + clone = attributes.clone + + clone.freeze + + assert clone.frozen? + assert_not attributes.frozen? + end + + test "to_hash returns a hash of the type cast values" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) + attributes = builder.build_from_database(foo: '1.1', bar: '2.2') + + assert_equal({ foo: 1, bar: 2.2 }, attributes.to_hash) + assert_equal({ foo: 1, bar: 2.2 }, attributes.to_h) + end + + test "values_before_type_cast" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new) + attributes = builder.build_from_database(foo: '1.1', bar: '2.2') + + assert_equal({ foo: '1.1', bar: '2.2' }, attributes.values_before_type_cast) + end + + test "known columns are built with uninitialized attributes" do + attributes = attributes_with_uninitialized_key + assert attributes[:foo].initialized? + assert_not attributes[:bar].initialized? + end + + test "uninitialized attributes are not included in the attributes hash" do + attributes = attributes_with_uninitialized_key + assert_equal({ foo: 1 }, attributes.to_hash) + end + + test "uninitialized attributes are not included in keys" do + attributes = attributes_with_uninitialized_key + assert_equal [:foo], attributes.keys + end + + test "uninitialized attributes return false for key?" do + attributes = attributes_with_uninitialized_key + assert attributes.key?(:foo) + assert_not attributes.key?(:bar) + end + + test "unknown attributes return false for key?" do + attributes = attributes_with_uninitialized_key + assert_not attributes.key?(:wibble) + end + + test "fetch_value returns the value for the given initialized attribute" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) + attributes = builder.build_from_database(foo: '1.1', bar: '2.2') + + assert_equal 1, attributes.fetch_value(:foo) + assert_equal 2.2, attributes.fetch_value(:bar) + end + + test "fetch_value returns nil for unknown attributes" do + attributes = attributes_with_uninitialized_key + assert_nil attributes.fetch_value(:wibble) + end + + test "fetch_value uses the given block for uninitialized attributes" do + attributes = attributes_with_uninitialized_key + value = attributes.fetch_value(:bar) { |n| n.to_s + '!' } + assert_equal 'bar!', value + end + + test "fetch_value returns nil for uninitialized attributes if no block is given" do + attributes = attributes_with_uninitialized_key + assert_nil attributes.fetch_value(:bar) + end + + class MyType + def type_cast_from_user(value) + return if value.nil? + value + " from user" + end + + def type_cast_from_database(value) + return if value.nil? + value + " from database" + end + end + + test "write_from_database sets the attribute with database typecasting" do + builder = AttributeSet::Builder.new(foo: MyType.new) + attributes = builder.build_from_database + + assert_nil attributes.fetch_value(:foo) + + attributes.write_from_database(:foo, "value") + + assert_equal "value from database", attributes.fetch_value(:foo) + end + + test "write_from_user sets the attribute with user typecasting" do + builder = AttributeSet::Builder.new(foo: MyType.new) + attributes = builder.build_from_database + + assert_nil attributes.fetch_value(:foo) + + attributes.write_from_user(:foo, "value") + + assert_equal "value from user", attributes.fetch_value(:foo) + end + + def attributes_with_uninitialized_key + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) + builder.build_from_database(foo: '1.1') + end + end +end diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb new file mode 100644 index 0000000000..24452fdec2 --- /dev/null +++ b/activerecord/test/cases/attribute_test.rb @@ -0,0 +1,142 @@ +require 'cases/helper' +require 'minitest/mock' + +module ActiveRecord + class AttributeTest < ActiveRecord::TestCase + setup do + @type = MiniTest::Mock.new + end + + teardown do + assert @type.verify + end + + test "from_database + read type casts from database" do + @type.expect(:type_cast_from_database, 'type cast from database', ['a value']) + attribute = Attribute.from_database(nil, 'a value', @type) + + type_cast_value = attribute.value + + assert_equal 'type cast from database', type_cast_value + end + + test "from_user + read type casts from user" do + @type.expect(:type_cast_from_user, 'type cast from user', ['a value']) + attribute = Attribute.from_user(nil, 'a value', @type) + + type_cast_value = attribute.value + + assert_equal 'type cast from user', type_cast_value + end + + test "reading memoizes the value" do + @type.expect(:type_cast_from_database, 'from the database', ['whatever']) + attribute = Attribute.from_database(nil, 'whatever', @type) + + type_cast_value = attribute.value + second_read = attribute.value + + assert_equal 'from the database', type_cast_value + assert_same type_cast_value, second_read + end + + test "reading memoizes falsy values" do + @type.expect(:type_cast_from_database, false, ['whatever']) + attribute = Attribute.from_database(nil, 'whatever', @type) + + attribute.value + attribute.value + end + + test "read_before_typecast returns the given value" do + attribute = Attribute.from_database(nil, 'raw value', @type) + + raw_value = attribute.value_before_type_cast + + assert_equal 'raw value', raw_value + end + + test "from_database + read_for_database type casts to and from database" do + @type.expect(:type_cast_from_database, 'read from database', ['whatever']) + @type.expect(:type_cast_for_database, 'ready for database', ['read from database']) + attribute = Attribute.from_database(nil, 'whatever', @type) + + type_cast_for_database = attribute.value_for_database + + assert_equal 'ready for database', type_cast_for_database + end + + test "from_user + read_for_database type casts from the user to the database" do + @type.expect(:type_cast_from_user, 'read from user', ['whatever']) + @type.expect(:type_cast_for_database, 'ready for database', ['read from user']) + attribute = Attribute.from_user(nil, 'whatever', @type) + + type_cast_for_database = attribute.value_for_database + + assert_equal 'ready for database', type_cast_for_database + end + + test "duping dups the value" do + @type.expect(:type_cast_from_database, 'type cast', ['a value']) + attribute = Attribute.from_database(nil, 'a value', @type) + + value_from_orig = attribute.value + value_from_clone = attribute.dup.value + value_from_orig << ' foo' + + assert_equal 'type cast foo', value_from_orig + assert_equal 'type cast', value_from_clone + end + + test "duping does not dup the value if it is not dupable" do + @type.expect(:type_cast_from_database, false, ['a value']) + attribute = Attribute.from_database(nil, 'a value', @type) + + assert_same attribute.value, attribute.dup.value + end + + test "duping does not eagerly type cast if we have not yet type cast" do + attribute = Attribute.from_database(nil, 'a value', @type) + attribute.dup + end + + class MyType + def type_cast_from_user(value) + value + " from user" + end + + def type_cast_from_database(value) + value + " from database" + end + end + + test "with_value_from_user returns a new attribute with the value from the user" do + old = Attribute.from_database(nil, "old", MyType.new) + new = old.with_value_from_user("new") + + assert_equal "old from database", old.value + assert_equal "new from user", new.value + end + + test "with_value_from_database returns a new attribute with the value from the database" do + old = Attribute.from_user(nil, "old", MyType.new) + new = old.with_value_from_database("new") + + assert_equal "old from user", old.value + assert_equal "new from database", new.value + end + + test "uninitialized attributes yield their name if a block is given to value" do + block = proc { |name| name.to_s + "!" } + foo = Attribute.uninitialized(:foo, nil) + bar = Attribute.uninitialized(:bar, nil) + + assert_equal "foo!", foo.value(&block) + assert_equal "bar!", bar.value(&block) + end + + test "uninitialized attributes have no value" do + assert_nil Attribute.uninitialized(:foo, nil).value + end + end +end diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb new file mode 100644 index 0000000000..79ef0502cb --- /dev/null +++ b/activerecord/test/cases/attributes_test.rb @@ -0,0 +1,111 @@ +require 'cases/helper' + +class OverloadedType < ActiveRecord::Base + attribute :overloaded_float, Type::Integer.new + attribute :overloaded_string_with_limit, Type::String.new(limit: 50) + attribute :non_existent_decimal, Type::Decimal.new + attribute :string_with_default, Type::String.new, default: 'the overloaded default' +end + +class ChildOfOverloadedType < OverloadedType +end + +class GrandchildOfOverloadedType < ChildOfOverloadedType + attribute :overloaded_float, Type::Float.new +end + +class UnoverloadedType < ActiveRecord::Base + self.table_name = 'overloaded_types' +end + +module ActiveRecord + class CustomPropertiesTest < ActiveRecord::TestCase + def test_overloading_types + data = OverloadedType.new + + data.overloaded_float = "1.1" + data.unoverloaded_float = "1.1" + + assert_equal 1, data.overloaded_float + assert_equal 1.1, data.unoverloaded_float + end + + def test_overloaded_properties_save + data = OverloadedType.new + + data.overloaded_float = "2.2" + data.save! + data.reload + + assert_equal 2, data.overloaded_float + assert_kind_of Fixnum, OverloadedType.last.overloaded_float + assert_equal 2.0, UnoverloadedType.last.overloaded_float + assert_kind_of Float, UnoverloadedType.last.overloaded_float + end + + def test_properties_assigned_in_constructor + data = OverloadedType.new(overloaded_float: '3.3') + + assert_equal 3, data.overloaded_float + end + + def test_overloaded_properties_with_limit + assert_equal 50, OverloadedType.columns_hash['overloaded_string_with_limit'].limit + assert_equal 255, UnoverloadedType.columns_hash['overloaded_string_with_limit'].limit + end + + def test_nonexistent_attribute + data = OverloadedType.new(non_existent_decimal: 1) + + assert_equal BigDecimal.new(1), data.non_existent_decimal + assert_raise ActiveRecord::UnknownAttributeError do + UnoverloadedType.new(non_existent_decimal: 1) + end + end + + def test_changing_defaults + data = OverloadedType.new + unoverloaded_data = UnoverloadedType.new + + assert_equal 'the overloaded default', data.string_with_default + assert_equal 'the original default', unoverloaded_data.string_with_default + end + + def test_children_inherit_custom_properties + data = ChildOfOverloadedType.new(overloaded_float: '4.4') + + assert_equal 4, data.overloaded_float + end + + def test_children_can_override_parents + data = GrandchildOfOverloadedType.new(overloaded_float: '4.4') + + assert_equal 4.4, data.overloaded_float + end + + def test_overloading_properties_does_not_change_column_order + column_names = OverloadedType.column_names + assert_equal %w(id overloaded_float unoverloaded_float overloaded_string_with_limit string_with_default non_existent_decimal), column_names + end + + def test_caches_are_cleared + klass = Class.new(OverloadedType) + + assert_equal 6, klass.columns.length + assert_not klass.columns_hash.key?('wibble') + assert_equal 6, klass.column_types.length + assert_equal 6, klass.column_defaults.length + assert_not klass.column_names.include?('wibble') + assert_equal 5, klass.content_columns.length + + klass.attribute :wibble, Type::Value.new + + assert_equal 7, klass.columns.length + assert klass.columns_hash.key?('wibble') + assert_equal 7, klass.column_types.length + assert_equal 7, klass.column_defaults.length + assert klass.column_names.include?('wibble') + assert_equal 6, klass.content_columns.length + end + end +end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index f7584c3a51..09892d50ba 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -683,10 +683,23 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase end end + @ship.pirate.catchphrase = "Changed Catchphrase" + assert_raise(RuntimeError) { assert !@pirate.save } assert_not_nil @pirate.reload.ship end + def test_should_save_changed_has_one_changed_object_if_child_is_saved + @pirate.ship.name = "NewName" + assert @pirate.save + assert_equal "NewName", @pirate.ship.reload.name + end + + def test_should_not_save_changed_has_one_unchanged_object_if_child_is_saved + @pirate.ship.expects(:save).never + assert @pirate.save + end + # belongs_to def test_should_destroy_a_parent_association_as_part_of_the_save_transaction_if_it_was_marked_for_destroyal assert !@ship.pirate.marked_for_destruction? diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 2e5b8cffa6..4c0b0c868a 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -102,8 +102,8 @@ class BasicsTest < ActiveRecord::TestCase end def test_columns_should_obey_set_primary_key - pk = Subscriber.columns.find { |x| x.name == 'nick' } - assert pk.primary, 'nick should be primary key' + pk = Subscriber.columns_hash[Subscriber.primary_key] + assert_equal 'nick', pk.name, 'nick should be primary key' end def test_primary_key_with_no_id @@ -160,19 +160,11 @@ class BasicsTest < ActiveRecord::TestCase end def test_preserving_date_objects - if current_adapter?(:SybaseAdapter) - # Sybase ctlib does not (yet?) support the date type; use datetime instead. - assert_kind_of( - Time, Topic.find(1).last_read, - "The last_read attribute should be of the Time class" - ) - else - # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb) - assert_kind_of( - Date, Topic.find(1).last_read, - "The last_read attribute should be of the Date class" - ) - end + # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb) + assert_kind_of( + Date, Topic.find(1).last_read, + "The last_read attribute should be of the Date class" + ) end def test_previously_changed @@ -480,8 +472,8 @@ class BasicsTest < ActiveRecord::TestCase end end - # Oracle, and Sybase do not have a TIME datatype. - unless current_adapter?(:OracleAdapter, :SybaseAdapter) + # Oracle does not have a TIME datatype. + unless current_adapter?(:OracleAdapter) def test_utc_as_time_zone with_timezone_config default: :utc do attributes = { "bonus_time" => "5:42:00AM" } @@ -515,12 +507,7 @@ class BasicsTest < ActiveRecord::TestCase topic = Topic.find(topic.id) assert_nil topic.last_read - # Sybase adapter does not allow nulls in boolean columns - if current_adapter?(:SybaseAdapter) - assert topic.approved == false - else - assert_nil topic.approved - end + assert_nil topic.approved end def test_equality @@ -531,6 +518,10 @@ class BasicsTest < ActiveRecord::TestCase assert_equal Topic.find('1-meowmeow'), Topic.find(1) end + def test_find_by_slug_with_array + assert_equal Topic.find(['1-meowmeow', '2-hello']), Topic.find([1, 2]) + end + def test_equality_of_new_records assert_not_equal Topic.new, Topic.new assert_equal false, Topic.new == Topic.new @@ -685,8 +676,8 @@ class BasicsTest < ActiveRecord::TestCase end def test_attributes_on_dummy_time - # Oracle, and Sybase do not have a TIME datatype. - return true if current_adapter?(:OracleAdapter, :SybaseAdapter) + # Oracle does not have a TIME datatype. + return true if current_adapter?(:OracleAdapter) with_timezone_config default: :local do attributes = { @@ -699,8 +690,8 @@ class BasicsTest < ActiveRecord::TestCase end def test_attributes_on_dummy_time_with_invalid_time - # Oracle, and Sybase do not have a TIME datatype. - return true if current_adapter?(:OracleAdapter, :SybaseAdapter) + # Oracle does not have a TIME datatype. + return true if current_adapter?(:OracleAdapter) attributes = { "bonus_time" => "not a time" @@ -787,8 +778,14 @@ class BasicsTest < ActiveRecord::TestCase assert_equal("c", duped_topic.title) end + DeveloperSalary = Struct.new(:amount) def test_dup_with_aggregate_of_same_name_as_attribute - dev = DeveloperWithAggregate.find(1) + developer_with_aggregate = Class.new(ActiveRecord::Base) do + self.table_name = 'developers' + composed_of :salary, :class_name => 'BasicsTest::DeveloperSalary', :mapping => [%w(salary amount)] + end + + dev = developer_with_aggregate.find(1) assert_kind_of DeveloperSalary, dev.salary dup = nil @@ -987,6 +984,10 @@ class BasicsTest < ActiveRecord::TestCase class NumericData < ActiveRecord::Base self.table_name = 'numeric_data' + + attribute :world_population, Type::Integer.new + attribute :my_house_population, Type::Integer.new + attribute :atoms_in_universe, Type::Integer.new end def test_big_decimal_conditions @@ -1346,14 +1347,32 @@ class BasicsTest < ActiveRecord::TestCase end def test_compute_type_no_method_error - ActiveSupport::Dependencies.stubs(:constantize).raises(NoMethodError) + ActiveSupport::Dependencies.stubs(:safe_constantize).raises(NoMethodError) assert_raises NoMethodError do ActiveRecord::Base.send :compute_type, 'InvalidModel' end end + def test_compute_type_on_undefined_method + error = nil + begin + Class.new(Author) do + alias_method :foo, :bar + end + rescue => e + error = e + end + + ActiveSupport::Dependencies.stubs(:safe_constantize).raises(e) + + exception = assert_raises NameError do + ActiveRecord::Base.send :compute_type, 'InvalidModel' + end + assert_equal error.message, exception.message + end + def test_compute_type_argument_error - ActiveSupport::Dependencies.stubs(:constantize).raises(ArgumentError) + ActiveSupport::Dependencies.stubs(:safe_constantize).raises(ArgumentError) assert_raises ArgumentError do ActiveRecord::Base.send :compute_type, 'InvalidModel' end @@ -1487,15 +1506,14 @@ class BasicsTest < ActiveRecord::TestCase attrs = topic.attributes.dup attrs.delete 'id' - typecast = Class.new { + typecast = Class.new(ActiveRecord::Type::Value) { def type_cast value "t.lo" end } types = { 'author_name' => typecast.new } - topic = Topic.allocate.init_with 'attributes' => attrs, - 'column_types' => types + topic = Topic.instantiate(attrs, types) assert_equal 't.lo', topic.author_name end @@ -1596,4 +1614,11 @@ class BasicsTest < ActiveRecord::TestCase assert_equal after_handler, new_handler assert_equal orig_handler, klass.connection_handler end + + # Note: This is a performance optimization for Array#uniq and Hash#[] with + # AR::Base objects. If the future has made this irrelevant, feel free to + # delete this. + test "records without an id have unique hashes" do + assert_not_equal Post.new.hash, Post.new.hash + end end diff --git a/activerecord/test/cases/binary_test.rb b/activerecord/test/cases/binary_test.rb index 9a486cf8b8..ccf2be369d 100644 --- a/activerecord/test/cases/binary_test.rb +++ b/activerecord/test/cases/binary_test.rb @@ -2,9 +2,9 @@ require "cases/helper" # Without using prepared statements, it makes no sense to test -# BLOB data with DB2 or Firebird, because the length of a statement +# BLOB data with DB2, because the length of a statement # is limited to 32KB. -unless current_adapter?(:SybaseAdapter, :DB2Adapter, :FirebirdAdapter) +unless current_adapter?(:DB2Adapter) require 'models/binary' class BinaryTest < ActiveRecord::TestCase @@ -21,7 +21,7 @@ unless current_adapter?(:SybaseAdapter, :DB2Adapter, :FirebirdAdapter) name = binary.name - # Mysql adapter doesn't properly encode things, so we have to do it + # MySQL adapter doesn't properly encode things, so we have to do it if current_adapter?(:MysqlAdapter) name.force_encoding(Encoding::UTF_8) end diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index 40f73cd68c..0bc7ee6d64 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -21,7 +21,7 @@ module ActiveRecord super @connection = ActiveRecord::Base.connection @subscriber = LogListener.new - @pk = Topic.columns.find { |c| c.primary } + @pk = Topic.columns_hash[Topic.primary_key] @subscription = ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber) end @@ -60,12 +60,10 @@ module ActiveRecord 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]] + :binds => [[@pk, 10]] } event = ActiveSupport::Notifications::Event.new( 'foo', @@ -87,7 +85,7 @@ module ActiveRecord }.new logger.sql event - assert_match([[pk.name, 10]].inspect, logger.debugs.first) + assert_match([[@pk.name, 10]].inspect, logger.debugs.first) end end end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index b8de78934e..319ea9260a 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -15,6 +15,10 @@ Company.has_many :accounts class NumericData < ActiveRecord::Base self.table_name = 'numeric_data' + + attribute :world_population, Type::Integer.new + attribute :my_house_population, Type::Integer.new + attribute :atoms_in_universe, Type::Integer.new end class CalculationsTest < ActiveRecord::TestCase @@ -49,11 +53,6 @@ class CalculationsTest < ActiveRecord::TestCase assert_nil NumericData.average(:bank_balance) end - def test_type_cast_calculated_value_should_convert_db_averages_of_fixnum_class_to_decimal - assert_equal 0, NumericData.all.send(:type_cast_calculated_value, 0, nil, 'avg') - assert_equal 53.0, NumericData.all.send(:type_cast_calculated_value, 53, nil, 'avg') - end - def test_should_get_maximum_of_field assert_equal 60, Account.maximum(:credit_limit) end @@ -606,4 +605,11 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal [1,2,3,4,5], taks_relation.pluck(:id) assert_equal [false, true, true, true, true], taks_relation.pluck(:approved) end + + def test_pluck_columns_with_same_name + expected = [["The First Topic", "The Second Topic of the day"], ["The Third Topic of the day", "The Fourth Topic of the day"]] + actual = Topic.joins(:replies) + .pluck('topics.title', 'replies_topics.title') + assert_equal expected, actual + end end diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb index c1dd1f1c69..bcfd66b4bf 100644 --- a/activerecord/test/cases/column_definition_test.rb +++ b/activerecord/test/cases/column_definition_test.rb @@ -11,30 +11,10 @@ module ActiveRecord @viz = @adapter.schema_creation end - def test_can_set_coder - column = Column.new("title", nil, "varchar(20)") - column.coder = YAML - assert_equal YAML, column.coder - end - - def test_encoded? - column = Column.new("title", nil, "varchar(20)") - assert !column.encoded? - - column.coder = YAML - assert column.encoded? - end - - def test_type_case_coded_column - column = Column.new("title", nil, "varchar(20)") - column.coder = YAML - assert_equal "hello", column.type_cast("--- hello") - end - # Avoid column definitions in create table statements like: # `title` varchar(255) DEFAULT NULL def test_should_not_include_default_clause_when_default_is_null - column = Column.new("title", nil, "varchar(20)") + column = Column.new("title", nil, Type::String.new(limit: 20)) column_def = ColumnDefinition.new( column.name, "string", column.limit, column.precision, column.scale, column.default, column.null) @@ -42,7 +22,7 @@ module ActiveRecord end def test_should_include_default_clause_when_default_is_present - column = Column.new("title", "Hello", "varchar(20)") + column = Column.new("title", "Hello", Type::String.new(limit: 20)) column_def = ColumnDefinition.new( column.name, "string", column.limit, column.precision, column.scale, column.default, column.null) @@ -50,7 +30,7 @@ module ActiveRecord end def test_should_specify_not_null_if_null_option_is_false - column = Column.new("title", "Hello", "varchar(20)", false) + column = Column.new("title", "Hello", Type::String.new(limit: 20), "varchar(20)", false) column_def = ColumnDefinition.new( column.name, "string", column.limit, column.precision, column.scale, column.default, column.null) @@ -59,68 +39,68 @@ module ActiveRecord if current_adapter?(:MysqlAdapter) def test_should_set_default_for_mysql_binary_data_types - binary_column = MysqlAdapter::Column.new("title", "a", "binary(1)") + binary_column = MysqlAdapter::Column.new("title", "a", Type::Binary.new, "binary(1)") assert_equal "a", binary_column.default - varbinary_column = MysqlAdapter::Column.new("title", "a", "varbinary(1)") + varbinary_column = MysqlAdapter::Column.new("title", "a", Type::Binary.new, "varbinary(1)") assert_equal "a", varbinary_column.default end def test_should_not_set_default_for_blob_and_text_data_types assert_raise ArgumentError do - MysqlAdapter::Column.new("title", "a", "blob") + MysqlAdapter::Column.new("title", "a", Type::Binary.new, "blob") end assert_raise ArgumentError do - MysqlAdapter::Column.new("title", "Hello", "text") + MysqlAdapter::Column.new("title", "Hello", Type::Text.new) end - text_column = MysqlAdapter::Column.new("title", nil, "text") + text_column = MysqlAdapter::Column.new("title", nil, Type::Text.new) assert_equal nil, text_column.default - not_null_text_column = MysqlAdapter::Column.new("title", nil, "text", false) + not_null_text_column = MysqlAdapter::Column.new("title", nil, Type::Text.new, "text", false) assert_equal "", not_null_text_column.default end def test_has_default_should_return_false_for_blob_and_text_data_types - blob_column = MysqlAdapter::Column.new("title", nil, "blob") + blob_column = MysqlAdapter::Column.new("title", nil, Type::Binary.new, "blob") assert !blob_column.has_default? - text_column = MysqlAdapter::Column.new("title", nil, "text") + text_column = MysqlAdapter::Column.new("title", nil, Type::Text.new) assert !text_column.has_default? end end if current_adapter?(:Mysql2Adapter) def test_should_set_default_for_mysql_binary_data_types - binary_column = Mysql2Adapter::Column.new("title", "a", "binary(1)") + binary_column = Mysql2Adapter::Column.new("title", "a", Type::Binary.new, "binary(1)") assert_equal "a", binary_column.default - varbinary_column = Mysql2Adapter::Column.new("title", "a", "varbinary(1)") + varbinary_column = Mysql2Adapter::Column.new("title", "a", Type::Binary.new, "varbinary(1)") assert_equal "a", varbinary_column.default end def test_should_not_set_default_for_blob_and_text_data_types assert_raise ArgumentError do - Mysql2Adapter::Column.new("title", "a", "blob") + Mysql2Adapter::Column.new("title", "a", Type::Binary.new, "blob") end assert_raise ArgumentError do - Mysql2Adapter::Column.new("title", "Hello", "text") + Mysql2Adapter::Column.new("title", "Hello", Type::Text.new) end - text_column = Mysql2Adapter::Column.new("title", nil, "text") + text_column = Mysql2Adapter::Column.new("title", nil, Type::Text.new) assert_equal nil, text_column.default - not_null_text_column = Mysql2Adapter::Column.new("title", nil, "text", false) + not_null_text_column = Mysql2Adapter::Column.new("title", nil, Type::Text.new, "text", false) assert_equal "", not_null_text_column.default end def test_has_default_should_return_false_for_blob_and_text_data_types - blob_column = Mysql2Adapter::Column.new("title", nil, "blob") + blob_column = Mysql2Adapter::Column.new("title", nil, Type::Binary.new, "blob") assert !blob_column.has_default? - text_column = Mysql2Adapter::Column.new("title", nil, "text") + text_column = Mysql2Adapter::Column.new("title", nil, Type::Text.new) assert !text_column.has_default? end end diff --git a/activerecord/test/cases/column_test.rb b/activerecord/test/cases/column_test.rb deleted file mode 100644 index 2a6d8cc2ab..0000000000 --- a/activerecord/test/cases/column_test.rb +++ /dev/null @@ -1,123 +0,0 @@ -require "cases/helper" -require 'models/company' - -module ActiveRecord - module ConnectionAdapters - class ColumnTest < ActiveRecord::TestCase - def test_type_cast_boolean - column = Column.new("field", nil, "boolean") - assert column.type_cast('').nil? - assert column.type_cast(nil).nil? - - assert column.type_cast(true) - assert column.type_cast(1) - assert column.type_cast('1') - assert column.type_cast('t') - assert column.type_cast('T') - assert column.type_cast('true') - assert column.type_cast('TRUE') - assert column.type_cast('on') - assert column.type_cast('ON') - - # explicitly check for false vs nil - assert_equal false, column.type_cast(false) - assert_equal false, column.type_cast(0) - assert_equal false, column.type_cast('0') - assert_equal false, column.type_cast('f') - assert_equal false, column.type_cast('F') - assert_equal false, column.type_cast('false') - assert_equal false, column.type_cast('FALSE') - assert_equal false, column.type_cast('off') - assert_equal false, column.type_cast('OFF') - assert_equal false, column.type_cast(' ') - assert_equal false, column.type_cast("\u3000\r\n") - assert_equal false, column.type_cast("\u0000") - assert_equal false, column.type_cast('SOMETHING RANDOM') - end - - def test_type_cast_integer - column = Column.new("field", nil, "integer") - assert_equal 1, column.type_cast(1) - assert_equal 1, column.type_cast('1') - assert_equal 1, column.type_cast('1ignore') - assert_equal 0, column.type_cast('bad1') - assert_equal 0, column.type_cast('bad') - assert_equal 1, column.type_cast(1.7) - assert_equal 0, column.type_cast(false) - assert_equal 1, column.type_cast(true) - assert_nil column.type_cast(nil) - end - - def test_type_cast_non_integer_to_integer - column = Column.new("field", nil, "integer") - assert_nil column.type_cast([1,2]) - assert_nil column.type_cast({1 => 2}) - assert_nil column.type_cast((1..2)) - end - - def test_type_cast_activerecord_to_integer - column = Column.new("field", nil, "integer") - firm = Firm.create(:name => 'Apple') - assert_nil column.type_cast(firm) - end - - def test_type_cast_object_without_to_i_to_integer - column = Column.new("field", nil, "integer") - assert_nil column.type_cast(Object.new) - end - - def test_type_cast_nan_and_infinity_to_integer - column = Column.new("field", nil, "integer") - assert_nil column.type_cast(Float::NAN) - assert_nil column.type_cast(1.0/0.0) - end - - def test_type_cast_time - column = Column.new("field", nil, "time") - assert_equal nil, column.type_cast(nil) - assert_equal nil, column.type_cast('') - assert_equal nil, column.type_cast('ABC') - - time_string = Time.now.utc.strftime("%T") - assert_equal time_string, column.type_cast(time_string).strftime("%T") - end - - def test_type_cast_datetime_and_timestamp - [Column.new("field", nil, "datetime"), Column.new("field", nil, "timestamp")].each do |column| - assert_equal nil, column.type_cast(nil) - assert_equal nil, column.type_cast('') - assert_equal nil, column.type_cast(' ') - assert_equal nil, column.type_cast('ABC') - - datetime_string = Time.now.utc.strftime("%FT%T") - assert_equal datetime_string, column.type_cast(datetime_string).strftime("%FT%T") - end - end - - def test_type_cast_date - column = Column.new("field", nil, "date") - assert_equal nil, column.type_cast(nil) - assert_equal nil, column.type_cast('') - assert_equal nil, column.type_cast(' ') - assert_equal nil, column.type_cast('ABC') - - date_string = Time.now.utc.strftime("%F") - assert_equal date_string, column.type_cast(date_string).strftime("%F") - end - - def test_type_cast_duration_to_integer - column = Column.new("field", nil, "integer") - 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_adapters/merge_and_resolve_default_url_config_test.rb b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb index da852aaa02..e1b2804a18 100644 --- a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb +++ b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb @@ -144,6 +144,18 @@ module ActiveRecord assert_equal nil, actual[:test] end + def test_database_url_with_ipv6_host_and_port + ENV['DATABASE_URL'] = "postgres://[::1]:5454/foo" + + config = {} + actual = resolve_config(config) + expected = { "adapter" => "postgresql", + "database" => "foo", + "host" => "::1", + "port" => 5454 } + assert_equal expected, actual["default_env"] + end + def test_url_sub_key_with_database_url ENV['DATABASE_URL'] = "NOT-POSTGRES://localhost/NOT_FOO" diff --git a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb new file mode 100644 index 0000000000..d4d67487db --- /dev/null +++ b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb @@ -0,0 +1,61 @@ +require "cases/helper" + +if current_adapter?(:MysqlAdapter, :Mysql2Adapter) +module ActiveRecord + module ConnectionAdapters + class MysqlTypeLookupTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + end + + def test_boolean_types + emulate_booleans(true) do + assert_lookup_type :boolean, 'tinyint(1)' + assert_lookup_type :boolean, 'TINYINT(1)' + end + end + + def test_string_types + assert_lookup_type :string, "enum('one', 'two', 'three')" + assert_lookup_type :string, "ENUM('one', 'two', 'three')" + assert_lookup_type :string, "set('one', 'two', 'three')" + assert_lookup_type :string, "SET('one', 'two', 'three')" + end + + def test_binary_types + assert_lookup_type :binary, 'bit' + assert_lookup_type :binary, 'BIT' + end + + def test_integer_types + emulate_booleans(false) do + assert_lookup_type :integer, 'tinyint(1)' + assert_lookup_type :integer, 'TINYINT(1)' + assert_lookup_type :integer, 'year' + assert_lookup_type :integer, 'YEAR' + end + end + + private + + def assert_lookup_type(type, lookup) + cast_type = @connection.type_map.lookup(lookup) + assert_equal type, cast_type.type + end + + def emulate_booleans(value) + old_emulate_booleans = @connection.emulate_booleans + change_emulate_booleans(value) + yield + ensure + change_emulate_booleans(old_emulate_booleans) + end + + def change_emulate_booleans(value) + @connection.emulate_booleans = value + @connection.clear_cache! + end + end + end +end +end diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb index ecad7c942f..c7531f5418 100644 --- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb +++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb @@ -45,8 +45,8 @@ module ActiveRecord @cache = Marshal.load(Marshal.dump(@cache)) - assert_equal 12, @cache.columns('posts').size - assert_equal 12, @cache.columns_hash('posts').size + assert_equal 11, @cache.columns('posts').size + assert_equal 11, @cache.columns_hash('posts').size assert @cache.tables('posts') assert_equal 'id', @cache.primary_keys('posts') end diff --git a/activerecord/test/cases/connection_adapters/type_lookup_test.rb b/activerecord/test/cases/connection_adapters/type_lookup_test.rb new file mode 100644 index 0000000000..d5c1dc1e5d --- /dev/null +++ b/activerecord/test/cases/connection_adapters/type_lookup_test.rb @@ -0,0 +1,101 @@ +require "cases/helper" + +unless current_adapter?(:PostgreSQLAdapter) # PostgreSQL does not use type strigns for lookup +module ActiveRecord + module ConnectionAdapters + class TypeLookupTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + end + + def test_boolean_types + assert_lookup_type :boolean, 'boolean' + assert_lookup_type :boolean, 'BOOLEAN' + end + + def test_string_types + assert_lookup_type :string, 'char' + assert_lookup_type :string, 'varchar' + assert_lookup_type :string, 'VARCHAR' + assert_lookup_type :string, 'varchar(255)' + assert_lookup_type :string, 'character varying' + end + + def test_binary_types + assert_lookup_type :binary, 'binary' + assert_lookup_type :binary, 'BINARY' + assert_lookup_type :binary, 'blob' + assert_lookup_type :binary, 'BLOB' + end + + def test_text_types + assert_lookup_type :text, 'text' + assert_lookup_type :text, 'TEXT' + assert_lookup_type :text, 'clob' + assert_lookup_type :text, 'CLOB' + end + + def test_date_types + assert_lookup_type :date, 'date' + assert_lookup_type :date, 'DATE' + end + + def test_time_types + assert_lookup_type :time, 'time' + assert_lookup_type :time, 'TIME' + end + + def test_datetime_types + assert_lookup_type :datetime, 'datetime' + assert_lookup_type :datetime, 'DATETIME' + assert_lookup_type :datetime, 'timestamp' + assert_lookup_type :datetime, 'TIMESTAMP' + end + + def test_decimal_types + assert_lookup_type :decimal, 'decimal' + assert_lookup_type :decimal, 'decimal(2,8)' + assert_lookup_type :decimal, 'DECIMAL' + assert_lookup_type :decimal, 'numeric' + assert_lookup_type :decimal, 'numeric(2,8)' + assert_lookup_type :decimal, 'NUMERIC' + assert_lookup_type :decimal, 'number' + assert_lookup_type :decimal, 'number(2,8)' + assert_lookup_type :decimal, 'NUMBER' + end + + def test_float_types + assert_lookup_type :float, 'float' + assert_lookup_type :float, 'FLOAT' + assert_lookup_type :float, 'double' + assert_lookup_type :float, 'DOUBLE' + end + + def test_integer_types + assert_lookup_type :integer, 'integer' + assert_lookup_type :integer, 'INTEGER' + assert_lookup_type :integer, 'tinyint' + assert_lookup_type :integer, 'smallint' + assert_lookup_type :integer, 'bigint' + end + + def test_decimal_without_scale + types = %w{decimal(2) decimal(2,0) numeric(2) numeric(2,0) number(2) number(2,0)} + types.each do |type| + cast_type = @connection.type_map.lookup(type) + + assert_equal :decimal, cast_type.type + assert_equal 2, cast_type.type_cast_from_user(2.1) + end + end + + private + + def assert_lookup_type(type, lookup) + cast_type = @connection.type_map.lookup(lookup) + assert_equal type, cast_type.type + end + end + end +end +end diff --git a/activerecord/test/cases/core_test.rb b/activerecord/test/cases/core_test.rb index 2a52bf574c..715d92af99 100644 --- a/activerecord/test/cases/core_test.rb +++ b/activerecord/test/cases/core_test.rb @@ -1,6 +1,8 @@ require 'cases/helper' require 'models/person' require 'models/topic' +require 'pp' +require 'active_support/core_ext/string/strip' class NonExistentTable < ActiveRecord::Base; end @@ -30,4 +32,70 @@ class CoreTest < ActiveRecord::TestCase def test_inspect_class_without_table assert_equal "NonExistentTable(Table doesn't exist)", NonExistentTable.inspect end + + def test_pretty_print_new + topic = Topic.new + actual = '' + PP.pp(topic, StringIO.new(actual)) + expected = <<-PRETTY.strip_heredoc + #<Topic:0xXXXXXX + id: nil, + title: nil, + author_name: nil, + author_email_address: "test@test.com", + written_on: nil, + bonus_time: nil, + last_read: nil, + content: nil, + important: nil, + approved: true, + replies_count: 0, + unique_replies_count: 0, + parent_id: nil, + parent_title: nil, + type: nil, + group: nil, + created_at: nil, + updated_at: nil> + PRETTY + assert actual.start_with?(expected.split('XXXXXX').first) + assert actual.end_with?(expected.split('XXXXXX').last) + end + + def test_pretty_print_persisted + topic = topics(:first) + actual = '' + PP.pp(topic, StringIO.new(actual)) + expected = <<-PRETTY.strip_heredoc + #<Topic:0x\\w+ + id: 1, + title: "The First Topic", + author_name: "David", + author_email_address: "david@loudthinking.com", + written_on: 2003-07-16 14:28:11 UTC, + bonus_time: 2000-01-01 14:28:00 UTC, + last_read: Thu, 15 Apr 2004, + content: "Have a nice day", + important: nil, + approved: false, + replies_count: 1, + unique_replies_count: 0, + parent_id: nil, + parent_title: nil, + type: nil, + group: nil, + created_at: [^,]+, + updated_at: [^,>]+> + PRETTY + assert_match(/\A#{expected}\z/, actual) + end + + def test_pretty_print_uninitialized + topic = Topic.allocate + actual = '' + PP.pp(topic, StringIO.new(actual)) + expected = "#<Topic:XXXXXX not initialized>\n" + assert actual.start_with?(expected.split('XXXXXX').first) + assert actual.end_with?(expected.split('XXXXXX').last) + end end diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index ee3d8a81c2..07a182070b 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -19,6 +19,7 @@ class CounterCacheTest < ActiveRecord::TestCase class ::SpecialTopic < ::Topic has_many :special_replies, :foreign_key => 'parent_id' + has_many :lightweight_special_replies, -> { select('topics.id, topics.title') }, :foreign_key => 'parent_id', :class_name => 'SpecialReply' end class ::SpecialReply < ::Reply @@ -51,6 +52,16 @@ class CounterCacheTest < ActiveRecord::TestCase end end + test "reset counters by counter name" do + # throw the count off by 1 + Topic.increment_counter(:replies_count, @topic.id) + + # check that it gets reset + assert_difference '@topic.reload.replies_count', -1 do + Topic.reset_counters(@topic.id, :replies_count) + end + end + test 'reset multiple counters' do Topic.update_counters @topic.id, replies_count: 1, unique_replies_count: 1 assert_difference ['@topic.reload.replies_count', '@topic.reload.unique_replies_count'], -1 do @@ -154,10 +165,19 @@ class CounterCacheTest < ActiveRecord::TestCase end end - test "the passed symbol needs to be an association name" do + test "the passed symbol needs to be an association name or counter name" do e = assert_raises(ArgumentError) do - Topic.reset_counters(@topic.id, :replies_count) + Topic.reset_counters(@topic.id, :undefined_count) + end + assert_equal "'Topic' has no association called 'undefined_count'", e.message + end + + test "reset counter works with select declared on association" do + special = SpecialTopic.create!(:title => 'Special') + SpecialTopic.increment_counter(:replies_count, special.id) + + assert_difference 'special.reload.replies_count', -1 do + SpecialTopic.reset_counters(special.id, :lightweight_special_replies) end - assert_equal "'Topic' has no association called 'replies_count'", e.message end end diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index 7d438803a1..c089e63128 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -18,7 +18,7 @@ class DefaultTest < ActiveRecord::TestCase end end - if current_adapter?(:PostgreSQLAdapter, :FirebirdAdapter, :OpenBaseAdapter, :OracleAdapter) + if current_adapter?(:PostgreSQLAdapter, :OracleAdapter) def test_default_integers default = Default.new assert_instance_of Fixnum, default.positive_integer @@ -154,7 +154,7 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter) t.column :omit, :integer, :null => false end - assert_equal 0, klass.columns_hash['zero'].default + assert_equal '0', klass.columns_hash['zero'].default assert !klass.columns_hash['zero'].null # 0 in MySQL 4, nil in 5. assert [0, nil].include?(klass.columns_hash['omit'].default) @@ -206,6 +206,11 @@ if current_adapter?(:PostgreSQLAdapter) assert_equal "some text", Default.new.text_col, "Default of text column was not correctly parse after updating default using '::text' since postgreSQL will add parens to the default in db" end + def test_default_containing_quote_and_colons + @connection.execute "ALTER TABLE defaults ALTER COLUMN string_col SET DEFAULT 'foo''::bar'" + assert_equal "foo'::bar", Default.new.string_col + end + teardown do @connection.schema_search_path = @old_search_path Default.reset_column_information diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index df4183c065..69a7f25213 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -169,7 +169,19 @@ class DirtyTest < ActiveRecord::TestCase pirate = Pirate.create!(:catchphrase => 'Yar!') pirate.catchphrase = 'Ahoy!' - pirate.reset_catchphrase! + assert_deprecated do + pirate.reset_catchphrase! + end + assert_equal "Yar!", pirate.catchphrase + assert_equal Hash.new, pirate.changes + assert !pirate.catchphrase_changed? + end + + def test_restore_attribute! + pirate = Pirate.create!(:catchphrase => 'Yar!') + pirate.catchphrase = 'Ahoy!' + + pirate.restore_catchphrase! assert_equal "Yar!", pirate.catchphrase assert_equal Hash.new, pirate.changes assert !pirate.catchphrase_changed? @@ -309,16 +321,14 @@ class DirtyTest < ActiveRecord::TestCase def test_attribute_will_change! pirate = Pirate.create!(:catchphrase => 'arr') - pirate.catchphrase << ' matey' assert !pirate.catchphrase_changed? - assert pirate.catchphrase_will_change! assert pirate.catchphrase_changed? - assert_equal ['arr matey', 'arr matey'], pirate.catchphrase_change + assert_equal ['arr', 'arr'], pirate.catchphrase_change - pirate.catchphrase << '!' + pirate.catchphrase << ' matey!' assert pirate.catchphrase_changed? - assert_equal ['arr matey', 'arr matey!'], pirate.catchphrase_change + assert_equal ['arr', 'arr matey!'], pirate.catchphrase_change end def test_association_assignment_changes_foreign_key @@ -400,7 +410,7 @@ class DirtyTest < ActiveRecord::TestCase def test_dup_objects_should_not_copy_dirty_flag_from_creator pirate = Pirate.create!(:catchphrase => "shiver me timbers") pirate_dup = pirate.dup - pirate_dup.reset_catchphrase! + pirate_dup.restore_catchphrase! pirate.catchphrase = "I love Rum" assert pirate.catchphrase_changed? assert !pirate_dup.catchphrase_changed? @@ -445,11 +455,20 @@ class DirtyTest < ActiveRecord::TestCase def test_save_should_store_serialized_attributes_even_with_partial_writes with_partial_writes(Topic) do topic = Topic.create!(:content => {:a => "a"}) + + assert_not topic.changed? + topic.content[:b] = "b" - #assert topic.changed? # Known bug, will fail + + assert topic.changed? + topic.save! + + assert_not topic.changed? assert_equal "b", topic.content[:b] + topic.reload + assert_equal "b", topic.content[:b] end end @@ -616,6 +635,32 @@ class DirtyTest < ActiveRecord::TestCase end end + test "defaults with type that implements `type_cast_for_database`" do + type = Class.new(ActiveRecord::Type::Value) do + def type_cast(value) + value.to_i + end + + def type_cast_for_database(value) + value.to_s + end + end + + model_class = Class.new(ActiveRecord::Base) do + self.table_name = 'numeric_data' + attribute :foo, type.new, default: 1 + end + + model = model_class.new + assert_not model.foo_changed? + + model = model_class.new(foo: 1) + assert_not model.foo_changed? + + model = model_class.new(foo: '1') + assert_not model.foo_changed? + end + private def with_partial_writes(klass, on = true) old = klass.partial_writes? diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb index 1e6ccecfab..638cffe0e6 100644 --- a/activerecord/test/cases/dup_test.rb +++ b/activerecord/test/cases/dup_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require 'models/reply' require 'models/topic' module ActiveRecord @@ -32,6 +33,14 @@ module ActiveRecord assert duped.new_record?, 'topic is new' end + def test_dup_not_destroyed + topic = Topic.first + topic.destroy + + duped = topic.dup + assert_not duped.destroyed? + end + def test_dup_has_no_id topic = Topic.first duped = topic.dup @@ -132,5 +141,17 @@ module ActiveRecord ensure Topic.default_scopes = prev_default_scopes end + + def test_dup_without_primary_key + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'parrots_pirates' + end + + record = klass.create! + + assert_nothing_raised do + record.dup + end + end end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index c0440744e9..40e51a0cdc 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -33,6 +33,17 @@ class FinderTest < ActiveRecord::TestCase assert_equal(topics(:first).title, Topic.find(1).title) end + def test_find_with_proc_parameter_and_block + exception = assert_raises(RuntimeError) do + Topic.all.find(-> { raise "should happen" }) { |e| e.title == "non-existing-title" } + end + assert_equal "should happen", exception.message + + assert_nothing_raised(RuntimeError) do + Topic.all.find(-> { raise "should not happen" }) { |e| e.title == topics(:first).title } + end + end + def test_find_passing_active_record_object_is_deprecated assert_deprecated do Topic.find(Topic.last) @@ -133,8 +144,8 @@ class FinderTest < ActiveRecord::TestCase def test_exists_with_distinct_association_includes_limit_and_order author = Author.first - assert_equal false, author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(0).exists? - assert_equal true, author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(1).exists? + assert_equal false, author.unique_categorized_posts.includes(:special_comments).order('comments.tags_count DESC').limit(0).exists? + assert_equal true, author.unique_categorized_posts.includes(:special_comments).order('comments.tags_count DESC').limit(1).exists? end def test_exists_with_empty_table_and_no_args_given diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 8bbc0af758..042fdaf0bb 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -84,12 +84,6 @@ class FixturesTest < ActiveRecord::TestCase assert fixtures.detect { |f| f.name == 'collections' }, "no fixtures named 'collections' in #{fixtures.map(&:name).inspect}" end - def test_create_symbol_fixtures_is_deprecated - assert_deprecated do - ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT, :collections, :collections => 'Course') { Course.connection } - end - end - def test_attributes topics = create_fixtures("topics").first assert_equal("The First Topic", topics["first"]["title"]) diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index eaf2cada9d..6a8aff4b69 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -9,6 +9,7 @@ require 'active_record' require 'cases/test_case' require 'active_support/dependencies' require 'active_support/logger' +require 'active_support/core_ext/string/strip' require 'support/config' require 'support/connection' @@ -198,3 +199,5 @@ module InTimeZone ActiveRecord::Base.time_zone_aware_attributes = old_tz end end + +require 'mocha/setup' # FIXME: stop using mocha diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index f5f85f2412..792950d24d 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -95,16 +95,8 @@ class InheritanceTest < ActiveRecord::TestCase end def test_a_bad_type_column - #SQLServer need to turn Identity Insert On before manually inserting into the Identity column - if current_adapter?(:SybaseAdapter) - Company.connection.execute "SET IDENTITY_INSERT companies ON" - end Company.connection.insert "INSERT INTO companies (id, #{QUOTED_TYPE}, name) VALUES(100, 'bad_class!', 'Not happening')" - #We then need to turn it back Off before continuing. - if current_adapter?(:SybaseAdapter) - Company.connection.execute "SET IDENTITY_INSERT companies OFF" - end assert_raise(ActiveRecord::SubclassNotFound) { Company.find(100) } end diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index c373dc1511..0c9dff2c25 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -273,8 +273,11 @@ class OptimisticLockingTest < ActiveRecord::TestCase 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 } + def test_yaml_dumping_with_lock_column + t1 = LockWithoutDefault.new + t2 = YAML.load(YAML.dump(t1)) + + assert_equal t1.attributes, t2.attributes end end @@ -339,8 +342,6 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase def add_counter_column_to(model, col='test_count') model.connection.add_column model.table_name, col, :integer, :null => false, :default => 0 model.reset_column_information - # OpenBase does not set a value to existing rows when adding a not null default column - model.update_all(col => 0) if current_adapter?(:OpenBaseAdapter) end def remove_counter_column_from(model, col = :test_count) @@ -367,7 +368,7 @@ end # is so cumbersome. Will deadlock Ruby threads if the underlying db.execute # blocks, so separate script called by Kernel#system is needed. # (See exec vs. async_exec in the PostgreSQL adapter.) -unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) || in_memory_db? +unless in_memory_db? class PessimisticLockingTest < ActiveRecord::TestCase self.use_transactional_fixtures = false fixtures :people, :readers diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index 97c0350911..a578e81844 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -125,5 +125,12 @@ class LogSubscriberTest < ActiveRecord::TestCase wait assert_match(/<16 bytes of binary data>/, @logger.logged(:debug).join) end + + def test_nil_binary_data_is_logged + binary = Binary.create(data: "") + binary.update_attributes(data: nil) + wait + assert_match(/<NULL 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 5418d913b0..c66eaf1ee1 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -14,6 +14,7 @@ module ActiveRecord teardown do connection.drop_table :testings rescue nil ActiveRecord::Base.primary_key_prefix_type = nil + ActiveRecord::Base.clear_cache! end def test_create_table_without_id @@ -67,9 +68,9 @@ module ActiveRecord five = columns.detect { |c| c.name == "five" } unless mysql assert_equal "hello", one.default - assert_equal true, two.default - assert_equal false, three.default - assert_equal 1, four.default + assert_equal true, two.type_cast_from_database(two.default) + assert_equal false, three.type_cast_from_database(three.default) + assert_equal '1', four.default assert_equal "hello", five.default unless mysql end @@ -204,9 +205,9 @@ module ActiveRecord connection.create_table table_name end - # Sybase, and SQLite3 will not allow you to add a NOT NULL + # SQLite3 will not allow you to add a NOT NULL # column to a table without a default value. - unless current_adapter?(:SybaseAdapter, :SQLite3Adapter) + unless current_adapter?(:SQLite3Adapter) def test_add_column_not_null_without_default connection.create_table :testings do |t| t.column :foo, :string @@ -225,18 +226,28 @@ module ActiveRecord end con = connection - connection.enable_identity_insert("testings", true) if current_adapter?(:SybaseAdapter) connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}) values (1, 'hello')" - connection.enable_identity_insert("testings", false) if current_adapter?(:SybaseAdapter) assert_nothing_raised {connection.add_column :testings, :bar, :string, :null => false, :default => "default" } assert_raises(ActiveRecord::StatementInvalid) do - unless current_adapter?(:OpenBaseAdapter) - connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) values (2, 'hello', NULL)" - else - connection.insert("INSERT INTO testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) VALUES (2, 'hello', NULL)", - "Testing Insert","id",2) - end + connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) values (2, 'hello', NULL)" + end + end + + def test_add_column_with_timestamp_type + connection.create_table :testings do |t| + t.column :foo, :timestamp + end + + klass = Class.new(ActiveRecord::Base) + klass.table_name = 'testings' + + assert_equal :datetime, klass.columns_hash['foo'].type + + if current_adapter?(:PostgreSQLAdapter) + assert_equal 'timestamp without time zone', klass.columns_hash['foo'].sql_type + else + assert_equal klass.connection.type_to_sql('datetime'), klass.columns_hash['foo'].sql_type end end @@ -264,7 +275,7 @@ module ActiveRecord person_klass.connection.add_column "testings", "wealth", :integer, :null => false, :default => 99 person_klass.reset_column_information - assert_equal 99, person_klass.columns_hash["wealth"].default + assert_equal 99, person_klass.column_defaults["wealth"] assert_equal false, person_klass.columns_hash["wealth"].null # Oracle needs primary key value from sequence if current_adapter?(:OracleAdapter) @@ -276,20 +287,20 @@ module ActiveRecord # change column default to see that column doesn't lose its not null definition person_klass.connection.change_column_default "testings", "wealth", 100 person_klass.reset_column_information - assert_equal 100, person_klass.columns_hash["wealth"].default + assert_equal 100, person_klass.column_defaults["wealth"] assert_equal false, person_klass.columns_hash["wealth"].null # rename column to see that column doesn't lose its not null and/or default definition person_klass.connection.rename_column "testings", "wealth", "money" person_klass.reset_column_information assert_nil person_klass.columns_hash["wealth"] - assert_equal 100, person_klass.columns_hash["money"].default + assert_equal 100, person_klass.column_defaults["money"] assert_equal false, person_klass.columns_hash["money"].null # change column person_klass.connection.change_column "testings", "money", :integer, :null => false, :default => 1000 person_klass.reset_column_information - assert_equal 1000, person_klass.columns_hash["money"].default + assert_equal 1000, person_klass.column_defaults["money"] assert_equal false, person_klass.columns_hash["money"].null # change column, make it nullable and clear default diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb index a6d506b04a..3e9d957ed3 100644 --- a/activerecord/test/cases/migration/change_table_test.rb +++ b/activerecord/test/cases/migration/change_table_test.rb @@ -72,6 +72,20 @@ module ActiveRecord end end + def test_references_column_type_with_polymorphic_and_type + with_change_table do |t| + @connection.expect :add_reference, nil, [:delete_me, :taggable, polymorphic: true, type: :string] + t.references :taggable, polymorphic: true, type: :string + end + end + + def test_remove_references_column_type_with_polymorphic_and_type + with_change_table do |t| + @connection.expect :remove_reference, nil, [:delete_me, :taggable, polymorphic: true, type: :string] + t.remove_references :taggable, polymorphic: true, type: :string + end + end + def test_timestamps_creates_updated_at_and_created_at with_change_table do |t| @connection.expect :add_timestamps, nil, [:delete_me] diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index 6a02873cba..763aa88f72 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -51,46 +51,46 @@ module ActiveRecord end end - # We specifically do a manual INSERT here, and then test only the SELECT - # functionality. This allows us to more easily catch INSERT being broken, - # but SELECT actually working fine. - def test_native_decimal_insert_manual_vs_automatic - correct_value = '0012345678901234567890.0123456789'.to_d - - connection.add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10' - - # Do a manual insertion - if current_adapter?(:OracleAdapter) - connection.execute "insert into test_models (id, wealth) values (people_seq.nextval, 12345678901234567890.0123456789)" - elsif current_adapter?(:OpenBaseAdapter) || (current_adapter?(:MysqlAdapter) && Mysql.client_version < 50003) #before mysql 5.0.3 decimals stored as strings - connection.execute "insert into test_models (wealth) values ('12345678901234567890.0123456789')" - elsif current_adapter?(:PostgreSQLAdapter) - connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)" - else - connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)" - end + unless current_adapter?(:SQLite3Adapter) + # We specifically do a manual INSERT here, and then test only the SELECT + # functionality. This allows us to more easily catch INSERT being broken, + # but SELECT actually working fine. + def test_native_decimal_insert_manual_vs_automatic + correct_value = '0012345678901234567890.0123456789'.to_d + + connection.add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10' + + # Do a manual insertion + if current_adapter?(:OracleAdapter) + connection.execute "insert into test_models (id, wealth) values (people_seq.nextval, 12345678901234567890.0123456789)" + elsif current_adapter?(:MysqlAdapter) && Mysql.client_version < 50003 #before MySQL 5.0.3 decimals stored as strings + connection.execute "insert into test_models (wealth) values ('12345678901234567890.0123456789')" + elsif current_adapter?(:PostgreSQLAdapter) + connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)" + else + connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)" + end - # SELECT - row = TestModel.first - assert_kind_of BigDecimal, row.wealth + # SELECT + row = TestModel.first + assert_kind_of BigDecimal, row.wealth - # If this assert fails, that means the SELECT is broken! - unless current_adapter?(:SQLite3Adapter) - assert_equal correct_value, row.wealth - end + # If this assert fails, that means the SELECT is broken! + unless current_adapter?(:SQLite3Adapter) + assert_equal correct_value, row.wealth + end - # Reset to old state - TestModel.delete_all + # Reset to old state + TestModel.delete_all - # Now use the Rails insertion - TestModel.create :wealth => BigDecimal.new("12345678901234567890.0123456789") + # Now use the Rails insertion + TestModel.create :wealth => BigDecimal.new("12345678901234567890.0123456789") - # SELECT - row = TestModel.first - assert_kind_of BigDecimal, row.wealth + # SELECT + row = TestModel.first + assert_kind_of BigDecimal, row.wealth - # If these asserts fail, that means the INSERT (create function, or cast to SQL) is broken! - unless current_adapter?(:SQLite3Adapter) + # If these asserts fail, that means the INSERT (create function, or cast to SQL) is broken! assert_equal correct_value, row.wealth end end @@ -121,54 +121,54 @@ module ActiveRecord end end - def test_native_types - add_column "test_models", "first_name", :string - add_column "test_models", "last_name", :string - add_column "test_models", "bio", :text - add_column "test_models", "age", :integer - add_column "test_models", "height", :float - add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10' - add_column "test_models", "birthday", :datetime - add_column "test_models", "favorite_day", :date - add_column "test_models", "moment_of_truth", :datetime - add_column "test_models", "male", :boolean - - TestModel.create :first_name => 'bob', :last_name => 'bobsen', - :bio => "I was born ....", :age => 18, :height => 1.78, - :wealth => BigDecimal.new("12345678901234567890.0123456789"), - :birthday => 18.years.ago, :favorite_day => 10.days.ago, - :moment_of_truth => "1782-10-10 21:40:18", :male => true - - bob = TestModel.first - assert_equal 'bob', bob.first_name - assert_equal 'bobsen', bob.last_name - assert_equal "I was born ....", bob.bio - assert_equal 18, bob.age - - # Test for 30 significant digits (beyond the 16 of float), 10 of them - # after the decimal place. - - unless current_adapter?(:SQLite3Adapter) + unless current_adapter?(:SQLite3Adapter) + def test_native_types + add_column "test_models", "first_name", :string + add_column "test_models", "last_name", :string + add_column "test_models", "bio", :text + add_column "test_models", "age", :integer + add_column "test_models", "height", :float + add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10' + add_column "test_models", "birthday", :datetime + add_column "test_models", "favorite_day", :date + add_column "test_models", "moment_of_truth", :datetime + add_column "test_models", "male", :boolean + + TestModel.create :first_name => 'bob', :last_name => 'bobsen', + :bio => "I was born ....", :age => 18, :height => 1.78, + :wealth => BigDecimal.new("12345678901234567890.0123456789"), + :birthday => 18.years.ago, :favorite_day => 10.days.ago, + :moment_of_truth => "1782-10-10 21:40:18", :male => true + + bob = TestModel.first + assert_equal 'bob', bob.first_name + assert_equal 'bobsen', bob.last_name + assert_equal "I was born ....", bob.bio + assert_equal 18, bob.age + + # Test for 30 significant digits (beyond the 16 of float), 10 of them + # after the decimal place. + assert_equal BigDecimal.new("0012345678901234567890.0123456789"), bob.wealth - end - assert_equal true, bob.male? + assert_equal true, bob.male? - assert_equal String, bob.first_name.class - assert_equal String, bob.last_name.class - assert_equal String, bob.bio.class - assert_equal Fixnum, bob.age.class - assert_equal Time, bob.birthday.class + assert_equal String, bob.first_name.class + assert_equal String, bob.last_name.class + assert_equal String, bob.bio.class + assert_equal Fixnum, bob.age.class + assert_equal Time, bob.birthday.class - if current_adapter?(:OracleAdapter, :SybaseAdapter) - # Sybase, and Oracle don't differentiate between date/time - assert_equal Time, bob.favorite_day.class - else - assert_equal Date, bob.favorite_day.class - end + if current_adapter?(:OracleAdapter) + # Oracle doesn't differentiate between date/time + assert_equal Time, bob.favorite_day.class + else + assert_equal Date, bob.favorite_day.class + end - assert_instance_of TrueClass, bob.male? - assert_kind_of BigDecimal, bob.wealth + assert_instance_of TrueClass, bob.male? + assert_kind_of BigDecimal, bob.wealth + end end if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb index 2d7a7ec73a..4e6d7963aa 100644 --- a/activerecord/test/cases/migration/columns_test.rb +++ b/activerecord/test/cases/migration/columns_test.rb @@ -53,13 +53,13 @@ module ActiveRecord add_column 'test_models', 'salary', :integer, :default => 70000 default_before = connection.columns("test_models").find { |c| c.name == "salary" }.default - assert_equal 70000, default_before + assert_equal '70000', default_before rename_column "test_models", "salary", "annual_salary" assert TestModel.column_names.include?("annual_salary") default_after = connection.columns("test_models").find { |c| c.name == "annual_salary" }.default - assert_equal 70000, default_after + assert_equal '70000', default_after end if current_adapter?(:MysqlAdapter, :Mysql2Adapter) @@ -193,14 +193,21 @@ module ActiveRecord old_columns = connection.columns(TestModel.table_name) assert old_columns.find { |c| - c.name == 'approved' && c.type == :boolean && c.default == true + default = c.type_cast_from_database(c.default) + c.name == 'approved' && c.type == :boolean && default == true } change_column :test_models, :approved, :boolean, :default => false new_columns = connection.columns(TestModel.table_name) - assert_not new_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == true } - assert new_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == false } + assert_not new_columns.find { |c| + default = c.type_cast_from_database(c.default) + c.name == 'approved' and c.type == :boolean and default == true + } + assert new_columns.find { |c| + default = c.type_cast_from_database(c.default) + c.name == 'approved' and c.type == :boolean and default == false + } change_column :test_models, :approved, :boolean, :default => true end @@ -274,6 +281,16 @@ module ActiveRecord ensure connection.drop_table(:my_table) rescue nil end + + def test_column_with_index + connection.create_table "my_table", force: true do |t| + t.string :item_number, index: true + end + + assert connection.index_exists?("my_table", :item_number, name: :index_my_table_on_item_number) + ensure + connection.drop_table(:my_table) rescue nil + end end end end diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index a925cf4c05..e955beae1a 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -157,6 +157,23 @@ module ActiveRecord assert_equal [:remove_column, [:table, :column, :type, {}], nil], remove end + def test_invert_change_column + assert_raises(ActiveRecord::IrreversibleMigration) do + @recorder.inverse_of :change_column, [:table, :column, :type, {}] + end + end + + def test_invert_change_column_default + assert_raises(ActiveRecord::IrreversibleMigration) do + @recorder.inverse_of :change_column_default, [:table, :column, 'default_value'] + end + end + + def test_invert_change_column_null + add = @recorder.inverse_of :change_column_null, [:table, :column, true] + assert_equal [:change_column_null, [:table, :column, false]], add + end + def test_invert_remove_column add = @recorder.inverse_of :remove_column, [:table, :column, :type, {}] assert_equal [:add_column, [:table, :column, :type, {}], nil], add @@ -253,6 +270,31 @@ module ActiveRecord enable = @recorder.inverse_of :disable_extension, ['uuid-ossp'] assert_equal [:enable_extension, ['uuid-ossp'], nil], enable end + + def test_invert_add_foreign_key + enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people] + assert_equal [:remove_foreign_key, [:dogs, :people]], enable + end + + def test_invert_add_foreign_key_with_column + enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id"] + assert_equal [:remove_foreign_key, [:dogs, column: "owner_id"]], enable + end + + def test_invert_add_foreign_key_with_column_and_name + enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id", name: "fk"] + assert_equal [:remove_foreign_key, [:dogs, name: "fk"]], enable + end + + def test_remove_foreign_key_is_irreversible + assert_raises ActiveRecord::IrreversibleMigration do + @recorder.inverse_of :remove_foreign_key, [:dogs, column: "owner_id"] + end + + assert_raises ActiveRecord::IrreversibleMigration do + @recorder.inverse_of :remove_foreign_key, [:dogs, name: "fk"] + end + end end end end diff --git a/activerecord/test/cases/migration/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb index 62b60f7f7b..bea9d6b2c9 100644 --- a/activerecord/test/cases/migration/create_join_table_test.rb +++ b/activerecord/test/cases/migration/create_join_table_test.rb @@ -119,6 +119,30 @@ module ActiveRecord assert !connection.tables.include?('artists_musics') end + + def test_create_and_drop_join_table_with_common_prefix + with_table_cleanup do + connection.create_join_table 'audio_artists', 'audio_musics' + assert_includes connection.tables, 'audio_artists_musics' + + connection.drop_join_table 'audio_artists', 'audio_musics' + assert !connection.tables.include?('audio_artists_musics'), "Should have dropped join table, but didn't" + end + end + + private + + def with_table_cleanup + tables_before = connection.tables + + yield + ensure + tables_after = connection.tables - tables_before + + tables_after.each do |table| + connection.execute "DROP TABLE #{table}" + end + end end end end diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb new file mode 100644 index 0000000000..c985092b4c --- /dev/null +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -0,0 +1,242 @@ +require 'cases/helper' +require 'support/ddl_helper' +require 'support/schema_dumping_helper' + +if ActiveRecord::Base.connection.supports_foreign_keys? +module ActiveRecord + class Migration + class ForeignKeyTest < ActiveRecord::TestCase + include DdlHelper + include SchemaDumpingHelper + + class Rocket < ActiveRecord::Base + end + + class Astronaut < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table "rockets" do |t| + t.string :name + end + + @connection.create_table "astronauts" do |t| + t.string :name + t.references :rocket + end + end + + teardown do + if defined?(@connection) + @connection.execute "DROP TABLE IF EXISTS astronauts" + @connection.execute "DROP TABLE IF EXISTS rockets" + end + end + + def test_foreign_keys + foreign_keys = @connection.foreign_keys("fk_test_has_fk") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "fk_test_has_fk", fk.from_table + assert_equal "fk_test_has_pk", fk.to_table + assert_equal "fk_id", fk.column + assert_equal "pk_id", fk.primary_key + assert_equal "fk_name", fk.name + end + + def test_add_foreign_key_inferes_column + @connection.add_foreign_key :astronauts, :rockets + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "astronauts", fk.from_table + assert_equal "rockets", fk.to_table + assert_equal "rocket_id", fk.column + assert_equal "id", fk.primary_key + assert_match(/^fk_rails_.{10}$/, fk.name) + end + + def test_add_foreign_key_with_column + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id" + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "astronauts", fk.from_table + assert_equal "rockets", fk.to_table + assert_equal "rocket_id", fk.column + assert_equal "id", fk.primary_key + assert_match(/^fk_rails_.{10}$/, fk.name) + end + + def test_add_foreign_key_with_non_standard_primary_key + with_example_table @connection, "space_shuttles", "pk integer PRIMARY KEY" do + @connection.add_foreign_key(:astronauts, :space_shuttles, + column: "rocket_id", primary_key: "pk", name: "custom_pk") + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "astronauts", fk.from_table + assert_equal "space_shuttles", fk.to_table + assert_equal "pk", fk.primary_key + + @connection.remove_foreign_key :astronauts, name: "custom_pk" + end + end + + def test_add_on_delete_restrict_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :restrict + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + # ON DELETE RESTRICT is the default on MySQL + assert_equal nil, fk.on_delete + else + assert_equal :restrict, fk.on_delete + end + end + + def test_add_on_delete_cascade_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :cascade + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal :cascade, fk.on_delete + end + + def test_add_on_delete_nullify_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal :nullify, fk.on_delete + end + + def test_on_update_and_on_delete_raises_with_invalid_values + assert_raises ArgumentError do + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :invalid + end + + assert_raises ArgumentError do + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :invalid + end + end + + def test_add_foreign_key_with_on_update + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :nullify + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal :nullify, fk.on_update + end + + def test_remove_foreign_key_inferes_column + @connection.add_foreign_key :astronauts, :rockets + + assert_equal 1, @connection.foreign_keys("astronauts").size + @connection.remove_foreign_key :astronauts, :rockets + assert_equal [], @connection.foreign_keys("astronauts") + end + + def test_remove_foreign_key_by_column + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id" + + assert_equal 1, @connection.foreign_keys("astronauts").size + @connection.remove_foreign_key :astronauts, column: "rocket_id" + assert_equal [], @connection.foreign_keys("astronauts") + end + + def test_remove_foreign_key_by_name + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk" + + assert_equal 1, @connection.foreign_keys("astronauts").size + @connection.remove_foreign_key :astronauts, name: "fancy_named_fk" + assert_equal [], @connection.foreign_keys("astronauts") + end + + def test_remove_foreign_non_existing_foreign_key_raises + assert_raises ArgumentError do + @connection.remove_foreign_key :astronauts, :rockets + end + end + + def test_schema_dumping + @connection.add_foreign_key :astronauts, :rockets + output = dump_table_schema "astronauts" + assert_match %r{\s+add_foreign_key "astronauts", "rockets"$}, output + end + + def test_schema_dumping_with_options + output = dump_table_schema "fk_test_has_fk" + assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "pk_id", name: "fk_name"$}, output + end + + def test_schema_dumping_on_delete_and_on_update_options + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify, on_update: :cascade + + output = dump_table_schema "astronauts" + assert_match %r{\s+add_foreign_key "astronauts",.+on_update: :cascade,.+on_delete: :nullify$}, output + end + + class CreateCitiesAndHousesMigration < ActiveRecord::Migration + def change + create_table("cities") { |t| } + + create_table("houses") do |t| + t.column :city_id, :integer + end + add_foreign_key :houses, :cities, column: "city_id" + end + end + + def test_add_foreign_key_is_reversible + migration = CreateCitiesAndHousesMigration.new + silence_stream($stdout) { migration.migrate(:up) } + assert_equal 1, @connection.foreign_keys("houses").size + ensure + silence_stream($stdout) { migration.migrate(:down) } + end + end + end +end +else +module ActiveRecord + class Migration + class NoForeignKeySupportTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + end + + def test_add_foreign_key_should_be_noop + @connection.add_foreign_key :clubs, :categories + end + + def test_remove_foreign_key_should_be_noop + @connection.remove_foreign_key :clubs, :categories + end + + def test_foreign_keys_should_raise_not_implemented + assert_raises NotImplementedError do + @connection.foreign_keys("clubs") + end + end + end + end +end +end diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb index 35af11f672..93c3bfae7a 100644 --- a/activerecord/test/cases/migration/index_test.rb +++ b/activerecord/test/cases/migration/index_test.rb @@ -26,28 +26,25 @@ module ActiveRecord ActiveRecord::Base.primary_key_prefix_type = nil end - 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 + 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 - def test_double_add_index + def test_double_add_index + connection.add_index(table_name, [:foo], :name => 'some_idx') + assert_raises(ArgumentError) { connection.add_index(table_name, [:foo], :name => 'some_idx') - assert_raises(ArgumentError) { - connection.add_index(table_name, [:foo], :name => 'some_idx') - } - end + } + end - 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 + def test_remove_nonexistent_index + assert_raise(ArgumentError) { connection.remove_index(table_name, "no_such_index") } end def test_add_index_works_with_long_index_names @@ -126,50 +123,37 @@ module ActiveRecord connection.add_index("testings", "last_name") connection.remove_index("testings", "last_name") - # Orcl nds shrt indx nms. Sybs 2. - # OpenBase does not have named indexes. You must specify a single column name - unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) + connection.add_index("testings", ["last_name", "first_name"]) + connection.remove_index("testings", :column => ["last_name", "first_name"]) + + # Oracle adapter cannot have specified index name larger than 30 characters + # Oracle adapter is shortening index name when just column list is given + unless current_adapter?(:OracleAdapter) connection.add_index("testings", ["last_name", "first_name"]) - connection.remove_index("testings", :column => ["last_name", "first_name"]) - - # Oracle adapter cannot have specified index name larger than 30 characters - # Oracle adapter is shortening index name when just column list is given - unless current_adapter?(:OracleAdapter) - connection.add_index("testings", ["last_name", "first_name"]) - connection.remove_index("testings", :name => :index_testings_on_last_name_and_first_name) - connection.add_index("testings", ["last_name", "first_name"]) - connection.remove_index("testings", "last_name_and_first_name") - end + connection.remove_index("testings", :name => :index_testings_on_last_name_and_first_name) connection.add_index("testings", ["last_name", "first_name"]) - connection.remove_index("testings", ["last_name", "first_name"]) + connection.remove_index("testings", "last_name_and_first_name") + end + connection.add_index("testings", ["last_name", "first_name"]) + connection.remove_index("testings", ["last_name", "first_name"]) - connection.add_index("testings", ["last_name"], :length => 10) - connection.remove_index("testings", "last_name") + connection.add_index("testings", ["last_name"], :length => 10) + connection.remove_index("testings", "last_name") - connection.add_index("testings", ["last_name"], :length => {:last_name => 10}) - connection.remove_index("testings", ["last_name"]) + connection.add_index("testings", ["last_name"], :length => {:last_name => 10}) + connection.remove_index("testings", ["last_name"]) - connection.add_index("testings", ["last_name", "first_name"], :length => 10) - connection.remove_index("testings", ["last_name", "first_name"]) + connection.add_index("testings", ["last_name", "first_name"], :length => 10) + connection.remove_index("testings", ["last_name", "first_name"]) - connection.add_index("testings", ["last_name", "first_name"], :length => {:last_name => 10, :first_name => 20}) - connection.remove_index("testings", ["last_name", "first_name"]) - end + connection.add_index("testings", ["last_name", "first_name"], :length => {:last_name => 10, :first_name => 20}) + connection.remove_index("testings", ["last_name", "first_name"]) - # quoting - # Note: changed index name from "key" to "key_idx" since "key" is a Firebird reserved word - # OpenBase does not have named indexes. You must specify a single column name - unless current_adapter?(:OpenBaseAdapter) - connection.add_index("testings", ["key"], :name => "key_idx", :unique => true) - connection.remove_index("testings", :name => "key_idx", :unique => true) - end + connection.add_index("testings", ["key"], :name => "key_idx", :unique => true) + connection.remove_index("testings", :name => "key_idx", :unique => true) - # Sybase adapter does not support indexes on :boolean columns - # OpenBase does not have named indexes. You must specify a single column - unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) - connection.add_index("testings", %w(last_name first_name administrator), :name => "named_admin") - connection.remove_index("testings", :name => "named_admin") - end + connection.add_index("testings", %w(last_name first_name administrator), :name => "named_admin") + connection.remove_index("testings", :name => "named_admin") # Selected adapters support index sort order if current_adapter?(:SQLite3Adapter, :MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) diff --git a/activerecord/test/cases/migration/logger_test.rb b/activerecord/test/cases/migration/logger_test.rb index 84224e6e4c..319d3e1af3 100644 --- a/activerecord/test/cases/migration/logger_test.rb +++ b/activerecord/test/cases/migration/logger_test.rb @@ -3,7 +3,7 @@ require "cases/helper" module ActiveRecord class Migration class LoggerTest < ActiveRecord::TestCase - # mysql can't roll back ddl changes + # MySQL can't roll back ddl changes self.use_transactional_fixtures = false Migration = Struct.new(:name, :version) do diff --git a/activerecord/test/cases/migration/pending_migrations_test.rb b/activerecord/test/cases/migration/pending_migrations_test.rb new file mode 100644 index 0000000000..517ee695ce --- /dev/null +++ b/activerecord/test/cases/migration/pending_migrations_test.rb @@ -0,0 +1,53 @@ +require 'cases/helper' +require "minitest/mock" + +module ActiveRecord + class Migration + class PendingMigrationsTest < ActiveRecord::TestCase + def setup + super + @connection = MiniTest::Mock.new + @app = MiniTest::Mock.new + conn = @connection + @pending = Class.new(CheckPending) { + define_method(:connection) { conn } + }.new(@app) + @pending.instance_variable_set :@last_check, -1 # Force checking + end + + def teardown + assert @connection.verify + assert @app.verify + super + end + + def test_errors_if_pending + @connection.expect :supports_migrations?, true + + ActiveRecord::Migrator.stub :needs_migration?, true do + assert_raise ActiveRecord::PendingMigrationError do + @pending.call(nil) + end + end + end + + def test_checks_if_supported + @connection.expect :supports_migrations?, true + @app.expect :call, nil, [:foo] + + ActiveRecord::Migrator.stub :needs_migration?, false do + @pending.call(:foo) + end + end + + def test_doesnt_check_if_unsupported + @connection.expect :supports_migrations?, false + @app.expect :call, nil, [:foo] + + ActiveRecord::Migrator.stub :needs_migration?, true do + @pending.call(:foo) + end + end + end + end +end diff --git a/activerecord/test/cases/migration/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb index e9545f2cce..b8b4fa1135 100644 --- a/activerecord/test/cases/migration/references_statements_test.rb +++ b/activerecord/test/cases/migration/references_statements_test.rb @@ -55,6 +55,11 @@ module ActiveRecord assert index_exists?(table_name, :tag_id, name: 'index_taggings_on_tag_id') end + def test_creates_reference_id_with_specified_type + add_reference table_name, :user, type: :string + assert column_exists?(table_name, :user_id, :string) + end + def test_deletes_reference_id_column remove_reference table_name, :supplier assert_not column_exists?(table_name, :supplier_id, :integer) diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index 2a7fafc559..ba39fb1dec 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -42,13 +42,8 @@ module ActiveRecord def test_rename_table rename_table :test_models, :octopi - # Using explicit id in insert for compatibility across all databases - connection.enable_identity_insert("octopi", true) if current_adapter?(:SybaseAdapter) - connection.execute "INSERT INTO octopi (#{connection.quote_column_name('id')}, #{connection.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')" - connection.enable_identity_insert("octopi", false) if current_adapter?(:SybaseAdapter) - assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', connection.select_value("SELECT url FROM octopi WHERE id=1") end @@ -57,10 +52,7 @@ module ActiveRecord rename_table :test_models, :octopi - # Using explicit id in insert for compatibility across all databases - connection.enable_identity_insert("octopi", true) if current_adapter?(:SybaseAdapter) connection.execute "INSERT INTO octopi (#{connection.quote_column_name('id')}, #{connection.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')" - connection.enable_identity_insert("octopi", false) if current_adapter?(:SybaseAdapter) assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', connection.select_value("SELECT url FROM octopi WHERE id=1") index = connection.indexes(:octopi).first @@ -82,7 +74,17 @@ module ActiveRecord pk, seq = connection.pk_and_sequence_for('octopi') - assert_equal "octopi_#{pk}_seq", seq + assert_equal ConnectionAdapters::PostgreSQL::Name.new("public", "octopi_#{pk}_seq"), seq + end + + def test_renaming_table_doesnt_attempt_to_rename_non_existent_sequences + enable_uuid_ossp!(connection) + connection.create_table :cats, id: :uuid + assert_nothing_raised { rename_table :cats, :felines } + assert connection.table_exists? :felines + ensure + connection.drop_table :cats if connection.table_exists? :cats + connection.drop_table :felines if connection.table_exists? :felines end end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 455ec78f68..ef3f073472 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -11,7 +11,13 @@ require MIGRATIONS_ROOT + "/rename/1_we_need_things" require MIGRATIONS_ROOT + "/rename/2_rename_things" require MIGRATIONS_ROOT + "/decimal/1_give_me_big_numbers" -class BigNumber < ActiveRecord::Base; end +class BigNumber < ActiveRecord::Base + unless current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter) + attribute :value_of_e, Type::Integer.new + end + attribute :world_population, Type::Integer.new + attribute :my_house_population, Type::Integer.new +end class Reminder < ActiveRecord::Base; end @@ -57,7 +63,7 @@ class MigrationTest < ActiveRecord::TestCase end Person.connection.remove_column("people", "first_name") rescue nil Person.connection.remove_column("people", "middle_name") rescue nil - Person.connection.add_column("people", "first_name", :string, :limit => 40) + Person.connection.add_column("people", "first_name", :string) Person.reset_column_information end @@ -327,47 +333,6 @@ class MigrationTest < ActiveRecord::TestCase Reminder.reset_table_name end - def test_proper_table_name_on_migrator - reminder_class = new_isolated_reminder_class - assert_deprecated do - assert_equal "table", ActiveRecord::Migrator.proper_table_name('table') - end - assert_deprecated do - assert_equal "table", ActiveRecord::Migrator.proper_table_name(:table) - end - assert_deprecated do - assert_equal "reminders", ActiveRecord::Migrator.proper_table_name(reminder_class) - end - reminder_class.reset_table_name - assert_deprecated do - assert_equal reminder_class.table_name, ActiveRecord::Migrator.proper_table_name(reminder_class) - end - - # Use the model's own prefix/suffix if a model is given - ActiveRecord::Base.table_name_prefix = "ARprefix_" - ActiveRecord::Base.table_name_suffix = "_ARsuffix" - reminder_class.table_name_prefix = 'prefix_' - reminder_class.table_name_suffix = '_suffix' - reminder_class.reset_table_name - assert_deprecated do - assert_equal "prefix_reminders_suffix", ActiveRecord::Migrator.proper_table_name(reminder_class) - end - reminder_class.table_name_prefix = '' - reminder_class.table_name_suffix = '' - reminder_class.reset_table_name - - # Use AR::Base's prefix/suffix if string or symbol is given - ActiveRecord::Base.table_name_prefix = "prefix_" - ActiveRecord::Base.table_name_suffix = "_suffix" - reminder_class.reset_table_name - assert_deprecated do - assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name('table') - end - assert_deprecated do - assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name(:table) - end - end - def test_proper_table_name_on_migration reminder_class = new_isolated_reminder_class migration = ActiveRecord::Migration.new @@ -602,7 +567,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? assert_equal 8, columns.size [:name, :qualification, :experience].each {|s| assert_equal :string, column(s).type } - assert_equal 0, column(:age).default + assert_equal '0', column(:age).default end def test_removing_columns @@ -930,4 +895,14 @@ class CopyMigrationsTest < ActiveRecord::TestCase ensure ActiveRecord::Base.logger = old end + + private + + def quietly + silence_stream(STDOUT) do + silence_stream(STDERR) do + yield + end + end + end end diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb index f7db195521..e87773df94 100644 --- a/activerecord/test/cases/modules_test.rb +++ b/activerecord/test/cases/modules_test.rb @@ -112,6 +112,34 @@ class ModulesTest < ActiveRecord::TestCase classes.each(&:reset_table_name) end + def test_module_table_name_suffix + assert_equal 'companies_suffixed', MyApplication::Business::Suffixed::Company.table_name, 'inferred table_name for ActiveRecord model in module with table_name_suffix' + assert_equal 'companies_suffixed', MyApplication::Business::Suffixed::Nested::Company.table_name, 'table_name for ActiveRecord model in nested module with a parent table_name_suffix' + assert_equal 'companies', MyApplication::Business::Suffixed::Firm.table_name, 'explicit table_name for ActiveRecord model in module with table_name_suffix should not be suffixed' + end + + def test_module_table_name_suffix_with_global_suffix + classes = [ MyApplication::Business::Company, + MyApplication::Business::Firm, + MyApplication::Business::Client, + MyApplication::Business::Client::Contact, + MyApplication::Business::Developer, + MyApplication::Business::Project, + MyApplication::Business::Suffixed::Company, + MyApplication::Business::Suffixed::Nested::Company, + MyApplication::Billing::Account ] + + ActiveRecord::Base.table_name_suffix = '_global' + classes.each(&:reset_table_name) + assert_equal 'companies_global', MyApplication::Business::Company.table_name, 'inferred table_name for ActiveRecord model in module without table_name_suffix' + assert_equal 'companies_suffixed', MyApplication::Business::Suffixed::Company.table_name, 'inferred table_name for ActiveRecord model in module with table_name_suffix' + assert_equal 'companies_suffixed', MyApplication::Business::Suffixed::Nested::Company.table_name, 'table_name for ActiveRecord model in nested module with a parent table_name_suffix' + assert_equal 'companies', MyApplication::Business::Suffixed::Firm.table_name, 'explicit table_name for ActiveRecord model in module with table_name_suffix should not be suffixed' + ensure + ActiveRecord::Base.table_name_suffix = '' + classes.each(&:reset_table_name) + end + def test_compute_type_can_infer_class_name_of_sibling_inside_module old = ActiveRecord::Base.store_full_sti_class ActiveRecord::Base.store_full_sti_class = true diff --git a/activerecord/test/cases/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb index c70a8f296f..14d4ef457d 100644 --- a/activerecord/test/cases/multiparameter_attributes_test.rb +++ b/activerecord/test/cases/multiparameter_attributes_test.rb @@ -240,8 +240,8 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase Topic.skip_time_zone_conversion_for_attributes = [] end - # Oracle, and Sybase do not have a TIME datatype. - unless current_adapter?(:OracleAdapter, :SybaseAdapter) + # Oracle does not have a TIME datatype. + unless current_adapter?(:OracleAdapter) def test_multiparameter_attributes_on_time_only_column_with_time_zone_aware_attributes_does_not_do_time_zone_conversion with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do attributes = { diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 9209672ac5..2170fe6118 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require 'models/aircraft' require 'models/post' require 'models/comment' require 'models/author' @@ -19,7 +20,7 @@ require 'models/toy' require 'rexml/document' class PersistenceTest < ActiveRecord::TestCase - fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts, :minivans, :pets, :toys + fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :author_addresses, :categorizations, :categories, :posts, :minivans, :pets, :toys # Oracle UPDATE does not support ORDER BY unless current_adapter?(:OracleAdapter) @@ -234,19 +235,12 @@ class PersistenceTest < ActiveRecord::TestCase end def test_save_with_duping_of_destroyed_object - developer = Developer.create(name: "Kuldeep") + developer = Developer.first developer.destroy new_developer = developer.dup new_developer.save assert new_developer.persisted? - end - - def test_dup_of_destroyed_object_is_not_destroyed - developer = Developer.create(name: "Kuldeep") - developer.destroy - new_developer = developer.dup - new_developer.save - assert_equal new_developer.destroyed?, false + assert_not new_developer.destroyed? end def test_create_many @@ -256,11 +250,9 @@ class PersistenceTest < ActiveRecord::TestCase end def test_create_columns_not_equal_attributes - topic = Topic.allocate.init_with( - 'attributes' => { - 'title' => 'Another New Topic', - 'does_not_exist' => 'test' - } + topic = Topic.instantiate( + 'title' => 'Another New Topic', + 'does_not_exist' => 'test' ) assert_nothing_raised { topic.save } end @@ -306,10 +298,7 @@ class PersistenceTest < ActiveRecord::TestCase topic.title = "Still another topic" topic.save - topic_reloaded = Topic.allocate - topic_reloaded.init_with( - 'attributes' => topic.attributes.merge('does_not_exist' => 'test') - ) + topic_reloaded = Topic.instantiate(topic.attributes.merge('does_not_exist' => 'test')) topic_reloaded.title = 'A New Topic' assert_nothing_raised { topic_reloaded.save } end @@ -339,6 +328,15 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal "Reply", topic.type end + def test_update_sti_subclass_type + assert_instance_of Topic, topics(:first) + + reply = topics(:first).becomes!(Reply) + assert_instance_of Reply, reply + reply.save! + assert_instance_of Reply, Reply.find(reply.id) + end + def test_update_after_create klass = Class.new(Topic) do def self.name; 'Topic'; end @@ -511,14 +509,14 @@ class PersistenceTest < ActiveRecord::TestCase def test_update_column_should_not_leave_the_object_dirty topic = Topic.find(1) - topic.update_column("content", "Have a nice day") + topic.update_column("content", "--- Have a nice day\n...\n") topic.reload - topic.update_column(:content, "You too") + topic.update_column(:content, "--- You too\n...\n") assert_equal [], topic.changed topic.reload - topic.update_column("content", "Have a nice day") + topic.update_column("content", "--- Have a nice day\n...\n") assert_equal [], topic.changed end @@ -602,14 +600,14 @@ class PersistenceTest < ActiveRecord::TestCase def test_update_columns_should_not_leave_the_object_dirty topic = Topic.find(1) - topic.update({ "content" => "Have a nice day", :author_name => "Jose" }) + topic.update({ "content" => "--- Have a nice day\n...\n", :author_name => "Jose" }) topic.reload - topic.update_columns({ content: "You too", "author_name" => "Sebastian" }) + topic.update_columns({ content: "--- You too\n...\n", "author_name" => "Sebastian" }) assert_equal [], topic.changed topic.reload - topic.update_columns({ content: "Have a nice day", author_name: "Jose" }) + topic.update_columns({ content: "--- Have a nice day\n...\n", author_name: "Jose" }) assert_equal [], topic.changed end @@ -836,4 +834,47 @@ class PersistenceTest < ActiveRecord::TestCase end end + def test_persist_inherited_class_with_different_table_name + minimalistic_aircrafts = Class.new(Minimalistic) do + self.table_name = "aircraft" + end + + assert_difference "Aircraft.count", 1 do + aircraft = minimalistic_aircrafts.create(name: "Wright Flyer") + aircraft.name = "Wright Glider" + aircraft.save + end + + assert_equal "Wright Glider", Aircraft.last.name + end + + def test_instantiate_creates_a_new_instance + post = Post.instantiate("title" => "appropriate documentation", "type" => "SpecialPost") + assert_equal "appropriate documentation", post.title + assert_instance_of SpecialPost, post + + # body was not initialized + assert_raises ActiveModel::MissingAttributeError do + post.body + end + end + + def test_reload_removes_custom_selects + post = Post.select('posts.*, 1 as wibble').last! + + assert_equal 1, post[:wibble] + assert_nil post.reload[:wibble] + end + + def test_find_via_reload + post = Post.new + + assert post.new_record? + + post.id = 1 + post.reload + + assert_equal "Welcome to the weblog", post.title + assert_not post.new_record? + end end diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb index dd0e934ec2..8eea10143f 100644 --- a/activerecord/test/cases/pooled_connections_test.rb +++ b/activerecord/test/cases/pooled_connections_test.rb @@ -48,4 +48,4 @@ class PooledConnectionsTest < ActiveRecord::TestCase def add_record(name) ActiveRecord::Base.connection_pool.with_connection { Project.create! :name => name } end -end unless current_adapter?(:FrontBase) || in_memory_db? +end unless in_memory_db? diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 51ddd406ed..b04df7ce43 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -92,6 +92,7 @@ class PrimaryKeysTest < ActiveRecord::TestCase end def test_primary_key_prefix + old_primary_key_prefix_type = ActiveRecord::Base.primary_key_prefix_type ActiveRecord::Base.primary_key_prefix_type = :table_name Topic.reset_primary_key assert_equal "topicid", Topic.primary_key @@ -103,6 +104,8 @@ class PrimaryKeysTest < ActiveRecord::TestCase ActiveRecord::Base.primary_key_prefix_type = nil Topic.reset_primary_key assert_equal "id", Topic.primary_key + ensure + ActiveRecord::Base.primary_key_prefix_type = old_primary_key_prefix_type end def test_delete_should_quote_pkey @@ -131,14 +134,22 @@ class PrimaryKeysTest < ActiveRecord::TestCase end def test_primary_key_returns_value_if_it_exists + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'developers' + end + if ActiveRecord::Base.connection.supports_primary_key? - assert_equal 'id', ActiveRecord::Base.connection.primary_key('developers') + assert_equal 'id', klass.primary_key end end def test_primary_key_returns_nil_if_it_does_not_exist + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'developers_projects' + end + if ActiveRecord::Base.connection.supports_primary_key? - assert_nil ActiveRecord::Base.connection.primary_key('developers_projects') + assert_nil klass.primary_key end end @@ -149,38 +160,6 @@ class PrimaryKeysTest < ActiveRecord::TestCase assert_equal k.connection.quote_column_name("foo"), k.quoted_primary_key end - def test_two_models_with_same_table_but_different_primary_key - k1 = Class.new(ActiveRecord::Base) - k1.table_name = 'posts' - k1.primary_key = 'id' - - k2 = Class.new(ActiveRecord::Base) - k2.table_name = 'posts' - k2.primary_key = 'title' - - assert k1.columns.find { |c| c.name == 'id' }.primary - assert !k1.columns.find { |c| c.name == 'title' }.primary - assert k1.columns_hash['id'].primary - assert !k1.columns_hash['title'].primary - - assert !k2.columns.find { |c| c.name == 'id' }.primary - assert k2.columns.find { |c| c.name == 'title' }.primary - assert !k2.columns_hash['id'].primary - assert k2.columns_hash['title'].primary - end - - def test_models_with_same_table_have_different_columns - k1 = Class.new(ActiveRecord::Base) - k1.table_name = 'posts' - - k2 = Class.new(ActiveRecord::Base) - k2.table_name = 'posts' - - k1.columns.zip(k2.columns).each do |col1, col2| - assert !col1.equal?(col2) - end - end - def test_auto_detect_primary_key_from_schema MixedCaseMonkey.reset_primary_key assert_equal "monkeyID", MixedCaseMonkey.primary_key @@ -219,3 +198,29 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter) end end end + +if current_adapter?(:PostgreSQLAdapter) + class PrimaryKeyBigSerialTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class Widget < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:widgets, id: :bigserial) { |t| } + end + + teardown do + @connection.drop_table :widgets + end + + def test_bigserial_primary_key + assert_equal "id", Widget.primary_key + assert_equal :integer, Widget.columns_hash[Widget.primary_key].type + + widget = Widget.create! + assert_not_nil widget.id + end + end +end diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index e2439b9a24..1d6ae2f67f 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -3,14 +3,6 @@ require "cases/helper" module ActiveRecord module ConnectionAdapters class QuotingTest < ActiveRecord::TestCase - class FakeColumn < ActiveRecord::ConnectionAdapters::Column - attr_accessor :type - - def initialize type - @type = type - end - end - def setup @quoter = Class.new { include Quoting }.new end @@ -91,69 +83,56 @@ module ActiveRecord def test_quote_with_quoted_id assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1), nil) - assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1), 'foo') end def test_quote_nil assert_equal 'NULL', @quoter.quote(nil, nil) - assert_equal 'NULL', @quoter.quote(nil, 'foo') end def test_quote_true assert_equal @quoter.quoted_true, @quoter.quote(true, nil) - assert_equal '1', @quoter.quote(true, Struct.new(:type).new(:integer)) end def test_quote_false assert_equal @quoter.quoted_false, @quoter.quote(false, nil) - assert_equal '0', @quoter.quote(false, Struct.new(:type).new(:integer)) end def test_quote_float float = 1.2 assert_equal float.to_s, @quoter.quote(float, nil) - assert_equal float.to_s, @quoter.quote(float, Object.new) end def test_quote_fixnum fixnum = 1 assert_equal fixnum.to_s, @quoter.quote(fixnum, nil) - assert_equal fixnum.to_s, @quoter.quote(fixnum, Object.new) end def test_quote_bignum bignum = 1 << 100 assert_equal bignum.to_s, @quoter.quote(bignum, nil) - assert_equal bignum.to_s, @quoter.quote(bignum, Object.new) end def test_quote_bigdecimal bigdec = BigDecimal.new((1 << 100).to_s) assert_equal bigdec.to_s('F'), @quoter.quote(bigdec, nil) - assert_equal bigdec.to_s('F'), @quoter.quote(bigdec, Object.new) end def test_dates_and_times @quoter.extend(Module.new { def quoted_date(value) 'lol' end }) assert_equal "'lol'", @quoter.quote(Date.today, nil) - assert_equal "'lol'", @quoter.quote(Date.today, Object.new) assert_equal "'lol'", @quoter.quote(Time.now, nil) - assert_equal "'lol'", @quoter.quote(Time.now, Object.new) assert_equal "'lol'", @quoter.quote(DateTime.now, nil) - assert_equal "'lol'", @quoter.quote(DateTime.now, Object.new) end def test_crazy_object crazy = Class.new.new expected = "'#{YAML.dump(crazy)}'" assert_equal expected, @quoter.quote(crazy, nil) - assert_equal expected, @quoter.quote(crazy, Object.new) end def test_crazy_object_calls_quote_string crazy = Class.new { def initialize; @lol = 'lo\l' end }.new assert_match "lo\\\\l", @quoter.quote(crazy, nil) - assert_match "lo\\\\l", @quoter.quote(crazy, Object.new) end def test_quote_string_no_column @@ -165,36 +144,13 @@ module ActiveRecord assert_equal "'lo\\\\l'", @quoter.quote(string, nil) end - def test_quote_string_int_column - assert_equal "1", @quoter.quote('1', FakeColumn.new(:integer)) - assert_equal "1", @quoter.quote('1.2', FakeColumn.new(:integer)) - end - - def test_quote_string_float_column - assert_equal "1.0", @quoter.quote('1', FakeColumn.new(:float)) - assert_equal "1.2", @quoter.quote('1.2', FakeColumn.new(:float)) - end - - def test_quote_as_mb_chars_binary_column - string = ActiveSupport::Multibyte::Chars.new('lo\l') - assert_equal "'lo\\\\l'", @quoter.quote(string, FakeColumn.new(:binary)) - end - - def test_quote_binary_without_string_to_binary - assert_equal "'lo\\\\l'", @quoter.quote('lo\l', FakeColumn.new(:binary)) - end - def test_string_with_crazy_column - assert_equal "'lo\\\\l'", @quoter.quote('lo\l', FakeColumn.new(:foo)) + assert_equal "'lo\\\\l'", @quoter.quote('lo\l') end def test_quote_duration assert_equal "1800", @quoter.quote(30.minutes) end - - def test_quote_duration_int_column - assert_equal "7200", @quoter.quote(2.hours, FakeColumn.new(:integer)) - end end end end diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index c085fcf161..84abaf0291 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -80,8 +80,14 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal :integer, @first.column_for_attribute("id").type end + def test_non_existent_columns_return_nil + assert_deprecated do + assert_nil @first.column_for_attribute("attribute_that_doesnt_exist") + end + end + def test_reflection_klass_for_nested_class_name - reflection = MacroReflection.new(:company, nil, nil, { :class_name => 'MyApplication::Business::Company' }, ActiveRecord::Base) + reflection = ActiveRecord::Reflection.create(:has_many, nil, nil, { :class_name => 'MyApplication::Business::Company' }, ActiveRecord::Base) assert_nothing_raised do assert_equal MyApplication::Business::Company, reflection.klass end @@ -91,21 +97,21 @@ class ReflectionTest < ActiveRecord::TestCase ActiveSupport::Inflector.inflections do |inflect| inflect.irregular 'plural_irregular', 'plurales_irregulares' end - reflection = AssociationReflection.new(:has_many, 'plurales_irregulares', nil, {}, ActiveRecord::Base) + reflection = ActiveRecord::Reflection.create(:has_many, 'plurales_irregulares', nil, {}, ActiveRecord::Base) assert_equal 'PluralIrregular', reflection.class_name end def test_aggregation_reflection reflection_for_address = AggregateReflection.new( - :composed_of, :address, nil, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer + :address, nil, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer ) reflection_for_balance = AggregateReflection.new( - :composed_of, :balance, nil, { :class_name => "Money", :mapping => %w(balance amount) }, Customer + :balance, nil, { :class_name => "Money", :mapping => %w(balance amount) }, Customer ) reflection_for_gps_location = AggregateReflection.new( - :composed_of, :gps_location, nil, { }, Customer + :gps_location, nil, { }, Customer ) assert Customer.reflect_on_all_aggregations.include?(reflection_for_gps_location) @@ -129,7 +135,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_has_many_reflection - reflection_for_clients = AssociationReflection.new(:has_many, :clients, nil, { :order => "id", :dependent => :destroy }, Firm) + reflection_for_clients = ActiveRecord::Reflection.create(:has_many, :clients, nil, { :order => "id", :dependent => :destroy }, Firm) assert_equal reflection_for_clients, Firm.reflect_on_association(:clients) @@ -141,7 +147,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_has_one_reflection - reflection_for_account = AssociationReflection.new(:has_one, :account, nil, { :foreign_key => "firm_id", :dependent => :destroy }, Firm) + reflection_for_account = ActiveRecord::Reflection.create(:has_one, :account, nil, { :foreign_key => "firm_id", :dependent => :destroy }, Firm) assert_equal reflection_for_account, Firm.reflect_on_association(:account) assert_equal Account, Firm.reflect_on_association(:account).klass @@ -200,7 +206,12 @@ class ReflectionTest < ActiveRecord::TestCase end def test_reflection_should_not_raise_error_when_compared_to_other_object - assert_nothing_raised { Firm.reflections['clients'] == Object.new } + assert_not_equal Object.new, Firm._reflections['clients'] + end + + def test_has_and_belongs_to_many_reflection + assert_equal :has_and_belongs_to_many, Category.reflections['posts'].macro + assert_equal :posts, Category.reflect_on_all_associations(:has_and_belongs_to_many).first.name end def test_has_many_through_reflection @@ -273,12 +284,12 @@ class ReflectionTest < ActiveRecord::TestCase end def test_association_primary_key_raises_when_missing_primary_key - reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :edge, nil, {}, Author) + reflection = ActiveRecord::Reflection.create(:has_many, :edge, nil, {}, Author) assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.association_primary_key } through = Class.new(ActiveRecord::Reflection::ThroughReflection) { define_method(:source_reflection) { reflection } - }.new(:fuu, :edge, nil, {}, Author) + }.new(reflection) assert_raises(ActiveRecord::UnknownPrimaryKey) { through.association_primary_key } end @@ -288,7 +299,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_active_record_primary_key_raises_when_missing_primary_key - reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :author, nil, {}, Edge) + reflection = ActiveRecord::Reflection.create(:has_many, :author, nil, {}, Edge) assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.active_record_primary_key } end @@ -306,32 +317,28 @@ class ReflectionTest < ActiveRecord::TestCase end def test_default_association_validation - assert AssociationReflection.new(:has_many, :clients, nil, {}, Firm).validate? + assert ActiveRecord::Reflection.create(:has_many, :clients, nil, {}, Firm).validate? - assert !AssociationReflection.new(:has_one, :client, nil, {}, Firm).validate? - assert !AssociationReflection.new(:belongs_to, :client, nil, {}, Firm).validate? - assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, {}, Firm).validate? + assert !ActiveRecord::Reflection.create(:has_one, :client, nil, {}, Firm).validate? + assert !ActiveRecord::Reflection.create(:belongs_to, :client, nil, {}, Firm).validate? end def test_always_validate_association_if_explicit - assert AssociationReflection.new(:has_one, :client, nil, { :validate => true }, Firm).validate? - assert AssociationReflection.new(:belongs_to, :client, nil, { :validate => true }, Firm).validate? - assert AssociationReflection.new(:has_many, :clients, nil, { :validate => true }, Firm).validate? - assert AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :validate => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:has_one, :client, nil, { :validate => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :validate => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:has_many, :clients, nil, { :validate => true }, Firm).validate? end def test_validate_association_if_autosave - assert AssociationReflection.new(:has_one, :client, nil, { :autosave => true }, Firm).validate? - assert AssociationReflection.new(:belongs_to, :client, nil, { :autosave => true }, Firm).validate? - assert AssociationReflection.new(:has_many, :clients, nil, { :autosave => true }, Firm).validate? - assert AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :autosave => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:has_one, :client, nil, { :autosave => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :autosave => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:has_many, :clients, nil, { :autosave => true }, Firm).validate? end def test_never_validate_association_if_explicit - assert !AssociationReflection.new(:has_one, :client, nil, { :autosave => true, :validate => false }, Firm).validate? - assert !AssociationReflection.new(:belongs_to, :client, nil, { :autosave => true, :validate => false }, Firm).validate? - assert !AssociationReflection.new(:has_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate? - assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate? + assert !ActiveRecord::Reflection.create(:has_one, :client, nil, { :autosave => true, :validate => false }, Firm).validate? + assert !ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :autosave => true, :validate => false }, Firm).validate? + assert !ActiveRecord::Reflection.create(:has_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate? end def test_foreign_key @@ -353,11 +360,11 @@ class ReflectionTest < ActiveRecord::TestCase category = Struct.new(:table_name, :pluralize_table_names).new('categories', true) product = Struct.new(:table_name, :pluralize_table_names).new('products', true) - reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, product) + reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, product) reflection.stubs(:klass).returns(category) assert_equal 'categories_products', reflection.join_table - reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, {}, category) + reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category) reflection.stubs(:klass).returns(product) assert_equal 'categories_products', reflection.join_table end @@ -366,11 +373,11 @@ class ReflectionTest < ActiveRecord::TestCase category = Struct.new(:table_name, :pluralize_table_names).new('catalog_categories', true) product = Struct.new(:table_name, :pluralize_table_names).new('catalog_products', true) - reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, product) + reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, product) reflection.stubs(:klass).returns(category) assert_equal 'catalog_categories_products', reflection.join_table - reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, {}, category) + reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category) reflection.stubs(:klass).returns(product) assert_equal 'catalog_categories_products', reflection.join_table end @@ -379,11 +386,11 @@ class ReflectionTest < ActiveRecord::TestCase category = Struct.new(:table_name, :pluralize_table_names).new('catalog_categories', true) page = Struct.new(:table_name, :pluralize_table_names).new('content_pages', true) - reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, page) + reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, page) reflection.stubs(:klass).returns(category) assert_equal 'catalog_categories_content_pages', reflection.join_table - reflection = AssociationReflection.new(:has_and_belongs_to_many, :pages, nil, {}, category) + reflection = ActiveRecord::Reflection.create(:has_many, :pages, nil, {}, category) reflection.stubs(:klass).returns(page) assert_equal 'catalog_categories_content_pages', reflection.join_table end @@ -392,15 +399,47 @@ class ReflectionTest < ActiveRecord::TestCase category = Struct.new(:table_name, :pluralize_table_names).new('categories', true) product = Struct.new(:table_name, :pluralize_table_names).new('products', true) - reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, { :join_table => 'product_categories' }, product) + reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, { :join_table => 'product_categories' }, product) reflection.stubs(:klass).returns(category) assert_equal 'product_categories', reflection.join_table - reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, { :join_table => 'product_categories' }, category) + reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, { :join_table => 'product_categories' }, category) reflection.stubs(:klass).returns(product) assert_equal 'product_categories', reflection.join_table end + def test_includes_accepts_symbols + hotel = Hotel.create! + department = hotel.departments.create! + department.chefs.create! + + assert_nothing_raised do + assert_equal department.chefs, Hotel.includes([departments: :chefs]).first.chefs + end + end + + def test_includes_accepts_strings + hotel = Hotel.create! + department = hotel.departments.create! + department.chefs.create! + + assert_nothing_raised do + assert_equal department.chefs, Hotel.includes(['departments' => 'chefs']).first.chefs + end + end + + def test_reflect_on_association_accepts_symbols + assert_nothing_raised do + assert_equal Hotel.reflect_on_association(:departments).name, :departments + end + end + + def test_reflect_on_association_accepts_strings + assert_nothing_raised do + assert_equal Hotel.reflect_on_association("departments").name, :departments + end + end + private def assert_reflection(klass, association, options) assert reflection = klass.reflect_on_association(association) diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index 9b2bfed039..29c9d0e2af 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -32,7 +32,7 @@ module ActiveRecord :exclude?, :find_all, :flat_map, :group_by, :include?, :length, :map, :none?, :one?, :partition, :reject, :reverse, :sample, :second, :sort, :sort_by, :third, - :to_ary, :to_set, :to_xml, :to_yaml + :to_ary, :to_set, :to_xml, :to_yaml, :join ] ARRAY_DELEGATES.each do |method| diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb index ff1c2a0d82..2b5c2fd5a4 100644 --- a/activerecord/test/cases/relation/merging_test.rb +++ b/activerecord/test/cases/relation/merging_test.rb @@ -17,8 +17,9 @@ class RelationMergingTest < ActiveRecord::TestCase end def test_relation_to_sql - sql = Post.first.comments.to_sql - assert_no_match(/\?/, sql) + post = Post.first + sql = post.comments.to_sql + assert_match(/.?post_id.? = #{post.id}\Z/i, sql) end def test_relation_merging_with_arel_equalities_keeps_last_equality @@ -107,6 +108,11 @@ class RelationMergingTest < ActiveRecord::TestCase merged = left.merge(right) assert_equal post, merged.first end + + def test_merging_compares_symbols_and_strings_as_equal + post = PostThatLoadsCommentsInAnAfterSaveHook.create!(title: "First Post", body: "Blah blah blah.") + assert_equal "First comment!", post.comments.where(body: "First comment!").first_or_create.body + end end class MergingDifferentRelationsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/relation/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb index c6decaad89..b9e69bdb08 100644 --- a/activerecord/test/cases/relation/where_chain_test.rb +++ b/activerecord/test/cases/relation/where_chain_test.rb @@ -99,7 +99,7 @@ module ActiveRecord assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual assert_equal 'ruby on rails', bind.last end - + def test_rewhere_with_one_condition relation = Post.where(title: 'hello').where(title: 'world').rewhere(title: 'alone') diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index 937f226b1d..a6a36a6fd9 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -6,10 +6,11 @@ require 'models/post' require 'models/comment' require 'models/edge' require 'models/topic' +require 'models/binary' module ActiveRecord class WhereTest < ActiveRecord::TestCase - fixtures :posts, :edges, :authors + fixtures :posts, :edges, :authors, :binaries def test_where_copies_bind_params author = authors(:david) @@ -179,5 +180,35 @@ module ActiveRecord assert_equal 4, Edge.where(blank).order("sink_id").to_a.size end end + + def test_where_with_integer_for_string_column + count = Post.where(:title => 0).count + assert_equal 0, count + end + + def test_where_with_float_for_string_column + count = Post.where(:title => 0.0).count + assert_equal 0, count + end + + def test_where_with_boolean_for_string_column + count = Post.where(:title => false).count + assert_equal 0, count + end + + def test_where_with_decimal_for_string_column + count = Post.where(:title => BigDecimal.new(0)).count + assert_equal 0, count + end + + def test_where_with_duration_for_string_column + count = Post.where(:title => 0.seconds).count + assert_equal 0, count + end + + def test_where_with_integer_for_binary_column + count = Binary.where(:data => 0).count + assert_equal 0, count + end end end diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index fb0b906c07..3280945d09 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -235,5 +235,33 @@ module ActiveRecord posts_with_special_comments_with_ratings = Post.group("posts.id").joins(:special_comments).merge(special_comments_with_ratings) assert_equal 3, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count.length end + + class EnsureRoundTripTypeCasting < ActiveRecord::Type::Value + def type + :string + end + + def type_cast_from_database(value) + raise value unless value == "type cast for database" + "type cast from database" + end + + def type_cast_for_database(value) + raise value unless value == "value from user" + "type cast for database" + end + end + + class UpdateAllTestModel < ActiveRecord::Base + self.table_name = 'posts' + + attribute :body, EnsureRoundTripTypeCasting.new + end + + def test_update_all_goes_through_normal_type_casting + UpdateAllTestModel.update_all(body: "value from user", type: nil) # No STI + + assert_equal "type cast from database", UpdateAllTestModel.first.body + end end end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 68e62934c1..88df997a2f 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -366,6 +366,14 @@ class RelationTest < ActiveRecord::TestCase assert_equal({ 'salary' => 100_000 }, Developer.none.where(salary: 100_000).where_values_hash) end + def test_null_relation_sum + ac = Aircraft.new + assert_equal Hash.new, ac.engines.group(:id).sum(:id) + assert_equal 0, ac.engines.count + ac.save + assert_equal Hash.new, ac.engines.group(:id).sum(:id) + assert_equal 0, ac.engines.count + end def test_null_relation_count ac = Aircraft.new @@ -376,6 +384,42 @@ class RelationTest < ActiveRecord::TestCase assert_equal 0, ac.engines.count end + def test_null_relation_size + ac = Aircraft.new + assert_equal Hash.new, ac.engines.group(:id).size + assert_equal 0, ac.engines.size + ac.save + assert_equal Hash.new, ac.engines.group(:id).size + assert_equal 0, ac.engines.size + end + + def test_null_relation_average + ac = Aircraft.new + assert_equal Hash.new, ac.engines.group(:car_id).average(:id) + assert_equal nil, ac.engines.average(:id) + ac.save + assert_equal Hash.new, ac.engines.group(:car_id).average(:id) + assert_equal nil, ac.engines.average(:id) + end + + def test_null_relation_minimum + ac = Aircraft.new + assert_equal Hash.new, ac.engines.group(:car_id).minimum(:id) + assert_equal nil, ac.engines.minimum(:id) + ac.save + assert_equal Hash.new, ac.engines.group(:car_id).minimum(:id) + assert_equal nil, ac.engines.minimum(:id) + end + + def test_null_relation_maximum + ac = Aircraft.new + assert_equal Hash.new, ac.engines.group(:car_id).maximum(:id) + assert_equal nil, ac.engines.maximum(:id) + ac.save + assert_equal Hash.new, ac.engines.group(:car_id).maximum(:id) + assert_equal nil, ac.engines.maximum(:id) + end + def test_joins_with_nil_argument assert_nothing_raised { DependentFirm.joins(nil).first } end @@ -699,6 +743,13 @@ class RelationTest < ActiveRecord::TestCase assert_equal [], relation.to_a end + def test_typecasting_where_with_array + ids = Author.pluck(:id) + slugs = ids.map { |id| "#{id}-as-a-slug" } + + assert_equal Author.all.to_a, Author.where(id: slugs).to_a + end + def test_find_all_using_where_with_relation david = authors(:david) # switching the lines below would succeed in current rails @@ -831,8 +882,12 @@ class RelationTest < ActiveRecord::TestCase assert davids.loaded? end - def test_delete_all_limit_error + def test_delete_all_with_unpermitted_relation_raises_error assert_raises(ActiveRecord::ActiveRecordError) { Author.limit(10).delete_all } + assert_raises(ActiveRecord::ActiveRecordError) { Author.uniq.delete_all } + assert_raises(ActiveRecord::ActiveRecordError) { Author.group(:name).delete_all } + assert_raises(ActiveRecord::ActiveRecordError) { Author.having('SUM(id) < 3').delete_all } + assert_raises(ActiveRecord::ActiveRecordError) { Author.offset(10).delete_all } end def test_select_with_aggregates @@ -1677,4 +1732,8 @@ class RelationTest < ActiveRecord::TestCase merged = left.merge(right) assert_equal post, merged.first end + + def test_relation_join_method + assert_equal 'Thank you for the welcome,Thank you again for the welcome', Post.first.comments.join(",") + end end diff --git a/activerecord/test/cases/result_test.rb b/activerecord/test/cases/result_test.rb index 2131b32a0c..d6decafad9 100644 --- a/activerecord/test/cases/result_test.rb +++ b/activerecord/test/cases/result_test.rb @@ -10,7 +10,7 @@ module ActiveRecord ]) end - def test_to_hash_returns_row_hashes + test "to_hash returns row_hashes" do assert_equal [ {'col_1' => 'row 1 col 1', 'col_2' => 'row 1 col 2'}, {'col_1' => 'row 2 col 1', 'col_2' => 'row 2 col 2'}, @@ -18,13 +18,13 @@ module ActiveRecord ], result.to_hash end - def test_each_with_block_returns_row_hashes + test "each with block returns row hashes" do result.each do |row| assert_equal ['col_1', 'col_2'], row.keys end end - def test_each_without_block_returns_an_enumerator + test "each without block returns an enumerator" do result.each.with_index do |row, index| assert_equal ['col_1', 'col_2'], row.keys assert_kind_of Integer, index @@ -32,9 +32,45 @@ module ActiveRecord end if Enumerator.method_defined? :size - def test_each_without_block_returns_a_sized_enumerator + test "each without block returns a sized enumerator" do assert_equal 3, result.each.size end end + + test "cast_values returns rows after type casting" do + values = [["1.1", "2.2"], ["3.3", "4.4"]] + columns = ["col1", "col2"] + types = { "col1" => Type::Integer.new, "col2" => Type::Float.new } + result = Result.new(columns, values, types) + + assert_equal [[1, 2.2], [3, 4.4]], result.cast_values + end + + test "cast_values uses identity type for unknown types" do + values = [["1.1", "2.2"], ["3.3", "4.4"]] + columns = ["col1", "col2"] + types = { "col1" => Type::Integer.new } + result = Result.new(columns, values, types) + + assert_equal [[1, "2.2"], [3, "4.4"]], result.cast_values + end + + test "cast_values returns single dimensional array if single column" do + values = [["1.1"], ["3.3"]] + columns = ["col1"] + types = { "col1" => Type::Integer.new } + result = Result.new(columns, values, types) + + assert_equal [1, 3], result.cast_values + end + + test "cast_values can receive types to use instead" do + values = [["1.1", "2.2"], ["3.3", "4.4"]] + columns = ["col1", "col2"] + types = { "col1" => Type::Integer.new, "col2" => Type::Float.new } + result = Result.new(columns, values, types) + + assert_equal [[1.1, 2.2], [3.3, 4.4]], result.cast_values("col1" => Type::Float.new) + end end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index fd0ef2f89f..4e71d04bc0 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -1,10 +1,9 @@ require "cases/helper" +require 'support/schema_dumping_helper' class SchemaDumperTest < ActiveRecord::TestCase - def setup - super + setup do ActiveRecord::SchemaMigration.create_table - @stream = StringIO.new end def standard_dump @@ -25,7 +24,8 @@ class SchemaDumperTest < ActiveRecord::TestCase end def test_magic_comment - assert_match "# encoding: #{@stream.external_encoding.name}", standard_dump + output = standard_dump + assert_match "# encoding: #{@stream.external_encoding.name}", output end def test_schema_dump @@ -63,7 +63,7 @@ class SchemaDumperTest < ActiveRecord::TestCase next if column_set.empty? lengths = column_set.map do |column| - if match = column.match(/t\.(?:integer|decimal|float|datetime|timestamp|time|date|text|binary|string|boolean|uuid)\s+"/) + if match = column.match(/t\.(?:integer|decimal|float|datetime|timestamp|time|date|text|binary|string|boolean|uuid|point)\s+"/) match[0].length end end @@ -353,9 +353,9 @@ class SchemaDumperTest < ActiveRecord::TestCase output = standard_dump # Oracle supports precision up to 38 and it identifies decimals with scale 0 as integers if current_adapter?(:OracleAdapter) - assert_match %r{t.integer\s+"atoms_in_universe",\s+precision: 38,\s+scale: 0}, output + assert_match %r{t.integer\s+"atoms_in_universe",\s+precision: 38}, output else - assert_match %r{t.decimal\s+"atoms_in_universe",\s+precision: 55,\s+scale: 0}, output + assert_match %r{t.decimal\s+"atoms_in_universe",\s+precision: 55}, output end end @@ -372,15 +372,28 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{create_table "subscribers", id: false}, output end + if ActiveRecord::Base.connection.supports_foreign_keys? + def test_foreign_keys_are_dumped_at_the_bottom_to_circumvent_dependency_issues + output = standard_dump + assert_match(/^\s+add_foreign_key "fk_test_has_fk"[^\n]+\n\s+add_foreign_key "lessons_students"/, output) + end + end + class CreateDogMigration < ActiveRecord::Migration def up + create_table("dog_owners") do |t| + end + create_table("dogs") do |t| t.column :name, :string + t.column :owner_id, :integer end add_index "dogs", [:name] + add_foreign_key :dogs, :dog_owners, column: "owner_id" if supports_foreign_keys? end def down drop_table("dogs") + drop_table("dog_owners") end end @@ -394,13 +407,45 @@ class SchemaDumperTest < ActiveRecord::TestCase output = standard_dump assert_no_match %r{create_table "foo_.+_bar"}, output - assert_no_match %r{create_index "foo_.+_bar"}, output + assert_no_match %r{add_index "foo_.+_bar"}, output assert_no_match %r{create_table "schema_migrations"}, output + + if ActiveRecord::Base.connection.supports_foreign_keys? + assert_no_match %r{add_foreign_key "foo_.+_bar"}, output + assert_no_match %r{add_foreign_key "[^"]+", "foo_.+_bar"}, output + end ensure migration.migrate(:down) ActiveRecord::Base.table_name_suffix = ActiveRecord::Base.table_name_prefix = '' $stdout = original end +end + +class SchemaDumperDefaultsTest < ActiveRecord::TestCase + include SchemaDumpingHelper + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table :defaults, force: true do |t| + t.string :string_with_default, default: "Hello!" + t.date :date_with_default, default: '2014-06-05' + t.datetime :datetime_with_default, default: "2014-06-05 07:17:04" + t.time :time_with_default, default: "07:17:04" + end + end + teardown do + return unless @connection + @connection.execute 'DROP TABLE IF EXISTS defaults' + end + + def test_schema_dump_defaults_with_universally_supported_types + output = dump_table_schema('defaults') + + assert_match %r{t\.string\s+"string_with_default",\s+default: "Hello!"}, output + assert_match %r{t\.date\s+"date_with_default",\s+default: '2014-06-05'}, output + assert_match %r{t\.datetime\s+"datetime_with_default",\s+default: '2014-06-05 07:17:04'}, output + assert_match %r{t\.time\s+"time_with_default",\s+default: '2000-01-01 07:17:04'}, output + end end diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index c46060a646..3f52e80e11 100644 --- a/activerecord/test/cases/serialization_test.rb +++ b/activerecord/test/cases/serialization_test.rb @@ -1,8 +1,11 @@ require "cases/helper" require 'models/contact' require 'models/topic' +require 'models/book' class SerializationTest < ActiveRecord::TestCase + fixtures :books + FORMATS = [ :xml, :json ] def setup @@ -65,4 +68,28 @@ class SerializationTest < ActiveRecord::TestCase ensure ActiveRecord::Base.include_root_in_json = original_root_in_json end + + def test_read_attribute_for_serialization_with_format_without_method_missing + klazz = Class.new(ActiveRecord::Base) + klazz.table_name = 'books' + + book = klazz.new + assert_nil book.read_attribute_for_serialization(:format) + end + + def test_read_attribute_for_serialization_with_format_after_init + klazz = Class.new(ActiveRecord::Base) + klazz.table_name = 'books' + + book = klazz.new(format: 'paperback') + assert_equal 'paperback', book.read_attribute_for_serialization(:format) + end + + def test_read_attribute_for_serialization_with_format_after_find + klazz = Class.new(ActiveRecord::Base) + klazz.table_name = 'books' + + book = klazz.find(books(:awdr).id) + assert_equal 'paperback', book.read_attribute_for_serialization(:format) + end end diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index 5609cf310c..f8d87a3661 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -3,10 +3,11 @@ require 'models/topic' require 'models/reply' require 'models/person' require 'models/traffic_light' +require 'models/post' require 'bcrypt' class SerializedAttributeTest < ActiveRecord::TestCase - fixtures :topics + fixtures :topics, :posts MyObject = Struct.new :attribute1, :attribute2 @@ -14,8 +15,17 @@ class SerializedAttributeTest < ActiveRecord::TestCase Topic.serialize("content") end + def test_serialize_does_not_eagerly_load_columns + assert_no_queries do + Topic.reset_column_information + Topic.serialize(:content) + end + end + def test_list_of_serialized_attributes - assert_equal %w(content), Topic.serialized_attributes.keys + assert_deprecated do + assert_equal %w(content), Topic.serialized_attributes.keys + end end def test_serialized_attribute @@ -29,12 +39,6 @@ class SerializedAttributeTest < ActiveRecord::TestCase assert_equal(myobj, topic.content) end - def test_serialized_attribute_init_with - topic = Topic.allocate - topic.init_with('attributes' => { 'content' => '--- foo' }) - assert_equal 'foo', topic.content - end - def test_serialized_attribute_in_base_class Topic.serialize("content", Hash) @@ -46,34 +50,56 @@ class SerializedAttributeTest < ActiveRecord::TestCase assert_equal(hash, important_topic.content) end - # This test was added to fix GH #4004. Obviously the value returned - # is not really the value 'before type cast' so we should maybe think - # about changing that in the future. - def test_serialized_attribute_before_type_cast_returns_unserialized_value + def test_serialized_attributes_from_database_on_subclass Topic.serialize :content, Hash - t = Topic.new(content: { foo: :bar }) - assert_equal({ foo: :bar }, t.content_before_type_cast) + t = Reply.new(content: { foo: :bar }) + assert_equal({ foo: :bar }, t.content) t.save! - t.reload - assert_equal({ foo: :bar }, t.content_before_type_cast) + t = Reply.last + assert_equal({ foo: :bar }, t.content) end - def test_serialized_attributes_before_type_cast_returns_unserialized_value - Topic.serialize :content, Hash + def test_serialized_attribute_calling_dup_method + Topic.serialize :content, JSON - t = Topic.new(content: { foo: :bar }) - assert_equal({ foo: :bar }, t.attributes_before_type_cast["content"]) + orig = Topic.new(content: { foo: :bar }) + clone = orig.dup + assert_equal(orig.content, clone.content) + end + + def test_serialized_json_attribute_returns_unserialized_value + Topic.serialize :content, JSON + my_post = posts(:welcome) + + t = Topic.new(content: my_post) t.save! t.reload - assert_equal({ foo: :bar }, t.attributes_before_type_cast["content"]) + + assert_instance_of(Hash, t.content) + assert_equal(my_post.id, t.content["id"]) + assert_equal(my_post.title, t.content["title"]) end - def test_serialized_attribute_calling_dup_method + def test_json_read_legacy_null Topic.serialize :content, JSON - t = Topic.new(:content => { :foo => :bar }).dup - assert_equal({ :foo => :bar }, t.content_before_type_cast) + # Force a row to have a JSON "null" instead of a database NULL (this is how + # null values are saved on 4.1 and before) + id = Topic.connection.insert "INSERT INTO topics (content) VALUES('null')" + t = Topic.find(id) + + assert_nil t.content + end + + def test_json_read_db_null + Topic.serialize :content, JSON + + # Force a row to have a database NULL instead of a JSON "null" + id = Topic.connection.insert "INSERT INTO topics (content) VALUES(NULL)" + t = Topic.find(id) + + assert_nil t.content end def test_serialized_attribute_declared_in_subclass @@ -116,8 +142,10 @@ class SerializedAttributeTest < ActiveRecord::TestCase def test_serialized_attribute_should_raise_exception_on_save_with_wrong_type Topic.serialize(:content, Hash) - topic = Topic.new(:content => "string") - assert_raise(ActiveRecord::SerializationTypeMismatch) { topic.save } + assert_raise(ActiveRecord::SerializationTypeMismatch) do + topic = Topic.new(content: 'string') + topic.save + end end def test_should_raise_exception_on_serialized_attribute_with_type_mismatch @@ -168,45 +196,22 @@ class SerializedAttributeTest < ActiveRecord::TestCase end def test_serialize_with_coder - coder = Class.new { - # Identity - def load(thing) - thing - end - - # base 64 - def dump(thing) - [thing].pack('m') + some_class = Struct.new(:foo) do + def self.dump(value) + value.foo end - }.new - Topic.serialize(:content, coder) - s = 'hello world' - topic = Topic.new(:content => s) - assert topic.save - topic = topic.reload - assert_equal [s].pack('m'), topic.content - end - - def test_serialize_with_bcrypt_coder - crypt_coder = Class.new { - def load(thing) - return unless thing - BCrypt::Password.new thing + def self.load(value) + new(value) end + end - def dump(thing) - BCrypt::Password.create(thing).to_s - end - }.new - - Topic.serialize(:content, crypt_coder) - password = 'password' - topic = Topic.new(:content => password) - assert topic.save - topic = topic.reload - assert_kind_of BCrypt::Password, topic.content - assert_equal(true, topic.content == password, 'password should equal') + Topic.serialize(:content, some_class) + topic = Topic.new(:content => some_class.new('my value')) + topic.save! + topic.reload + assert_kind_of some_class, topic.content + assert_equal topic.content, some_class.new('my value') end def test_serialize_attribute_via_select_method_when_time_zone_available @@ -235,13 +240,19 @@ class SerializedAttributeTest < ActiveRecord::TestCase assert_equal [], light.long_state end - def test_serialized_column_should_not_be_wrapped_twice - Topic.serialize(:content, MyObject) + def test_serialized_column_should_unserialize_after_update_column + t = Topic.create(content: "first") + assert_equal("first", t.content) - myobj = MyObject.new('value1', 'value2') - Topic.create(content: myobj) - Topic.create(content: myobj) - type = Topic.column_types["content"] - assert !type.instance_variable_get("@column").is_a?(ActiveRecord::AttributeMethods::Serialization::Type) + t.update_column(:content, Topic.type_for_attribute('content').type_cast_for_database("second")) + assert_equal("second", t.content) + end + + def test_serialized_column_should_unserialize_after_update_attribute + t = Topic.create(content: "first") + assert_equal("first", t.content) + + t.update_attribute(:content, "second") + assert_equal("second", t.content) end end diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index 978cee9cfb..e9cdf94c99 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -163,22 +163,24 @@ class StoreTest < ActiveRecord::TestCase assert_equal [:width, :height], second_model.stored_attributes[:data] end - test "YAML coder initializes the store when a Nil value is given" do - assert_equal({}, @john.params) - end + test "stored_attributes are tracked per subclass" do + first_model = Class.new(ActiveRecord::Base) do + store_accessor :data, :color + end + second_model = Class.new(first_model) do + store_accessor :data, :width, :height + end + third_model = Class.new(first_model) do + store_accessor :data, :area, :volume + end - test "attributes_for_coder should return stored fields already serialized" do - attributes = { - "id" => @john.id, - "name"=> @john.name, - "settings" => "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\ncolor: black\n", - "preferences" => "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nremember_login: true\n", - "json_data" => "{\"height\":\"tall\"}", "json_data_empty"=>"{\"is_a_good_guy\":true}", - "params" => "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess {}\n", - "account_id"=> @john.account_id - } + assert_equal [:color], first_model.stored_attributes[:data] + assert_equal [:color, :width, :height], second_model.stored_attributes[:data] + assert_equal [:color, :area, :volume], third_model.stored_attributes[:data] + end - assert_equal attributes, @john.attributes_for_coder + test "YAML coder initializes the store when a Nil value is given" do + assert_equal({}, @john.params) end test "dump, load and dump again a model" do @@ -187,7 +189,6 @@ class StoreTest < ActiveRecord::TestCase assert_equal @john, loaded second_dump = YAML.dump(loaded) - assert_equal dumped, second_dump assert_equal @john, YAML.load(second_dump) end end diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index bf9e14fa4f..0f48c8d5fc 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -205,7 +205,7 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.drop_all end - def test_creates_configurations_with_local_ip + def test_drops_configurations_with_local_ip @configurations[:development].merge!('host' => '127.0.0.1') ActiveRecord::Tasks::DatabaseTasks.expects(:drop) @@ -213,7 +213,7 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.drop_all end - def test_creates_configurations_with_local_host + def test_drops_configurations_with_local_host @configurations[:development].merge!('host' => 'localhost') ActiveRecord::Tasks::DatabaseTasks.expects(:drop) @@ -221,7 +221,7 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.drop_all end - def test_creates_configurations_with_blank_hosts + def test_drops_configurations_with_blank_hosts @configurations[:development].merge!('host' => nil) ActiveRecord::Tasks::DatabaseTasks.expects(:drop) @@ -241,7 +241,7 @@ module ActiveRecord ActiveRecord::Base.stubs(:configurations).returns(@configurations) end - def test_creates_current_environment_database + def test_drops_current_environment_database ActiveRecord::Tasks::DatabaseTasks.expects(:drop). with('database' => 'prod-db') @@ -285,6 +285,34 @@ module ActiveRecord end end + class DatabaseTasksPurgeCurrentTest < ActiveRecord::TestCase + def test_purges_current_environment_database + configurations = { + 'development' => {'database' => 'dev-db'}, + 'test' => {'database' => 'test-db'}, + 'production' => {'database' => 'prod-db'} + } + ActiveRecord::Base.stubs(:configurations).returns(configurations) + + ActiveRecord::Tasks::DatabaseTasks.expects(:purge). + with('database' => 'prod-db') + + ActiveRecord::Tasks::DatabaseTasks.purge_current('production') + end + end + + class DatabaseTasksPurgeAllTest < ActiveRecord::TestCase + def test_purge_all_local_configurations + configurations = {:development => {'database' => 'my-db'}} + ActiveRecord::Base.stubs(:configurations).returns(configurations) + + ActiveRecord::Tasks::DatabaseTasks.expects(:purge). + with('database' => 'my-db') + + ActiveRecord::Tasks::DatabaseTasks.purge_all + end + end + class DatabaseTasksCharsetTest < ActiveRecord::TestCase include DatabaseTasksSetupper diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index 3e3a2828f3..f58535f044 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -1,5 +1,6 @@ require 'cases/helper' +if current_adapter?(:MysqlAdapter, :Mysql2Adapter) module ActiveRecord class MysqlDBCreateTest < ActiveRecord::TestCase def setup @@ -196,8 +197,8 @@ module ActiveRecord ActiveRecord::Base.stubs(:establish_connection).returns(true) end - def test_establishes_connection_to_test_database - ActiveRecord::Base.expects(:establish_connection).with(:test) + def test_establishes_connection_to_the_appropriate_database + ActiveRecord::Base.expects(:establish_connection).with(@configuration) ActiveRecord::Tasks::DatabaseTasks.purge @configuration end @@ -307,3 +308,4 @@ module ActiveRecord end end +end diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index 6ea225178f..0d574d071c 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -1,5 +1,6 @@ require 'cases/helper' +if current_adapter?(:PostgreSQLAdapter) module ActiveRecord class PostgreSQLDBCreateTest < ActiveRecord::TestCase def setup @@ -241,3 +242,4 @@ module ActiveRecord end end +end diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb index da3471adf9..750d5e42dc 100644 --- a/activerecord/test/cases/tasks/sqlite_rake_test.rb +++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb @@ -1,6 +1,7 @@ require 'cases/helper' require 'pathname' +if current_adapter?(:SQLite3Adapter) module ActiveRecord class SqliteDBCreateTest < ActiveRecord::TestCase def setup @@ -189,3 +190,4 @@ module ActiveRecord end end end +end diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index 803a054d7e..23a170388e 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -10,13 +10,24 @@ module ActiveRecord end def assert_date_from_db(expected, actual, message = nil) - # SybaseAdapter doesn't have a separate column type just for dates, - # so the time is in the string and incorrectly formatted - if current_adapter?(:SybaseAdapter) - assert_equal expected.to_s, actual.to_date.to_s, message - else - assert_equal expected.to_s, actual.to_s, message - end + assert_equal expected.to_s, actual.to_s, message + end + + def capture(stream) + stream = stream.to_s + captured_stream = Tempfile.new(stream) + stream_io = eval("$#{stream}") + origin_stream = stream_io.dup + stream_io.reopen(captured_stream) + + yield + + stream_io.rewind + return captured_stream.read + ensure + captured_stream.close + captured_stream.unlink + stream_io.reopen(origin_stream) end def capture_sql diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 77ab427be0..0472246f71 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -71,24 +71,6 @@ class TimestampTest < ActiveRecord::TestCase assert_equal @previously_updated_at, @developer.updated_at end - def test_saving_when_callback_sets_record_timestamps_to_false_doesnt_update_its_timestamp - klass = Class.new(Developer) do - before_update :cancel_record_timestamps - def cancel_record_timestamps - self.record_timestamps = false - return true - end - end - - developer = klass.first - previously_updated_at = developer.updated_at - - developer.name = "New Name" - developer.save! - - assert_equal previously_updated_at, developer.updated_at - end - def test_touching_an_attribute_updates_timestamp previously_created_at = @developer.created_at @developer.touch(:created_at) @@ -399,6 +381,19 @@ class TimestampTest < ActiveRecord::TestCase assert_not_equal time, pet.updated_at end + def test_timestamp_column_values_are_present_in_the_callbacks + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'people' + + before_create do + self.born_at = self.created_at + end + end + + person = klass.create first_name: 'David' + assert_not_equal person.born_at, nil + end + def test_timestamp_attributes_for_create toy = Toy.first assert_equal [:created_at, :created_on], toy.send(:timestamp_attributes_for_create) diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index e6ed85394b..f28a7b00e2 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -123,6 +123,19 @@ class TransactionTest < ActiveRecord::TestCase assert !Topic.find(1).approved? end + def test_rolling_back_in_a_callback_rollbacks_before_save + def @first.before_save_for_transaction + raise ActiveRecord::Rollback + end + assert !@first.approved + + Topic.transaction do + @first.approved = true + @first.save! + end + assert !Topic.find(@first.id).approved?, "Should not commit the approved flag" + end + def test_raising_exception_in_nested_transaction_restore_state_in_save topic = Topic.new @@ -411,6 +424,26 @@ class TransactionTest < ActiveRecord::TestCase end end + def test_savepoints_name + Topic.transaction do + assert_nil Topic.connection.current_savepoint_name + assert_nil Topic.connection.current_transaction.savepoint_name + + Topic.transaction(requires_new: true) do + assert_equal "active_record_1", Topic.connection.current_savepoint_name + assert_equal "active_record_1", Topic.connection.current_transaction.savepoint_name + + Topic.transaction(requires_new: true) do + assert_equal "active_record_2", Topic.connection.current_savepoint_name + assert_equal "active_record_2", Topic.connection.current_transaction.savepoint_name + end + + assert_equal "active_record_1", Topic.connection.current_savepoint_name + assert_equal "active_record_1", Topic.connection.current_transaction.savepoint_name + end + end + end + def test_rollback_when_commit_raises Topic.connection.expects(:begin_db_transaction) Topic.connection.expects(:commit_db_transaction).raises('OH NOES') diff --git a/activerecord/test/cases/type/decimal_test.rb b/activerecord/test/cases/type/decimal_test.rb new file mode 100644 index 0000000000..951cd879dd --- /dev/null +++ b/activerecord/test/cases/type/decimal_test.rb @@ -0,0 +1,33 @@ +require "cases/helper" + +module ActiveRecord + module Type + class DecimalTest < ActiveRecord::TestCase + def test_type_cast_decimal + type = Decimal.new + assert_equal BigDecimal.new("0"), type.type_cast_from_user(BigDecimal.new("0")) + assert_equal BigDecimal.new("123"), type.type_cast_from_user(123.0) + assert_equal BigDecimal.new("1"), type.type_cast_from_user(:"1") + end + + def test_type_cast_decimal_from_rational_with_precision + type = Decimal.new(precision: 2) + assert_equal BigDecimal("0.33"), type.type_cast_from_user(Rational(1, 3)) + end + + def test_type_cast_decimal_from_rational_without_precision_defaults_to_18_36 + type = Decimal.new + assert_equal BigDecimal("0.333333333333333333E0"), type.type_cast_from_user(Rational(1, 3)) + end + + def test_type_cast_decimal_from_object_responding_to_d + value = Object.new + def value.to_d + BigDecimal.new("1") + end + type = Decimal.new + assert_equal BigDecimal("1"), type.type_cast_from_user(value) + end + end + end +end diff --git a/activerecord/test/cases/type/string_test.rb b/activerecord/test/cases/type/string_test.rb new file mode 100644 index 0000000000..420177ed49 --- /dev/null +++ b/activerecord/test/cases/type/string_test.rb @@ -0,0 +1,36 @@ +require 'cases/helper' + +module ActiveRecord + class StringTypeTest < ActiveRecord::TestCase + test "type casting" do + type = Type::String.new + assert_equal "1", type.type_cast_from_user(true) + assert_equal "0", type.type_cast_from_user(false) + assert_equal "123", type.type_cast_from_user(123) + end + + test "values are duped coming out" do + s = "foo" + type = Type::String.new + assert_not_same s, type.type_cast_from_user(s) + assert_not_same s, type.type_cast_from_database(s) + end + + test "string mutations are detected" do + klass = Class.new(Base) + klass.table_name = 'authors' + + author = klass.create!(name: 'Sean') + assert_not author.changed? + + author.name << ' Griffin' + assert author.name_changed? + + author.save! + author.reload + + assert_equal 'Sean Griffin', author.name + assert_not author.changed? + end + end +end diff --git a/activerecord/test/cases/type/type_map_test.rb b/activerecord/test/cases/type/type_map_test.rb new file mode 100644 index 0000000000..4e32f92dd0 --- /dev/null +++ b/activerecord/test/cases/type/type_map_test.rb @@ -0,0 +1,130 @@ +require "cases/helper" + +module ActiveRecord + module Type + class TypeMapTest < ActiveRecord::TestCase + def test_default_type + mapping = TypeMap.new + + assert_kind_of Value, mapping.lookup(:undefined) + end + + def test_registering_types + boolean = Boolean.new + mapping = TypeMap.new + + mapping.register_type(/boolean/i, boolean) + + assert_equal mapping.lookup('boolean'), boolean + end + + def test_overriding_registered_types + time = Time.new + timestamp = DateTime.new + mapping = TypeMap.new + + mapping.register_type(/time/i, time) + mapping.register_type(/time/i, timestamp) + + assert_equal mapping.lookup('time'), timestamp + end + + def test_fuzzy_lookup + string = String.new + mapping = TypeMap.new + + mapping.register_type(/varchar/i, string) + + assert_equal mapping.lookup('varchar(20)'), string + end + + def test_aliasing_types + string = String.new + mapping = TypeMap.new + + mapping.register_type(/string/i, string) + mapping.alias_type(/varchar/i, 'string') + + assert_equal mapping.lookup('varchar'), string + end + + def test_changing_type_changes_aliases + time = Time.new + timestamp = DateTime.new + mapping = TypeMap.new + + mapping.register_type(/timestamp/i, time) + mapping.alias_type(/datetime/i, 'timestamp') + mapping.register_type(/timestamp/i, timestamp) + + assert_equal mapping.lookup('datetime'), timestamp + end + + def test_aliases_keep_metadata + mapping = TypeMap.new + + mapping.register_type(/decimal/i) { |sql_type| sql_type } + mapping.alias_type(/number/i, 'decimal') + + assert_equal mapping.lookup('number(20)'), 'decimal(20)' + assert_equal mapping.lookup('number'), 'decimal' + end + + def test_register_proc + string = String.new + binary = Binary.new + mapping = TypeMap.new + + mapping.register_type(/varchar/i) do |type| + if type.include?('(') + string + else + binary + end + end + + assert_equal mapping.lookup('varchar(20)'), string + assert_equal mapping.lookup('varchar'), binary + end + + def test_additional_lookup_args + mapping = TypeMap.new + + mapping.register_type(/varchar/i) do |type, limit| + if limit > 255 + 'text' + else + 'string' + end + end + mapping.alias_type(/string/i, 'varchar') + + assert_equal mapping.lookup('varchar', 200), 'string' + assert_equal mapping.lookup('varchar', 400), 'text' + assert_equal mapping.lookup('string', 400), 'text' + end + + def test_requires_value_or_block + mapping = TypeMap.new + + assert_raises(ArgumentError) do + mapping.register_type(/only key/i) + end + end + + def test_lookup_non_strings + mapping = HashLookupTypeMap.new + + mapping.register_type(1, 'string') + mapping.register_type(2, 'int') + mapping.alias_type(3, 1) + + assert_equal mapping.lookup(1), 'string' + assert_equal mapping.lookup(2), 'int' + assert_equal mapping.lookup(3), 'string' + assert_kind_of Type::Value, mapping.lookup(4) + end + end + end +end + diff --git a/activerecord/test/cases/types_test.rb b/activerecord/test/cases/types_test.rb new file mode 100644 index 0000000000..5c54812f30 --- /dev/null +++ b/activerecord/test/cases/types_test.rb @@ -0,0 +1,163 @@ +require "cases/helper" +require 'models/company' + +module ActiveRecord + module ConnectionAdapters + class TypesTest < ActiveRecord::TestCase + def test_type_cast_boolean + type = Type::Boolean.new + assert type.type_cast_from_user('').nil? + assert type.type_cast_from_user(nil).nil? + + assert type.type_cast_from_user(true) + assert type.type_cast_from_user(1) + assert type.type_cast_from_user('1') + assert type.type_cast_from_user('t') + assert type.type_cast_from_user('T') + assert type.type_cast_from_user('true') + assert type.type_cast_from_user('TRUE') + assert type.type_cast_from_user('on') + assert type.type_cast_from_user('ON') + + # explicitly check for false vs nil + assert_equal false, type.type_cast_from_user(false) + assert_equal false, type.type_cast_from_user(0) + assert_equal false, type.type_cast_from_user('0') + assert_equal false, type.type_cast_from_user('f') + assert_equal false, type.type_cast_from_user('F') + assert_equal false, type.type_cast_from_user('false') + assert_equal false, type.type_cast_from_user('FALSE') + assert_equal false, type.type_cast_from_user('off') + assert_equal false, type.type_cast_from_user('OFF') + assert_equal false, type.type_cast_from_user(' ') + assert_equal false, type.type_cast_from_user("\u3000\r\n") + assert_equal false, type.type_cast_from_user("\u0000") + assert_equal false, type.type_cast_from_user('SOMETHING RANDOM') + end + + def test_type_cast_integer + type = Type::Integer.new + assert_equal 1, type.type_cast_from_user(1) + assert_equal 1, type.type_cast_from_user('1') + assert_equal 1, type.type_cast_from_user('1ignore') + assert_equal 0, type.type_cast_from_user('bad1') + assert_equal 0, type.type_cast_from_user('bad') + assert_equal 1, type.type_cast_from_user(1.7) + assert_equal 0, type.type_cast_from_user(false) + assert_equal 1, type.type_cast_from_user(true) + assert_nil type.type_cast_from_user(nil) + end + + def test_type_cast_non_integer_to_integer + type = Type::Integer.new + assert_nil type.type_cast_from_user([1,2]) + assert_nil type.type_cast_from_user({1 => 2}) + assert_nil type.type_cast_from_user((1..2)) + end + + def test_type_cast_activerecord_to_integer + type = Type::Integer.new + firm = Firm.create(:name => 'Apple') + assert_nil type.type_cast_from_user(firm) + end + + def test_type_cast_object_without_to_i_to_integer + type = Type::Integer.new + assert_nil type.type_cast_from_user(Object.new) + end + + def test_type_cast_nan_and_infinity_to_integer + type = Type::Integer.new + assert_nil type.type_cast_from_user(Float::NAN) + assert_nil type.type_cast_from_user(1.0/0.0) + end + + def test_changing_integers + type = Type::Integer.new + + assert type.changed?(5, 5, '5wibble') + assert_not type.changed?(5, 5, '5') + assert_not type.changed?(5, 5, '5.0') + assert_not type.changed?(nil, nil, nil) + end + + def test_type_cast_float + type = Type::Float.new + assert_equal 1.0, type.type_cast_from_user("1") + end + + def test_changing_float + type = Type::Float.new + + assert type.changed?(5.0, 5.0, '5wibble') + assert_not type.changed?(5.0, 5.0, '5') + assert_not type.changed?(5.0, 5.0, '5.0') + assert_not type.changed?(nil, nil, nil) + end + + def test_type_cast_binary + type = Type::Binary.new + assert_equal nil, type.type_cast_from_user(nil) + assert_equal "1", type.type_cast_from_user("1") + assert_equal 1, type.type_cast_from_user(1) + end + + def test_type_cast_time + type = Type::Time.new + assert_equal nil, type.type_cast_from_user(nil) + assert_equal nil, type.type_cast_from_user('') + assert_equal nil, type.type_cast_from_user('ABC') + + time_string = Time.now.utc.strftime("%T") + assert_equal time_string, type.type_cast_from_user(time_string).strftime("%T") + end + + def test_type_cast_datetime_and_timestamp + type = Type::DateTime.new + assert_equal nil, type.type_cast_from_user(nil) + assert_equal nil, type.type_cast_from_user('') + assert_equal nil, type.type_cast_from_user(' ') + assert_equal nil, type.type_cast_from_user('ABC') + + datetime_string = Time.now.utc.strftime("%FT%T") + assert_equal datetime_string, type.type_cast_from_user(datetime_string).strftime("%FT%T") + end + + def test_type_cast_date + type = Type::Date.new + assert_equal nil, type.type_cast_from_user(nil) + assert_equal nil, type.type_cast_from_user('') + assert_equal nil, type.type_cast_from_user(' ') + assert_equal nil, type.type_cast_from_user('ABC') + + date_string = Time.now.utc.strftime("%F") + assert_equal date_string, type.type_cast_from_user(date_string).strftime("%F") + end + + def test_type_cast_duration_to_integer + type = Type::Integer.new + assert_equal 1800, type.type_cast_from_user(30.minutes) + assert_equal 7200, type.type_cast_from_user(2.hours) + end + + def test_string_to_time_with_timezone + [:utc, :local].each do |zone| + with_timezone_config default: zone do + type = Type::DateTime.new + assert_equal Time.utc(2013, 9, 4, 0, 0, 0), type.type_cast_from_user("Wed, 04 Sep 2013 03:00:00 EAT") + end + end + end + + if current_adapter?(:SQLite3Adapter) + def test_binary_encoding + type = SQLite3Binary.new + utf8_string = "a string".encode(Encoding::UTF_8) + type_cast = type.type_cast_from_user(utf8_string) + + assert_equal Encoding::ASCII_8BIT, type_cast.encoding + end + end + end + end +end diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb index 3790d3c8cf..4f38849131 100644 --- a/activerecord/test/cases/validations/presence_validation_test.rb +++ b/activerecord/test/cases/validations/presence_validation_test.rb @@ -52,14 +52,15 @@ class PresenceValidationTest < ActiveRecord::TestCase end def test_validates_presence_doesnt_convert_to_array - Speedometer.validates_presence_of :dashboard + speedometer = Class.new(Speedometer) + speedometer.validates_presence_of :dashboard dash = Dashboard.new # dashboard has to_a method def dash.to_a; ['(/)', '(\)']; end - s = Speedometer.new + s = speedometer.new s.dashboard = dash assert_nothing_raised { s.valid? } diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index d80da06e27..55804f9576 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -14,28 +14,28 @@ class ValidationsTest < ActiveRecord::TestCase # Other classes we mess with will be dealt with in the specific tests repair_validations(Topic) - def test_error_on_create + def test_valid_uses_create_context_when_new r = WrongReply.new r.title = "Wrong Create" - assert !r.save + assert_not r.valid? assert r.errors[:title].any?, "A reply with a bad title should mark that attribute as invalid" assert_equal ["is Wrong Create"], r.errors[:title], "A reply with a bad content should contain an error" end - def test_error_on_update + def test_valid_uses_update_context_when_persisted r = WrongReply.new r.title = "Bad" r.content = "Good" - assert r.save, "First save should be successful" + assert r.save, "First validation should be successful" r.title = "Wrong Update" - assert !r.save, "Second save should fail" + assert_not r.valid?, "Second validation should fail" assert r.errors[:title].any?, "A reply with a bad title should mark that attribute as invalid" assert_equal ["is Wrong Update"], r.errors[:title], "A reply with a bad content should contain an error" end - def test_error_on_given_context + def test_valid_using_special_context r = WrongReply.new(:title => "Valid title") assert !r.valid?(:special_case) assert_equal "Invalid", r.errors[:author_name].join @@ -45,11 +45,11 @@ class ValidationsTest < ActiveRecord::TestCase assert r.valid?(:special_case) r.author_name = nil - assert !r.save(:context => :special_case) + assert_not r.valid?(:special_case) assert_equal "Invalid", r.errors[:author_name].join r.author_name = "secret" - assert r.save(:context => :special_case) + assert r.valid?(:special_case) end def test_validate @@ -78,6 +78,20 @@ class ValidationsTest < ActiveRecord::TestCase assert_equal r, invalid.record end + def test_validate_with_bang + assert_raise(ActiveRecord::RecordInvalid) do + WrongReply.new.validate! + end + end + + def test_validate_with_bang_and_context + assert_raise(ActiveRecord::RecordInvalid) do + WrongReply.new.validate!(:special_case) + end + r = WrongReply.new(:title => "Valid title", :author_name => "secret", :content => "Good") + assert r.validate!(:special_case) + end + def test_exception_on_create_bang_many assert_raise(ActiveRecord::RecordInvalid) do WrongReply.create!([ { "title" => "OK" }, { "title" => "Wrong Create" }]) @@ -100,7 +114,7 @@ class ValidationsTest < ActiveRecord::TestCase end end - def test_create_without_validation + def test_save_without_validation reply = WrongReply.new assert !reply.save assert reply.save(:validate => false) diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb index 3cb617497d..c34e7d5a30 100644 --- a/activerecord/test/cases/xml_serialization_test.rb +++ b/activerecord/test/cases/xml_serialization_test.rb @@ -226,7 +226,6 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase xml = REXML::Document.new(topics(:first).to_xml(:indent => 0)) bonus_time_in_current_timezone = topics(:first).bonus_time.xmlschema written_on_in_current_timezone = topics(:first).written_on.xmlschema - last_read_in_current_timezone = topics(:first).last_read.xmlschema assert_equal "topic", xml.root.name assert_equal "The First Topic" , xml.elements["//title"].text @@ -248,14 +247,9 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase assert_equal "integer", xml.elements["//parent-id"].attributes['type'] assert_equal "true", xml.elements["//parent-id"].attributes['nil'] - if current_adapter?(:SybaseAdapter) - assert_equal last_read_in_current_timezone, xml.elements["//last-read"].text - assert_equal "dateTime" , xml.elements["//last-read"].attributes['type'] - else - # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb) - assert_equal "2004-04-15", xml.elements["//last-read"].text - assert_equal "date" , xml.elements["//last-read"].attributes['type'] - end + # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb) + assert_equal "2004-04-15", xml.elements["//last-read"].text + assert_equal "date" , xml.elements["//last-read"].attributes['type'] # Oracle and DB2 don't have true boolean or time-only fields unless current_adapter?(:OracleAdapter, :DB2Adapter) @@ -422,8 +416,9 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase def test_should_support_aliased_attributes xml = Author.select("name as firstname").to_xml - array = Hash.from_xml(xml)['authors'] - assert_equal array.size, array.select { |author| author.has_key? 'firstname' }.size + Author.all.each do |author| + assert xml.include?(%(<firstname>#{author.name}</firstname>)), xml + end end def test_array_to_xml_including_has_many_association diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb index 15815d56e4..bce59b4fcd 100644 --- a/activerecord/test/cases/yaml_serialization_test.rb +++ b/activerecord/test/cases/yaml_serialization_test.rb @@ -1,8 +1,11 @@ require 'cases/helper' require 'models/topic' +require 'models/reply' +require 'models/post' +require 'models/author' class YamlSerializationTest < ActiveRecord::TestCase - fixtures :topics + fixtures :topics, :authors, :posts def test_to_yaml_with_time_with_zone_should_not_raise_exception with_timezone_config aware_attributes: true, zone: "Pacific Time (US & Canada)" do @@ -23,13 +26,6 @@ class YamlSerializationTest < ActiveRecord::TestCase assert_equal({:omg=>:lol}, YAML.load(YAML.dump(topic)).content) end - def test_encode_with_coder - topic = Topic.first - coder = {} - topic.encode_with coder - assert_equal({'attributes' => topic.attributes}, coder) - end - def test_psych_roundtrip topic = Topic.first assert topic @@ -47,4 +43,44 @@ class YamlSerializationTest < ActiveRecord::TestCase def test_active_record_relation_serialization [Topic.all].to_yaml end + + def test_raw_types_are_not_changed_on_round_trip + topic = Topic.new(parent_id: "123") + assert_equal "123", topic.parent_id_before_type_cast + assert_equal "123", YAML.load(YAML.dump(topic)).parent_id_before_type_cast + end + + def test_cast_types_are_not_changed_on_round_trip + topic = Topic.new(parent_id: "123") + assert_equal 123, topic.parent_id + assert_equal 123, YAML.load(YAML.dump(topic)).parent_id + end + + def test_new_records_remain_new_after_round_trip + topic = Topic.new + + assert topic.new_record?, "Sanity check that new records are new" + assert YAML.load(YAML.dump(topic)).new_record?, "Record should be new after deserialization" + + topic.save! + + assert_not topic.new_record?, "Saved records are not new" + assert_not YAML.load(YAML.dump(topic)).new_record?, "Saved record should not be new after deserialization" + + topic = Topic.select('title').last + + assert_not topic.new_record?, "Loaded records without ID are not new" + assert_not YAML.load(YAML.dump(topic)).new_record?, "Record should not be new after deserialization" + end + + def test_types_of_virtual_columns_are_not_changed_on_round_trip + author = Author.select('authors.*, count(posts.id) as posts_count') + .joins(:posts) + .group('authors.id') + .first + dumped = YAML.load(YAML.dump(author)) + + assert_equal 5, author.posts_count + assert_equal 5, dumped.posts_count + end end diff --git a/activerecord/test/config.example.yml b/activerecord/test/config.example.yml index 479b8c050d..a54914c372 100644 --- a/activerecord/test/config.example.yml +++ b/activerecord/test/config.example.yml @@ -51,28 +51,6 @@ connections: password: arunit database: arunit2 - firebird: - arunit: - host: localhost - username: rails - password: rails - charset: UTF8 - arunit2: - host: localhost - username: rails - password: rails - charset: UTF8 - - frontbase: - arunit: - host: localhost - username: rails - session_name: unittest-<%= $$ %> - arunit2: - host: localhost - username: rails - session_name: unittest-<%= $$ %> - mysql: arunit: username: rails @@ -130,11 +108,3 @@ connections: arunit2: adapter: sqlite3 database: ':memory:' - - sybase: - arunit: - host: database_ASE - username: sa - arunit2: - host: database_ASE - username: sa diff --git a/activerecord/test/fixtures/books.yml b/activerecord/test/fixtures/books.yml index fb48645456..abe56752c6 100644 --- a/activerecord/test/fixtures/books.yml +++ b/activerecord/test/fixtures/books.yml @@ -2,8 +2,10 @@ awdr: author_id: 1 id: 1 name: "Agile Web Development with Rails" + format: "paperback" rfr: author_id: 1 id: 2 name: "Ruby for Rails" + format: "ebook" diff --git a/activerecord/test/fixtures/fk_test_has_pk.yml b/activerecord/test/fixtures/fk_test_has_pk.yml index c93952180b..73882bac41 100644 --- a/activerecord/test/fixtures/fk_test_has_pk.yml +++ b/activerecord/test/fixtures/fk_test_has_pk.yml @@ -1,2 +1,2 @@ first: - id: 1
\ No newline at end of file + pk_id: 1
\ No newline at end of file diff --git a/activerecord/test/fixtures/posts.yml b/activerecord/test/fixtures/posts.yml index 7298096025..86d46f753a 100644 --- a/activerecord/test/fixtures/posts.yml +++ b/activerecord/test/fixtures/posts.yml @@ -4,7 +4,6 @@ welcome: title: Welcome to the weblog body: Such a lovely day comments_count: 2 - taggings_count: 1 tags_count: 1 type: Post @@ -14,7 +13,6 @@ thinking: title: So I was thinking body: Like I hopefully always am comments_count: 1 - taggings_count: 1 tags_count: 1 type: SpecialPost diff --git a/activerecord/test/fixtures/topics.yml b/activerecord/test/fixtures/topics.yml index bf049abbf1..4c98b10380 100644 --- a/activerecord/test/fixtures/topics.yml +++ b/activerecord/test/fixtures/topics.yml @@ -6,7 +6,7 @@ first: written_on: 2003-07-16t15:28:11.2233+01:00 last_read: 2004-04-15 bonus_time: 2005-01-30t15:28:00.00+01:00 - content: Have a nice day + content: "--- Have a nice day\n...\n" approved: false replies_count: 1 @@ -15,7 +15,7 @@ second: title: The Second Topic of the day author_name: Mary written_on: 2004-07-15t15:28:00.0099+01:00 - content: Have a nice day + content: "--- Have a nice day\n...\n" approved: true replies_count: 0 parent_id: 1 @@ -26,7 +26,7 @@ third: title: The Third Topic of the day author_name: Carl written_on: 2012-08-12t20:24:22.129346+00:00 - content: I'm a troll + content: "--- I'm a troll\n...\n" approved: true replies_count: 1 @@ -35,7 +35,7 @@ fourth: title: The Fourth Topic of the day author_name: Carl written_on: 2006-07-15t15:28:00.0099+01:00 - content: Why not? + content: "--- Why not?\n...\n" approved: true type: Reply parent_id: 3 @@ -45,5 +45,5 @@ fifth: title: The Fifth Topic of the day author_name: Jason written_on: 2013-07-13t12:11:00.0099+01:00 - content: Omakase + content: "--- Omakase\n...\n" approved: true diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index c197951c71..8949cf5826 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -140,6 +140,8 @@ class Author < ActiveRecord::Base has_many :posts_with_default_include, :class_name => 'PostWithDefaultInclude' has_many :comments_on_posts_with_default_include, :through => :posts_with_default_include, :source => :comments + has_many :posts_with_signature, ->(record) { where("posts.title LIKE ?", "%by #{record.name.downcase}%") }, class_name: "Post" + scope :relation_include_posts, -> { includes(:posts) } scope :relation_include_tags, -> { includes(:tags) } diff --git a/activerecord/test/models/club.rb b/activerecord/test/models/club.rb index a762ad4bb5..6ceafe5858 100644 --- a/activerecord/test/models/club.rb +++ b/activerecord/test/models/club.rb @@ -14,3 +14,10 @@ class Club < ActiveRecord::Base "I'm sorry sir, this is a *private* club, not a *pirate* club" end end + +class SuperClub < ActiveRecord::Base + self.table_name = "clubs" + + has_many :memberships, class_name: 'SuperMembership', foreign_key: 'club_id' + has_many :members, through: :memberships +end diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index ede5fbd0c6..15970758db 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -7,6 +7,9 @@ class Comment < ActiveRecord::Base scope :created, -> { all } belongs_to :post, :counter_cache => true + belongs_to :author, polymorphic: true + belongs_to :resource, polymorphic: true + has_many :ratings belongs_to :first_post, :foreign_key => :post_id @@ -26,6 +29,10 @@ class Comment < ActiveRecord::Base all end scope :all_as_scope, -> { all } + + def to_s + body + end end class SpecialComment < Comment @@ -36,3 +43,11 @@ end class VerySpecialComment < Comment end + +class CommentThatAutomaticallyAltersPostBody < Comment + belongs_to :post, class_name: "PostThatLoadsCommentsInAnAfterSaveHook", foreign_key: :post_id + + after_save do |comment| + comment.post.update_attributes(body: "Automatically altered") + end +end diff --git a/activerecord/test/models/company_in_module.rb b/activerecord/test/models/company_in_module.rb index 38b0b6aafa..dae102d12b 100644 --- a/activerecord/test/models/company_in_module.rb +++ b/activerecord/test/models/company_in_module.rb @@ -46,6 +46,24 @@ module MyApplication end end end + + module Suffixed + def self.table_name_suffix + '_suffixed' + end + + class Company < ActiveRecord::Base + end + + class Firm < Company + self.table_name = 'companies' + end + + module Nested + class Company < ActiveRecord::Base + end + end + end end module Billing diff --git a/activerecord/test/models/contact.rb b/activerecord/test/models/contact.rb index a1cb8d62b6..3ea17c3abf 100644 --- a/activerecord/test/models/contact.rb +++ b/activerecord/test/models/contact.rb @@ -8,6 +8,7 @@ module ContactFakeColumns table_name => 'id' } + column :id, :integer column :name, :string column :age, :integer column :avatar, :binary diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index 762259ffa3..5bd2f00129 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -13,6 +13,8 @@ class Developer < ActiveRecord::Base end end + accepts_nested_attributes_for :projects + has_and_belongs_to_many :projects_extended_by_name, -> { extending(DeveloperProjectsAssociationExtension) }, :class_name => "Project", @@ -74,12 +76,6 @@ class AuditLog < ActiveRecord::Base belongs_to :unvalidated_developer, :class_name => 'Developer' end -DeveloperSalary = Struct.new(:amount) -class DeveloperWithAggregate < ActiveRecord::Base - self.table_name = 'developers' - composed_of :salary, :class_name => 'DeveloperSalary', :mapping => [%w(salary amount)] -end - class DeveloperWithBeforeDestroyRaise < ActiveRecord::Base self.table_name = 'developers' has_and_belongs_to_many :projects, :join_table => 'developers_projects', :foreign_key => 'developer_id' diff --git a/activerecord/test/models/face.rb b/activerecord/test/models/face.rb index edb75d333f..91e46f83e5 100644 --- a/activerecord/test/models/face.rb +++ b/activerecord/test/models/face.rb @@ -1,6 +1,8 @@ class Face < ActiveRecord::Base belongs_to :man, :inverse_of => :face belongs_to :polymorphic_man, :polymorphic => true, :inverse_of => :polymorphic_face + # Oracle identifier lengh is limited to 30 bytes or less, `polymorphic` renamed `poly` + belongs_to :poly_man_without_inverse, :polymorphic => true # These is a "broken" inverse_of for the purposes of testing belongs_to :horrible_man, :class_name => 'Man', :inverse_of => :horrible_face belongs_to :horrible_polymorphic_man, :polymorphic => true, :inverse_of => :horrible_polymorphic_face diff --git a/activerecord/test/models/man.rb b/activerecord/test/models/man.rb index f4d127730c..4fbb6b226b 100644 --- a/activerecord/test/models/man.rb +++ b/activerecord/test/models/man.rb @@ -1,6 +1,7 @@ class Man < ActiveRecord::Base has_one :face, :inverse_of => :man has_one :polymorphic_face, :class_name => 'Face', :as => :polymorphic_man, :inverse_of => :polymorphic_man + has_one :polymorphic_face_without_inverse, :class_name => 'Face', :as => :poly_man_without_inverse has_many :interests, :inverse_of => :man has_many :polymorphic_interests, :class_name => 'Interest', :as => :polymorphic_man, :inverse_of => :polymorphic_man # These are "broken" inverse_of associations for the purposes of testing diff --git a/activerecord/test/models/owner.rb b/activerecord/test/models/owner.rb index cf24502d3a..2e3a9a3681 100644 --- a/activerecord/test/models/owner.rb +++ b/activerecord/test/models/owner.rb @@ -3,6 +3,18 @@ class Owner < ActiveRecord::Base has_many :pets, -> { order 'pets.name desc' } has_many :toys, :through => :pets + belongs_to :last_pet, class_name: 'Pet' + scope :including_last_pet, -> { + select(%q[ + owners.*, ( + select p.pet_id from pets p + where p.owner_id = owners.owner_id + order by p.name desc + limit 1 + ) as last_pet_id + ]).includes(:last_pet) + } + after_commit :execute_blocks def blocks diff --git a/activerecord/test/models/pirate.rb b/activerecord/test/models/pirate.rb index e472cf951d..90a3c3ecee 100644 --- a/activerecord/test/models/pirate.rb +++ b/activerecord/test/models/pirate.rb @@ -84,3 +84,9 @@ end class DestructivePirate < Pirate has_one :dependent_ship, :class_name => 'Ship', :foreign_key => :pirate_id, :dependent => :destroy end + +class FamousPirate < ActiveRecord::Base + self.table_name = 'pirates' + has_many :famous_ships + validates_presence_of :catchphrase, on: :conference +end
\ No newline at end of file diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 6399a68d95..a29858213b 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -88,7 +88,7 @@ class Post < ActiveRecord::Base has_and_belongs_to_many :categories has_and_belongs_to_many :special_categories, :join_table => "categories_posts", :association_foreign_key => 'category_id' - has_many :taggings, :as => :taggable + has_many :taggings, :as => :taggable, :counter_cache => :tags_count has_many :tags, :through => :taggings do def add_joins_and_select select('tags.*, authors.id as author_id') @@ -167,10 +167,6 @@ class Post < ActiveRecord::Base return @log if message.nil? @log << [message, side, new_record] end - - def self.what_are_you - 'a post...' - end end class SpecialPost < Post; end @@ -212,3 +208,12 @@ class SpecialPostWithDefaultScope < ActiveRecord::Base self.table_name = 'posts' default_scope { where(:id => [1, 5,6]) } end + +class PostThatLoadsCommentsInAnAfterSaveHook < ActiveRecord::Base + self.table_name = 'posts' + has_many :comments, class_name: "CommentThatAutomaticallyAltersPostBody", foreign_key: :post_id + + after_save do |post| + post.comments.load + end +end diff --git a/activerecord/test/models/publisher.rb b/activerecord/test/models/publisher.rb new file mode 100644 index 0000000000..0d4a7f9235 --- /dev/null +++ b/activerecord/test/models/publisher.rb @@ -0,0 +1,2 @@ +module Publisher +end diff --git a/activerecord/test/models/publisher/article.rb b/activerecord/test/models/publisher/article.rb new file mode 100644 index 0000000000..d73a8eb936 --- /dev/null +++ b/activerecord/test/models/publisher/article.rb @@ -0,0 +1,4 @@ +class Publisher::Article < ActiveRecord::Base + has_and_belongs_to_many :magazines + has_and_belongs_to_many :tags +end diff --git a/activerecord/test/models/publisher/magazine.rb b/activerecord/test/models/publisher/magazine.rb new file mode 100644 index 0000000000..82e1a14008 --- /dev/null +++ b/activerecord/test/models/publisher/magazine.rb @@ -0,0 +1,3 @@ +class Publisher::Magazine < ActiveRecord::Base + has_and_belongs_to_many :articles +end diff --git a/activerecord/test/models/ship.rb b/activerecord/test/models/ship.rb index 3da031946f..77a4728d0b 100644 --- a/activerecord/test/models/ship.rb +++ b/activerecord/test/models/ship.rb @@ -17,3 +17,9 @@ class Ship < ActiveRecord::Base false end end + +class FamousShip < ActiveRecord::Base + self.table_name = 'ships' + belongs_to :famous_pirate + validates_presence_of :name, on: :conference +end diff --git a/activerecord/test/models/tagging.rb b/activerecord/test/models/tagging.rb index f91f2ad2e9..a6c05da26a 100644 --- a/activerecord/test/models/tagging.rb +++ b/activerecord/test/models/tagging.rb @@ -8,6 +8,6 @@ class Tagging < ActiveRecord::Base belongs_to :invalid_tag, :class_name => 'Tag', :foreign_key => 'tag_id' belongs_to :blue_tag, -> { where :tags => { :name => 'Blue' } }, :class_name => 'Tag', :foreign_key => :tag_id belongs_to :tag_with_primary_key, :class_name => 'Tag', :foreign_key => :tag_id, :primary_key => :custom_primary_key - belongs_to :taggable, :polymorphic => true, :counter_cache => true + belongs_to :taggable, :polymorphic => true, :counter_cache => :tags_count has_many :things, :through => :taggable end diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index 4fcbf4dbd2..e9294a11b9 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -1,6 +1,6 @@ ActiveRecord::Schema.define do - %w(postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings postgresql_uuids postgresql_ltrees + %w(postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_uuids postgresql_ltrees postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent postgresql_json_data_type postgresql_citext).each do |table_name| execute "DROP TABLE IF EXISTS #{quote_table_name table_name}" end @@ -118,13 +118,6 @@ _SQL end execute <<_SQL - CREATE TABLE postgresql_moneys ( - id SERIAL PRIMARY KEY, - wealth MONEY - ); -_SQL - - execute <<_SQL CREATE TABLE postgresql_numbers ( id SERIAL PRIMARY KEY, single REAL, @@ -150,14 +143,6 @@ _SQL _SQL execute <<_SQL - CREATE TABLE postgresql_bit_strings ( - id SERIAL PRIMARY KEY, - bit_string BIT(8), - bit_string_varying BIT VARYING(8) - ); -_SQL - - execute <<_SQL CREATE TABLE postgresql_oids ( id SERIAL PRIMARY KEY, obj_id OID diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index da3074e90f..a8b21904ac 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -9,14 +9,6 @@ ActiveRecord::Schema.define do #put adapter specific setup here case adapter_name - # For Firebird, set the sequence values 10000 when create_table is called; - # this prevents primary key collisions between "normally" created records - # and fixture-based (YAML) records. - when "Firebird" - def create_table(*args, &block) - ActiveRecord::Base.connection.create_table(*args, &block) - ActiveRecord::Base.connection.execute "SET GENERATOR #{args.first}_seq TO 10000" - end when "PostgreSQL" enable_uuid_ossp!(ActiveRecord::Base.connection) create_table :uuid_parents, id: :uuid, force: true do |t| @@ -62,6 +54,19 @@ ActiveRecord::Schema.define do t.string :name end + create_table :articles, force: true do |t| + end + + create_table :articles_magazines, force: true do |t| + t.references :article + t.references :magazine + end + + create_table :articles_tags, force: true do |t| + t.references :article + t.references :tag + end + create_table :audit_logs, force: true do |t| t.column :message, :string, null: false t.column :developer_id, :integer, null: false @@ -79,6 +84,8 @@ ActiveRecord::Schema.define do create_table :author_addresses, force: true do |t| end + add_foreign_key :authors, :author_addresses + create_table :author_favorites, force: true do |t| t.column :author_id, :integer t.column :favorite_author_id, :integer @@ -103,6 +110,7 @@ ActiveRecord::Schema.define do create_table :books, force: true do |t| t.integer :author_id + t.string :format t.column :name, :string t.column :status, :integer, default: 0 t.column :read_status, :integer, default: 0 @@ -184,9 +192,12 @@ ActiveRecord::Schema.define do t.text :body, null: false end t.string :type - t.integer :taggings_count, default: 0 + t.integer :tags_count, default: 0 t.integer :children_count, default: 0 t.integer :parent_id + t.references :author, polymorphic: true + t.string :resource_id + t.string :resource_type end create_table :companies, force: true do |t| @@ -385,6 +396,9 @@ ActiveRecord::Schema.define do t.column :custom_lock_version, :integer end + create_table :magazines, force: true do |t| + end + create_table :mateys, id: false, force: true do |t| t.column :pirate_id, :integer t.column :target_id, :integer @@ -522,6 +536,7 @@ ActiveRecord::Schema.define do t.references :best_friend t.references :best_friend_of t.integer :insures, null: false, default: 0 + t.timestamp :born_at t.timestamps end @@ -556,7 +571,6 @@ ActiveRecord::Schema.define do 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 @@ -767,6 +781,8 @@ ActiveRecord::Schema.define do t.integer :man_id t.integer :polymorphic_man_id t.string :polymorphic_man_type + t.integer :poly_man_without_inverse_id + t.string :poly_man_without_inverse_type t.integer :horrible_polymorphic_man_id t.string :horrible_polymorphic_man_type end @@ -841,12 +857,18 @@ ActiveRecord::Schema.define do 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, primary_key: "pk_id" 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'})" + add_foreign_key :fk_test_has_fk, :fk_test_has_pk, column: "fk_id", name: "fk_name", primary_key: "pk_id" + add_foreign_key :lessons_students, :students + end - execute "ALTER TABLE lessons_students ADD CONSTRAINT student_id_fk FOREIGN KEY (#{quote_column_name 'student_id'}) REFERENCES #{quote_table_name 'students'} (#{quote_column_name 'id'})" + create_table :overloaded_types, force: true do |t| + t.float :overloaded_float, default: 500 + t.float :unoverloaded_float + t.string :overloaded_string_with_limit, limit: 255 + t.string :string_with_default, default: 'the original default' end end diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb index b7aff4f47d..b5552c2755 100644 --- a/activerecord/test/schema/sqlite_specific_schema.rb +++ b/activerecord/test/schema/sqlite_specific_schema.rb @@ -7,7 +7,7 @@ ActiveRecord::Schema.define do execute "DROP TABLE fk_test_has_pk" rescue nil execute <<_SQL CREATE TABLE 'fk_test_has_pk' ( - 'id' INTEGER NOT NULL PRIMARY KEY + 'pk_id' INTEGER NOT NULL PRIMARY KEY ); _SQL @@ -16,7 +16,7 @@ _SQL 'id' INTEGER NOT NULL PRIMARY KEY, 'fk_id' INTEGER NOT NULL, - FOREIGN KEY ('fk_id') REFERENCES 'fk_test_has_pk'('id') + FOREIGN KEY ('fk_id') REFERENCES 'fk_test_has_pk'('pk_id') ); _SQL -end
\ No newline at end of file +end diff --git a/activerecord/test/support/ddl_helper.rb b/activerecord/test/support/ddl_helper.rb index 0107babaaf..43cb235e01 100644 --- a/activerecord/test/support/ddl_helper.rb +++ b/activerecord/test/support/ddl_helper.rb @@ -1,8 +1,8 @@ module DdlHelper def with_example_table(connection, table_name, definition = nil) - connection.exec_query("CREATE TABLE #{table_name}(#{definition})") + connection.execute("CREATE TABLE #{table_name}(#{definition})") yield ensure - connection.exec_query("DROP TABLE #{table_name}") + connection.execute("DROP TABLE #{table_name}") end end diff --git a/activerecord/test/support/schema_dumping_helper.rb b/activerecord/test/support/schema_dumping_helper.rb new file mode 100644 index 0000000000..2ae8d299e5 --- /dev/null +++ b/activerecord/test/support/schema_dumping_helper.rb @@ -0,0 +1,11 @@ +module SchemaDumpingHelper + def dump_table_schema(table, connection = ActiveRecord::Base.connection) + old_ignore_tables = ActiveRecord::SchemaDumper.ignore_tables + ActiveRecord::SchemaDumper.ignore_tables = connection.tables - [table] + stream = StringIO.new + ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) + stream.string + ensure + ActiveRecord::SchemaDumper.ignore_tables = old_ignore_tables + end +end diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 5e430d20fa..02aea8d21f 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,4 +1,174 @@ -* Add `SecureRandom::uuid_v3` and `SecureRandom::uuid_v5` to support stable +* Make `Hash#with_indifferent_access` copy the default proc too. + + *arthurnn*, *Xanders* + +* Add `String#truncate_words` to truncate a string by a number of words. + + *Mohamed Osama* + +* Deprecate `capture` and `quietly`. + + These methods are not thread safe and may cause issues when used in threaded environments. + To avoid problems we are deprecating them. + + *Tom Meier* + +* `DateTime#to_f` now preserves the fractional seconds instead of always + rounding to `.0`. + + Fixes #15994. + + *John Paul Ashenfelter* + +* Add `Hash#transform_values` to simplify a common pattern where the values of a + hash must change, but the keys are left the same. + + *Sean Griffin* + +* Always instrument `ActiveSupport::Cache`. + + Since `ActiveSupport::Notifications` only instrument items when there + are subscriber we don't need to disable instrumentation. + + *Peter Wagenet* + +* Make the `apply_inflections` method case-insensitive when checking + whether a word is uncountable or not. + + *Robin Dupret* + +* Make Dependencies pass a name to NameError error. + + *arthurnn* + +* Fixed `ActiveSupport::Cache::FileStore` exploding with long paths. + + *Adam Panzer / Michael Grosser* + +* Fixed `ActiveSupport::TimeWithZone#-` so precision is not unnecessarily lost + when working with objects with a nanosecond component. + + `ActiveSupport::TimeWithZone#-` should return the same result as if we were + using `Time#-`: + + Time.now.end_of_day - Time.now.beginning_of_day #=> 86399.999999999 + + Before: + + Time.zone.now.end_of_day.nsec #=> 999999999 + Time.zone.now.end_of_day - Time.zone.now.beginning_of_day #=> 86400.0 + + After: + + Time.zone.now.end_of_day - Time.zone.now.beginning_of_day + #=> 86399.999999999 + + *Gordon Chan* + +* Fixed precision error in NumberHelper when using Rationals. + + Before: + + ActiveSupport::NumberHelper.number_to_rounded Rational(1000, 3), precision: 2 + #=> "330.00" + + After: + + ActiveSupport::NumberHelper.number_to_rounded Rational(1000, 3), precision: 2 + #=> "333.33" + + See #15379. + + *Juanjo Bazán* + +* Removed deprecated `Numeric#ago` and friends + + Replacements: + + 5.ago => 5.seconds.ago + 5.until => 5.seconds.until + 5.since => 5.seconds.since + 5.from_now => 5.seconds.from_now + + See #12389 for the history and rationale behind this. + + *Godfrey Chan* + +* DateTime `advance` now supports partial days. + + Before: + + DateTime.now.advance(days: 1, hours: 12) + + After: + + DateTime.now.advance(days: 1.5) + + Fixes #12005. + + *Shay Davidson* + +* `Hash#deep_transform_keys` and `Hash#deep_transform_keys!` now transform hashes + in nested arrays. This change also applies to `Hash#deep_stringify_keys`, + `Hash#deep_stringify_keys!`, `Hash#deep_symbolize_keys` and + `Hash#deep_symbolize_keys!`. + + *OZAWA Sakuro* + +* Fixed confusing `DelegationError` in `Module#delegate`. + + See #15186. + + *Vladimir Yarotsky* + +* Fixed `ActiveSupport::Subscriber` so that no duplicate subscriber is created + when a subscriber method is redefined. + + *Dennis Schön* + +* Remove deprecated string based terminators for `ActiveSupport::Callbacks`. + + *Eileen M. Uchitelle* + +* Fixed an issue when using + `ActiveSupport::NumberHelper::NumberToDelimitedConverter` to + convert a value that is an `ActiveSupport::SafeBuffer` introduced + in 2da9d67. + + See #15064. + + *Mark J. Titorenko* + +* `TimeZone#parse` defaults the day of the month to '1' if any other date + components are specified. This is more consistent with the behavior of + `Time#parse`. + + *Ulysse Carion* + +* `humanize` strips leading underscores, if any. + + Before: + + '_id'.humanize # => "" + + After: + + '_id'.humanize # => "Id" + + *Xavier Noria* + +* Fixed backward compatibility isues introduced in 326e652. + + Empty Hash or Array should not present in serialization result. + + {a: []}.to_query # => "" + {a: {}}.to_query # => "" + + For more info see #14948. + + *Bogdan Gusiev* + +* Add `Digest::UUID::uuid_v3` and `Digest::UUID::uuid_v5` to support stable UUID fixtures on PostgreSQL. *Roderick van Domburg* diff --git a/activesupport/README.rdoc b/activesupport/README.rdoc index f3582767c0..a6424a353a 100644 --- a/activesupport/README.rdoc +++ b/activesupport/README.rdoc @@ -30,6 +30,11 @@ API documentation is at: * http://api.rubyonrails.org -Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here: +Bug reports can be filed for the Ruby on Rails project here: * https://github.com/rails/rails/issues + +Feature requests should be discussed on the rails-core mailing list here: + +* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core + diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index f3625e8b79..c0b457c341 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -20,7 +20,7 @@ Gem::Specification.new do |s| s.rdoc_options.concat ['--encoding', 'UTF-8'] - s.add_dependency 'i18n', '~> 0.6', '>= 0.6.9' + s.add_dependency 'i18n', '>= 0.7.0.dev', '< 0.8' s.add_dependency 'json', '~> 1.7', '>= 1.7.7' s.add_dependency 'tzinfo', '~> 1.1' s.add_dependency 'minitest', '~> 5.1' diff --git a/activesupport/lib/active_support/backtrace_cleaner.rb b/activesupport/lib/active_support/backtrace_cleaner.rb index d58578b7bc..d06f22ad5c 100644 --- a/activesupport/lib/active_support/backtrace_cleaner.rb +++ b/activesupport/lib/active_support/backtrace_cleaner.rb @@ -13,7 +13,7 @@ module ActiveSupport # can focus on the rest. # # bc = BacktraceCleaner.new - # bc.add_filter { |line| line.gsub(Rails.root, '') } # strip the Rails.root prefix + # bc.add_filter { |line| line.gsub(Rails.root.to_s, '') } # strip the Rails.root prefix # bc.add_silencer { |line| line =~ /mongrel|rubygems/ } # skip any lines from mongrel or rubygems # bc.clean(exception.backtrace) # perform the cleanup # @@ -65,14 +65,14 @@ module ActiveSupport @silencers << block end - # Will remove all silencers, but leave in the filters. This is useful if - # your context of debugging suddenly expands as you suspect a bug in one of + # Removes all silencers, but leaves in the filters. Useful if your + # context of debugging suddenly expands as you suspect a bug in one of # the libraries you use. def remove_silencers! @silencers = [] end - # Removes all filters, but leaves in silencers. Useful if you suddenly + # Removes all filters, but leaves in the silencers. Useful if you suddenly # need to see entire filepaths in the backtrace that you had already # filtered out. def remove_filters! diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index a627fa8651..a3f672d4cc 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -8,6 +8,7 @@ require 'active_support/core_ext/numeric/bytes' require 'active_support/core_ext/numeric/time' require 'active_support/core_ext/object/to_param' require 'active_support/core_ext/string/inflections' +require 'active_support/deprecation' module ActiveSupport # See ActiveSupport::Cache::Store for documentation. @@ -178,14 +179,16 @@ module ActiveSupport @silence = previous_silence end - # Set to +true+ if cache stores should be instrumented. - # Default is +false+. + # :deprecated: def self.instrument=(boolean) - Thread.current[:instrument_cache_store] = boolean + ActiveSupport::Deprecation.warn "ActiveSupport::Cache.instrument= is deprecated and will be removed in Rails 5. Instrumentation is now always on so you can safely stop using it." + true end + # :deprecated: def self.instrument - Thread.current[:instrument_cache_store] || false + ActiveSupport::Deprecation.warn "ActiveSupport::Cache.instrument is deprecated and will be removed in Rails 5. Instrumentation is now always on so you can safely stop using it." + true end # Fetches data from the cache, using the given key. If there is data in @@ -539,13 +542,9 @@ module ActiveSupport def instrument(operation, key, options = nil) log(operation, key, options) - if self.class.instrument - payload = { :key => key } - payload.merge!(options) if options.is_a?(Hash) - ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload){ yield(payload) } - else - yield(nil) - end + payload = { :key => key } + payload.merge!(options) if options.is_a?(Hash) + ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload){ yield(payload) } end def log(operation, key, options = nil) diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index 8ed60aebac..d08ecd2f7d 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -14,6 +14,7 @@ module ActiveSupport DIR_FORMATTER = "%03X" FILENAME_MAX_SIZE = 228 # max filename size on file system is 255, minus room for timestamp and random characters appended by Tempfile (used by atomic write) + FILEPATH_MAX_SIZE = 900 # max is 1024, plus some room EXCLUDED_DIRS = ['.', '..'].freeze def initialize(cache_path, options = nil) @@ -117,6 +118,10 @@ module ActiveSupport # Translate a key into a file path. def key_file_path(key) + if key.size > FILEPATH_MAX_SIZE + key = Digest::MD5.hexdigest(key) + end + fname = URI.encode_www_form_component(key) hash = Zlib.adler32(fname) hash, dir_1 = hash.divmod(0x1000) diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index 05ca943776..cd467e13f6 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -71,7 +71,8 @@ module ActiveSupport # order. # # If the callback chain was halted, returns +false+. Otherwise returns the - # result of the block, or +true+ if no block is given. + # result of the block, +nil+ if no callbacks have been set, or +true+ + # if callbacks have been set but no block is given. # # run_callbacks :save do # save @@ -297,14 +298,14 @@ module ActiveSupport target = env.target value = env.value - unless env.halted + if env.halted + next_callback.call env + else user_callback.call(target, value) { env = next_callback.call env env.value } env - else - next_callback.call env end } end @@ -415,15 +416,8 @@ module ActiveSupport # Procs:: A proc to call with the object. # Objects:: An object with a <tt>before_foo</tt> method on it to call. # - # All of these objects are compiled into methods and handled - # the same after this point: - # - # Symbols:: Already methods. - # Strings:: class_eval'd into methods. - # Procs:: using define_method compiled into methods. - # Objects:: - # a method is created that calls the before_foo method - # on the object. + # All of these objects are converted into a lambda and handled + # the same after this point. def make_lambda(filter) case filter when Symbol @@ -572,7 +566,7 @@ module ActiveSupport # # set_callback :save, :before, :before_meth # set_callback :save, :after, :after_meth, if: :condition - # set_callback :save, :around, ->(r, &block) { stuff; result = block.call; stuff } + # set_callback :save, :around, ->(r, block) { stuff; result = block.call; stuff } # # The second arguments indicates whether the callback is to be run +:before+, # +:after+, or +:around+ the event. If omitted, +:before+ is assumed. This @@ -724,12 +718,6 @@ module ActiveSupport # would call <tt>Audit#save</tt>. def define_callbacks(*names) options = names.extract_options! - if options.key?(:terminator) && String === options[:terminator] - ActiveSupport::Deprecation.warn "String based terminators are deprecated, please use a lambda" - value = options[:terminator] - line = class_eval "lambda { |result| #{value} }", __FILE__, __LINE__ - options[:terminator] = lambda { |target, result| target.instance_exec(result, &line) } - end names.each do |name| class_attribute "_#{name}_callbacks" diff --git a/activesupport/lib/active_support/core_ext/array/access.rb b/activesupport/lib/active_support/core_ext/array/access.rb index 67f58bc0fe..caa499dfa2 100644 --- a/activesupport/lib/active_support/core_ext/array/access.rb +++ b/activesupport/lib/active_support/core_ext/array/access.rb @@ -5,6 +5,8 @@ class Array # %w( a b c d ).from(2) # => ["c", "d"] # %w( a b c d ).from(10) # => [] # %w().from(0) # => [] + # %w( a b c d ).from(-2) # => ["c", "d"] + # %w( a b c ).from(-10) # => [] def from(position) self[position, length] || [] end @@ -15,8 +17,10 @@ class Array # %w( a b c d ).to(2) # => ["a", "b", "c"] # %w( a b c d ).to(10) # => ["a", "b", "c", "d"] # %w().to(0) # => [] + # %w( a b c d ).to(-2) # => ["a", "b", "c"] + # %w( a b c ).to(-10) # => [] def to(position) - first position + 1 + self[0..position] end # Equal to <tt>self[1]</tt>. diff --git a/activesupport/lib/active_support/core_ext/date_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_time/calculations.rb index 73ad0aa097..289ca12b5e 100644 --- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb @@ -53,6 +53,16 @@ class DateTime # <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>, <tt>:hours</tt>, # <tt>:minutes</tt>, <tt>:seconds</tt>. def advance(options) + unless options[:weeks].nil? + options[:weeks], partial_weeks = options[:weeks].divmod(1) + options[:days] = options.fetch(:days, 0) + 7 * partial_weeks + end + + unless options[:days].nil? + options[:days], partial_days = options[:days].divmod(1) + options[:hours] = options.fetch(:hours, 0) + 24 * partial_days + end + d = to_date.advance(options) datetime_advanced_by_date = change(:year => d.year, :month => d.month, :day => d.day) seconds_to_advance = \ @@ -63,7 +73,7 @@ class DateTime if seconds_to_advance.zero? datetime_advanced_by_date else - datetime_advanced_by_date.since seconds_to_advance + datetime_advanced_by_date.since(seconds_to_advance) end end diff --git a/activesupport/lib/active_support/core_ext/date_time/conversions.rb b/activesupport/lib/active_support/core_ext/date_time/conversions.rb index 6ddfb72a0d..2a9c09fc29 100644 --- a/activesupport/lib/active_support/core_ext/date_time/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date_time/conversions.rb @@ -71,9 +71,9 @@ class DateTime civil(year, month, day, hour, min, sec, offset) end - # Converts +self+ to a floating-point number of seconds since the Unix epoch. + # Converts +self+ to a floating-point number of seconds, including fractional microseconds, since the Unix epoch. def to_f - seconds_since_unix_epoch.to_f + seconds_since_unix_epoch.to_f + sec_fraction end # Converts +self+ to an integer number of seconds since the Unix epoch. diff --git a/activesupport/lib/active_support/core_ext/digest/uuid.rb b/activesupport/lib/active_support/core_ext/digest/uuid.rb new file mode 100644 index 0000000000..593c51bba2 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/digest/uuid.rb @@ -0,0 +1,51 @@ +require 'securerandom' + +module Digest + module UUID + DNS_NAMESPACE = "k\xA7\xB8\x10\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc: + URL_NAMESPACE = "k\xA7\xB8\x11\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc: + OID_NAMESPACE = "k\xA7\xB8\x12\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc: + X500_NAMESPACE = "k\xA7\xB8\x14\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc: + + # Generates a v5 non-random UUID (Universally Unique IDentifier). + # + # Using Digest::MD5 generates version 3 UUIDs; Digest::SHA1 generates version 5 UUIDs. + # uuid_from_hash always generates the same UUID for a given name and namespace combination. + # + # See RFC 4122 for details of UUID at: http://www.ietf.org/rfc/rfc4122.txt + def self.uuid_from_hash(hash_class, uuid_namespace, name) + if hash_class == Digest::MD5 + version = 3 + elsif hash_class == Digest::SHA1 + version = 5 + else + raise ArgumentError, "Expected Digest::SHA1 or Digest::MD5, got #{hash_class.name}." + end + + hash = hash_class.new + hash.update(uuid_namespace) + hash.update(name) + + ary = hash.digest.unpack('NnnnnN') + ary[2] = (ary[2] & 0x0FFF) | (version << 12) + ary[3] = (ary[3] & 0x3FFF) | 0x8000 + + "%08x-%04x-%04x-%04x-%04x%08x" % ary + end + + # Convenience method for uuid_from_hash using Digest::MD5. + def self.uuid_v3(uuid_namespace, name) + self.uuid_from_hash(Digest::MD5, uuid_namespace, name) + end + + # Convenience method for uuid_from_hash using Digest::SHA1. + def self.uuid_v5(uuid_namespace, name) + self.uuid_from_hash(Digest::SHA1, uuid_namespace, name) + end + + # Convenience method for SecureRandom.uuid. + def self.uuid_v4 + SecureRandom.uuid + end + end +end diff --git a/activesupport/lib/active_support/core_ext/hash.rb b/activesupport/lib/active_support/core_ext/hash.rb index f68e1662f9..af4d1da0eb 100644 --- a/activesupport/lib/active_support/core_ext/hash.rb +++ b/activesupport/lib/active_support/core_ext/hash.rb @@ -6,3 +6,4 @@ require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/hash/reverse_merge' require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/hash/transform_values' diff --git a/activesupport/lib/active_support/core_ext/hash/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb index 6c3e48a3ca..2149d4439d 100644 --- a/activesupport/lib/active_support/core_ext/hash/conversions.rb +++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb @@ -221,7 +221,7 @@ module ActiveSupport def garbage?(value) # If the type is the only element which makes it then # this still makes the value nil, except if type is - # a XML node(where type['value'] is a Hash) + # an XML node(where type['value'] is a Hash) value['type'] && !value['type'].is_a?(::Hash) && value.size == 1 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 dc86c92003..763d563231 100644 --- a/activesupport/lib/active_support/core_ext/hash/deep_merge.rb +++ b/activesupport/lib/active_support/core_ext/hash/deep_merge.rb @@ -1,27 +1,38 @@ 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 = { a: true, b: { c: [1, 2, 3] } } + # h2 = { a: false, b: { x: [3, 4, 5] } } # - # 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"]} + # h1.deep_merge(h2) #=> { a: false, b: { c: [1, 2, 3], x: [3, 4, 5] } } + # + # Like with Hash#merge in the standard library, a block can be provided + # to merge values: + # + # h1 = { a: 100, b: 200, c: { c1: 100 } } + # h2 = { b: 250, c: { c1: 200 } } + # h1.deep_merge(h2) { |key, this_val, other_val| this_val + other_val } + # # => { a: 100, b: 450, c: { c1: 300 } } def deep_merge(other_hash, &block) dup.deep_merge!(other_hash, &block) end # Same as +deep_merge+, but modifies +self+. def deep_merge!(other_hash, &block) - other_hash.each_pair do |k,v| - tv = self[k] - if tv.is_a?(Hash) && v.is_a?(Hash) - self[k] = tv.deep_merge(v, &block) + other_hash.each_pair do |current_key, other_value| + this_value = self[current_key] + + self[current_key] = if this_value.is_a?(Hash) && other_value.is_a?(Hash) + this_value.deep_merge(other_value, &block) else - self[k] = block && tv ? block.call(k, tv, v) : v + if block_given? && key?(current_key) + block.call(current_key, this_value, other_value) + else + other_value + end end end + self end end diff --git a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb index 970d6faa1d..28cb3e2a3b 100644 --- a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb +++ b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb @@ -18,6 +18,6 @@ class Hash # # b = { b: 1 } # { a: b }.with_indifferent_access['a'] # calls b.nested_under_indifferent_access - # # => {"b"=>32} + # # => {"b"=>1} alias nested_under_indifferent_access with_indifferent_access end diff --git a/activesupport/lib/active_support/core_ext/hash/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb index 3d41aa8572..f4105f66b0 100644 --- a/activesupport/lib/active_support/core_ext/hash/keys.rb +++ b/activesupport/lib/active_support/core_ext/hash/keys.rb @@ -6,7 +6,8 @@ class Hash # hash.transform_keys{ |key| key.to_s.upcase } # # => {"NAME"=>"Rob", "AGE"=>"28"} def transform_keys - result = {} + return enum_for(:transform_keys) unless block_given? + result = self.class.new each_key do |key| result[yield(key)] = self[key] end @@ -16,6 +17,7 @@ class Hash # Destructively convert all keys using the block operations. # Same as transform_keys but modifies +self+. def transform_keys! + return enum_for(:transform_keys!) unless block_given? keys.each do |key| self[yield(key)] = delete(key) end @@ -27,7 +29,7 @@ class Hash # 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 @@ -44,7 +46,7 @@ class Hash # 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 @@ -57,9 +59,11 @@ class Hash end alias_method :to_options!, :symbolize_keys! - # Validate all keys in a hash match <tt>*valid_keys</tt>, raising ArgumentError - # on a mismatch. Note that keys are NOT treated indifferently, meaning if you - # use strings for keys but assert symbols as keys, this will fail. + # Validate all keys in a hash match <tt>*valid_keys</tt>, raising + # ArgumentError on a mismatch. + # + # Note that keys are treated differently than HashWithIndifferentAccess, + # meaning that string and symbol keys will not match. # # { name: 'Rob', years: '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: :years. Valid keys are: :name, :age" # { name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: :name. Valid keys are: 'name', 'age'" @@ -75,34 +79,26 @@ class Hash # 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. + # nested hashes and arrays. # # hash = { person: { name: 'Rob', age: '28' } } # # hash.deep_transform_keys{ |key| key.to_s.upcase } # # => {"PERSON"=>{"NAME"=>"Rob", "AGE"=>"28"}} def deep_transform_keys(&block) - result = {} - each do |key, value| - result[yield(key)] = value.is_a?(Hash) ? value.deep_transform_keys(&block) : value - end - result + _deep_transform_keys_in_object(self, &block) end # Destructively convert all keys by using the block operation. # This includes the keys from the root hash and from all - # nested hashes. + # nested hashes and arrays. def deep_transform_keys!(&block) - keys.each do |key| - value = delete(key) - self[yield(key)] = value.is_a?(Hash) ? value.deep_transform_keys!(&block) : value - end - self + _deep_transform_keys_in_object!(self, &block) end # Returns a new hash with all keys converted to strings. # This includes the keys from the root hash and from all - # nested hashes. + # nested hashes and arrays. # # hash = { person: { name: 'Rob', age: '28' } } # @@ -114,14 +110,14 @@ class Hash # Destructively convert all keys to strings. # This includes the keys from the root hash and from all - # nested hashes. + # nested hashes and arrays. def deep_stringify_keys! deep_transform_keys!{ |key| key.to_s } end # 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. + # and from all nested hashes and arrays. # # hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } } # @@ -133,8 +129,38 @@ class Hash # Destructively convert all keys to symbols, as long as they respond # to +to_sym+. This includes the keys from the root hash and from all - # nested hashes. + # nested hashes and arrays. def deep_symbolize_keys! deep_transform_keys!{ |key| key.to_sym rescue key } end + + private + # support methods for deep transforming nested hashes and arrays + def _deep_transform_keys_in_object(object, &block) + case object + when Hash + object.each_with_object({}) do |(key, value), result| + result[yield(key)] = _deep_transform_keys_in_object(value, &block) + end + when Array + object.map {|e| _deep_transform_keys_in_object(e, &block) } + else + object + end + end + + def _deep_transform_keys_in_object!(object, &block) + case object + when Hash + object.keys.each do |key| + value = object.delete(key) + object[yield(key)] = _deep_transform_keys_in_object!(value, &block) + end + object + when Array + object.map! {|e| _deep_transform_keys_in_object!(e, &block)} + else + object + end + end end diff --git a/activesupport/lib/active_support/core_ext/hash/transform_values.rb b/activesupport/lib/active_support/core_ext/hash/transform_values.rb new file mode 100644 index 0000000000..e9bcce761f --- /dev/null +++ b/activesupport/lib/active_support/core_ext/hash/transform_values.rb @@ -0,0 +1,23 @@ +class Hash + # Returns a new hash with the results of running +block+ once for every value. + # The keys are unchanged. + # + # { a: 1, b: 2, c: 3 }.transform_values { |x| x * 2 } + # # => { a: 2, b: 4, c: 6 } + def transform_values + return enum_for(:transform_values) unless block_given? + result = self.class.new + each do |key, value| + result[key] = yield(value) + end + result + end + + # Destructive +transform_values+ + def transform_values! + return enum_for(:transform_values!) unless block_given? + each do |key, value| + self[key] = yield(value) + end + end +end diff --git a/activesupport/lib/active_support/core_ext/kernel/reporting.rb b/activesupport/lib/active_support/core_ext/kernel/reporting.rb index f3f8416905..80c531b694 100644 --- a/activesupport/lib/active_support/core_ext/kernel/reporting.rb +++ b/activesupport/lib/active_support/core_ext/kernel/reporting.rb @@ -31,9 +31,13 @@ module Kernel # For compatibility def silence_stderr #:nodoc: + ActiveSupport::Deprecation.warn( + "#silence_stderr is deprecated and will be removed in the next release" + ) #not thread-safe silence_stream(STDERR) { yield } end + # Deprecated : this method is not thread safe # Silences any stream for the duration of the block. # # silence_stream(STDOUT) do @@ -82,6 +86,9 @@ module Kernel # stream = capture(:stderr) { system('echo error 1>&2') } # stream # => "error\n" def capture(stream) + ActiveSupport::Deprecation.warn( + "#capture(stream) is deprecated and will be removed in the next release" + ) #not thread-safe stream = stream.to_s captured_stream = Tempfile.new(stream) stream_io = eval("$#{stream}") @@ -105,6 +112,9 @@ module Kernel # # This method is not thread-safe. def quietly + ActiveSupport::Deprecation.warn( + "#quietly is deprecated and will be removed in the next release" + ) #not thread-safe silence_stream(STDOUT) do silence_stream(STDERR) do yield diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index f855833a24..e926392952 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -170,38 +170,26 @@ class Module # methods still accept two arguments. definition = (method =~ /[^\]]=$/) ? 'arg' : '*args, &block' - # The following generated methods call the target exactly once, storing + # The following generated method calls the target exactly once, storing # the returned value in a dummy variable. # # Reason is twofold: On one hand doing less calls is in general better. # On the other hand it could be that the target has side-effects, # whereas conceptually, from the user point of view, the delegator should # be doing one call. - if allow_nil - method_def = [ - "def #{method_prefix}#{method}(#{definition})", # def customer_name(*args, &block) - "_ = #{to}", # _ = client - "if !_.nil? || nil.respond_to?(:#{method})", # if !_.nil? || nil.respond_to?(:name) - " _.#{method}(#{definition})", # _.name(*args, &block) - "end", # end - "end" # end - ].join ';' - else - exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}") - method_def = [ - "def #{method_prefix}#{method}(#{definition})", # def customer_name(*args, &block) - " _ = #{to}", # _ = client - " _.#{method}(#{definition})", # _.name(*args, &block) - "rescue NoMethodError => e", # rescue NoMethodError => e - " if _.nil? && e.name == :#{method}", # if _.nil? && e.name == :name - " #{exception}", # # add helpful message to the exception - " else", # else - " raise", # raise - " end", # end - "end" # end - ].join ';' - end + exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}") + + method_def = [ + "def #{method_prefix}#{method}(#{definition})", + " _ = #{to}", + " if !_.nil? || nil.respond_to?(:#{method})", + " _.#{method}(#{definition})", + " else", + " #{exception unless allow_nil}", + " end", + "end" + ].join ';' module_eval(method_def, file, line) end diff --git a/activesupport/lib/active_support/core_ext/numeric/time.rb b/activesupport/lib/active_support/core_ext/numeric/time.rb index 704c4248d9..689fae4830 100644 --- a/activesupport/lib/active_support/core_ext/numeric/time.rb +++ b/activesupport/lib/active_support/core_ext/numeric/time.rb @@ -61,25 +61,7 @@ class Numeric end alias :fortnight :fortnights - # Reads best without arguments: 10.minutes.ago - def ago(time = ::Time.current) - ActiveSupport::Deprecation.warn "Calling #ago or #until on a number (e.g. 5.ago) is deprecated and will be removed in the future, use 5.seconds.ago instead" - time - self - end - - # Reads best with argument: 10.minutes.until(time) - alias :until :ago - - # Reads best with argument: 10.minutes.since(time) - def since(time = ::Time.current) - ActiveSupport::Deprecation.warn "Calling #since or #from_now on a number (e.g. 5.since) is deprecated and will be removed in the future, use 5.seconds.since instead" - time + self - end - - # Reads best without arguments: 10.minutes.from_now - alias :from_now :since - - # Used with the standard time durations, like 1.hour.in_milliseconds -- + # 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 diff --git a/activesupport/lib/active_support/core_ext/object/duplicable.rb b/activesupport/lib/active_support/core_ext/object/duplicable.rb index 3d2c809c9f..c5d59128e5 100644 --- a/activesupport/lib/active_support/core_ext/object/duplicable.rb +++ b/activesupport/lib/active_support/core_ext/object/duplicable.rb @@ -19,7 +19,7 @@ class Object # Can you safely dup this object? # - # False for +nil+, +false+, +true+, symbol, and number objects; + # False for +nil+, +false+, +true+, symbol, number and BigDecimal(in 1.9.x) objects; # true otherwise. def duplicable? true diff --git a/activesupport/lib/active_support/core_ext/object/to_json.rb b/activesupport/lib/active_support/core_ext/object/to_json.rb deleted file mode 100644 index f58364f9c6..0000000000 --- a/activesupport/lib/active_support/core_ext/object/to_json.rb +++ /dev/null @@ -1,5 +0,0 @@ -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.' - -require 'active_support/core_ext/object/json'
\ No newline at end of file diff --git a/activesupport/lib/active_support/core_ext/object/to_param.rb b/activesupport/lib/active_support/core_ext/object/to_param.rb index 13be0038c2..e65fc5bac1 100644 --- a/activesupport/lib/active_support/core_ext/object/to_param.rb +++ b/activesupport/lib/active_support/core_ext/object/to_param.rb @@ -51,12 +51,10 @@ class Hash # # This method is also aliased as +to_query+. def to_param(namespace = nil) - if empty? - namespace ? nil.to_query(namespace) : '' - else - collect do |key, value| + collect do |key, value| + unless (value.is_a?(Hash) || value.is_a?(Array)) && value.empty? value.to_query(namespace ? "#{namespace}[#{key}]" : key) - end.sort! * '&' - end + end + end.compact.sort! * '&' end end diff --git a/activesupport/lib/active_support/core_ext/object/to_query.rb b/activesupport/lib/active_support/core_ext/object/to_query.rb index 37352fa608..172f06ed64 100644 --- a/activesupport/lib/active_support/core_ext/object/to_query.rb +++ b/activesupport/lib/active_support/core_ext/object/to_query.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/object/to_param' +require 'cgi' class Object # Converts an object into a string suitable for use as a URL query string, using the given <tt>key</tt> as the @@ -6,7 +7,6 @@ class Object # # Note: This method is defined as a default implementation for all Objects for Hash#to_query to work. def to_query(key) - require 'cgi' unless defined?(CGI) && defined?(CGI::escape) "#{CGI.escape(key.to_param)}=#{CGI.escape(to_param.to_s)}" end end diff --git a/activesupport/lib/active_support/core_ext/securerandom.rb b/activesupport/lib/active_support/core_ext/securerandom.rb deleted file mode 100644 index fec8f7c0ec..0000000000 --- a/activesupport/lib/active_support/core_ext/securerandom.rb +++ /dev/null @@ -1,47 +0,0 @@ -module SecureRandom - UUID_DNS_NAMESPACE = "k\xA7\xB8\x10\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc: - UUID_URL_NAMESPACE = "k\xA7\xB8\x11\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc: - UUID_OID_NAMESPACE = "k\xA7\xB8\x12\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc: - UUID_X500_NAMESPACE = "k\xA7\xB8\x14\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc: - - # Generates a v5 non-random UUID (Universally Unique IDentifier). - # - # Using Digest::MD5 generates version 3 UUIDs; Digest::SHA1 generates version 5 UUIDs. - # ::uuid_from_hash always generates the same UUID for a given name and namespace combination. - # - # See RFC 4122 for details of UUID at: http://www.ietf.org/rfc/rfc4122.txt - def self.uuid_from_hash(hash_class, uuid_namespace, name) - if hash_class == Digest::MD5 - version = 3 - elsif hash_class == Digest::SHA1 - version = 5 - else - raise ArgumentError, "Expected Digest::SHA1 or Digest::MD5, got #{hash_class.name}." - end - - hash = hash_class.new - hash.update(uuid_namespace) - hash.update(name) - - ary = hash.digest.unpack('NnnnnN') - ary[2] = (ary[2] & 0x0FFF) | (version << 12) - ary[3] = (ary[3] & 0x3FFF) | 0x8000 - - "%08x-%04x-%04x-%04x-%04x%08x" % ary - end - - # Convenience method for ::uuid_from_hash using Digest::MD5. - def self.uuid_v3(uuid_namespace, name) - self.uuid_from_hash(Digest::MD5, uuid_namespace, name) - end - - # Convenience method for ::uuid_from_hash using Digest::SHA1. - def self.uuid_v5(uuid_namespace, name) - self.uuid_from_hash(Digest::SHA1, uuid_namespace, name) - end - - class << self - # Alias for ::uuid. - alias_method :uuid_v4, :uuid - end -end diff --git a/activesupport/lib/active_support/core_ext/string/filters.rb b/activesupport/lib/active_support/core_ext/string/filters.rb index 49c0df6026..1dfaf76673 100644 --- a/activesupport/lib/active_support/core_ext/string/filters.rb +++ b/activesupport/lib/active_support/core_ext/string/filters.rb @@ -62,4 +62,28 @@ class String "#{self[0, stop]}#{omission}" end + + # Truncates a given +text+ after a given number of words (<tt>words_count</tt>): + # + # 'Once upon a time in a world far far away'.truncate_words(4) + # # => "Once upon a time..." + # + # Pass a string or regexp <tt>:separator</tt> to specify a different separator of words: + # + # 'Once<br>upon<br>a<br>time<br>in<br>a<br>world'.truncate_words(5, separator: '<br>') + # # => "Once<br>upon<br>a<br>time<br>in..." + # + # The last characters will be replaced with the <tt>:omission</tt> string (defaults to "..."): + # + # 'And they found that many people were sleeping better.'.truncate_words(5, omission: '... (continued)') + # # => "And they found that many... (continued)" + def truncate_words(words_count, options = {}) + sep = options[:separator] || /\s+/ + sep = Regexp.escape(sep.to_s) unless Regexp === sep + if self =~ /\A((?:.+?#{sep}){#{words_count - 1}}.+?)#{sep}.*/m + $1 + (options[:omission] || '...') + else + dup + end + end end diff --git a/activesupport/lib/active_support/core_ext/string/output_safety.rb b/activesupport/lib/active_support/core_ext/string/output_safety.rb index 2c8995be9a..c761325108 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -7,7 +7,7 @@ class ERB HTML_ESCAPE = { '&' => '&', '>' => '>', '<' => '<', '"' => '"', "'" => ''' } JSON_ESCAPE = { '&' => '\u0026', '>' => '\u003e', '<' => '\u003c', "\u2028" => '\u2028', "\u2029" => '\u2029' } HTML_ESCAPE_REGEXP = /[&"'><]/ - HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/ + HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+)|(#[xX][\dA-Fa-f]+));)/ JSON_ESCAPE_REGEXP = /[\u2028\u2029&><]/u # A utility method for escaping HTML tag characters. @@ -19,12 +19,7 @@ class ERB # puts html_escape('is a > 0 & a < 10?') # # => is a > 0 & a < 10? def html_escape(s) - s = s.to_s - if s.html_safe? - s - else - s.gsub(HTML_ESCAPE_REGEXP, HTML_ESCAPE).html_safe - end + unwrapped_html_escape(s).html_safe end # Aliasing twice issues a warning "discarding old...". Remove first to avoid it. @@ -36,6 +31,18 @@ class ERB singleton_class.send(:remove_method, :html_escape) module_function :html_escape + # HTML escapes strings but doesn't wrap them with an ActiveSupport::SafeBuffer. + # This method is not for public consumption! Seriously! + def unwrapped_html_escape(s) # :nodoc: + s = s.to_s + if s.html_safe? + s + else + s.gsub(HTML_ESCAPE_REGEXP, HTML_ESCAPE) + end + end + module_function :unwrapped_html_escape + # A utility method for escaping HTML without affecting existing escaped entities. # # html_escape_once('1 < 2 & 3') @@ -170,13 +177,15 @@ module ActiveSupport #:nodoc: self[0, 0] end - %w[concat prepend].each do |method_name| - define_method method_name do |value| - super(html_escape_interpolated_argument(value)) - end + def concat(value) + super(html_escape_interpolated_argument(value)) end alias << concat + def prepend(value) + super(html_escape_interpolated_argument(value)) + end + def prepend!(value) ActiveSupport::Deprecation.deprecation_warning "ActiveSupport::SafeBuffer#prepend!", :prepend prepend value @@ -231,7 +240,8 @@ module ActiveSupport #:nodoc: private def html_escape_interpolated_argument(arg) - (!html_safe? || arg.html_safe?) ? arg : ERB::Util.h(arg) + (!html_safe? || arg.html_safe?) ? arg : + arg.to_s.gsub(ERB::Util::HTML_ESCAPE_REGEXP, ERB::Util::HTML_ESCAPE) end end end diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index 59675d744e..93a11d4586 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -180,13 +180,14 @@ module ActiveSupport #:nodoc: Dependencies.load_missing_constant(from_mod, const_name) end - # Dependencies assumes the name of the module reflects the nesting (unless - # it can be proven that is not the case), and the path to the file that - # defines the constant. Anonymous modules cannot follow these conventions - # and we assume therefore the user wants to refer to a top-level constant. + # We assume that the name of the module reflects the nesting + # (unless it can be proven that is not the case) and the path to the file + # that defines the constant. Anonymous modules cannot follow these + # conventions and therefore we assume that the user wants to refer to a + # top-level constant. def guess_for_anonymous(const_name) if Object.const_defined?(const_name) - raise NameError, "#{const_name} cannot be autoloaded from an anonymous class or module" + raise NameError.new "#{const_name} cannot be autoloaded from an anonymous class or module", const_name else Object end @@ -515,9 +516,9 @@ module ActiveSupport #:nodoc: end end - raise NameError, - "uninitialized constant #{qualified_name}", - caller.reject { |l| l.starts_with? __FILE__ } + name_error = NameError.new("uninitialized constant #{qualified_name}", const_name) + name_error.set_backtrace(caller.reject {|l| l.starts_with? __FILE__ }) + raise name_error end # Remove the constants that have been autoloaded, and those that have been diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb index 09eb732ef7..0ae641d05b 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -105,8 +105,7 @@ module ActiveSupport # We define it as a workaround to Ruby 2.0.0-p353 bug. # For more information, check rails/rails#13055. - # It should be dropped once a new Ruby patch-level - # release after 2.0.0-p353 happens. + # Remove it when we drop support for 2.0.0-p353. def ===(other) #:nodoc: value === other end diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index a4ebdea598..dc76a77a6c 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -57,7 +57,7 @@ module ActiveSupport def initialize(constructor = {}) if constructor.respond_to?(:to_hash) super() - update(constructor.to_hash) + update(constructor) else super(constructor) end @@ -75,6 +75,7 @@ module ActiveSupport hash = hash.to_hash new(hash).tap do |new_hash| new_hash.default = hash.default + new_hash.default_proc = hash.default_proc if hash.default_proc end end @@ -176,7 +177,14 @@ module ActiveSupport indices.collect { |key| self[convert_key(key)] } end - # Returns an exact copy of the hash. + # Returns a shallow copy of the hash. + # + # hash = ActiveSupport::HashWithIndifferentAccess.new({ a: { b: 'b' } }) + # dup = hash.dup + # dup[:a][:c] = 'c' + # + # hash[:a][:c] # => nil + # dup[:a][:c] # => "c" def dup self.class.new(self).tap do |new_hash| new_hash.default = default diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb index 23cd6716e3..affcfb7398 100644 --- a/activesupport/lib/active_support/i18n_railtie.rb +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -8,8 +8,6 @@ module I18n config.i18n.railties_load_path = [] config.i18n.load_path = [] config.i18n.fallbacks = ActiveSupport::OrderedOptions.new - # Enforce I18n to check the available locales when setting a locale. - config.i18n.enforce_available_locales = true # Set the i18n configuration after initialization since a lot of # configuration is still usually done in application initializers. @@ -36,7 +34,7 @@ module I18n # Avoid issues with setting the default_locale by disabling available locales # check while configuring. enforce_available_locales = app.config.i18n.delete(:enforce_available_locales) - enforce_available_locales = I18n.enforce_available_locales unless I18n.enforce_available_locales.nil? + enforce_available_locales = I18n.enforce_available_locales if enforce_available_locales.nil? I18n.enforce_available_locales = false app.config.i18n.each do |setting, value| diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb index eda0edff28..97401ccec7 100644 --- a/activesupport/lib/active_support/inflector/inflections.rb +++ b/activesupport/lib/active_support/inflector/inflections.rb @@ -160,7 +160,7 @@ module ActiveSupport # uncountable 'money', 'information' # uncountable %w( money information rice ) def uncountable(*words) - (@uncountables << words).flatten! + @uncountables += words.flatten.map(&:downcase) end # Specifies a humanized form of a string by a regular expression rule or diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index 69f77453e7..51720d0192 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -99,26 +99,46 @@ module ActiveSupport word end - # Capitalizes the first word, turns underscores into spaces, and strips a - # trailing '_id' if present. - # Like +titleize+, this is meant for creating pretty output. + # Tweaks an attribute name for display to end users. + # + # Specifically, +humanize+ performs these transformations: + # + # * Applies human inflection rules to the argument. + # * Deletes leading underscores, if any. + # * Removes a "_id" suffix if present. + # * Replaces underscores with spaces, if any. + # * Downcases all words except acronyms. + # * Capitalizes the first word. # # The capitalization of the first word can be turned off by setting the - # optional parameter +capitalize+ to false. - # By default, this parameter is true. + # +:capitalize+ option to false (default is true). # # humanize('employee_salary') # => "Employee salary" # humanize('author_id') # => "Author" # humanize('author_id', capitalize: false) # => "author" + # humanize('_id') # => "Id" + # + # If "SSL" was defined to be an acronym: + # + # humanize('ssl_error') # => "SSL error" + # 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.sub!(/\A_+/, '') + result.sub!(/_id\z/, '') result.tr!('_', ' ') - result.gsub!(/([a-z\d]*)/i) { |match| + + result.gsub!(/([a-z\d]*)/i) do |match| "#{inflections.acronyms[match] || match.downcase}" - } - result.gsub!(/^\w/) { |match| match.upcase } if options.fetch(:capitalize, true) + end + + if options.fetch(:capitalize, true) + result.sub!(/\A\w/) { |match| match.upcase } + end + result end diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb index 7a96c66626..325a3d75dc 100644 --- a/activesupport/lib/active_support/notifications.rb +++ b/activesupport/lib/active_support/notifications.rb @@ -141,6 +141,11 @@ module ActiveSupport # # ActiveSupport::Notifications.unsubscribe(subscriber) # + # You can also unsubscribe by passing the name of the subscriber object. Note + # that this will unsubscribe all subscriptions with the given name: + # + # ActiveSupport::Notifications.unsubscribe("render") + # # == Default Queue # # Notifications ships with a queue implementation that consumes and publishes events @@ -173,8 +178,8 @@ module ActiveSupport unsubscribe(subscriber) end - def unsubscribe(args) - notifier.unsubscribe(args) + def unsubscribe(subscriber_or_name) + notifier.unsubscribe(subscriber_or_name) end def instrumenter diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb index 8f5fa646e8..6bf8c7d5de 100644 --- a/activesupport/lib/active_support/notifications/fanout.rb +++ b/activesupport/lib/active_support/notifications/fanout.rb @@ -25,9 +25,15 @@ module ActiveSupport subscriber end - def unsubscribe(subscriber) + def unsubscribe(subscriber_or_name) synchronize do - @subscribers.reject! { |s| s.matches?(subscriber) } + case subscriber_or_name + when String + @subscribers.reject! { |s| s.matches?(subscriber_or_name) } + else + @subscribers.delete(subscriber_or_name) + end + @listeners_for.clear end end @@ -97,12 +103,11 @@ module ActiveSupport end def subscribed_to?(name) - @pattern === name.to_s + @pattern === name end - def matches?(subscriber_or_name) - self === subscriber_or_name || - @pattern && @pattern === subscriber_or_name + def matches?(name) + @pattern && @pattern === name end end diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb index b169e3af01..5ecda9593a 100644 --- a/activesupport/lib/active_support/number_helper.rb +++ b/activesupport/lib/active_support/number_helper.rb @@ -232,12 +232,8 @@ module ActiveSupport # number_to_human_size(1234567, precision: 2) # => 1.2 MB # number_to_human_size(483989, precision: 2) # => 470 KB # number_to_human_size(1234567, precision: 2, separator: ',') # => 1,2 MB - # - # Non-significant zeros after the fractional separator are stripped out by - # default (set <tt>:strip_insignificant_zeros</tt> to +false+ to change that): - # - # number_to_human_size(1234567890123, precision: 5) # => "1.1229 TB" - # number_to_human_size(524288000, precision: 5) # => "500 MB" + # number_to_human_size(1234567890123, precision: 5) # => "1.1228 TB" + # number_to_human_size(524288000, precision: 5) # => "500 MB" def number_to_human_size(number, options = {}) NumberToHumanSizeConverter.convert(number, options) end @@ -305,12 +301,15 @@ module ActiveSupport # separator: ',', # significant: false) # => "1,2 Million" # + # number_to_human(500000000, precision: 5) # => "500 Million" + # number_to_human(12345012345, significant: false) # => "12.345 Billion" + # # Non-significant zeros after the decimal separator are stripped # out by default (set <tt>:strip_insignificant_zeros</tt> to # +false+ to change that): # - # number_to_human(12345012345, significant_digits: 6) # => "12.345 Billion" - # number_to_human(500000000, precision: 5) # => "500 Million" + # number_to_human(12.00001) # => "12" + # number_to_human(12.00001, strip_insignificant_zeros: false) # => "12.0" # # ==== Custom Unit Quantifiers # diff --git a/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb index 6405afc9a6..d85cc086d7 100644 --- a/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb @@ -13,7 +13,9 @@ module ActiveSupport def parts left, right = number.to_s.split('.') - left.gsub!(DELIMITED_REGEX) { "#{$1}#{options[:delimiter]}" } + left.gsub!(DELIMITED_REGEX) do |digit_to_delimit| + "#{digit_to_delimit}#{options[:delimiter]}" + end [left, right].compact end end diff --git a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb index c45f6cdcfa..01597b288a 100644 --- a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb @@ -12,11 +12,7 @@ module ActiveSupport when Float, String @number = BigDecimal(number.to_s) when Rational - if significant - @number = BigDecimal(number, digit_count(number.to_i) + precision) - else - @number = BigDecimal(number, precision) - end + @number = BigDecimal(number, digit_count(number.to_i) + precision) else @number = number.to_d end diff --git a/activesupport/lib/active_support/subscriber.rb b/activesupport/lib/active_support/subscriber.rb index 4b9b48539f..98be78b41b 100644 --- a/activesupport/lib/active_support/subscriber.rb +++ b/activesupport/lib/active_support/subscriber.rb @@ -64,12 +64,21 @@ module ActiveSupport def add_event_subscriber(event) return if %w{ start finish }.include?(event.to_s) - notifier.subscribe("#{event}.#{namespace}", subscriber) + pattern = "#{event}.#{namespace}" + + # don't add multiple subscribers (eg. if methods are redefined) + return if subscriber.patterns.include?(pattern) + + subscriber.patterns << pattern + notifier.subscribe(pattern, subscriber) end end + attr_reader :patterns # :nodoc: + def initialize @queue_key = [self.class.name, object_id].join "-" + @patterns = [] super end diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb index 2fb5c04316..751fdaef78 100644 --- a/activesupport/lib/active_support/test_case.rb +++ b/activesupport/lib/active_support/test_case.rb @@ -1,5 +1,3 @@ -gem 'minitest' # make sure we get the gem, not stdlib -require 'minitest' require 'active_support/testing/tagged_logging' require 'active_support/testing/setup_and_teardown' require 'active_support/testing/assertions' @@ -11,11 +9,6 @@ require 'active_support/testing/time_helpers' require 'active_support/core_ext/kernel/reporting' require 'active_support/deprecation' -begin - silence_warnings { require 'mocha/setup' } -rescue LoadError -end - module ActiveSupport class TestCase < ::Minitest::Test Assertion = Minitest::Assertion diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb index 76a591bc3b..11cca82995 100644 --- a/activesupport/lib/active_support/testing/assertions.rb +++ b/activesupport/lib/active_support/testing/assertions.rb @@ -9,7 +9,7 @@ module ActiveSupport # # assert_not nil # => true # assert_not false # => true - # assert_not 'foo' # => 'foo' is not nil or false + # assert_not 'foo' # => Expected "foo" to be nil or false # # An error message can be specified. # diff --git a/activesupport/lib/active_support/testing/declarative.rb b/activesupport/lib/active_support/testing/declarative.rb index e709e6edf5..0bf3643a56 100644 --- a/activesupport/lib/active_support/testing/declarative.rb +++ b/activesupport/lib/active_support/testing/declarative.rb @@ -1,30 +1,6 @@ module ActiveSupport module Testing module Declarative - - def self.extended(klass) #:nodoc: - klass.class_eval do - - unless method_defined?(:describe) - def self.describe(text) - if block_given? - super - else - message = "`describe` without a block is deprecated, please switch to: `def self.name; #{text.inspect}; end`\n" - ActiveSupport::Deprecation.warn message - - class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 - def self.name - "#{text}" - end - RUBY_EVAL - end - end - end - - end - end - unless defined?(Spec) # Helper to define a test method using a String. Under the hood, it replaces # spaces with underscores and defines the test method. diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb index 908af176be..68bda35980 100644 --- a/activesupport/lib/active_support/testing/isolation.rb +++ b/activesupport/lib/active_support/testing/isolation.rb @@ -70,14 +70,24 @@ module ActiveSupport exit! else Tempfile.open("isolation") do |tmpfile| - ENV["ISOLATION_TEST"] = self.class.name - ENV["ISOLATION_OUTPUT"] = tmpfile.path + env = { + ISOLATION_TEST: self.class.name, + ISOLATION_OUTPUT: tmpfile.path + } load_paths = $-I.map {|p| "-I\"#{File.expand_path(p)}\"" }.join(" ") - `#{Gem.ruby} #{load_paths} #{$0} #{ORIG_ARGV.join(" ")}` - - ENV.delete("ISOLATION_TEST") - ENV.delete("ISOLATION_OUTPUT") + orig_args = ORIG_ARGV.join(" ") + test_opts = "-n#{self.class.name}##{self.name}" + command = "#{Gem.ruby} #{load_paths} #{$0} #{orig_args} #{test_opts}" + + # IO.popen lets us pass env in a cross-platform way + child = IO.popen([env, command]) + + begin + Process.wait(child.pid) + rescue Errno::ECHILD # The child process may exit before we wait + nil + end return tmpfile.read.unpack("m")[0] end diff --git a/activesupport/lib/active_support/testing/tagged_logging.rb b/activesupport/lib/active_support/testing/tagged_logging.rb index f4cee64091..843ce4a867 100644 --- a/activesupport/lib/active_support/testing/tagged_logging.rb +++ b/activesupport/lib/active_support/testing/tagged_logging.rb @@ -6,7 +6,7 @@ module ActiveSupport attr_writer :tagged_logger def before_setup - if tagged_logger + if tagged_logger && tagged_logger.info? heading = "#{self.class}: #{name}" divider = '-' * heading.size tagged_logger.info divider diff --git a/activesupport/lib/active_support/time.rb b/activesupport/lib/active_support/time.rb index 92a593965e..ea2d3391bd 100644 --- a/activesupport/lib/active_support/time.rb +++ b/activesupport/lib/active_support/time.rb @@ -1,5 +1,3 @@ -require 'active_support' - module ActiveSupport autoload :Duration, 'active_support/duration' autoload :TimeWithZone, 'active_support/time_with_zone' diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index c25c97cfa8..4a0ed356b1 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -185,8 +185,11 @@ module ActiveSupport end alias_method :rfc822, :rfc2822 - # <tt>:db</tt> format outputs time in UTC; all others output time in local. - # Uses TimeWithZone's +strftime+, so <tt>%Z</tt> and <tt>%z</tt> work correctly. + # Returns a string of the object's date and time. + # Accepts an optional <tt>format</tt>: + # * <tt>:default</tt> - default value, mimics Ruby 1.9 Time#to_s format. + # * <tt>:db</tt> - format outputs time in UTC :db time. See Time#to_formatted_s(:db). + # * Any key in <tt>Time::DATE_FORMATS</tt> can be used. See active_support/core_ext/time/conversions.rb. def to_s(format = :default) if format == :db utc.to_s(format) @@ -259,7 +262,7 @@ module ActiveSupport # If we're subtracting a Duration of variable length (i.e., years, months, days), move backwards from #time, # otherwise move backwards #utc, for accuracy when moving across DST boundaries if other.acts_like?(:time) - utc.to_f - other.to_f + to_time - other.to_time elsif duration_of_variable_length?(other) method_missing(:-, other) else @@ -350,6 +353,14 @@ module ActiveSupport initialize(variables[0].utc, ::Time.find_zone(variables[1]), variables[2].utc) end + # respond_to_missing? is not called in some cases, such as when type conversion is + # performed with Kernel#String + def respond_to?(sym, include_priv = false) + # ensure that we're not going to throw and rescue from NoMethodError in method_missing which is slow + return false if sym.to_sym == :to_str + super + end + # Ensure proxy class responds to all methods that underlying time instance # responds to. def respond_to_missing?(sym, include_priv) diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index 38f0d268f4..ee62523824 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -188,16 +188,72 @@ module ActiveSupport @lazy_zones_map = ThreadSafe::Cache.new - # Assumes self represents an offset from UTC in seconds (as returned from - # Time#utc_offset) and turns this into an +HH:MM formatted string. - # - # TimeZone.seconds_to_utc_offset(-21_600) # => "-06:00" - def self.seconds_to_utc_offset(seconds, colon = true) - format = colon ? UTC_OFFSET_WITH_COLON : UTC_OFFSET_WITHOUT_COLON - sign = (seconds < 0 ? '-' : '+') - hours = seconds.abs / 3600 - minutes = (seconds.abs % 3600) / 60 - format % [sign, hours, minutes] + class << self + # Assumes self represents an offset from UTC in seconds (as returned from + # Time#utc_offset) and turns this into an +HH:MM formatted string. + # + # TimeZone.seconds_to_utc_offset(-21_600) # => "-06:00" + def seconds_to_utc_offset(seconds, colon = true) + format = colon ? UTC_OFFSET_WITH_COLON : UTC_OFFSET_WITHOUT_COLON + sign = (seconds < 0 ? '-' : '+') + hours = seconds.abs / 3600 + minutes = (seconds.abs % 3600) / 60 + format % [sign, hours, minutes] + end + + def find_tzinfo(name) + TZInfo::TimezoneProxy.new(MAPPING[name] || name) + end + + alias_method :create, :new + + # 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 + + # 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 + @zones ||= zones_map.values.sort + end + + def zones_map + @zones_map ||= begin + MAPPING.each_key {|place| self[place]} # load all the zones + @lazy_zones_map + end + end + + # Locate a specific time zone object. If the argument is a string, it + # is interpreted to mean the name of the timezone to locate. If it is a + # numeric value it is either the hour offset, or the second offset, of the + # timezone to find. (The first one with that offset will be returned.) + # Returns +nil+ if no such time zone is known to the system. + def [](arg) + case arg + when String + begin + @lazy_zones_map[arg] ||= create(arg).tap { |tz| tz.utc_offset } + rescue TZInfo::InvalidTimezoneIdentifier + nil + end + when Numeric, ActiveSupport::Duration + arg *= 3600 if arg.abs <= 13 + all.find { |z| z.utc_offset == arg.to_i } + else + raise ArgumentError, "invalid argument to TimeZone[]: #{arg.inspect}" + end + end + + # A convenience method for returning a collection of TimeZone objects + # for time zones in the USA. + def us_zones + @us_zones ||= all.find_all { |z| z.name =~ /US|Arizona|Indiana|Hawaii|Alaska/ } + end end include Comparable @@ -282,6 +338,11 @@ module ActiveSupport # # Time.zone.now # => Fri, 31 Dec 1999 14:00:00 HST -10:00 # Time.zone.parse('22:30:00') # => Fri, 31 Dec 1999 22:30:00 HST -10:00 + # + # However, if the date component is not provided, but any other upper + # components are supplied, then the day of the month defaults to 1: + # + # Time.zone.parse('Mar 2000') # => Wed, 01 Mar 2000 00:00:00 HST -10:00 def parse(str, now=now()) parts = Date._parse(str, false) return if parts.empty? @@ -289,7 +350,7 @@ module ActiveSupport time = Time.new( parts.fetch(:year, now.year), parts.fetch(:mon, now.month), - parts.fetch(:mday, now.day), + parts.fetch(:mday, parts[:year] || parts[:mon] ? 1 : now.day), parts.fetch(:hour, 0), parts.fetch(:min, 0), parts.fetch(:sec, 0) + parts.fetch(:sec_fraction, 0), @@ -356,66 +417,9 @@ module ActiveSupport tzinfo.periods_for_local(time) end - def self.find_tzinfo(name) - TZInfo::TimezoneProxy.new(MAPPING[name] || name) - end - - class << self - alias_method :create, :new - - # 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 - - # 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 - @zones ||= zones_map.values.sort - end - - def zones_map - @zones_map ||= begin - MAPPING.each_key {|place| self[place]} # load all the zones - @lazy_zones_map - end - end - - # Locate a specific time zone object. If the argument is a string, it - # is interpreted to mean the name of the timezone to locate. If it is a - # numeric value it is either the hour offset, or the second offset, of the - # timezone to find. (The first one with that offset will be returned.) - # Returns +nil+ if no such time zone is known to the system. - def [](arg) - case arg - when String - begin - @lazy_zones_map[arg] ||= create(arg).tap { |tz| tz.utc_offset } - rescue TZInfo::InvalidTimezoneIdentifier - nil - end - when Numeric, ActiveSupport::Duration - arg *= 3600 if arg.abs <= 13 - all.find { |z| z.utc_offset == arg.to_i } - else - raise ArgumentError, "invalid argument to TimeZone[]: #{arg.inspect}" - end - end - - # A convenience method for returning a collection of TimeZone objects - # for time zones in the USA. - def us_zones - @us_zones ||= all.find_all { |z| z.name =~ /US|Arizona|Indiana|Hawaii|Alaska/ } - end - end - private - - def time_now - Time.now - end + def time_now + Time.now + end end end diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb index 0b393e0c7a..7ffcae6007 100644 --- a/activesupport/test/abstract_unit.rb +++ b/activesupport/test/abstract_unit.rb @@ -36,3 +36,5 @@ end def jruby_skip(message = '') skip message if defined?(JRUBY_VERSION) end + +require 'mocha/setup' # FIXME: stop using mocha diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index 18923f61d1..8287e62f4c 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -60,36 +60,25 @@ class CacheKeyTest < ActiveSupport::TestCase end def test_expand_cache_key_with_rails_cache_id - begin - ENV['RAILS_CACHE_ID'] = 'c99' + with_env('RAILS_CACHE_ID' => 'c99') do assert_equal 'c99/foo', ActiveSupport::Cache.expand_cache_key(:foo) assert_equal 'c99/foo', ActiveSupport::Cache.expand_cache_key([:foo]) assert_equal 'c99/foo/bar', ActiveSupport::Cache.expand_cache_key([:foo, :bar]) assert_equal 'nm/c99/foo', ActiveSupport::Cache.expand_cache_key(:foo, :nm) assert_equal 'nm/c99/foo', ActiveSupport::Cache.expand_cache_key([:foo], :nm) assert_equal 'nm/c99/foo/bar', ActiveSupport::Cache.expand_cache_key([:foo, :bar], :nm) - ensure - ENV['RAILS_CACHE_ID'] = nil end end def test_expand_cache_key_with_rails_app_version - begin - ENV['RAILS_APP_VERSION'] = 'rails3' + with_env('RAILS_APP_VERSION' => 'rails3') do assert_equal 'rails3/foo', ActiveSupport::Cache.expand_cache_key(:foo) - ensure - ENV['RAILS_APP_VERSION'] = nil end end def test_expand_cache_key_rails_cache_id_should_win_over_rails_app_version - begin - ENV['RAILS_CACHE_ID'] = 'c99' - ENV['RAILS_APP_VERSION'] = 'rails3' + with_env('RAILS_CACHE_ID' => 'c99', 'RAILS_APP_VERSION' => 'rails3') do assert_equal 'c99/foo', ActiveSupport::Cache.expand_cache_key(:foo) - ensure - ENV['RAILS_CACHE_ID'] = nil - ENV['RAILS_APP_VERSION'] = nil end end @@ -124,6 +113,16 @@ class CacheKeyTest < ActiveSupport::TestCase def test_expand_cache_key_of_array_like_object assert_equal 'foo/bar/baz', ActiveSupport::Cache.expand_cache_key(%w{foo bar baz}.to_enum) end + + private + + def with_env(kv) + old_values = {} + kv.each { |key, value| old_values[key], ENV[key] = ENV[key], value } + yield + ensure + old_values.each { |key, value| ENV[key] = value} + end end class CacheStoreSettingTest < ActiveSupport::TestCase @@ -692,6 +691,11 @@ class FileStoreTest < ActiveSupport::TestCase assert File.exist?(filepath) end + def test_long_keys + @cache.write("a"*10000, 1) + assert_equal 1, @cache.read("a"*10000) + end + def test_key_transformation key = @cache.send(:key_file_path, "views/index?id=1") assert_equal "views/index?id=1", @cache.send(:file_path_key, key) diff --git a/activesupport/test/clean_backtrace_test.rb b/activesupport/test/clean_backtrace_test.rb index dd67a45cf6..05580352a9 100644 --- a/activesupport/test/clean_backtrace_test.rb +++ b/activesupport/test/clean_backtrace_test.rb @@ -34,6 +34,11 @@ class BacktraceCleanerSilencerTest < ActiveSupport::TestCase [ "/other/class.rb" ], @bc.clean([ "/mongrel/class.rb", "/other/class.rb", "/mongrel/stuff.rb" ]) end + + test "backtrace cleaner should allow removing silencer" do + @bc.remove_silencers! + assert_equal ["/mongrel/stuff.rb"], @bc.clean(["/mongrel/stuff.rb"]) + end end class BacktraceCleanerMultipleSilencersTest < ActiveSupport::TestCase diff --git a/activesupport/test/constantize_test_cases.rb b/activesupport/test/constantize_test_cases.rb index bbeb710a0c..8a9fd4996b 100644 --- a/activesupport/test/constantize_test_cases.rb +++ b/activesupport/test/constantize_test_cases.rb @@ -27,13 +27,24 @@ module ConstantizeTestCases assert_equal Ace::Base::Case, yield("Ace::Base::Case") assert_equal Ace::Base::Case, yield("::Ace::Base::Case") assert_equal Ace::Base::Case::Dice, yield("Ace::Base::Case::Dice") + assert_equal Ace::Base::Case::Dice, yield("Ace::Base::Fase::Dice") assert_equal Ace::Base::Fase::Dice, yield("Ace::Base::Fase::Dice") + assert_equal Ace::Gas::Case, yield("Ace::Gas::Case") assert_equal Ace::Gas::Case::Dice, yield("Ace::Gas::Case::Dice") + assert_equal Ace::Base::Case::Dice, yield("Ace::Gas::Case::Dice") + assert_equal Case::Dice, yield("Case::Dice") + assert_equal AddtlGlobalConstants::Case::Dice, yield("Case::Dice") + assert_equal Object::AddtlGlobalConstants::Case::Dice, yield("Case::Dice") + assert_equal Case::Dice, yield("Object::Case::Dice") + assert_equal AddtlGlobalConstants::Case::Dice, yield("Object::Case::Dice") + assert_equal Object::AddtlGlobalConstants::Case::Dice, yield("Case::Dice") + assert_equal ConstantizeTestCases, yield("ConstantizeTestCases") assert_equal ConstantizeTestCases, yield("::ConstantizeTestCases") + assert_raises(NameError) { yield("UnknownClass") } assert_raises(NameError) { yield("UnknownClass::Ace") } assert_raises(NameError) { yield("UnknownClass::Ace::Base") } diff --git a/activesupport/test/core_ext/array_ext_test.rb b/activesupport/test/core_ext/array_ext_test.rb index 57722fd52a..bd1b818717 100644 --- a/activesupport/test/core_ext/array_ext_test.rb +++ b/activesupport/test/core_ext/array_ext_test.rb @@ -1,22 +1,25 @@ require 'abstract_unit' require 'active_support/core_ext/array' require 'active_support/core_ext/big_decimal' +require 'active_support/core_ext/hash' require 'active_support/core_ext/object/conversions' - -require 'active_support/core_ext' # FIXME: pulling in all to_xml extensions -require 'active_support/hash_with_indifferent_access' +require 'active_support/core_ext/string' class ArrayExtAccessTests < ActiveSupport::TestCase def test_from assert_equal %w( a b c d ), %w( a b c d ).from(0) assert_equal %w( c d ), %w( a b c d ).from(2) assert_equal %w(), %w( a b c d ).from(10) + assert_equal %w( d e ), %w( a b c d e ).from(-2) + assert_equal %w(), %w( a b c d e ).from(-10) end def test_to assert_equal %w( a ), %w( a b c d ).to(0) assert_equal %w( a b c ), %w( a b c d ).to(2) assert_equal %w( a b c d ), %w( a b c d ).to(10) + assert_equal %w( a b c ), %w( a b c d ).to(-2) + assert_equal %w(), %w( a b c ).to(-10) end def test_second_through_tenth @@ -234,7 +237,7 @@ class ArraySplitTests < ActiveSupport::TestCase end class ArrayToXmlTests < ActiveSupport::TestCase - def test_to_xml + def test_to_xml_with_hash_elements xml = [ { :name => "David", :age => 26, :age_in_millis => 820497600000 }, { :name => "Jason", :age => 31, :age_in_millis => BigDecimal.new('1.0') } @@ -249,6 +252,22 @@ class ArrayToXmlTests < ActiveSupport::TestCase assert xml.include?(%(<name>Jason</name>)), xml end + def test_to_xml_with_non_hash_elements + xml = [1, 2, 3].to_xml(:skip_instruct => true, :indent => 0) + + assert_equal '<fixnums type="array"><fixnum', xml.first(29) + assert xml.include?(%(<fixnum type="integer">2</fixnum>)), xml + end + + def test_to_xml_with_non_hash_different_type_elements + xml = [1, 2.0, '3'].to_xml(:skip_instruct => true, :indent => 0) + + assert_equal '<objects type="array"><object', xml.first(29) + assert xml.include?(%(<object type="integer">1</object>)), xml + assert xml.include?(%(<object type="float">2.0</object>)), xml + assert xml.include?(%(object>3</object>)), xml + end + def test_to_xml_with_dedicated_name xml = [ { :name => "David", :age => 26, :age_in_millis => 820497600000 }, { :name => "Jason", :age => 31 } @@ -269,6 +288,18 @@ class ArrayToXmlTests < ActiveSupport::TestCase assert xml.include?(%(<name>Jason</name>)) end + def test_to_xml_with_indent_set + xml = [ + { :name => "David", :street_address => "Paulina" }, { :name => "Jason", :street_address => "Evergreen" } + ].to_xml(:skip_instruct => true, :skip_types => true, :indent => 4) + + assert_equal "<objects>\n <object>", xml.first(22) + assert xml.include?(%(\n <street-address>Paulina</street-address>)) + assert xml.include?(%(\n <name>David</name>)) + assert xml.include?(%(\n <street-address>Evergreen</street-address>)) + assert xml.include?(%(\n <name>Jason</name>)) + end + def test_to_xml_with_dasherize_false xml = [ { :name => "David", :street_address => "Paulina" }, { :name => "Jason", :street_address => "Evergreen" } @@ -289,7 +320,7 @@ class ArrayToXmlTests < ActiveSupport::TestCase assert xml.include?(%(<street-address>Evergreen</street-address>)) end - def test_to_with_instruct + def test_to_xml_with_instruct xml = [ { :name => "David", :age => 26, :age_in_millis => 820497600000 }, { :name => "Jason", :age => 31, :age_in_millis => BigDecimal.new('1.0') } diff --git a/activesupport/test/core_ext/date_ext_test.rb b/activesupport/test/core_ext/date_ext_test.rb index 5d0af035cc..e89be25b53 100644 --- a/activesupport/test/core_ext/date_ext_test.rb +++ b/activesupport/test/core_ext/date_ext_test.rb @@ -1,6 +1,7 @@ require 'abstract_unit' require 'active_support/time' require 'core_ext/date_and_time_behavior' +require 'time_zone_test_helpers' class DateExtCalculationsTest < ActiveSupport::TestCase def date_time_init(year,month,day,*args) @@ -8,6 +9,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase end include DateAndTimeBehavior + include TimeZoneTestHelpers def test_yesterday_in_calendar_reform assert_equal Date.new(1582,10,4), Date.new(1582,10,15).yesterday @@ -349,22 +351,6 @@ class DateExtCalculationsTest < ActiveSupport::TestCase Date.new(2005,2,28).advance(options) assert_equal({ :years => 3, :months => 11, :days => 2 }, options) end - - protected - def with_env_tz(new_tz = 'US/Eastern') - old_tz, ENV['TZ'] = ENV['TZ'], new_tz - yield - ensure - old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') - end - - def with_tz_default(tz = nil) - old_tz = Time.zone - Time.zone = tz - yield - ensure - Time.zone = old_tz - end end class DateExtBehaviorTest < ActiveSupport::TestCase diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb index 0a40aeb96c..74319ecd09 100644 --- a/activesupport/test/core_ext/date_time_ext_test.rb +++ b/activesupport/test/core_ext/date_time_ext_test.rb @@ -1,6 +1,7 @@ require 'abstract_unit' require 'active_support/time' require 'core_ext/date_and_time_behavior' +require 'time_zone_test_helpers' class DateTimeExtCalculationsTest < ActiveSupport::TestCase def date_time_init(year,month,day,hour,minute,second,*args) @@ -8,6 +9,7 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase end include DateAndTimeBehavior + include TimeZoneTestHelpers def test_to_s datetime = DateTime.new(2005, 2, 21, 14, 30, 0, 0) @@ -162,6 +164,12 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase assert_equal DateTime.civil(2013,10,17,20,22,19), DateTime.civil(2005,2,28,15,15,10).advance(:years => 7, :months => 19, :weeks => 2, :days => 5, :hours => 5, :minutes => 7, :seconds => 9) end + def test_advance_partial_days + assert_equal DateTime.civil(2012,9,29,13,15,10), DateTime.civil(2012,9,28,1,15,10).advance(:days => 1.5) + assert_equal DateTime.civil(2012,9,28,13,15,10), DateTime.civil(2012,9,28,1,15,10).advance(:days => 0.5) + assert_equal DateTime.civil(2012,10,29,13,15,10), DateTime.civil(2012,9,28,1,15,10).advance(:days => 1.5, :months => 1) + end + def test_advanced_processes_first_the_date_deltas_and_then_the_time_deltas # If the time deltas were processed first, the following datetimes would be advanced to 2010/04/01 instead. assert_equal DateTime.civil(2010, 3, 29), DateTime.civil(2010, 2, 28, 23, 59, 59).advance(:months => 1, :seconds => 1) @@ -330,6 +338,7 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase def test_to_f assert_equal 946684800.0, DateTime.civil(2000).to_f assert_equal 946684800.0, DateTime.civil(1999,12,31,19,0,0,Rational(-5,24)).to_f + assert_equal 946684800.5, DateTime.civil(1999,12,31,19,0,0.5,Rational(-5,24)).to_f end def test_to_i @@ -346,12 +355,4 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase assert_equal 0, DateTime.civil(2000).nsec assert_equal 500000000, DateTime.civil(2000, 1, 1, 0, 0, Rational(1,2)).nsec end - - protected - def with_env_tz(new_tz = 'US/Eastern') - old_tz, ENV['TZ'] = ENV['TZ'], new_tz - yield - ensure - old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') - end end diff --git a/activesupport/test/core_ext/digest/uuid_test.rb b/activesupport/test/core_ext/digest/uuid_test.rb new file mode 100644 index 0000000000..08e0a1d6e1 --- /dev/null +++ b/activesupport/test/core_ext/digest/uuid_test.rb @@ -0,0 +1,24 @@ +require 'abstract_unit' +require 'active_support/core_ext/digest/uuid' + +class DigestUUIDExt < ActiveSupport::TestCase + def test_v3_uuids + assert_equal "3d813cbb-47fb-32ba-91df-831e1593ac29", Digest::UUID.uuid_v3(Digest::UUID::DNS_NAMESPACE, "www.widgets.com") + assert_equal "86df55fb-428e-3843-8583-ba3c05f290bc", Digest::UUID.uuid_v3(Digest::UUID::URL_NAMESPACE, "http://www.widgets.com") + assert_equal "8c29ab0e-a2dc-3482-b5eb-20cb2e2387a1", Digest::UUID.uuid_v3(Digest::UUID::OID_NAMESPACE, "1.2.3") + assert_equal "ee49149d-53a4-304a-890b-468229f6afc3", Digest::UUID.uuid_v3(Digest::UUID::X500_NAMESPACE, "cn=John Doe, ou=People, o=Acme, Inc., c=US") + end + + def test_v5_uuids + assert_equal "21f7f8de-8051-5b89-8680-0195ef798b6a", Digest::UUID.uuid_v5(Digest::UUID::DNS_NAMESPACE, "www.widgets.com") + assert_equal "4e570fd8-186d-5a74-90f0-4d28e34673a1", Digest::UUID.uuid_v5(Digest::UUID::URL_NAMESPACE, "http://www.widgets.com") + assert_equal "42d5e23b-3a02-5135-85c6-52d1102f1f00", Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, "1.2.3") + assert_equal "fd5b2ddf-bcfe-58b6-90d6-db50f74db527", Digest::UUID.uuid_v5(Digest::UUID::X500_NAMESPACE, "cn=John Doe, ou=People, o=Acme, Inc., c=US") + end + + def test_invalid_hash_class + assert_raise ArgumentError do + Digest::UUID.uuid_from_hash(Digest::SHA2, Digest::UUID::OID_NAMESPACE, '1.2.3') + end + end +end diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb index c8f17f4618..31af3c4521 100644 --- a/activesupport/test/core_ext/duration_test.rb +++ b/activesupport/test/core_ext/duration_test.rb @@ -2,8 +2,11 @@ require 'abstract_unit' require 'active_support/inflector' require 'active_support/time' require 'active_support/json' +require 'time_zone_test_helpers' class DurationTest < ActiveSupport::TestCase + include TimeZoneTestHelpers + def test_is_a d = 1.day assert d.is_a?(ActiveSupport::Duration) @@ -159,6 +162,10 @@ class DurationTest < ActiveSupport::TestCase assert_equal counter, 60 end + def test_as_json + assert_equal 172800, 2.days.as_json + end + def test_to_json assert_equal '172800', 2.days.to_json end @@ -167,12 +174,4 @@ class DurationTest < ActiveSupport::TestCase cased = case 1.day when 1.day then "ok" end assert_equal cased, "ok" end - - protected - def with_env_tz(new_tz = 'US/Eastern') - old_tz, ENV['TZ'] = ENV['TZ'], new_tz - yield - ensure - old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') - end end diff --git a/activesupport/test/core_ext/hash/transform_keys_test.rb b/activesupport/test/core_ext/hash/transform_keys_test.rb new file mode 100644 index 0000000000..a7e12117f3 --- /dev/null +++ b/activesupport/test/core_ext/hash/transform_keys_test.rb @@ -0,0 +1,32 @@ +require 'abstract_unit' +require 'active_support/core_ext/hash/keys' + +class TransformKeysTest < ActiveSupport::TestCase + test "transform_keys returns a new hash with the keys computed from the block" do + original = { a: 'a', b: 'b' } + mapped = original.transform_keys { |k| "#{k}!".to_sym } + + assert_equal({ a: 'a', b: 'b' }, original) + assert_equal({ a!: 'a', b!: 'b' }, mapped) + end + + test "transform_keys! modifies the keys of the original" do + original = { a: 'a', b: 'b' } + mapped = original.transform_keys! { |k| "#{k}!".to_sym } + + assert_equal({ a!: 'a', b!: 'b' }, original) + assert_same original, mapped + end + + test "transform_keys returns an Enumerator if no block is given" do + original = { a: 'a', b: 'b' } + enumerator = original.transform_keys + assert_equal Enumerator, enumerator.class + end + + test "transform_keys is chainable with Enumerable methods" do + original = { a: 'a', b: 'b' } + mapped = original.transform_keys.with_index { |k, i| [k, i].join.to_sym } + assert_equal({ a0: 'a', b1: 'b' }, mapped) + end +end diff --git a/activesupport/test/core_ext/hash/transform_values_test.rb b/activesupport/test/core_ext/hash/transform_values_test.rb new file mode 100644 index 0000000000..45ed11fef7 --- /dev/null +++ b/activesupport/test/core_ext/hash/transform_values_test.rb @@ -0,0 +1,61 @@ +require 'abstract_unit' +require 'active_support/core_ext/hash/indifferent_access' +require 'active_support/core_ext/hash/transform_values' + +class TransformValuesTest < ActiveSupport::TestCase + test "transform_values returns a new hash with the values computed from the block" do + original = { a: 'a', b: 'b' } + mapped = original.transform_values { |v| v + '!' } + + assert_equal({ a: 'a', b: 'b' }, original) + assert_equal({ a: 'a!', b: 'b!' }, mapped) + end + + test "transform_values! modifies the values of the original" do + original = { a: 'a', b: 'b' } + mapped = original.transform_values! { |v| v + '!' } + + assert_equal({ a: 'a!', b: 'b!' }, original) + assert_same original, mapped + end + + test "indifferent access is still indifferent after mapping values" do + original = { a: 'a', b: 'b' }.with_indifferent_access + mapped = original.transform_values { |v| v + '!' } + + assert_equal 'a!', mapped[:a] + assert_equal 'a!', mapped['a'] + end + + # This is to be consistent with the behavior of Ruby's built in methods + # (e.g. #select, #reject) as of 2.2 + test "default values do not persist during mapping" do + original = Hash.new('foo') + original[:a] = 'a' + mapped = original.transform_values { |v| v + '!' } + + assert_equal 'a!', mapped[:a] + assert_nil mapped[:b] + end + + test "default procs do not persist after mapping" do + original = Hash.new { 'foo' } + original[:a] = 'a' + mapped = original.transform_values { |v| v + '!' } + + assert_equal 'a!', mapped[:a] + assert_nil mapped[:b] + end + + test "transform_values returns an Enumerator if no block is given" do + original = { a: 'a', b: 'b' } + enumerator = original.transform_values + assert_equal Enumerator, enumerator.class + end + + test "transform_values is chainable with Enumerable methods" do + original = { a: 'a', b: 'b' } + mapped = original.transform_values.with_index { |v, i| [v, i].join } + assert_equal({ a: 'a0', b: 'b1' }, mapped) + end +end diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index d824a16e98..dbbb2d77da 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -46,6 +46,10 @@ class HashExtTest < ActiveSupport::TestCase @nested_illegal_symbols = { [] => { [] => 3} } @upcase_strings = { 'A' => 1, 'B' => 2 } @nested_upcase_strings = { 'A' => { 'B' => { 'C' => 3 } } } + @string_array_of_hashes = { 'a' => [ { 'b' => 2 }, { 'c' => 3 }, 4 ] } + @symbol_array_of_hashes = { :a => [ { :b => 2 }, { :c => 3 }, 4 ] } + @mixed_array_of_hashes = { :a => [ { :b => 2 }, { 'c' => 3 }, 4 ] } + @upcase_array_of_hashes = { 'A' => [ { 'B' => 2 }, { 'C' => 3 }, 4 ] } end def test_methods @@ -66,6 +70,8 @@ class HashExtTest < ActiveSupport::TestCase assert_respond_to h, :to_options! assert_respond_to h, :compact assert_respond_to h, :compact! + assert_respond_to h, :except + assert_respond_to h, :except! end def test_transform_keys @@ -84,6 +90,9 @@ class HashExtTest < ActiveSupport::TestCase assert_equal @nested_upcase_strings, @nested_symbols.deep_transform_keys{ |key| key.to_s.upcase } assert_equal @nested_upcase_strings, @nested_strings.deep_transform_keys{ |key| key.to_s.upcase } assert_equal @nested_upcase_strings, @nested_mixed.deep_transform_keys{ |key| key.to_s.upcase } + assert_equal @upcase_array_of_hashes, @string_array_of_hashes.deep_transform_keys{ |key| key.to_s.upcase } + assert_equal @upcase_array_of_hashes, @symbol_array_of_hashes.deep_transform_keys{ |key| key.to_s.upcase } + assert_equal @upcase_array_of_hashes, @mixed_array_of_hashes.deep_transform_keys{ |key| key.to_s.upcase } end def test_deep_transform_keys_not_mutates @@ -109,6 +118,9 @@ class HashExtTest < ActiveSupport::TestCase assert_equal @nested_upcase_strings, @nested_symbols.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase } assert_equal @nested_upcase_strings, @nested_strings.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase } assert_equal @nested_upcase_strings, @nested_mixed.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase } + assert_equal @upcase_array_of_hashes, @string_array_of_hashes.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase } + assert_equal @upcase_array_of_hashes, @symbol_array_of_hashes.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase } + assert_equal @upcase_array_of_hashes, @mixed_array_of_hashes.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase } end def test_deep_transform_keys_with_bang_mutates @@ -134,6 +146,9 @@ class HashExtTest < ActiveSupport::TestCase assert_equal @nested_symbols, @nested_symbols.deep_symbolize_keys assert_equal @nested_symbols, @nested_strings.deep_symbolize_keys assert_equal @nested_symbols, @nested_mixed.deep_symbolize_keys + assert_equal @symbol_array_of_hashes, @string_array_of_hashes.deep_symbolize_keys + assert_equal @symbol_array_of_hashes, @symbol_array_of_hashes.deep_symbolize_keys + assert_equal @symbol_array_of_hashes, @mixed_array_of_hashes.deep_symbolize_keys end def test_deep_symbolize_keys_not_mutates @@ -159,6 +174,9 @@ class HashExtTest < ActiveSupport::TestCase assert_equal @nested_symbols, @nested_symbols.deep_dup.deep_symbolize_keys! assert_equal @nested_symbols, @nested_strings.deep_dup.deep_symbolize_keys! assert_equal @nested_symbols, @nested_mixed.deep_dup.deep_symbolize_keys! + assert_equal @symbol_array_of_hashes, @string_array_of_hashes.deep_dup.deep_symbolize_keys! + assert_equal @symbol_array_of_hashes, @symbol_array_of_hashes.deep_dup.deep_symbolize_keys! + assert_equal @symbol_array_of_hashes, @mixed_array_of_hashes.deep_dup.deep_symbolize_keys! end def test_deep_symbolize_keys_with_bang_mutates @@ -204,6 +222,9 @@ class HashExtTest < ActiveSupport::TestCase assert_equal @nested_strings, @nested_symbols.deep_stringify_keys assert_equal @nested_strings, @nested_strings.deep_stringify_keys assert_equal @nested_strings, @nested_mixed.deep_stringify_keys + assert_equal @string_array_of_hashes, @string_array_of_hashes.deep_stringify_keys + assert_equal @string_array_of_hashes, @symbol_array_of_hashes.deep_stringify_keys + assert_equal @string_array_of_hashes, @mixed_array_of_hashes.deep_stringify_keys end def test_deep_stringify_keys_not_mutates @@ -229,6 +250,9 @@ class HashExtTest < ActiveSupport::TestCase assert_equal @nested_strings, @nested_symbols.deep_dup.deep_stringify_keys! assert_equal @nested_strings, @nested_strings.deep_dup.deep_stringify_keys! assert_equal @nested_strings, @nested_mixed.deep_dup.deep_stringify_keys! + assert_equal @string_array_of_hashes, @string_array_of_hashes.deep_dup.deep_stringify_keys! + assert_equal @string_array_of_hashes, @symbol_array_of_hashes.deep_dup.deep_stringify_keys! + assert_equal @string_array_of_hashes, @mixed_array_of_hashes.deep_dup.deep_stringify_keys! end def test_deep_stringify_keys_with_bang_mutates @@ -635,6 +659,14 @@ class HashExtTest < ActiveSupport::TestCase assert_equal 1, h['first'] end + def test_to_options_on_indifferent_preserves_works_as_hash_with_dup + h = HashWithIndifferentAccess.new({ a: { b: 'b' } }) + dup = h.dup + + dup[:a][:c] = 'c' + assert_equal 'c', h[:a][:c] + end + def test_indifferent_sub_hashes h = {'user' => {'id' => 5}}.with_indifferent_access ['user', :user].each {|user| [:id, 'id'].each {|id| assert_equal 5, h[user][id], "h[#{user.inspect}][#{id.inspect}] should be 5"}} @@ -659,6 +691,11 @@ class HashExtTest < ActiveSupport::TestCase { :failure => "stuff", :funny => "business" }.assert_valid_keys([ :failure, :funny ]) { :failure => "stuff", :funny => "business" }.assert_valid_keys(:failure, :funny) end + # not all valid keys are required to be present + assert_nothing_raised do + { :failure => "stuff", :funny => "business" }.assert_valid_keys([ :failure, :funny, :sunny ]) + { :failure => "stuff", :funny => "business" }.assert_valid_keys(:failure, :funny, :sunny) + end exception = assert_raise ArgumentError do { :failore => "stuff", :funny => "business" }.assert_valid_keys([ :failure, :funny ]) @@ -669,6 +706,16 @@ class HashExtTest < ActiveSupport::TestCase { :failore => "stuff", :funny => "business" }.assert_valid_keys(:failure, :funny) end assert_equal "Unknown key: :failore. Valid keys are: :failure, :funny", exception.message + + exception = assert_raise ArgumentError do + { :failore => "stuff", :funny => "business" }.assert_valid_keys([ :failure ]) + end + assert_equal "Unknown key: :failore. Valid keys are: :failure", exception.message + + exception = assert_raise ArgumentError do + { :failore => "stuff", :funny => "business" }.assert_valid_keys(:failure) + end + assert_equal "Unknown key: :failore. Valid keys are: :failure", exception.message end def test_assorted_keys_not_stringified @@ -697,6 +744,16 @@ class HashExtTest < ActiveSupport::TestCase assert_equal expected, hash_1 end + def test_deep_merge_with_falsey_values + hash_1 = { e: false } + hash_2 = { e: 'e' } + expected = { e: [:e, false, 'e'] } + assert_equal(expected, hash_1.deep_merge(hash_2) { |k, o, n| [k, o, n] }) + + hash_1.deep_merge!(hash_2) { |k, o, n| [k, o, n] } + assert_equal expected, hash_1 + end + def test_deep_merge_on_indifferent_access hash_1 = HashWithIndifferentAccess.new({ :a => "a", :b => "b", :c => { :c1 => "c1", :c2 => "c2", :c3 => { :d1 => "d1" } } }) hash_2 = HashWithIndifferentAccess.new({ :a => 1, :c => { :c1 => 2, :c3 => { :d2 => "d2" } } }) @@ -887,13 +944,19 @@ class HashExtTest < ActiveSupport::TestCase def test_except_with_more_than_one_argument original = { :a => 'x', :b => 'y', :c => 10 } expected = { :a => 'x' } + assert_equal expected, original.except(:b, :c) + + assert_equal expected, original.except!(:b, :c) + assert_equal expected, original end def test_except_with_original_frozen original = { :a => 'x', :b => 'y' } original.freeze assert_nothing_raised { original.except(:a) } + + assert_raise(RuntimeError) { original.except!(:a) } end def test_except_with_mocha_expectation_on_original @@ -905,11 +968,11 @@ class HashExtTest < ActiveSupport::TestCase def test_compact hash_contain_nil_value = @symbols.merge(z: nil) hash_with_only_nil_values = { a: nil, b: nil } - + h = hash_contain_nil_value.dup assert_equal(@symbols, h.compact) assert_equal(hash_contain_nil_value, h) - + h = hash_with_only_nil_values.dup assert_equal({}, h.compact) assert_equal(hash_with_only_nil_values, h) @@ -918,11 +981,11 @@ class HashExtTest < ActiveSupport::TestCase def test_compact! hash_contain_nil_value = @symbols.merge(z: nil) hash_with_only_nil_values = { a: nil, b: nil } - + h = hash_contain_nil_value.dup assert_equal(@symbols, h.compact!) assert_equal(@symbols, h) - + h = hash_with_only_nil_values.dup assert_equal({}, h.compact!) assert_equal({}, h) @@ -1437,6 +1500,16 @@ class HashToXmlTest < ActiveSupport::TestCase end end + def test_from_xml_array_one + expected = { 'numbers' => { 'type' => 'Array', 'value' => '1' }} + assert_equal expected, Hash.from_xml('<numbers type="Array"><value>1</value></numbers>') + end + + def test_from_xml_array_many + expected = { 'numbers' => { 'type' => 'Array', 'value' => [ '1', '2' ] }} + assert_equal expected, Hash.from_xml('<numbers type="Array"><value>1</value><value>2</value></numbers>') + end + def test_from_trusted_xml_allows_symbol_and_yaml_types expected = { 'product' => { 'name' => :value }} assert_equal expected, Hash.from_trusted_xml('<product><name type="symbol">value</name></product>') @@ -1470,6 +1543,17 @@ class HashToXmlTest < ActiveSupport::TestCase assert_equal 3, hash_wia.default end + def test_should_copy_the_default_proc_when_converting_to_hash_with_indifferent_access + hash = Hash.new do + 2 + 1 + end + assert_equal 3, hash[:foo] + + hash_wia = hash.with_indifferent_access + assert_equal 3, hash_wia[:foo] + assert_equal 3, hash_wia[:bar] + end + # The XML builder seems to fail miserably when trying to tag something # with the same name as a Kernel method (throw, test, loop, select ...) def test_kernel_method_names_to_xml diff --git a/activesupport/test/core_ext/kernel/concern_test.rb b/activesupport/test/core_ext/kernel/concern_test.rb index 9b1fdda3b0..478a00d2d2 100644 --- a/activesupport/test/core_ext/kernel/concern_test.rb +++ b/activesupport/test/core_ext/kernel/concern_test.rb @@ -6,7 +6,8 @@ class KernelConcernTest < ActiveSupport::TestCase mod = ::TOPLEVEL_BINDING.eval 'concern(:ToplevelConcern) { }' assert_equal mod, ::ToplevelConcern assert_kind_of ActiveSupport::Concern, ::ToplevelConcern - assert !Object.ancestors.include?(::ToplevelConcern), mod.ancestors.inspect + assert_not Object.ancestors.include?(::ToplevelConcern), mod.ancestors.inspect + ensure Object.send :remove_const, :ToplevelConcern end end diff --git a/activesupport/test/core_ext/kernel_test.rb b/activesupport/test/core_ext/kernel_test.rb index d8bf81d02b..a87af0007c 100644 --- a/activesupport/test/core_ext/kernel_test.rb +++ b/activesupport/test/core_ext/kernel_test.rb @@ -30,14 +30,6 @@ class KernelTest < ActiveSupport::TestCase end - def test_silence_stderr - old_stderr_position = STDERR.tell - silence_stderr { STDERR.puts 'hello world' } - assert_equal old_stderr_position, STDERR.tell - rescue Errno::ESPIPE - # Skip if we can't STDERR.tell - end - def test_silence_stream old_stream_position = STDOUT.tell silence_stream(STDOUT) { STDOUT.puts 'hello world' } @@ -56,9 +48,11 @@ class KernelTest < ActiveSupport::TestCase def test_quietly old_stdout_position, old_stderr_position = STDOUT.tell, STDERR.tell - quietly do - puts 'see me, feel me' - STDERR.puts 'touch me, heal me' + assert_deprecated do + quietly do + puts 'see me, feel me' + STDERR.puts 'touch me, heal me' + end end assert_equal old_stdout_position, STDOUT.tell assert_equal old_stderr_position, STDERR.tell @@ -66,10 +60,6 @@ class KernelTest < ActiveSupport::TestCase # Skip if we can't STDERR.tell end - def test_silence_stderr_with_return_value - assert_equal 1, silence_stderr { 1 } - end - def test_class_eval o = Object.new class << o; @x = 1; end @@ -77,10 +67,18 @@ class KernelTest < ActiveSupport::TestCase end def test_capture - assert_equal 'STDERR', capture(:stderr) { $stderr.print 'STDERR' } - assert_equal 'STDOUT', capture(:stdout) { print 'STDOUT' } - assert_equal "STDERR\n", capture(:stderr) { system('echo STDERR 1>&2') } - assert_equal "STDOUT\n", capture(:stdout) { system('echo STDOUT') } + assert_deprecated do + assert_equal 'STDERR', capture(:stderr) { $stderr.print 'STDERR' } + end + assert_deprecated do + assert_equal 'STDOUT', capture(:stdout) { print 'STDOUT' } + end + assert_deprecated do + assert_equal "STDERR\n", capture(:stderr) { system('echo STDERR 1>&2') } + end + assert_deprecated do + assert_equal "STDOUT\n", capture(:stdout) { system('echo STDOUT') } + end end end diff --git a/activesupport/test/core_ext/load_error_test.rb b/activesupport/test/core_ext/load_error_test.rb index 31863d0aca..5f804c749b 100644 --- a/activesupport/test/core_ext/load_error_test.rb +++ b/activesupport/test/core_ext/load_error_test.rb @@ -14,6 +14,15 @@ class TestMissingSourceFile < ActiveSupport::TestCase assert_equal 'nor/this/one.rb', e.path end end + + def test_is_missing + begin load 'nor_does_this_one' + rescue MissingSourceFile => e + assert e.is_missing?('nor_does_this_one') + assert e.is_missing?('nor_does_this_one.rb') + assert_not e.is_missing?('some_other_file') + end + end end class TestLoadError < ActiveSupport::TestCase @@ -29,4 +38,4 @@ class TestLoadError < ActiveSupport::TestCase assert_equal 'nor/this/one.rb', e.path end end -end
\ No newline at end of file +end diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb index ff6e21854e..380f5ad42b 100644 --- a/activesupport/test/core_ext/module_test.rb +++ b/activesupport/test/core_ext/module_test.rb @@ -72,7 +72,7 @@ Product = Struct.new(:name) do def type @type ||= begin - nil.type_name + :thing_without_same_method_name_as_delegated.name end end end diff --git a/activesupport/test/core_ext/numeric_ext_test.rb b/activesupport/test/core_ext/numeric_ext_test.rb index 3b1dabea8d..b82448458d 100644 --- a/activesupport/test/core_ext/numeric_ext_test.rb +++ b/activesupport/test/core_ext/numeric_ext_test.rb @@ -22,18 +22,6 @@ class NumericExtTimeAndDateTimeTest < ActiveSupport::TestCase end end - def test_deprecated_since_and_ago - assert_equal @now + 1, assert_deprecated { 1.since(@now) } - assert_equal @now - 1, assert_deprecated { 1.ago(@now) } - end - - def test_deprecated_since_and_ago_without_argument - now = Time.now - assert assert_deprecated { 1.since } >= now + 1 - now = Time.now - assert assert_deprecated { 1.ago } >= now - 1 - end - def test_irregular_durations assert_equal @now.advance(:days => 3000), 3000.days.since(@now) assert_equal @now.advance(:months => 1), 1.month.since(@now) @@ -83,44 +71,6 @@ class NumericExtTimeAndDateTimeTest < ActiveSupport::TestCase assert_equal Time.utc(2005,2,28,15,15,10), Time.utc(2004,2,29,15,15,10) + 1.year assert_equal DateTime.civil(2005,2,28,15,15,10), DateTime.civil(2004,2,29,15,15,10) + 1.year end - - def test_since_and_ago_anchored_to_time_now_when_time_zone_is_not_set - Time.zone = nil - with_env_tz 'US/Eastern' do - Time.stubs(:now).returns Time.local(2000) - # since - assert_not_instance_of ActiveSupport::TimeWithZone, assert_deprecated { 5.since } - assert_equal Time.local(2000,1,1,0,0,5), assert_deprecated { 5.since } - # ago - assert_not_instance_of ActiveSupport::TimeWithZone, assert_deprecated { 5.ago } - assert_equal Time.local(1999,12,31,23,59,55), assert_deprecated { 5.ago } - end - end - - def test_since_and_ago_anchored_to_time_zone_now_when_time_zone_is_set - Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] - with_env_tz 'US/Eastern' do - Time.stubs(:now).returns Time.local(2000) - # since - assert_instance_of ActiveSupport::TimeWithZone, assert_deprecated { 5.since } - assert_equal Time.utc(2000,1,1,0,0,5), assert_deprecated { 5.since.time } - assert_equal 'Eastern Time (US & Canada)', assert_deprecated { 5.since.time_zone.name } - # ago - assert_instance_of ActiveSupport::TimeWithZone, assert_deprecated { 5.ago } - assert_equal Time.utc(1999,12,31,23,59,55), assert_deprecated { 5.ago.time } - assert_equal 'Eastern Time (US & Canada)', assert_deprecated { 5.ago.time_zone.name } - end - ensure - Time.zone = nil - end - - protected - def with_env_tz(new_tz = 'US/Eastern') - old_tz, ENV['TZ'] = ENV['TZ'], new_tz - yield - ensure - old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') - end end class NumericExtDateTest < ActiveSupport::TestCase @@ -435,7 +385,7 @@ 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 diff --git a/activesupport/test/core_ext/blank_test.rb b/activesupport/test/core_ext/object/blank_test.rb index 246bc7fa61..246bc7fa61 100644 --- a/activesupport/test/core_ext/blank_test.rb +++ b/activesupport/test/core_ext/object/blank_test.rb diff --git a/activesupport/test/core_ext/object/inclusion_test.rb b/activesupport/test/core_ext/object/inclusion_test.rb index b054a8dd31..32d512eca3 100644 --- a/activesupport/test/core_ext/object/inclusion_test.rb +++ b/activesupport/test/core_ext/object/inclusion_test.rb @@ -37,11 +37,14 @@ class InTest < ActiveSupport::TestCase end class C < B end + class D + end def test_in_module assert A.in?(B) assert A.in?(C) assert !A.in?(A) + assert !A.in?(D) end def test_no_method_catching @@ -51,5 +54,6 @@ class InTest < ActiveSupport::TestCase def test_presence_in assert_equal "stuff", "stuff".presence_in(%w( lots of stuff )) assert_nil "stuff".presence_in(%w( lots of crap )) + assert_raise(ArgumentError) { 1.presence_in(1) } end end diff --git a/activesupport/test/core_ext/object/json_test.rb b/activesupport/test/core_ext/object/json_test.rb deleted file mode 100644 index d3d31530df..0000000000 --- a/activesupport/test/core_ext/object/json_test.rb +++ /dev/null @@ -1,9 +0,0 @@ -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/object/to_query_test.rb b/activesupport/test/core_ext/object/to_query_test.rb index f887a9e613..47220e9509 100644 --- a/activesupport/test/core_ext/object/to_query_test.rb +++ b/activesupport/test/core_ext/object/to_query_test.rb @@ -46,16 +46,22 @@ class ToQueryTest < ActiveSupport::TestCase :person => {:id => [20, 10]} end + def test_empty_array + assert_equal "person%5B%5D=", [].to_query('person') + end + def test_nested_empty_hash assert_equal '', {}.to_query - assert_query_equal 'a=1&b%5Bc%5D=3&b%5Bd%5D=', + assert_query_equal 'a=1&b%5Bc%5D=3', { a: 1, b: { c: 3, d: {} } } + assert_query_equal '', + { a: {b: {c: {}}} } assert_query_equal 'b%5Bc%5D=false&b%5Be%5D=&b%5Bf%5D=&p=12', { p: 12, b: { c: false, e: nil, f: '' } } - assert_query_equal 'b%5Bc%5D=3&b%5Bf%5D=&b%5Bk%5D=', + assert_query_equal 'b%5Bc%5D=3&b%5Bf%5D=', { b: { c: 3, k: {}, f: '' } } - assert_query_equal 'a%5B%5D=&b=3', + assert_query_equal 'b=3', {a: [], b: 3} end diff --git a/activesupport/test/core_ext/object_and_class_ext_test.rb b/activesupport/test/core_ext/object_and_class_ext_test.rb index 0f454fdd95..f692eb4fa3 100644 --- a/activesupport/test/core_ext/object_and_class_ext_test.rb +++ b/activesupport/test/core_ext/object_and_class_ext_test.rb @@ -1,7 +1,6 @@ require 'abstract_unit' require 'active_support/time' require 'active_support/core_ext/object' -require 'active_support/core_ext/class/subclasses' class ObjectTests < ActiveSupport::TestCase class DuckTime diff --git a/activesupport/test/core_ext/range_ext_test.rb b/activesupport/test/core_ext/range_ext_test.rb index 150e6b65fb..cfe31b75e8 100644 --- a/activesupport/test/core_ext/range_ext_test.rb +++ b/activesupport/test/core_ext/range_ext_test.rb @@ -12,7 +12,7 @@ class RangeTest < ActiveSupport::TestCase date_range = Time.utc(2005, 12, 10, 15, 30)..Time.utc(2005, 12, 10, 17, 30) assert_equal "BETWEEN '2005-12-10 15:30:00' AND '2005-12-10 17:30:00'", date_range.to_s(:db) end - + def test_date_range assert_instance_of Range, DateTime.new..DateTime.new assert_instance_of Range, DateTime::Infinity.new..DateTime::Infinity.new @@ -116,4 +116,9 @@ class RangeTest < ActiveSupport::TestCase datetime = DateTime.now assert ((datetime - 1.hour)..datetime).each {} end + + def test_date_time_with_step + datetime = DateTime.now + assert ((datetime - 1.hour)..datetime).step(1) {} + end end diff --git a/activesupport/test/core_ext/securerandom_test.rb b/activesupport/test/core_ext/securerandom_test.rb deleted file mode 100644 index 71980f6910..0000000000 --- a/activesupport/test/core_ext/securerandom_test.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'abstract_unit' -require 'active_support/core_ext/securerandom' - -class SecureRandomExt < ActiveSupport::TestCase - def test_v3_uuids - assert_equal "3d813cbb-47fb-32ba-91df-831e1593ac29", SecureRandom.uuid_v3(SecureRandom::UUID_DNS_NAMESPACE, "www.widgets.com") - assert_equal "86df55fb-428e-3843-8583-ba3c05f290bc", SecureRandom.uuid_v3(SecureRandom::UUID_URL_NAMESPACE, "http://www.widgets.com") - assert_equal "8c29ab0e-a2dc-3482-b5eb-20cb2e2387a1", SecureRandom.uuid_v3(SecureRandom::UUID_OID_NAMESPACE, "1.2.3") - assert_equal "ee49149d-53a4-304a-890b-468229f6afc3", SecureRandom.uuid_v3(SecureRandom::UUID_X500_NAMESPACE, "cn=John Doe, ou=People, o=Acme, Inc., c=US") - end - - def test_v5_uuids - assert_equal "21f7f8de-8051-5b89-8680-0195ef798b6a", SecureRandom.uuid_v5(SecureRandom::UUID_DNS_NAMESPACE, "www.widgets.com") - assert_equal "4e570fd8-186d-5a74-90f0-4d28e34673a1", SecureRandom.uuid_v5(SecureRandom::UUID_URL_NAMESPACE, "http://www.widgets.com") - assert_equal "42d5e23b-3a02-5135-85c6-52d1102f1f00", SecureRandom.uuid_v5(SecureRandom::UUID_OID_NAMESPACE, "1.2.3") - assert_equal "fd5b2ddf-bcfe-58b6-90d6-db50f74db527", SecureRandom.uuid_v5(SecureRandom::UUID_X500_NAMESPACE, "cn=John Doe, ou=People, o=Acme, Inc., c=US") - end - - def test_uuid_v4_alias - assert_equal SecureRandom.method(:uuid_v4), SecureRandom.method(:uuid) - end - - def test_invalid_hash_class - assert_raise ArgumentError do - SecureRandom.uuid_from_hash(Digest::SHA2, SecureRandom::UUID_OID_NAMESPACE, '1.2.3') - end - end -end diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index 95df173880..d77e6be595 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -10,10 +10,12 @@ require 'active_support/time' require 'active_support/core_ext/string/strip' require 'active_support/core_ext/string/output_safety' require 'active_support/core_ext/string/indent' +require 'time_zone_test_helpers' class StringInflectionsTest < ActiveSupport::TestCase include InflectorTestCases include ConstantizeTestCases + include TimeZoneTestHelpers def test_strip_heredoc_on_an_empty_string assert_equal '', ''.strip_heredoc @@ -214,19 +216,40 @@ class StringInflectionsTest < ActiveSupport::TestCase assert_equal "Hello Wor...", "Hello World!!".truncate(12) end - def test_truncate_with_omission_and_seperator + def test_truncate_with_omission_and_separator assert_equal "Hello[...]", "Hello World!".truncate(10, :omission => "[...]") assert_equal "Hello[...]", "Hello Big World!".truncate(13, :omission => "[...]", :separator => ' ') assert_equal "Hello Big[...]", "Hello Big World!".truncate(14, :omission => "[...]", :separator => ' ') assert_equal "Hello Big[...]", "Hello Big World!".truncate(15, :omission => "[...]", :separator => ' ') end - def test_truncate_with_omission_and_regexp_seperator + def test_truncate_with_omission_and_regexp_separator assert_equal "Hello[...]", "Hello Big World!".truncate(13, :omission => "[...]", :separator => /\s/) assert_equal "Hello Big[...]", "Hello Big World!".truncate(14, :omission => "[...]", :separator => /\s/) assert_equal "Hello Big[...]", "Hello Big World!".truncate(15, :omission => "[...]", :separator => /\s/) end + def test_truncate_words + assert_equal "Hello Big World!", "Hello Big World!".truncate_words(3) + assert_equal "Hello Big...", "Hello Big World!".truncate_words(2) + end + + def test_truncate_words_with_omission + assert_equal "Hello Big World!", "Hello Big World!".truncate_words(3, :omission => "[...]") + assert_equal "Hello Big[...]", "Hello Big World!".truncate_words(2, :omission => "[...]") + end + + def test_truncate_words_with_separator + assert_equal "Hello<br>Big<br>World!...", "Hello<br>Big<br>World!<br>".truncate_words(3, :separator => '<br>') + assert_equal "Hello<br>Big<br>World!", "Hello<br>Big<br>World!".truncate_words(3, :separator => '<br>') + assert_equal "Hello\n<br>Big...", "Hello\n<br>Big<br>Wide<br>World!".truncate_words(2, :separator => '<br>') + end + + def test_truncate_words_with_separator_and_omission + assert_equal "Hello<br>Big<br>World![...]", "Hello<br>Big<br>World!<br>".truncate_words(3, :omission => "[...]", :separator => '<br>') + assert_equal "Hello<br>Big<br>World!", "Hello<br>Big<br>World!".truncate_words(3, :omission => "[...]", :separator => '<br>') + end + def test_truncate_multibyte assert_equal "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 ...".force_encoding(Encoding::UTF_8), "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244".force_encoding(Encoding::UTF_8).truncate(10) @@ -354,6 +377,8 @@ class StringAccessTest < ActiveSupport::TestCase end class StringConversionsTest < ActiveSupport::TestCase + include TimeZoneTestHelpers + def test_string_to_time with_env_tz "Europe/Moscow" do assert_equal Time.utc(2005, 2, 27, 23, 50), "2005-02-27 23:50".to_time(:utc) @@ -523,14 +548,6 @@ class StringConversionsTest < ActiveSupport::TestCase assert_nil "".to_date assert_equal Date.new(Date.today.year, 2, 3), "Feb 3rd".to_date end - - protected - def with_env_tz(new_tz = 'US/Eastern') - old_tz, ENV['TZ'] = ENV['TZ'], new_tz - yield - ensure - old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') - end end class StringBehaviourTest < ActiveSupport::TestCase @@ -758,6 +775,14 @@ class OutputSafetyTest < ActiveSupport::TestCase string = "<b>hello</b>".html_safe assert_equal string, ERB::Util.html_escape(string) end + + test "ERB::Util.html_escape_once only escapes once" do + string = '1 < 2 & 3' + escaped_string = "1 < 2 & 3" + + assert_equal escaped_string, ERB::Util.html_escape_once(string) + assert_equal escaped_string, ERB::Util.html_escape_once(escaped_string) + end end class StringExcludeTest < ActiveSupport::TestCase diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb index e0a4b1be3e..c8283cddc5 100644 --- a/activesupport/test/core_ext/time_ext_test.rb +++ b/activesupport/test/core_ext/time_ext_test.rb @@ -1,6 +1,7 @@ require 'abstract_unit' require 'active_support/time' require 'core_ext/date_and_time_behavior' +require 'time_zone_test_helpers' class TimeExtCalculationsTest < ActiveSupport::TestCase def date_time_init(year,month,day,hour,minute,second,usec=0) @@ -8,6 +9,7 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase end include DateAndTimeBehavior + include TimeZoneTestHelpers def test_seconds_since_midnight assert_equal 1,Time.local(2005,1,1,0,0,1).seconds_since_midnight @@ -847,15 +849,6 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase def test_all_year assert_equal Time.local(2011,1,1,0,0,0)..Time.local(2011,12,31,23,59,59,Rational(999999999, 1000)), Time.local(2011,6,7,10,10,10).all_year end - - protected - def with_env_tz(new_tz = 'US/Eastern') - old_tz, ENV['TZ'] = ENV['TZ'], new_tz - yield - ensure - old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') - end - end class TimeExtMarshalingTest < ActiveSupport::TestCase diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index 7fe4d4a6b2..3000da8da4 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -1,7 +1,9 @@ require 'abstract_unit' require 'active_support/time' +require 'time_zone_test_helpers' class TimeWithZoneTest < ActiveSupport::TestCase + include TimeZoneTestHelpers def setup @utc = Time.utc(2000, 1, 1, 0) @@ -246,16 +248,31 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_equal 86_400.0, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2), ActiveSupport::TimeZone['Hawaii'] ) - Time.utc(2000, 1, 1) end + def test_minus_with_time_precision + assert_equal 86_399.999999998, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone['UTC'] ) - Time.utc(2000, 1, 2, 0, 0, 0, Rational(1, 1000)) + assert_equal 86_399.999999998, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone['Hawaii'] ) - Time.utc(2000, 1, 2, 0, 0, 0, Rational(1, 1000)) + end + def test_minus_with_time_with_zone twz1 = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1), ActiveSupport::TimeZone['UTC'] ) twz2 = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2), ActiveSupport::TimeZone['UTC'] ) assert_equal 86_400.0, twz2 - twz1 end + def test_minus_with_time_with_zone_precision + twz1 = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 0, 0, 0, Rational(1, 1000)), ActiveSupport::TimeZone['UTC'] ) + twz2 = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone['UTC'] ) + assert_equal 86_399.999999998, twz2 - twz1 + end + def test_minus_with_datetime assert_equal 86_400.0, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2), ActiveSupport::TimeZone['UTC'] ) - DateTime.civil(2000, 1, 1) end + def test_minus_with_datetime_precision + assert_equal 86_399.999999999, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone['UTC'] ) - DateTime.civil(2000, 1, 1) + end + def test_minus_with_wrapped_datetime assert_equal 86_400.0, ActiveSupport::TimeWithZone.new( DateTime.civil(2000, 1, 2), ActiveSupport::TimeZone['UTC'] ) - Time.utc(2000, 1, 1) assert_equal 86_400.0, ActiveSupport::TimeWithZone.new( DateTime.civil(2000, 1, 2), ActiveSupport::TimeZone['UTC'] ) - DateTime.civil(2000, 1, 1) @@ -350,6 +367,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_acts_like_time + assert @twz.acts_like_time? assert @twz.acts_like?(:time) assert ActiveSupport::TimeWithZone.new(DateTime.civil(2000), @time_zone).acts_like?(:time) end @@ -795,23 +813,17 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_equal "undefined method `this_method_does_not_exist' for Fri, 31 Dec 1999 19:00:00 EST -05:00:Time", e.message assert_no_match "rescue", e.backtrace.first end - - protected - def with_env_tz(new_tz = 'US/Eastern') - old_tz, ENV['TZ'] = ENV['TZ'], new_tz - yield - ensure - old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') - end end class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase + include TimeZoneTestHelpers + def setup - @t, @dt = Time.utc(2000), DateTime.civil(2000) + @t, @dt, @zone = Time.utc(2000), DateTime.civil(2000), Time.zone end def teardown - Time.zone = nil + Time.zone = @zone end def test_in_time_zone @@ -867,8 +879,6 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase def test_localtime Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] assert_equal @dt.in_time_zone.localtime, @dt.in_time_zone.utc.to_time.getlocal - ensure - Time.zone = nil end def test_use_zone @@ -907,6 +917,7 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase end def test_time_zone_getter_and_setter_with_zone_default_set + old_zone_default = Time.zone_default Time.zone_default = ActiveSupport::TimeZone['Alaska'] assert_equal ActiveSupport::TimeZone['Alaska'], Time.zone Time.zone = ActiveSupport::TimeZone['Hawaii'] @@ -914,8 +925,7 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase Time.zone = nil assert_equal ActiveSupport::TimeZone['Alaska'], Time.zone ensure - Time.zone = nil - Time.zone_default = nil + Time.zone_default = old_zone_default end def test_time_zone_setter_is_thread_safe @@ -987,8 +997,6 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase assert_equal 'Eastern Time (US & Canada)', Time.current.time_zone.name assert_equal Time.utc(2000), Time.current.time end - ensure - Time.zone = nil end def test_time_in_time_zone_doesnt_affect_receiver @@ -999,25 +1007,15 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase assert_not time.utc?, 'time expected to be local, but is UTC' end end - - protected - def with_env_tz(new_tz = 'US/Eastern') - old_tz, ENV['TZ'] = ENV['TZ'], new_tz - yield - ensure - old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') - end end class TimeWithZoneMethodsForDate < ActiveSupport::TestCase + include TimeZoneTestHelpers + def setup @d = Date.civil(2000) end - def teardown - Time.zone = nil - end - def test_in_time_zone with_tz_default 'Alaska' do assert_equal 'Sat, 01 Jan 2000 00:00:00 AKST -09:00', @d.in_time_zone.inspect @@ -1050,28 +1048,17 @@ class TimeWithZoneMethodsForDate < ActiveSupport::TestCase assert_raise(ArgumentError) { @d.in_time_zone(-15.hours) } assert_raise(ArgumentError) { @d.in_time_zone(Object.new) } end - - protected - def with_tz_default(tz = nil) - old_tz = Time.zone - Time.zone = tz - yield - ensure - Time.zone = old_tz - end end class TimeWithZoneMethodsForString < ActiveSupport::TestCase + include TimeZoneTestHelpers + def setup @s = "Sat, 01 Jan 2000 00:00:00" @u = "Sat, 01 Jan 2000 00:00:00 UTC +00:00" @z = "Fri, 31 Dec 1999 19:00:00 EST -05:00" end - def teardown - Time.zone = nil - end - def test_in_time_zone with_tz_default 'Alaska' do assert_equal 'Sat, 01 Jan 2000 00:00:00 AKST -09:00', @s.in_time_zone.inspect @@ -1126,13 +1113,4 @@ class TimeWithZoneMethodsForString < ActiveSupport::TestCase assert_raise(ArgumentError) { @u.in_time_zone(Object.new) } assert_raise(ArgumentError) { @z.in_time_zone(Object.new) } end - - protected - def with_tz_default(tz = nil) - old_tz = Time.zone - Time.zone = tz - yield - ensure - Time.zone = old_tz - end end diff --git a/activesupport/test/core_ext/uri_ext_test.rb b/activesupport/test/core_ext/uri_ext_test.rb index 03e388dd7a..43a5997ddd 100644 --- a/activesupport/test/core_ext/uri_ext_test.rb +++ b/activesupport/test/core_ext/uri_ext_test.rb @@ -7,7 +7,7 @@ class URIExtTest < ActiveSupport::TestCase def test_uri_decode_handle_multibyte str = "\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E" # Ni-ho-nn-go in UTF-8, means Japanese. - parser = URI::Parser.new + parser = URI.parser assert_equal str, parser.unescape(parser.escape(str)) end end diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index 4ca63b3417..a013aadd67 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -367,9 +367,11 @@ class DependenciesTest < ActiveSupport::TestCase with_autoloading_fixtures do e = assert_raise(NameError) { A::DoesNotExist.nil? } assert_equal "uninitialized constant A::DoesNotExist", e.message + assert_equal :DoesNotExist, e.name e = assert_raise(NameError) { A::B::DoesNotExist.nil? } assert_equal "uninitialized constant A::B::DoesNotExist", e.message + assert_equal :DoesNotExist, e.name end end @@ -537,6 +539,7 @@ class DependenciesTest < ActiveSupport::TestCase mod = Module.new e = assert_raise(NameError) { mod::E } assert_equal 'E cannot be autoloaded from an anonymous class or module', e.message + assert_equal :E, e.name end end @@ -954,7 +957,7 @@ class DependenciesTest < ActiveSupport::TestCase assert_kind_of Class, A::B # Necessary to load A::B for the test ActiveSupport::Dependencies.mark_for_unload(A::B) ActiveSupport::Dependencies.remove_unloadable_constants! - + A::B # Make sure no circular dependency error end end diff --git a/activesupport/test/deprecation_test.rb b/activesupport/test/deprecation_test.rb index ee1c69502e..7aff56cbad 100644 --- a/activesupport/test/deprecation_test.rb +++ b/activesupport/test/deprecation_test.rb @@ -355,4 +355,21 @@ class DeprecationTest < ActiveSupport::TestCase end deprecator end + + def capture(stream) + stream = stream.to_s + captured_stream = Tempfile.new(stream) + stream_io = eval("$#{stream}") + origin_stream = stream_io.dup + stream_io.reopen(captured_stream) + + yield + + stream_io.rewind + return captured_stream.read + ensure + captured_stream.close + captured_stream.unlink + stream_io.reopen(origin_stream) + end end diff --git a/activesupport/test/i18n_test.rb b/activesupport/test/i18n_test.rb index 5ef59b6e6b..3faa15e7fd 100644 --- a/activesupport/test/i18n_test.rb +++ b/activesupport/test/i18n_test.rb @@ -99,7 +99,6 @@ class I18nTest < ActiveSupport::TestCase end def test_to_sentence_with_empty_i18n_store - I18n.backend.store_translations 'empty', {} assert_equal 'a, b, and c', %w[a b c].to_sentence(locale: 'empty') end end diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index b0b4738eb3..58fdea0972 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -498,10 +498,10 @@ class InflectorTest < ActiveSupport::TestCase end %w(plurals singulars uncountables humans acronyms).each do |scope| - ActiveSupport::Inflector.inflections do |inflect| - define_method("test_clear_inflections_with_#{scope}") do - with_dup do - # clear the inflections + define_method("test_clear_inflections_with_#{scope}") do + with_dup do + # clear the inflections + ActiveSupport::Inflector.inflections do |inflect| inflect.clear(scope) assert_equal [], inflect.send(scope) end @@ -509,6 +509,14 @@ class InflectorTest < ActiveSupport::TestCase end end + def test_inflections_with_uncountable_words + ActiveSupport::Inflector.inflections do |inflect| + inflect.uncountable "HTTP" + end + + assert_equal "HTTP", ActiveSupport::Inflector.pluralize("HTTP") + end + # Dups the singleton and yields, restoring the original inflections later. # Use this in tests what modify the state of the singleton. # @@ -516,9 +524,10 @@ class InflectorTest < ActiveSupport::TestCase # there are module functions that access ActiveSupport::Inflector.inflections, # so we need to replace the singleton itself. def with_dup - original = ActiveSupport::Inflector::Inflections.instance_variable_get(:@__instance__) - ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, original.dup) + original = ActiveSupport::Inflector::Inflections.instance_variable_get(:@__instance__)[:en] + ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original.dup) + yield ensure - ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, original) + ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original) end end diff --git a/activesupport/test/inflector_test_cases.rb b/activesupport/test/inflector_test_cases.rb index dd03a61176..b556da0046 100644 --- a/activesupport/test/inflector_test_cases.rb +++ b/activesupport/test/inflector_test_cases.rb @@ -208,9 +208,11 @@ module InflectorTestCases } UnderscoreToHuman = { - "employee_salary" => "Employee salary", - "employee_id" => "Employee", - "underground" => "Underground" + 'employee_salary' => 'Employee salary', + 'employee_id' => 'Employee', + 'underground' => 'Underground', + '_id' => 'Id', + '_external_id' => 'External' } UnderscoreToHumanWithoutCapitalize = { diff --git a/activesupport/test/json/decoding_test.rb b/activesupport/test/json/decoding_test.rb index 07d7e530ca..80bf255080 100644 --- a/activesupport/test/json/decoding_test.rb +++ b/activesupport/test/json/decoding_test.rb @@ -73,22 +73,20 @@ class TestJSONDecoding < ActiveSupport::TestCase TESTS.each_with_index do |(json, expected), index| test "json decodes #{index}" do - prev = ActiveSupport.parse_json_times - ActiveSupport.parse_json_times = true - silence_warnings do - assert_equal expected, ActiveSupport::JSON.decode(json), "JSON decoding \ - failed for #{json}" + with_parse_json_times(true) do + silence_warnings do + assert_equal expected, ActiveSupport::JSON.decode(json), "JSON decoding \ + failed for #{json}" + end end - ActiveSupport.parse_json_times = prev end end test "json decodes time json with time parsing disabled" do - prev = ActiveSupport.parse_json_times - ActiveSupport.parse_json_times = false - expected = {"a" => "2007-01-01 01:12:34 Z"} - assert_equal expected, ActiveSupport::JSON.decode(%({"a": "2007-01-01 01:12:34 Z"})) - ActiveSupport.parse_json_times = prev + with_parse_json_times(false) do + expected = {"a" => "2007-01-01 01:12:34 Z"} + assert_equal expected, ActiveSupport::JSON.decode(%({"a": "2007-01-01 01:12:34 Z"})) + end end def test_failed_json_decoding @@ -101,5 +99,15 @@ class TestJSONDecoding < ActiveSupport::TestCase def test_cannot_pass_unsupported_options assert_raise(ArgumentError) { ActiveSupport::JSON.decode("", create_additions: true) } end + + private + + def with_parse_json_times(value) + old_value = ActiveSupport.parse_json_times + ActiveSupport.parse_json_times = value + yield + ensure + ActiveSupport.parse_json_times = old_value + end end diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb index f22d7b8b02..ad358ad21d 100644 --- a/activesupport/test/json/encoding_test.rb +++ b/activesupport/test/json/encoding_test.rb @@ -4,8 +4,11 @@ require 'abstract_unit' require 'active_support/core_ext/string/inflections' require 'active_support/json' require 'active_support/time' +require 'time_zone_test_helpers' class TestJSONEncoding < ActiveSupport::TestCase + include TimeZoneTestHelpers + class Foo def initialize(a, b) @a, @b = a, b @@ -491,31 +494,28 @@ EXPECTED def test_twz_to_json_with_custom_time_precision with_standard_json_time_format(true) do - ActiveSupport::JSON::Encoding.time_precision = 0 - zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] - time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone) - assert_equal "\"1999-12-31T19:00:00-05:00\"", ActiveSupport::JSON.encode(time) + with_time_precision(0) do + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone) + assert_equal "\"1999-12-31T19:00:00-05:00\"", ActiveSupport::JSON.encode(time) + end end - ensure - ActiveSupport::JSON::Encoding.time_precision = 3 end def test_time_to_json_with_custom_time_precision with_standard_json_time_format(true) do - ActiveSupport::JSON::Encoding.time_precision = 0 - assert_equal "\"2000-01-01T00:00:00Z\"", ActiveSupport::JSON.encode(Time.utc(2000)) + with_time_precision(0) do + assert_equal "\"2000-01-01T00:00:00Z\"", ActiveSupport::JSON.encode(Time.utc(2000)) + end end - ensure - ActiveSupport::JSON::Encoding.time_precision = 3 end def test_datetime_to_json_with_custom_time_precision with_standard_json_time_format(true) do - ActiveSupport::JSON::Encoding.time_precision = 0 - assert_equal "\"2000-01-01T00:00:00+00:00\"", ActiveSupport::JSON.encode(DateTime.new(2000)) + with_time_precision(0) do + assert_equal "\"2000-01-01T00:00:00+00:00\"", ActiveSupport::JSON.encode(DateTime.new(2000)) + end end - ensure - ActiveSupport::JSON::Encoding.time_precision = 3 end def test_twz_to_json_when_wrapping_a_date_time @@ -530,17 +530,18 @@ EXPECTED json_object[1..-2].scan(/([^{}:,\s]+):/).flatten.sort end - def with_env_tz(new_tz = 'US/Eastern') - old_tz, ENV['TZ'] = ENV['TZ'], new_tz + def with_standard_json_time_format(boolean = true) + old, ActiveSupport.use_standard_json_time_format = ActiveSupport.use_standard_json_time_format, boolean yield ensure - old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') + ActiveSupport.use_standard_json_time_format = old end - def with_standard_json_time_format(boolean = true) - old, ActiveSupport.use_standard_json_time_format = ActiveSupport.use_standard_json_time_format, boolean + def with_time_precision(value) + old_value = ActiveSupport::JSON::Encoding.time_precision + ActiveSupport::JSON::Encoding.time_precision = value yield ensure - ActiveSupport.use_standard_json_time_format = old + ActiveSupport::JSON::Encoding.time_precision = old_value end end diff --git a/activesupport/test/key_generator_test.rb b/activesupport/test/key_generator_test.rb index 525082d313..f7e8e9a795 100644 --- a/activesupport/test/key_generator_test.rb +++ b/activesupport/test/key_generator_test.rb @@ -29,4 +29,34 @@ class KeyGeneratorTest < ActiveSupport::TestCase end end +class CachingKeyGeneratorTest < ActiveSupport::TestCase + def setup + @secret = SecureRandom.hex(64) + @generator = ActiveSupport::KeyGenerator.new(@secret, :iterations=>2) + @caching_generator = ActiveSupport::CachingKeyGenerator.new(@generator) + end + + test "Generating a cached key for same salt and key size" do + derived_key = @caching_generator.generate_key("some_salt", 32) + cached_key = @caching_generator.generate_key("some_salt", 32) + + assert_equal derived_key, cached_key + assert_equal derived_key.object_id, cached_key.object_id + end + + test "Does not cache key for different salt" do + derived_key = @caching_generator.generate_key("some_salt", 32) + different_salt_key = @caching_generator.generate_key("other_salt", 32) + + assert_not_equal derived_key, different_salt_key + end + + test "Does not cache key for different length" do + derived_key = @caching_generator.generate_key("some_salt", 32) + different_length_key = @caching_generator.generate_key("some_salt", 64) + + assert_not_equal derived_key, different_length_key + end +end + end diff --git a/activesupport/test/multibyte_conformance_test.rb b/activesupport/test/multibyte_conformance_test.rb index 6ab8fa28ee..aba81b8248 100644 --- a/activesupport/test/multibyte_conformance_test.rb +++ b/activesupport/test/multibyte_conformance_test.rb @@ -10,7 +10,6 @@ require 'tmpdir' class Downloader def self.download(from, to) unless File.exist?(to) - $stderr.puts "Downloading #{from} to #{to}" unless File.exist?(File.dirname(to)) system "mkdir -p #{File.dirname(to)}" end @@ -22,6 +21,7 @@ class Downloader end end end + true end end @@ -31,11 +31,15 @@ class MultibyteConformanceTest < ActiveSupport::TestCase UNIDATA_URL = "http://www.unicode.org/Public/#{ActiveSupport::Multibyte::Unicode::UNICODE_VERSION}/ucd" UNIDATA_FILE = '/NormalizationTest.txt' CACHE_DIR = File.join(Dir.tmpdir, 'cache') + FileUtils.mkdir_p(CACHE_DIR) + RUN_P = begin + Downloader.download(UNIDATA_URL + UNIDATA_FILE, CACHE_DIR + UNIDATA_FILE) + rescue + end def setup - FileUtils.mkdir_p(CACHE_DIR) - Downloader.download(UNIDATA_URL + UNIDATA_FILE, CACHE_DIR + UNIDATA_FILE) @proxy = ActiveSupport::Multibyte::Chars + skip "Unable to download test data" unless RUN_P end def test_normalizations_C @@ -126,4 +130,4 @@ class MultibyteConformanceTest < ActiveSupport::TestCase def inspect_codepoints(str) str.to_s.unpack("U*").map{|cp| cp.to_s(16) }.join(' ') end -end
\ No newline at end of file +end diff --git a/activesupport/test/multibyte_proxy_test.rb b/activesupport/test/multibyte_proxy_test.rb new file mode 100644 index 0000000000..d8ffd7ca9c --- /dev/null +++ b/activesupport/test/multibyte_proxy_test.rb @@ -0,0 +1,34 @@ +# encoding: utf-8 + +require 'abstract_unit' + +class MultibyteProxyText < ActiveSupport::TestCase + class AsciiOnlyEncoder + attr_reader :wrapped_string + alias to_s wrapped_string + + def initialize(string) + @wrapped_string = string.gsub(/[^\u0000-\u007F]/, '?') + end + end + + def with_custom_encoder(encoder) + original_proxy_class = ActiveSupport::Multibyte.proxy_class + + begin + ActiveSupport::Multibyte.proxy_class = encoder + + yield + ensure + ActiveSupport::Multibyte.proxy_class = original_proxy_class + end + end + + test "custom multibyte encoder" do + with_custom_encoder(AsciiOnlyEncoder) do + assert_equal "s?me string 123", "søme string 123".mb_chars.to_s + end + + assert_equal "søme string 123", "søme string 123".mb_chars.to_s + end +end diff --git a/activesupport/test/number_helper_i18n_test.rb b/activesupport/test/number_helper_i18n_test.rb index 65aecece71..e6925e9083 100644 --- a/activesupport/test/number_helper_i18n_test.rb +++ b/activesupport/test/number_helper_i18n_test.rb @@ -43,6 +43,10 @@ module ActiveSupport :custom_units_for_number_to_human => {:mili => "mm", :centi => "cm", :deci => "dm", :unit => "m", :ten => "dam", :hundred => "hm", :thousand => "km"} end + def teardown + I18n.backend.reload! + end + def test_number_to_i18n_currency assert_equal("&$ - 10.00", number_to_currency(10, :locale => 'ts')) assert_equal("(&$ - 10.00)", number_to_currency(-10, :locale => 'ts')) @@ -50,8 +54,6 @@ module ActiveSupport end def test_number_to_currency_with_empty_i18n_store - I18n.backend.store_translations 'empty', {} - assert_equal("$10.00", number_to_currency(10, :locale => 'empty')) assert_equal("-$10.00", number_to_currency(-10, :locale => 'empty')) end @@ -80,8 +82,6 @@ module ActiveSupport end def test_number_with_i18n_precision_and_empty_i18n_store - I18n.backend.store_translations 'empty', {} - assert_equal("123456789.123", number_to_rounded(123456789.123456789, :locale => 'empty')) assert_equal("1.000", number_to_rounded(1.0000, :locale => 'empty')) end @@ -92,8 +92,6 @@ module ActiveSupport end def test_number_with_i18n_delimiter_and_empty_i18n_store - I18n.backend.store_translations 'empty', {} - assert_equal("1,000,000.234", number_to_delimited(1000000.234, :locale => 'empty')) end @@ -107,8 +105,6 @@ module ActiveSupport end def test_number_to_i18n_percentage_and_empty_i18n_store - I18n.backend.store_translations 'empty', {} - assert_equal("1.000%", number_to_percentage(1, :locale => 'empty')) assert_equal("1.243%", number_to_percentage(1.2434, :locale => 'empty')) assert_equal("12434.000%", number_to_percentage(12434, :locale => 'empty')) @@ -121,8 +117,6 @@ module ActiveSupport end def test_number_to_i18n_human_size_with_empty_i18n_store - I18n.backend.store_translations 'empty', {} - assert_equal("2 KB", number_to_human_size(2048, :locale => 'empty')) assert_equal("42 Bytes", number_to_human_size(42, :locale => 'empty')) end @@ -142,8 +136,6 @@ module ActiveSupport end def test_number_to_human_with_empty_i18n_store - I18n.backend.store_translations 'empty', {} - assert_equal "2 Thousand", number_to_human(2000, :locale => 'empty') assert_equal "1.23 Billion", number_to_human(1234567890, :locale => 'empty') end diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb index 9bdb92024e..bb51cc68f2 100644 --- a/activesupport/test/number_helper_test.rb +++ b/activesupport/test/number_helper_test.rb @@ -1,5 +1,6 @@ require 'abstract_unit' require 'active_support/number_helper' +require 'active_support/core_ext/string/output_safety' module ActiveSupport module NumberHelper @@ -97,6 +98,7 @@ module ActiveSupport assert_equal("123,456,789.78901", number_helper.number_to_delimited(123456789.78901)) assert_equal("0.78901", number_helper.number_to_delimited(0.78901)) assert_equal("123,456.78", number_helper.number_to_delimited("123456.78")) + assert_equal("123,456.78", number_helper.number_to_delimited("123456.78".html_safe)) end end @@ -132,6 +134,7 @@ module ActiveSupport assert_equal("111.23460000000000000000", number_helper.number_to_rounded('111.2346', :precision => 20)) assert_equal("111.23460000000000000000", number_helper.number_to_rounded(BigDecimal(111.2346, Float::DIG), :precision => 20)) assert_equal("111.2346" + "0"*96, number_helper.number_to_rounded('111.2346', :precision => 100)) + assert_equal("111.2346", number_helper.number_to_rounded(Rational(1112346, 10000), :precision => 4)) end end @@ -172,6 +175,7 @@ module ActiveSupport assert_equal "9775.0000000000000000", number_helper.number_to_rounded(BigDecimal(9775), :precision => 20, :significant => true ) assert_equal "9775.0000000000000000", number_helper.number_to_rounded("9775", :precision => 20, :significant => true ) assert_equal "9775." + "0"*96, number_helper.number_to_rounded("9775", :precision => 100, :significant => true ) + assert_equal("97.7", number_helper.number_to_rounded(Rational(9772, 100), :precision => 3, :significant => true)) end end diff --git a/activesupport/test/subscriber_test.rb b/activesupport/test/subscriber_test.rb index 253411aa3d..21e4ba0cee 100644 --- a/activesupport/test/subscriber_test.rb +++ b/activesupport/test/subscriber_test.rb @@ -4,20 +4,28 @@ require 'active_support/subscriber' class TestSubscriber < ActiveSupport::Subscriber attach_to :doodle - cattr_reader :event + cattr_reader :events def self.clear - @@event = nil + @@events = [] end def open_party(event) - @@event = event + events << event end private def private_party(event) - @@event = event + events << event + end +end + +# Monkey patch subscriber to test that only one subscriber per method is added. +class TestSubscriber + remove_method :open_party + def open_party(event) + events << event end end @@ -29,12 +37,18 @@ class SubscriberTest < ActiveSupport::TestCase def test_attaches_subscribers ActiveSupport::Notifications.instrument("open_party.doodle") - assert_equal "open_party.doodle", TestSubscriber.event.name + assert_equal "open_party.doodle", TestSubscriber.events.first.name + end + + def test_attaches_only_one_subscriber + ActiveSupport::Notifications.instrument("open_party.doodle") + + assert_equal 1, TestSubscriber.events.size end def test_does_not_attach_private_methods ActiveSupport::Notifications.instrument("private_party.doodle") - assert_nil TestSubscriber.event + assert_equal TestSubscriber.events, [] end end diff --git a/activesupport/test/test_test.rb b/activesupport/test/test_test.rb index 0fa08c0e3a..6f63a8a725 100644 --- a/activesupport/test/test_test.rb +++ b/activesupport/test/test_test.rb @@ -28,12 +28,30 @@ class AssertDifferenceTest < ActiveSupport::TestCase assert_equal 'custom', e.message end - def test_assert_no_difference + def test_assert_no_difference_pass assert_no_difference '@object.num' do # ... end end + def test_assert_no_difference_fail + error = assert_raises(Minitest::Assertion) do + assert_no_difference '@object.num' do + @object.increment + end + end + assert_equal "\"@object.num\" didn't change by 0.\nExpected: 0\n Actual: 1", error.message + end + + def test_assert_no_difference_with_message_fail + error = assert_raises(Minitest::Assertion) do + assert_no_difference '@object.num', 'Object Changed' do + @object.increment + end + end + assert_equal "Object Changed.\n\"@object.num\" didn't change by 0.\nExpected: 0\n Actual: 1", error.message + end + def test_assert_difference assert_difference '@object.num', +1 do @object.increment diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb index 79ec57af2b..b7a89ed332 100644 --- a/activesupport/test/time_zone_test.rb +++ b/activesupport/test/time_zone_test.rb @@ -1,7 +1,10 @@ require 'abstract_unit' require 'active_support/time' +require 'time_zone_test_helpers' class TimeZoneTest < ActiveSupport::TestCase + include TimeZoneTestHelpers + def test_utc_to_local zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] assert_equal Time.utc(1999, 12, 31, 19), zone.utc_to_local(Time.utc(2000, 1)) # standard offset -0500 @@ -254,6 +257,15 @@ class TimeZoneTest < ActiveSupport::TestCase assert_equal Time.utc(1999,12,31,19), twz.time end + def test_parse_with_day_omitted + with_env_tz 'US/Eastern' do + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + assert_equal Time.local(2000, 2, 1), zone.parse('Feb', Time.local(2000, 1, 1)) + assert_equal Time.local(2005, 2, 1), zone.parse('Feb 2005', Time.local(2000, 1, 1)) + assert_equal Time.local(2005, 2, 2), zone.parse('2 Feb 2005', Time.local(2000, 1, 1)) + end + end + def test_parse_should_not_black_out_system_timezone_dst_jump with_env_tz('EET') do zone = ActiveSupport::TimeZone['Pacific Time (US & Canada)'] @@ -407,12 +419,4 @@ class TimeZoneTest < ActiveSupport::TestCase assert ActiveSupport::TimeZone.us_zones.include?(ActiveSupport::TimeZone["Hawaii"]) assert !ActiveSupport::TimeZone.us_zones.include?(ActiveSupport::TimeZone["Kuala Lumpur"]) end - - protected - def with_env_tz(new_tz = 'US/Eastern') - old_tz, ENV['TZ'] = ENV['TZ'], new_tz - yield - ensure - old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') - end end diff --git a/activesupport/test/time_zone_test_helpers.rb b/activesupport/test/time_zone_test_helpers.rb new file mode 100644 index 0000000000..9632b89d09 --- /dev/null +++ b/activesupport/test/time_zone_test_helpers.rb @@ -0,0 +1,16 @@ +module TimeZoneTestHelpers + def with_tz_default(tz = nil) + old_tz = Time.zone + Time.zone = tz + yield + ensure + Time.zone = old_tz + end + + def with_env_tz(new_tz = 'US/Eastern') + old_tz, ENV['TZ'] = ENV['TZ'], new_tz + yield + ensure + old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') + end +end diff --git a/ci/travis.rb b/ci/travis.rb index 7e68993332..956a01dbee 100755 --- a/ci/travis.rb +++ b/ci/travis.rb @@ -52,7 +52,7 @@ class Build def tasks if activerecord? - ['mysql:rebuild_databases', "#{adapter}:#{'isolated_' if isolated?}test"] + ['db:mysql:rebuild', "#{adapter}:#{'isolated_' if isolated?}test"] else ["test#{':isolated' if isolated?}"] end diff --git a/guides/CHANGELOG.md b/guides/CHANGELOG.md index 14ad4fd424..2770fc73e7 100644 --- a/guides/CHANGELOG.md +++ b/guides/CHANGELOG.md @@ -1,3 +1,21 @@ +* Change Posts to Articles in Getting Started sample application in order to +better align with the actual guides. + + *John Kelly Ferguson* + +* Update all Rails 4.1.0 references to 4.1.1 within the guides and code. + + *John Kelly Ferguson* + +* Split up rows in the Explain Queries table of the ActiveRecord Querying section +in order to improve readability. + + *John Kelly Ferguson* + +* Change all non-HTTP method 'post' references to 'article'. + + *John Kelly Ferguson* + * Updates the maintenance policy to match the latest versions of Rails *Matias Korhonen* diff --git a/guides/assets/javascripts/guides.js b/guides/assets/javascripts/guides.js index e4d25dfb21..a9c7f0d016 100644 --- a/guides/assets/javascripts/guides.js +++ b/guides/assets/javascripts/guides.js @@ -51,3 +51,9 @@ var guidesIndex = { window.location = url; } }; + +// Disable autolink inside example code blocks of guides. +$(document).ready(function() { + SyntaxHighlighter.defaults['auto-links'] = false; + SyntaxHighlighter.all(); +}); diff --git a/guides/assets/stylesheets/main.css b/guides/assets/stylesheets/main.css index 898f9ff05b..318a1ef1c7 100644 --- a/guides/assets/stylesheets/main.css +++ b/guides/assets/stylesheets/main.css @@ -381,9 +381,12 @@ a, a:link, a:visited { font: inherit; padding-left: .75em; font-size: .95em; - background-position: 96% -65px; + background-position: 96% 16px; -webkit-appearance: none; } + .guides-index-small .guides-index-item:hover{ + background-position: 96% -65px; + } } #guides { diff --git a/guides/bug_report_templates/action_controller_master.rb b/guides/bug_report_templates/action_controller_master.rb index d44fd9196a..20c64b4a85 100644 --- a/guides/bug_report_templates/action_controller_master.rb +++ b/guides/bug_report_templates/action_controller_master.rb @@ -2,6 +2,9 @@ unless File.exist?('Gemfile') File.write('Gemfile', <<-GEMFILE) source 'https://rubygems.org' gem 'rails', github: 'rails/rails' + gem 'arel', github: 'rails/arel' + gem 'rack', github: 'rack/rack' + gem 'i18n', github: 'svenfuchs/i18n' GEMFILE system 'bundle' diff --git a/guides/bug_report_templates/active_record_master.rb b/guides/bug_report_templates/active_record_master.rb index d95354e12d..e7f5d0d5ff 100644 --- a/guides/bug_report_templates/active_record_master.rb +++ b/guides/bug_report_templates/active_record_master.rb @@ -3,6 +3,8 @@ unless File.exist?('Gemfile') source 'https://rubygems.org' gem 'rails', github: 'rails/rails' gem 'arel', github: 'rails/arel' + gem 'rack', github: 'rack/rack' + gem 'i18n', github: 'svenfuchs/i18n' gem 'sqlite3' GEMFILE diff --git a/guides/code/getting_started/.gitignore b/guides/code/getting_started/.gitignore deleted file mode 100644 index 6a502e997f..0000000000 --- a/guides/code/getting_started/.gitignore +++ /dev/null @@ -1,16 +0,0 @@ -# See https://help.github.com/articles/ignoring-files for more about ignoring files. -# -# If you find yourself ignoring temporary files generated by your text editor -# or operating system, you probably want to add a global ignore instead: -# git config --global core.excludesfile '~/.gitignore_global' - -# Ignore bundler config. -/.bundle - -# Ignore the default SQLite database. -/db/*.sqlite3 -/db/*.sqlite3-journal - -# Ignore all logfiles and tempfiles. -/log/*.log -/tmp diff --git a/guides/code/getting_started/Gemfile b/guides/code/getting_started/Gemfile deleted file mode 100644 index c3d7e96c4d..0000000000 --- a/guides/code/getting_started/Gemfile +++ /dev/null @@ -1,40 +0,0 @@ -source 'https://rubygems.org' - - -# Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem 'rails', '4.1.0' -# Use sqlite3 as the database for Active Record -gem 'sqlite3' -# Use SCSS for stylesheets -gem 'sass-rails', '~> 4.0.1' -# Use Uglifier as compressor for JavaScript assets -gem 'uglifier', '>= 1.3.0' -# Use CoffeeScript for .js.coffee assets and views -gem 'coffee-rails', '~> 4.0.0' -# See https://github.com/sstephenson/execjs#readme for more supported runtimes -# gem 'therubyracer', platforms: :ruby - -# Use jquery as the JavaScript library -gem 'jquery-rails' -# Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks -gem 'turbolinks' -# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder -gem 'jbuilder', '~> 2.0' -# bundle exec rake doc:rails generates the API under doc/api. -gem 'sdoc', '~> 0.4.0', group: :doc - -# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring -gem 'spring', group: :development - -# Use ActiveModel has_secure_password -# gem 'bcrypt', '~> 3.1.7' - -# Use unicorn as the app server -# gem 'unicorn' - -# Use Capistrano for deployment -# gem 'capistrano-rails', group: :development - -# Use debugger -# gem 'debugger', group: [:development, :test] - diff --git a/guides/code/getting_started/Gemfile.lock b/guides/code/getting_started/Gemfile.lock deleted file mode 100644 index a2ab76c908..0000000000 --- a/guides/code/getting_started/Gemfile.lock +++ /dev/null @@ -1,126 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - actionmailer (4.1.0) - actionpack (= 4.1.0) - actionview (= 4.1.0) - mail (~> 2.5.4) - actionpack (4.1.0) - actionview (= 4.1.0) - activesupport (= 4.1.0) - rack (~> 1.5.2) - rack-test (~> 0.6.2) - actionview (4.1.0) - activesupport (= 4.1.0) - builder (~> 3.1) - erubis (~> 2.7.0) - activemodel (4.1.0) - activesupport (= 4.1.0) - builder (~> 3.1) - activerecord (4.1.0) - activemodel (= 4.1.0) - activesupport (= 4.1.0) - arel (~> 5.0.0) - activesupport (4.1.0) - i18n (~> 0.6, >= 0.6.9) - json (~> 1.7, >= 1.7.7) - minitest (~> 5.1) - thread_safe (~> 0.1) - tzinfo (~> 1.1) - arel (5.0.0) - atomic (1.1.14) - builder (3.2.2) - coffee-rails (4.0.1) - coffee-script (>= 2.2.0) - railties (>= 4.0.0, < 5.0) - coffee-script (2.2.0) - coffee-script-source - execjs - coffee-script-source (1.6.3) - erubis (2.7.0) - execjs (2.0.2) - hike (1.2.3) - i18n (0.6.9) - jbuilder (2.0.2) - activesupport (>= 3.0.0) - multi_json (>= 1.2.0) - jquery-rails (3.0.4) - railties (>= 3.0, < 5.0) - thor (>= 0.14, < 2.0) - json (1.8.1) - mail (2.5.4) - mime-types (~> 1.16) - treetop (~> 1.4.8) - mime-types (1.25.1) - minitest (5.2.1) - multi_json (1.8.4) - polyglot (0.3.3) - rack (1.5.2) - rack-test (0.6.2) - rack (>= 1.0) - rails (4.1.0) - actionmailer (= 4.1.0) - actionpack (= 4.1.0) - actionview (= 4.1.0) - activemodel (= 4.1.0) - activerecord (= 4.1.0) - activesupport (= 4.1.0) - bundler (>= 1.3.0, < 2.0) - railties (= 4.1.0) - sprockets-rails (~> 2.0.0) - railties (4.1.0) - actionpack (= 4.1.0) - activesupport (= 4.1.0) - rake (>= 0.8.7) - thor (>= 0.18.1, < 2.0) - rake (10.1.1) - rdoc (4.1.1) - json (~> 1.4) - sass (3.2.13) - sass-rails (4.0.1) - railties (>= 4.0.0, < 5.0) - sass (>= 3.1.10) - sprockets-rails (~> 2.0.0) - sdoc (0.4.0) - json (~> 1.8) - rdoc (~> 4.0, < 5.0) - spring (1.0.0) - sprockets (2.10.1) - hike (~> 1.2) - multi_json (~> 1.0) - rack (~> 1.0) - tilt (~> 1.1, != 1.3.0) - sprockets-rails (2.0.1) - actionpack (>= 3.0) - activesupport (>= 3.0) - sprockets (~> 2.8) - sqlite3 (1.3.8) - thor (0.18.1) - thread_safe (0.1.3) - atomic - tilt (1.4.1) - treetop (1.4.15) - polyglot - polyglot (>= 0.3.1) - turbolinks (2.2.0) - coffee-rails - tzinfo (1.1.0) - thread_safe (~> 0.1) - uglifier (2.4.0) - execjs (>= 0.3.0) - json (>= 1.8.0) - -PLATFORMS - ruby - -DEPENDENCIES - coffee-rails (~> 4.0.0) - jbuilder (~> 2.0) - jquery-rails - rails (= 4.1.0) - sass-rails (~> 4.0.1) - sdoc (~> 0.4.0) - spring - sqlite3 - turbolinks - uglifier (>= 1.3.0) diff --git a/guides/code/getting_started/README.rdoc b/guides/code/getting_started/README.rdoc deleted file mode 100644 index dd4e97e22e..0000000000 --- a/guides/code/getting_started/README.rdoc +++ /dev/null @@ -1,28 +0,0 @@ -== README - -This README would normally document whatever steps are necessary to get the -application up and running. - -Things you may want to cover: - -* Ruby version - -* System dependencies - -* Configuration - -* Database creation - -* Database initialization - -* How to run the test suite - -* Services (job queues, cache servers, search engines, etc.) - -* Deployment instructions - -* ... - - -Please feel free to use a different markup language if you do not plan to run -<tt>rake doc:app</tt>. diff --git a/guides/code/getting_started/Rakefile b/guides/code/getting_started/Rakefile deleted file mode 100644 index ba6b733dd2..0000000000 --- a/guides/code/getting_started/Rakefile +++ /dev/null @@ -1,6 +0,0 @@ -# Add your own tasks in files placed in lib/tasks ending in .rake, -# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. - -require File.expand_path('../config/application', __FILE__) - -Rails.application.load_tasks diff --git a/guides/code/getting_started/app/assets/javascripts/application.js b/guides/code/getting_started/app/assets/javascripts/application.js deleted file mode 100644 index 5a4fbaa370..0000000000 --- a/guides/code/getting_started/app/assets/javascripts/application.js +++ /dev/null @@ -1,15 +0,0 @@ -// This is a manifest file that'll be compiled into application.js, which will include all the files -// listed below. -// -// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, -// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. -// -// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -// compiled file. -// -// stub path allows dependency to be excluded from the asset bundle. -// -//= require jquery -//= require jquery_ujs -//= require turbolinks -//= require_tree . diff --git a/guides/code/getting_started/app/assets/javascripts/comments.js.coffee b/guides/code/getting_started/app/assets/javascripts/comments.js.coffee deleted file mode 100644 index 24f83d18bb..0000000000 --- a/guides/code/getting_started/app/assets/javascripts/comments.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/guides/code/getting_started/app/assets/javascripts/posts.js.coffee b/guides/code/getting_started/app/assets/javascripts/posts.js.coffee deleted file mode 100644 index 24f83d18bb..0000000000 --- a/guides/code/getting_started/app/assets/javascripts/posts.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/guides/code/getting_started/app/assets/javascripts/welcome.js.coffee b/guides/code/getting_started/app/assets/javascripts/welcome.js.coffee deleted file mode 100644 index 24f83d18bb..0000000000 --- a/guides/code/getting_started/app/assets/javascripts/welcome.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/guides/code/getting_started/app/assets/stylesheets/application.css b/guides/code/getting_started/app/assets/stylesheets/application.css deleted file mode 100644 index 3192ec897b..0000000000 --- a/guides/code/getting_started/app/assets/stylesheets/application.css +++ /dev/null @@ -1,13 +0,0 @@ -/* - * This is a manifest file that'll be compiled into application.css, which will include all the files - * listed below. - * - * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, - * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. - * - * You're free to add application-wide styles to this file and they'll appear at the top of the - * compiled file, but it's generally better to create a new file per style scope. - * - *= require_self - *= require_tree . - */ diff --git a/guides/code/getting_started/app/assets/stylesheets/comments.css.scss b/guides/code/getting_started/app/assets/stylesheets/comments.css.scss deleted file mode 100644 index e730912783..0000000000 --- a/guides/code/getting_started/app/assets/stylesheets/comments.css.scss +++ /dev/null @@ -1,3 +0,0 @@ -// Place all the styles related to the Comments controller here. -// They will automatically be included in application.css. -// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/guides/code/getting_started/app/assets/stylesheets/posts.css.scss b/guides/code/getting_started/app/assets/stylesheets/posts.css.scss deleted file mode 100644 index 1a7e15390c..0000000000 --- a/guides/code/getting_started/app/assets/stylesheets/posts.css.scss +++ /dev/null @@ -1,3 +0,0 @@ -// Place all the styles related to the posts controller here. -// They will automatically be included in application.css. -// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/guides/code/getting_started/app/assets/stylesheets/welcome.css.scss b/guides/code/getting_started/app/assets/stylesheets/welcome.css.scss deleted file mode 100644 index 77ce11a740..0000000000 --- a/guides/code/getting_started/app/assets/stylesheets/welcome.css.scss +++ /dev/null @@ -1,3 +0,0 @@ -// Place all the styles related to the welcome controller here. -// They will automatically be included in application.css. -// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/guides/code/getting_started/app/controllers/application_controller.rb b/guides/code/getting_started/app/controllers/application_controller.rb deleted file mode 100644 index d83690e1b9..0000000000 --- a/guides/code/getting_started/app/controllers/application_controller.rb +++ /dev/null @@ -1,5 +0,0 @@ -class ApplicationController < ActionController::Base - # Prevent CSRF attacks by raising an exception. - # For APIs, you may want to use :null_session instead. - protect_from_forgery with: :exception -end diff --git a/guides/code/getting_started/app/controllers/comments_controller.rb b/guides/code/getting_started/app/controllers/comments_controller.rb deleted file mode 100644 index b2d9bcdf7f..0000000000 --- a/guides/code/getting_started/app/controllers/comments_controller.rb +++ /dev/null @@ -1,23 +0,0 @@ -class CommentsController < ApplicationController - - http_basic_authenticate_with name: "dhh", password: "secret", only: :destroy - - def create - @post = Post.find(params[:post_id]) - @comment = @post.comments.create(comment_params) - redirect_to post_path(@post) - end - - def destroy - @post = Post.find(params[:post_id]) - @comment = @post.comments.find(params[:id]) - @comment.destroy - redirect_to post_path(@post) - end - - private - - def comment_params - params.require(:comment).permit(:commenter, :body) - end -end diff --git a/guides/code/getting_started/app/controllers/concerns/.keep b/guides/code/getting_started/app/controllers/concerns/.keep deleted file mode 100644 index e69de29bb2..0000000000 --- a/guides/code/getting_started/app/controllers/concerns/.keep +++ /dev/null diff --git a/guides/code/getting_started/app/controllers/posts_controller.rb b/guides/code/getting_started/app/controllers/posts_controller.rb deleted file mode 100644 index 02689ad67b..0000000000 --- a/guides/code/getting_started/app/controllers/posts_controller.rb +++ /dev/null @@ -1,53 +0,0 @@ -class PostsController < ApplicationController - - http_basic_authenticate_with name: "dhh", password: "secret", except: [:index, :show] - - def index - @posts = Post.all - end - - def show - @post = Post.find(params[:id]) - end - - def edit - @post = Post.find(params[:id]) - end - - def update - @post = Post.find(params[:id]) - - if @post.update(post_params) - redirect_to action: :show, id: @post.id - else - render 'edit' - end - end - - def new - @post = Post.new - end - - def create - @post = Post.new(post_params) - - if @post.save - redirect_to action: :show, id: @post.id - else - render 'new' - end - end - - def destroy - @post = Post.find(params[:id]) - @post.destroy - - redirect_to action: :index - end - - private - - def post_params - params.require(:post).permit(:title, :text) - end -end diff --git a/guides/code/getting_started/app/controllers/welcome_controller.rb b/guides/code/getting_started/app/controllers/welcome_controller.rb deleted file mode 100644 index f9b859b9c9..0000000000 --- a/guides/code/getting_started/app/controllers/welcome_controller.rb +++ /dev/null @@ -1,4 +0,0 @@ -class WelcomeController < ApplicationController - def index - end -end diff --git a/guides/code/getting_started/app/helpers/application_helper.rb b/guides/code/getting_started/app/helpers/application_helper.rb deleted file mode 100644 index de6be7945c..0000000000 --- a/guides/code/getting_started/app/helpers/application_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module ApplicationHelper -end diff --git a/guides/code/getting_started/app/helpers/comments_helper.rb b/guides/code/getting_started/app/helpers/comments_helper.rb deleted file mode 100644 index 0ec9ca5f2d..0000000000 --- a/guides/code/getting_started/app/helpers/comments_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module CommentsHelper -end diff --git a/guides/code/getting_started/app/helpers/posts_helper.rb b/guides/code/getting_started/app/helpers/posts_helper.rb deleted file mode 100644 index a7b8cec898..0000000000 --- a/guides/code/getting_started/app/helpers/posts_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module PostsHelper -end diff --git a/guides/code/getting_started/app/helpers/welcome_helper.rb b/guides/code/getting_started/app/helpers/welcome_helper.rb deleted file mode 100644 index eeead45fc9..0000000000 --- a/guides/code/getting_started/app/helpers/welcome_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module WelcomeHelper -end diff --git a/guides/code/getting_started/app/mailers/.keep b/guides/code/getting_started/app/mailers/.keep deleted file mode 100644 index e69de29bb2..0000000000 --- a/guides/code/getting_started/app/mailers/.keep +++ /dev/null diff --git a/guides/code/getting_started/app/models/.keep b/guides/code/getting_started/app/models/.keep deleted file mode 100644 index e69de29bb2..0000000000 --- a/guides/code/getting_started/app/models/.keep +++ /dev/null diff --git a/guides/code/getting_started/app/models/comment.rb b/guides/code/getting_started/app/models/comment.rb deleted file mode 100644 index 4e76c5b5b0..0000000000 --- a/guides/code/getting_started/app/models/comment.rb +++ /dev/null @@ -1,3 +0,0 @@ -class Comment < ActiveRecord::Base - belongs_to :post -end diff --git a/guides/code/getting_started/app/models/concerns/.keep b/guides/code/getting_started/app/models/concerns/.keep deleted file mode 100644 index e69de29bb2..0000000000 --- a/guides/code/getting_started/app/models/concerns/.keep +++ /dev/null diff --git a/guides/code/getting_started/app/models/post.rb b/guides/code/getting_started/app/models/post.rb deleted file mode 100644 index 64e0d721fd..0000000000 --- a/guides/code/getting_started/app/models/post.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Post < ActiveRecord::Base - has_many :comments, dependent: :destroy - - validates :title, - presence: true, - length: { minimum: 5 } -end diff --git a/guides/code/getting_started/app/views/comments/_comment.html.erb b/guides/code/getting_started/app/views/comments/_comment.html.erb deleted file mode 100644 index 593493339e..0000000000 --- a/guides/code/getting_started/app/views/comments/_comment.html.erb +++ /dev/null @@ -1,15 +0,0 @@ -<p> - <strong>Commenter:</strong> - <%= comment.commenter %> -</p> - -<p> - <strong>Comment:</strong> - <%= comment.body %> -</p> - -<p> - <%= link_to 'Destroy Comment', [comment.post, comment], - method: :delete, - data: { confirm: 'Are you sure?' } %> -</p> diff --git a/guides/code/getting_started/app/views/comments/_form.html.erb b/guides/code/getting_started/app/views/comments/_form.html.erb deleted file mode 100644 index 00cb3a08f0..0000000000 --- a/guides/code/getting_started/app/views/comments/_form.html.erb +++ /dev/null @@ -1,13 +0,0 @@ -<%= form_for([@post, @post.comments.build]) do |f| %> - <p> - <%= f.label :commenter %><br /> - <%= f.text_field :commenter %> - </p> - <p> - <%= f.label :body %><br /> - <%= f.text_area :body %> - </p> - <p> - <%= f.submit %> - </p> -<% end %> diff --git a/guides/code/getting_started/app/views/layouts/application.html.erb b/guides/code/getting_started/app/views/layouts/application.html.erb deleted file mode 100644 index d0ba8415e6..0000000000 --- a/guides/code/getting_started/app/views/layouts/application.html.erb +++ /dev/null @@ -1,14 +0,0 @@ -<!DOCTYPE html> -<html> -<head> - <title>Blog</title> - <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> - <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> - <%= csrf_meta_tags %> -</head> -<body> - -<%= yield %> - -</body> -</html> diff --git a/guides/code/getting_started/app/views/posts/_form.html.erb b/guides/code/getting_started/app/views/posts/_form.html.erb deleted file mode 100644 index f2f83585e1..0000000000 --- a/guides/code/getting_started/app/views/posts/_form.html.erb +++ /dev/null @@ -1,27 +0,0 @@ -<%= form_for @post do |f| %> - <% if @post.errors.any? %> - <div id="error_explanation"> - <h2><%= pluralize(@post.errors.count, "error") %> prohibited - this post from being saved:</h2> - <ul> - <% @post.errors.full_messages.each do |msg| %> - <li><%= msg %></li> - <% end %> - </ul> - </div> - <% end %> - <p> - <%= f.label :title %><br> - <%= f.text_field :title %> - </p> - - <p> - <%= f.label :text %><br> - <%= f.text_area :text %> - </p> - - <p> - <%= f.submit %> - </p> -<% end %> - diff --git a/guides/code/getting_started/app/views/posts/edit.html.erb b/guides/code/getting_started/app/views/posts/edit.html.erb deleted file mode 100644 index 393e7430d0..0000000000 --- a/guides/code/getting_started/app/views/posts/edit.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<h1>Edit post</h1> - -<%= render 'form' %> - -<%= link_to 'Back', action: :index %> diff --git a/guides/code/getting_started/app/views/posts/index.html.erb b/guides/code/getting_started/app/views/posts/index.html.erb deleted file mode 100644 index 7369f0396f..0000000000 --- a/guides/code/getting_started/app/views/posts/index.html.erb +++ /dev/null @@ -1,21 +0,0 @@ -<h1>Listing Posts</h1> -<table> - <tr> - <th>Title</th> - <th>Text</th> - <th></th> - <th></th> - <th></th> - </tr> - -<% @posts.each do |post| %> - <tr> - <td><%= post.title %></td> - <td><%= post.text %></td> - <td><%= link_to 'Show', action: :show, id: post.id %></td> - <td><%= link_to 'Edit', action: :edit, id: post.id %></td> - <td><%= link_to 'Destroy', { action: :destroy, id: post.id }, - method: :delete, data: { confirm: 'Are you sure?' } %></td> - </tr> -<% end %> -</table> diff --git a/guides/code/getting_started/app/views/posts/new.html.erb b/guides/code/getting_started/app/views/posts/new.html.erb deleted file mode 100644 index efa81038ec..0000000000 --- a/guides/code/getting_started/app/views/posts/new.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<h1>New post</h1> - -<%= render 'form' %> - -<%= link_to 'Back', action: :index %> diff --git a/guides/code/getting_started/app/views/posts/show.html.erb b/guides/code/getting_started/app/views/posts/show.html.erb deleted file mode 100644 index e99e9edbb3..0000000000 --- a/guides/code/getting_started/app/views/posts/show.html.erb +++ /dev/null @@ -1,18 +0,0 @@ -<p> - <strong>Title:</strong> - <%= @post.title %> -</p> - -<p> - <strong>Text:</strong> - <%= @post.text %> -</p> - -<h2>Comments</h2> -<%= render @post.comments %> - -<h2>Add a comment:</h2> -<%= render "comments/form" %> - -<%= link_to 'Edit Post', edit_post_path(@post) %> | -<%= link_to 'Back to Posts', posts_path %> diff --git a/guides/code/getting_started/app/views/welcome/index.html.erb b/guides/code/getting_started/app/views/welcome/index.html.erb deleted file mode 100644 index 56be8dd3cc..0000000000 --- a/guides/code/getting_started/app/views/welcome/index.html.erb +++ /dev/null @@ -1,4 +0,0 @@ -<h1>Hello, Rails!</h1> - -<%= link_to "My Blog", controller: "posts" %> -<%= link_to "New Post", new_post_path %> diff --git a/guides/code/getting_started/bin/bundle b/guides/code/getting_started/bin/bundle deleted file mode 100755 index 45cf37fba4..0000000000 --- a/guides/code/getting_started/bin/bundle +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env ruby -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) -require 'rubygems' -load Gem.bin_path('bundler', 'bundle') diff --git a/guides/code/getting_started/bin/rails b/guides/code/getting_started/bin/rails deleted file mode 100755 index 728cd85aa5..0000000000 --- a/guides/code/getting_started/bin/rails +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env ruby -APP_PATH = File.expand_path('../../config/application', __FILE__) -require_relative '../config/boot' -require 'rails/commands' diff --git a/guides/code/getting_started/bin/rake b/guides/code/getting_started/bin/rake deleted file mode 100755 index 17240489f6..0000000000 --- a/guides/code/getting_started/bin/rake +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env ruby -require_relative '../config/boot' -require 'rake' -Rake.application.run diff --git a/guides/code/getting_started/config.ru b/guides/code/getting_started/config.ru deleted file mode 100644 index 5bc2a619e8..0000000000 --- a/guides/code/getting_started/config.ru +++ /dev/null @@ -1,4 +0,0 @@ -# This file is used by Rack-based servers to start the application. - -require ::File.expand_path('../config/environment', __FILE__) -run Rails.application diff --git a/guides/code/getting_started/config/application.rb b/guides/code/getting_started/config/application.rb deleted file mode 100644 index 3d7604b659..0000000000 --- a/guides/code/getting_started/config/application.rb +++ /dev/null @@ -1,18 +0,0 @@ -require File.expand_path('../boot', __FILE__) - -require 'rails/all' - -# Require the gems listed in Gemfile, including any gems -# you've limited to :test, :development, or :production. -Bundler.require(:default, Rails.env) - -module Blog - class Application < Rails::Application - # Settings in config/environments/* take precedence over those specified here. - # Application configuration should go into files in config/initializers - # -- all .rb files in that directory are automatically loaded. - - # Custom directories with classes and modules you want to be autoloadable. - # config.autoload_paths += %W(#{config.root}/extras) - end -end diff --git a/guides/code/getting_started/config/boot.rb b/guides/code/getting_started/config/boot.rb deleted file mode 100644 index 5e5f0c1fac..0000000000 --- a/guides/code/getting_started/config/boot.rb +++ /dev/null @@ -1,4 +0,0 @@ -# Set up gems listed in the Gemfile. -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) - -require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) diff --git a/guides/code/getting_started/config/database.yml b/guides/code/getting_started/config/database.yml deleted file mode 100644 index 51a4dd459d..0000000000 --- a/guides/code/getting_started/config/database.yml +++ /dev/null @@ -1,25 +0,0 @@ -# SQLite version 3.x -# gem install sqlite3 -# -# Ensure the SQLite 3 gem is defined in your Gemfile -# gem 'sqlite3' -development: - adapter: sqlite3 - database: db/development.sqlite3 - pool: 5 - timeout: 5000 - -# 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 - database: db/test.sqlite3 - pool: 5 - timeout: 5000 - -production: - adapter: sqlite3 - database: db/production.sqlite3 - pool: 5 - timeout: 5000 diff --git a/guides/code/getting_started/config/environment.rb b/guides/code/getting_started/config/environment.rb deleted file mode 100644 index ee8d90dc65..0000000000 --- a/guides/code/getting_started/config/environment.rb +++ /dev/null @@ -1,5 +0,0 @@ -# Load the Rails application. -require File.expand_path('../application', __FILE__) - -# Initialize the Rails application. -Rails.application.initialize! diff --git a/guides/code/getting_started/config/environments/development.rb b/guides/code/getting_started/config/environments/development.rb deleted file mode 100644 index ae9ffe209a..0000000000 --- a/guides/code/getting_started/config/environments/development.rb +++ /dev/null @@ -1,30 +0,0 @@ -Rails.application.configure do - # Settings specified here will take precedence over those in config/application.rb. - - # In the development environment your application's code is reloaded on - # every request. This slows down response time but is perfect for development - # since you don't have to restart the web server when you make code changes. - config.cache_classes = false - - # Do not eager load code on boot. - config.eager_load = false - - # Show full error reports and disable caching. - config.consider_all_requests_local = true - config.action_controller.perform_caching = false - - # Don't care if the mailer can't send. - config.action_mailer.raise_delivery_errors = false - - # Print deprecation notices to the Rails logger. - config.active_support.deprecation = :log - - # Only use best-standards-support built into browsers. - config.action_dispatch.best_standards_support = :builtin - - # Raise an error on page load if there are pending migrations. - config.active_record.migration_error = :page_load - - # Debug mode disables concatenation and preprocessing of assets. - config.assets.debug = true -end diff --git a/guides/code/getting_started/config/environments/production.rb b/guides/code/getting_started/config/environments/production.rb deleted file mode 100644 index c8ae858574..0000000000 --- a/guides/code/getting_started/config/environments/production.rb +++ /dev/null @@ -1,80 +0,0 @@ -Rails.application.configure do - # Settings specified here will take precedence over those in config/application.rb. - - # Code is not reloaded between requests. - config.cache_classes = true - - # Eager load code on boot. This eager loads most of Rails and - # your application in memory, allowing both threaded web servers - # and those relying on copy on write to perform better. - # Rake tasks automatically ignore this option for performance. - config.eager_load = true - - # Full error reports are disabled and caching is turned on. - config.consider_all_requests_local = false - config.action_controller.perform_caching = true - - # Enable Rack::Cache to put a simple HTTP cache in front of your application - # Add `rack-cache` to your Gemfile before enabling this. - # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. - # config.action_dispatch.rack_cache = true - - # Disable Rails's static asset server (Apache or nginx will already do this). - config.serve_static_assets = false - - # Compress JavaScripts and CSS. - config.assets.js_compressor = :uglifier - # config.assets.css_compressor = :sass - - # Whether to fallback to assets pipeline if a precompiled asset is missed. - config.assets.compile = false - - # Generate digests for assets URLs. - config.assets.digest = true - - # Version of your assets, change this if you want to expire all your assets. - config.assets.version = '1.0' - - # Specifies the header that your server uses for sending files. - # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache - # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx - - # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - # config.force_ssl = true - - # Set to :debug to see everything in the log. - config.log_level = :info - - # Prepend all log lines with the following tags. - # config.log_tags = [ :subdomain, :uuid ] - - # Use a different logger for distributed setups. - # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) - - # Use a different cache store in production. - # config.cache_store = :mem_cache_store - - # Enable serving of images, stylesheets, and JavaScripts from an asset server. - # config.action_controller.asset_host = "http://assets.example.com" - - # Precompile additional assets. - # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. - # config.assets.precompile += %w( search.js ) - - # Ignore bad email addresses and do not raise email delivery errors. - # Set this to true and configure the email server for immediate delivery to raise delivery errors. - # config.action_mailer.raise_delivery_errors = false - - # Enable locale fallbacks for I18n (makes lookups for any locale fall back to - # the I18n.default_locale when a translation cannot be found). - config.i18n.fallbacks = true - - # Send deprecation notices to registered listeners. - config.active_support.deprecation = :notify - - # Disable automatic flushing of the log to improve performance. - # config.autoflush_log = false - - # Use default logging formatter so that PID and timestamp are not suppressed. - config.log_formatter = ::Logger::Formatter.new -end diff --git a/guides/code/getting_started/config/environments/test.rb b/guides/code/getting_started/config/environments/test.rb deleted file mode 100644 index 680d0b9e06..0000000000 --- a/guides/code/getting_started/config/environments/test.rb +++ /dev/null @@ -1,36 +0,0 @@ -Rails.application.configure do - # Settings specified here will take precedence over those in config/application.rb. - - # The test environment is used exclusively to run your application's - # test suite. You never need to work with it otherwise. Remember that - # your test database is "scratch space" for the test suite and is wiped - # and recreated between test runs. Don't rely on the data there! - config.cache_classes = true - - # Do not eager load code on boot. This avoids loading your whole application - # just for the purpose of running a single test. If you are using a tool that - # preloads Rails for running tests, you may have to set it to true. - config.eager_load = false - - # Configure static asset server for tests with Cache-Control for performance. - config.serve_static_assets = true - config.static_cache_control = 'public, max-age=3600' - - # Show full error reports and disable caching. - config.consider_all_requests_local = true - config.action_controller.perform_caching = false - - # Raise exceptions instead of rendering exception templates. - config.action_dispatch.show_exceptions = false - - # Disable request forgery protection in test environment. - config.action_controller.allow_forgery_protection = false - - # Tell Action Mailer not to deliver emails to the real world. - # The :test delivery method accumulates sent emails in the - # ActionMailer::Base.deliveries array. - config.action_mailer.delivery_method = :test - - # Print deprecation notices to the stderr. - config.active_support.deprecation = :stderr -end diff --git a/guides/code/getting_started/config/initializers/backtrace_silencers.rb b/guides/code/getting_started/config/initializers/backtrace_silencers.rb deleted file mode 100644 index 59385cdf37..0000000000 --- a/guides/code/getting_started/config/initializers/backtrace_silencers.rb +++ /dev/null @@ -1,7 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. -# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } - -# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. -# Rails.backtrace_cleaner.remove_silencers! diff --git a/guides/code/getting_started/config/initializers/filter_parameter_logging.rb b/guides/code/getting_started/config/initializers/filter_parameter_logging.rb deleted file mode 100644 index 4a994e1e7b..0000000000 --- a/guides/code/getting_started/config/initializers/filter_parameter_logging.rb +++ /dev/null @@ -1,4 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Configure sensitive parameters which will be filtered from the log file. -Rails.application.config.filter_parameters += [:password] diff --git a/guides/code/getting_started/config/initializers/inflections.rb b/guides/code/getting_started/config/initializers/inflections.rb deleted file mode 100644 index ac033bf9dc..0000000000 --- a/guides/code/getting_started/config/initializers/inflections.rb +++ /dev/null @@ -1,16 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Add new inflection rules using the following format. Inflections -# are locale specific, and you may define rules for as many different -# locales as you wish. All of these examples are active by default: -# ActiveSupport::Inflector.inflections(:en) do |inflect| -# inflect.plural /^(ox)$/i, '\1en' -# inflect.singular /^(ox)en/i, '\1' -# inflect.irregular 'person', 'people' -# inflect.uncountable %w( fish sheep ) -# end - -# These inflection rules are supported but not enabled by default: -# ActiveSupport::Inflector.inflections(:en) do |inflect| -# inflect.acronym 'RESTful' -# end diff --git a/guides/code/getting_started/config/initializers/locale.rb b/guides/code/getting_started/config/initializers/locale.rb deleted file mode 100644 index d89dac7c6a..0000000000 --- a/guides/code/getting_started/config/initializers/locale.rb +++ /dev/null @@ -1,9 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. -# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. -# Rails.application.config.time_zone = 'Central Time (US & Canada)' - -# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. -# Rails.application.config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] -# Rails.application.config.i18n.default_locale = :de diff --git a/guides/code/getting_started/config/initializers/mime_types.rb b/guides/code/getting_started/config/initializers/mime_types.rb deleted file mode 100644 index 72aca7e441..0000000000 --- a/guides/code/getting_started/config/initializers/mime_types.rb +++ /dev/null @@ -1,5 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Add new mime types for use in respond_to blocks: -# Mime::Type.register "text/richtext", :rtf -# Mime::Type.register_alias "text/html", :iphone diff --git a/guides/code/getting_started/config/initializers/secret_token.rb b/guides/code/getting_started/config/initializers/secret_token.rb deleted file mode 100644 index c2a549c299..0000000000 --- a/guides/code/getting_started/config/initializers/secret_token.rb +++ /dev/null @@ -1,12 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Your secret key for verifying the integrity of signed cookies. -# If you change this key, all old signed cookies will become invalid! - -# Make sure the secret is at least 30 characters and all random, -# no regular words or you'll be exposed to dictionary attacks. -# You can use `rake secret` to generate a secure secret key. - -# Make sure your secret_key_base is kept private -# if you're sharing your code publicly. -Rails.application.config.secret_key_base = 'e8aab50cec8a06a75694111a4cbaf6e22fc288ccbc6b268683aae7273043c69b15ca07d10c92a788dd6077a54762cbfcc55f19c3459f7531221b3169f8171a53' diff --git a/guides/code/getting_started/config/initializers/session_store.rb b/guides/code/getting_started/config/initializers/session_store.rb deleted file mode 100644 index 1b9fa324d4..0000000000 --- a/guides/code/getting_started/config/initializers/session_store.rb +++ /dev/null @@ -1,3 +0,0 @@ -# Be sure to restart your server when you modify this file. - -Rails.application.config.session_store :cookie_store, key: '_blog_session' diff --git a/guides/code/getting_started/config/initializers/wrap_parameters.rb b/guides/code/getting_started/config/initializers/wrap_parameters.rb deleted file mode 100644 index 33725e95fd..0000000000 --- a/guides/code/getting_started/config/initializers/wrap_parameters.rb +++ /dev/null @@ -1,14 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# This file contains settings for ActionController::ParamsWrapper which -# is enabled by default. - -# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. -ActiveSupport.on_load(:action_controller) do - wrap_parameters format: [:json] if respond_to?(:wrap_parameters) -end - -# To enable root element in JSON for ActiveRecord objects. -# ActiveSupport.on_load(:active_record) do -# self.include_root_in_json = true -# end diff --git a/guides/code/getting_started/config/locales/en.yml b/guides/code/getting_started/config/locales/en.yml deleted file mode 100644 index 0653957166..0000000000 --- a/guides/code/getting_started/config/locales/en.yml +++ /dev/null @@ -1,23 +0,0 @@ -# Files in the config/locales directory are used for internationalization -# and are automatically loaded by Rails. If you want to use locales other -# than English, add the necessary files in this directory. -# -# To use the locales, use `I18n.t`: -# -# I18n.t 'hello' -# -# In views, this is aliased to just `t`: -# -# <%= t('hello') %> -# -# To use a different locale, set it with `I18n.locale`: -# -# I18n.locale = :es -# -# This would use the information in config/locales/es.yml. -# -# To learn more, please read the Rails Internationalization guide -# available at http://guides.rubyonrails.org/i18n.html. - -en: - hello: "Hello world" diff --git a/guides/code/getting_started/config/routes.rb b/guides/code/getting_started/config/routes.rb deleted file mode 100644 index 65d273b58d..0000000000 --- a/guides/code/getting_started/config/routes.rb +++ /dev/null @@ -1,7 +0,0 @@ -Rails.application.routes.draw do - resources :posts do - resources :comments - end - - root "welcome#index" -end diff --git a/guides/code/getting_started/db/migrate/20130122042648_create_posts.rb b/guides/code/getting_started/db/migrate/20130122042648_create_posts.rb deleted file mode 100644 index 602bef31ab..0000000000 --- a/guides/code/getting_started/db/migrate/20130122042648_create_posts.rb +++ /dev/null @@ -1,10 +0,0 @@ -class CreatePosts < ActiveRecord::Migration - def change - create_table :posts do |t| - t.string :title - t.text :text - - t.timestamps - end - end -end diff --git a/guides/code/getting_started/db/migrate/20130122045842_create_comments.rb b/guides/code/getting_started/db/migrate/20130122045842_create_comments.rb deleted file mode 100644 index 3e51f9c0f7..0000000000 --- a/guides/code/getting_started/db/migrate/20130122045842_create_comments.rb +++ /dev/null @@ -1,11 +0,0 @@ -class CreateComments < ActiveRecord::Migration - def change - create_table :comments do |t| - t.string :commenter - t.text :body - t.references :post, index: true - - t.timestamps - end - end -end diff --git a/guides/code/getting_started/db/schema.rb b/guides/code/getting_started/db/schema.rb deleted file mode 100644 index 101fe712a1..0000000000 --- a/guides/code/getting_started/db/schema.rb +++ /dev/null @@ -1,33 +0,0 @@ -# encoding: UTF-8 -# This file is auto-generated from the current state of the database. Instead -# of editing this file, please use the migrations feature of Active Record to -# incrementally modify your database, and then regenerate this schema definition. -# -# Note that this schema.rb definition is the authoritative source for your -# database schema. If you need to create the application database on another -# system, you should be using db:schema:load, not running all the migrations -# from scratch. The latter is a flawed and unsustainable approach (the more migrations -# you'll amass, the slower it'll run and the greater likelihood for issues). -# -# It's strongly recommended that you check this file into your version control system. - -ActiveRecord::Schema.define(version: 20130122045842) do - - create_table "comments", force: true do |t| - t.string "commenter" - t.text "body" - t.integer "post_id" - t.datetime "created_at" - t.datetime "updated_at" - end - - add_index "comments", ["post_id"], name: "index_comments_on_post_id" - - create_table "posts", force: true do |t| - t.string "title" - t.text "text" - t.datetime "created_at" - t.datetime "updated_at" - end - -end diff --git a/guides/code/getting_started/db/seeds.rb b/guides/code/getting_started/db/seeds.rb deleted file mode 100644 index 4edb1e857e..0000000000 --- a/guides/code/getting_started/db/seeds.rb +++ /dev/null @@ -1,7 +0,0 @@ -# This file should contain all the record creation needed to seed the database with its default values. -# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). -# -# Examples: -# -# cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) -# Mayor.create(name: 'Emanuel', city: cities.first) diff --git a/guides/code/getting_started/lib/assets/.keep b/guides/code/getting_started/lib/assets/.keep deleted file mode 100644 index e69de29bb2..0000000000 --- a/guides/code/getting_started/lib/assets/.keep +++ /dev/null diff --git a/guides/code/getting_started/lib/tasks/.keep b/guides/code/getting_started/lib/tasks/.keep deleted file mode 100644 index e69de29bb2..0000000000 --- a/guides/code/getting_started/lib/tasks/.keep +++ /dev/null diff --git a/guides/code/getting_started/log/.keep b/guides/code/getting_started/log/.keep deleted file mode 100644 index e69de29bb2..0000000000 --- a/guides/code/getting_started/log/.keep +++ /dev/null diff --git a/guides/code/getting_started/public/404.html b/guides/code/getting_started/public/404.html deleted file mode 100644 index 3265cc8e33..0000000000 --- a/guides/code/getting_started/public/404.html +++ /dev/null @@ -1,60 +0,0 @@ -<!DOCTYPE html> -<html> -<head> - <title>The page you were looking for doesn't exist (404)</title> - <style> - body { - background-color: #EFEFEF; - color: #2E2F30; - text-align: center; - font-family: arial, sans-serif; - } - - div.dialog { - width: 25em; - margin: 4em auto 0 auto; - border: 1px solid #CCC; - border-right-color: #999; - border-left-color: #999; - border-bottom-color: #BBB; - border-top: #B00100 solid 4px; - border-top-left-radius: 9px; - 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 { - font-size: 100%; - color: #730E15; - line-height: 1.5em; - } - - body > p { - width: 33em; - margin: 0 auto 1em; - padding: 1em 0; - 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); - } - </style> -</head> - -<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> - <p>If you are the application owner check the logs for more information.</p> -</body> -</html> diff --git a/guides/code/getting_started/public/422.html b/guides/code/getting_started/public/422.html deleted file mode 100644 index d823a8fc77..0000000000 --- a/guides/code/getting_started/public/422.html +++ /dev/null @@ -1,60 +0,0 @@ -<!DOCTYPE html> -<html> -<head> - <title>The change you wanted was rejected (422)</title> - <style> - body { - background-color: #EFEFEF; - color: #2E2F30; - text-align: center; - font-family: arial, sans-serif; - } - - div.dialog { - width: 25em; - margin: 4em auto 0 auto; - border: 1px solid #CCC; - border-right-color: #999; - border-left-color: #999; - border-bottom-color: #BBB; - border-top: #B00100 solid 4px; - border-top-left-radius: 9px; - 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 { - font-size: 100%; - color: #730E15; - line-height: 1.5em; - } - - body > p { - width: 33em; - margin: 0 auto 1em; - padding: 1em 0; - 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); - } - </style> -</head> - -<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> - <p>If you are the application owner check the logs for more information.</p> -</body> -</html> diff --git a/guides/code/getting_started/public/500.html b/guides/code/getting_started/public/500.html deleted file mode 100644 index ebf6d4c00c..0000000000 --- a/guides/code/getting_started/public/500.html +++ /dev/null @@ -1,59 +0,0 @@ -<!DOCTYPE html> -<html> -<head> - <title>We're sorry, but something went wrong (500)</title> - <style> - body { - background-color: #EFEFEF; - color: #2E2F30; - text-align: center; - font-family: arial, sans-serif; - } - - div.dialog { - width: 25em; - margin: 4em auto 0 auto; - border: 1px solid #CCC; - border-right-color: #999; - border-left-color: #999; - border-bottom-color: #BBB; - border-top: #B00100 solid 4px; - border-top-left-radius: 9px; - 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 { - font-size: 100%; - color: #730E15; - line-height: 1.5em; - } - - body > p { - width: 33em; - margin: 0 auto 1em; - padding: 1em 0; - 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); - } - </style> -</head> - -<body> - <!-- This file lives in public/500.html --> - <div class="dialog"> - <h1>We're sorry, but something went wrong.</h1> - </div> - <p>If you are the application owner check the logs for more information.</p> -</body> -</html> diff --git a/guides/code/getting_started/public/favicon.ico b/guides/code/getting_started/public/favicon.ico deleted file mode 100644 index e69de29bb2..0000000000 --- a/guides/code/getting_started/public/favicon.ico +++ /dev/null diff --git a/guides/code/getting_started/public/robots.txt b/guides/code/getting_started/public/robots.txt deleted file mode 100644 index 3c9c7c01f3..0000000000 --- a/guides/code/getting_started/public/robots.txt +++ /dev/null @@ -1,5 +0,0 @@ -# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file -# -# To ban all spiders from the entire site uncomment the next two lines: -# User-agent: * -# Disallow: / diff --git a/guides/code/getting_started/test/controllers/.keep b/guides/code/getting_started/test/controllers/.keep deleted file mode 100644 index e69de29bb2..0000000000 --- a/guides/code/getting_started/test/controllers/.keep +++ /dev/null diff --git a/guides/code/getting_started/test/controllers/comments_controller_test.rb b/guides/code/getting_started/test/controllers/comments_controller_test.rb deleted file mode 100644 index 2ec71b4ec5..0000000000 --- a/guides/code/getting_started/test/controllers/comments_controller_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class CommentsControllerTest < ActionController::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/guides/code/getting_started/test/controllers/posts_controller_test.rb b/guides/code/getting_started/test/controllers/posts_controller_test.rb deleted file mode 100644 index 7a6ee4f1db..0000000000 --- a/guides/code/getting_started/test/controllers/posts_controller_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class PostsControllerTest < ActionController::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/guides/code/getting_started/test/controllers/welcome_controller_test.rb b/guides/code/getting_started/test/controllers/welcome_controller_test.rb deleted file mode 100644 index dff8e9d2c5..0000000000 --- a/guides/code/getting_started/test/controllers/welcome_controller_test.rb +++ /dev/null @@ -1,9 +0,0 @@ -require 'test_helper' - -class WelcomeControllerTest < ActionController::TestCase - test "should get index" do - get :index - assert_response :success - end - -end diff --git a/guides/code/getting_started/test/fixtures/.keep b/guides/code/getting_started/test/fixtures/.keep deleted file mode 100644 index e69de29bb2..0000000000 --- a/guides/code/getting_started/test/fixtures/.keep +++ /dev/null diff --git a/guides/code/getting_started/test/fixtures/comments.yml b/guides/code/getting_started/test/fixtures/comments.yml deleted file mode 100644 index 9e409d8a61..0000000000 --- a/guides/code/getting_started/test/fixtures/comments.yml +++ /dev/null @@ -1,11 +0,0 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html - -one: - commenter: MyString - body: MyText - post_id: - -two: - commenter: MyString - body: MyText - post_id: diff --git a/guides/code/getting_started/test/fixtures/posts.yml b/guides/code/getting_started/test/fixtures/posts.yml deleted file mode 100644 index 46b01c3bb4..0000000000 --- a/guides/code/getting_started/test/fixtures/posts.yml +++ /dev/null @@ -1,9 +0,0 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html - -one: - title: MyString - text: MyText - -two: - title: MyString - text: MyText diff --git a/guides/code/getting_started/test/helpers/.keep b/guides/code/getting_started/test/helpers/.keep deleted file mode 100644 index e69de29bb2..0000000000 --- a/guides/code/getting_started/test/helpers/.keep +++ /dev/null diff --git a/guides/code/getting_started/test/helpers/comments_helper_test.rb b/guides/code/getting_started/test/helpers/comments_helper_test.rb deleted file mode 100644 index 2518c16bd5..0000000000 --- a/guides/code/getting_started/test/helpers/comments_helper_test.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'test_helper' - -class CommentsHelperTest < ActionView::TestCase -end diff --git a/guides/code/getting_started/test/helpers/posts_helper_test.rb b/guides/code/getting_started/test/helpers/posts_helper_test.rb deleted file mode 100644 index 48549c2ea1..0000000000 --- a/guides/code/getting_started/test/helpers/posts_helper_test.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'test_helper' - -class PostsHelperTest < ActionView::TestCase -end diff --git a/guides/code/getting_started/test/helpers/welcome_helper_test.rb b/guides/code/getting_started/test/helpers/welcome_helper_test.rb deleted file mode 100644 index d6ded5995f..0000000000 --- a/guides/code/getting_started/test/helpers/welcome_helper_test.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'test_helper' - -class WelcomeHelperTest < ActionView::TestCase -end diff --git a/guides/code/getting_started/test/integration/.keep b/guides/code/getting_started/test/integration/.keep deleted file mode 100644 index e69de29bb2..0000000000 --- a/guides/code/getting_started/test/integration/.keep +++ /dev/null diff --git a/guides/code/getting_started/test/mailers/.keep b/guides/code/getting_started/test/mailers/.keep deleted file mode 100644 index e69de29bb2..0000000000 --- a/guides/code/getting_started/test/mailers/.keep +++ /dev/null diff --git a/guides/code/getting_started/test/models/.keep b/guides/code/getting_started/test/models/.keep deleted file mode 100644 index e69de29bb2..0000000000 --- a/guides/code/getting_started/test/models/.keep +++ /dev/null diff --git a/guides/code/getting_started/test/models/comment_test.rb b/guides/code/getting_started/test/models/comment_test.rb deleted file mode 100644 index b6d6131a96..0000000000 --- a/guides/code/getting_started/test/models/comment_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class CommentTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/guides/code/getting_started/test/models/post_test.rb b/guides/code/getting_started/test/models/post_test.rb deleted file mode 100644 index 6d9d463a71..0000000000 --- a/guides/code/getting_started/test/models/post_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class PostTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/guides/code/getting_started/test/test_helper.rb b/guides/code/getting_started/test/test_helper.rb deleted file mode 100644 index f91a4375dc..0000000000 --- a/guides/code/getting_started/test/test_helper.rb +++ /dev/null @@ -1,15 +0,0 @@ -ENV["RAILS_ENV"] = "test" -require File.expand_path('../../config/environment', __FILE__) -require 'rails/test_help' - -class ActiveSupport::TestCase - ActiveRecord::Migration.check_pending! - - # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. - # - # Note: You'll currently still have to declare fixtures explicitly in integration tests - # -- they do not yet inherit this setting - fixtures :all - - # Add more helper methods to be used by all tests here... -end diff --git a/guides/code/getting_started/vendor/assets/javascripts/.keep b/guides/code/getting_started/vendor/assets/javascripts/.keep deleted file mode 100644 index e69de29bb2..0000000000 --- a/guides/code/getting_started/vendor/assets/javascripts/.keep +++ /dev/null diff --git a/guides/code/getting_started/vendor/assets/stylesheets/.keep b/guides/code/getting_started/vendor/assets/stylesheets/.keep deleted file mode 100644 index e69de29bb2..0000000000 --- a/guides/code/getting_started/vendor/assets/stylesheets/.keep +++ /dev/null diff --git a/guides/rails_guides.rb b/guides/rails_guides.rb index 9d488a8a15..9d1d5567f6 100644 --- a/guides/rails_guides.rb +++ b/guides/rails_guides.rb @@ -24,11 +24,11 @@ begin require 'redcarpet' rescue LoadError # This can happen if doc:guides is executed in an application. - $stderr.puts('Generating guides requires Redcarpet 2.1.1+.') + $stderr.puts('Generating guides requires Redcarpet 3.1.2+.') $stderr.puts(<<ERROR) if bundler? Please add - gem 'redcarpet', '~> 2.1.1' + gem 'redcarpet', '~> 3.1.2' to the Gemfile, run diff --git a/guides/rails_guides/helpers.rb b/guides/rails_guides/helpers.rb index 169453400f..a78c2e9fca 100644 --- a/guides/rails_guides/helpers.rb +++ b/guides/rails_guides/helpers.rb @@ -39,7 +39,7 @@ module RailsGuides def author(name, nick, image = 'credits_pic_blank.gif', &block) image = "images/#{image}" - result = content_tag(:img, nil, :src => image, :class => 'left pic', :alt => name, :width => 91, :height => 91) + result = tag(:img, :src => image, :class => 'left pic', :alt => name, :width => 91, :height => 91) result << content_tag(:h3, name) result << content_tag(:p, capture(&block)) content_tag(:div, result, :class => 'clearfix', :id => nick) diff --git a/guides/rails_guides/levenshtein.rb b/guides/rails_guides/levenshtein.rb index 489aa3ea7a..8a908a4339 100644 --- a/guides/rails_guides/levenshtein.rb +++ b/guides/rails_guides/levenshtein.rb @@ -1,31 +1,39 @@ module RailsGuides module Levenshtein - # Based on the pseudocode in http://en.wikipedia.org/wiki/Levenshtein_distance - def self.distance(s1, s2) - s = s1.unpack('U*') - t = s2.unpack('U*') - m = s.length - n = t.length + # This code is based directly on the Text gem implementation + # Returns a value representing the "cost" of transforming str1 into str2 + def self.distance str1, str2 + s = str1 + t = str2 + n = s.length + m = t.length + max = n/2 - # matrix initialization - d = [] - 0.upto(m) { |i| d << [i] } - 0.upto(n) { |j| d[0][j] = j } + return m if (0 == n) + return n if (0 == m) + return n if (n - m).abs > max - # distance computation - 1.upto(m) do |i| - 1.upto(n) do |j| - cost = s[i] == t[j] ? 0 : 1 - d[i][j] = [ - d[i-1][j] + 1, # deletion - d[i][j-1] + 1, # insertion - d[i-1][j-1] + cost, # substitution - ].min + d = (0..m).to_a + x = nil + + str1.each_char.each_with_index do |char1,i| + e = i+1 + + str2.each_char.each_with_index do |char2,j| + cost = (char1 == char2) ? 0 : 1 + x = [ + d[j+1] + 1, # insertion + e + 1, # deletion + d[j] + cost # substitution + ].min + d[j] = e + e = x end + + d[m] = x end - # all done - return d[m][n] + return x end end end diff --git a/guides/rails_guides/markdown.rb b/guides/rails_guides/markdown.rb index 547c6d2c15..1ea18ba9f5 100644 --- a/guides/rails_guides/markdown.rb +++ b/guides/rails_guides/markdown.rb @@ -47,8 +47,7 @@ module RailsGuides end def dom_id_text(text) - text.downcase.gsub(/\?/, '-questionmark').gsub(/!/, '-bang').gsub(/[^a-z0-9]+/, ' ') - .strip.gsub(/\s+/, '-') + text.downcase.gsub(/\?/, '-questionmark').gsub(/!/, '-bang').gsub(/\s+/, '-') end def engine @@ -79,10 +78,10 @@ module RailsGuides def generate_structure @headings_for_index = [] if @body.present? - @body = Nokogiri::HTML(@body).tap do |doc| + @body = Nokogiri::HTML.fragment(@body).tap do |doc| hierarchy = [] - doc.at('body').children.each do |node| + doc.children.each do |node| if node.name =~ /^h[3-6]$/ case node.name when 'h3' @@ -116,7 +115,7 @@ module RailsGuides end end - @index = Nokogiri::HTML(engine.render(raw_index)).tap do |doc| + @index = Nokogiri::HTML.fragment(engine.render(raw_index)).tap do |doc| doc.at('ol')[:class] = 'chapters' end.to_html @@ -130,8 +129,8 @@ module RailsGuides end def generate_title - if heading = Nokogiri::HTML(@header).at(:h2) - @title = "#{heading.text} — Ruby on Rails Guides".html_safe + if heading = Nokogiri::HTML.fragment(@header).at(:h2) + @title = "#{heading.text} — Ruby on Rails Guides" else @title = "Ruby on Rails Guides" end diff --git a/guides/source/2_3_release_notes.md b/guides/source/2_3_release_notes.md index 8c633fa169..52eeb4c2bc 100644 --- a/guides/source/2_3_release_notes.md +++ b/guides/source/2_3_release_notes.md @@ -8,7 +8,7 @@ Rails 2.3 delivers a variety of new and improved features, including pervasive R Application Architecture ------------------------ -There are two major changes in the architecture of Rails applications: complete integration of the [Rack](http://rack.rubyforge.org/) modular web server interface, and renewed support for Rails Engines. +There are two major changes in the architecture of Rails applications: complete integration of the [Rack](http://rack.github.io/) modular web server interface, and renewed support for Rails Engines. ### Rack Integration @@ -594,7 +594,7 @@ The internals of the various <code>rake gem</code> tasks have been substantially * Various files in /public that deal with CGI and FCGI dispatching are no longer generated in every Rails application by default (you can still get them if you need them by adding `--with-dispatchers` when you run the `rails` command, or add them later with `rake rails:update:generate_dispatchers`). * Rails Guides have been converted from AsciiDoc to Textile markup. * Scaffolded views and controllers have been cleaned up a bit. -* `script/server` now accepts a <tt>--path</tt> argument to mount a Rails application from a specific path. +* `script/server` now accepts a `--path` argument to mount a Rails application from a specific path. * If any configured gems are missing, the gem rake tasks will skip loading much of the environment. This should solve many of the "chicken-and-egg" problems where rake gems:install couldn't run because gems were missing. * Gems are now unpacked exactly once. This fixes issues with gems (hoe, for instance) which are packed with read-only permissions on the files. @@ -618,4 +618,4 @@ A few pieces of older code are deprecated in this release: Credits ------- -Release notes compiled by [Mike Gunderloy](http://afreshcup.com.) This version of the Rails 2.3 release notes was compiled based on RC2 of Rails 2.3. +Release notes compiled by [Mike Gunderloy](http://afreshcup.com). This version of the Rails 2.3 release notes was compiled based on RC2 of Rails 2.3. diff --git a/guides/source/3_0_release_notes.md b/guides/source/3_0_release_notes.md index 2d4be0cda7..aec3a383d6 100644 --- a/guides/source/3_0_release_notes.md +++ b/guides/source/3_0_release_notes.md @@ -308,7 +308,7 @@ More Information: Major re-write was done in the Action View helpers, implementing Unobtrusive JavaScript (UJS) hooks and removing the old inline AJAX commands. This enables Rails to use any compliant UJS driver to implement the UJS hooks in the helpers. -What this means is that all previous `remote_<method>` helpers have been removed from Rails core and put into the [Prototype Legacy Helper](http://github.com/rails/prototype_legacy_helper.) To get UJS hooks into your HTML, you now pass `:remote => true` instead. For example: +What this means is that all previous `remote_<method>` helpers have been removed from Rails core and put into the [Prototype Legacy Helper](http://github.com/rails/prototype_legacy_helper). To get UJS hooks into your HTML, you now pass `:remote => true` instead. For example: ```ruby form_for @post, :remote => true @@ -521,7 +521,7 @@ A large effort was made in Active Support to make it cherry pickable, that is, y These are the main changes in Active Support: * Large clean up of the library removing unused methods throughout. -* Active Support no longer provides vendored versions of [TZInfo](http://tzinfo.rubyforge.org/), [Memcache Client](http://deveiate.org/projects/RMemCache/) and [Builder](http://builder.rubyforge.org/,) these are all included as dependencies and installed via the `bundle install` command. +* Active Support no longer provides vendored versions of TZInfo, Memcache Client and Builder. These are all included as dependencies and installed via the `bundle install` command. * Safe buffers are implemented in `ActiveSupport::SafeBuffer`. * Added `Array.uniq_by` and `Array.uniq_by!`. * Removed `Array#rand` and backported `Array#sample` from Ruby 1.9. @@ -608,4 +608,4 @@ Credits See the [full list of contributors to Rails](http://contributors.rubyonrails.org/) for the many people who spent many hours making Rails 3. Kudos to all of them. -Rails 3.0 Release Notes were compiled by [Mikel Lindsaar](http://lindsaar.net.) +Rails 3.0 Release Notes were compiled by [Mikel Lindsaar](http://lindsaar.net). diff --git a/guides/source/3_1_release_notes.md b/guides/source/3_1_release_notes.md index 485f8c756b..7626296e7d 100644 --- a/guides/source/3_1_release_notes.md +++ b/guides/source/3_1_release_notes.md @@ -173,7 +173,7 @@ The assets pipeline is powered by [Sprockets](https://github.com/sstephenson/spr ### HTTP Streaming -HTTP Streaming is another change that is new in Rails 3.1. This lets the browser download your stylesheets and JavaScript files while the server is still generating the response. This requires Ruby 1.9.2, is opt-in and requires support from the web server as well, but the popular combo of nginx and unicorn is ready to take advantage of it. +HTTP Streaming is another change that is new in Rails 3.1. This lets the browser download your stylesheets and JavaScript files while the server is still generating the response. This requires Ruby 1.9.2, is opt-in and requires support from the web server as well, but the popular combo of NGINX and Unicorn is ready to take advantage of it. ### Default JS library is now jQuery diff --git a/guides/source/3_2_release_notes.md b/guides/source/3_2_release_notes.md index cdcde67869..2416e1a228 100644 --- a/guides/source/3_2_release_notes.md +++ b/guides/source/3_2_release_notes.md @@ -562,4 +562,4 @@ Credits See the [full list of contributors to Rails](http://contributors.rubyonrails.org/) for the many people who spent many hours making Rails, the stable and robust framework it is. Kudos to all of them. -Rails 3.2 Release Notes were compiled by [Vijay Dev](https://github.com/vijaydev.) +Rails 3.2 Release Notes were compiled by [Vijay Dev](https://github.com/vijaydev). diff --git a/guides/source/4_1_release_notes.md b/guides/source/4_1_release_notes.md index 822943d81e..5f4bdaaa8f 100644 --- a/guides/source/4_1_release_notes.md +++ b/guides/source/4_1_release_notes.md @@ -157,7 +157,7 @@ By default, these preview classes live in `test/mailers/previews`. This can be configured using the `preview_path` option. See its -[documentation](http://api.rubyonrails.org/v4.1.0/classes/ActionMailer/Base.html) +[documentation](http://api.rubyonrails.org/v4.1.0/classes/ActionMailer/Base.html#class-ActionMailer::Base-label-Previewing+emails) for a detailed write up. ### Active Record enums diff --git a/guides/source/4_2_release_notes.md b/guides/source/4_2_release_notes.md new file mode 100644 index 0000000000..12db528b91 --- /dev/null +++ b/guides/source/4_2_release_notes.md @@ -0,0 +1,393 @@ +Ruby on Rails 4.2 Release Notes +=============================== + +Highlights in Rails 4.2: + +These release notes cover only the major changes. To know about various bug +fixes and changes, please refer to the change logs or check out the +[list of commits](https://github.com/rails/rails/commits/master) in the main +Rails repository on GitHub. + +-------------------------------------------------------------------------------- + +Upgrading to Rails 4.2 +---------------------- + +If you're upgrading an existing application, it's a great idea to have good test +coverage before going in. You should also first upgrade to Rails 4.1 in case you +haven't and make sure your application still runs as expected before attempting +an update to Rails 4.2. A list of things to watch out for when upgrading is +available in the +[Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-4-1-to-rails-4-2) +guide. + + +Major Features +-------------- + +### Foreign key support + +The migration DSL now supports adding and removing foreign keys. They are dumped +to `schema.rb` as well. At this time, only the `mysql`, `mysql2` and `postgresql` +adapters support foreign keys. + +```ruby +# add a foreign key to `articles.author_id` referencing `authors.id` +add_foreign_key :articles, :authors + +# add a foreign key to `articles.author_id` referencing `users.lng_id` +add_foreign_key :articles, :users, column: :author_id, primary_key: "lng_id" + +# remove the foreign key on `accounts.branch_id` +remove_foreign_key :accounts, :branches + +# remove the foreign key on `accounts.owner_id` +remove_foreign_key :accounts, column: :owner_id +``` + +See the API documentation on +[add_foreign_key](http://api.rubyonrails.org/v4.2.0/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_foreign_key) +and +[remove_foreign_key](http://api.rubyonrails.org/v4.2.0/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-remove_foreign_key) +for a full description. + + +Railties +-------- + +Please refer to the [Changelog][railties] for detailed changes. + +### Removals + +* The `rails application` command has been removed without replacement. + ([Pull Request](https://github.com/rails/rails/pull/11616)) + +### Deprecations + +* Deprecated `Rails::Rack::LogTailer` without replacement. + ([Commit](https://github.com/rails/rails/commit/84a13e019e93efaa8994b3f8303d635a7702dbce)) + +### Notable changes + +* Introduced `--skip-gems` option in the app generator to skip gems such as + `turbolinks` and `coffee-rails` that does not have their own specific flags. + ([Commit](https://github.com/rails/rails/commit/10565895805887d4faf004a6f71219da177f78b7)) + +* Introduced `bin/setup` script to bootstrap an application. + ([Pull Request](https://github.com/rails/rails/pull/15189)) + +* Changed default value for `config.assets.digest` to `true` in development. + ([Pull Request](https://github.com/rails/rails/pull/15155)) + +* Introduced an API to register new extensions for `rake notes`. + ([Pull Request](https://github.com/rails/rails/pull/14379)) + +* Introduced `Rails.gem_version` as a convenience method to return `Gem::Version.new(Rails.version)`. + ([Pull Request](https://github.com/rails/rails/pull/14101)) + + +Action Pack +----------- + +Please refer to the [Changelog][action-pack] for detailed changes. + +### Deprecations + +* Deprecated support for setting the `:to` option of a router to a symbol or a + string that does not contain a `#` character: + + ```ruby + get '/posts', to: MyRackApp => (No change necessary) + get '/posts', to: 'post#index' => (No change necessary) + get '/posts', to: 'posts' => get '/posts', controller: :posts + get '/posts', to: :index => get '/posts', action: :index + ``` + + ([Commit](https://github.com/rails/rails/commit/cc26b6b7bccf0eea2e2c1a9ebdcc9d30ca7390d9)) + +### Notable changes + +* `render nothing: true` or rendering a `nil` body no longer add a single + space padding to the response body. + ([Pull Request](https://github.com/rails/rails/pull/14883)) + +* Introduced the `always_permitted_parameters` option to configure which + parameters are permitted globally. The default value of this configuration + is `['controller', 'action']`. + ([Pull Request](https://github.com/rails/rails/pull/15933)) + +* The `*_filter` family methods has been removed from the documentation. Their + usage are discouraged in favor of the `*_action` family methods: + + ``` + after_filter => after_action + append_after_filter => append_after_action + append_around_filter => append_around_action + append_before_filter => append_before_action + around_filter => around_action + before_filter => before_action + prepend_after_filter => prepend_after_action + prepend_around_filter => prepend_around_action + prepend_before_filter => prepend_before_action + skip_after_filter => skip_after_action + skip_around_filter => skip_around_action + skip_before_filter => skip_before_action + skip_filter => skip_action_callback + ``` + + If your application is depending on these methods, you should use the + replacement `*_action` methods instead. These methods will be deprecated in + the future and eventually removed from Rails. + + (Commit [1](https://github.com/rails/rails/commit/6c5f43bab8206747a8591435b2aa0ff7051ad3de), + [2](https://github.com/rails/rails/commit/489a8f2a44dc9cea09154ee1ee2557d1f037c7d4)) + +* Added HTTP method `MKCALENDAR` from RFC-4791 + ([Pull Request](https://github.com/rails/rails/pull/15121)) + +* `*_fragment.action_controller` notifications now include the controller and action name + in the payload. + ([Pull Request](https://github.com/rails/rails/pull/14137)) + +* Segments that are passed into URL helpers are now automatically escaped. + ([Commit](https://github.com/rails/rails/commit/5460591f0226a9d248b7b4f89186bd5553e7768f)) + +* Improved Routing Error page with fuzzy matching for route search. + ([Pull Request](https://github.com/rails/rails/pull/14619)) + +* Added option to disable logging of CSRF failures. + ([Pull Request](https://github.com/rails/rails/pull/14280)) + + +Action View +------------- + +Please refer to the [Changelog][action-view] for detailed changes. + +### Deprecations + +* Deprecated `AbstractController::Base.parent_prefixes`. + Override `AbstractController::Base.local_prefixes` when you want to change + where to find views. + ([Pull Request](https://github.com/rails/rails/pull/15026)) + +* Deprecated `ActionView::Digestor#digest(name, format, finder, options = {})`, + arguments should be passed as a hash instead. + ([Pull Request](https://github.com/rails/rails/pull/14243)) + +### Notable changes + +* The form helpers no longer generate a `<div>` element with inline CSS around + the hidden fields. + ([Pull Request](https://github.com/rails/rails/pull/14738)) + + +Action Mailer +------------- + +Please refer to the [Changelog][action-mailer] for detailed changes. + +### Notable changes + +* Added the `show_previews` configuration option for enabling mailer previews + outside of the development environment. + ([Pull Request](https://github.com/rails/rails/pull/15970)) + + +Active Record +------------- + +Please refer to the +[Changelog](https://github.com/rails/rails/blob/4-2-stable/activerecord/CHANGELOG.md) +for detailed changes. + +### Removals + +* Removed `cache_attributes` and friends. All attributes are cached. + ([Pull Request](https://github.com/rails/rails/pull/15429)) + +* Removed deprecated method `ActiveRecord::Base.quoted_locking_column`. + ([Pull Request](https://github.com/rails/rails/pull/15612)) + +* Removed deprecated `ActiveRecord::Migrator.proper_table_name`. Use the + `proper_table_name` instance method on `ActiveRecord::Migration` instead. + ([Pull Request](https://github.com/rails/rails/pull/15512)) + +* Removed unused `:timestamp` type. Transparently alias it to `:datetime` + in all cases. Fixes inconsistencies when column types are sent outside of + `ActiveRecord`, such as for XML Serialization. + ([Pull Request](https://github.com/rails/rails/pull/15184)) + +### Deprecations + +* Deprecated broken support for automatic detection of counter caches on + `has_many :through` associations. You should instead manually specify the + counter cache on the `has_many` and `belongs_to` associations for the + through records. + ([Pull Request](https://github.com/rails/rails/pull/15754)) + +* Deprecated `serialized_attributes` without replacement. + ([Pull Request](https://github.com/rails/rails/pull/15704)) + +* Deprecated returning `nil` from `column_for_attribute` when no column + exists. It will return a null object in Rails 5.0 + ([Pull Request](https://github.com/rails/rails/pull/15878)) + +* Deprecated using `.joins`, `.preload` and `.eager_load` with associations + that depends on the instance state (i.e. those defined with a scope that + takes an argument) without replacement. + ([Commit](https://github.com/rails/rails/commit/ed56e596a0467390011bc9d56d462539776adac1)) + +* Deprecated passing Active Record objects to `.find` or `.exists?`. Call + `#id` on the objects first. + (Commit [1](https://github.com/rails/rails/commit/d92ae6ccca3bcfd73546d612efaea011270bd270), + [2](https://github.com/rails/rails/commit/d35f0033c7dec2b8d8b52058fb8db495d49596f7)) + +* Deprecated half-baked support for PostgreSQL range values with excluding + beginnings. We currently map PostgreSQL ranges to Ruby ranges. This conversion + is not fully possible because the Ruby range does not support excluded + beginnings. + + The current solution of incrementing the beginning is not correct + and is now deprecated. For subtypes where we don't know how to increment + (e.g. `#succ` is not defined) it will raise an `ArgumentError` for ranges + with excluding beginnings. + + ([Commit](https://github.com/rails/rails/commit/91949e48cf41af9f3e4ffba3e5eecf9b0a08bfc3)) + +### Notable changes + +* Added a `:required` option to singular associations, which defines a + presence validation on the association. + ([Pull Request](https://github.com/rails/rails/pull/16056)) + +* Introduced `ActiveRecord::Base#validate!` that raises `RecordInvalid` if the + record is invalid. + ([Pull Request](https://github.com/rails/rails/pull/8639)) + +* `ActiveRecord::Base#reload` now behaves the same as `m = Model.find(m.id)`, + meaning that it no longer retains the extra attributes from custom + `select`s. + ([Pull Request](https://github.com/rails/rails/pull/15866)) + +* Introduced the `bin/rake db:purge` task to empty the database for the + current environment. + ([Commit](https://github.com/rails/rails/commit/e2f232aba15937a4b9d14bd91e0392c6d55be58d)) + +* `ActiveRecord::Dirty` now detects in-place changes to mutable values. + Serialized attributes on ActiveRecord models will no longer save when + unchanged. This also works with other types such as string columns and json + columns on PostgreSQL. + (Pull Requests [1](https://github.com/rails/rails/pull/15674), + [2](https://github.com/rails/rails/pull/15786), + [3](https://github.com/rails/rails/pull/15788)) + +* Added support for `#pretty_print` in `ActiveRecord::Base` objects. + ([Pull Request](https://github.com/rails/rails/pull/15172)) + +* PostgreSQL and SQLite adapters no longer add a default limit of 255 + characters on string columns. + ([Pull Request](https://github.com/rails/rails/pull/14579)) + +* `sqlite3:///some/path` now resolves to the absolute system path + `/some/path`. For relative paths, use `sqlite3:some/path` instead. + (Previously, `sqlite3:///some/path` resolved to the relative path + `some/path`. This behaviour was deprecated on Rails 4.1.) + ([Pull Request](https://github.com/rails/rails/pull/14569)) + +* Introduced `#validate` as an alias for `#valid?`. + ([Pull Request](https://github.com/rails/rails/pull/14456)) + +* `#touch` now accepts multiple attributes to be touched at once. + ([Pull Request](https://github.com/rails/rails/pull/14423)) + +* Added support for fractional seconds for MySQL 5.6 and above. + (Pull Request [1](https://github.com/rails/rails/pull/8240), + [2](https://github.com/rails/rails/pull/14359)) + +* Added support for the `citext` column type in PostgreSQL adapter. + ([Pull Request](https://github.com/rails/rails/pull/12523)) + +* Added support for user-created range types in PostgreSQL adapter. + ([Commit](https://github.com/rails/rails/commit/4cb47167e747e8f9dc12b0ddaf82bdb68c03e032)) + + +Active Model +------------ + +Please refer to the [Changelog][active-model] for detailed changes. + +### Removals + +* Removed deprecated `Validator#setup` without replacement. + ([Pull Request](https://github.com/rails/rails/pull/15617)) + +### Notable changes + +* Introduced `undo_changes` method in `ActiveModel::Dirty` to restore the + changed (dirty) attributes to their previous values. + ([Pull Request](https://github.com/rails/rails/pull/14861)) + +* `has_secure_password` now verifies that the given password is less than 72 + characters if validations are enabled. + ([Pull Request](https://github.com/rails/rails/pull/15708)) + +* Introduced `#validate` as an alias for `#valid?`. + ([Pull Request](https://github.com/rails/rails/pull/14456)) + + +Active Support +-------------- + +Please refer to the [Changelog][active-support] for detailed changes. + +### Removals + +* Removed deprecated `Numeric#ago`, `Numeric#until`, `Numeric#since`, + `Numeric#from_now`. + ([Commit](https://github.com/rails/rails/commit/f1eddea1e3f6faf93581c43651348f48b2b7d8bb)) + +* Removed deprecated string based terminators for `ActiveSupport::Callbacks`. + ([Pull Request](https://github.com/rails/rails/pull/15100)) + +### Deprecations + +* Deprecated `Class#superclass_delegating_accessor`, use + `Class#class_attribute` instead. + ([Pull Request](https://github.com/rails/rails/pull/14271)) + +* Deprecated `ActiveSupport::SafeBuffer#prepend!` as + `ActiveSupport::SafeBuffer#prepend` now performs the same function. + ([Pull Request](https://github.com/rails/rails/pull/14529)) + +### Notable changes + +* Added `Hash#transform_values` and `Hash#transform_values!` to simplify a + common pattern where the values of a hash must change, but the keys are left + the same. + ([Pull Request](https://github.com/rails/rails/pull/15819)) + +* The `humanize` inflector helper now strips any leading underscores. + ([Commit](https://github.com/rails/rails/commit/daaa21bc7d20f2e4ff451637423a25ff2d5e75c7)) + +* Introduce `Concern#class_methods` as an alternative to + `module ClassMethods`, as well as `Kernel#concern` to avoid the + `module Foo; extend ActiveSupport::Concern; end` boilerplate. + ([Commit](https://github.com/rails/rails/commit/b16c36e688970df2f96f793a759365b248b582ad)) + + +Credits +------- + +See the +[full list of contributors to Rails](http://contributors.rubyonrails.org/) for +the many people who spent many hours making Rails the stable and robust +framework it is today. Kudos to all of them. + +[railties]: https://github.com/rails/rails/blob/4-2-stable/railties/CHANGELOG.md +[action-pack]: https://github.com/rails/rails/blob/4-2-stable/actionpack/CHANGELOG.md +[action-view]: https://github.com/rails/rails/blob/4-2-stable/actionview/CHANGELOG.md +[action-mailer]: https://github.com/rails/rails/blob/4-2-stable/actionmailer/CHANGELOG.md +[active-record]: https://github.com/rails/rails/blob/4-2-stable/activerecord/CHANGELOG.md +[active-model]: https://github.com/rails/rails/blob/4-2-stable/activemodel/CHANGELOG.md +[active-support]: https://github.com/rails/rails/blob/4-2-stable/activesupport/CHANGELOG.md diff --git a/guides/source/_license.html.erb b/guides/source/_license.html.erb index 00b4466f50..d22f016948 100644 --- a/guides/source/_license.html.erb +++ b/guides/source/_license.html.erb @@ -1,2 +1,2 @@ -<p>This work is licensed under a <a href="http://creativecommons.org/licenses/by-sa/3.0/">Creative Commons Attribution-Share Alike 3.0</a> License</p> +<p>This work is licensed under a <a href="https://creativecommons.org/licenses/by-sa/4.0/">Creative Commons Attribution-ShareAlike 4.0 International</a> License</p> <p>"Rails", "Ruby on Rails", and the Rails logo are trademarks of David Heinemeier Hansson. All rights reserved.</p> diff --git a/guides/source/_welcome.html.erb b/guides/source/_welcome.html.erb index 7e39f761f2..f84f1cb376 100644 --- a/guides/source/_welcome.html.erb +++ b/guides/source/_welcome.html.erb @@ -10,16 +10,10 @@ </p> <% else %> <p> - These are the new guides for Rails 4.1 based on <a href="https://github.com/rails/rails/tree/<%= @version %>"><%= @version %></a>. + These are the new guides for Rails 4.2 based on <a href="https://github.com/rails/rails/tree/<%= @version %>"><%= @version %></a>. These guides are designed to make you immediately productive with Rails, and to help you understand how all of the pieces fit together. </p> <% end %> <p> - The guides for Rails 4.0.x are available at <a href="http://guides.rubyonrails.org/v4.0.4/">http://guides.rubyonrails.org/v4.0.4/</a>. -</p> -<p> - The guides for Rails 3.2.x are available at <a href="http://guides.rubyonrails.org/v3.2.17/">http://guides.rubyonrails.org/v3.2.17/</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>. + The guides for earlier releases: <a href="http://guides.rubyonrails.org/v4.1.4/">Rails 4.1.4</a>, <a href="http://guides.rubyonrails.org/v4.0.8/">Rails 4.0.8</a>, <a href="http://guides.rubyonrails.org/v3.2.19/">Rails 3.2.19</a> and <a href="http://guides.rubyonrails.org/v2.3.11/">Rails 2.3.11</a>. </p> diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index 1735188f27..4c04a06dbb 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -1078,7 +1078,7 @@ Rails keeps a log file for each environment in the `log` folder. These are extre ### Parameters Filtering -You can filter certain request parameters from your log files by appending them to `config.filter_parameters` in the application configuration. These parameters will be marked [FILTERED] in the log. +You can filter out sensitive request parameters from your log files by appending them to `config.filter_parameters` in the application configuration. These parameters will be marked [FILTERED] in the log. ```ruby config.filter_parameters << :password @@ -1086,7 +1086,7 @@ config.filter_parameters << :password ### Redirects Filtering -Sometimes it's desirable to filter out from log files some sensible locations your application is redirecting to. +Sometimes it's desirable to filter out from log files some sensitive locations your application is redirecting to. You can do that by using the `config.filter_redirect` configuration option: ```ruby @@ -1164,8 +1164,65 @@ class ClientsController < ApplicationController end ``` +WARNING: You shouldn't do `rescue_from Exception` or `rescue_from StandardError` unless you have a particular reason as it will cause serious side-effects (e.g. you won't be able to see exception details and tracebacks during development). If you would like to dynamically generate error pages, see [Custom errors page](#custom-errors-page). + NOTE: Certain exceptions are only rescuable from the `ApplicationController` class, as they are raised before the controller gets initialized and the action gets executed. See Pratik Naik's [article](http://m.onkey.org/2008/7/20/rescue-from-dispatching) on the subject for more information. + +### Custom errors page + +You can customize the layout of your error handling using controllers and views. +First define your app own routes to display the errors page. + +* `config/application.rb` + + ```ruby + config.exceptions_app = self.routes + ``` + +* `config/routes.rb` + + ```ruby + get '/404', to: 'errors#not_found' + get '/422', to: 'errors#unprocessable_entity' + get '/500', to: 'errors#server_error' + ``` + +Create the controller and views. + +* `app/controllers/errors_controller.rb` + + ```ruby + class ErrorsController < ActionController::Base + layout 'error' + + def not_found + render status: :not_found + end + + def unprocessable_entity + render status: :unprocessable_entity + end + + def server_error + render status: :server_error + end + end + ``` + +* `app/views` + + ``` + errors/ + not_found.html.erb + unprocessable_entity.html.erb + server_error.html.erb + layouts/ + error.html.erb + ``` + +Do not forget to set the correct status code on the controller as shown before. You should avoid using the database or any complex operations because the user is already on the error page. Generating another error while on an error page could cause issues. + Force HTTPS protocol -------------------- diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index 6dc7fb1606..cb1c1c653d 100644 --- a/guides/source/action_mailer_basics.md +++ b/guides/source/action_mailer_basics.md @@ -17,7 +17,10 @@ After reading this guide, you will know: Introduction ------------ -Action Mailer allows you to send emails from your application using mailer classes and views. Mailers work very similarly to controllers. They inherit from `ActionMailer::Base` and live in `app/mailers`, and they have associated views that appear in `app/views`. +Action Mailer allows you to send emails from your application using mailer classes +and views. Mailers work very similarly to controllers. They inherit from +`ActionMailer::Base` and live in `app/mailers`, and they have associated views +that appear in `app/views`. Sending Emails -------------- @@ -30,7 +33,7 @@ views. #### Create the Mailer ```bash -$ rails generate mailer UserMailer +$ bin/rails generate mailer UserMailer create app/mailers/user_mailer.rb invoke erb create app/views/user_mailer @@ -84,8 +87,11 @@ Here is a quick explanation of the items presented in the preceding method. For a full list of all available options, please have a look further down at the Complete List of Action Mailer user-settable attributes section. -* `default Hash` - This is a hash of default values for any email you send from this mailer. In this case we are setting the `:from` header to a value for all messages in this class. This can be overridden on a per-email basis. -* `mail` - The actual email message, we are passing the `:to` and `:subject` headers in. +* `default Hash` - This is a hash of default values for any email you send from +this mailer. In this case we are setting the `:from` header to a value for all +messages in this class. This can be overridden on a per-email basis. +* `mail` - The actual email message, we are passing the `:to` and `:subject` +headers in. Just like controllers, any instance variables we define in the method become available for use in the views. @@ -146,12 +152,12 @@ Setting this up is painfully simple. First, let's create a simple `User` scaffold: ```bash -$ rails generate scaffold user name email login -$ rake db:migrate +$ bin/rails generate scaffold user name email login +$ bin/rake db:migrate ``` Now that we have a user model to play with, we will just edit the -`app/controllers/users_controller.rb` make it instruct the UserMailer to deliver +`app/controllers/users_controller.rb` make it instruct the `UserMailer` to deliver an email to the newly created user by editing the create action and inserting a call to `UserMailer.welcome_email` right after the user is successfully saved: @@ -230,9 +236,11 @@ different, encode your content and pass in the encoded content and encoding in a ```ruby encoded_content = SpecialEncode(File.read('/path/to/filename.jpg')) - attachments['filename.jpg'] = {mime_type: 'application/x-gzip', - encoding: 'SpecialEncoding', - content: encoded_content } + attachments['filename.jpg'] = { + mime_type: 'application/x-gzip', + encoding: 'SpecialEncoding', + content: encoded_content + } ``` NOTE: If you specify an encoding, Mail will assume that your content is already @@ -301,7 +309,7 @@ email address in the format `"Full Name <email>"`. ```ruby def welcome_email(user) @user = user - email_with_name = "#{@user.name} <#{@user.email}>" + email_with_name = %("#{@user.name}" <#{@user.email}>) mail(to: email_with_name, subject: 'Welcome to My Awesome Site') end ``` @@ -608,7 +616,7 @@ files (environment.rb, production.rb, etc...) | Configuration | Description | |---------------|-------------| |`logger`|Generates information on the mailing run if available. Can be set to `nil` for no logging. Compatible with both Ruby's own `Logger` and `Log4r` loggers.| -|`smtp_settings`|Allows detailed configuration for `:smtp` delivery method:<ul><li>`:address` - Allows you to use a remote mail server. Just change it from its default "localhost" setting.</li><li>`:port` - On the off chance that your mail server doesn't run on port 25, you can change it.</li><li>`:domain` - If you need to specify a HELO domain, you can do it here.</li><li>`:user_name` - If your mail server requires authentication, set the username in this setting.</li><li>`:password` - If your mail server requires authentication, set the password in this setting.</li><li>`:authentication` - If your mail server requires authentication, you need to specify the authentication type here. This is a symbol and one of `:plain`, `:login`, `:cram_md5`.</li><li>`:enable_starttls_auto` - Set this to `false` if there is a problem with your server certificate that you cannot resolve.</li></ul>| +|`smtp_settings`|Allows detailed configuration for `:smtp` delivery method:<ul><li>`:address` - Allows you to use a remote mail server. Just change it from its default `"localhost"` setting.</li><li>`:port` - On the off chance that your mail server doesn't run on port 25, you can change it.</li><li>`:domain` - If you need to specify a HELO domain, you can do it here.</li><li>`:user_name` - If your mail server requires authentication, set the username in this setting.</li><li>`:password` - If your mail server requires authentication, set the password in this setting.</li><li>`:authentication` - If your mail server requires authentication, you need to specify the authentication type here. This is a symbol and one of `:plain`, `:login`, `:cram_md5`.</li><li>`:enable_starttls_auto` - Set this to `false` if there is a problem with your server certificate that you cannot resolve.</li></ul>| |`sendmail_settings`|Allows you to override options for the `:sendmail` delivery method.<ul><li>`:location` - The location of the sendmail executable. Defaults to `/usr/sbin/sendmail`.</li><li>`:arguments` - The command line arguments to be passed to sendmail. Defaults to `-i -t`.</li></ul>| |`raise_delivery_errors`|Whether or not errors should be raised if the email fails to be delivered. This only works if the external email server is configured for immediate delivery.| |`delivery_method`|Defines a delivery method. Possible values are:<ul><li>`:smtp` (default), can be configured by using `config.action_mailer.smtp_settings`.</li><li>`:sendmail`, can be configured by using `config.action_mailer.sendmail_settings`.</li><li>`:file`: save emails to files; can be configured by using `config.action_mailer.file_settings`.</li><li>`:test`: save emails to `ActionMailer::Base.deliveries` array.</li></ul>See [API docs](http://api.rubyonrails.org/classes/ActionMailer/Base.html) for more info.| @@ -617,7 +625,7 @@ files (environment.rb, production.rb, etc...) |`default_options`|Allows you to set default values for the `mail` method options (`:from`, `:reply_to`, etc.).| For a complete writeup of possible configurations see the -[Action Mailer section](configuring.html#configuring-action-mailer) in +[Configuring Action Mailer](configuring.html#configuring-action-mailer) in our Configuring Rails Applications guide. ### Example Action Mailer Configuration @@ -662,6 +670,7 @@ You can find detailed instructions on how to test your mailers in the Intercepting Emails ------------------- + There are situations where you need to edit an email before it's delivered. Fortunately Action Mailer provides hooks to intercept every email. You can register an interceptor to make modifications to mail messages @@ -685,5 +694,5 @@ ActionMailer::Base.register_interceptor(SandboxEmailInterceptor) if Rails.env.st NOTE: The example above uses a custom environment called "staging" for a production like server but for testing purposes. You can read -[Creating Rails environments](./configuring.html#creating-rails-environments) +[Creating Rails environments](configuring.html#creating-rails-environments) for more information about custom Rails environments. diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md index 74f95bfcfd..ef7ef5a50e 100644 --- a/guides/source/action_view_overview.md +++ b/guides/source/action_view_overview.md @@ -28,22 +28,22 @@ For each controller there is an associated directory in the `app/views` director Let's take a look at what Rails does by default when creating a new resource using the scaffold generator: ```bash -$ rails generate scaffold post +$ bin/rails generate scaffold article [...] invoke scaffold_controller - create app/controllers/posts_controller.rb + create app/controllers/articles_controller.rb invoke erb - create app/views/posts - create app/views/posts/index.html.erb - create app/views/posts/edit.html.erb - create app/views/posts/show.html.erb - create app/views/posts/new.html.erb - create app/views/posts/_form.html.erb + create app/views/articles + create app/views/articles/index.html.erb + create app/views/articles/edit.html.erb + create app/views/articles/show.html.erb + create app/views/articles/new.html.erb + create app/views/articles/_form.html.erb [...] ``` There is a naming convention for views in Rails. Typically, the views share their name with the associated controller action, as you can see above. -For example, the index controller action of the `posts_controller.rb` will use the `index.html.erb` view file in the `app/views/posts` directory. +For example, the index controller action of the `articles_controller.rb` will use the `index.html.erb` view file in the `app/views/articles` directory. The complete HTML returned to the client is composed of a combination of this ERB file, a layout template that wraps it, and all the partials that the view may reference. Later on this guide you can find a more detailed documentation of each one of these three components. @@ -276,23 +276,23 @@ Partial Layouts Partials can have their own layouts applied to them. These layouts are different than the ones that are specified globally for the entire action, but they work in a similar fashion. -Let's say we're displaying a post on a page, that should be wrapped in a `div` for display purposes. First, we'll create a new `Post`: +Let's say we're displaying an article on a page, that should be wrapped in a `div` for display purposes. First, we'll create a new `Article`: ```ruby -Post.create(body: 'Partial Layouts are cool!') +Article.create(body: 'Partial Layouts are cool!') ``` -In the `show` template, we'll render the `_post` partial wrapped in the `box` layout: +In the `show` template, we'll render the `_article` partial wrapped in the `box` layout: -**posts/show.html.erb** +**articles/show.html.erb** ```erb -<%= render partial: 'post', layout: 'box', locals: {post: @post} %> +<%= render partial: 'article', layout: 'box', locals: {article: @article} %> ``` -The `box` layout simply wraps the `_post` partial in a `div`: +The `box` layout simply wraps the `_article` partial in a `div`: -**posts/_box.html.erb** +**articles/_box.html.erb** ```html+erb <div class='box'> @@ -300,13 +300,13 @@ The `box` layout simply wraps the `_post` partial in a `div`: </div> ``` -The `_post` partial wraps the post's `body` in a `div` with the `id` of the post using the `div_for` helper: +The `_article` partial wraps the article's `body` in a `div` with the `id` of the article using the `div_for` helper: -**posts/_post.html.erb** +**articles/_article.html.erb** ```html+erb -<%= div_for(post) do %> - <p><%= post.body %></p> +<%= div_for(article) do %> + <p><%= article.body %></p> <% end %> ``` @@ -314,22 +314,22 @@ this would output the following: ```html <div class='box'> - <div id='post_1'> + <div id='article_1'> <p>Partial Layouts are cool!</p> </div> </div> ``` -Note that the partial layout has access to the local `post` variable that was passed into the `render` call. However, unlike application-wide layouts, partial layouts still have the underscore prefix. +Note that the partial layout has access to the local `article` variable that was passed into the `render` call. However, unlike application-wide layouts, partial layouts still have the underscore prefix. -You can also render a block of code within a partial layout instead of calling `yield`. For example, if we didn't have the `_post` partial, we could do this instead: +You can also render a block of code within a partial layout instead of calling `yield`. For example, if we didn't have the `_article` partial, we could do this instead: -**posts/show.html.erb** +**articles/show.html.erb** ```html+erb -<% render(layout: 'box', locals: {post: @post}) do %> - <%= div_for(post) do %> - <p><%= post.body %></p> +<% render(layout: 'box', locals: {article: @article}) do %> + <%= div_for(article) do %> + <p><%= article.body %></p> <% end %> <% end %> ``` @@ -356,18 +356,18 @@ This module provides methods for generating container tags, such as `div`, for y Renders a container tag that relates to your Active Record Object. -For example, given `@post` is the object of `Post` class, you can do: +For example, given `@article` is the object of `Article` class, you can do: ```html+erb -<%= content_tag_for(:tr, @post) do %> - <td><%= @post.title %></td> +<%= content_tag_for(:tr, @article) do %> + <td><%= @article.title %></td> <% end %> ``` This will generate this HTML output: ```html -<tr id="post_1234" class="post"> +<tr id="article_1234" class="article"> <td>Hello World!</td> </tr> ``` @@ -375,34 +375,34 @@ This will generate this HTML output: You can also supply HTML attributes as an additional option hash. For example: ```html+erb -<%= content_tag_for(:tr, @post, class: "frontpage") do %> - <td><%= @post.title %></td> +<%= content_tag_for(:tr, @article, class: "frontpage") do %> + <td><%= @article.title %></td> <% end %> ``` Will generate this HTML output: ```html -<tr id="post_1234" class="post frontpage"> +<tr id="article_1234" class="article frontpage"> <td>Hello World!</td> </tr> ``` -You can pass a collection of Active Record objects. This method will loop through your objects and create a container for each of them. For example, given `@posts` is an array of two `Post` objects: +You can pass a collection of Active Record objects. This method will loop through your objects and create a container for each of them. For example, given `@articles` is an array of two `Article` objects: ```html+erb -<%= content_tag_for(:tr, @posts) do |post| %> - <td><%= post.title %></td> +<%= content_tag_for(:tr, @articles) do |article| %> + <td><%= article.title %></td> <% end %> ``` Will generate this HTML output: ```html -<tr id="post_1234" class="post"> +<tr id="article_1234" class="article"> <td>Hello World!</td> </tr> -<tr id="post_1235" class="post"> +<tr id="article_1235" class="article"> <td>Ruby on Rails Rocks!</td> </tr> ``` @@ -412,15 +412,15 @@ Will generate this HTML output: This is actually a convenient method which calls `content_tag_for` internally with `:div` as the tag name. You can pass either an Active Record object or a collection of objects. For example: ```html+erb -<%= div_for(@post, class: "frontpage") do %> - <td><%= @post.title %></td> +<%= div_for(@article, class: "frontpage") do %> + <td><%= @article.title %></td> <% end %> ``` Will generate this HTML output: ```html -<div id="post_1234" class="post frontpage"> +<div id="article_1234" class="article frontpage"> <td>Hello World!</td> </div> ``` @@ -590,14 +590,14 @@ This helper makes building an Atom feed easy. Here's a full usage example: **config/routes.rb** ```ruby -resources :posts +resources :articles ``` -**app/controllers/posts_controller.rb** +**app/controllers/articles_controller.rb** ```ruby def index - @posts = Post.all + @articles = Article.all respond_to do |format| format.html @@ -606,20 +606,20 @@ def index end ``` -**app/views/posts/index.atom.builder** +**app/views/articles/index.atom.builder** ```ruby atom_feed do |feed| - feed.title("Posts Index") - feed.updated((@posts.first.created_at)) + feed.title("Articles Index") + feed.updated((@articles.first.created_at)) - @posts.each do |post| - feed.entry(post) do |entry| - entry.title(post.title) - entry.content(post.body, type: 'html') + @articles.each do |article| + feed.entry(article) do |entry| + entry.title(article.title) + entry.content(article.body, type: 'html') entry.author do |author| - author.name(post.author_name) + author.name(article.author_name) end end end @@ -697,7 +697,7 @@ For example, let's say we have a standard application layout, but also a special </html> ``` -**app/views/posts/special.html.erb** +**app/views/articles/special.html.erb** ```html+erb <p>This is a special page.</p> @@ -714,7 +714,7 @@ For example, let's say we have a standard application layout, but also a special Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based attribute. ```ruby -date_select("post", "published_on") +date_select("article", "published_on") ``` #### datetime_select @@ -722,7 +722,7 @@ date_select("post", "published_on") Returns a set of select tags (one for year, month, day, hour, and minute) pre-selected for accessing a specified datetime-based attribute. ```ruby -datetime_select("post", "published_on") +datetime_select("article", "published_on") ``` #### distance_of_time_in_words @@ -904,10 +904,10 @@ The params hash has a nested person value, which can therefore be accessed with Returns a checkbox tag tailored for accessing a specified attribute. ```ruby -# Let's say that @post.validated? is 1: -check_box("post", "validated") -# => <input type="checkbox" id="post_validated" name="post[validated]" value="1" /> -# <input name="post[validated]" type="hidden" value="0" /> +# Let's say that @article.validated? is 1: +check_box("article", "validated") +# => <input type="checkbox" id="article_validated" name="article[validated]" value="1" /> +# <input name="article[validated]" type="hidden" value="0" /> ``` #### fields_for @@ -939,7 +939,7 @@ file_field(:user, :avatar) Creates a form and a scope around a specific model object that is used as a base for questioning about values for the fields. ```html+erb -<%= form_for @post do |f| %> +<%= form_for @article do |f| %> <%= f.label :title, 'Title' %>: <%= f.text_field :title %><br> <%= f.label :body, 'Body' %>: @@ -961,8 +961,8 @@ hidden_field(:user, :token) Returns a label tag tailored for labelling an input field for a specified attribute. ```ruby -label(:post, :title) -# => <label for="post_title">Title</label> +label(:article, :title) +# => <label for="article_title">Title</label> ``` #### password_field @@ -979,11 +979,11 @@ password_field(:login, :pass) Returns a radio button tag for accessing a specified attribute. ```ruby -# Let's say that @post.category returns "rails": -radio_button("post", "category", "rails") -radio_button("post", "category", "java") -# => <input type="radio" id="post_category_rails" name="post[category]" value="rails" checked="checked" /> -# <input type="radio" id="post_category_java" name="post[category]" value="java" /> +# Let's say that @article.category returns "rails": +radio_button("article", "category", "rails") +radio_button("article", "category", "java") +# => <input type="radio" id="article_category_rails" name="article[category]" value="rails" checked="checked" /> +# <input type="radio" id="article_category_java" name="article[category]" value="java" /> ``` #### text_area @@ -1002,8 +1002,8 @@ text_area(:comment, :text, size: "20x30") Returns an input tag of the "text" type tailored for accessing a specified attribute. ```ruby -text_field(:post, :title) -# => <input type="text" id="post_title" name="post[title]" value="#{@post.title}" /> +text_field(:article, :title) +# => <input type="text" id="article_title" name="article[title]" value="#{@article.title}" /> ``` #### email_field @@ -1035,28 +1035,28 @@ Returns `select` and `option` tags for the collection of existing return values Example object structure for use with this method: ```ruby -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base belongs_to :author end class Author < ActiveRecord::Base - has_many :posts + has_many :articles def name_with_initial "#{first_name.first}. #{last_name}" end end ``` -Sample usage (selecting the associated Author for an instance of Post, `@post`): +Sample usage (selecting the associated Author for an instance of Article, `@article`): ```ruby -collection_select(:post, :author_id, Author.all, :id, :name_with_initial, {prompt: true}) +collection_select(:article, :author_id, Author.all, :id, :name_with_initial, {prompt: true}) ``` -If `@post.author_id` is 1, this would return: +If `@article.author_id` is 1, this would return: ```html -<select name="post[author_id]"> +<select name="article[author_id]"> <option value="">Please select</option> <option value="1" selected="selected">D. Heinemeier Hansson</option> <option value="2">D. Thomas</option> @@ -1071,33 +1071,33 @@ Returns `radio_button` tags for the collection of existing return values of `met Example object structure for use with this method: ```ruby -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base belongs_to :author end class Author < ActiveRecord::Base - has_many :posts + has_many :articles def name_with_initial "#{first_name.first}. #{last_name}" end end ``` -Sample usage (selecting the associated Author for an instance of Post, `@post`): +Sample usage (selecting the associated Author for an instance of Article, `@article`): ```ruby -collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) +collection_radio_buttons(:article, :author_id, Author.all, :id, :name_with_initial) ``` -If `@post.author_id` is 1, this would return: +If `@article.author_id` is 1, this would return: ```html -<input id="post_author_id_1" name="post[author_id]" type="radio" value="1" checked="checked" /> -<label for="post_author_id_1">D. Heinemeier Hansson</label> -<input id="post_author_id_2" name="post[author_id]" type="radio" value="2" /> -<label for="post_author_id_2">D. Thomas</label> -<input id="post_author_id_3" name="post[author_id]" type="radio" value="3" /> -<label for="post_author_id_3">M. Clark</label> +<input id="article_author_id_1" name="article[author_id]" type="radio" value="1" checked="checked" /> +<label for="article_author_id_1">D. Heinemeier Hansson</label> +<input id="article_author_id_2" name="article[author_id]" type="radio" value="2" /> +<label for="article_author_id_2">D. Thomas</label> +<input id="article_author_id_3" name="article[author_id]" type="radio" value="3" /> +<label for="article_author_id_3">M. Clark</label> ``` #### collection_check_boxes @@ -1107,34 +1107,34 @@ Returns `check_box` tags for the collection of existing return values of `method Example object structure for use with this method: ```ruby -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base has_and_belongs_to_many :authors end class Author < ActiveRecord::Base - has_and_belongs_to_many :posts + has_and_belongs_to_many :articles def name_with_initial "#{first_name.first}. #{last_name}" end end ``` -Sample usage (selecting the associated Authors for an instance of Post, `@post`): +Sample usage (selecting the associated Authors for an instance of Article, `@article`): ```ruby -collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) +collection_check_boxes(:article, :author_ids, Author.all, :id, :name_with_initial) ``` -If `@post.author_ids` is [1], this would return: +If `@article.author_ids` is [1], this would return: ```html -<input id="post_author_ids_1" name="post[author_ids][]" type="checkbox" value="1" checked="checked" /> -<label for="post_author_ids_1">D. Heinemeier Hansson</label> -<input id="post_author_ids_2" name="post[author_ids][]" type="checkbox" value="2" /> -<label for="post_author_ids_2">D. Thomas</label> -<input id="post_author_ids_3" name="post[author_ids][]" type="checkbox" value="3" /> -<label for="post_author_ids_3">M. Clark</label> -<input name="post[author_ids][]" type="hidden" value="" /> +<input id="article_author_ids_1" name="article[author_ids][]" type="checkbox" value="1" checked="checked" /> +<label for="article_author_ids_1">D. Heinemeier Hansson</label> +<input id="article_author_ids_2" name="article[author_ids][]" type="checkbox" value="2" /> +<label for="article_author_ids_2">D. Thomas</label> +<input id="article_author_ids_3" name="article[author_ids][]" type="checkbox" value="3" /> +<label for="article_author_ids_3">M. Clark</label> +<input name="article[author_ids][]" type="hidden" value="" /> ``` #### country_options_for_select @@ -1222,13 +1222,13 @@ Create a select tag and a series of contained option tags for the provided objec Example: ```ruby -select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: true}) +select("article", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: true}) ``` -If `@post.person_id` is 1, this would become: +If `@article.person_id` is 1, this would become: ```html -<select name="post[person_id]"> +<select name="article[person_id]"> <option value=""></option> <option value="1" selected="selected">David</option> <option value="2">Sam</option> @@ -1303,10 +1303,10 @@ file_field_tag 'attachment' Starts a form tag that points the action to an url configured with `url_for_options` just like `ActionController::Base#url_for`. ```html+erb -<%= form_tag '/posts' do %> +<%= form_tag '/articles' do %> <div><%= submit_tag 'Save' %></div> <% end %> -# => <form action="/posts" method="post"><div><input type="submit" name="submit" value="Save" /></div></form> +# => <form action="/articles" method="post"><div><input type="submit" name="submit" value="Save" /></div></form> ``` #### hidden_field_tag @@ -1368,8 +1368,8 @@ select_tag "people", "<option>David</option>" Creates a submit button with the text provided as the caption. ```ruby -submit_tag "Publish this post" -# => <input name="commit" type="submit" value="Publish this post" /> +submit_tag "Publish this article" +# => <input name="commit" type="submit" value="Publish this article" /> ``` #### text_area_tag @@ -1377,8 +1377,8 @@ submit_tag "Publish this post" Creates a text input area; use a textarea for longer text inputs such as blog posts or descriptions. ```ruby -text_area_tag 'post' -# => <textarea id="post" name="post"></textarea> +text_area_tag 'article' +# => <textarea id="article" name="article"></textarea> ``` #### text_field_tag @@ -1602,7 +1602,7 @@ Localized Views Action View has the ability render different templates depending on the current locale. -For example, suppose you have a `PostsController` with a show action. By default, calling this action will render `app/views/posts/show.html.erb`. But if you set `I18n.locale = :de`, then `app/views/posts/show.de.html.erb` will be rendered instead. If the localized template isn't present, the undecorated version will be used. This means you're not required to provide localized views for all cases, but they will be preferred and used if available. +For example, suppose you have a `ArticlesController` with a show action. By default, calling this action will render `app/views/articles/show.html.erb`. But if you set `I18n.locale = :de`, then `app/views/articles/show.de.html.erb` will be rendered instead. If the localized template isn't present, the undecorated version will be used. This means you're not required to provide localized views for all cases, but they will be preferred and used if available. You can use the same technique to localize the rescue files in your public directory. For example, setting `I18n.locale = :de` and creating `public/500.de.html` and `public/404.de.html` would allow you to have localized rescue pages. @@ -1616,6 +1616,6 @@ def set_expert_locale end ``` -Then you could create special views like `app/views/posts/show.expert.html.erb` that would only be displayed to expert users. +Then you could create special views like `app/views/articles/show.expert.html.erb` that would only be displayed to expert users. You can read more about the Rails Internationalization (I18n) API [here](i18n.html). diff --git a/guides/source/active_model_basics.md b/guides/source/active_model_basics.md index 0019d08328..3eaeeff389 100644 --- a/guides/source/active_model_basics.md +++ b/guides/source/active_model_basics.md @@ -198,3 +198,26 @@ person.valid? # => true person.token = nil person.valid? # => raises ActiveModel::StrictValidationFailed ``` + +### ActiveModel::Naming + +Naming adds a number of class methods which make the naming and routing +easier to manage. The module defines the `model_name` class method which +will define a number of accessors using some `ActiveSupport::Inflector` methods. + +```ruby +class Person + extend ActiveModel::Naming +end + +Person.model_name.name # => "Person" +Person.model_name.singular # => "person" +Person.model_name.plural # => "people" +Person.model_name.element # => "person" +Person.model_name.human # => "Person" +Person.model_name.collection # => "people" +Person.model_name.param_key # => "person" +Person.model_name.i18n_key # => :person +Person.model_name.route_key # => "people" +Person.model_name.singular_route_key # => "person" +``` diff --git a/guides/source/active_record_basics.md b/guides/source/active_record_basics.md index a184f0753d..eff93ce41d 100644 --- a/guides/source/active_record_basics.md +++ b/guides/source/active_record_basics.md @@ -82,13 +82,13 @@ by underscores. Examples: * Model Class - Singular with the first letter of each word capitalized (e.g., `BookClub`). -| Model / Class | Table / Schema | -| ------------- | -------------- | -| `Post` | `posts` | -| `LineItem` | `line_items` | -| `Deer` | `deers` | -| `Mouse` | `mice` | -| `Person` | `people` | +| Model / Class | Table / Schema | +| ---------------- | -------------- | +| `Article` | `articles` | +| `LineItem` | `line_items` | +| `Deer` | `deers` | +| `Mouse` | `mice` | +| `Person` | `people` | ### Schema Conventions @@ -120,9 +120,9 @@ to Active Record instances: * `(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 - associations. For example, a `comments_count` column in a `Post` class that + associations. For example, a `comments_count` column in a `Articles` class that has many instances of `Comment` will cache the number of existent comments - for each post. + for each article. NOTE: While these column names are optional, they are in fact reserved by Active Record. Steer clear of reserved keywords unless you want the extra functionality. For example, `type` is a reserved keyword used to designate a table using Single Table Inheritance (STI). If you are not using STI, try an analogous keyword like "context", that may still accurately describe the data you are modeling. @@ -309,11 +309,11 @@ into the database. There are several methods that you can use to check your models and validate that an attribute value is not empty, is unique and not already in the database, follows a specific format and many more. -Validation is a very important issue to consider when persisting to database, so -the methods `create`, `save` and `update` take it into account when +Validation is a very important issue to consider when persisting to the database, so +the methods `save` and `update` take it into account when running: they return `false` when validation fails and they didn't actually -perform any operation on database. All of these have a bang counterpart (that -is, `create!`, `save!` and `update!`), which are stricter in that +perform any operation on the database. All of these have a bang counterpart (that +is, `save!` and `update!`), which are stricter in that they raise the exception `ActiveRecord::RecordInvalid` if validation fails. A quick example to illustrate: @@ -322,8 +322,9 @@ class User < ActiveRecord::Base validates :name, presence: true end -User.create # => false -User.create! # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank +user = User.new +user.save # => false +user.save! # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank ``` You can learn more about validations in the [Active Record Validations diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index fbcce325ed..9c7e60cbb0 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -15,7 +15,7 @@ After reading this guide, you will know: The Object Life Cycle --------------------- -During the normal operation of a Rails application, objects may be created, updated, and destroyed. Active Record provides hooks into this <em>object life cycle</em> so that you can control your application and its data. +During the normal operation of a Rails application, objects may be created, updated, and destroyed. Active Record provides hooks into this *object life cycle* so that you can control your application and its data. Callbacks allow you to trigger logic before or after an alteration of an object's state. @@ -261,27 +261,27 @@ WARNING. Any exception that is not `ActiveRecord::Rollback` will be re-raised by Relational Callbacks -------------------- -Callbacks work through model relationships, and can even be defined by them. Suppose an example where a user has many posts. A user's posts should be destroyed if the user is destroyed. Let's add an `after_destroy` callback to the `User` model by way of its relationship to the `Post` model: +Callbacks work through model relationships, and can even be defined by them. Suppose an example where a user has many articles. A user's articles should be destroyed if the user is destroyed. Let's add an `after_destroy` callback to the `User` model by way of its relationship to the `Article` model: ```ruby class User < ActiveRecord::Base - has_many :posts, dependent: :destroy + has_many :articles, dependent: :destroy end -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base after_destroy :log_destroy_action def log_destroy_action - puts 'Post destroyed' + puts 'Article destroyed' end end >> user = User.first => #<User id: 1> ->> user.posts.create! -=> #<Post id: 1, user_id: 1> +>> user.articles.create! +=> #<Article id: 1, user_id: 1> >> user.destroy -Post destroyed +Article destroyed => #<User id: 1> ``` @@ -328,7 +328,7 @@ When writing conditional callbacks, it is possible to mix both `:if` and `:unles ```ruby class Comment < ActiveRecord::Base after_create :send_email_to_author, if: :author_wants_emails?, - unless: Proc.new { |comment| comment.post.ignore_comments? } + unless: Proc.new { |comment| comment.article.ignore_comments? } end ``` diff --git a/guides/source/migrations.md b/guides/source/active_record_migrations.md index c61ccfe94a..229c6ee458 100644 --- a/guides/source/migrations.md +++ b/guides/source/active_record_migrations.md @@ -121,7 +121,7 @@ Of course, calculating timestamps is no fun, so Active Record provides a generator to handle making it for you: ```bash -$ rails generate migration AddPartNumberToProducts +$ bin/rails generate migration AddPartNumberToProducts ``` This will create an empty but appropriately named migration: @@ -138,7 +138,7 @@ followed by a list of column names and types then a migration containing the appropriate `add_column` and `remove_column` statements will be created. ```bash -$ rails generate migration AddPartNumberToProducts part_number:string +$ bin/rails generate migration AddPartNumberToProducts part_number:string ``` will generate @@ -154,7 +154,7 @@ end If you'd like to add an index on the new column, you can do that as well: ```bash -$ rails generate migration AddPartNumberToProducts part_number:string:index +$ bin/rails generate migration AddPartNumberToProducts part_number:string:index ``` will generate @@ -172,7 +172,7 @@ end Similarly, you can generate a migration to remove a column from the command line: ```bash -$ rails generate migration RemovePartNumberFromProducts part_number:string +$ bin/rails generate migration RemovePartNumberFromProducts part_number:string ``` generates @@ -188,7 +188,7 @@ end You are not limited to one magically generated column. For example: ```bash -$ rails generate migration AddDetailsToProducts part_number:string price:decimal +$ bin/rails generate migration AddDetailsToProducts part_number:string price:decimal ``` generates @@ -207,7 +207,7 @@ followed by a list of column names and types then a migration creating the table XXX with the columns listed will be generated. For example: ```bash -$ rails generate migration CreateProducts name:string part_number:string +$ bin/rails generate migration CreateProducts name:string part_number:string ``` generates @@ -231,7 +231,7 @@ Also, the generator accepts column type as `references`(also available as `belongs_to`). For instance: ```bash -$ rails generate migration AddUserRefToProducts user:references +$ bin/rails generate migration AddUserRefToProducts user:references ``` generates @@ -249,7 +249,7 @@ This migration will create a `user_id` column and appropriate index. There is also a generator which will produce join tables if `JoinTable` is part of the name: ```bash -rails g migration CreateJoinTableCustomerProduct customer product +$ bin/rails g migration CreateJoinTableCustomerProduct customer product ``` will produce the following migration: @@ -273,7 +273,7 @@ relevant table. If you tell Rails what columns you want, then statements for adding these columns will also be created. For example, running: ```bash -$ rails generate model Product name:string description:text +$ bin/rails generate model Product name:string description:text ``` will create a migration that looks like this @@ -293,21 +293,15 @@ end You can append as many column name/type pairs as you want. -### Supported Type Modifiers +### Passing Modifiers -You can also specify some options just after the field type between curly -braces. You can use the following modifiers: - -* `limit` Sets the maximum size of the `string/text/binary/integer` fields. -* `precision` Defines the precision for the `decimal` fields, representing the total number of digits in the number. -* `scale` Defines the scale for the `decimal` fields, representing the number of digits after the decimal point. -* `polymorphic` Adds a `type` column for `belongs_to` associations. -* `null` Allows or disallows `NULL` values in the column. +Some commonly used [type modifiers](#column-modifiers) can be passed directly on +the command line. They are enclosed by curly braces and follow the field type: For instance, running: ```bash -$ rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplier:references{polymorphic} +$ bin/rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplier:references{polymorphic} ``` will produce a migration that looks like this @@ -321,6 +315,8 @@ class AddDetailsToProducts < ActiveRecord::Migration end ``` +TIP: Have a look at the generators help output for further details. + Writing a Migration ------------------- @@ -415,6 +411,85 @@ end removes the `description` and `name` columns, creates a `part_number` string column and adds an index on it. Finally it renames the `upccode` column. +### Changing Columns + +Like the `remove_column` and `add_column` Rails provides the `change_column` +migration method. + +```ruby +change_column :products, :part_number, :text +``` + +This changes the column `part_number` on products table to be a `:text` field. + +Besides `change_column`, the `change_column_null` and `change_column_default` +methods are used specifically to change the null and default values of a +column. + +```ruby +change_column_null :products, :name, false +change_column_default :products, :approved, false +``` + +This sets `:name` field on products to a `NOT NULL` column and the default +value of the `:approved` field to false. + +TIP: Unlike `change_column` (and `change_column_default`), `change_column_null` +is reversible. + +### Column Modifiers + +Column modifiers can be applied when creating or changing a column: + +* `limit` Sets the maximum size of the `string/text/binary/integer` fields. +* `precision` Defines the precision for the `decimal` fields, representing the +total number of digits in the number. +* `scale` Defines the scale for the `decimal` fields, representing the +number of digits after the decimal point. +* `polymorphic` Adds a `type` column for `belongs_to` associations. +* `null` Allows or disallows `NULL` values in the column. +* `default` Allows to set a default value on the column. Note that if you +are using a dynamic value (such as a date), the default will only be calculated +the first time (i.e. on the date the migration is applied). +* `index` Adds an index for the column. + +Some adapters may support additional options; see the adapter specific API docs +for further information. + +### Foreign Keys + +While it's not required you might want to add foreign key constraints to +[guarantee referential integrity](#active-record-and-referential-integrity). + +```ruby +add_foreign_key :articles, :authors +``` + +This adds a new foreign key to the `author_id` column of the `articles` +table. The key references the `id` column of the `articles` table. If the +column names can not be derived from the table names, you can use the +`:column` and `:primary_key` options. + +Rails will generate a name for every foreign key starting with +`fk_rails_` followed by 10 random characters. +There is a `:name` option to specify a different name if needed. + +NOTE: Active Record only supports single column foreign keys. `execute` and +`structure.sql` are required to use composite foreign keys. + +Removing a foreign key is easy as well: + +```ruby +# let Active Record figure out the column name +remove_foreign_key :accounts, :branches + +# remove foreign key for a specific column +remove_foreign_key :accounts, column: :owner_id + +# remove foreign key by name +remove_foreign_key :accounts, name: :special_fk_name +``` + ### When Helpers aren't Enough If the helpers provided by Active Record aren't enough you can use the `execute` @@ -445,6 +520,7 @@ definitions: * `add_index` * `add_reference` * `add_timestamps` +* `add_foreign_key` * `create_table` * `create_join_table` * `drop_table` (must supply a block) @@ -470,24 +546,23 @@ migration what else to do when reverting it. For example: ```ruby class ExampleMigration < ActiveRecord::Migration def change - create_table :products do |t| - t.references :category + create_table :distributors do |t| + t.string :zipcode end reversible do |dir| dir.up do - #add a foreign key + # add a CHECK constraint execute <<-SQL - ALTER TABLE products - ADD CONSTRAINT fk_products_categories - FOREIGN KEY (category_id) - REFERENCES categories(id) + ALTER TABLE distributors + ADD CONSTRAINT zipchk + CHECK (char_length(zipcode) = 5) NO INHERIT; SQL end dir.down do execute <<-SQL - ALTER TABLE products - DROP FOREIGN KEY fk_products_categories + ALTER TABLE distributors + DROP CONSTRAINT zipchk SQL end end @@ -501,7 +576,7 @@ end Using `reversible` will ensure that the instructions are executed in the right order too. If the previous example migration is reverted, the `down` block will be run after the `home_page_url` column is removed and -right before the table `products` is dropped. +right before the table `distributors` is dropped. Sometimes your migration will do something which is just plain irreversible; for example, it might destroy some data. In such cases, you can raise @@ -524,16 +599,15 @@ made in the `up` method. The example in the `reversible` section is equivalent t ```ruby class ExampleMigration < ActiveRecord::Migration def up - create_table :products do |t| - t.references :category + create_table :distributors do |t| + t.string :zipcode end - # add a foreign key + # add a CHECK constraint execute <<-SQL - ALTER TABLE products - ADD CONSTRAINT fk_products_categories - FOREIGN KEY (category_id) - REFERENCES categories(id) + ALTER TABLE distributors + ADD CONSTRAINT zipchk + CHECK (char_length(zipcode) = 5); SQL add_column :users, :home_page_url, :string @@ -545,11 +619,11 @@ class ExampleMigration < ActiveRecord::Migration remove_column :users, :home_page_url execute <<-SQL - ALTER TABLE products - DROP FOREIGN KEY fk_products_categories + ALTER TABLE distributors + DROP CONSTRAINT zipchk SQL - drop_table :products + drop_table :distributors end end ``` @@ -580,43 +654,27 @@ end The `revert` method also accepts a block of instructions to reverse. This could be useful to revert selected parts of previous migrations. For example, let's imagine that `ExampleMigration` is committed and it -is later decided it would be best to serialize the product list instead. -One could write: +is later decided it would be best to use Active Record validations, +in place of the `CHECK` constraint, to verify the zipcode. ```ruby -class SerializeProductListMigration < ActiveRecord::Migration +class DontUseConstraintForZipcodeValidationMigration < ActiveRecord::Migration def change - add_column :categories, :product_list - - reversible do |dir| - dir.up do - # transfer data from Products to Category#product_list - end - dir.down do - # create Products from Category#product_list - end - end - revert do # copy-pasted code from ExampleMigration - create_table :products do |t| - t.references :category - end - reversible do |dir| dir.up do - #add a foreign key + # add a CHECK constraint execute <<-SQL - ALTER TABLE products - ADD CONSTRAINT fk_products_categories - FOREIGN KEY (category_id) - REFERENCES categories(id) + ALTER TABLE distributors + ADD CONSTRAINT zipchk + CHECK (char_length(zipcode) = 5); SQL end dir.down do execute <<-SQL - ALTER TABLE products - DROP FOREIGN KEY fk_products_categories + ALTER TABLE distributors + DROP CONSTRAINT zipchk SQL end end @@ -653,7 +711,7 @@ is the numerical prefix on the migration's filename. For example, to migrate to version 20080906120000 run: ```bash -$ rake db:migrate VERSION=20080906120000 +$ bin/rake db:migrate VERSION=20080906120000 ``` If version 20080906120000 is greater than the current version (i.e., it is @@ -670,7 +728,7 @@ mistake in it and wish to correct it. Rather than tracking down the version number associated with the previous migration you can run: ```bash -$ rake db:rollback +$ bin/rake db:rollback ``` This will rollback the latest migration, either by reverting the `change` @@ -678,7 +736,7 @@ method or by running the `down` method. If you need to undo several migrations you can provide a `STEP` parameter: ```bash -$ rake db:rollback STEP=3 +$ bin/rake db:rollback STEP=3 ``` will revert the last 3 migrations. @@ -688,7 +746,7 @@ 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: ```bash -$ rake db:migrate:redo STEP=3 +$ bin/rake db:migrate:redo STEP=3 ``` Neither of these Rake tasks do anything you could not do with `db:migrate`. They @@ -718,7 +776,7 @@ the corresponding migration will have its `change`, `up` or `down` method invoked, for example: ```bash -$ rake db:migrate:up VERSION=20080906120000 +$ bin/rake db:migrate:up VERSION=20080906120000 ``` will run the 20080906120000 migration by running the `change` method (or the @@ -734,7 +792,7 @@ To run migrations against another environment you can specify it using the migrations against the `test` environment you could run: ```bash -$ rake db:migrate RAILS_ENV=test +$ bin/rake db:migrate RAILS_ENV=test ``` ### Changing the Output of Running Migrations @@ -881,10 +939,10 @@ that Active Record supports. This could be very useful if you were to distribute an application that is able to run against multiple databases. There is however a trade-off: `db/schema.rb` cannot express database specific -items such as foreign key constraints, triggers, or stored procedures. While in -a migration you can execute custom SQL statements, the schema dumper cannot -reconstitute those statements from the database. If you are using features like -this, then you should set the schema format to `:sql`. +items such as triggers, or stored procedures. While in a migration you can +execute custom SQL statements, the schema dumper cannot reconstitute those +statements from the database. If you are using features like 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` @@ -902,11 +960,16 @@ schema into a RDBMS other than the one used to create it. Because schema dumps are the authoritative source for your database schema, it is strongly recommended that you check them into source control. +`db/schema.rb` contains the current version number of the database. This +ensures conflicts are going to happen in the case of a merge where both +branches touched the schema. When that happens, solve conflicts manually, +keeping the highest version number of the two. + Active Record and Referential Integrity --------------------------------------- The Active Record way claims that intelligence belongs in your models, not in -the database. As such, features such as triggers or foreign key constraints, +the database. As such, features such as triggers or constraints, which push some of that intelligence back into the database, are not heavily used. @@ -915,14 +978,10 @@ which models can enforce data integrity. The `:dependent` option on associations allows models to automatically destroy child objects when the parent is destroyed. Like anything which operates at the application level, these cannot guarantee referential integrity and so some people augment them -with foreign key constraints in the database. - -Although Active Record does not provide any tools for working directly with -such features, the `execute` method can be used to execute arbitrary SQL. You -can also use a gem like -[foreigner](https://github.com/matthuhiggins/foreigner) which adds foreign key -support to Active Record (including support for dumping foreign keys in -`db/schema.rb`). +with [foreign key constraints](#foreign-keys) in the database. + +Although Active Record does not provide all the tools for working directly with +such features, the `execute` method can be used to execute arbitrary SQL. Migrations and Seed Data ------------------------ diff --git a/guides/source/active_record_postgresql.md b/guides/source/active_record_postgresql.md new file mode 100644 index 0000000000..a5649e3903 --- /dev/null +++ b/guides/source/active_record_postgresql.md @@ -0,0 +1,437 @@ +Active Record and PostgreSQL +============================ + +This guide covers PostgreSQL specific usage of Active Record. + +After reading this guide, you will know: + +* How to use PostgreSQL's datatypes. +* How to use UUID primary keys. +* How to implement full text search with PostgreSQL. +* How to back your Active Record models with database views. + +-------------------------------------------------------------------------------- + +In order to use the PostgreSQL adapter you need to have at least version 8.2 +installed. Older versions are not supported. + +To get started with PostgreSQL have a look at the +[configuring Rails guide](configuring.html#configuring-a-postgresql-database). +It describes how to properly setup Active Record for PostgreSQL. + +Datatypes +--------- + +PostgreSQL offers a number of specific datatypes. Following is a list of types, +that are supported by the PostgreSQL adapter. + +### Bytea + +* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-binary.html) +* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-binarystring.html) + +```ruby +# db/migrate/20140207133952_create_documents.rb +create_table :documents do |t| + t.binary 'payload' +end + +# app/models/document.rb +class Document < ActiveRecord::Base +end + +# Usage +data = File.read(Rails.root + "tmp/output.pdf") +Document.create payload: data +``` + +### Array + +* [type definition](http://www.postgresql.org/docs/9.3/static/arrays.html) +* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-array.html) + +```ruby +# db/migrate/20140207133952_create_books.rb +create_table :books do |t| + t.string 'title' + t.string 'tags', array: true + t.integer 'ratings', array: true +end +add_index :books, :tags, using: 'gin' +add_index :books, :ratings, using: 'gin' + +# app/models/book.rb +class Book < ActiveRecord::Base +end + +# Usage +Book.create title: "Brave New World", + tags: ["fantasy", "fiction"], + ratings: [4, 5] + +## Books for a single tag +Book.where("'fantasy' = ANY (tags)") + +## Books for multiple tags +Book.where("tags @> ARRAY[?]::varchar[]", ["fantasy", "fiction"]) + +## Books with 3 or more ratings +Book.where("array_length(ratings, 1) >= 3") +``` + +### Hstore + +* [type definition](http://www.postgresql.org/docs/9.3/static/hstore.html) + +```ruby +# db/migrate/20131009135255_create_profiles.rb +ActiveRecord::Schema.define do + create_table :profiles do |t| + t.hstore 'settings' + end +end + +# app/models/profile.rb +class Profile < ActiveRecord::Base +end + +# Usage +Profile.create(settings: { "color" => "blue", "resolution" => "800x600" }) + +profile = Profile.first +profile.settings # => {"color"=>"blue", "resolution"=>"800x600"} + +profile.settings = {"color" => "yellow", "resolution" => "1280x1024"} +profile.save! + +## you need to call _will_change! if you are editing the store in place +profile.settings["color"] = "green" +profile.settings_will_change! +profile.save! +``` + +### JSON + +* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-json.html) +* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-json.html) + +```ruby +# db/migrate/20131220144913_create_events.rb +create_table :events do |t| + t.json 'payload' +end + +# app/models/event.rb +class Event < ActiveRecord::Base +end + +# Usage +Event.create(payload: { kind: "user_renamed", change: ["jack", "john"]}) + +event = Event.first +event.payload # => {"kind"=>"user_renamed", "change"=>["jack", "john"]} + +## Query based on JSON document +Event.where("payload->'kind' = ?", "user_renamed") +``` + +### Range Types + +* [type definition](http://www.postgresql.org/docs/9.3/static/rangetypes.html) +* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-range.html) + +This type is mapped to Ruby [`Range`](http://www.ruby-doc.org/core-2.1.1/Range.html) objects. + +```ruby +# db/migrate/20130923065404_create_events.rb +create_table :events do |t| + t.daterange 'duration' +end + +# app/models/event.rb +class Event < ActiveRecord::Base +end + +# Usage +Event.create(duration: Date.new(2014, 2, 11)..Date.new(2014, 2, 12)) + +event = Event.first +event.duration # => Tue, 11 Feb 2014...Thu, 13 Feb 2014 + +## All Events on a given date +Event.where("duration @> ?::date", Date.new(2014, 2, 12)) + +## Working with range bounds +event = Event. + select("lower(duration) AS starts_at"). + select("upper(duration) AS ends_at").first + +event.starts_at # => Tue, 11 Feb 2014 +event.ends_at # => Thu, 13 Feb 2014 +``` + +### Composite Types + +* [type definition](http://www.postgresql.org/docs/9.3/static/rowtypes.html) + +Currently there is no special support for composite types. They are mapped to +normal text columns: + +```sql +CREATE TYPE full_address AS +( + city VARCHAR(90), + street VARCHAR(90) +); +``` + +```ruby +# db/migrate/20140207133952_create_contacts.rb +execute <<-SQL + CREATE TYPE full_address AS + ( + city VARCHAR(90), + street VARCHAR(90) + ); +SQL +create_table :contacts do |t| + t.column :address, :full_address +end + +# app/models/contact.rb +class Contact < ActiveRecord::Base +end + +# Usage +Contact.create address: "(Paris,Champs-Élysées)" +contact = Contact.first +contact.address # => "(Paris,Champs-Élysées)" +contact.address = "(Paris,Rue Basse)" +contact.save! +``` + +### Enumerated Types + +* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-enum.html) + +Currently there is no special support for enumerated types. They are mapped as +normal text columns: + +```ruby +# db/migrate/20131220144913_create_events.rb +execute <<-SQL + CREATE TYPE article_status AS ENUM ('draft', 'published'); +SQL +create_table :articles do |t| + t.column :status, :article_status +end + +# app/models/article.rb +class Article < ActiveRecord::Base +end + +# Usage +Article.create status: "draft" +article = Article.first +article.status # => "draft" + +article.status = "published" +article.save! +``` + +### UUID + +* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-uuid.html) +* [generator functions](http://www.postgresql.org/docs/9.3/static/uuid-ossp.html) + + +```ruby +# db/migrate/20131220144913_create_revisions.rb +create_table :revisions do |t| + t.column :identifier, :uuid +end + +# app/models/revision.rb +class Revision < ActiveRecord::Base +end + +# Usage +Revision.create identifier: "A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11" + +revision = Revision.first +revision.identifier # => "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11" +``` + +### Bit String Types + +* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-bit.html) +* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-bitstring.html) + +```ruby +# db/migrate/20131220144913_create_users.rb +create_table :users, force: true do |t| + t.column :settings, "bit(8)" +end + +# app/models/device.rb +class User < ActiveRecord::Base +end + +# Usage +User.create settings: "01010011" +user = User.first +user.settings # => "(Paris,Champs-Élysées)" +user.settings = "0xAF" +user.settings # => 10101111 +user.save! +``` + +### Network Address Types + +* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-net-types.html) + +The types `inet` and `cidr` are mapped to Ruby +[`IPAddr`](http://www.ruby-doc.org/stdlib-2.1.1/libdoc/ipaddr/rdoc/IPAddr.html) +objects. The `macaddr` type is mapped to normal text. + +```ruby +# db/migrate/20140508144913_create_devices.rb +create_table(:devices, force: true) do |t| + t.inet 'ip' + t.cidr 'network' + t.macaddr 'address' +end + +# app/models/device.rb +class Device < ActiveRecord::Base +end + +# Usage +macbook = Device.create(ip: "192.168.1.12", + network: "192.168.2.0/24", + address: "32:01:16:6d:05:ef") + +macbook.ip +# => #<IPAddr: IPv4:192.168.1.12/255.255.255.255> + +macbook.network +# => #<IPAddr: IPv4:192.168.2.0/255.255.255.0> + +macbook.address +# => "32:01:16:6d:05:ef" +``` + +### Geometric Types + +* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-geometric.html) + +All geometric types, with the exception of `points` are mapped to normal text. +A point is casted to an array containing `x` and `y` coordinates. + + +UUID Primary Keys +----------------- + +NOTE: you need to enable the `uuid-ossp` extension to generate UUIDs. + +```ruby +# db/migrate/20131220144913_create_devices.rb +enable_extension 'uuid-ossp' unless extension_enabled?('uuid-ossp') +create_table :devices, id: :uuid, default: 'uuid_generate_v4()' do |t| + t.string :kind +end + +# app/models/device.rb +class Device < ActiveRecord::Base +end + +# Usage +device = Device.create +device.id # => "814865cd-5a1d-4771-9306-4268f188fe9e" +``` + +Full Text Search +---------------- + +```ruby +# db/migrate/20131220144913_create_documents.rb +create_table :documents do |t| + t.string 'title' + t.string 'body' +end + +execute "CREATE INDEX documents_idx ON documents USING gin(to_tsvector('english', title || ' ' || body));" + +# app/models/document.rb +class Document < ActiveRecord::Base +end + +# Usage +Document.create(title: "Cats and Dogs", body: "are nice!") + +## all documents matching 'cat & dog' +Document.where("to_tsvector('english', title || ' ' || body) @@ to_tsquery(?)", + "cat & dog") +``` + +Database Views +-------------- + +* [view creation](http://www.postgresql.org/docs/9.3/static/sql-createview.html) + +Imagine you need to work with a legacy database containing the following table: + +``` +rails_pg_guide=# \d "TBL_ART" + Table "public.TBL_ART" + Column | Type | Modifiers +------------+-----------------------------+------------------------------------------------------------ + INT_ID | integer | not null default nextval('"TBL_ART_INT_ID_seq"'::regclass) + STR_TITLE | character varying | + STR_STAT | character varying | default 'draft'::character varying + DT_PUBL_AT | timestamp without time zone | + BL_ARCH | boolean | default false +Indexes: + "TBL_ART_pkey" PRIMARY KEY, btree ("INT_ID") +``` + +This table does not follow the Rails conventions at all. +Because simple PostgreSQL views are updateable by default, +we can wrap it as follows: + +```ruby +# db/migrate/20131220144913_create_articles_view.rb +execute <<-SQL +CREATE VIEW articles AS + SELECT "INT_ID" AS id, + "STR_TITLE" AS title, + "STR_STAT" AS status, + "DT_PUBL_AT" AS published_at, + "BL_ARCH" AS archived + FROM "TBL_ART" + WHERE "BL_ARCH" = 'f' + SQL + +# app/models/article.rb +class Article < ActiveRecord::Base + self.primary_key = "id" + def archive! + update_attribute :archived, true + end +end + +# Usage +first = Article.create! title: "Winter is coming", + status: "published", + published_at: 1.year.ago +second = Article.create! title: "Brace yourself", + status: "draft", + published_at: 1.month.ago + +Article.count # => 1 +first.archive! +Article.count # => 2 +``` + +NOTE: This application only cares about non-archived `Articles`. A view also +allows for conditions so we can exclude the archived `Articles` directly. diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index 2a76df156c..c9e265de08 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -93,9 +93,9 @@ The primary operation of `Model.find(options)` can be summarized as: Active Record provides several different ways of retrieving a single object. -#### Using a Primary Key +#### `find` -Using `Model.find(primary_key)`, you can retrieve the object corresponding to the specified _primary key_ that matches any supplied options. For example: +Using the `find` method, you can retrieve the object corresponding to the specified _primary key_ that matches any supplied options. For example: ```ruby # Find the client with primary key (id) 10. @@ -109,215 +109,180 @@ The SQL equivalent of the above is: SELECT * FROM clients WHERE (clients.id = 10) LIMIT 1 ``` -`Model.find(primary_key)` will raise an `ActiveRecord::RecordNotFound` exception if no matching record is found. +The `find` method will raise an `ActiveRecord::RecordNotFound` exception if no matching record is found. -#### `take` - -`Model.take` retrieves a record without any implicit ordering. For example: +You can also use this method to query for multiple objects. Call the `find` method and pass in an array of primary keys. The return will be an array containing all of the matching records for the supplied _primary keys_. For example: ```ruby -client = Client.take -# => #<Client id: 1, first_name: "Lifo"> +# Find the clients with primary keys 1 and 10. +client = Client.find([1, 10]) # Or even Client.find(1, 10) +# => [#<Client id: 1, first_name: "Lifo">, #<Client id: 10, first_name: "Ryan">] ``` The SQL equivalent of the above is: ```sql -SELECT * FROM clients LIMIT 1 +SELECT * FROM clients WHERE (clients.id IN (1,10)) ``` -`Model.take` returns `nil` if no record is found and no exception will be raised. - -TIP: The retrieved record may vary depending on the database engine. +WARNING: The `find` method will raise an `ActiveRecord::RecordNotFound` exception unless a matching record is found for **all** of the supplied primary keys. -#### `first` +#### `take` -`Model.first` finds the first record ordered by the primary key. For example: +The `take` method retrieves a record without any implicit ordering. For example: ```ruby -client = Client.first +client = Client.take # => #<Client id: 1, first_name: "Lifo"> ``` The SQL equivalent of the above is: ```sql -SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1 +SELECT * FROM clients LIMIT 1 ``` -`Model.first` returns `nil` if no matching record is found and no exception will be raised. - -#### `last` +The `take` method returns `nil` if no record is found and no exception will be raised. -`Model.last` finds the last record ordered by the primary key. For example: +You can pass in a numerical argument to the `take` method to return up to that number of results. For example ```ruby -client = Client.last -# => #<Client id: 221, first_name: "Russel"> +client = Client.take(2) +# => [ + #<Client id: 1, first_name: "Lifo">, + #<Client id: 220, first_name: "Sara"> +] ``` The SQL equivalent of the above is: ```sql -SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1 +SELECT * FROM clients LIMIT 2 ``` -`Model.last` returns `nil` if no matching record is found and no exception will be raised. +The `take!` method behaves exactly like `take`, except that it will raise `ActiveRecord::RecordNotFound` if no matching record is found. -#### `find_by` +TIP: The retrieved record may vary depending on the database engine. + +#### `first` -`Model.find_by` finds the first record matching some conditions. For example: +The `first` method finds the first record ordered by the primary key. For example: ```ruby -Client.find_by first_name: 'Lifo' +client = Client.first # => #<Client id: 1, first_name: "Lifo"> - -Client.find_by first_name: 'Jon' -# => nil ``` -It is equivalent to writing: +The SQL equivalent of the above is: -```ruby -Client.where(first_name: 'Lifo').take +```sql +SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1 ``` -#### `take!` +The `first` method returns `nil` if no matching record is found and no exception will be raised. -`Model.take!` retrieves a record without any implicit ordering. For example: +You can pass in a numerical argument to the `first` method to return up to that number of results. For example ```ruby -client = Client.take! -# => #<Client id: 1, first_name: "Lifo"> +client = Client.first(3) +# => [ + #<Client id: 1, first_name: "Lifo">, + #<Client id: 2, first_name: "Fifo">, + #<Client id: 3, first_name: "Filo"> +] ``` The SQL equivalent of the above is: ```sql -SELECT * FROM clients LIMIT 1 +SELECT * FROM clients ORDER BY clients.id ASC LIMIT 3 ``` -`Model.take!` raises `ActiveRecord::RecordNotFound` if no matching record is found. +The `first!` method behaves exactly like `first`, except that it will raise `ActiveRecord::RecordNotFound` if no matching record is found. -#### `first!` +#### `last` -`Model.first!` finds the first record ordered by the primary key. For example: +The `last` method finds the last record ordered by the primary key. For example: ```ruby -client = Client.first! -# => #<Client id: 1, first_name: "Lifo"> +client = Client.last +# => #<Client id: 221, first_name: "Russel"> ``` The SQL equivalent of the above is: ```sql -SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1 +SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1 ``` -`Model.first!` raises `ActiveRecord::RecordNotFound` if no matching record is found. - -#### `last!` +The `last` method returns `nil` if no matching record is found and no exception will be raised. -`Model.last!` finds the last record ordered by the primary key. For example: +You can pass in a numerical argument to the `last` method to return up to that number of results. For example ```ruby -client = Client.last! -# => #<Client id: 221, first_name: "Russel"> +client = Client.last(3) +# => [ + #<Client id: 219, first_name: "James">, + #<Client id: 220, first_name: "Sara">, + #<Client id: 221, first_name: "Russel"> +] ``` The SQL equivalent of the above is: ```sql -SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1 +SELECT * FROM clients ORDER BY clients.id DESC LIMIT 3 ``` -`Model.last!` raises `ActiveRecord::RecordNotFound` if no matching record is found. +The `last!` method behaves exactly like `last`, except that it will raise `ActiveRecord::RecordNotFound` if no matching record is found. -#### `find_by!` +#### `find_by` -`Model.find_by!` finds the first record matching some conditions. It raises `ActiveRecord::RecordNotFound` if no matching record is found. For example: +The `find_by` method finds the first record matching some conditions. For example: ```ruby -Client.find_by! first_name: 'Lifo' +Client.find_by first_name: 'Lifo' # => #<Client id: 1, first_name: "Lifo"> -Client.find_by! first_name: 'Jon' -# => ActiveRecord::RecordNotFound +Client.find_by first_name: 'Jon' +# => nil ``` It is equivalent to writing: ```ruby -Client.where(first_name: 'Lifo').take! +Client.where(first_name: 'Lifo').take ``` -### Retrieving Multiple Objects - -#### Using Multiple Primary Keys - -`Model.find(array_of_primary_key)` accepts an array of _primary keys_, returning an array containing all of the matching records for the supplied _primary keys_. For example: +The `find_by!` method behaves exactly like `find_by`, except that it will raise `ActiveRecord::RecordNotFound` if no matching record is found. For example: ```ruby -# Find the clients with primary keys 1 and 10. -client = Client.find([1, 10]) # Or even Client.find(1, 10) -# => [#<Client id: 1, first_name: "Lifo">, #<Client id: 10, first_name: "Ryan">] -``` - -The SQL equivalent of the above is: - -```sql -SELECT * FROM clients WHERE (clients.id IN (1,10)) +Client.find_by! first_name: 'does not exist' +# => ActiveRecord::RecordNotFound ``` -WARNING: `Model.find(array_of_primary_key)` will raise an `ActiveRecord::RecordNotFound` exception unless a matching record is found for **all** of the supplied primary keys. - -#### take - -`Model.take(limit)` retrieves the first number of records specified by `limit` without any explicit ordering: +This is equivalent to writing: ```ruby -Client.take(2) -# => [#<Client id: 1, first_name: "Lifo">, - #<Client id: 2, first_name: "Raf">] -``` - -The SQL equivalent of the above is: - -```sql -SELECT * FROM clients LIMIT 2 +Client.where(first_name: 'does not exist').take! ``` -#### first +#### `last!` -`Model.first(limit)` finds the first number of records specified by `limit` ordered by primary key: +`Model.last!` finds the last record ordered by the primary key. For example: ```ruby -Client.first(2) -# => [#<Client id: 1, first_name: "Lifo">, - #<Client id: 2, first_name: "Raf">] +client = Client.last! +# => #<Client id: 221, first_name: "Russel"> ``` The SQL equivalent of the above is: ```sql -SELECT * FROM clients ORDER BY id ASC LIMIT 2 -``` - -#### last - -`Model.last(limit)` finds the number of records specified by `limit` ordered by primary key in descending order: - -```ruby -Client.last(2) -# => [#<Client id: 10, first_name: "Ryan">, - #<Client id: 9, first_name: "John">] +SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1 ``` -The SQL equivalent of the above is: - -```sql -SELECT * FROM clients ORDER BY id DESC LIMIT 2 -``` +`Model.last!` raises `ActiveRecord::RecordNotFound` if no matching record is found. ### Retrieving Multiple Objects in Batches @@ -344,7 +309,15 @@ The `find_each` method retrieves a batch of records and then yields _each_ recor ```ruby User.find_each do |user| - NewsLetter.weekly_deliver(user) + NewsMailer.weekly(user).deliver +end +``` + +To add conditions to a `find_each` operation you can chain other Active Record methods such as `where`: + +```ruby +User.where(weekly_subscriber: true).find_each do |user| + NewsMailer.weekly(user).deliver end ``` @@ -472,8 +445,8 @@ Client.where('locked' => true) In the case of a belongs_to relationship, an association key can be used to specify the model if an Active Record object is used as the value. This method works with polymorphic relationships as well. ```ruby -Post.where(author: author) -Author.joins(:posts).where(posts: { author: author }) +Article.where(author: author) +Author.joins(:articles).where(articles: { author: author }) ``` NOTE: The values cannot be symbols. For example, you cannot do `Client.where(status: :active)`. @@ -511,7 +484,7 @@ SELECT * FROM clients WHERE (clients.orders_count IN (1,3,5)) `NOT` SQL queries can be built by `where.not`. ```ruby -Post.where.not(author: author) +Article.where.not(author: author) ``` In other words, this query can be generated by calling `where` with no argument, then immediately chain with `not` passing `where` conditions. @@ -659,6 +632,23 @@ FROM orders GROUP BY date(created_at) ``` +### Total of grouped items + +To get the total of grouped items on a single query call `count` after the `group`. + +```ruby +Order.group(:status).count +# => { 'awaiting_approval' => 7, 'paid' => 12 } +``` + +The SQL that would be executed would be something like this: + +```sql +SELECT COUNT (*) AS count_all, status AS status +FROM "orders" +GROUP BY status +``` + Having ------ @@ -690,32 +680,32 @@ Overriding Conditions 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) +Article.where('id > 10').limit(20).order('id asc').unscope(:order) ``` The SQL that would be executed: ```sql -SELECT * FROM posts WHERE id > 10 LIMIT 20 +SELECT * FROM articles WHERE id > 10 LIMIT 20 # Original query without `unscope` -SELECT * FROM posts WHERE id > 10 ORDER BY id asc LIMIT 20 +SELECT * FROM articles WHERE id > 10 ORDER BY id asc LIMIT 20 ``` -You can additionally unscope specific where clauses. For example: +You can also unscope specific `where` clauses. For example: ```ruby -Post.where(id: 10, trashed: false).unscope(where: :id) -# SELECT "posts".* FROM "posts" WHERE trashed = 0 +Article.where(id: 10, trashed: false).unscope(where: :id) +# SELECT "articles".* FROM "articles" WHERE trashed = 0 ``` A relation which has used `unscope` will affect any relation it is merged in to: ```ruby -Post.order('id asc').merge(Post.unscope(:order)) -# SELECT "posts".* FROM "posts" +Article.order('id asc').merge(Article.unscope(:order)) +# SELECT "articles".* FROM "articles" ``` ### `only` @@ -723,16 +713,16 @@ Post.order('id asc').merge(Post.unscope(:order)) You can also override conditions using the `only` method. For example: ```ruby -Post.where('id > 10').limit(20).order('id desc').only(:order, :where) +Article.where('id > 10').limit(20).order('id desc').only(:order, :where) ``` The SQL that would be executed: ```sql -SELECT * FROM posts WHERE id > 10 ORDER BY id DESC +SELECT * FROM articles WHERE id > 10 ORDER BY id DESC # Original query without `only` -SELECT "posts".* FROM "posts" WHERE (id > 10) ORDER BY id desc LIMIT 20 +SELECT "articles".* FROM "articles" WHERE (id > 10) ORDER BY id desc LIMIT 20 ``` @@ -741,25 +731,25 @@ SELECT "posts".* FROM "posts" WHERE (id > 10) ORDER BY id desc LIMIT 20 The `reorder` method overrides the default scope order. For example: ```ruby -class Post < ActiveRecord::Base - .. - .. +class Article < ActiveRecord::Base has_many :comments, -> { order('posted_at DESC') } end -Post.find(10).comments.reorder('name') +Article.find(10).comments.reorder('name') ``` The SQL that would be executed: ```sql -SELECT * FROM posts WHERE id = 10 ORDER BY name +SELECT * FROM articles WHERE id = 10 +SELECT * FROM comments WHERE article_id = 10 ORDER BY name ``` In case the `reorder` clause is not used, the SQL executed would be: ```sql -SELECT * FROM posts WHERE id = 10 ORDER BY posted_at DESC +SELECT * FROM articles WHERE id = 10 +SELECT * FROM comments WHERE article_id = 10 ORDER BY posted_at DESC ``` ### `reverse_order` @@ -795,25 +785,25 @@ This method accepts **no** arguments. The `rewhere` method overrides an existing, named where condition. For example: ```ruby -Post.where(trashed: true).rewhere(trashed: false) +Article.where(trashed: true).rewhere(trashed: false) ``` The SQL that would be executed: ```sql -SELECT * FROM posts WHERE `trashed` = 0 +SELECT * FROM articles WHERE `trashed` = 0 ``` In case the `rewhere` clause is not used, ```ruby -Post.where(trashed: true).where(trashed: false) +Article.where(trashed: true).where(trashed: false) ``` the SQL executed would be: ```sql -SELECT * FROM posts WHERE `trashed` = 1 AND `trashed` = 0 +SELECT * FROM articles WHERE `trashed` = 1 AND `trashed` = 0 ``` Null Relation @@ -822,21 +812,21 @@ Null Relation The `none` method returns a chainable relation with no records. Any subsequent conditions chained to the returned relation will continue generating empty relations. This is useful in scenarios where you need a chainable response to a method or a scope that could return zero results. ```ruby -Post.none # returns an empty Relation and fires no queries. +Article.none # returns an empty Relation and fires no queries. ``` ```ruby -# The visible_posts method below is expected to return a Relation. -@posts = current_user.visible_posts.where(name: params[:name]) +# The visible_articles method below is expected to return a Relation. +@articles = current_user.visible_articles.where(name: params[:name]) -def visible_posts +def visible_articles case role when 'Country Manager' - Post.where(country: country) + Article.where(country: country) when 'Reviewer' - Post.published + Article.published when 'Bad User' - Post.none # => returning [] or nil breaks the caller code in this case + Article.none # => returning [] or nil breaks the caller code in this case end end ``` @@ -963,21 +953,21 @@ 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` clauses for those associations when using the `joins` method. -For example, consider the following `Category`, `Post`, `Comment`, `Guest` and `Tag` models: +For example, consider the following `Category`, `Article`, `Comment`, `Guest` and `Tag` models: ```ruby class Category < ActiveRecord::Base - has_many :posts + has_many :articles end -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base belongs_to :category has_many :comments has_many :tags end class Comment < ActiveRecord::Base - belongs_to :post + belongs_to :article has_one :guest end @@ -986,7 +976,7 @@ class Guest < ActiveRecord::Base end class Tag < ActiveRecord::Base - belongs_to :post + belongs_to :article end ``` @@ -995,64 +985,64 @@ Now all of the following will produce the expected join queries using `INNER JOI #### Joining a Single Association ```ruby -Category.joins(:posts) +Category.joins(:articles) ``` This produces: ```sql SELECT categories.* FROM categories - INNER JOIN posts ON posts.category_id = categories.id + INNER JOIN articles ON articles.category_id = categories.id ``` -Or, in English: "return a Category object for all categories with posts". Note that you will see duplicate categories if more than one post has the same category. If you want unique categories, you can use `Category.joins(:posts).uniq`. +Or, in English: "return a Category object for all categories with articles". Note that you will see duplicate categories if more than one article has the same category. If you want unique categories, you can use `Category.joins(:articles).uniq`. #### Joining Multiple Associations ```ruby -Post.joins(:category, :comments) +Article.joins(:category, :comments) ``` This produces: ```sql -SELECT posts.* FROM posts - INNER JOIN categories ON posts.category_id = categories.id - INNER JOIN comments ON comments.post_id = posts.id +SELECT articles.* FROM articles + INNER JOIN categories ON articles.category_id = categories.id + INNER JOIN comments ON comments.article_id = articles.id ``` -Or, in English: "return all posts that have a category and at least one comment". Note again that posts with multiple comments will show up multiple times. +Or, in English: "return all articles that have a category and at least one comment". Note again that articles with multiple comments will show up multiple times. #### Joining Nested Associations (Single Level) ```ruby -Post.joins(comments: :guest) +Article.joins(comments: :guest) ``` This produces: ```sql -SELECT posts.* FROM posts - INNER JOIN comments ON comments.post_id = posts.id +SELECT articles.* FROM articles + INNER JOIN comments ON comments.article_id = articles.id INNER JOIN guests ON guests.comment_id = comments.id ``` -Or, in English: "return all posts that have a comment made by a guest." +Or, in English: "return all articles that have a comment made by a guest." #### Joining Nested Associations (Multiple Level) ```ruby -Category.joins(posts: [{ comments: :guest }, :tags]) +Category.joins(articles: [{ comments: :guest }, :tags]) ``` This produces: ```sql SELECT categories.* FROM categories - INNER JOIN posts ON posts.category_id = categories.id - INNER JOIN comments ON comments.post_id = posts.id + INNER JOIN articles ON articles.category_id = categories.id + INNER JOIN comments ON comments.article_id = articles.id INNER JOIN guests ON guests.comment_id = comments.id - INNER JOIN tags ON tags.post_id = posts.id + INNER JOIN tags ON tags.article_id = articles.id ``` ### Specifying Conditions on the Joined Tables @@ -1121,18 +1111,18 @@ Active Record lets you eager load any number of associations with a single `Mode #### Array of Multiple Associations ```ruby -Post.includes(:category, :comments) +Article.includes(:category, :comments) ``` -This loads all the posts and the associated category and comments for each post. +This loads all the articles and the associated category and comments for each article. #### Nested Associations Hash ```ruby -Category.includes(posts: [{ comments: :guest }, :tags]).find(1) +Category.includes(articles: [{ 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. +This will find the category with id 1 and eager load all of the associated articles, the associated articles' tags and comments, and every comment's guest association. ### Specifying Conditions on Eager Loaded Associations @@ -1141,18 +1131,31 @@ Even though Active Record lets you specify conditions on the eager loaded associ However if you must do this, you may use `where` as you would normally. ```ruby -Post.includes(:comments).where("comments.visible" => true) +Article.includes(:comments).where(comments: { visible: true }) ``` -This would generate a query which contains a `LEFT OUTER JOIN` whereas the `joins` method would generate one using the `INNER JOIN` function instead. +This would generate a query which contains a `LEFT OUTER JOIN` whereas the +`joins` method would generate one using the `INNER JOIN` function instead. ```ruby - SELECT "posts"."id" AS t0_r0, ... "comments"."updated_at" AS t1_r5 FROM "posts" LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id" WHERE (comments.visible = 1) + SELECT "articles"."id" AS t0_r0, ... "comments"."updated_at" AS t1_r5 FROM "articles" LEFT OUTER JOIN "comments" ON "comments"."article_id" = "articles"."id" WHERE (comments.visible = 1) ``` If there was no `where` condition, this would generate the normal set of two queries. -If, in the case of this `includes` query, there were no comments for any posts, all the posts would still be loaded. By using `joins` (an INNER JOIN), the join conditions **must** match, otherwise no records will be returned. +NOTE: Using `where` like this will only work when you pass it a Hash. For +SQL-fragments you need use `references` to force joined tables: + +```ruby +Article.includes(:comments).where("comments.visible = true").references(:comments) +``` + +If, in the case of this `includes` query, there were no comments for any +articles, all the articles would still be loaded. By using `joins` (an INNER +JOIN), the join conditions **must** match, otherwise no records will be +returned. + + Scopes ------ @@ -1162,7 +1165,7 @@ Scoping allows you to specify commonly-used queries which can be referenced as m To define a simple scope, we use the `scope` method inside the class, passing the query that we'd like to run when this scope is called: ```ruby -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base scope :published, -> { where(published: true) } end ``` @@ -1170,7 +1173,7 @@ end This is exactly the same as defining a class method, and which you use is a matter of personal preference: ```ruby -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base def self.published where(published: true) end @@ -1180,7 +1183,7 @@ end Scopes are also chainable within scopes: ```ruby -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base scope :published, -> { where(published: true) } scope :published_and_commented, -> { published.where("comments_count > 0") } end @@ -1189,14 +1192,14 @@ end To call this `published` scope we can call it on either the class: ```ruby -Post.published # => [published posts] +Article.published # => [published articles] ``` -Or on an association consisting of `Post` objects: +Or on an association consisting of `Article` objects: ```ruby category = Category.first -category.posts.published # => [published posts belonging to this category] +category.articles.published # => [published articles belonging to this category] ``` ### Passing in arguments @@ -1204,7 +1207,7 @@ category.posts.published # => [published posts belonging to this category] Your scope can take arguments: ```ruby -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base scope :created_before, ->(time) { where("created_at < ?", time) } end ``` @@ -1212,13 +1215,13 @@ end Call the scope as if it were a class method: ```ruby -Post.created_before(Time.zone.now) +Article.created_before(Time.zone.now) ``` However, this is just duplicating the functionality that would be provided to you by a class method. ```ruby -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base def self.created_before(time) where("created_at < ?", time) end @@ -1228,7 +1231,7 @@ end Using a class method is the preferred way to accept arguments for scopes. These methods will still be accessible on the association objects: ```ruby -category.posts.created_before(time) +category.articles.created_before(time) ``` ### Applying a default scope @@ -1455,6 +1458,11 @@ If you'd like to use your own SQL to find records in a table you can use `find_b Client.find_by_sql("SELECT * FROM clients INNER JOIN orders ON clients.id = orders.client_id ORDER BY clients.created_at desc") +# => [ + #<Client id: 1, first_name: "Lucas" >, + #<Client id: 2, first_name: "Jan" >, + # ... +] ``` `find_by_sql` provides you with a simple way of making custom calls to the database and retrieving instantiated objects. @@ -1464,7 +1472,11 @@ Client.find_by_sql("SELECT * FROM clients `find_by_sql` has a close relative called `connection#select_all`. `select_all` will retrieve objects from the database using custom SQL just like `find_by_sql` but will not instantiate them. Instead, you will get an array of hashes where each hash indicates a record. ```ruby -Client.connection.select_all("SELECT * FROM clients WHERE id = '1'") +Client.connection.select_all("SELECT first_name, created_at FROM clients WHERE id = '1'") +# => [ + {"first_name"=>"Rafael", "created_at"=>"2012-11-10 23:23:45.281189"}, + {"first_name"=>"Eileen", "created_at"=>"2013-12-09 11:22:35.221282"} +] ``` ### `pluck` @@ -1591,20 +1603,20 @@ You can also use `any?` and `many?` to check for existence on a model or relatio ```ruby # via a model -Post.any? -Post.many? +Article.any? +Article.many? # via a named scope -Post.recent.any? -Post.recent.many? +Article.recent.any? +Article.recent.many? # via a relation -Post.where(published: true).any? -Post.where(published: true).many? +Article.where(published: true).any? +Article.where(published: true).many? # via an association -Post.first.categories.any? -Post.first.categories.many? +Article.first.categories.any? +Article.first.categories.many? ``` Calculations @@ -1694,19 +1706,26 @@ Running EXPLAIN You can run EXPLAIN on the queries triggered by relations. For example, ```ruby -User.where(id: 1).joins(:posts).explain +User.where(id: 1).joins(:articles).explain ``` may yield ``` -EXPLAIN for: SELECT `users`.* FROM `users` INNER JOIN `posts` ON `posts`.`user_id` = `users`.`id` WHERE `users`.`id` = 1 -+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ -| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | -+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ -| 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | | -| 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where | -+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ +EXPLAIN for: SELECT `users`.* FROM `users` INNER JOIN `articles` ON `articles`.`user_id` = `users`.`id` WHERE `users`.`id` = 1 ++----+-------------+----------+-------+---------------+ +| id | select_type | table | type | possible_keys | ++----+-------------+----------+-------+---------------+ +| 1 | SIMPLE | users | const | PRIMARY | +| 1 | SIMPLE | articles | ALL | NULL | ++----+-------------+----------+-------+---------------+ ++---------+---------+-------+------+-------------+ +| key | key_len | ref | rows | Extra | ++---------+---------+-------+------+-------------+ +| PRIMARY | 4 | const | 1 | | +| NULL | NULL | NULL | 1 | Using where | ++---------+---------+-------+------+-------------+ + 2 rows in set (0.00 sec) ``` @@ -1716,15 +1735,15 @@ Active Record performs a pretty printing that emulates the one of the database shells. So, the same query running with the PostgreSQL adapter would yield instead ``` -EXPLAIN for: SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" WHERE "users"."id" = 1 +EXPLAIN for: SELECT "users".* FROM "users" INNER JOIN "articles" ON "articles"."user_id" = "users"."id" WHERE "users"."id" = 1 QUERY PLAN ------------------------------------------------------------------------------ Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0) - Join Filter: (posts.user_id = users.id) + Join Filter: (articles.user_id = users.id) -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4) Index Cond: (id = 1) - -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4) - Filter: (posts.user_id = 1) + -> Seq Scan on articles (cost=0.00..28.88 rows=8 width=4) + Filter: (articles.user_id = 1) (6 rows) ``` @@ -1733,26 +1752,39 @@ may need the results of previous ones. Because of that, `explain` actually executes the query, and then asks for the query plans. For example, ```ruby -User.where(id: 1).includes(:posts).explain +User.where(id: 1).includes(:articles).explain ``` yields ``` EXPLAIN for: SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 -+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+ -| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | -+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+ -| 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | | -+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+ ++----+-------------+-------+-------+---------------+ +| id | select_type | table | type | possible_keys | ++----+-------------+-------+-------+---------------+ +| 1 | SIMPLE | users | const | PRIMARY | ++----+-------------+-------+-------+---------------+ ++---------+---------+-------+------+-------+ +| key | key_len | ref | rows | Extra | ++---------+---------+-------+------+-------+ +| PRIMARY | 4 | const | 1 | | ++---------+---------+-------+------+-------+ + 1 row in set (0.00 sec) -EXPLAIN for: SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` IN (1) -+----+-------------+-------+------+---------------+------+---------+------+------+-------------+ -| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | -+----+-------------+-------+------+---------------+------+---------+------+------+-------------+ -| 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where | -+----+-------------+-------+------+---------------+------+---------+------+------+-------------+ +EXPLAIN for: SELECT `articles`.* FROM `articles` WHERE `articles`.`user_id` IN (1) ++----+-------------+----------+------+---------------+ +| id | select_type | table | type | possible_keys | ++----+-------------+----------+------+---------------+ +| 1 | SIMPLE | articles | ALL | NULL | ++----+-------------+----------+------+---------------+ ++------+---------+------+------+-------------+ +| key | key_len | ref | rows | Extra | ++------+---------+------+------+-------------+ +| NULL | NULL | NULL | 1 | Using where | ++------+---------+------+------+-------------+ + + 1 row in set (0.00 sec) ``` diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md index a483a6dd24..582bb240dd 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -85,7 +85,7 @@ end We can see how it works by looking at some `rails console` output: ```ruby -$ rails console +$ bin/rails console >> p = Person.new(name: "John Doe") => #<Person id: nil, name: "John Doe", created_at: nil, updated_at: nil> >> p.new_record? @@ -871,7 +871,7 @@ should happen, an `Array` can be used. Moreover, you can apply both `:if` and ```ruby class Computer < ActiveRecord::Base validates :mouse, presence: true, - if: ["market.retail?", :desktop?] + if: ["market.retail?", :desktop?], unless: Proc.new { |c| c.trackpad.present? } end ``` @@ -910,8 +910,8 @@ end The easiest way to add custom validators for validating individual attributes is with the convenient `ActiveModel::EachValidator`. In this case, the custom validator class must implement a `validate_each` method which takes three -arguments: record, attribute and value which correspond to the instance, the -attribute to be validated and the value of the attribute in the passed +arguments: record, attribute, and value. These correspond to the instance, the +attribute to be validated, and the value of the attribute in the passed instance. ```ruby @@ -1129,15 +1129,15 @@ generating a scaffold, Rails will put some ERB into the `_form.html.erb` that it generates that displays the full list of errors on that model. Assuming we have a model that's been saved in an instance variable named -`@post`, it looks like this: +`@article`, it looks like this: ```ruby -<% if @post.errors.any? %> +<% if @article.errors.any? %> <div id="error_explanation"> - <h2><%= pluralize(@post.errors.count, "error") %> prohibited this post from being saved:</h2> + <h2><%= pluralize(@article.errors.count, "error") %> prohibited this article from being saved:</h2> <ul> - <% @post.errors.full_messages.each do |msg| %> + <% @article.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> @@ -1151,7 +1151,7 @@ the entry. ``` <div class="field_with_errors"> - <input id="post_title" name="post[title]" size="30" type="text" value=""> + <input id="article_title" name="article[title]" size="30" type="text" value=""> </div> ``` diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md index 5a4e15cfa9..5ed392d43d 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -572,12 +572,12 @@ NOTE: Defined in `active_support/core_ext/module/aliasing.rb`. #### `alias_attribute` -Model attributes have a reader, a writer, and a predicate. You can alias a model attribute having the corresponding three methods defined for you in one shot. As in other aliasing methods, the new name is the first argument, and the old name is the second (my mnemonic is they go in the same order as if you did an assignment): +Model attributes have a reader, a writer, and a predicate. You can alias a model attribute having the corresponding three methods defined for you in one shot. As in other aliasing methods, the new name is the first argument, and the old name is the second (one mnemonic is that they go in the same order as if you did an assignment): ```ruby class User < ActiveRecord::Base - # let me refer to the email column as "login", - # possibly meaningful for authentication code + # You can refer to the email column as "login". + # This can be meaningful for authentication code. alias_attribute :login, :email end ``` @@ -1106,7 +1106,7 @@ end A model may find it useful to set `:instance_accessor` to `false` as a way to prevent mass-assignment from setting the attribute. -NOTE: Defined in `active_support/core_ext/module/attribute_accessors.rb`. `active_support/core_ext/class/attribute_accessors.rb` is deprecated and will be removed in Ruby on Rails 4.2. +NOTE: Defined in `active_support/core_ext/module/attribute_accessors.rb`. ### Subclasses & Descendants @@ -1165,9 +1165,9 @@ Inserting data into HTML templates needs extra care. For example, you can't just #### Safe Strings -Active Support has the concept of <i>(html) safe</i> strings. A safe string is one that is marked as being insertable into HTML as is. It is trusted, no matter whether it has been escaped or not. +Active Support has the concept of _(html) safe_ strings. A safe string is one that is marked as being insertable into HTML as is. It is trusted, no matter whether it has been escaped or not. -Strings are considered to be <i>unsafe</i> by default: +Strings are considered to be _unsafe_ by default: ```ruby "".html_safe? # => false @@ -1768,21 +1768,36 @@ NOTE: Defined in `active_support/core_ext/string/inflections.rb`. #### `humanize` -The method `humanize` gives you a sensible name for display out of an attribute name. To do so it replaces underscores with spaces, removes any "_id" suffix, and capitalizes the first word: +The method `humanize` tweaks an attribute name for display to end users. + +Specifically performs these transformations: + + * Applies human inflection rules to the argument. + * Deletes leading underscores, if any. + * Removes a "_id" suffix if present. + * Replaces underscores with spaces, if any. + * Downcases all words except acronyms. + * Capitalizes the first word. + +The capitalization of the first word can be turned off by setting the ++:capitalize+ option to false (default is true). ```ruby -"name".humanize # => "Name" -"author_id".humanize # => "Author" -"comments_count".humanize # => "Comments count" +"name".humanize # => "Name" +"author_id".humanize # => "Author" +"author_id".humanize(capitalize: false) # => "author" +"comments_count".humanize # => "Comments count" +"_id".humanize # => "Id" ``` -The capitalization of the first word can be turned off by setting the optional parameter `capitalize` to false: +If "SSL" was defined to be an acronym: ```ruby -"author_id".humanize(capitalize: false) # => "author" +'ssl_error'.humanize # => "SSL error" ``` -The helper method `full_messages` uses `humanize` as a fallback to include attribute names: +The helper method `full_messages` uses `humanize` as a fallback to include +attribute names: ```ruby def full_messages @@ -3657,9 +3672,9 @@ t.advance(seconds: 1) #### `Time.current` -Active Support defines `Time.current` to be today in the current time zone. That's like `Time.now`, except that it honors the user time zone, if defined. It also defines `Time.yesterday` and `Time.tomorrow`, and the instance predicates `past?`, `today?`, and `future?`, all of them relative to `Time.current`. +Active Support defines `Time.current` to be today in the current time zone. That's like `Time.now`, except that it honors the user time zone, if defined. It also defines the instance predicates `past?`, `today?`, and `future?`, all of them relative to `Time.current`. -When making Time comparisons using methods which honor the user time zone, make sure to use `Time.current` and not `Time.now`. There are cases where the user time zone might be in the future compared to the system time zone, which `Time.today` uses by default. This means `Time.now` may equal `Time.yesterday`. +When making Time comparisons using methods which honor the user time zone, make sure to use `Time.current` instead of `Time.now`. There are cases where the user time zone might be in the future compared to the system time zone, which `Time.now` uses by default. This means `Time.now.to_date` may equal `Date.yesterday`. #### `all_day`, `all_week`, `all_month`, `all_quarter` and `all_year` @@ -3823,7 +3838,7 @@ The name may be given as a symbol or string. A symbol is tested against the bare TIP: A symbol can represent a fully-qualified constant name as in `:"ActiveRecord::Base"`, so the behavior for symbols is defined for convenience, not because it has to be that way technically. -For example, when an action of `PostsController` is called Rails tries optimistically to use `PostsHelper`. It is OK that the helper module does not exist, so if an exception for that constant name is raised it should be silenced. But it could be the case that `posts_helper.rb` raises a `NameError` due to an actual unknown constant. That should be reraised. The method `missing_name?` provides a way to distinguish both cases: +For example, when an action of `ArticlesController` is called Rails tries optimistically to use `ArticlesHelper`. It is OK that the helper module does not exist, so if an exception for that constant name is raised it should be silenced. But it could be the case that `articles_helper.rb` raises a `NameError` due to an actual unknown constant. That should be reraised. The method `missing_name?` provides a way to distinguish both cases: ```ruby def default_helper_module! @@ -3846,7 +3861,7 @@ Active Support adds `is_missing?` to `LoadError`, and also assigns that class to Given a path name `is_missing?` tests whether the exception was raised due to that particular file (except perhaps for the ".rb" extension). -For example, when an action of `PostsController` is called Rails tries to load `posts_helper.rb`, but that file may not exist. That's fine, the helper module is not mandatory so Rails silences a load error. But it could be the case that the helper module does exist and in turn requires another library that is missing. In that case Rails must reraise the exception. The method `is_missing?` provides a way to distinguish both cases: +For example, when an action of `ArticlesController` is called Rails tries to load `articles_helper.rb`, but that file may not exist. That's fine, the helper module is not mandatory so Rails silences a load error. But it could be the case that the helper module does exist and in turn requires another library that is missing. In that case Rails must reraise the exception. The method `is_missing?` provides a way to distinguish both cases: ```ruby def default_helper_module! diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md index 121cdc0199..7033947468 100644 --- a/guides/source/active_support_instrumentation.md +++ b/guides/source/active_support_instrumentation.md @@ -17,7 +17,7 @@ After reading this guide, you will know: Introduction to instrumentation ------------------------------- -The instrumentation API provided by Active Support allows developers to provide hooks which other developers may hook into. There are several of these within the Rails framework, as described below in <TODO: link to section detailing each hook point>. With this API, developers can choose to be notified when certain events occur inside their application or another piece of Ruby code. +The instrumentation API provided by Active Support allows developers to provide hooks which other developers may hook into. There are several of these within the Rails framework, as described below in (TODO: link to section detailing each hook point). With this API, developers can choose to be notified when certain events occur inside their application or another piece of Ruby code. For example, there is a hook provided within Active Record that is called every time Active Record uses an SQL query on a database. This hook could be **subscribed** to, and used to track the number of queries during a certain action. There's another hook around the processing of an action of a controller. This could be used, for instance, to track how long a specific action has taken. @@ -364,7 +364,7 @@ INFO. Options passed to fetch will be merged with the payload. | ------ | --------------------- | | `:key` | Key used in the store | -INFO. Cache stores my add their own keys +INFO. Cache stores may add their own keys ```ruby { diff --git a/guides/source/api_documentation_guidelines.md b/guides/source/api_documentation_guidelines.md index 261538d0be..a2ebf55335 100644 --- a/guides/source/api_documentation_guidelines.md +++ b/guides/source/api_documentation_guidelines.md @@ -13,7 +13,19 @@ After reading this guide, you will know: RDoc ---- -The Rails API documentation is generated with RDoc. Please consult the documentation for help with the [markup](http://rdoc.rubyforge.org/RDoc/Markup.html), and also take into account these [additional directives](http://rdoc.rubyforge.org/RDoc/Parser/Ruby.html). +The [Rails API documentation](http://api.rubyonrails.org) is generated with +[RDoc](http://docs.seattlerb.org/rdoc/). + +```bash + bundle exec rake rdoc +``` + +Resulting HTML files can be found in the ./doc/rdoc directory. + +Please consult the RDoc documentation for help with the +[markup](http://docs.seattlerb.org/rdoc/RDoc/Markup.html), +and also take into account these [additional +directives](http://docs.seattlerb.org/rdoc/RDoc/Parser/Ruby.html). Wording ------- @@ -67,7 +79,7 @@ used. Instead of: 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 (*color*, *center*, *modularize*, 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 ------------ @@ -110,14 +122,14 @@ The results of expressions follow them and are introduced by "# => ", vertically If a line is too long, the comment may be placed on the next line: ```ruby -# label(:post, :title) -# # => <label for="post_title">Title</label> +# label(:article, :title) +# # => <label for="article_title">Title</label> # -# label(:post, :title, "A short title") -# # => <label for="post_title">A short title</label> +# label(:article, :title, "A short title") +# # => <label for="article_title">A short title</label> # -# label(:post, :title, "A short title", class: "title_label") -# # => <label for="post_title" class="title_label">A short title</label> +# label(:article, :title, "A short title", class: "title_label") +# # => <label for="article_title" class="title_label">A short title</label> ``` Avoid using any printing methods like `puts` or `p` for that purpose. @@ -175,8 +187,8 @@ end The API is careful not to commit to any particular value, the method has predicate semantics, that's enough. -Filenames ---------- +File Names +---------- As a rule of thumb, use filenames relative to the application root: @@ -286,3 +298,64 @@ self.class_eval %{ end } ``` + +Method Visibility +----------------- + +When writing documentation for Rails, it's important to understand the difference between public user-facing API vs internal API. + +Rails, like most libraries, uses the private keyword from Ruby for defining internal API. However, public API follows a slightly different convention. Instead of assuming all public methods are designed for user consumption, Rails uses the `:nodoc:` directive to annotate these kinds of methods as internal API. + +This means that there are methods in Rails with `public` visibility that aren't meant for user consumption. + +An example of this is `ActiveRecord::Core::ClassMethods#arel_table`: + +```ruby +module ActiveRecord::Core::ClassMethods + def arel_table #:nodoc: + # do some magic.. + end +end +``` + +If you thought, "this method looks like a public class method for `ActiveRecord::Core`", you were right. But actually the Rails team doesn't want users to rely on this method. So they mark it as `:nodoc:` and it's removed from public documentation. The reasoning behind this is to allow the team to change these methods according to their internal needs across releases as they see fit. The name of this method could change, or the return value, or this entire class may disappear; there's no guarantee and so you shouldn't depend on this API in your plugins or applications. Otherwise, you risk your app or gem breaking when you upgrade to a newer release of Rails. + +As a contributor, it's important to think about whether this API is meant for end-user consumption. The Rails team is committed to not making any breaking changes to public API across releases without going through a full deprecation cycle. It's recommended that you `:nodoc:` any of your internal methods/classes unless they're already private (meaning visibility), in which case it's internal by default. Once the API stabilizes the visibility can change, but changing public API is much harder due to backwards compatibility. + +A class or module is marked with `:nodoc:` to indicate that all methods are internal API and should never be used directly. + +If you come across an existing `:nodoc:` you should tread lightly. Consider asking someone from the core team or author of the code before removing it. This should almost always happen through a pull request instead of the docrails project. + +A `:nodoc:` should never be added simply because a method or class is missing documentation. There may be an instance where an internal public method wasn't given a `:nodoc:` by mistake, for example when switching a method from private to public visibility. When this happens it should be discussed over a PR on a case-by-case basis and never committed directly to docrails. + +To summarize, the Rails team uses `:nodoc:` to mark publicly visible methods and classes for internal use; changes to the visibility of API should be considered carefully and discussed over a pull request first. + +Regarding the Rails Stack +------------------------- + +When documenting parts of Rails API, it's important to remember all of the +pieces that go into the Rails stack. + +This means that behavior may change depending on the scope or context of the +method or class you're trying to document. + +In various places there is different behavior when you take the entire stack +into account, one such example is +`ActionView::Helpers::AssetTagHelper#image_tag`: + +```ruby +# image_tag("icon.png") +# # => <img alt="Icon" src="/assets/icon.png" /> +``` + +Although the default behavior for `#image_tag` is to always return +`/images/icon.png`, we take into account the full Rails stack (including the +Asset Pipeline) we may see the result seen above. + +We're only concerned with the behavior experienced when using the full default +Rails stack. + +In this case, we want to document the behavior of the _framework_, and not just +this specific method. + +If you have a question on how the Rails team handles certain API, don't hesitate to open a ticket or send a patch to the [issue tracker](https://github.com/rails/rails/issues). diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index 52fc9726d9..e31cefa5bb 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -124,19 +124,22 @@ with a built-in helper. In the source the generated code looked like this: The query string strategy has several disadvantages: 1. **Not all caches will reliably cache content where the filename only differs by -query parameters**<br> +query parameters** + [Steve Souders recommends](http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/), "...avoiding a querystring for cacheable resources". He found that in this case 5-20% of requests will not be cached. Query strings in particular do not work at all with some CDNs for cache invalidation. -2. **The file name can change between nodes in multi-server environments.**<br> +2. **The file name can change between nodes in multi-server environments.** + The default query string in Rails 2.x is based on the modification time of the files. When assets are deployed to a cluster, there is no guarantee that the timestamps will be the same, resulting in different values being used depending on which server handles the request. -3. **Too much cache invalidation**<br> +3. **Too much cache invalidation** + When static assets are deployed with each new release of code, the mtime (time of last modification) of _all_ these files changes, forcing all remote clients to fetch them again, even when the content of those assets has not changed. @@ -198,12 +201,9 @@ will result in your assets being included more than once. WARNING: When using asset precompilation, you will need to ensure that your controller assets will be precompiled when loading them on a per page basis. By -default .coffee and .scss files will not be precompiled on their own. This will -result in false positives during development as these files will work just fine -since assets are compiled on the fly in development mode. When running in -production, however, you will see 500 errors since live compilation is turned -off by default. See [Precompiling Assets](#precompiling-assets) for more -information on how precompiling works. +default .coffee and .scss files will not be precompiled on their own. See +[Precompiling Assets](#precompiling-assets) for more information on how +precompiling works. NOTE: You must have an ExecJS supported runtime in order to use CoffeeScript. If you are using Mac OS X or Windows, you have a JavaScript runtime installed in @@ -493,14 +493,13 @@ The directives that work in JavaScript files also work in stylesheets one, requiring all stylesheets from the current directory. In this example, `require_self` is used. This puts the CSS contained within the -file (if any) at the precise location of the `require_self` call. If -`require_self` is called more than once, only the last call is respected. +file (if any) at the precise location of the `require_self` call. NOTE. If you want to use multiple Sass files, you should generally use the [Sass `@import` 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 +instead of these Sprockets directives. When using Sprockets directives, Sass files exist within their own scope, making variables or mixins only available within the 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 do file globbing as well using `@import "*"`, and `@import "**/*"` to add the whole tree which is 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 @@ -581,8 +580,21 @@ runtime. To disable this behavior you can set: config.assets.raise_runtime_errors = false ``` -When this option is true asset pipeline will check if all the assets loaded in your application -are included in the `config.assets.precompile` list. +When this option is true, the asset pipeline will check if all the assets loaded +in your application are included in the `config.assets.precompile` list. +If `config.assets.digest` is also true, the asset pipeline will require that +all requests for assets include digests. + +### Turning Digests Off + +You can turn off digests by updating `config/environments/development.rb` to +include: + +```ruby +config.assets.digest = false +``` + +When this option is true, digests will be generated for asset URLs. ### Turning Debugging Off @@ -676,7 +688,7 @@ information on compiling locally. The rake task is: ```bash -$ RAILS_ENV=production bundle exec rake assets:precompile +$ RAILS_ENV=production bin/rake assets:precompile ``` Capistrano (v2.15.1 and above) includes a recipe to handle this in deployment. @@ -699,7 +711,7 @@ The default matcher for compiling files includes `application.js`, automatically) from `app/assets` folders including your gems: ```ruby -[ Proc.new { |path, fn| fn =~ /app\/assets/ && !%w(.js .css).include?(File.extname(path)) }, +[ Proc.new { |filename, path| path =~ /app\/assets/ && !%w(.js .css).include?(File.extname(filename)) }, /application.(css|js)$/ ] ``` @@ -750,7 +762,7 @@ typical manifest file looks like: "digest":"12b3c7dd74d2e9df37e7cbb1efa76a6d"},"application-1c5752789588ac18d7e1a50b1f0fd4c2.css":{"logical_path":"application.css","mtime":"2013-07-26T22:56:17-07:00","size":1591, "digest":"1c5752789588ac18d7e1a50b1f0fd4c2"},"favicon-a9c641bf2b81f0476e876f7c5e375969.ico":{"logical_path":"favicon.ico","mtime":"2013-07-26T23:00:10-07:00","size":1406, "digest":"a9c641bf2b81f0476e876f7c5e375969"},"my_image-231a680f23887d9dd70710ea5efd3c62.png":{"logical_path":"my_image.png","mtime":"2013-07-26T23:00:27-07:00","size":6646, -"digest":"231a680f23887d9dd70710ea5efd3c62"}},"assets"{"application.js": +"digest":"231a680f23887d9dd70710ea5efd3c62"}},"assets":{"application.js": "application-723d1be6cc741a3aabb1cec24276d681.js","application.css": "application-1c5752789588ac18d7e1a50b1f0fd4c2.css", "favicon.ico":"favicona9c641bf2b81f0476e876f7c5e375969.ico","my_image.png": @@ -778,13 +790,15 @@ For Apache: # `mod_expires` to be enabled. <Location /assets/> # Use of ETag is discouraged when Last-Modified is present - Header unset ETag FileETag None + Header unset ETag + FileETag None # RFC says only cache for 1 year - ExpiresActive On ExpiresDefault "access plus 1 year" + ExpiresActive On + ExpiresDefault "access plus 1 year" </Location> ``` -For nginx: +For NGINX: ```nginx location ~ ^/assets/ { @@ -806,7 +820,7 @@ compression ratio, thus reducing the size of the data transfer to the minimum. On the other hand, web servers can be configured to serve compressed content directly from disk, rather than deflating non-compressed files themselves. -Nginx is able to do this automatically enabling `gzip_static`: +NGINX is able to do this automatically enabling `gzip_static`: ```nginx location ~ ^/(assets)/ { @@ -825,7 +839,7 @@ the module compiled. Otherwise, you may need to perform a manual compilation: ./configure --with-http_gzip_static_module ``` -If you're compiling nginx with Phusion Passenger you'll need to pass that option +If you're compiling NGINX with Phusion Passenger you'll need to pass that option when prompted. A robust configuration for Apache is possible but tricky; please Google around. @@ -844,10 +858,12 @@ duplication of work. Local compilation allows you to commit the compiled files into source control, and deploy as normal. -There are two caveats: +There are three caveats: * You must not run the Capistrano deployment task that precompiles assets. -* You must change the following two application configuration settings. +* You must ensure any necessary compressors or minifiers are +available on your development system. +* You must change the following application configuration setting: In `config/environments/development.rb`, place the following line: @@ -861,9 +877,6 @@ development mode, and pass all requests to Sprockets. The prefix is still set to would serve the precompiled assets from `/assets` in development, and you would not see any local changes until you compile assets again. -You will also need to ensure any necessary compressors or minifiers are -available on your development system. - In practice, this will allow you to precompile locally, have those files in your working tree, and commit those files to source control when needed. Development mode will work as expected. @@ -910,9 +923,17 @@ cache forever. This can cause problems. If you use Every cache is different, so evaluate how your CDN handles caching and make sure that it plays nicely with the pipeline. You may find quirks related to your -specific set up, you may not. The defaults nginx uses, for example, should give +specific set up, you may not. The defaults NGINX uses, for example, should give you no problems when used as an HTTP cache. +If you want to serve only some assets from your CDN, you can use custom +`:host` option of `asset_url` helper, which overwrites value set in +`config.action_controller.asset_host`. + +```ruby +asset_url 'image.png', :host => 'http://cdn.example.com' +``` + Customizing the Pipeline ------------------------ @@ -1006,12 +1027,12 @@ this passes responsibility for serving the file to the web server, which is faster. Have a look at [send_file](http://api.rubyonrails.org/classes/ActionController/DataStreaming.html#method-i-send_file) on how to use this feature. -Apache and nginx support this option, which can be enabled in +Apache and NGINX support this option, which can be enabled in `config/environments/production.rb`: ```ruby -# config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache -# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx +# config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache +# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX ``` WARNING: If you are upgrading an existing application and intend to use this @@ -1021,7 +1042,7 @@ and any other environments you define with production behavior (not TIP: For further details have a look at the docs of your production web server: - [Apache](https://tn123.org/mod_xsendfile/) -- [Nginx](http://wiki.nginx.org/XSendfile) +- [NGINX](http://wiki.nginx.org/XSendfile) Assets Cache Store ------------------ diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index df38bd7321..daf4113b66 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -105,7 +105,7 @@ class CreateOrders < ActiveRecord::Migration end create_table :orders do |t| - t.belongs_to :customer + t.belongs_to :customer, index: true t.datetime :order_date t.timestamps end @@ -136,7 +136,7 @@ class CreateSuppliers < ActiveRecord::Migration end create_table :accounts do |t| - t.belongs_to :supplier + t.belongs_to :supplier, index: true t.string :account_number t.timestamps end @@ -169,7 +169,7 @@ class CreateCustomers < ActiveRecord::Migration end create_table :orders do |t| - t.belongs_to :customer + t.belongs_to :customer, index:true t.datetime :order_date t.timestamps end @@ -216,8 +216,8 @@ class CreateAppointments < ActiveRecord::Migration end create_table :appointments do |t| - t.belongs_to :physician - t.belongs_to :patient + t.belongs_to :physician, index: true + t.belongs_to :patient, index: true t.datetime :appointment_date t.timestamps end @@ -295,13 +295,13 @@ class CreateAccountHistories < ActiveRecord::Migration end create_table :accounts do |t| - t.belongs_to :supplier + t.belongs_to :supplier, index: true t.string :account_number t.timestamps end create_table :account_histories do |t| - t.belongs_to :account + t.belongs_to :account, index: true t.integer :credit_rating t.timestamps end @@ -341,8 +341,8 @@ class CreateAssembliesAndParts < ActiveRecord::Migration end create_table :assemblies_parts, id: false do |t| - t.belongs_to :assembly - t.belongs_to :part + t.belongs_to :assembly, index: true + t.belongs_to :part, index: true end end end @@ -379,6 +379,8 @@ class CreateSuppliers < ActiveRecord::Migration t.string :account_number t.timestamps end + + add_index :accounts, :supplier_id end end ``` @@ -455,6 +457,8 @@ class CreatePictures < ActiveRecord::Migration t.string :imageable_type t.timestamps end + + add_index :pictures, :imageable_id end end ``` @@ -466,7 +470,7 @@ class CreatePictures < ActiveRecord::Migration def change create_table :pictures do |t| t.string :name - t.references :imageable, polymorphic: true + t.references :imageable, polymorphic: true, index: true t.timestamps end end @@ -496,7 +500,7 @@ In your migrations/schema, you will add a references column to the model itself. class CreateEmployees < ActiveRecord::Migration def change create_table :employees do |t| - t.references :manager + t.references :manager, index: true t.timestamps end end @@ -561,6 +565,8 @@ class CreateOrders < ActiveRecord::Migration t.string :order_number t.integer :customer_id end + + add_index :orders, :customer_id end end ``` @@ -594,6 +600,9 @@ class CreateAssembliesPartsJoinTable < ActiveRecord::Migration t.integer :assembly_id t.integer :part_id end + + add_index :assemblies_parts, :assembly_id + add_index :assemblies_parts, :part_id end end ``` @@ -1131,7 +1140,7 @@ The `has_one` association supports these options: ##### `:as` -Setting the `:as` option indicates that this is a polymorphic association. Polymorphic associations were discussed in detail <a href="#polymorphic-associations">earlier in this guide</a>. +Setting the `:as` option indicates that this is a polymorphic association. Polymorphic associations were discussed in detail [earlier in this guide](#polymorphic-associations). ##### `:autosave` @@ -1203,7 +1212,7 @@ The `:source_type` option specifies the source association type for a `has_one : ##### `:through` -The `:through` option specifies a join model through which to perform the query. `has_one :through` associations were discussed in detail <a href="#the-has-one-through-association">earlier in this guide</a>. +The `:through` option specifies a join model through which to perform the query. `has_one :through` associations were discussed in detail [earlier in this guide](#the-has-one-through-association). ##### `:validate` @@ -1497,7 +1506,7 @@ The `has_many` association supports these options: ##### `:as` -Setting the `:as` option indicates that this is a polymorphic association, as discussed <a href="#polymorphic-associations">earlier in this guide</a>. +Setting the `:as` option indicates that this is a polymorphic association, as discussed [earlier in this guide](#polymorphic-associations). ##### `:autosave` @@ -1579,7 +1588,7 @@ The `:source_type` option specifies the source association type for a `has_many ##### `:through` -The `:through` option specifies a join model through which to perform the query. `has_many :through` associations provide a way to implement many-to-many relationships, as discussed <a href="#the-has-many-through-association">earlier in this guide</a>. +The `:through` option specifies a join model through which to perform the query. `has_many :through` associations provide a way to implement many-to-many relationships, as discussed [earlier in this guide](#the-has-many-through-association). ##### `:validate` @@ -1632,7 +1641,7 @@ If you use a hash-style `where` option, then record creation via this associatio ##### `extending` -The `extending` method specifies a named module to extend the association proxy. Association extensions are discussed in detail <a href="#association-extensions">later in this guide</a>. +The `extending` method specifies a named module to extend the association proxy. Association extensions are discussed in detail [later in this guide](#association-extensions). ##### `group` @@ -1725,58 +1734,58 @@ mostly useful together with the `:through` option. ```ruby class Person < ActiveRecord::Base has_many :readings - has_many :posts, through: :readings + has_many :articles, through: :readings end person = Person.create(name: 'John') -post = Post.create(name: 'a1') -person.posts << post -person.posts << post -person.posts.inspect # => [#<Post id: 5, name: "a1">, #<Post id: 5, name: "a1">] -Reading.all.inspect # => [#<Reading id: 12, person_id: 5, post_id: 5>, #<Reading id: 13, person_id: 5, post_id: 5>] +article = Article.create(name: 'a1') +person.articles << article +person.articles << article +person.articles.inspect # => [#<Article id: 5, name: "a1">, #<Article id: 5, name: "a1">] +Reading.all.inspect # => [#<Reading id: 12, person_id: 5, article_id: 5>, #<Reading id: 13, person_id: 5, article_id: 5>] ``` -In the above case there are two readings and `person.posts` brings out both of -them even though these records are pointing to the same post. +In the above case there are two readings and `person.articles` brings out both of +them even though these records are pointing to the same article. Now let's set `distinct`: ```ruby class Person has_many :readings - has_many :posts, -> { distinct }, through: :readings + has_many :articles, -> { distinct }, through: :readings end person = Person.create(name: 'Honda') -post = Post.create(name: 'a1') -person.posts << post -person.posts << post -person.posts.inspect # => [#<Post id: 7, name: "a1">] -Reading.all.inspect # => [#<Reading id: 16, person_id: 7, post_id: 7>, #<Reading id: 17, person_id: 7, post_id: 7>] +article = Article.create(name: 'a1') +person.articles << article +person.articles << article +person.articles.inspect # => [#<Article id: 7, name: "a1">] +Reading.all.inspect # => [#<Reading id: 16, person_id: 7, article_id: 7>, #<Reading id: 17, person_id: 7, article_id: 7>] ``` -In the above case there are still two readings. However `person.posts` shows -only one post because the collection loads only unique records. +In the above case there are still two readings. However `person.articles` shows +only one article because the collection loads only unique records. If you want to make sure that, upon insertion, all of the records in the persisted association are distinct (so that you can be sure that when you inspect the association that you will never find duplicate records), you should add a unique index on the table itself. For example, if you have a table named -`person_posts` and you want to make sure all the posts are unique, you could +`person_articles` and you want to make sure all the articles are unique, you could add the following in a migration: ```ruby -add_index :person_posts, :post, unique: true +add_index :person_articles, :article, unique: true ``` Note that checking for uniqueness using something like `include?` is subject to race conditions. Do not attempt to use `include?` to enforce distinctness -in an association. For instance, using the post example from above, the +in an association. For instance, using the article example from above, the following code would be racy because multiple users could be attempting this at the same time: ```ruby -person.posts << post unless person.posts.include?(post) +person.articles << article unless person.articles.include?(article) ``` #### When are Objects Saved? @@ -2082,7 +2091,7 @@ If you use a hash-style `where`, then record creation via this association will ##### `extending` -The `extending` method specifies a named module to extend the association proxy. Association extensions are discussed in detail <a href="#association-extensions">later in this guide</a>. +The `extending` method specifies a named module to extend the association proxy. Association extensions are discussed in detail [later in this guide](#association-extensions). ##### `group` diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md index b6423dd44e..0902e347e2 100644 --- a/guides/source/caching_with_rails.md +++ b/guides/source/caching_with_rails.md @@ -28,7 +28,7 @@ config.action_controller.perform_caching = true ### Page Caching -Page caching is a Rails mechanism which allows the request for a generated page to be fulfilled by the webserver (i.e. Apache or nginx), without ever having to go through the Rails stack at all. Obviously, this is super-fast. Unfortunately, it can't be applied to every situation (such as pages that need authentication) and since the webserver is literally just serving a file from the filesystem, cache expiration is an issue that needs to be dealt with. +Page caching is a Rails mechanism which allows the request for a generated page to be fulfilled by the webserver (i.e. Apache or NGINX), without ever having to go through the Rails stack at all. Obviously, this is super-fast. Unfortunately, it can't be applied to every situation (such as pages that need authentication) and since the webserver is literally just serving a file from the filesystem, cache expiration is an issue that needs to be dealt with. INFO: Page Caching has been removed from Rails 4. See the [actionpack-page_caching gem](https://github.com/rails/actionpack-page_caching). See [DHH's key-based cache expiration overview](http://signalvnoise.com/posts/3113-how-key-based-cache-expiration-works) for the newly-preferred method. @@ -105,7 +105,7 @@ This method generates a cache key that depends on all products and can be used i <% end %> ``` -If you want to cache a fragment under certain condition you can use `cache_if` or `cache_unless` +If you want to cache a fragment under certain condition you can use `cache_if` or `cache_unless` ```erb <% cache_if (condition, cache_key_for_products) do %> @@ -185,7 +185,7 @@ end Cache Stores ------------ -Rails provides different stores for the cached data created by <b>action</b> and <b>fragment</b> caches. +Rails provides different stores for the cached data created by **action** and **fragment** caches. TIP: Page caches are always stored on disk. diff --git a/guides/source/command_line.md b/guides/source/command_line.md index 756c8f8b51..3a78c3bb3f 100644 --- a/guides/source/command_line.md +++ b/guides/source/command_line.md @@ -7,7 +7,6 @@ After reading this guide, you will know: * How to generate models, controllers, database migrations, and unit tests. * How to start a development server. * How to experiment with objects through an interactive shell. -* How to profile and benchmark your new creation. -------------------------------------------------------------------------------- @@ -60,9 +59,9 @@ With no further work, `rails server` will run our new shiny Rails app: ```bash $ cd commandsapp -$ rails server +$ bin/rails server => Booting WEBrick -=> Rails 4.0.0 application starting in development on http://0.0.0.0:3000 +=> Rails 4.2.0 application starting in development on http://0.0.0.0:3000 => Call with -d to detach => Ctrl-C to shutdown server [2013-08-07 02:00:01] INFO WEBrick 1.3.1 @@ -77,7 +76,7 @@ INFO: You can also use the alias "s" to start the server: `rails s`. The server can be run on a different port using the `-p` option. The default development environment can be changed using `-e`. ```bash -$ rails server -e production -p 4000 +$ bin/rails server -e production -p 4000 ``` The `-b` option binds Rails to the specified IP, by default it is 0.0.0.0. You can run a server as a daemon by passing a `-d` option. @@ -89,7 +88,7 @@ The `rails generate` command uses templates to create a whole lot of things. Run INFO: You can also use the alias "g" to invoke the generator command: `rails g`. ```bash -$ rails generate +$ bin/rails generate Usage: rails generate GENERATOR [args] [options] ... @@ -114,7 +113,7 @@ Let's make our own controller with the controller generator. But what command sh INFO: All Rails console utilities have help text. As with most *nix utilities, you can try adding `--help` or `-h` to the end, for example `rails server --help`. ```bash -$ rails generate controller +$ bin/rails generate controller Usage: rails generate controller NAME [action action] [options] ... @@ -123,25 +122,24 @@ Usage: rails generate controller NAME [action action] [options] Description: ... - To create a controller within a module, specify the controller name as a - path like 'parent_module/controller_name'. + To create a controller within a module, specify the controller name as a path like 'parent_module/controller_name'. ... Example: - `rails generate controller CreditCard open debit credit close` + `rails generate controller CreditCards open debit credit close` - Credit card controller with URLs like /credit_card/debit. + Credit card controller with URLs like /credit_cards/debit. Controller: app/controllers/credit_card_controller.rb - Test: test/controllers/credit_card_controller_test.rb - Views: app/views/credit_card/debit.html.erb [...] - Helper: app/helpers/credit_card_helper.rb + Test: test/controllers/credit_cards_controller_test.rb + Views: app/views/credit_cards/debit.html.erb [...] + Helper: app/helpers/credit_cards_helper.rb ``` The controller generator is expecting parameters in the form of `generate controller ControllerName action1 action2`. Let's make a `Greetings` controller with an action of **hello**, which will say something nice to us. ```bash -$ rails generate controller Greetings hello +$ bin/rails generate controller Greetings hello create app/controllers/greetings_controller.rb route get "greetings/hello" invoke erb @@ -182,7 +180,7 @@ Then the view, to display our message (in `app/views/greetings/hello.html.erb`): Fire up your server using `rails server`. ```bash -$ rails server +$ bin/rails server => Booting WEBrick... ``` @@ -193,7 +191,7 @@ INFO: With a normal, plain-old Rails application, your URLs will generally follo Rails comes with a generator for data models too. ```bash -$ rails generate model +$ bin/rails generate model Usage: rails generate model NAME [field[:type][:index] field[:type][:index]] [options] @@ -216,7 +214,7 @@ But instead of generating a model directly (which we'll be doing later), let's s We will set up a simple resource called "HighScore" that will keep track of our highest score on video games we play. ```bash -$ rails generate scaffold HighScore game:string score:integer +$ bin/rails generate scaffold HighScore game:string score:integer invoke active_record create db/migrate/20130717151933_create_high_scores.rb create app/models/high_score.rb @@ -257,7 +255,7 @@ The generator checks that there exist the directories for models, controllers, h The migration requires that we **migrate**, that is, run some Ruby code (living in that `20130717151933_create_high_scores.rb`) to modify the schema of our database. Which database? The SQLite3 database that Rails will create for you when we run the `rake db:migrate` command. We'll talk more about Rake in-depth in a little while. ```bash -$ rake db:migrate +$ bin/rake db:migrate == CreateHighScores: migrating =============================================== -- create_table(:high_scores) -> 0.0017s @@ -269,7 +267,7 @@ INFO: Let's talk about unit tests. Unit tests are code that tests and makes asse Let's see the interface Rails created for us. ```bash -$ rails server +$ bin/rails server ``` Go to your browser and open [http://localhost:3000/high_scores](http://localhost:3000/high_scores), now we can create new high scores (55,160 on Space Invaders!) @@ -283,18 +281,43 @@ INFO: You can also use the alias "c" to invoke the console: `rails c`. You can specify the environment in which the `console` command should operate. ```bash -$ rails console staging +$ bin/rails console staging ``` If you wish to test out some code without changing any data, you can do that by invoking `rails console --sandbox`. ```bash -$ rails console --sandbox -Loading development environment in sandbox (Rails 4.0.0) +$ bin/rails console --sandbox +Loading development environment in sandbox (Rails 4.2.0) Any modifications you make will be rolled back on exit irb(main):001:0> ``` +#### The app and helper objects + +Inside the `rails console` you have access to the `app` and `helper` instances. + +With the `app` method you can access url and path helpers, as well as do requests. + +```bash +>> app.root_path +=> "/" + +>> app.get _ +Started GET "/" for 127.0.0.1 at 2014-06-19 10:41:57 -0300 +... +``` + +With the `helper` method it is possible to access Rails and your application's helpers. + +```bash +>> helper.time_ago_in_words 30.days.ago +=> "about 1 month" + +>> helper.my_custom_helper +=> "my custom helper" +``` + ### `rails dbconsole` `rails dbconsole` figures out which database you're using and drops you into whichever command line interface you would use with it (and figures out the command line parameters to give to it, too!). It supports MySQL, PostgreSQL, SQLite and SQLite3. @@ -306,7 +329,7 @@ INFO: You can also use the alias "db" to invoke the dbconsole: `rails db`. `runner` runs Ruby code in the context of Rails non-interactively. For instance: ```bash -$ rails runner "Model.long_running_method" +$ bin/rails runner "Model.long_running_method" ``` INFO: You can also use the alias "r" to invoke the runner: `rails r`. @@ -314,7 +337,7 @@ INFO: You can also use the alias "r" to invoke the runner: `rails r`. You can specify the environment in which the `runner` command should operate using the `-e` switch. ```bash -$ rails runner -e staging "Model.long_running_method" +$ bin/rails runner -e staging "Model.long_running_method" ``` ### `rails destroy` @@ -324,7 +347,7 @@ Think of `destroy` as the opposite of `generate`. It'll figure out what generate INFO: You can also use the alias "d" to invoke the destroy command: `rails d`. ```bash -$ rails generate model Oops +$ bin/rails generate model Oops invoke active_record create db/migrate/20120528062523_create_oops.rb create app/models/oops.rb @@ -333,7 +356,7 @@ $ rails generate model Oops create test/fixtures/oops.yml ``` ```bash -$ rails destroy model Oops +$ bin/rails destroy model Oops invoke active_record remove db/migrate/20120528062523_create_oops.rb remove app/models/oops.rb @@ -353,9 +376,10 @@ To get the full backtrace for running rake task you can pass the option ```--trace``` to command line, for example ```rake db:create --trace```. ```bash -$ rake --tasks +$ bin/rake --tasks rake about # List versions of all Rails frameworks and the environment -rake assets:clean # Remove compiled assets +rake assets:clean # Remove old compiled assets +rake assets:clobber # Remove compiled assets rake assets:precompile # Compile all the assets named in config.assets.precompile rake db:create # Create the database from config/database.yml for the current Rails.env ... @@ -372,18 +396,18 @@ INFO: You can also use ```rake -T``` to get the list of tasks. `rake about` gives information about version numbers for Ruby, RubyGems, Rails, the Rails subcomponents, your application's folder, the current Rails environment name, your app's database adapter, and schema version. It is useful when you need to ask for help, check if a security patch might affect you, or when you need some stats for an existing Rails installation. ```bash -$ rake about +$ bin/rake about 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.1.0 +Rails version 4.2.0 JavaScript Runtime Node.js (V8) -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 +Active Record version 4.2.0 +Action Pack version 4.2.0 +Action View version 4.2.0 +Action Mailer version 4.2.0 +Active Support version 4.2.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 @@ -393,7 +417,12 @@ Database schema version 20110805173523 ### `assets` -You can precompile the assets in `app/assets` using `rake assets:precompile` and remove those compiled assets using `rake assets:clean`. +You can precompile the assets in `app/assets` using `rake assets:precompile`, +and remove older compiled assets using `rake assets:clean`. The `assets:clean` +task allows for rolling deploys that may still be linking to an old asset while +the new assets are being built. + +If you want to clear `public/assets` completely, you can use `rake assets:clobber`. ### `db` @@ -414,7 +443,7 @@ The `doc:` namespace has the tools to generate documentation for your app, API d `rake notes` will search through your code for comments beginning with FIXME, OPTIMIZE or TODO. The search is done in files with extension `.builder`, `.rb`, `.rake`, `.yml`, `.yaml`, `.ruby`, `.css`, `.js` and `.erb` for both default and custom annotations. ```bash -$ rake notes +$ bin/rake notes (in /home/foobar/commandsapp) app/controllers/admin/users_controller.rb: * [ 20] [TODO] any other way to do this? @@ -434,7 +463,7 @@ config.annotations.register_extensions("scss", "sass", "less") { |annotation| /\ If you are looking for a specific annotation, say FIXME, you can use `rake notes:fixme`. Note that you have to lower case the annotation's name. ```bash -$ rake notes:fixme +$ bin/rake notes:fixme (in /home/foobar/commandsapp) app/controllers/admin/users_controller.rb: * [132] high priority for next deploy @@ -446,9 +475,9 @@ app/models/school.rb: You can also use custom annotations in your code and list them using `rake notes:custom` by specifying the annotation using an environment variable `ANNOTATION`. ```bash -$ rake notes:custom ANNOTATION=BUG +$ bin/rake notes:custom ANNOTATION=BUG (in /home/foobar/commandsapp) -app/models/post.rb: +app/models/article.rb: * [ 23] Have to fix this one before pushing! ``` @@ -458,7 +487,7 @@ By default, `rake notes` will look in the `app`, `config`, `lib`, `bin` and `tes ```bash $ export SOURCE_ANNOTATION_DIRECTORIES='spec,vendor' -$ rake notes +$ bin/rake notes (in /home/foobar/commandsapp) app/models/user.rb: * [ 35] [FIXME] User should have a subscription at this point @@ -530,9 +559,9 @@ end Invocation of the tasks will look like: ```bash -rake task_name -rake "task_name[value 1]" # entire argument string should be quoted -rake db:nothing +$ bin/rake task_name +$ bin/rake "task_name[value 1]" # entire argument string should be quoted +$ bin/rake db:nothing ``` NOTE: If your need to interact with your application models, perform database queries and so on, your task should depend on the `environment` task, which will load your application code. diff --git a/guides/source/configuring.md b/guides/source/configuring.md index ae382fc54d..13020fb286 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -120,7 +120,7 @@ numbers. New applications filter out passwords by adding the following `config.f * `secrets.secret_key_base` is used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get `secrets.secret_key_base` initialized to a random key present in `config/secrets.yml`. -* `config.serve_static_assets` configures Rails itself to serve static assets. Defaults to true, but in the production environment is turned off as the server software (e.g. Nginx or Apache) used to run the application should serve static assets instead. Unlike the default setting set this to true when running (absolutely not recommended!) or testing your app in production mode using WEBrick. Otherwise you won't be able use page caching and requests for files that exist regularly under the public directory will anyway hit your Rails app. +* `config.serve_static_assets` configures Rails itself to serve static assets. Defaults to true, but in the production environment is turned off as the server software (e.g. NGINX or Apache) used to run the application should serve static assets instead. Unlike the default setting set this to true when running (absolutely not recommended!) or testing your app in production mode using WEBrick. Otherwise you won't be able use page caching and requests for files that exist regularly under the public directory will anyway hit your Rails app. * `config.session_store` is usually set up in `config/initializers/session_store.rb` and specifies what class to use to store the session. Possible values are `:cookie_store` which is the default, `:mem_cache_store`, and `:disabled`. The last one tells Rails not to deal with sessions. Custom session stores can also be specified: @@ -330,6 +330,8 @@ The schema dumper adds one additional configuration option: * `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. +* `config.action_controller.always_permitted_parameters` sets a list of whitelisted parameters that are permitted by default. The default values are `['controller', 'action']`. + ### Configuring Action Dispatch * `config.action_dispatch.session_store` sets the name of the store for session data. The default is `:cookie_store`; other valid options include `:active_record_store`, `:mem_cache_store` or the name of your own custom class. @@ -388,13 +390,13 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. * `config.action_view.embed_authenticity_token_in_remote_forms` allows you to set the default behavior for `authenticity_token` in forms with `:remote => true`. By default it's set to false, which means that remote forms will not include `authenticity_token`, which is helpful when you're fragment-caching the form. Remote forms get the authenticity from the `meta` tag, so embedding is unnecessary unless you support browsers without JavaScript. In such case you can either pass `:authenticity_token => true` as a form option or set this config setting to `true` -* `config.action_view.prefix_partial_path_with_controller_namespace` determines whether or not partials are looked up from a subdirectory in templates rendered from namespaced controllers. For example, consider a controller named `Admin::PostsController` which renders this template: +* `config.action_view.prefix_partial_path_with_controller_namespace` determines whether or not partials are looked up from a subdirectory in templates rendered from namespaced controllers. For example, consider a controller named `Admin::ArticlesController` which renders this template: ```erb - <%= render @post %> + <%= render @article %> ``` - The default setting is `true`, which uses the partial at `/admin/posts/_post.erb`. Setting the value to `false` would render `/posts/_post.erb`, which is the same behavior as rendering from a non-namespaced controller such as `PostsController`. + The default setting is `true`, which uses the partial at `/admin/articles/_article.erb`. Setting the value to `false` would render `/articles/_article.erb`, which is the same behavior as rendering from a non-namespaced controller such as `ArticlesController`. * `config.action_view.raise_on_missing_translations` determines whether an error should be raised for missing translations @@ -451,6 +453,18 @@ There are a number of settings available on `config.action_mailer`: config.action_mailer.interceptors = ["MailInterceptor"] ``` +* `config.action_mailer.preview_path` specifies the location of mailer previews. + + ```ruby + config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews" + ``` + +* `config.action_mailer.show_previews` enable or disable mailer previews. By default this is `true` in development. + + ```ruby + config.action_mailer.show_previews = false + ``` + ### Configuring Active Support There are a few configuration options available in Active Support: @@ -552,7 +566,7 @@ development: $ echo $DATABASE_URL postgresql://localhost/my_database -$ rails runner 'puts ActiveRecord::Base.connections' +$ bin/rails runner 'puts ActiveRecord::Base.configurations' {"development"=>{"adapter"=>"postgresql", "host"=>"localhost", "database"=>"my_database"}} ``` @@ -569,7 +583,7 @@ development: $ echo $DATABASE_URL postgresql://localhost/my_database -$ rails runner 'puts ActiveRecord::Base.connections' +$ bin/rails runner 'puts ActiveRecord::Base.configurations' {"development"=>{"adapter"=>"postgresql", "host"=>"localhost", "database"=>"my_database", "pool"=>5}} ``` @@ -585,7 +599,7 @@ development: $ echo $DATABASE_URL postgresql://localhost/my_database -$ rails runner 'puts ActiveRecord::Base.connections' +$ bin/rails runner 'puts ActiveRecord::Base.configurations' {"development"=>{"adapter"=>"sqlite3", "database"=>"NOT_my_database"}} ``` @@ -729,13 +743,47 @@ Rails will now prepend "/app1" when generating links. #### Using Passenger -Passenger makes it easy to run your application in a subdirectory. You can find -the relevant configuration in the -[passenger manual](http://www.modrails.com/documentation/Users%20guide%20Apache.html#deploying_rails_to_sub_uri). +Passenger makes it easy to run your application in a subdirectory. You can find the relevant configuration in the [Passenger manual](http://www.modrails.com/documentation/Users%20guide%20Apache.html#deploying_rails_to_sub_uri). #### Using a Reverse Proxy -TODO +Deploying your application using a reverse proxy has definite advantages over traditional deploys. They allow you to have more control over your server by layering the components required by your application. + +Many modern web servers can be used as a proxy server to balance third-party elements such as caching servers or application servers. + +One such application server you can use is [Unicorn](http://unicorn.bogomips.org/) to run behind a reverse proxy. + +In this case, you would need to configure the proxy server (NGINX, Apache, etc) to accept connections from your application server (Unicorn). By default Unicorn will listen for TCP connections on port 8080, but you can change the port or configure it to use sockets instead. + +You can find more information in the [Unicorn readme](http://unicorn.bogomips.org/README.html) and understand the [philosophy](http://unicorn.bogomips.org/PHILOSOPHY.html) behind it. + +Once you've configured the application server, you must proxy requests to it by configuring your web server appropriately. For example your NGINX config may include: + +``` +upstream application_server { + server 0.0.0.0:8080 +} + +server { + listen 80; + server_name localhost; + + root /root/path/to/your_app/public; + + try_files $uri/index.html $uri.html @app; + + location @app { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_pass http://application_server; + } + + # some other configuration +} +``` + +Be sure to read the [NGINX documentation](http://nginx.org/en/docs/) for the most up-to-date information. #### Considerations when deploying to a subdirectory diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md index 28e1172274..0b05725623 100644 --- a/guides/source/contributing_to_ruby_on_rails.md +++ b/guides/source/contributing_to_ruby_on_rails.md @@ -109,9 +109,7 @@ After applying their branch, test it out! Here are some things to think about: Once you're happy that the pull request contains a good change, comment on the GitHub issue indicating your approval. Your comment should indicate that you like the change and what you like about it. Something like: -<blockquote> -I like the way you've restructured that code in generate_finder_sql - much nicer. The tests look good too. -</blockquote> +>I like the way you've restructured that code in generate_finder_sql - much nicer. The tests look good too. If your comment simply says "+1", then odds are that other reviewers aren't going to take it too seriously. Show that you took the time to review the pull request. @@ -144,7 +142,7 @@ WARNING: Docrails has a very strict policy: no code can be touched whatsoever, n Contributing to the Rails Code ------------------------------ -### Setting Up a Development Environment ### +### Setting Up a Development Environment To move on from submitting bugs to helping resolve existing issues or contributing your own code to Ruby on Rails, you _must_ be able to run its test suite. In this section of the guide you'll learn how to setup the tests on your own computer. @@ -156,7 +154,7 @@ The easiest and recommended way to get a development environment ready to hack i In case you can't use the Rails development box, see [this other guide](development_dependencies_install.html). -### Clone the Rails Repository ### +### Clone the Rails Repository To be able to contribute code, you need to clone the Rails repository: @@ -173,7 +171,7 @@ $ git checkout -b my_new_branch It doesn't matter much what name you use, because this branch will only exist on your local computer and your personal repository on GitHub. It won't be part of the Rails Git repository. -### Running an Application Against Your Local Branch ### +### Running an Application Against Your Local Branch In case you need a dummy Rails app to test changes, the `--dev` flag of `rails new` generates an application that uses your local branch: @@ -185,9 +183,9 @@ $ bundle exec rails new ~/my-test-app --dev The application generated in `~/my-test-app` runs against your local branch and in particular sees any modifications upon server reboot. -### Write Your Code ### +### Write Your Code -Now get busy and add/edit code. You're on your branch now, so you can write whatever you want (you can check to make sure you're on the right branch with `git branch -a`). But if you're planning to submit your change back for inclusion in Rails, keep a few things in mind: +Now get busy and add/edit code. You're on your branch now, so you can write whatever you want (make sure you're on the right branch with `git branch -a`). But if you're planning to submit your change back for inclusion in Rails, keep a few things in mind: * Get the code right. * Use Rails idioms and helpers. @@ -215,7 +213,38 @@ Rails follows a simple set of coding style conventions: The above are guidelines - please use your best judgment in using them. -### Running Tests ### +### Benchmark Your Code + +If your change has an impact on the performance of Rails, please use the +[benchmark-ips](https://github.com/evanphx/benchmark-ips) gem to provide +benchmark results for comparison. + +Here's an example of using benchmark-ips: + +```ruby +require 'benchmark/ips' + +Benchmark.ips do |x| + x.report('addition') { 1 + 2 } + x.report('addition with send') { 1.send(:+, 2) } +end +``` + +This will generate a report with the following information: + +``` +Calculating ------------------------------------- + addition 69114 i/100ms + addition with send 64062 i/100ms +------------------------------------------------- + addition 5307644.4 (±3.5%) i/s - 26539776 in 5.007219s + addition with send 3702897.9 (±3.5%) i/s - 18513918 in 5.006723s +``` + +Please see the benchmark/ips [README](https://github.com/evanphx/benchmark-ips/blob/master/README.md) for more information. + +### Running Tests + It is not customary in Rails to run the full test suite before pushing changes. The railties test suite in particular takes a long time, and even more if the source code is mounted in `/vagrant` as happens in the recommended @@ -228,35 +257,51 @@ tests are passing, that's enough to propose your contribution. We have unexpected breakages elsewhere. #### Entire Rails: + To run all the tests, do: + ```bash $ cd rails $ bundle exec rake test ``` -#### Particular component of Rails -To run tests only for particular component(ActionPack, ActiveRecord, etc.). For -example, to run `ActionMailer` tests you can: + +#### For a Particular Component + +You can run tests only for a particular component (e.g. Action Pack). For example, +to run Action Mailer tests: ```bash $ cd actionmailer $ bundle exec rake test ``` +#### Running a Single Test + +You can run a single test through ruby. For instance: + +```bash +$ cd actionmailer +$ ruby -w -Itest test/mail_layout_test.rb -n test_explicit_class_layout +``` + +The `-n` option allows you to run a single method instead of the whole +file. + ##### Testing Active Record This is how you run the Active Record test suite only for SQLite3: ```bash $ cd activerecord -$ bundle exec rake test_sqlite3 +$ bundle exec rake test:sqlite3 ``` You can now run the tests as you did for `sqlite3`. The tasks are respectively ```bash -test_mysql -test_mysql2 -test_postgresql +test:mysql +test:mysql2 +test:postgresql ``` Finally, @@ -273,17 +318,15 @@ You can also run any single test separately: $ ARCONN=sqlite3 ruby -Itest test/cases/associations/has_many_associations_test.rb ``` -You can invoke `test_jdbcmysql`, `test_jdbcsqlite3` or `test_jdbcpostgresql` also. See the file `activerecord/RUNNING_UNIT_TESTS.rdoc` for information on running more targeted database tests, or the file `ci/travis.rb` for the test suite run by the continuous integration server. - -#### Single Test separately -to run just one test. For example, to run `LayoutMailerTest` you can: +To run a single test against all adapters, use: ```bash -$ cd actionmailer -$ ruby -w -Ilib:test test/mail_layout_test.rb +$ bundle exec rake TEST=test/cases/associations/has_many_associations_test.rb ``` -### Warnings ### +You can invoke `test_jdbcmysql`, `test_jdbcsqlite3` or `test_jdbcpostgresql` also. See the file `activerecord/RUNNING_UNIT_TESTS.rdoc` for information on running more targeted database tests, or the file `ci/travis.rb` for the test suite run by the continuous integration server. + +### Warnings The test suite runs with warnings enabled. Ideally, Ruby on Rails should issue no warnings, but there may be a few, as well as some from third-party libraries. Please ignore (or fix!) them, if any, and submit patches that do not issue new warnings. @@ -292,7 +335,8 @@ If you are sure about what you are doing and would like to have a more clear out ```bash $ RUBYOPT=-W0 bundle exec rake test ``` -### Updating the CHANGELOG ### + +### Updating the CHANGELOG The CHANGELOG is an important part of every release. It keeps the list of changes for every Rails version. @@ -317,7 +361,7 @@ A CHANGELOG entry should summarize what was changed and should end with author's Your name can be added directly after the last word if you don't provide any code examples or don't need multiple paragraphs. Otherwise, it's best to make as a new paragraph. -### Sanity Check ### +### Sanity Check You should not be the only person who looks at the code before you submit it. If you know someone else who uses Rails, try asking them if they'll check out @@ -327,7 +371,7 @@ private before you push a patch out publicly is the "smoke test" for a patch: if you can't convince one other developer of the beauty of your code, you’re unlikely to convince the core team either. -### Commit Your Changes ### +### Commit Your Changes When you're happy with the code on your computer, you need to commit the changes to Git: @@ -351,9 +395,9 @@ it should not be necessary to visit a webpage to check the history. Description can have multiple paragraphs and you can use code examples inside, just indent it with 4 spaces: - class PostsController + class ArticlesController def index - respond_with Post.limit(10) + respond_with Article.limit(10) end end @@ -367,7 +411,7 @@ You can also add bullet points: TIP. Please squash your commits into a single commit when appropriate. This simplifies future cherry picks, and also keeps the git log clean. -### Update Your Branch ### +### Update Your Branch It's pretty likely that other changes to master have happened while you were working. Go get them: @@ -385,7 +429,7 @@ $ git rebase master No conflicts? Tests still pass? Change still seems reasonable to you? Then move on. -### Fork ### +### Fork Navigate to the Rails [GitHub repository](https://github.com/rails/rails) and press "Fork" in the upper right hand corner. @@ -515,14 +559,13 @@ $ git push origin my_pull_request -f You should be able to refresh the pull request on GitHub and see that it has been updated. +### Older Versions of Ruby on Rails -### Older Versions of Ruby on Rails ### - -If you want to add a fix to older versions of Ruby on Rails, you'll need to set up and switch to your own local tracking branch. Here is an example to switch to the 3-0-stable branch: +If you want to add a fix to older versions of Ruby on Rails, you'll need to set up and switch to your own local tracking branch. Here is an example to switch to the 4-0-stable branch: ```bash -$ git branch --track 3-0-stable origin/3-0-stable -$ git checkout 3-0-stable +$ git branch --track 4-0-stable origin/4-0-stable +$ git checkout 4-0-stable ``` TIP: You may want to [put your Git branch name in your shell prompt](http://qugstart.com/blog/git-and-svn/add-colored-git-branch-name-to-your-shell-prompt/) to make it easier to remember which version of the code you're working with. diff --git a/guides/source/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md index b067d9efb7..53b8566d83 100644 --- a/guides/source/debugging_rails_applications.md +++ b/guides/source/debugging_rails_applications.md @@ -26,17 +26,17 @@ One common task is to inspect the contents of a variable. In Rails, you can do t The `debug` helper will return a \<pre> tag that renders the object using the YAML format. This will generate human-readable data from any object. For example, if you have this code in a view: ```html+erb -<%= debug @post %> +<%= debug @article %> <p> <b>Title:</b> - <%= @post.title %> + <%= @article.title %> </p> ``` You'll see something like this: ```yaml ---- !ruby/object:Post +--- !ruby/object Article attributes: updated_at: 2008-09-05 22:55:47 body: It's a very helpful guide for debugging your Rails app. @@ -55,10 +55,10 @@ Title: Rails debugging guide Displaying an instance variable, or any other object or method, in YAML format can be achieved this way: ```html+erb -<%= simple_format @post.to_yaml %> +<%= simple_format @article.to_yaml %> <p> <b>Title:</b> - <%= @post.title %> + <%= @article.title %> </p> ``` @@ -67,7 +67,7 @@ The `to_yaml` method converts the method to YAML format leaving it more readable As a result of this, you will have something like this in your view: ```yaml ---- !ruby/object:Post +--- !ruby/object Article attributes: updated_at: 2008-09-05 22:55:47 body: It's a very helpful guide for debugging your Rails app. @@ -88,7 +88,7 @@ Another useful method for displaying object values is `inspect`, especially when <%= [1, 2, 3, 4, 5].inspect %> <p> <b>Title:</b> - <%= @post.title %> + <%= @article.title %> </p> ``` @@ -153,18 +153,18 @@ logger.fatal "Terminating application, raised unrecoverable error!!!" Here's an example of a method instrumented with extra logging: ```ruby -class PostsController < ApplicationController +class ArticlesController < ApplicationController # ... def create - @post = Post.new(params[:post]) - logger.debug "New post: #{@post.attributes.inspect}" - logger.debug "Post should be valid: #{@post.valid?}" - - if @post.save - flash[:notice] = 'Post was successfully created.' - logger.debug "The post was saved and now the user is going to be redirected..." - redirect_to(@post) + @article = Article.new(params[:article]) + logger.debug "New article: #{@article.attributes.inspect}" + logger.debug "Article should be valid: #{@article.valid?}" + + if @article.save + flash[:notice] = 'Article was successfully created.' + logger.debug "The article was saved and now the user is going to be redirected..." + redirect_to(@article) else render action: "new" end @@ -177,21 +177,21 @@ end Here's an example of the log generated when this controller action is executed: ``` -Processing PostsController#create (for 127.0.0.1 at 2008-09-08 11:52:54) [POST] +Processing ArticlesController#create (for 127.0.0.1 at 2008-09-08 11:52:54) [POST] Session ID: BAh7BzoMY3NyZl9pZCIlMDY5MWU1M2I1ZDRjODBlMzkyMWI1OTg2NWQyNzViZjYiCmZsYXNoSUM6J0FjdGl vbkNvbnRyb2xsZXI6OkZsYXNoOjpGbGFzaEhhc2h7AAY6CkB1c2VkewA=--b18cd92fba90eacf8137e5f6b3b06c4d724596a4 - Parameters: {"commit"=>"Create", "post"=>{"title"=>"Debugging Rails", + Parameters: {"commit"=>"Create", "article"=>{"title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs!!!", "published"=>"0"}, - "authenticity_token"=>"2059c1286e93402e389127b1153204e0d1e275dd", "action"=>"create", "controller"=>"posts"} -New post: {"updated_at"=>nil, "title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs!!!", + "authenticity_token"=>"2059c1286e93402e389127b1153204e0d1e275dd", "action"=>"create", "controller"=>"articles"} +New article: {"updated_at"=>nil, "title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs!!!", "published"=>false, "created_at"=>nil} -Post should be valid: true - Post Create (0.000443) INSERT INTO "posts" ("updated_at", "title", "body", "published", +Article should be valid: true + Article Create (0.000443) INSERT INTO "articles" ("updated_at", "title", "body", "published", "created_at") VALUES('2008-09-08 14:52:54', 'Debugging Rails', 'I''m learning how to print in logs!!!', 'f', '2008-09-08 14:52:54') -The post was saved and now the user is going to be redirected... -Redirected to #<Post:0x20af760> -Completed in 0.01224 (81 reqs/sec) | DB: 0.00044 (3%) | 302 Found [http://localhost/posts] +The article was saved and now the user is going to be redirected... +Redirected to # Article:0x20af760> +Completed in 0.01224 (81 reqs/sec) | DB: 0.00044 (3%) | 302 Found [http://localhost/articles] ``` Adding extra logging like this makes it easy to search for unexpected or unusual behavior in your logs. If you add extra logging, be sure to make sensible use of log levels to avoid filling your production logs with useless trivia. @@ -286,17 +286,17 @@ Before the prompt, the code around the line that is about to be run will be displayed and the current line will be marked by '=>'. Like this: ``` -[1, 10] in /PathTo/project/app/controllers/posts_controller.rb +[1, 10] in /PathTo/project/app/controllers/articles_controller.rb 3: - 4: # GET /posts - 5: # GET /posts.json + 4: # GET /articles + 5: # GET /articles.json 6: def index 7: byebug -=> 8: @posts = Post.find_recent +=> 8: @articles = Article.find_recent 9: 10: respond_to do |format| 11: format.html # index.html.erb - 12: format.json { render json: @posts } + 12: format.json { render json: @articles } (byebug) ``` @@ -309,7 +309,7 @@ For example: ```bash => Booting WEBrick -=> Rails 4.1.0 application starting in development on http://0.0.0.0:3000 +=> Rails 4.2.0 application starting in development on http://0.0.0.0:3000 => Run `rails server -h` for more startup options => Notice: server is listening on all interfaces (0.0.0.0). Consider using 127.0.0.1 (--binding option) => Ctrl-C to shutdown server @@ -320,19 +320,19 @@ For example: Started GET "/" for 127.0.0.1 at 2014-04-11 13:11:48 +0200 ActiveRecord::SchemaMigration Load (0.2ms) SELECT "schema_migrations".* FROM "schema_migrations" -Processing by PostsController#index as HTML +Processing by ArticlesController#index as HTML -[3, 12] in /PathTo/project/app/controllers/posts_controller.rb +[3, 12] in /PathTo/project/app/controllers/articles_controller.rb 3: - 4: # GET /posts - 5: # GET /posts.json + 4: # GET /articles + 5: # GET /articles.json 6: def index 7: byebug -=> 8: @posts = Post.find_recent +=> 8: @articles = Article.find_recent 9: 10: respond_to do |format| 11: format.html # index.html.erb - 12: format.json { render json: @posts } + 12: format.json { render json: @articles } (byebug) ``` @@ -365,15 +365,15 @@ To see the previous ten lines you should type `list-` (or `l-`) ``` (byebug) l- -[1, 10] in /PathTo/project/app/controllers/posts_controller.rb - 1 class PostsController < ApplicationController - 2 before_action :set_post, only: [:show, :edit, :update, :destroy] +[1, 10] in /PathTo/project/app/controllers/articles_controller.rb + 1 class ArticlesController < ApplicationController + 2 before_action :set_article, only: [:show, :edit, :update, :destroy] 3 - 4 # GET /posts - 5 # GET /posts.json + 4 # GET /articles + 5 # GET /articles.json 6 def index 7 byebug - 8 @posts = Post.find_recent + 8 @articles = Article.find_recent 9 10 respond_to do |format| @@ -386,17 +386,17 @@ the code again you can type `list=` ``` (byebug) list= -[3, 12] in /PathTo/project/app/controllers/posts_controller.rb +[3, 12] in /PathTo/project/app/controllers/articles_controller.rb 3: - 4: # GET /posts - 5: # GET /posts.json + 4: # GET /articles + 5: # GET /articles.json 6: def index 7: byebug -=> 8: @posts = Post.find_recent +=> 8: @articles = Article.find_recent 9: 10: respond_to do |format| 11: format.html # index.html.erb - 12: format.json { render json: @posts } + 12: format.json { render json: @articles } (byebug) ``` @@ -419,14 +419,14 @@ then `backtrace` will supply the answer. ``` (byebug) where ---> #0 PostsController.index - at /PathTo/project/test_app/app/controllers/posts_controller.rb:8 +--> #0 ArticlesController.index + at /PathTo/project/test_app/app/controllers/articles_controller.rb:8 #1 ActionController::ImplicitRender.send_action(method#String, *args#Array) - at /PathToGems/actionpack-4.1.0/lib/action_controller/metal/implicit_render.rb:4 + at /PathToGems/actionpack-4.2.0/lib/action_controller/metal/implicit_render.rb:4 #2 AbstractController::Base.process_action(action#NilClass, *args#Array) - at /PathToGems/actionpack-4.1.0/lib/abstract_controller/base.rb:189 + at /PathToGems/actionpack-4.2.0/lib/abstract_controller/base.rb:189 #3 ActionController::Rendering.process_action(action#NilClass, *args#NilClass) - at /PathToGems/actionpack-4.1.0/lib/action_controller/metal/rendering.rb:10 + at /PathToGems/actionpack-4.2.0/lib/action_controller/metal/rendering.rb:10 ... ``` @@ -438,7 +438,7 @@ context. ``` (byebug) frame 2 -[184, 193] in /PathToGems/actionpack-4.1.0/lib/abstract_controller/base.rb +[184, 193] in /PathToGems/actionpack-4.2.0/lib/abstract_controller/base.rb 184: # is the intended way to override action dispatching. 185: # 186: # Notice that the first argument is the method to be dispatched @@ -487,17 +487,17 @@ This example shows how you can print the instance variables defined within the current context: ``` -[3, 12] in /PathTo/project/app/controllers/posts_controller.rb +[3, 12] in /PathTo/project/app/controllers/articles_controller.rb 3: - 4: # GET /posts - 5: # GET /posts.json + 4: # GET /articles + 5: # GET /articles.json 6: def index 7: byebug -=> 8: @posts = Post.find_recent +=> 8: @articles = Article.find_recent 9: 10: respond_to do |format| 11: format.html # index.html.erb - 12: format.json { render json: @posts } + 12: format.json { render json: @articles } (byebug) instance_variables [:@_action_has_layout, :@_routes, :@_headers, :@_status, :@_request, @@ -512,15 +512,15 @@ command later in this guide). ``` (byebug) next -[5, 14] in /PathTo/project/app/controllers/posts_controller.rb - 5 # GET /posts.json +[5, 14] in /PathTo/project/app/controllers/articles_controller.rb + 5 # GET /articles.json 6 def index 7 byebug - 8 @posts = Post.find_recent + 8 @articles = Article.find_recent 9 => 10 respond_to do |format| 11 format.html # index.html.erb - 12 format.json { render json: @posts } + 12 format.json { render json: @articles } 13 end 14 end 15 @@ -530,11 +530,11 @@ command later in this guide). And then ask again for the instance_variables: ``` -(byebug) instance_variables.include? "@posts" +(byebug) instance_variables.include? "@articles" true ``` -Now `@posts` is included in the instance variables, because the line defining it +Now `@articles` is included in the instance variables, because the line defining it was executed. TIP: You can also step into **irb** mode with the command `irb` (of course!). @@ -564,7 +564,7 @@ example, to check that we have no local variables currently defined. You can also inspect for an object method this way: ``` -(byebug) var instance Post.new +(byebug) var instance Article.new @_start_transaction_state = {} @aggregation_cache = {} @association_cache = {} @@ -581,8 +581,8 @@ You can use also `display` to start watching variables. This is a good way of tracking the values of a variable while the execution goes on. ``` -(byebug) display @posts -1: @posts = nil +(byebug) display @articles +1: @articles = nil ``` The variables inside the displaying list will be printed with their values after @@ -611,10 +611,10 @@ For example, consider the following situation: ```ruby Started GET "/" for 127.0.0.1 at 2014-04-11 13:39:23 +0200 -Processing by PostsController#index as HTML +Processing by ArticlesController#index as HTML -[1, 8] in /home/davidr/Proyectos/test_app/app/models/post.rb - 1: class Post < ActiveRecord::Base +[1, 8] in /home/davidr/Proyectos/test_app/app/models/article.rb + 1: class Article < ActiveRecord::Base 2: 3: def self.find_recent(limit = 10) 4: byebug @@ -634,15 +634,15 @@ the method, so `byebug` will jump to next next line of the previous frame. (byebug) next Next went up a frame because previous frame finished -[4, 13] in /PathTo/project/test_app/app/controllers/posts_controller.rb - 4: # GET /posts - 5: # GET /posts.json +[4, 13] in /PathTo/project/test_app/app/controllers/articles_controller.rb + 4: # GET /articles + 5: # GET /articles.json 6: def index - 7: @posts = Post.find_recent + 7: @articles = Article.find_recent 8: => 9: respond_to do |format| 10: format.html # index.html.erb - 11: format.json { render json: @posts } + 11: format.json { render json: @articles } 12: end 13: end @@ -655,7 +655,7 @@ instruction to be executed. In this case, the activesupport's `week` method. ``` (byebug) step -[50, 59] in /PathToGems/activesupport-4.1.0/lib/active_support/core_ext/numeric/time.rb +[50, 59] in /PathToGems/activesupport-4.2.0/lib/active_support/core_ext/numeric/time.rb 50: ActiveSupport::Duration.new(self * 24.hours, [[:days, self]]) 51: end 52: alias :day :days @@ -693,20 +693,20 @@ _expression_ works the same way as with file:line. For example, in the previous situation ``` -[4, 13] in /PathTo/project/app/controllers/posts_controller.rb - 4: # GET /posts - 5: # GET /posts.json +[4, 13] in /PathTo/project/app/controllers/articles_controller.rb + 4: # GET /articles + 5: # GET /articles.json 6: def index - 7: @posts = Post.find_recent + 7: @articles = Article.find_recent 8: => 9: respond_to do |format| 10: format.html # index.html.erb - 11: format.json { render json: @posts } + 11: format.json { render json: @articles } 12: end 13: end (byebug) break 11 -Created breakpoint 1 at /PathTo/project/app/controllers/posts_controller.rb:11 +Created breakpoint 1 at /PathTo/project/app/controllers/articles_controller.rb:11 ``` @@ -716,7 +716,7 @@ supply a number, it lists that breakpoint. Otherwise it lists all breakpoints. ``` (byebug) info breakpoints Num Enb What -1 y at /PathTo/project/app/controllers/posts_controller.rb:11 +1 y at /PathTo/project/app/controllers/articles_controller.rb:11 ``` To delete breakpoints: use the command `delete _n_` to remove the breakpoint diff --git a/guides/source/development_dependencies_install.md b/guides/source/development_dependencies_install.md index b0e070120d..b134c9d2d0 100644 --- a/guides/source/development_dependencies_install.md +++ b/guides/source/development_dependencies_install.md @@ -249,7 +249,7 @@ and create the test databases: ```bash $ cd activerecord -$ bundle exec rake mysql:build_databases +$ bundle exec rake db:mysql:build ``` PostgreSQL's authentication works differently. A simple way to set up the development environment for example is to run with your development account @@ -267,7 +267,7 @@ and then create the test databases with ```bash $ cd activerecord -$ bundle exec rake postgresql:build_databases +$ bundle exec rake db:postgresql:build ``` It is possible to build databases for both PostgreSQL and MySQL with diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml index a160c462b2..82e248ee38 100644 --- a/guides/source/documents.yaml +++ b/guides/source/documents.yaml @@ -13,8 +13,8 @@ url: active_record_basics.html description: This guide will get you started with models, persistence to database and the Active Record pattern and library. - - name: Rails Database Migrations - url: migrations.html + name: Active Record Migrations + url: active_record_migrations.html description: This guide covers how you can use Active Record migrations to alter your database in a structured and organized manner. - name: Active Record Validations @@ -96,11 +96,6 @@ url: command_line.html description: This guide covers the command line tools and rake tasks provided by Rails. - - name: Caching with Rails - work_in_progress: true - url: caching_with_rails.html - description: Various caching techniques provided by Rails. - - name: Asset Pipeline url: asset_pipeline.html description: This guide documents the asset pipeline. @@ -162,7 +157,6 @@ - name: Upgrading Ruby on Rails url: upgrading_ruby_on_rails.html - work_in_progress: true description: This guide helps in upgrading applications to latest Ruby on Rails versions. - name: Ruby on Rails 4.1 Release Notes diff --git a/guides/source/engines.md b/guides/source/engines.md index 8f9ba0995f..a5f8ee27b8 100644 --- a/guides/source/engines.md +++ b/guides/source/engines.md @@ -31,27 +31,28 @@ Engines are also closely related to plugins. The two share a common `lib` directory structure, and are both generated using the `rails plugin new` generator. The difference is that an engine is considered a "full plugin" by Rails (as indicated by the `--full` option that's passed to the generator -command). This guide will refer to them simply as "engines" throughout. An -engine **can** be a plugin, and a plugin **can** be an engine. +command). We'll actually be using the `--mountable` option here, which includes +all the features of `--full`, and then some. This guide will refer to these +"full plugins" simply as "engines" throughout. An engine **can** be a plugin, +and a plugin **can** be an engine. -The engine that will be created in this guide will be called "blorgh". The +The engine that will be created in this guide will be called "blorgh". This engine will provide blogging functionality to its host applications, allowing -for new posts and comments to be created. At the beginning of this guide, you +for new articles and comments to be created. At the beginning of this guide, you will be working solely within the engine itself, but in later sections you'll see how to hook it into an application. Engines can also be isolated from their host applications. This means that an application is able to have a path provided by a routing helper such as -`posts_path` and use an engine also that provides a path also called -`posts_path`, and the two would not clash. Along with this, controllers, models +`articles_path` and use an engine also that provides a path also called +`articles_path`, and the two would not clash. Along with this, controllers, models and table names are also namespaced. You'll see how to do this later in this guide. It's important to keep in mind at all times that the application should **always** take precedence over its engines. An application is the object that -has final say in what goes on in the universe (with the universe being the -application's environment) where the engine should only be enhancing it, rather -than changing it drastically. +has final say in what goes on in its environment. The engine should +only be enhancing it, rather than changing it drastically. To see demonstrations of other engines, check out [Devise](https://github.com/plataformatec/devise), an engine that provides @@ -73,17 +74,20 @@ options as appropriate to the need. For the "blorgh" example, you will need to create a "mountable" engine, running this command in a terminal: ```bash -$ rails plugin new blorgh --mountable +$ bin/rails plugin new blorgh --mountable ``` The full list of options for the plugin generator may be seen by typing: ```bash -$ rails plugin --help +$ bin/rails plugin --help ``` -The `--full` option tells the generator that you want to create an engine, -including a skeleton structure that provides the following: +The `--mountable` option tells the generator that you want to create a +"mountable" and namespace-isolated engine. This generator will provide the same +skeleton structure as would the `--full` option. The `--full` option tells the +generator that you want to create an engine, including a skeleton structure +that provides the following: * An `app` directory tree * A `config/routes.rb` file: @@ -94,7 +98,7 @@ including a skeleton structure that provides the following: ``` * A file at `lib/blorgh/engine.rb`, which is identical in function to a - * standard Rails application's `config/application.rb` file: + standard Rails application's `config/application.rb` file: ```ruby module Blorgh @@ -103,9 +107,7 @@ including a skeleton structure that provides the following: end ``` -The `--mountable` option tells the generator that you want to create a -"mountable" and namespace-isolated engine. This generator will provide the same -skeleton structure as would the `--full` option, and will add: +The `--mountable` option will add to the `--full` option: * Asset manifest files (`application.js` and `application.css`) * A namespaced `ApplicationController` stub @@ -197,12 +199,12 @@ within the `Engine` class definition. Without it, classes generated in an engine **may** conflict with an application. What this isolation of the namespace means is that a model generated by a call -to `rails g model`, such as `rails g model post`, won't be called `Post`, but -instead be namespaced and called `Blorgh::Post`. In addition, the table for the -model is namespaced, becoming `blorgh_posts`, rather than simply `posts`. -Similar to the model namespacing, a controller called `PostsController` becomes -`Blorgh::PostsController` and the views for that controller will not be at -`app/views/posts`, but `app/views/blorgh/posts` instead. Mailers are namespaced +to `bin/rails g model`, such as `bin/rails g model article`, won't be called `Article`, but +instead be namespaced and called `Blorgh::Article`. In addition, the table for the +model is namespaced, becoming `blorgh_articles`, rather than simply `articles`. +Similar to the model namespacing, a controller called `ArticlesController` becomes +`Blorgh::ArticlesController` and the views for that controller will not be at +`app/views/articles`, but `app/views/blorgh/articles` instead. Mailers are namespaced as well. Finally, routes will also be isolated within the engine. This is one of the most @@ -253,7 +255,7 @@ This means that you will be able to generate new controllers and models for this engine very easily by running commands like this: ```bash -rails g model +$ bin/rails g model ``` Keep in mind, of course, that anything generated with these commands inside of @@ -283,74 +285,74 @@ created in the `test` directory as well. For example, you may wish to create a Providing engine functionality ------------------------------ -The engine that this guide covers provides posting and commenting functionality -and follows a similar thread to the [Getting Started +The engine that this guide covers provides submitting articles and commenting +functionality and follows a similar thread to the [Getting Started Guide](getting_started.html), with some new twists. -### Generating a Post Resource +### Generating an Article Resource -The first thing to generate for a blog engine is the `Post` model and related +The first thing to generate for a blog engine is the `Article` model and related controller. To quickly generate this, you can use the Rails scaffold generator. ```bash -$ rails generate scaffold post title:string text:text +$ bin/rails generate scaffold article title:string text:text ``` This command will output this information: ``` invoke active_record -create db/migrate/[timestamp]_create_blorgh_posts.rb -create app/models/blorgh/post.rb +create db/migrate/[timestamp]_create_blorgh_articles.rb +create app/models/blorgh/article.rb invoke test_unit -create test/models/blorgh/post_test.rb -create test/fixtures/blorgh/posts.yml +create test/models/blorgh/article_test.rb +create test/fixtures/blorgh/articles.yml invoke resource_route - route resources :posts + route resources :articles invoke scaffold_controller -create app/controllers/blorgh/posts_controller.rb +create app/controllers/blorgh/articles_controller.rb invoke erb -create app/views/blorgh/posts -create app/views/blorgh/posts/index.html.erb -create app/views/blorgh/posts/edit.html.erb -create app/views/blorgh/posts/show.html.erb -create app/views/blorgh/posts/new.html.erb -create app/views/blorgh/posts/_form.html.erb +create app/views/blorgh/articles +create app/views/blorgh/articles/index.html.erb +create app/views/blorgh/articles/edit.html.erb +create app/views/blorgh/articles/show.html.erb +create app/views/blorgh/articles/new.html.erb +create app/views/blorgh/articles/_form.html.erb invoke test_unit -create test/controllers/blorgh/posts_controller_test.rb +create test/controllers/blorgh/articles_controller_test.rb invoke helper -create app/helpers/blorgh/posts_helper.rb +create app/helpers/blorgh/articles_helper.rb invoke test_unit -create test/helpers/blorgh/posts_helper_test.rb +create test/helpers/blorgh/articles_helper_test.rb invoke assets invoke js -create app/assets/javascripts/blorgh/posts.js +create app/assets/javascripts/blorgh/articles.js invoke css -create app/assets/stylesheets/blorgh/posts.css +create app/assets/stylesheets/blorgh/articles.css invoke css create app/assets/stylesheets/scaffold.css ``` The first thing that the scaffold generator does is invoke the `active_record` generator, which generates a migration and a model for the resource. Note here, -however, that the migration is called `create_blorgh_posts` rather than the -usual `create_posts`. This is due to the `isolate_namespace` method called in +however, that the migration is called `create_blorgh_articles` rather than the +usual `create_articles`. This is due to the `isolate_namespace` method called in the `Blorgh::Engine` class's definition. The model here is also namespaced, -being placed at `app/models/blorgh/post.rb` rather than `app/models/post.rb` due +being placed at `app/models/blorgh/article.rb` rather than `app/models/article.rb` due to the `isolate_namespace` call within the `Engine` class. Next, the `test_unit` generator is invoked for this model, generating a model -test at `test/models/blorgh/post_test.rb` (rather than -`test/models/post_test.rb`) and a fixture at `test/fixtures/blorgh/posts.yml` -(rather than `test/fixtures/posts.yml`). +test at `test/models/blorgh/article_test.rb` (rather than +`test/models/article_test.rb`) and a fixture at `test/fixtures/blorgh/articles.yml` +(rather than `test/fixtures/articles.yml`). After that, a line for the resource is inserted into the `config/routes.rb` file -for the engine. This line is simply `resources :posts`, turning the +for the engine. This line is simply `resources :articles`, turning the `config/routes.rb` file for the engine into this: ```ruby Blorgh::Engine.routes.draw do - resources :posts + resources :articles end ``` @@ -362,18 +364,18 @@ be isolated from those routes that are within the application. The [Routes](#routes) section of this guide describes it in detail. Next, the `scaffold_controller` generator is invoked, generating a controller -called `Blorgh::PostsController` (at -`app/controllers/blorgh/posts_controller.rb`) and its related views at -`app/views/blorgh/posts`. This generator also generates a test for the -controller (`test/controllers/blorgh/posts_controller_test.rb`) and a helper -(`app/helpers/blorgh/posts_controller.rb`). +called `Blorgh::ArticlesController` (at +`app/controllers/blorgh/articles_controller.rb`) and its related views at +`app/views/blorgh/articles`. This generator also generates a test for the +controller (`test/controllers/blorgh/articles_controller_test.rb`) and a helper +(`app/helpers/blorgh/articles_controller.rb`). Everything this generator has created is neatly namespaced. The controller's class is defined within the `Blorgh` module: ```ruby module Blorgh - class PostsController < ApplicationController + class ArticlesController < ApplicationController ... end end @@ -382,22 +384,22 @@ end NOTE: The `ApplicationController` class being inherited from here is the `Blorgh::ApplicationController`, not an application's `ApplicationController`. -The helper inside `app/helpers/blorgh/posts_helper.rb` is also namespaced: +The helper inside `app/helpers/blorgh/articles_helper.rb` is also namespaced: ```ruby module Blorgh - module PostsHelper + module ArticlesHelper ... end end ``` This helps prevent conflicts with any other engine or application that may have -a post resource as well. +an article resource as well. Finally, the assets for this resource are generated in two files: -`app/assets/javascripts/blorgh/posts.js` and -`app/assets/stylesheets/blorgh/posts.css`. You'll see how to use these a little +`app/assets/javascripts/blorgh/articles.js` and +`app/assets/stylesheets/blorgh/articles.css`. You'll see how to use these a little later. By default, the scaffold styling is not applied to the engine because the @@ -412,46 +414,46 @@ tag of this layout: You can see what the engine has so far by running `rake db:migrate` at the root of our engine to run the migration generated by the scaffold generator, and then running `rails server` in `test/dummy`. When you open -`http://localhost:3000/blorgh/posts` you will see the default scaffold that has +`http://localhost:3000/blorgh/articles` you will see the default scaffold that has been generated. Click around! You've just generated your first engine's first functions. If you'd rather play around in the console, `rails console` will also work just -like a Rails application. Remember: the `Post` model is namespaced, so to -reference it you must call it as `Blorgh::Post`. +like a Rails application. Remember: the `Article` model is namespaced, so to +reference it you must call it as `Blorgh::Article`. ```ruby ->> Blorgh::Post.find(1) -=> #<Blorgh::Post id: 1 ...> +>> Blorgh::Article.find(1) +=> #<Blorgh::Article id: 1 ...> ``` -One final thing is that the `posts` resource for this engine should be the root +One final thing is that the `articles` resource for this engine should be the root of the engine. Whenever someone goes to the root path where the engine is -mounted, they should be shown a list of posts. This can be made to happen if +mounted, they should be shown a list of articles. This can be made to happen if this line is inserted into the `config/routes.rb` file inside the engine: ```ruby -root to: "posts#index" +root to: "articles#index" ``` -Now people will only need to go to the root of the engine to see all the posts, -rather than visiting `/posts`. This means that instead of -`http://localhost:3000/blorgh/posts`, you only need to go to +Now people will only need to go to the root of the engine to see all the articles, +rather than visiting `/articles`. This means that instead of +`http://localhost:3000/blorgh/articles`, you only need to go to `http://localhost:3000/blorgh` now. ### Generating a Comments Resource -Now that the engine can create new blog posts, it only makes sense to add +Now that the engine can create new articles, it only makes sense to add commenting functionality as well. To do this, you'll need to generate a comment -model, a comment controller and then modify the posts scaffold to display +model, a comment controller and then modify the articles scaffold to display comments and allow people to create new ones. From the application root, run the model generator. Tell it to generate a -`Comment` model, with the related table having two columns: a `post_id` integer +`Comment` model, with the related table having two columns: a `article_id` integer and `text` text column. ```bash -$ rails generate model Comment post_id:integer text:text +$ bin/rails generate model Comment article_id:integer text:text ``` This will output the following: @@ -474,17 +476,17 @@ table: $ rake db:migrate ``` -To show the comments on a post, edit `app/views/blorgh/posts/show.html.erb` and +To show the comments on an article, edit `app/views/blorgh/articles/show.html.erb` and add this line before the "Edit" link: ```html+erb <h3>Comments</h3> -<%= render @post.comments %> +<%= render @article.comments %> ``` This line will require there to be a `has_many` association for comments defined -on the `Blorgh::Post` model, which there isn't right now. To define one, open -`app/models/blorgh/post.rb` and add this line into the model: +on the `Blorgh::Article` model, which there isn't right now. To define one, open +`app/models/blorgh/article.rb` and add this line into the model: ```ruby has_many :comments @@ -494,7 +496,7 @@ Turning the model into this: ```ruby module Blorgh - class Post < ActiveRecord::Base + class Article < ActiveRecord::Base has_many :comments end end @@ -505,9 +507,9 @@ NOTE: Because the `has_many` is defined inside a class that is inside the model for these objects, so there's no need to specify that using the `:class_name` option here. -Next, there needs to be a form so that comments can be created on a post. To add -this, put this line underneath the call to `render @post.comments` in -`app/views/blorgh/posts/show.html.erb`: +Next, there needs to be a form so that comments can be created on an article. To +add this, put this line underneath the call to `render @article.comments` in +`app/views/blorgh/articles/show.html.erb`: ```erb <%= render "blorgh/comments/form" %> @@ -519,7 +521,7 @@ directory at `app/views/blorgh/comments` and in it a new file called ```html+erb <h3>New comment</h3> -<%= form_for [@post, @post.comments.build] do |f| %> +<%= form_for [@article, @article.comments.build] do |f| %> <p> <%= f.label :text %><br> <%= f.text_area :text %> @@ -529,12 +531,12 @@ directory at `app/views/blorgh/comments` and in it a new file called ``` When this form is submitted, it is going to attempt to perform a `POST` request -to a route of `/posts/:post_id/comments` within the engine. This route doesn't -exist at the moment, but can be created by changing the `resources :posts` line +to a route of `/articles/:article_id/comments` within the engine. This route doesn't +exist at the moment, but can be created by changing the `resources :articles` line inside `config/routes.rb` into these lines: ```ruby -resources :posts do +resources :articles do resources :comments end ``` @@ -545,7 +547,7 @@ The route now exists, but the controller that this route goes to does not. To create it, run this command from the application root: ```bash -$ rails g controller comments +$ bin/rails g controller comments ``` This will generate the following things: @@ -567,17 +569,17 @@ invoke css create app/assets/stylesheets/blorgh/comments.css ``` -The form will be making a `POST` request to `/posts/:post_id/comments`, which +The form will be making a `POST` request to `/articles/:article_id/comments`, which will correspond with the `create` action in `Blorgh::CommentsController`. This action needs to be created, which can be done by putting the following lines inside the class definition in `app/controllers/blorgh/comments_controller.rb`: ```ruby def create - @post = Post.find(params[:post_id]) - @comment = @post.comments.create(comment_params) + @article = Article.find(params[:article_id]) + @comment = @article.comments.create(comment_params) flash[:notice] = "Comment has been created!" - redirect_to posts_path + redirect_to articles_path end private @@ -590,11 +592,11 @@ This is the final step 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: -``` +``` Missing partial blorgh/comments/comment with {:handlers=>[:erb, :builder], :formats=>[:html], :locale=>[:en, :en]}. Searched in: * "/Users/ryan/Sites/side_projects/blorgh/test/dummy/app/views" * -"/Users/ryan/Sites/side_projects/blorgh/app/views" +"/Users/ryan/Sites/side_projects/blorgh/app/views" ``` The engine is unable to find the partial required for rendering the comments. @@ -612,7 +614,7 @@ line inside it: ``` The `comment_counter` local variable is given to us by the `<%= render -@post.comments %>` call, which will define it automatically and increment the +@article.comments %>` call, which will define it automatically and increment the counter as it iterates through each comment. It's used in this example to display a small number next to each comment when it's created. @@ -625,7 +627,7 @@ Hooking Into an Application Using an engine within an application is very easy. This section covers how to mount the engine into an application and the initial setup required, as well as linking the engine to a `User` class provided by the application to provide -ownership for posts and comments within the engine. +ownership for articles and comments within the engine. ### Mounting the Engine @@ -676,7 +678,7 @@ pre-defined path which may be customizable. ### Engine setup -The engine contains migrations for the `blorgh_posts` and `blorgh_comments` +The engine contains migrations for the `blorgh_articles` and `blorgh_comments` table which need to be created in the application's database so that the engine's models can query them correctly. To copy these migrations into the application use this command: @@ -698,7 +700,7 @@ haven't been copied over already. The first run for this command will output something such as this: ```bash -Copied migration [timestamp_1]_create_blorgh_posts.rb from blorgh +Copied migration [timestamp_1]_create_blorgh_articles.rb from blorgh Copied migration [timestamp_2]_create_blorgh_comments.rb from blorgh ``` @@ -709,7 +711,7 @@ migrations in the application. To run these migrations within the context of the application, simply run `rake db:migrate`. When accessing the engine through `http://localhost:3000/blog`, the -posts will be empty. This is because the table created inside the application is +articles will be empty. This is because the table created inside the application is different from the one created within the engine. Go ahead, play around with the newly mounted engine. You'll find that it's the same as when it was only an engine. @@ -734,17 +736,19 @@ rake db:migrate SCOPE=blorgh VERSION=0 When an engine is created, it may want to use specific classes from an application to provide links between the pieces of the engine and the pieces of -the application. In the case of the `blorgh` engine, making posts and comments +the application. In the case of the `blorgh` engine, making articles and comments have authors would make a lot of sense. A typical application might have a `User` class that would be used to represent -authors for a post or a comment. But there could be a case where the application -calls this class something different, such as `Person`. For this reason, the -engine should not hardcode associations specifically for a `User` class. +authors for an article or a comment. But there could be a case where the +application calls this class something different, such as `Person`. For this +reason, the engine should not hardcode associations specifically for a `User` +class. To keep it simple in this case, the application will have a class called `User` -that represents the users of the application. It can be generated using this -command inside the application: +that represents the users of the application (we'll get into making this +configurable further on). It can be generated using this command inside the +application: ```bash rails g model user name:string @@ -753,14 +757,14 @@ rails g model user name:string The `rake db:migrate` command needs to be run here to ensure that our application has the `users` table for future use. -Also, to keep it simple, the posts form will have a new text field called +Also, to keep it simple, the articles form will have a new text field called `author_name`, where users can elect to put their name. The engine will then take this name and either create a new `User` object from it, or find one that -already has that name. The engine will then associate the post with the found or +already has that name. The engine will then associate the article with the found or created `User` object. First, the `author_name` text field needs to be added to the -`app/views/blorgh/posts/_form.html.erb` partial inside the engine. This can be +`app/views/blorgh/articles/_form.html.erb` partial inside the engine. This can be added above the `title` field with this code: ```html+erb @@ -770,23 +774,23 @@ added above the `title` field with this code: </div> ``` -Next, we need to update our `Blorgh::PostController#post_params` method to +Next, we need to update our `Blorgh::ArticleController#article_params` method to permit the new form parameter: ```ruby -def post_params - params.require(:post).permit(:title, :text, :author_name) +def article_params + params.require(:article).permit(:title, :text, :author_name) end ``` -The `Blorgh::Post` model should then have some code to convert the `author_name` -field into an actual `User` object and associate it as that post's `author` -before the post is saved. It will also need to have an `attr_accessor` set up +The `Blorgh::Article` model should then have some code to convert the `author_name` +field into an actual `User` object and associate it as that article's `author` +before the article is saved. It will also need to have an `attr_accessor` set up for this field, so that the setter and getter methods are defined for it. To do all this, you'll need to add the `attr_accessor` for `author_name`, the association for the author and the `before_save` call into -`app/models/blorgh/post.rb`. The `author` association will be hard-coded to the +`app/models/blorgh/article.rb`. The `author` association will be hard-coded to the `User` class for the time being. ```ruby @@ -803,14 +807,14 @@ private By representing the `author` association's object with the `User` class, a link is established between the engine and the application. There needs to be a way -of associating the records in the `blorgh_posts` table with the records in the +of associating the records in the `blorgh_articles` table with the records in the `users` table. Because the association is called `author`, there should be an -`author_id` column added to the `blorgh_posts` table. +`author_id` column added to the `blorgh_articles` table. To generate this new column, run this command within the engine: ```bash -$ rails g migration add_author_id_to_blorgh_posts author_id:integer +$ bin/rails g migration add_author_id_to_blorgh_articles author_id:integer ``` NOTE: Due to the migration's name and the column specification after it, Rails @@ -828,12 +832,12 @@ $ rake blorgh:install:migrations Notice that only _one_ migration was copied over here. This is because the first two migrations were copied over the first time this command was run. -``` -NOTE Migration [timestamp]_create_blorgh_posts.rb from blorgh has been +``` +NOTE Migration [timestamp]_create_blorgh_articles.rb from blorgh has been skipped. Migration with the same name already exists. NOTE Migration [timestamp]_create_blorgh_comments.rb from blorgh has been skipped. Migration with the same name already exists. Copied migration -[timestamp]_add_author_id_to_blorgh_posts.rb from blorgh +[timestamp]_add_author_id_to_blorgh_articles.rb from blorgh ``` Run the migration using: @@ -843,20 +847,20 @@ $ rake db:migrate ``` Now with all the pieces in place, an action will take place that will associate -an author - represented by a record in the `users` table - with a post, -represented by the `blorgh_posts` table from the engine. +an author - represented by a record in the `users` table - with an article, +represented by the `blorgh_articles` table from the engine. -Finally, the author's name should be displayed on the post's page. Add this code -above the "Title" output inside `app/views/blorgh/posts/show.html.erb`: +Finally, the author's name should be displayed on the article's page. Add this code +above the "Title" output inside `app/views/blorgh/articles/show.html.erb`: ```html+erb <p> <b>Author:</b> - <%= @post.author %> + <%= @article.author %> </p> ``` -By outputting `@post.author` using the `<%=` tag, the `to_s` method will be +By outputting `@article.author` using the `<%=` tag, the `to_s` method will be called on the object. By default, this will look quite ugly: ``` @@ -925,15 +929,15 @@ This method works like its brothers, `attr_accessor` and `cattr_accessor`, but provides a setter and getter method on the module with the specified name. To use it, it must be referenced using `Blorgh.author_class`. -The next step is to switch the `Blorgh::Post` model over to this new setting. +The next step is to switch the `Blorgh::Article` model over to this new setting. Change the `belongs_to` association inside this model -(`app/models/blorgh/post.rb`) to this: +(`app/models/blorgh/article.rb`) to this: ```ruby belongs_to :author, class_name: Blorgh.author_class ``` -The `set_author` method in the `Blorgh::Post` model should also use this class: +The `set_author` method in the `Blorgh::Article` model should also use this class: ```ruby self.author = Blorgh.author_class.constantize.find_or_create_by(name: author_name) @@ -960,7 +964,7 @@ Resulting in something a little shorter, and more implicit in its behavior. The `author_class` method should always return a `Class` object. Since we changed the `author_class` method to return a `Class` instead of a -`String`, we must also modify our `belongs_to` definition in the `Blorgh::Post` +`String`, we must also modify our `belongs_to` definition in the `Blorgh::Article` model: ```ruby @@ -985,14 +989,14 @@ to load that class and then reference the related table. This could lead to problems if the table wasn't already existing. Therefore, a `String` should be used and then converted to a class using `constantize` in the engine later on. -Go ahead and try to create a new post. You will see that it works exactly in the +Go ahead and try to create a new article. You will see that it works exactly in the same way as before, except this time the engine is using the configuration setting in `config/initializers/blorgh.rb` to learn what the class is. There are now no strict dependencies on what the class is, only what the API for the class must be. The engine simply requires this class to define a `find_or_create_by` method which returns an object of that class, to be -associated with a post when it's created. This object, of course, should have +associated with an article when it's created. This object, of course, should have some sort of identifier by which it can be referenced. #### General Engine Configuration @@ -1107,12 +1111,12 @@ that isn't referenced by your main application. #### Implementing Decorator Pattern Using Class#class_eval -**Adding** `Post#time_since_created`: +**Adding** `Article#time_since_created`: ```ruby -# MyApp/app/decorators/models/blorgh/post_decorator.rb +# MyApp/app/decorators/models/blorgh/article_decorator.rb -Blorgh::Post.class_eval do +Blorgh::Article.class_eval do def time_since_created Time.current - created_at end @@ -1120,20 +1124,20 @@ end ``` ```ruby -# Blorgh/app/models/post.rb +# Blorgh/app/models/article.rb -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base has_many :comments end ``` -**Overriding** `Post#summary`: +**Overriding** `Article#summary`: ```ruby -# MyApp/app/decorators/models/blorgh/post_decorator.rb +# MyApp/app/decorators/models/blorgh/article_decorator.rb -Blorgh::Post.class_eval do +Blorgh::Article.class_eval do def summary "#{title} - #{truncate(text)}" end @@ -1141,9 +1145,9 @@ end ``` ```ruby -# Blorgh/app/models/post.rb +# Blorgh/app/models/article.rb -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base has_many :comments def summary "#{title}" @@ -1159,13 +1163,13 @@ class modifications, you might want to consider using [`ActiveSupport::Concern`] ActiveSupport::Concern manages load order of interlinked dependent modules and classes at run time allowing you to significantly modularize your code. -**Adding** `Post#time_since_created` and **Overriding** `Post#summary`: +**Adding** `Article#time_since_created` and **Overriding** `Article#summary`: ```ruby -# MyApp/app/models/blorgh/post.rb +# MyApp/app/models/blorgh/article.rb -class Blorgh::Post < ActiveRecord::Base - include Blorgh::Concerns::Models::Post +class Blorgh::Article < ActiveRecord::Base + include Blorgh::Concerns::Models::Article def time_since_created Time.current - created_at @@ -1178,22 +1182,22 @@ end ``` ```ruby -# Blorgh/app/models/post.rb +# Blorgh/app/models/article.rb -class Post < ActiveRecord::Base - include Blorgh::Concerns::Models::Post +class Article < ActiveRecord::Base + include Blorgh::Concerns::Models::Article end ``` ```ruby -# Blorgh/lib/concerns/models/post +# Blorgh/lib/concerns/models/article -module Blorgh::Concerns::Models::Post +module Blorgh::Concerns::Models::Article extend ActiveSupport::Concern # 'included do' causes the included code to be evaluated in the - # context where it is included (post.rb), rather than being - # executed in the module's context (blorgh/concerns/models/post). + # context where it is included (article.rb), rather than being + # executed in the module's context (blorgh/concerns/models/article). included do attr_accessor :author_name belongs_to :author, class_name: "User" @@ -1224,25 +1228,25 @@ 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, it will check in the `app/views` directories of all engines that have this directory. -When the application is asked to render the view for `Blorgh::PostsController`'s +When the application is asked to render the view for `Blorgh::ArticlesController`'s index action, it will first look for the path -`app/views/blorgh/posts/index.html.erb` within the application. If it cannot +`app/views/blorgh/articles/index.html.erb` 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 +`app/views/blorgh/articles/index.html.erb`. Then you can completely change what this view would normally output. -Try this now by creating a new file at `app/views/blorgh/posts/index.html.erb` +Try this now by creating a new file at `app/views/blorgh/articles/index.html.erb` and put this content in it: ```html+erb -<h1>Posts</h1> -<%= link_to "New Post", new_post_path %> -<% @posts.each do |post| %> - <h2><%= post.title %></h2> - <small>By <%= post.author %></small> - <%= simple_format(post.text) %> +<h1>Articles</h1> +<%= link_to "New Article", new_article_path %> +<% @articles.each do |article| %> + <h2><%= article.title %></h2> + <small>By <%= article.author %></small> + <%= simple_format(article.text) %> <hr> <% end %> ``` @@ -1259,30 +1263,30 @@ Routes inside an engine are drawn on the `Engine` class within ```ruby Blorgh::Engine.routes.draw do - resources :posts + resources :articles end ``` By having isolated routes such as this, if you wish to link to an area of an engine from within an application, you will need to use the engine's routing -proxy method. Calls to normal routing methods such as `posts_path` may end up +proxy method. Calls to normal routing methods such as `articles_path` may end up going to undesired locations if both the application and the engine have such a helper defined. -For instance, the following example would go to the application's `posts_path` -if that template was rendered from the application, or the engine's `posts_path` +For instance, the following example would go to the application's `articles_path` +if that template was rendered from the application, or the engine's `articles_path` if it was rendered from the engine: ```erb -<%= link_to "Blog posts", posts_path %> +<%= link_to "Blog articles", articles_path %> ``` -To make this route always use the engine's `posts_path` routing helper method, +To make this route always use the engine's `articles_path` routing helper method, we must call the method on the routing proxy method that shares the same name as the engine. ```erb -<%= link_to "Blog posts", blorgh.posts_path %> +<%= link_to "Blog articles", blorgh.articles_path %> ``` If you wish to reference the application inside the engine in a similar way, use diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md index 027b6303fc..048eb9a6e3 100644 --- a/guides/source/form_helpers.md +++ b/guides/source/form_helpers.md @@ -17,7 +17,6 @@ After reading this guide, you will know: NOTE: This guide is not intended to be a complete documentation of available form helpers and their arguments. Please visit [the Rails API documentation](http://api.rubyonrails.org/) for a complete reference. - Dealing with Basic Forms ------------------------ @@ -32,18 +31,14 @@ The most basic form helper is `form_tag`. When called without arguments like this, it creates a `<form>` tag which, when submitted, will POST to the current page. For instance, assuming the current page is `/home/index`, the generated HTML will look like this (some line breaks added for readability): ```html -<form accept-charset="UTF-8" action="/home/index" method="post"> - <div style="margin:0;padding:0"> - <input name="utf8" type="hidden" value="✓" /> - <input name="authenticity_token" type="hidden" value="f755bb0ed134b76c432144748a6d4b7a7ddf2b71" /> - </div> +<form accept-charset="UTF-8" action="/" method="post"> + <input name="utf8" type="hidden" value="✓" /> + <input name="authenticity_token" type="hidden" value="J7CBxfHalt49OSHp27hblqK20c9PgwJ108nDHX/8Cts=" /> Form contents </form> ``` -Now, you'll notice that the HTML contains something extra: a `div` element with two hidden input elements inside. This div is important, because the form cannot be successfully submitted without it. The first input element with name `utf8` enforces browsers to properly respect your form's character encoding and is generated for all forms whether their actions are "GET" or "POST". The second input element with name `authenticity_token` is a security feature of Rails called **cross-site request forgery protection**, and form helpers generate it for every non-GET form (provided that this security feature is enabled). You can read more about this in the [Security Guide](./security.html#cross-site-request-forgery-csrf). - -NOTE: Throughout this guide, the `div` with the hidden input elements will be excluded from code samples for brevity. +You'll notice that the HTML contains `input` element with type `hidden`. This `input` is important, because the form cannot be successfully submitted without it. The hidden input element has name attribute of `utf8` enforces browsers to properly respect your form's character encoding and is generated for all forms whether their actions are "GET" or "POST". The second input element with name `authenticity_token` is a security feature of Rails called **cross-site request forgery protection**, and form helpers generate it for every non-GET form (provided that this security feature is enabled). You can read more about this in the [Security Guide](security.html#cross-site-request-forgery-csrf). ### A Generic Search Form @@ -67,14 +62,15 @@ 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"><div style="margin:0;padding:0;display:inline"><input name="utf8" type="hidden" value="✓" /></div> +<form accept-charset="UTF-8" action="/search" method="get"> + <input name="utf8" type="hidden" value="✓" /> <label for="q">Search for:</label> <input id="q" name="q" type="text" /> <input name="commit" type="submit" value="Search" /> </form> ``` -TIP: For every form input, an ID attribute is generated from its name ("q" in the example). These IDs can be very useful for CSS styling or manipulation of form controls with JavaScript. +TIP: For every form input, an ID attribute is generated from its name (`"q"` in above example). These IDs can be very useful for CSS styling or manipulation of form controls with JavaScript. Besides `text_field_tag` and `submit_tag`, there is a similar helper for _every_ form control in HTML. @@ -146,7 +142,7 @@ Output: <label for="age_adult">I'm over 21</label> ``` -As with `check_box_tag`, the second parameter to `radio_button_tag` is the value of the input. Because these two radio buttons share the same name (`age`), the user will only be able to select one of them, and `params[:age]` will contain either "child" or "adult". +As with `check_box_tag`, the second parameter to `radio_button_tag` is the value of the input. Because these two radio buttons share the same name (`age`), the user will only be able to select one of them, and `params[:age]` will contain either `"child"` or `"adult"`. NOTE: Always use labels for checkbox and radio buttons. They associate text with a specific option and, by expanding the clickable region, @@ -217,7 +213,7 @@ Dealing with Model Objects ### Model Object Helpers -A particularly common task for a form is editing or creating a model object. While the `*_tag` helpers can certainly be used for this task they are somewhat verbose as for each tag you would have to ensure the correct parameter name is used and set the default value of the input appropriately. Rails provides helpers tailored to this task. These helpers lack the _tag suffix, for example `text_field`, `text_area`. +A particularly common task for a form is editing or creating a model object. While the `*_tag` helpers can certainly be used for this task they are somewhat verbose as for each tag you would have to ensure the correct parameter name is used and set the default value of the input appropriately. Rails provides helpers tailored to this task. These helpers lack the `_tag` suffix, for example `text_field`, `text_area`. For these helpers the first argument is the name of an instance variable and the second is the name of a method (usually an attribute) to call on that object. Rails will set the value of the input control to the return value of that method for the object and set an appropriate input name. If your controller has defined `@person` and that person's name is Henry then a form containing: @@ -239,7 +235,7 @@ Rails provides helpers for displaying the validation errors associated with a mo ### Binding a Form to an Object -While this is an increase in comfort it is far from perfect. If Person has many attributes to edit then we would be repeating the name of the edited object many times. What we want to do is somehow bind a form to a model object, which is exactly what `form_for` does. +While this is an increase in comfort it is far from perfect. If `Person` has many attributes to edit then we would be repeating the name of the edited object many times. What we want to do is somehow bind a form to a model object, which is exactly what `form_for` does. Assume we have a controller for dealing with articles `app/controllers/articles_controller.rb`: @@ -264,7 +260,7 @@ There are a few things to note here: * `@article` is the actual object being edited. * There is a single hash of options. Routing options are passed in the `:url` hash, HTML options are passed in the `:html` hash. Also you can provide a `:namespace` option for your form to ensure uniqueness of id attributes on form elements. The namespace attribute will be prefixed with underscore on the generated HTML id. * The `form_for` method yields a **form builder** object (the `f` variable). -* Methods to create form controls are called **on** the form builder object `f` +* Methods to create form controls are called **on** the form builder object `f`. The resulting HTML is: @@ -280,7 +276,7 @@ The name passed to `form_for` controls the key used in `params` to access the fo The helper methods called on the form builder are identical to the model object helpers except that it is not necessary to specify which object is being edited since this is already managed by the form builder. -You can create a similar binding without actually creating `<form>` tags with the `fields_for` helper. This is useful for editing additional model objects with the same form. For example if you had a Person model with an associated ContactDetail model you could create a form for creating both like so: +You can create a similar binding without actually creating `<form>` tags with the `fields_for` helper. This is useful for editing additional model objects with the same form. For example if you had a `Person` model with an associated `ContactDetail` model you could create a form for creating both like so: ```erb <%= form_for @person, url: {action: "create"} do |person_form| %> @@ -350,7 +346,6 @@ form_for [:admin, :management, @article] For more information on Rails' routing system and the associated conventions, please see the [routing guide](routing.html). - ### How do forms with PATCH, PUT, or DELETE methods work? The Rails framework encourages RESTful design of your applications, which means you'll be making a lot of "PATCH" and "DELETE" requests (besides "GET" and "POST"). However, most browsers _don't support_ methods other than "GET" and "POST" when it comes to submitting forms. @@ -365,12 +360,11 @@ output: ```html <form accept-charset="UTF-8" action="/search" method="post"> - <div style="margin:0;padding:0"> - <input name="_method" type="hidden" value="patch" /> - <input name="utf8" type="hidden" value="✓" /> - <input name="authenticity_token" type="hidden" value="f755bb0ed134b76c432144748a6d4b7a7ddf2b71" /> - </div> + <input name="_method" type="hidden" value="patch" /> + <input name="utf8" type="hidden" value="✓" /> + <input name="authenticity_token" type="hidden" value="f755bb0ed134b76c432144748a6d4b7a7ddf2b71" /> ... +</form> ``` When parsing POSTed data, Rails will take into account the special `_method` parameter and acts as if the HTTP method was the one specified inside it ("PATCH" in this example). @@ -435,7 +429,7 @@ output: Whenever Rails sees that the internal value of an option being generated matches this value, it will add the `selected` attribute to that option. -TIP: The second argument to `options_for_select` must be exactly equal to the desired internal value. In particular if the value is the integer 2 you cannot pass "2" to `options_for_select` - you must pass 2. Be aware of values extracted from the `params` hash as they are all strings. +TIP: The second argument to `options_for_select` must be exactly equal to the desired internal value. In particular if the value is the integer `2` you cannot pass `"2"` to `options_for_select` - you must pass `2`. Be aware of values extracted from the `params` hash as they are all strings. WARNING: when `:include_blank` or `:prompt` are not present, `:include_blank` is forced true if the select attribute `required` is true, display `size` is one and `multiple` is not true. @@ -489,11 +483,11 @@ You can also pass a block to `select` helper: <% end %> ``` -WARNING: If you are using `select` (or similar helpers such as `collection_select`, `select_tag`) to set a `belongs_to` association you must pass the name of the foreign key (in the example above `city_id`), not the name of association itself. If you specify `city` instead of `city_id` Active Record will raise an error along the lines of ` ActiveRecord::AssociationTypeMismatch: City(#17815740) expected, got String(#1138750) ` when you pass the `params` hash to `Person.new` or `update`. Another way of looking at this is that form helpers only edit attributes. You should also be aware of the potential security ramifications of allowing users to edit foreign keys directly. +WARNING: If you are using `select` (or similar helpers such as `collection_select`, `select_tag`) to set a `belongs_to` association you must pass the name of the foreign key (in the example above `city_id`), not the name of association itself. If you specify `city` instead of `city_id` Active Record will raise an error along the lines of `ActiveRecord::AssociationTypeMismatch: City(#17815740) expected, got String(#1138750)` when you pass the `params` hash to `Person.new` or `update`. Another way of looking at this is that form helpers only edit attributes. You should also be aware of the potential security ramifications of allowing users to edit foreign keys directly. ### Option Tags from a Collection of Arbitrary Objects -Generating options tags with `options_for_select` requires that you create an array containing the text and value for each option. But what if you had a City model (perhaps an Active Record one) and you wanted to generate option tags from a collection of those objects? One solution would be to make a nested array by iterating over them: +Generating options tags with `options_for_select` requires that you create an array containing the text and value for each option. But what if you had a `City` model (perhaps an Active Record one) and you wanted to generate option tags from a collection of those objects? One solution would be to make a nested array by iterating over them: ```erb <% cities_array = City.all.map { |city| [city.name, city.id] } %> @@ -540,7 +534,7 @@ Both of these families of helpers will create a series of select boxes for the d ### Barebones Helpers -The `select_*` family of helpers take as their first argument an instance of Date, Time or DateTime that is used as the currently selected value. You may omit this parameter, in which case the current date is used. For example +The `select_*` family of helpers take as their first argument an instance of `Date`, `Time` or `DateTime` that is used as the currently selected value. You may omit this parameter, in which case the current date is used. For example ```erb <%= select_date Date.today, prefix: :start_date %> @@ -554,7 +548,7 @@ outputs (with actual option values omitted for brevity) <select id="start_date_day" name="start_date[day]"> ... </select> ``` -The above inputs would result in `params[:start_date]` being a hash with keys `:year`, `:month`, `:day`. To get an actual Time or Date object you would have to extract these values and pass them to the appropriate constructor, for example +The above inputs would result in `params[:start_date]` being a hash with keys `:year`, `:month`, `:day`. To get an actual `Date`, `Time` or `DateTime` object you would have to extract these values and pass them to the appropriate constructor, for example ```ruby Date.civil(params[:start_date][:year].to_i, params[:start_date][:month].to_i, params[:start_date][:day].to_i) @@ -599,7 +593,7 @@ NOTE: In many cases the built-in date pickers are clumsy as they do not aid the Occasionally you need to display just a single date component such as a year or a month. Rails provides a series of helpers for this, one for each component `select_year`, `select_month`, `select_day`, `select_hour`, `select_minute`, `select_second`. These helpers are fairly straightforward. By default they will generate an input field named after the time component (for example "year" for `select_year`, "month" for `select_month` etc.) although this can be overridden with the `:field_name` option. The `:prefix` option works in the same way that it does for `select_date` and `select_time` and has the same default value. -The first parameter specifies which value should be selected and can either be an instance of a Date, Time or DateTime, in which case the relevant component will be extracted, or a numerical value. For example +The first parameter specifies which value should be selected and can either be an instance of a `Date`, `Time` or `DateTime`, in which case the relevant component will be extracted, or a numerical value. For example ```erb <%= select_year(2009) %> @@ -629,7 +623,7 @@ Rails provides the usual pair of helpers: the barebones `file_field_tag` and the ### What Gets Uploaded -The object in the `params` hash is an instance of a subclass of IO. Depending on the size of the uploaded file it may in fact be a StringIO or an instance of File backed by a temporary file. In both cases the object will have an `original_filename` attribute containing the name the file had on the user's computer and a `content_type` attribute containing the MIME type of the uploaded file. The following snippet saves the uploaded content in `#{Rails.root}/public/uploads` under the same name as the original file (assuming the form was the one in the previous example). +The object in the `params` hash is an instance of a subclass of `IO`. Depending on the size of the uploaded file it may in fact be a StringIO or an instance of `File` backed by a temporary file. In both cases the object will have an `original_filename` attribute containing the name the file had on the user's computer and a `content_type` attribute containing the MIME type of the uploaded file. The following snippet saves the uploaded content in `#{Rails.root}/public/uploads` under the same name as the original file (assuming the form was the one in the previous example). ```ruby def upload @@ -640,7 +634,7 @@ def upload end ``` -Once a file has been uploaded, there are a multitude of potential tasks, ranging from where to store the files (on disk, Amazon S3, etc) and associating them with models to resizing image files and generating thumbnails. The intricacies of this are beyond the scope of this guide, but there are several libraries designed to assist with these. Two of the better known ones are [CarrierWave](https://github.com/jnicklas/carrierwave) and [Paperclip](http://www.thoughtbot.com/projects/paperclip). +Once a file has been uploaded, there are a multitude of potential tasks, ranging from where to store the files (on disk, Amazon S3, etc) and associating them with models to resizing image files and generating thumbnails. The intricacies of this are beyond the scope of this guide, but there are several libraries designed to assist with these. Two of the better known ones are [CarrierWave](https://github.com/jnicklas/carrierwave) and [Paperclip](https://github.com/thoughtbot/paperclip). NOTE: If the user has not selected a file the corresponding parameter will be an empty string. @@ -651,7 +645,7 @@ Unlike other forms making an asynchronous file upload form is not as simple as p Customizing Form Builders ------------------------- -As mentioned previously the object yielded by `form_for` and `fields_for` is an instance of FormBuilder (or a subclass thereof). Form builders encapsulate the notion of displaying form elements for a single object. While you can of course write helpers for your forms in the usual way, you can also subclass FormBuilder and add the helpers there. For example +As mentioned previously the object yielded by `form_for` and `fields_for` is an instance of `FormBuilder` (or a subclass thereof). Form builders encapsulate the notion of displaying form elements for a single object. While you can of course write helpers for your forms in the usual way, you can also subclass `FormBuilder` and add the helpers there. For example ```erb <%= form_for @person do |f| %> @@ -667,7 +661,7 @@ can be replaced with <% end %> ``` -by defining a LabellingFormBuilder class similar to the following: +by defining a `LabellingFormBuilder` class similar to the following: ```ruby class LabellingFormBuilder < ActionView::Helpers::FormBuilder @@ -685,7 +679,7 @@ The form builder used also determines what happens when you do <%= render partial: f %> ``` -If `f` is an instance of FormBuilder then this will render the `form` partial, setting the partial's object to the form builder. If the form builder is of class LabellingFormBuilder then the `labelling_form` partial would be rendered instead. +If `f` is an instance of `FormBuilder` then this will render the `form` partial, setting the partial's object to the form builder. If the form builder is of class `LabellingFormBuilder` then the `labelling_form` partial would be rendered instead. Understanding Parameter Naming Conventions ------------------------------------------ @@ -862,7 +856,7 @@ Or if you don't want to render an `authenticity_token` field: Building Complex Forms ---------------------- -Many apps grow beyond simple forms editing a single object. For example when creating a Person you might want to allow the user to (on the same form) create multiple address records (home, work, etc.). When later editing that person the user should be able to add, remove or amend addresses as necessary. +Many apps grow beyond simple forms editing a single object. For example when creating a `Person` you might want to allow the user to (on the same form) create multiple address records (home, work, etc.). When later editing that person the user should be able to add, remove or amend addresses as necessary. ### Configuring the Model diff --git a/guides/source/generators.md b/guides/source/generators.md index 4a5377c206..be64f1638d 100644 --- a/guides/source/generators.md +++ b/guides/source/generators.md @@ -23,19 +23,19 @@ When you create an application using the `rails` command, you are in fact using ```bash $ rails new myapp $ cd myapp -$ rails generate +$ bin/rails generate ``` You will get a list of all generators that comes with Rails. If you need a detailed description of the helper generator, for example, you can simply do: ```bash -$ rails generate helper --help +$ bin/rails generate helper --help ``` Creating Your First Generator ----------------------------- -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`. +Since Rails 3.0, generators are built on top of [Thor](https://github.com/erikhuda/thor). Thor provides powerful options for 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: @@ -54,13 +54,13 @@ Our new generator is quite simple: it inherits from `Rails::Generators::Base` an To invoke our new generator, we just need to do: ```bash -$ rails generate initializer +$ bin/rails generate initializer ``` Before we go on, let's see our brand new generator description: ```bash -$ rails generate initializer --help +$ bin/rails generate initializer --help ``` Rails is usually able to generate good descriptions if a generator is namespaced, as `ActiveRecord::Generators::ModelGenerator`, but not in this particular case. We can solve this problem in two ways. The first one is calling `desc` inside our generator: @@ -82,7 +82,7 @@ Creating Generators with Generators Generators themselves have a generator: ```bash -$ rails generate generator initializer +$ bin/rails generate generator initializer create lib/generators/initializer create lib/generators/initializer/initializer_generator.rb create lib/generators/initializer/USAGE @@ -102,7 +102,7 @@ First, notice that we are inheriting from `Rails::Generators::NamedBase` instead We can see that by invoking the description of this new generator (don't forget to delete the old generator file): ```bash -$ rails generate initializer --help +$ bin/rails generate initializer --help Usage: rails generate initializer NAME [options] ``` @@ -130,7 +130,7 @@ end And let's execute our generator: ```bash -$ rails generate initializer core_extensions +$ bin/rails generate initializer core_extensions ``` We can see that now an initializer named core_extensions was created at `config/initializers/core_extensions.rb` with the contents of our template. That means that `copy_file` copied a file in our source root to the destination path we gave. The method `file_name` is automatically created when we inherit from `Rails::Generators::NamedBase`. @@ -169,7 +169,7 @@ end Before we customize our workflow, let's first see what our scaffold looks like: ```bash -$ rails generate scaffold User name:string +$ bin/rails generate scaffold User name:string invoke active_record create db/migrate/20130924151154_create_users.rb create app/models/user.rb @@ -207,7 +207,7 @@ $ rails generate scaffold User name:string Looking at this output, it's easy to understand how generators work in Rails 3.0 and above. The scaffold generator doesn't actually generate anything, it just invokes others to do the work. This allows us to add/replace/remove any of those invocations. For instance, the scaffold generator invokes the scaffold_controller generator, which invokes erb, test_unit and helper generators. Since each generator has a single responsibility, they are easy to reuse, avoiding code duplication. -Our first customization on the workflow will be to stop generating stylesheets, javascripts 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 stylesheet, JavaScript and test fixture files for scaffolds. We can achieve that by changing our configuration to the following: ```ruby config.generators do |g| @@ -219,12 +219,12 @@ config.generators do |g| end ``` -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. +If we generate another resource with the scaffold generator, we can see that stylesheet, JavaScript and fixture files 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 +$ bin/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 @@ -248,10 +248,10 @@ end end ``` -We can try out our new generator by creating a helper for users: +We can try out our new generator by creating a helper for products: ```bash -$ rails generate my_helper products +$ bin/rails generate my_helper products create app/helpers/products_helper.rb ``` @@ -279,10 +279,10 @@ end and see it in action when invoking the generator: ```bash -$ rails generate scaffold Post body:text +$ bin/rails generate scaffold Article body:text [...] invoke my_helper - create app/helpers/posts_helper.rb + create app/helpers/articles_helper.rb ``` We can notice on the output that our new helper was invoked instead of the Rails default. However one thing is missing, which is tests for our new generator and to do that, we are going to reuse old helpers test generators. @@ -365,7 +365,7 @@ end Now, if you create a Comment scaffold, you will see that the shoulda generators are being invoked, and at the end, they are just falling back to TestUnit generators: ```bash -$ rails generate scaffold Comment body:text +$ bin/rails generate scaffold Comment body:text invoke active_record create db/migrate/20130924143118_create_comments.rb create app/models/comment.rb @@ -507,7 +507,7 @@ Replaces text inside a file. gsub_file 'name_of_file.rb', 'method.to_be_replaced', 'method.the_replacing_code' ``` -Regular Expressions can be used to make this method more precise. You can also use append_file and prepend_file in the same way to place code at the beginning and end of a file respectively. +Regular Expressions can be used to make this method more precise. You can also use `append_file` and `prepend_file` in the same way to place code at the beginning and end of a file respectively. ### `application` diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index fa964e4450..656d74ef06 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -70,13 +70,11 @@ Creating a New Rails Project The best way to use this guide is to follow each step as it happens, no code or step needed to make this example application has been left out, so you can -literally follow along step by step. You can get the complete code -[here](https://github.com/rails/docrails/tree/master/guides/code/getting_started). +literally follow along step by step. By following along with this guide, you'll create a Rails project called -`blog`, a -(very) simple weblog. Before you can start building the application, you need to -make sure that you have Rails itself installed. +`blog`, a (very) simple weblog. Before you can start building the application, +you need to make sure that you have Rails itself installed. TIP: The examples below use `$` to represent your terminal prompt in a UNIX-like OS, though it may have been customized to appear differently. If you are using Windows, @@ -89,9 +87,9 @@ Open up a command line prompt. On Mac OS X open Terminal.app, on Windows choose dollar sign `$` should be run in the command line. Verify that you have a current version of Ruby installed: -TIP. A number of tools exist to help you quickly install Ruby and Ruby +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). +while Mac OS X users can use [Tokaido](https://github.com/tokaido/tokaidoapp). ```bash $ ruby -v @@ -125,7 +123,7 @@ run the following: $ rails --version ``` -If it says something like "Rails 4.1.0", you are ready to continue. +If it says something like "Rails 4.2.0", you are ready to continue. ### Creating the Blog Application @@ -163,11 +161,11 @@ of the files and folders that Rails created by default: | File/Folder | Purpose | | ----------- | ------- | |app/|Contains the controllers, models, views, helpers, mailers and assets for your application. You'll focus on this folder for the remainder of this guide.| -|bin/|Contains the rails script that starts your app and can contain other scripts you use to deploy or run your application.| +|bin/|Contains the rails script that starts your app and can contain other scripts you use to setup, deploy or run your application.| |config/|Configure your application's routes, database, and more. This is covered in more detail in [Configuring Rails Applications](configuring.html).| |config.ru|Rack configuration for Rack based servers used to start the application.| |db/|Contains your current database schema, as well as the database migrations.| -|Gemfile<br>Gemfile.lock|These files allow you to specify what gem dependencies are needed for your Rails application. These files are used by the Bundler gem. For more information about Bundler, see [the Bundler website](http://gembundler.com).| +|Gemfile<br>Gemfile.lock|These files allow you to specify what gem dependencies are needed for your Rails application. These files are used by the Bundler gem. For more information about Bundler, see [the Bundler website](http://bundler.io).| |lib/|Extended modules for your application.| |log/|Application log files.| |public/|The only folder seen by the world as-is. Contains static files and compiled assets.| @@ -190,7 +188,7 @@ start a web server on your development machine. You can do this by running the following in the `blog` directory: ```bash -$ rails server +$ bin/rails server ``` TIP: Compiling CoffeeScript to JavaScript requires a JavaScript runtime and the @@ -243,7 +241,7 @@ tell it you want a controller called "welcome" with an action called "index", just like this: ```bash -$ rails generate controller welcome index +$ bin/rails generate controller welcome index ``` Rails will create several files and a route for you. @@ -267,8 +265,9 @@ 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 @@ -358,7 +357,7 @@ will be seen later, but for now notice that Rails has inferred the singular form `article` and makes meaningful use of the distinction. ```bash -$ rake routes +$ bin/rake routes Prefix Verb URI Pattern Controller#Action articles GET /articles(.:format) articles#index POST /articles(.:format) articles#create @@ -396,7 +395,7 @@ a controller called `ArticlesController`. You can do this by running this command: ```bash -$ rails g controller articles +$ bin/rails g controller articles ``` If you open up the newly generated `app/controllers/articles_controller.rb` @@ -433,17 +432,16 @@ class, define a `new` method so that the controller now looks like this: ```ruby class ArticlesController < ApplicationController - def new end - end ``` With the `new` method defined in `ArticlesController`, if you refresh <http://localhost:3000/articles/new> you'll see another error: - +![Template is missing for articles/new] +(images/getting_started/template_is_missing_articles_new.png) 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 @@ -452,9 +450,7 @@ available, Rails errors out. In the above image, the bottom line has been truncated. Let's see what the full thing looks like: -<blockquote> -Missing template articles/new, application/new with {locale:[:en], formats:[:html], handlers:[:erb, :builder, :coffee]}. Searched in: * "/path/to/blog/app/views" -</blockquote> +>Missing template articles/new, application/new with {locale:[:en], formats:[:html], handlers:[:erb, :builder, :coffee]}. Searched in: * "/path/to/blog/app/views" That's quite a lot of text! Let's quickly go through and understand what each part of it does. @@ -500,14 +496,13 @@ harmoniously! It's time to create the form for a new article. ### The first form -To create a form within this template, you will use a <em>form -builder</em>. The primary form builder for Rails is provided by a helper +To create a form within this template, you will use a *form +builder*. The primary form builder for Rails is provided by a helper method called `form_for`. To use this method, add this code into `app/views/articles/new.html.erb`: ```html+erb <%= form_for :article do |f| %> - <p> <%= f.label :title %><br> <%= f.text_field :title %> @@ -521,7 +516,6 @@ method called `form_for`. To use this method, add this code into <p> <%= f.submit %> </p> - <% end %> ``` @@ -558,7 +552,7 @@ To see what Rails will do with this, we look back at the output of `rake routes`: ```bash -$ rake routes +$ bin/rake routes Prefix Verb URI Pattern Controller#Action articles GET /articles(.:format) articles#index POST /articles(.:format) articles#create @@ -571,11 +565,10 @@ edit_article GET /articles/:id/edit(.:format) articles#edit root GET / welcome#index ``` -The `articles_path` helper tells Rails to point the form -to the URI Pattern associated with the `articles` 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 `ArticlesController`. +The `articles_path` helper tells Rails to point the form to the URI Pattern +associated with the `articles` 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 `ArticlesController`. 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 @@ -596,13 +589,11 @@ underneath the `new` action, as shown: ```ruby class ArticlesController < ApplicationController - def new end def create end - end ``` @@ -628,6 +619,8 @@ method returns an `ActiveSupport::HashWithIndifferentAccess` object, which allows you to access the keys of the hash using either strings or symbols. In this situation, the only parameters that matter are the ones from the form. +TIP: Ensure you have a firm grasp of the `params` method, as you'll use it fairly regularly. Let's consider an example URL: **http://www.example.com/?username=dhh&email=dhh@email.com**. In this URL, `params[:username]` would equal "dhh" and `params[:email]` would equal "dhh@email.com". + If you re-submit the form one more time you'll now no longer get the missing template error. Instead, you'll see something that looks like the following: @@ -641,13 +634,13 @@ parameters but nothing in particular is being done with them. ### Creating the Article model -Models in Rails use a singular name, and their corresponding database tables use -a plural name. Rails provides a generator for creating models, which -most Rails developers tend to use when creating new models. -To create the new model, run this command in your terminal: +Models in Rails use a singular name, and their corresponding database tables +use a plural name. Rails provides a generator for creating models, which most +Rails developers tend to use when creating new models. To create the new model, +run this command in your terminal: ```bash -$ rails generate model Article title:string text:text +$ bin/rails generate model Article title:string text:text ``` With that command we told Rails that we want a `Article` model, together @@ -655,26 +648,23 @@ with a _title_ attribute of type string, and a _text_ attribute of type text. Those attributes are automatically added to the `articles` table in the database and mapped to the `Article` model. -Rails responded by creating a bunch of files. For -now, we're only interested in `app/models/article.rb` and -`db/migrate/20140120191729_create_articles.rb` (your name could be a bit -different). The latter is responsible -for creating the database structure, which is what we'll look at next. +Rails responded by creating a bunch of files. For now, we're only interested +in `app/models/article.rb` and `db/migrate/20140120191729_create_articles.rb` +(your name could be a bit different). The latter is responsible for creating +the database structure, which is what we'll look at next. -TIP: Active Record is smart enough to automatically map column names to -model attributes, which means you don't have to declare attributes -inside Rails models, as that will be done automatically by Active -Record. +TIP: Active Record is smart enough to automatically map column names to model +attributes, which means you don't have to declare attributes inside Rails +models, as that will be done automatically by Active Record. ### Running a Migration -As we've just seen, `rails generate model` created a _database -migration_ file inside the `db/migrate` directory. -Migrations are Ruby classes that are designed to make it simple to -create and modify database tables. Rails uses rake commands to run migrations, -and it's possible to undo a migration after it's been applied to your database. -Migration filenames include a timestamp to ensure that they're processed in the -order that they were created. +As we've just seen, `rails generate model` created a _database migration_ file +inside the `db/migrate` directory. Migrations are Ruby classes that are +designed to make it simple to create and modify database tables. Rails uses +rake commands to run migrations, and it's possible to undo a migration after +it's been applied to your database. Migration filenames include a timestamp to +ensure that they're processed in the order that they were created. If you look in the `db/migrate/20140120191729_create_articles.rb` file (remember, yours will have a slightly different name), here's what you'll find: @@ -699,13 +689,13 @@ in case you want to reverse it later. When you run this migration it will create an `articles` table with one string column and a text column. It also creates two timestamp fields to allow Rails to track article creation and update times. -TIP: For more information about migrations, refer to [Rails Database -Migrations](migrations.html). +TIP: For more information about migrations, refer to [Rails Database Migrations] +(migrations.html). At this point, you can use a rake command to run the migration: ```bash -$ rake db:migrate +$ bin/rake db:migrate ``` Rails will execute this migration command and tell you it created the Articles @@ -742,50 +732,48 @@ end Here's what's going on: every Rails model can be initialized with its respective attributes, which are automatically mapped to the respective -database columns. In the first line we do just that -(remember that `params[:article]` contains the attributes we're interested in). -Then, `@article.save` is responsible for saving the model in the database. -Finally, we redirect the user to the `show` action, which we'll define later. +database columns. In the first line we do just that (remember that +`params[:article]` contains the attributes we're interested in). Then, +`@article.save` is responsible for saving the model in the database. Finally, +we redirect the user to the `show` action, which we'll define later. + +TIP: You might be wondering why the `A` in `Article.new` is capitalized above, whereas most other references to articles in this guide have used lowercase. In this context, we are referring to the class named `Article` that is defined in `\models\article.rb`. Class names in Ruby must begin with a capital letter. -TIP: As we'll see later, `@article.save` returns a boolean indicating -whether the article was saved or not. +TIP: As we'll see later, `@article.save` returns a boolean indicating whether +the article was saved or not. -If you now go to -<http://localhost:3000/articles/new> you'll *almost* be able to create an -article. Try it! You should get an error that looks like this: +If you now go to <http://localhost:3000/articles/new> you'll *almost* be able +to create an article. Try it! You should get an error that looks like this: - +![Forbidden attributes for new article] +(images/getting_started/forbidden_attributes_for_new_article.png) Rails has several security features that help you write secure applications, -and you're running into one of them now. This one is called -`[strong_parameters](http://guides.rubyonrails.org/action_controller_overview.html#strong-parameters)`, -which requires us to tell Rails exactly which parameters are allowed into -our controller actions. - -Why do you have to bother? The ability to grab and automatically assign -all controller parameters to your model in one shot makes the programmer's -job easier, but this convenience also allows malicious use. What if a -request to the server was crafted to look like a new article form submit -but also included extra fields with values that violated your applications -integrity? They would be 'mass assigned' into your model and then into the -database along with the good stuff - potentially breaking your application -or worse. - -We have to whitelist our controller parameters to prevent wrongful -mass assignment. In this case, we want to both allow and require the -`title` and `text` parameters for valid use of `create`. The syntax for -this introduces `require` and `permit`. The change will involve one line -in the `create` action: +and you're running into one of them now. This one is called [strong parameters](http://guides.rubyonrails.org/action_controller_overview.html#strong-parameters), +which requires us to tell Rails exactly which parameters are allowed into our +controller actions. + +Why do you have to bother? The ability to grab and automatically assign all +controller parameters to your model in one shot makes the programmer's job +easier, but this convenience also allows malicious use. What if a request to +the server was crafted to look like a new article form submit but also included +extra fields with values that violated your applications integrity? They would +be 'mass assigned' into your model and then into the database along with the +good stuff - potentially breaking your application or worse. + +We have to whitelist our controller parameters to prevent wrongful mass +assignment. In this case, we want to both allow and require the `title` and +`text` parameters for valid use of `create`. The syntax for this introduces +`require` and `permit`. The change will involve one line in the `create` action: ```ruby @article = Article.new(params.require(:article).permit(:title, :text)) ``` -This is often factored out into its own method so it can be reused by -multiple actions in the same controller, for example `create` and `update`. -Above and beyond mass assignment issues, the method is often made -`private` to make sure it can't be called outside its intended context. -Here is the result: +This is often factored out into its own method so it can be reused by multiple +actions in the same controller, for example `create` and `update`. Above and +beyond mass assignment issues, the method is often made `private` to make sure +it can't be called outside its intended context. Here is the result: ```ruby def create @@ -802,13 +790,14 @@ private ``` TIP: For more information, refer to the reference above and -[this blog article about Strong Parameters](http://weblog.rubyonrails.org/2012/3/21/strong-parameters/). +[this blog article about Strong Parameters] +(http://weblog.rubyonrails.org/2012/3/21/strong-parameters/). ### Showing Articles -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. +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: @@ -824,17 +813,15 @@ As we did before, we need to add the `show` action in `app/controllers/articles_controller.rb` and its respective view. NOTE: A frequent practice is to place the standard CRUD actions in each -controller in the following order: `index`, `show`, `new`, `edit`, `create`, -`update` and `destroy`. You may use any order you choose, but keep in mind that -these are public methods; as mentioned earlier in this guide, they must be -placed before any private or protected method in the controller in order to -work. +controller in the following order: `index`, `show`, `new`, `edit`, `create`, `update` +and `destroy`. You may use any order you choose, but keep in mind that these +are public methods; as mentioned earlier in this guide, they must be placed +before any private or protected method in the controller in order to work. Given that, let's add the `show` action, as follows: ```ruby class ArticlesController < ApplicationController - def show @article = Article.find(params[:id]) end @@ -887,7 +874,6 @@ first method in the controller. Let's do it: ```ruby class ArticlesController < ApplicationController - def index @articles = Article.all end @@ -981,9 +967,9 @@ article can go back and view the whole list again: <%= link_to 'Back', articles_path %> ``` -TIP: If you want to link to an action in the same controller, you don't -need to specify the `:controller` option, as Rails will use the current -controller by default. +TIP: If you want to link to an action in the same controller, you don't need to +specify the `:controller` option, as Rails will use the current controller by +default. TIP: In development mode (which is what you're working in by default), Rails reloads your application with every browser request, so there's no need to stop @@ -1018,7 +1004,7 @@ These changes will ensure that all articles have a title that is at least five characters long. Rails can validate a variety of conditions in a model, including the presence or uniqueness of columns, their format, and the existence of associated objects. Validations are covered in detail in [Active -Record Validations](active_record_validations.html) +Record Validations](active_record_validations.html). With the validation now in place, when you call `@article.save` on an invalid article, it will return `false`. If you open @@ -1341,8 +1327,8 @@ The reason we can use this shorter, simpler `form_for` declaration to stand in for either of the other forms is that `@article` is a *resource* corresponding to a full set of RESTful routes, and Rails is able to infer 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). +For more information about this use of `form_for`, see [Resource-oriented style] +(http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for-label-Resource-oriented+style). Now, let's update the `app/views/articles/new.html.erb` view to use this new partial, rewriting it completely: @@ -1403,7 +1389,6 @@ The complete `ArticlesController` in the ```ruby class ArticlesController < ApplicationController - def index @articles = Article.all end @@ -1459,8 +1444,7 @@ them from the database. Note that we don't need to add a view for this action since we're redirecting to the `index` action. Finally, add a 'Destroy' link to your `index` action template -(`app/views/articles/index.html.erb`) to wrap everything -together. +(`app/views/articles/index.html.erb`) to wrap everything together. ```html+erb <h1>Listing Articles</h1> @@ -1517,7 +1501,7 @@ the `Article` model. This time we'll create a `Comment` model to hold reference of article comments. Run this command in your terminal: ```bash -$ rails generate model Comment commenter:string body:text article:references +$ bin/rails generate model Comment commenter:string body:text article:references ``` This command will generate four files: @@ -1565,7 +1549,7 @@ the two models. An index for this association is also created on this column. Go ahead and run the migration: ```bash -$ rake db:migrate +$ bin/rake db:migrate ``` Rails is smart enough to only execute the migrations that have not already been @@ -1641,7 +1625,7 @@ With the model in hand, you can turn your attention to creating a matching controller. Again, we'll use the same generator we used before: ```bash -$ rails generate controller Comments +$ bin/rails generate controller Comments ``` This creates six files and one empty directory: @@ -1886,7 +1870,7 @@ Then you make the `app/views/articles/show.html.erb` look like the following: <%= render @article.comments %> <h2>Add a comment:</h2> -<%= render "comments/form" %> +<%= render 'comments/form' %> <%= link_to 'Edit Article', edit_article_path(@article) %> | <%= link_to 'Back to Articles', articles_path %> @@ -2025,7 +2009,7 @@ class CommentsController < ApplicationController ``` Now if you try to create a new article, you will be greeted with a basic HTTP -Authentication challenge +Authentication challenge:  @@ -2040,7 +2024,7 @@ along with a number of others. Security, especially in web applications, is a broad and detailed area. Security in your Rails application is covered in more depth in -The [Ruby on Rails Security Guide](security.html) +the [Ruby on Rails Security Guide](security.html). What's Next? @@ -2051,7 +2035,7 @@ update it and experiment on your own. But you don't have to do everything without help. As you need assistance getting up and running with Rails, feel free to consult these support resources: -* The [Ruby on Rails guides](index.html) +* The [Ruby on Rails Guides](index.html) * The [Ruby on Rails Tutorial](http://railstutorial.org/book) * 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 diff --git a/guides/source/i18n.md b/guides/source/i18n.md index c1b575c7b7..1023598aa4 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -107,7 +107,7 @@ The **translations load path** (`I18n.load_path`) is just a Ruby Array of paths NOTE: The backend will lazy-load these translations when a translation is looked up for the first time. This makes it possible to just swap the backend with something else even after translations have already been announced. -The default `application.rb` files has instructions on how to add locales from another directory and how to set a different default locale. Just uncomment and edit the specific lines. +The default `application.rb` file has instructions on how to add locales from another directory and how to set a different default locale. Just uncomment and edit the specific lines. ```ruby # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. @@ -137,7 +137,7 @@ If you want to translate your Rails application to a **single language other tha However, you would probably like to **provide support for more locales** in your application. In such case, you need to set and pass the locale between requests. -WARNING: You may be tempted to store the chosen locale in a _session_ or a <em>cookie</em>. However, **do not do this**. The locale should be transparent and a part of the URL. This way you won't break people's basic assumptions about the web itself: if you send a URL to a friend, they should see the same page and content as you. A fancy word for this would be that you're being [<em>RESTful</em>](http://en.wikipedia.org/wiki/Representational_State_Transfer). Read more about the RESTful approach in [Stefan Tilkov's articles](http://www.infoq.com/articles/rest-introduction). Sometimes there are exceptions to this rule and those are discussed below. +WARNING: You may be tempted to store the chosen locale in a _session_ or a *cookie*. However, **do not do this**. The locale should be transparent and a part of the URL. This way you won't break people's basic assumptions about the web itself: if you send a URL to a friend, they should see the same page and content as you. A fancy word for this would be that you're being [*RESTful*](http://en.wikipedia.org/wiki/Representational_State_Transfer). Read more about the RESTful approach in [Stefan Tilkov's articles](http://www.infoq.com/articles/rest-introduction). Sometimes there are exceptions to this rule and those are discussed below. The _setting part_ is easy. You can set the locale in a `before_action` in the `ApplicationController` like this: @@ -262,7 +262,7 @@ get '/:locale' => 'dashboard#index' Do take special care about the **order of your routes**, so this route declaration does not "eat" other ones. (You may want to add it directly before the `root :to` declaration.) -NOTE: Have a look at two plugins which simplify work with routes in this way: Sven Fuchs's [routing_filter](https://github.com/svenfuchs/routing-filter/tree/master) and Raul Murciano's [translate_routes](https://github.com/raul/translate_routes/tree/master). +NOTE: Have a look at two plugins which simplify working with routes in this way: Sven Fuchs's [routing_filter](https://github.com/svenfuchs/routing-filter/tree/master) and Raul Murciano's [translate_routes](https://github.com/raul/translate_routes/tree/master). ### Setting the Locale from the Client Supplied Information @@ -437,11 +437,11 @@ TIP: Right now you might need to add some more date/time formats in order to mak ### Inflection Rules For Other Locales -Rails 4.0 allows you to define inflection rules (such as rules for singularization and pluralization) for locales other than English. In `config/initializers/inflections.rb`, you can define these rules for multiple locales. The initializer contains a default example for specifying additional rules for English; follow that format for other locales as you see fit. +Rails allows you to define inflection rules (such as rules for singularization and pluralization) for locales other than English. In `config/initializers/inflections.rb`, you can define these rules for multiple locales. The initializer contains a default example for specifying additional rules for English; follow that format for other locales as you see fit. ### Localized Views -Rails 2.3 introduces another convenient localization feature: localized views (templates). Let's say you have a _BooksController_ in your application. Your _index_ action renders content in `app/views/books/index.html.erb` template. When you put a _localized variant_ of this template: `index.es.html.erb` in the same directory, Rails will render content in this template, when the locale is set to `:es`. When the locale is set to the default locale, the generic `index.html.erb` view will be used. (Future Rails versions may well bring this _automagic_ localization to assets in `public`, etc.) +Let's say you have a _BooksController_ in your application. Your _index_ action renders content in `app/views/books/index.html.erb` template. When you put a _localized variant_ of this template: `index.es.html.erb` in the same directory, Rails will render content in this template, when the locale is set to `:es`. When the locale is set to the default locale, the generic `index.html.erb` view will be used. (Future Rails versions may well bring this _automagic_ localization to assets in `public`, etc.) You can make use of this feature, e.g. when working with a large amount of static content, which would be clumsy to put inside YAML or Ruby dictionaries. Bear in mind, though, that any change you would like to do later to the template must be propagated to all of them. @@ -712,6 +712,19 @@ en: Then `User.model_name.human(count: 2)` will return "Dudes". With `count: 1` or without params will return "Dude". +In the event you need to access nested attributes within a given model, you should nest these under `model/attribute` at the model level of your translation file: + +```yaml +en: + activerecord: + attributes: + user/gender: + female: "Female" + male: "Male" +``` + +Then `User.human_attribute_name("gender.female")` will return "Female". + #### Error Message Scopes Active Record validation error messages can also be translated easily. Active Record gives you a couple of namespaces where you can place your message translations in order to provide different messages and translation for certain models, attributes, and/or validations. It also transparently takes single table inheritance into account. @@ -847,6 +860,24 @@ en: subject: "Welcome to Rails Guides!" ``` +To send parameters to interpolation use the `default_i18n_subject` method on the mailer. + +```ruby +# user_mailer.rb +class UserMailer < ActionMailer::Base + def welcome(user) + mail(to: user.email, subject: default_i18n_subject(user: user.name)) + end +end +``` + +```yaml +en: + user_mailer: + welcome: + subject: "%{user}, welcome to Rails Guides!" +``` + ### Overview of Other Built-In Methods that Provide I18n Support Rails uses fixed strings and other localizations, such as format strings and other format information in a couple of helpers. Here's a brief overview. diff --git a/guides/source/index.html.erb b/guides/source/index.html.erb index 57c224c165..2fdf18a2e9 100644 --- a/guides/source/index.html.erb +++ b/guides/source/index.html.erb @@ -9,6 +9,7 @@ Ruby on Rails Guides <% content_for :index_section do %> <div id="subCol"> <dl> + <dt></dt> <dd class="kindle">Rails Guides are also available for <%= link_to 'Kindle', @mobi %>.</dd> <dd class="work-in-progress">Guides marked with this icon are currently being worked on and will not be available in the Guides Index menu. While still useful, they may contain incomplete information and even errors. You can help by reviewing them and posting your comments and corrections.</dd> </dl> diff --git a/guides/source/initialization.md b/guides/source/initialization.md index 00b2761716..b81b048c35 100644 --- a/guides/source/initialization.md +++ b/guides/source/initialization.md @@ -98,9 +98,9 @@ configure the load path for your Gemfile's dependencies. A standard Rails application depends on several gems, specifically: -* abstract * actionmailer * actionpack +* actionview * activemodel * activerecord * activesupport @@ -119,7 +119,7 @@ A standard Rails application depends on several gems, specifically: * rails * railties * rake -* sqlite3-ruby +* sqlite3 * thor * treetop * tzinfo @@ -301,7 +301,7 @@ def default_options end ``` -There is no `REQUEST_METHOD` key in `ENV` so we can skip over that line. The next line merges in the options from `opt_parser` which is defined plainly in `Rack::Server` +There is no `REQUEST_METHOD` key in `ENV` so we can skip over that line. The next line merges in the options from `opt_parser` which is defined plainly in `Rack::Server`: ```ruby def opt_parser @@ -559,7 +559,7 @@ initialized. When `config/application.rb` has finished loading Rails and defined the application namespace, we go back to `config/environment.rb`, where the application is initialized. For example, if the application was called `Blog`, here we would find `Rails.application.initialize!`, which is -defined in `rails/application.rb` +defined in `rails/application.rb`. ### `railties/lib/rails/application.rb` @@ -575,7 +575,7 @@ end ``` 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` +the `run_initializers` method which is defined in `railties/lib/rails/initializable.rb`: ```ruby def run_initializers(group=:default, *args) @@ -703,4 +703,4 @@ the last piece of our journey in the Rails initialization process. This high level overview will help you understand when your code is executed and how, and overall become a better Rails developer. If you still want to know more, the Rails source code itself is probably the -best place to go next.
\ No newline at end of file +best place to go next. diff --git a/guides/source/layout.html.erb b/guides/source/layout.html.erb index 1ac4e7f40c..1005057ca9 100644 --- a/guides/source/layout.html.erb +++ b/guides/source/layout.html.erb @@ -1,5 +1,4 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" - "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> @@ -36,7 +35,6 @@ <li class="more-info"><a href="https://github.com/rails/rails">Code</a></li> <li class="more-info"><a href="http://rubyonrails.org/screencasts">Screencasts</a></li> <li class="more-info"><a href="http://rubyonrails.org/documentation">Documentation</a></li> - <li class="more-info"><a href="http://rubyonrails.org/ecosystem">Ecosystem</a></li> <li class="more-info"><a href="http://rubyonrails.org/community">Community</a></li> <li class="more-info"><a href="http://weblog.rubyonrails.org/">Blog</a></li> </ul> @@ -78,7 +76,6 @@ </select> </li> </ul> - </div> </div> </div> <hr class="hide" /> diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md index bd33c5a146..f00f7bca1b 100644 --- a/guides/source/layouts_and_rendering.md +++ b/guides/source/layouts_and_rendering.md @@ -308,7 +308,7 @@ TIP: This option should be used only if you don't care about the content type of the response. Using `:plain` or `:html` might be more appropriate in most of the time. -NOTE: Unless overriden, your response returned from this render option will be +NOTE: Unless overridden, your response returned from this render option will be `text/html`, as that is the default content type of Action Dispatch response. #### Options for `render` @@ -506,33 +506,33 @@ Layout declarations cascade downward in the hierarchy, and more specific layout end ``` -* `posts_controller.rb` +* `articles_controller.rb` ```ruby - class PostsController < ApplicationController + class ArticlesController < ApplicationController end ``` -* `special_posts_controller.rb` +* `special_articles_controller.rb` ```ruby - class SpecialPostsController < PostsController + class SpecialArticlesController < ArticlesController layout "special" end ``` -* `old_posts_controller.rb` +* `old_articles_controller.rb` ```ruby - class OldPostsController < SpecialPostsController + class OldArticlesController < SpecialArticlesController layout false def show - @post = Post.find(params[:id]) + @article = Article.find(params[:id]) end def index - @old_posts = Post.older + @old_articles = Article.older render layout: "old" end # ... @@ -542,10 +542,10 @@ Layout declarations cascade downward in the hierarchy, and more specific layout In this application: * In general, views will be rendered in the `main` layout -* `PostsController#index` will use the `main` layout -* `SpecialPostsController#index` will use the `special` layout -* `OldPostsController#show` will use no layout at all -* `OldPostsController#index` will use the `old` layout +* `ArticlesController#index` will use the `main` layout +* `SpecialArticlesController#index` will use the `special` layout +* `OldArticlesController#show` will use no layout at all +* `OldArticlesController#index` will use the `old` layout #### Avoiding Double Render Errors diff --git a/guides/source/maintenance_policy.md b/guides/source/maintenance_policy.md index 8f119f36aa..6f8584b3b7 100644 --- a/guides/source/maintenance_policy.md +++ b/guides/source/maintenance_policy.md @@ -3,10 +3,29 @@ 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 +follows, all versions in `X.Y.Z` format. -------------------------------------------------------------------------------- +Rails follows a shifted version of [semver](http://semver.org/): + +**Patch `Z`** + +Only bug fixes, no API changes, no new features. +Except as necessary for security fixes. + +**Minor `Y`** + +New features, may contain API changes (Serve as major versions of Semver). +Breaking changes are paired with deprecation notices in the previous minor +or major release. + +**Major `X`** + +New features, will likely contain API changes. The difference between Rails' +minor and major releases is the magnitude of breaking changes, and usually +reserved for special occasions. + New Features ------------ @@ -20,7 +39,7 @@ 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.1.z, 4.0.z +**Currently included series:** `4.1.Z`, `4.0.Z`. Security Issues --------------- @@ -35,7 +54,7 @@ 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.1.z, 4.0.z +**Currently included series:** `4.1.Z`, `4.0.Z`. Severe Security Issues ---------------------- @@ -44,7 +63,7 @@ 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.1.z, 4.0.z, 3.2.z +**Currently included series:** `4.1.Z`, `4.0.Z`, `3.2.Z`. Unsupported Release Series -------------------------- diff --git a/guides/source/plugins.md b/guides/source/plugins.md index fe4215839f..a35648d341 100644 --- a/guides/source/plugins.md +++ b/guides/source/plugins.md @@ -39,13 +39,13 @@ to run integration tests using a dummy Rails application. Create your plugin with the command: ```bash -$ rails plugin new yaffle +$ bin/rails plugin new yaffle ``` See usage and options by asking for help: ```bash -$ rails plugin --help +$ bin/rails plugin --help ``` Testing Your Newly Generated Plugin @@ -124,7 +124,7 @@ To test that your method does what it says it does, run the unit tests with `rak To see this in action, change to the test/dummy directory, fire up a console and start squawking: ```bash -$ rails console +$ bin/rails console >> "Hello World".to_squawk => "squawk! Hello World" ``` @@ -214,8 +214,8 @@ test/dummy directory: ```bash $ cd test/dummy -$ rails generate model Hickwall last_squawk:string -$ rails generate model Wickwall last_squawk:string last_tweet:string +$ bin/rails generate model Hickwall last_squawk:string +$ bin/rails generate model Wickwall last_squawk:string last_tweet:string ``` Now you can create the necessary database tables in your testing database by navigating to your dummy app @@ -223,7 +223,7 @@ and migrating the database. First, run: ```bash $ cd test/dummy -$ rake db:migrate +$ bin/rake db:migrate ``` While you are here, change the Hickwall and Wickwall models so that they know that they are supposed to act @@ -433,7 +433,7 @@ Once your README is solid, go through and add rdoc comments to all of the method Once your comments are good to go, navigate to your plugin directory and run: ```bash -$ rake rdoc +$ bin/rake rdoc ``` ### References diff --git a/guides/source/rails_application_templates.md b/guides/source/rails_application_templates.md index e4222e1283..0bd608c007 100644 --- a/guides/source/rails_application_templates.md +++ b/guides/source/rails_application_templates.md @@ -23,8 +23,8 @@ $ rails new blog -m http://example.com/template.rb You can use the rake task `rails:template` to apply templates to an existing Rails application. The location of the template needs to be passed in to an environment variable named LOCATION. Again, this can either be path to a file or a URL. ```bash -$ rake rails:template LOCATION=~/template.rb -$ rake rails:template LOCATION=http://example.com/template.rb +$ bin/rake rails:template LOCATION=~/template.rb +$ bin/rake rails:template LOCATION=http://example.com/template.rb ``` Template API diff --git a/guides/source/rails_on_rack.md b/guides/source/rails_on_rack.md index b1b4c8fa4e..01941fa338 100644 --- a/guides/source/rails_on_rack.md +++ b/guides/source/rails_on_rack.md @@ -18,7 +18,7 @@ Introduction to Rack Rack provides a minimal, modular and adaptable interface for developing web applications in Ruby. By wrapping HTTP requests and responses in the simplest way possible, it unifies and distills the API for web servers, web frameworks, and software in between (the so-called middleware) into a single method call. -- [Rack API Documentation](http://rack.rubyforge.org/doc/) +* [Rack API Documentation](http://rack.github.io/) Explaining Rack is not really in the scope of this guide. In case you are not familiar with Rack's basics, you should check out the [Resources](#resources) section below. @@ -111,7 +111,7 @@ NOTE: `ActionDispatch::MiddlewareStack` is Rails equivalent of `Rack::Builder`, Rails has a handy rake task for inspecting the middleware stack in use: ```bash -$ rake middleware +$ bin/rake middleware ``` For a freshly generated Rails application, this might produce something like: @@ -194,7 +194,7 @@ And now if you inspect the middleware stack, you'll find that `Rack::Lock` is not a part of it. ```bash -$ rake middleware +$ bin/rake middleware (in /Users/lifo/Rails/blog) use ActionDispatch::Static use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x00000001c304c8> diff --git a/guides/source/routing.md b/guides/source/routing.md index 0783bce442..7a7334f25b 100644 --- a/guides/source/routing.md +++ b/guides/source/routing.md @@ -183,61 +183,61 @@ You may wish to organize groups of controllers under a namespace. Most commonly, ```ruby namespace :admin do - resources :posts, :comments + resources :articles, :comments end ``` -This will create a number of routes for each of the `posts` and `comments` controller. For `Admin::PostsController`, Rails will create: +This will create a number of routes for each of the `articles` and `comments` controller. For `Admin::ArticlesController`, Rails will create: -| 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) | +| HTTP Verb | Path | Controller#Action | Named Helper | +| --------- | ------------------------ | ---------------------- | ---------------------------- | +| GET | /admin/articles | admin/articles#index | admin_articles_path | +| GET | /admin/articles/new | admin/articles#new | new_admin_article_path | +| POST | /admin/articles | admin/articles#create | admin_articles_path | +| GET | /admin/articles/:id | admin/articles#show | admin_article_path(:id) | +| GET | /admin/articles/:id/edit | admin/articles#edit | edit_admin_article_path(:id) | +| PATCH/PUT | /admin/articles/:id | admin/articles#update | admin_article_path(:id) | +| DELETE | /admin/articles/:id | admin/articles#destroy | admin_article_path(:id) | -If you want to route `/posts` (without the prefix `/admin`) to `Admin::PostsController`, you could use: +If you want to route `/articles` (without the prefix `/admin`) to `Admin::ArticlesController`, you could use: ```ruby scope module: 'admin' do - resources :posts, :comments + resources :articles, :comments end ``` or, for a single case: ```ruby -resources :posts, module: 'admin' +resources :articles, module: 'admin' ``` -If you want to route `/admin/posts` to `PostsController` (without the `Admin::` module prefix), you could use: +If you want to route `/admin/articles` to `ArticlesController` (without the `Admin::` module prefix), you could use: ```ruby scope '/admin' do - resources :posts, :comments + resources :articles, :comments end ``` or, for a single case: ```ruby -resources :posts, path: '/admin/posts' +resources :articles, path: '/admin/articles' ``` 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 | 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) | +| HTTP Verb | Path | Controller#Action | Named Helper | +| --------- | ------------------------ | -------------------- | ---------------------- | +| GET | /admin/articles | articles#index | articles_path | +| GET | /admin/articles/new | articles#new | new_article_path | +| POST | /admin/articles | articles#create | articles_path | +| GET | /admin/articles/:id | articles#show | article_path(:id) | +| GET | /admin/articles/:id/edit | articles#edit | edit_article_path(:id) | +| PATCH/PUT | /admin/articles/:id | articles#update | article_path(:id) | +| DELETE | /admin/articles/:id | articles#destroy | article_path(:id) | TIP: _If you need to use a different controller namespace inside a `namespace` block you can specify an absolute controller path, e.g: `get '/foo' => '/foo#index'`._ @@ -304,7 +304,7 @@ TIP: _Resources should never be nested more than 1 level deep._ One way to avoid deep nesting (as recommended above) is to generate the collection actions scoped under the parent, so as to get a sense of the hierarchy, but to not nest the member actions. In other words, to only build routes with the minimal amount of information to uniquely identify the resource, like this: ```ruby -resources :posts do +resources :articles do resources :comments, only: [:index, :new, :create] end resources :comments, only: [:show, :edit, :update, :destroy] @@ -313,7 +313,7 @@ resources :comments, only: [:show, :edit, :update, :destroy] This idea strikes a balance between descriptive routes and deep nesting. There exists shorthand syntax to achieve just that, via the `:shallow` option: ```ruby -resources :posts do +resources :articles do resources :comments, shallow: true end ``` @@ -321,7 +321,7 @@ end This will generate the exact same routes as the first example. You can also specify the `:shallow` option in the parent resource, in which case all of the nested resources will be shallow: ```ruby -resources :posts, shallow: true do +resources :articles, shallow: true do resources :comments resources :quotes resources :drafts @@ -332,7 +332,7 @@ The `shallow` method of the DSL creates a scope inside of which every nesting is ```ruby shallow do - resources :posts do + resources :articles do resources :comments resources :quotes resources :drafts @@ -344,7 +344,7 @@ There exist two options for `scope` to customize shallow routes. `:shallow_path` ```ruby scope shallow_path: "sekret" do - resources :posts do + resources :articles do resources :comments, shallow: true end end @@ -352,21 +352,21 @@ end The comments resource here will have the following routes generated for it: -| HTTP Verb | Path | Controller#Action | Named Helper | -| --------- | -------------------------------------- | ----------------- | --------------------- | -| GET | /posts/:post_id/comments(.:format) | comments#index | post_comments_path | -| POST | /posts/:post_id/comments(.:format) | comments#create | post_comments_path | -| GET | /posts/:post_id/comments/new(.:format) | comments#new | new_post_comment_path | -| GET | /sekret/comments/:id/edit(.:format) | comments#edit | edit_comment_path | -| GET | /sekret/comments/:id(.:format) | comments#show | comment_path | -| PATCH/PUT | /sekret/comments/:id(.:format) | comments#update | comment_path | -| DELETE | /sekret/comments/:id(.:format) | comments#destroy | comment_path | +| HTTP Verb | Path | Controller#Action | Named Helper | +| --------- | -------------------------------------------- | ----------------- | ------------------------ | +| GET | /articles/:article_id/comments(.:format) | comments#index | article_comments_path | +| POST | /articles/:article_id/comments(.:format) | comments#create | article_comments_path | +| GET | /articles/:article_id/comments/new(.:format) | comments#new | new_article_comment_path | +| GET | /sekret/comments/:id/edit(.:format) | comments#edit | edit_comment_path | +| GET | /sekret/comments/:id(.:format) | comments#show | comment_path | +| PATCH/PUT | /sekret/comments/:id(.:format) | comments#update | comment_path | +| DELETE | /sekret/comments/:id(.:format) | comments#destroy | comment_path | The `:shallow_prefix` option adds the specified parameter to the named helpers: ```ruby scope shallow_prefix: "sekret" do - resources :posts do + resources :articles do resources :comments, shallow: true end end @@ -374,15 +374,15 @@ end The comments resource here will have the following routes generated for it: -| HTTP Verb | Path | Controller#Action | Named Helper | -| --------- | -------------------------------------- | ----------------- | ------------------------ | -| GET | /posts/:post_id/comments(.:format) | comments#index | post_comments_path | -| POST | /posts/:post_id/comments(.:format) | comments#create | post_comments_path | -| GET | /posts/:post_id/comments/new(.:format) | comments#new | new_post_comment_path | -| GET | /comments/:id/edit(.:format) | comments#edit | edit_sekret_comment_path | -| GET | /comments/:id(.:format) | comments#show | sekret_comment_path | -| PATCH/PUT | /comments/:id(.:format) | comments#update | sekret_comment_path | -| DELETE | /comments/:id(.:format) | comments#destroy | sekret_comment_path | +| HTTP Verb | Path | Controller#Action | Named Helper | +| --------- | -------------------------------------------- | ----------------- | --------------------------- | +| GET | /articles/:article_id/comments(.:format) | comments#index | article_comments_path | +| POST | /articles/:article_id/comments(.:format) | comments#create | article_comments_path | +| GET | /articles/:article_id/comments/new(.:format) | comments#new | new_article_comment_path | +| GET | /comments/:id/edit(.:format) | comments#edit | edit_sekret_comment_path | +| GET | /comments/:id(.:format) | comments#show | sekret_comment_path | +| PATCH/PUT | /comments/:id(.:format) | comments#update | sekret_comment_path | +| DELETE | /comments/:id(.:format) | comments#destroy | sekret_comment_path | ### Routing concerns @@ -403,7 +403,7 @@ These concerns can be used in resources to avoid code duplication and share beha ```ruby resources :messages, concerns: :commentable -resources :posts, concerns: [:commentable, :image_attachable] +resources :articles, concerns: [:commentable, :image_attachable] ``` The above is equivalent to: @@ -413,7 +413,7 @@ resources :messages do resources :comments end -resources :posts do +resources :articles do resources :comments resources :images, only: :index end @@ -422,7 +422,7 @@ end Also you can use them in any place that you want inside the routes, for example in a scope or namespace call: ```ruby -namespace :posts do +namespace :articles do concerns :commentable end ``` @@ -662,26 +662,26 @@ get 'photos/:id', to: 'photos#show', id: /[A-Z]\d{5}/ `:constraints` takes regular expressions with the restriction that regexp anchors can't be used. For example, the following route will not work: ```ruby -get '/:id', to: 'posts#show', constraints: {id: /^\d/} +get '/:id', to: 'articles#show', constraints: { id: /^\d/ } ``` However, note that you don't need to use anchors because all routes are anchored at the start. -For example, the following routes would allow for `posts` with `to_param` values like `1-hello-world` that always begin with a number and `users` with `to_param` values like `david` that never begin with a number to share the root namespace: +For example, the following routes would allow for `articles` with `to_param` values like `1-hello-world` that always begin with a number and `users` with `to_param` values like `david` that never begin with a number to share the root namespace: ```ruby -get '/:id', to: 'posts#show', constraints: { id: /\d.+/ } +get '/:id', to: 'articles#show', constraints: { id: /\d.+/ } get '/:username', to: 'users#show' ``` ### Request-Based Constraints -You can also constrain a route based on any method on the <a href="action_controller_overview.html#the-request-object">Request</a> object that returns a `String`. +You can also constrain a route based on any method on the [Request object](action_controller_overview.html#the-request-object) that returns a `String`. You specify a request-based constraint the same way that you specify a segment constraint: ```ruby -get 'photos', constraints: {subdomain: 'admin'} +get 'photos', to: 'photos#index', constraints: { subdomain: 'admin' } ``` You can also specify constraints in a block form: @@ -694,7 +694,7 @@ namespace :admin do end ``` -NOTE: Request constraints work by calling a method on the <a href="action_controller_overview.html#the-request-object">Request object</a> with the same name as the hash key and then compare the return value with the hash value. Therefore, constraint values should match the corresponding Request object method return type. For example: `constraints: { subdomain: 'api' }` will match an `api` subdomain as expected, however using a symbol `constraints: { subdomain: :api }` will not, because `request.subdomain` returns `'api'` as a String. +NOTE: Request constraints work by calling a method on the [Request object](action_controller_overview.html#the-request-object) with the same name as the hash key and then compare the return value with the hash value. Therefore, constraint values should match the corresponding Request object method return type. For example: `constraints: { subdomain: 'api' }` will match an `api` subdomain as expected, however using a symbol `constraints: { subdomain: :api }` will not, because `request.subdomain` returns `'api'` as a String. ### Advanced Constraints @@ -771,20 +771,20 @@ get '*pages', to: 'pages#show', format: true You can redirect any path to another path using the `redirect` helper in your router: ```ruby -get '/stories', to: redirect('/posts') +get '/stories', to: redirect('/articles') ``` You can also reuse dynamic segments from the match in the path to redirect to: ```ruby -get '/stories/:name', to: redirect('/posts/%{name}') +get '/stories/:name', to: redirect('/articles/%{name}') ``` You can also provide a block to redirect, which receives the symbolized path parameters and the request object: ```ruby -get '/stories/:name', to: redirect {|path_params, req| "/posts/#{path_params[:name].pluralize}" } -get '/stories', to: redirect {|path_params, req| "/posts/#{req.subdomain}" } +get '/stories/:name', to: redirect { |path_params, req| "/articles/#{path_params[:name].pluralize}" } +get '/stories', to: redirect { |path_params, req| "/articles/#{req.subdomain}" } ``` Please note that this redirection is a 301 "Moved Permanently" redirect. Keep in mind that some web browsers or proxy servers will cache this type of redirect, making the old page inaccessible. @@ -793,7 +793,7 @@ In all of these cases, if you don't provide the leading host (`http://www.exampl ### Routing to Rack Applications -Instead of a String like `'posts#index'`, which corresponds to the `index` action in the `PostsController`, you can specify any <a href="rails_on_rack.html">Rack application</a> as the endpoint for a matcher: +Instead of a String like `'articles#index'`, which corresponds to the `index` action in the `ArticlesController`, you can specify any [Rack application](rails_on_rack.html) as the endpoint for a matcher: ```ruby match '/application.js', to: Sprockets, via: :all @@ -801,7 +801,7 @@ match '/application.js', to: Sprockets, via: :all As long as `Sprockets` responds to `call` and returns a `[status, headers, body]`, the router won't know the difference between the Rack application and an action. This is an appropriate use of `via: :all`, as you will want to allow your Rack application to handle all verbs as it considers appropriate. -NOTE: For the curious, `'posts#index'` actually expands out to `PostsController.action(:index)`, which returns a valid Rack application. +NOTE: For the curious, `'articles#index'` actually expands out to `ArticlesController.action(:index)`, which returns a valid Rack application. ### Using `root` @@ -837,7 +837,7 @@ get 'こんにちは', to: 'welcome#index' Customizing Resourceful Routes ------------------------------ -While the default routes and helpers generated by `resources :posts` will usually serve you well, you may want to customize them in some way. Rails allows you to customize virtually any generic part of the resourceful helpers. +While the default routes and helpers generated by `resources :articles` will usually serve you well, you may want to customize them in some way. Rails allows you to customize virtually any generic part of the resourceful helpers. ### Specifying a Controller to Use @@ -879,7 +879,7 @@ a warning. You can use the `:constraints` option to specify a required format on the implicit `id`. For example: ```ruby -resources :photos, constraints: {id: /[A-Z][A-Z][0-9]+/} +resources :photos, constraints: { id: /[A-Z][A-Z][0-9]+/ } ``` This declaration constrains the `:id` parameter to match the supplied regular expression. So, in this case, the router would no longer match `/photos/1` to this route. Instead, `/photos/RR27` would match. @@ -919,7 +919,7 @@ will recognize incoming paths beginning with `/photos` and route the requests to ### Overriding the `new` and `edit` Segments -The `:path_names` option lets you override the automatically-generated "new" and "edit" segments in paths: +The `:path_names` option lets you override the automatically-generated `new` and `edit` segments in paths: ```ruby resources :photos, path_names: { new: 'make', edit: 'change' } @@ -954,7 +954,7 @@ end resources :photos ``` -This will provide route helpers such as `admin_photos_path`, `new_admin_photo_path` etc. +This will provide route helpers such as `admin_photos_path`, `new_admin_photo_path`, etc. To prefix a group of route helpers, use `:as` with `scope`: @@ -974,15 +974,15 @@ You can prefix routes with a named parameter also: ```ruby scope ':username' do - resources :posts + resources :articles end ``` -This will provide you with URLs such as `/bob/posts/1` and will allow you to reference the `username` part of the path as `params[:username]` in controllers, helpers and views. +This will provide you with URLs such as `/bob/articles/1` and will allow you to reference the `username` part of the path as `params[:username]` in controllers, helpers and views. ### Restricting the Routes Created -By default, Rails creates routes for the seven default actions (index, show, new, create, edit, update, and destroy) for every RESTful route in your application. You can use the `:only` and `:except` options to fine-tune this behavior. The `:only` option tells Rails to create only the specified routes: +By default, Rails creates routes for the seven default actions (`index`, `show`, `new`, `create`, `edit`, `update`, and `destroy`) for every RESTful route in your application. You can use the `:only` and `:except` options to fine-tune this behavior. The `:only` option tells Rails to create only the specified routes: ```ruby resources :photos, only: [:index, :show] @@ -1044,6 +1044,28 @@ end This will create routing helpers such as `magazine_periodical_ads_url` and `edit_magazine_periodical_ad_path`. +### Overriding Named Route Parameters + +The `:param` option overrides the default resource identifier `:id` (name of +the [dynamic segment](routing.html#dynamic-segments) used to generate the +routes). You can access that segment from your controller using +`params[<:param>]`. + +```ruby +resources :videos, param: :identifier +``` + +``` + videos GET /videos(.:format) videos#index + POST /videos(.:format) videos#create + new_videos GET /videos/new(.:format) videos#new +edit_videos GET /videos/:identifier/edit(.:format) videos#edit +``` + +```ruby +Video.find_by(identifier: params[:identifier]) +``` + Inspecting and Testing Routes ----------------------------- @@ -1072,7 +1094,7 @@ edit_user GET /users/:id/edit(.:format) users#edit You may restrict the listing to the routes that map to a particular controller setting the `CONTROLLER` environment variable: ```bash -$ CONTROLLER=users rake routes +$ CONTROLLER=users bin/rake routes ``` TIP: You'll find that the output from `rake routes` is much more readable if you widen your terminal window until the output lines don't wrap. diff --git a/guides/source/ruby_on_rails_guides_guidelines.md b/guides/source/ruby_on_rails_guides_guidelines.md index 8faf03e58c..f0230b428b 100644 --- a/guides/source/ruby_on_rails_guides_guidelines.md +++ b/guides/source/ruby_on_rails_guides_guidelines.md @@ -13,7 +13,7 @@ After reading this guide, you will know: Markdown ------- -Guides are written in [GitHub Flavored Markdown](http://github.github.com/github-flavored-markdown/). There is comprehensive [documentation for Markdown](http://daringfireball.net/projects/markdown/syntax), a [cheatsheet](http://daringfireball.net/projects/markdown/basics), and [additional documentation](http://github.github.com/github-flavored-markdown/) on the differences from traditional Markdown. +Guides are written in [GitHub Flavored Markdown](https://help.github.com/articles/github-flavored-markdown). There is comprehensive [documentation for Markdown](http://daringfireball.net/projects/markdown/syntax), a [cheatsheet](http://daringfireball.net/projects/markdown/basics). Prologue -------- diff --git a/guides/source/security.md b/guides/source/security.md index 0d347c9e4b..ebfcc5bdd0 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 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. +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. 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). @@ -25,7 +25,7 @@ The Gartner Group however estimates that 75% of attacks are at the web applicati The threats against web applications include user account hijacking, bypass of access control, reading or modifying sensitive data, or presenting fraudulent content. Or an attacker might be able to install a Trojan horse program or unsolicited e-mail sending software, aim at financial enrichment or cause brand name damage by modifying company resources. In order to prevent attacks, minimize their impact and remove points of attack, first of all, you have to fully understand the attack methods in order to find the correct countermeasures. That is what this guide aims at. -In order to develop secure web applications you have to keep up to date on all layers and know your enemies. To keep up to date subscribe to security mailing lists, read security blogs and make updating and security checks a habit (check the <a href="#additional-resources">Additional Resources</a> chapter). I do it manually because that's how you find the nasty logical security problems. +In order to develop secure web applications you have to keep up to date on all layers and know your enemies. To keep up to date subscribe to security mailing lists, read security blogs and make updating and security checks a habit (check the [Additional Resources](#additional-resources) chapter). It is done manually because that's how you find the nasty logical security problems. Sessions -------- @@ -68,7 +68,7 @@ Hence, the cookie serves as temporary authentication for the web application. An * Most people don't clear out the cookies after working at a public terminal. So if the last user didn't log out of a web application, you would be able to use it as this user. Provide the user with a _log-out button_ in the web application, and _make it prominent_. -* Many cross-site scripting (XSS) exploits aim at obtaining the user's cookie. You'll read <a href="#cross-site-scripting-xss">more about XSS</a> later. +* Many cross-site scripting (XSS) exploits aim at obtaining the user's cookie. You'll read [more about XSS](#cross-site-scripting-xss) later. * Instead of stealing a cookie unknown to the attacker, they fix a user's session identifier (in the cookie) known to them. Read more about this so-called session fixation later. @@ -135,8 +135,8 @@ NOTE: _Apart from stealing a user's session id, the attacker may fix a session i This attack focuses on fixing a user's session id known to the attacker, and forcing the user's browser into using this id. It is therefore not necessary for the attacker to steal the session id afterwards. Here is how this attack works: * The attacker creates a valid session id: They load the login page of the web application where they want to fix the session, and take the session id in the cookie from the response (see number 1 and 2 in the image). -* They possibly maintains the session. Expiring sessions, for example every 20 minutes, greatly reduces the time-frame for attack. Therefore they access the web application from time to time in order to keep the session alive. -* Now the attacker will force the user's browser into using this session id (see number 3 in the image). As you may not change a cookie of another domain (because of the same origin policy), the attacker has to run a JavaScript from the domain of the target web application. Injecting the JavaScript code into the application by XSS accomplishes this attack. Here is an example: `<script>document.cookie="_session_id=16d5b78abb28e3d6206b60f22a03c8d9";</script>`. Read more about XSS and injection later on. +* They maintain the session by accessing the web application periodically in order to keep an expiring session alive. +* The attacker forces the user's browser into using this session id (see number 3 in the image). As you may not change a cookie of another domain (because of the same origin policy), the attacker has to run a JavaScript from the domain of the target web application. Injecting the JavaScript code into the application by XSS accomplishes this attack. Here is an example: `<script>document.cookie="_session_id=16d5b78abb28e3d6206b60f22a03c8d9";</script>`. Read more about XSS and injection later on. * The attacker lures the victim to the infected page with the JavaScript code. By viewing the page, the victim's browser will change the session id to the trap session id. * As the new trap session is unused, the web application will require the user to authenticate. * From now on, the victim and the attacker will co-use the web application with the same session: The session became valid and the victim didn't notice the attack. @@ -187,7 +187,7 @@ This attack method works by including malicious code or a link in a page that ac  -In the <a href="#sessions">session chapter</a> you have learned that most Rails applications use cookie-based sessions. Either they store the session id in the cookie and have a server-side session hash, or the entire session hash is on the client-side. In either case the browser will automatically send along the cookie on every request to a domain, if it can find a cookie for that domain. The controversial point is, that it will also send the cookie, if the request comes from a site of a different domain. Let's start with an example: +In the [session chapter](#sessions) you have learned that most Rails applications use cookie-based sessions. Either they store the session id in the cookie and have a server-side session hash, or the entire session hash is on the client-side. In either case the browser will automatically send along the cookie on every request to a domain, if it can find a cookie for that domain. The controversial point is, that it will also send the cookie, if the request comes from a site of a different domain. Let's start with an example: * Bob browses a message board and views a post from a hacker where there is a crafted HTML image element. The element references a command in Bob's project management application, rather than an image file. * `<img src="http://www.webapp.com/project/1/destroy">` @@ -198,7 +198,7 @@ In the <a href="#sessions">session chapter</a> you have learned that most Rails It is important to notice that the actual crafted image or link doesn't necessarily have to be situated in the web application's domain, it can be anywhere - in a forum, blog post or email. -CSRF appears very rarely in CVE (Common Vulnerabilities and Exposures) - less than 0.1% in 2006 - but it really is a 'sleeping giant' [Grossman]. This is in stark contrast to the results in my (and others) security contract work - _CSRF is an important security issue_. +CSRF appears very rarely in CVE (Common Vulnerabilities and Exposures) - less than 0.1% in 2006 - but it really is a 'sleeping giant' [Grossman]. This is in stark contrast to the results in many security contract works - _CSRF is an important security issue_. ### CSRF Countermeasures @@ -257,7 +257,7 @@ end The above method can be placed in the `ApplicationController` and will be called when a CSRF token is not present or is incorrect on a non-GET request. -Note that _cross-site scripting (XSS) vulnerabilities bypass all CSRF protections_. XSS gives the attacker access to all elements on a page, so they can read the CSRF security token from a form or directly submit the form. Read <a href="#cross-site-scripting-xss">more about XSS</a> later. +Note that _cross-site scripting (XSS) vulnerabilities bypass all CSRF protections_. XSS gives the attacker access to all elements on a page, so they can read the CSRF security token from a form or directly submit the form. Read [more about XSS](#cross-site-scripting-xss) later. Redirection and Files --------------------- @@ -374,7 +374,7 @@ For _countermeasures against CSRF in administration interfaces and Intranet appl The common admin interface works like this: it's located at www.example.com/admin, may be accessed only if the admin flag is set in the User model, re-displays user input and allows the admin to delete/add/edit whatever data desired. Here are some thoughts about this: -* It is very important to _think about the worst case_: What if someone really got hold of my cookie or user credentials. You could _introduce roles_ for the admin interface to limit the possibilities of the attacker. Or how about _special login credentials_ for the admin interface, other than the ones used for the public part of the application. Or a _special password for very serious actions_? +* It is very important to _think about the worst case_: What if someone really got hold of your cookies or user credentials. You could _introduce roles_ for the admin interface to limit the possibilities of the attacker. Or how about _special login credentials_ for the admin interface, other than the ones used for the public part of the application. Or a _special password for very serious actions_? * Does the admin really have to access the interface from everywhere in the world? Think about _limiting the login to a bunch of source IP addresses_. Examine request.remote_ip to find out about the user's IP address. This is not bullet-proof, but a great barrier. Remember that there might be a proxy in use, though. @@ -406,7 +406,7 @@ If the parameter was nil, the resulting SQL query will be SELECT * FROM users WHERE (users.activation_code IS NULL) LIMIT 1 ``` -And thus it found the first user in the database, returned it and logged them in. You can find out more about it in [my blog post](http://www.rorsecurity.info/2007/10/28/restful_authentication-login-security/). _It is advisable to update your plug-ins from time to time_. Moreover, you can review your application to find more flaws like this. +And thus it found the first user in the database, returned it and logged them in. You can find out more about it in [this blog post](http://www.rorsecurity.info/2007/10/28/restful_authentication-login-security/). _It is advisable to update your plug-ins from time to time_. Moreover, you can review your application to find more flaws like this. ### Brute-Forcing Accounts @@ -477,7 +477,7 @@ config.filter_parameters << :password INFO: _Do you find it hard to remember all your passwords? Don't write them down, but use the initial letters of each word in an easy to remember sentence._ -Bruce Schneier, a security technologist, [has analyzed](http://www.schneier.com/blog/archives/2006/12/realworld_passw.html) 34,000 real-world user names and passwords from the MySpace phishing attack mentioned <a href="#examples-from-the-underground">below</a>. It turns out that most of the passwords are quite easy to crack. The 20 most common passwords are: +Bruce Schneier, a security technologist, [has analyzed](http://www.schneier.com/blog/archives/2006/12/realworld_passw.html) 34,000 real-world user names and passwords from the MySpace phishing attack mentioned [below](#examples-from-the-underground). It turns out that most of the passwords are quite easy to crack. The 20 most common passwords are: password1, abc123, myspace1, password, blink182, qwerty1, ****you, 123abc, baseball1, football1, 123456, soccer, monkey1, liverpool1, princess1, jordan23, slipknot1, superman1, iloveyou1, and monkey. @@ -630,7 +630,7 @@ Also, the second query renames some columns with the AS statement so that the we #### Countermeasures -Ruby on Rails has a built-in filter for special SQL characters, which will escape ' , " , NULL character and line breaks. <em class="highlight">Using `Model.find(id)` or `Model.find_by_some thing(something)` automatically applies this countermeasure</em>. But in SQL fragments, especially <em class="highlight">in conditions fragments (`where("...")`), the `connection.execute()` or `Model.find_by_sql()` methods, it has to be applied manually</em>. +Ruby on Rails has a built-in filter for special SQL characters, which will escape ' , " , NULL character and line breaks. *Using `Model.find(id)` or `Model.find_by_some thing(something)` automatically applies this countermeasure*. But in SQL fragments, especially *in conditions fragments (`where("...")`), the `connection.execute()` or `Model.find_by_sql()` methods, it has to be applied manually*. Instead of passing a string to the conditions option, you can pass an array to sanitize tainted strings like this: @@ -732,7 +732,7 @@ Imagine a blacklist deletes "script" from the user input. Now the attacker injec strip_tags("some<<b>script>alert('hello')<</b>/script>") ``` -This returned "some<script>alert('hello')</script>", which makes an attack work. That's why I vote for a whitelist approach, using the updated Rails 2 method sanitize(): +This returned "some<script>alert('hello')</script>", which makes an attack work. That's why a whitelist approach is better, using the updated Rails 2 method sanitize(): ```ruby tags = %w(a acronym b strong i em li ul ol h1 h2 h3 h4 h5 h6 blockquote br cite sub sup ins p) @@ -741,7 +741,7 @@ s = sanitize(user_input, tags: tags, attributes: %w(href title)) This allows only the given tags and does a good job, even against all kinds of tricks and malformed tags. -As a second step, _it is good practice to escape all output of the application_, especially when re-displaying user input, which hasn't been input-filtered (as in the search form example earlier on). _Use `escapeHTML()` (or its alias `h()`) method_ to replace the HTML input characters &, ", <, > by their uninterpreted representations in HTML (`&`, `"`, `<`;, and `>`). However, it can easily happen that the programmer forgets to use it, so _it is recommended to use the [SafeErb](http://safe-erb.rubyforge.org/svn/plugins/safe_erb/) plugin_. SafeErb reminds you to escape strings from external sources. +As a second step, _it is good practice to escape all output of the application_, especially when re-displaying user input, which hasn't been input-filtered (as in the search form example earlier on). _Use `escapeHTML()` (or its alias `h()`) method_ to replace the HTML input characters &, ", <, > by their uninterpreted representations in HTML (`&`, `"`, `<`;, and `>`). However, it can easily happen that the programmer forgets to use it, so _it is recommended to use the SafeErb gem. SafeErb reminds you to escape strings from external sources. ##### Obfuscation and Encoding Injection @@ -812,7 +812,7 @@ The [moz-binding](http://www.securiteam.com/securitynews/5LP051FHPE.html) CSS pr #### Countermeasures -This example, again, showed that a blacklist filter is never complete. However, as custom CSS in web applications is a quite rare feature, I am not aware of a whitelist CSS filter. _If you want to allow custom colors or images, you can allow the user to choose them and build the CSS in the web application_. Use Rails' `sanitize()` method as a model for a whitelist CSS filter, if you really need one. +This example, again, showed that a blacklist filter is never complete. However, as custom CSS in web applications is a quite rare feature, it may be hard to find a good whitelist CSS filter. _If you want to allow custom colors or images, you can allow the user to choose them and build the CSS in the web application_. Use Rails' `sanitize()` method as a model for a whitelist CSS filter, if you really need one. ### Textile Injection diff --git a/guides/source/testing.md b/guides/source/testing.md index 36d37f3af0..b2da25b19f 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -49,7 +49,9 @@ The `test_helper.rb` file holds the default configuration for your tests. ### The Low-Down on Fixtures -For good tests, you'll need to give some thought to setting up test data. In Rails, you can handle this by defining and customizing fixtures. +For good tests, you'll need to give some thought to setting up test data. +In Rails, you can handle this by defining and customizing fixtures. +You can find comprehensive documentation in the [fixture api documentation](http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html). #### What Are Fixtures? @@ -94,6 +96,12 @@ one: category: about ``` +Note: For associations to reference one another by name, you cannot specify the `id:` + attribute on the fixtures. Rails will auto assign a primary key to be consistent between + runs. If you manually specify an `id:` attribute, this behavior will not work. For more + information on this assocation behavior please read the + [fixture api documentation](http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html). + #### ERB'in It Up ERB allows you to embed Ruby code within templates. The YAML fixture format is pre-processed with ERB when Rails loads fixtures. This allows you to use Ruby to help you generate some sample data. For example, the following code generates a thousand users: @@ -134,27 +142,27 @@ Unit Testing your Models In Rails, models tests are what you write to test your models. -For this guide we will be using Rails _scaffolding_. It will create the model, a migration, controller and views for the new resource in a single operation. It will also create a full test suite following Rails best practices. I will be using examples from this generated code and will be supplementing it with additional examples where necessary. +For this guide we will be using Rails _scaffolding_. It will create the model, a migration, controller and views for the new resource in a single operation. It will also create a full test suite following Rails best practices. We will be using examples from this generated code and will be supplementing it with additional examples where necessary. -NOTE: For more information on Rails <i>scaffolding</i>, refer to [Getting Started with Rails](getting_started.html) +NOTE: For more information on Rails _scaffolding_, refer to [Getting Started with Rails](getting_started.html) When you use `rails generate scaffold`, for a resource among other things it creates a test stub in the `test/models` folder: ```bash -$ rails generate scaffold post title:string body:text +$ bin/rails generate scaffold article title:string body:text ... -create app/models/post.rb -create test/models/post_test.rb -create test/fixtures/posts.yml +create app/models/article.rb +create test/models/article_test.rb +create test/fixtures/articles.yml ... ``` -The default test stub in `test/models/post_test.rb` looks like this: +The default test stub in `test/models/article_test.rb` looks like this: ```ruby require 'test_helper' -class PostTest < ActiveSupport::TestCase +class ArticleTest < ActiveSupport::TestCase # test "the truth" do # assert true # end @@ -170,15 +178,15 @@ require 'test_helper' As you know by now, `test_helper.rb` specifies the default configuration to run our tests. This is included with all the tests, so any methods added to this file are available to all your tests. ```ruby -class PostTest < ActiveSupport::TestCase +class ArticleTest < ActiveSupport::TestCase ``` -The `PostTest` class defines a _test case_ because it inherits from `ActiveSupport::TestCase`. `PostTest` thus has all the methods available from `ActiveSupport::TestCase`. You'll see those methods a little later in this guide. +The `ArticleTest` class defines a _test case_ because it inherits from `ActiveSupport::TestCase`. `ArticleTest` thus has all the methods available from `ActiveSupport::TestCase`. You'll see those methods a little later in this guide. -Any method defined within a class inherited from `MiniTest::Unit::TestCase` -(which is the superclass of `ActiveSupport::TestCase`) that begins with `test` (case sensitive) is simply called a test. So, `test_password`, `test_valid_password` and `testValidPassword` all are legal test names and are run automatically when the test case is run. +Any method defined within a class inherited from `Minitest::Test` +(which is the superclass of `ActiveSupport::TestCase`) that begins with `test_` (case sensitive) is simply called a test. So, `test_password` and `test_valid_password` are legal test names and are run automatically when the test case is run. -Rails adds a `test` method that takes a test name and a block. It generates a normal `MiniTest::Unit` test with method names prefixed with `test_`. So, +Rails adds a `test` method that takes a test name and a block. It generates a normal `Minitest::Unit` test with method names prefixed with `test_`. So, ```ruby test "the truth" do @@ -220,7 +228,7 @@ In order to run your tests, your test database will need to have the current str Running a test is as simple as invoking the file containing the test cases through `rake test` command. ```bash -$ rake test test/models/post_test.rb +$ bin/rake test test/models/article_test.rb . Finished tests in 0.009262s, 107.9680 tests/s, 107.9680 assertions/s. @@ -231,7 +239,7 @@ Finished tests in 0.009262s, 107.9680 tests/s, 107.9680 assertions/s. You can also run a particular test method from the test case by running the test and providing the `test method name`. ```bash -$ rake test test/models/post_test.rb test_the_truth +$ bin/rake test test/models/article_test.rb test_the_truth . Finished tests in 0.009064s, 110.3266 tests/s, 110.3266 assertions/s. @@ -243,25 +251,25 @@ This will run all test methods from the test case. Note that `test_helper.rb` is The `.` (dot) above indicates a passing test. When a test fails you see an `F`; when a test throws an error you see an `E` in its place. The last line of the output is the summary. -To see how a test failure is reported, you can add a failing test to the `post_test.rb` test case. +To see how a test failure is reported, you can add a failing test to the `article_test.rb` test case. ```ruby -test "should not save post without title" do - post = Post.new - assert_not post.save +test "should not save article without title" do + article = Article.new + assert_not article.save end ``` Let us run this newly added test. ```bash -$ rake test test/models/post_test.rb test_should_not_save_post_without_title +$ bin/rake test test/models/article_test.rb test_should_not_save_article_without_title F Finished tests in 0.044632s, 22.4054 tests/s, 22.4054 assertions/s. 1) Failure: -test_should_not_save_post_without_title(PostTest) [test/models/post_test.rb:6]: +test_should_not_save_article_without_title(ArticleTest) [test/models/article_test.rb:6]: Failed assertion, no message given. 1 tests, 1 assertions, 1 failures, 0 errors, 0 skips @@ -270,9 +278,9 @@ Failed assertion, no message given. In the output, `F` denotes a failure. You can see the corresponding trace shown under `1)` along with the name of the failing test. The next few lines contain the stack trace followed by a message which mentions the actual value and the expected value by the assertion. The default assertion messages provide just enough information to help pinpoint the error. To make the assertion failure message more readable, every assertion provides an optional message parameter, as shown here: ```ruby -test "should not save post without title" do - post = Post.new - assert_not post.save, "Saved the post without a title" +test "should not save article without title" do + article = Article.new + assert_not article.save, "Saved the article without a title" end ``` @@ -280,14 +288,14 @@ Running this test shows the friendlier assertion message: ```bash 1) Failure: -test_should_not_save_post_without_title(PostTest) [test/models/post_test.rb:6]: -Saved the post without a title +test_should_not_save_article_without_title(ArticleTest) [test/models/article_test.rb:6]: +Saved the article without a title ``` Now to get this test to pass we can add a model level validation for the _title_ field. ```ruby -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base validates :title, presence: true end ``` @@ -295,7 +303,7 @@ end Now the test should pass. Let us verify by running the test again: ```bash -$ rake test test/models/post_test.rb test_should_not_save_post_without_title +$ bin/rake test test/models/article_test.rb test_should_not_save_article_without_title . Finished tests in 0.047721s, 20.9551 tests/s, 20.9551 assertions/s. @@ -320,15 +328,15 @@ end Now you can see even more output in the console from running the tests: ```bash -$ rake test test/models/post_test.rb test_should_report_error +$ bin/rake test test/models/article_test.rb test_should_report_error E Finished tests in 0.030974s, 32.2851 tests/s, 0.0000 assertions/s. 1) Error: -test_should_report_error(PostTest): -NameError: undefined local variable or method `some_undefined_variable' for #<PostTest:0x007fe32e24afe0> - test/models/post_test.rb:10:in `block in <class:PostTest>' +test_should_report_error(ArticleTest): +NameError: undefined local variable or method `some_undefined_variable' for #<ArticleTest:0x007fe32e24afe0> + test/models/article_test.rb:10:in `block in <class:ArticleTest>' 1 tests, 0 assertions, 0 failures, 1 errors, 0 skips ``` @@ -345,7 +353,7 @@ backtrace. simply set the `BACKTRACE` environment variable to enable this behavior: ```bash -$ BACKTRACE=1 rake test test/models/post_test.rb +$ BACKTRACE=1 bin/rake test test/models/article_test.rb ``` ### What to Include in Your Unit Tests @@ -356,8 +364,13 @@ Ideally, you would like to include a test for everything which could possibly br By now you've caught a glimpse of some of the assertions that are available. Assertions are the worker bees of testing. They are the ones that actually perform the checks to ensure that things are going as planned. -There are a bunch of different types of assertions you can use. -Here's an extract of the assertions you can use with `minitest`, the default testing library used by Rails. The `[msg]` parameter is an optional string message you can specify to make your test failure messages clearer. It's not required. +There are a bunch of different types of assertions you can use. Here's an +extract of the +[assertions](http://docs.seattlerb.org/minitest/Minitest/Assertions.html) you +can use with [minitest](https://github.com/seattlerb/minitest), the default +testing library used by Rails. The `[msg]` parameter is an optional string +message you can specify to make your test failure messages clearer. It's not +required. | Assertion | Purpose | | ---------------------------------------------------------------- | ------- | @@ -369,8 +382,12 @@ Here's an extract of the assertions you can use with `minitest`, the default tes | `assert_not_same( expected, actual, [msg] )` | Ensures that `expected.equal?(actual)` is false.| | `assert_nil( obj, [msg] )` | Ensures that `obj.nil?` is true.| | `assert_not_nil( obj, [msg] )` | Ensures that `obj.nil?` is false.| +| `assert_empty( obj, [msg] )` | Ensures that `obj` is `empty?`.| +| `assert_not_empty( obj, [msg] )` | Ensures that `obj` is not `empty?`.| | `assert_match( regexp, string, [msg] )` | Ensures that a string matches the regular expression.| | `assert_no_match( regexp, string, [msg] )` | Ensures that a string doesn't match the regular expression.| +| `assert_includes( collection, obj, [msg] )` | Ensures that `obj` is in `collection`.| +| `assert_not_includes( collection, obj, [msg] )` | Ensures that `obj` is not in `collection`.| | `assert_in_delta( expecting, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are within `delta` of each other.| | `assert_not_in_delta( expecting, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are not within `delta` of each other.| | `assert_throws( symbol, [msg] ) { block }` | Ensures that the given block throws the symbol.| @@ -384,6 +401,8 @@ Here's an extract of the assertions you can use with `minitest`, the default tes | `assert_not_respond_to( obj, symbol, [msg] )` | Ensures that `obj` does not respond to `symbol`.| | `assert_operator( obj1, operator, [obj2], [msg] )` | Ensures that `obj1.operator(obj2)` is true.| | `assert_not_operator( obj1, operator, [obj2], [msg] )` | Ensures that `obj1.operator(obj2)` is false.| +| `assert_predicate ( obj, predicate, [msg] )` | Ensures that `obj.predicate` is true, e.g. `assert_predicate str, :empty?`| +| `assert_not_predicate ( obj, predicate, [msg] )` | Ensures that `obj.predicate` is false, e.g. `assert_not_predicate str, :empty?`| | `assert_send( array, [msg] )` | Ensures that executing the method listed in `array[1]` on the object in `array[0]` with the parameters of `array[2 and up]` is true. This one is weird eh?| | `flunk( [msg] )` | Ensures failure. This is useful to explicitly mark a test that isn't finished yet.| @@ -393,7 +412,7 @@ NOTE: Creating your own assertions is an advanced topic that we won't cover in t ### Rails Specific Assertions -Rails adds some custom assertions of its own to the `test/unit` framework: +Rails adds some custom assertions of its own to the `minitest` framework: | Assertion | Purpose | | --------------------------------------------------------------------------------- | ------- | @@ -422,26 +441,26 @@ You should test for things such as: * was the correct object stored in the response template? * was the appropriate message displayed to the user in the view? -Now that we have used Rails scaffold generator for our `Post` resource, it has already created the controller code and tests. You can take look at the file `posts_controller_test.rb` in the `test/controllers` directory. +Now that we have used Rails scaffold generator for our `Article` resource, it has already created the controller code and tests. You can take look at the file `articles_controller_test.rb` in the `test/controllers` directory. -Let me take you through one such test, `test_should_get_index` from the file `posts_controller_test.rb`. +Let me take you through one such test, `test_should_get_index` from the file `articles_controller_test.rb`. ```ruby -class PostsControllerTest < ActionController::TestCase +class ArticlesControllerTest < ActionController::TestCase test "should get index" do get :index assert_response :success - assert_not_nil assigns(:posts) + assert_not_nil assigns(:articles) end end ``` -In the `test_should_get_index` test, Rails simulates a request on the action called `index`, making sure the request was successful and also ensuring that it assigns a valid `posts` instance variable. +In the `test_should_get_index` test, Rails simulates a request on the action called `index`, making sure the request was successful and also ensuring that it assigns a valid `articles` instance variable. The `get` method kicks off the web request and populates the results into the response. It accepts 4 arguments: * The action of the controller you are requesting. This can be in the form of a string or a symbol. -* An optional hash of request parameters to pass into the action (eg. query string parameters or post variables). +* An optional hash of request parameters to pass into the action (eg. query string parameters or article variables). * An optional hash of session variables to pass along with the request. * An optional hash of flash values. @@ -457,17 +476,17 @@ Another example: Calling the `:view` action, passing an `id` of 12 as the `param get(:view, {'id' => '12'}, nil, {'message' => 'booya!'}) ``` -NOTE: If you try running `test_should_create_post` test from `posts_controller_test.rb` it will fail on account of the newly added model level validation and rightly so. +NOTE: If you try running `test_should_create_article` test from `articles_controller_test.rb` it will fail on account of the newly added model level validation and rightly so. -Let us modify `test_should_create_post` test in `posts_controller_test.rb` so that all our test pass: +Let us modify `test_should_create_article` test in `articles_controller_test.rb` so that all our test pass: ```ruby -test "should create post" do - assert_difference('Post.count') do - post :create, post: {title: 'Some title'} +test "should create article" do + assert_difference('Article.count') do + post :create, article: {title: 'Some title'} end - assert_redirected_to post_path(assigns(:post)) + assert_redirected_to article_path(assigns(:article)) end ``` @@ -576,12 +595,12 @@ is the correct way to assert for the layout when the view renders a partial with Here's another example that uses `flash`, `assert_redirected_to`, and `assert_difference`: ```ruby -test "should create post" do - assert_difference('Post.count') do - post :create, post: {title: 'Hi', body: 'This is my first post.'} +test "should create article" do + assert_difference('Article.count') do + post :create, article: {title: 'Hi', body: 'This is my first article.'} end - assert_redirected_to post_path(assigns(:post)) - assert_equal 'Post was successfully created.', flash[:notice] + assert_redirected_to article_path(assigns(:article)) + assert_equal 'Article was successfully created.', flash[:notice] end ``` @@ -653,7 +672,7 @@ Integration tests are used to test the interaction among any number of controlle Unlike Unit and Functional tests, integration tests have to be explicitly created under the 'test/integration' folder within your application. Rails provides a generator to create an integration test skeleton for you. ```bash -$ rails generate integration_test user_flows +$ bin/rails generate integration_test user_flows exists test/integration/ create test/integration/user_flows_test.rb ``` @@ -699,8 +718,6 @@ A simple integration test that exercises multiple controllers: require 'test_helper' class UserFlowsTest < ActionDispatch::IntegrationTest - fixtures :users - test "login and browse site" do # login via https https! @@ -712,7 +729,7 @@ class UserFlowsTest < ActionDispatch::IntegrationTest assert_equal 'Welcome david!', flash[:notice] https!(false) - get "/posts/all" + get "/articles/all" assert_response :success assert assigns(:products) end @@ -727,10 +744,7 @@ Here's an example of multiple sessions and custom DSL in an integration test require 'test_helper' class UserFlowsTest < ActionDispatch::IntegrationTest - fixtures :users - test "login and browse site" do - # User david logs in david = login(:david) # User guest logs in @@ -793,57 +807,53 @@ when you initiate a Rails project. | `rake test:all:db` | Runs all tests quickly by merging all types and resetting db | -Brief Note About `MiniTest` +Brief Note About `Minitest` ----------------------------- -Ruby ships with a vast Standard Library for all common use-cases including testing. Ruby 1.8 provided `Test::Unit`, a framework for unit testing in Ruby. All the basic assertions discussed above are actually defined in `Test::Unit::Assertions`. The class `ActiveSupport::TestCase` which we have been using in our unit and functional tests extends `Test::Unit::TestCase`, allowing -us to use all of the basic assertions in our tests. - -Ruby 1.9 introduced `MiniTest`, an updated version of `Test::Unit` which provides a backwards compatible API for `Test::Unit`. You could also use `MiniTest` in Ruby 1.8 by installing the `minitest` gem. +Ruby ships with a vast Standard Library for all common use-cases including testing. Since version 1.9, Ruby provides `Minitest`, a framework for testing. All the basic assertions such as `assert_equal` discussed above are actually defined in `Minitest::Assertions`. The classes `ActiveSupport::TestCase`, `ActionController::TestCase`, `ActionMailer::TestCase`, `ActionView::TestCase` and `ActionDispatch::IntegrationTest` - which we have been inheriting in our test classes - include `Minitest::Assertions`, allowing us to use all of the basic assertions in our tests. -NOTE: For more information on `Test::Unit`, refer to [test/unit Documentation](http://ruby-doc.org/stdlib/libdoc/test/unit/rdoc/) -For more information on `MiniTest`, refer to [Minitest](http://www.ruby-doc.org/stdlib-1.9.3/libdoc/minitest/unit/rdoc/) +NOTE: For more information on `Minitest`, refer to [Minitest](http://ruby-doc.org/stdlib-2.1.0/libdoc/minitest/rdoc/MiniTest.html) Setup and Teardown ------------------ -If you would like to run a block of code before the start of each test and another block of code after the end of each test you have two special callbacks for your rescue. Let's take note of this by looking at an example for our functional test in `Posts` controller: +If you would like to run a block of code before the start of each test and another block of code after the end of each test you have two special callbacks for your rescue. Let's take note of this by looking at an example for our functional test in `Articles` controller: ```ruby require 'test_helper' -class PostsControllerTest < ActionController::TestCase +class ArticlesControllerTest < ActionController::TestCase # called before every single test def setup - @post = posts(:one) + @article = articles(:one) end # called after every single test def teardown - # as we are re-initializing @post before every test + # as we are re-initializing @article before every test # setting it to nil here is not essential but I hope # you understand how you can use the teardown method - @post = nil + @article = nil end - test "should show post" do - get :show, id: @post.id + test "should show article" do + get :show, id: @article.id assert_response :success end - test "should destroy post" do - assert_difference('Post.count', -1) do - delete :destroy, id: @post.id + test "should destroy article" do + assert_difference('Article.count', -1) do + delete :destroy, id: @article.id end - assert_redirected_to posts_path + assert_redirected_to articles_path end end ``` -Above, the `setup` method is called before each test and so `@post` is available for each of the tests. Rails implements `setup` and `teardown` as `ActiveSupport::Callbacks`. Which essentially means you need not only use `setup` and `teardown` as methods in your tests. You could specify them by using: +Above, the `setup` method is called before each test and so `@article` is available for each of the tests. Rails implements `setup` and `teardown` as `ActiveSupport::Callbacks`. Which essentially means you need not only use `setup` and `teardown` as methods in your tests. You could specify them by using: * a block * a method (like in the earlier example) @@ -855,38 +865,38 @@ Let's see the earlier example by specifying `setup` callback by specifying a met ```ruby require 'test_helper' -class PostsControllerTest < ActionController::TestCase +class ArticlesControllerTest < ActionController::TestCase # called before every single test - setup :initialize_post + setup :initialize_article # called after every single test def teardown - @post = nil + @article = nil end - test "should show post" do - get :show, id: @post.id + test "should show article" do + get :show, id: @article.id assert_response :success end - test "should update post" do - patch :update, id: @post.id, post: {} - assert_redirected_to post_path(assigns(:post)) + test "should update article" do + patch :update, id: @article.id, article: {} + assert_redirected_to article_path(assigns(:article)) end - test "should destroy post" do - assert_difference('Post.count', -1) do - delete :destroy, id: @post.id + test "should destroy article" do + assert_difference('Article.count', -1) do + delete :destroy, id: @article.id end - assert_redirected_to posts_path + assert_redirected_to articles_path end private - def initialize_post - @post = posts(:one) + def initialize_article + @article = articles(:one) end end ``` @@ -894,11 +904,11 @@ end Testing Routes -------------- -Like everything else in your Rails application, it is recommended that you test your routes. An example test for a route in the default `show` action of `Posts` controller above should look like: +Like everything else in your Rails application, it is recommended that you test your routes. An example test for a route in the default `show` action of `Articles` controller above should look like: ```ruby -test "should route to post" do - assert_routing '/posts/1', {controller: "posts", action: "show", id: "1"} +test "should route to article" do + assert_routing '/articles/1', {controller: "articles", action: "show", id: "1"} end ``` @@ -997,7 +1007,7 @@ class UserControllerTest < ActionController::TestCase assert_equal "You have been invited by me@example.com", invite_email.subject assert_equal 'friend@example.com', invite_email.to[0] - assert_match(/Hi friend@example.com/, invite_email.body) + assert_match(/Hi friend@example.com/, invite_email.body.to_s) end end ``` @@ -1011,7 +1021,7 @@ located under the `test/helpers` directory. Rails provides a generator which generates both the helper and the test file: ```bash -$ rails generate helper User +$ bin/rails generate helper User create app/helpers/user_helper.rb invoke test_unit create test/helpers/user_helper_test.rb @@ -1046,7 +1056,7 @@ access to Rails' helper methods such as `link_to` or `pluralize`. Other Testing Approaches ------------------------ -The built-in `test/unit` based testing is not the only way to test Rails applications. Rails developers have come up with a wide variety of other approaches and aids for testing, including: +The built-in `minitest` based testing is not the only way to test Rails applications. Rails developers have come up with a wide variety of other approaches and aids for testing, including: * [NullDB](http://avdi.org/projects/nulldb/), a way to speed up testing by avoiding database use. * [Factory Girl](https://github.com/thoughtbot/factory_girl/tree/master), a replacement for fixtures. diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index da161f84c9..b3e4505fc0 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -3,6 +3,8 @@ A Guide for Upgrading Ruby on Rails This guide provides steps to be followed when you upgrade your applications to a newer version of Ruby on Rails. These steps are also available in individual release guides. +-------------------------------------------------------------------------------- + General Advice -------------- @@ -22,6 +24,40 @@ Rails generally stays close to the latest released Ruby version when it's releas TIP: 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 straight to 1.9.3 for smooth sailing. +### The Rake Task + +Rails provides the `rails:update` rake task. After updating the Rails version +in the Gemfile, run this rake task. +This will help you with the creation of new files and changes of old files in a +interactive session. + +```bash +$ rake rails:update + identical config/boot.rb + exist config + conflict config/routes.rb +Overwrite /myapp/config/routes.rb? (enter "h" for help) [Ynaqdh] + force config/routes.rb + conflict config/application.rb +Overwrite /myapp/config/application.rb? (enter "h" for help) [Ynaqdh] + force config/application.rb + conflict config/environment.rb +... +``` + +Don't forget to review the difference, to see if there were any unexpected changes. + +Upgrading from Rails 4.1 to Rails 4.2 +------------------------------------- + +NOTE: This section is a work in progress. + +### Serialized attributes + +When assigning `nil` to a serialized attribute, it will be saved to the database +as `NULL` instead of passing the `nil` value through the coder (e.g. `"null"` +when using the `JSON` coder). + Upgrading from Rails 4.0 to Rails 4.1 ------------------------------------- @@ -82,10 +118,10 @@ secrets, you need to: 2. Use your existing `secret_key_base` from the `secret_token.rb` initializer to set the SECRET_KEY_BASE environment variable for whichever users run the Rails - app in production mode. Alternately, you can simply copy the existing - `secret_key_base` from the `secret_token.rb` initializer to `secrets.yml` + app in production mode. Alternately, you can simply copy the existing + `secret_key_base` from the `secret_token.rb` initializer to `secrets.yml` under the `production` section, replacing '<%= ENV["SECRET_KEY_BASE"] %>'. - + 3. Remove the `secret_token.rb` initializer. 4. Use `rake secret` to generate new keys for the `development` and `test` sections. @@ -140,7 +176,7 @@ If you use the cookie session store, this would apply to the `session` and Flash message keys are [normalized to strings](https://github.com/rails/rails/commit/a668beffd64106a1e1fedb71cc25eaaa11baf0c1). They -can still be accessed using either symbols or strings. Lopping through the flash +can still be accessed using either symbols or strings. Looping through the flash will always yield string keys: ```ruby @@ -210,6 +246,16 @@ If your application depends on one of these features, you can get them back by adding the [`activesupport-json_encoder`](https://github.com/rails/activesupport-json_encoder) gem to your Gemfile. +#### JSON representation of Time objects + +`#as_json` for objects with time component (`Time`, `DateTime`, `ActiveSupport::TimeWithZone`) +now returns millisecond precision by default. If you need to keep old behavior with no millisecond +precision, set the following in an initializer: + +``` +ActiveSupport::JSON::Encoding.time_precision = 0 +``` + ### Usage of `return` within inline callback blocks Previously, Rails allowed inline callback blocks to use `return` this way: @@ -309,10 +355,10 @@ authors.compact! ### Changes on Default Scopes -Default scopes are no longer overriden by chained conditions. +Default scopes are no longer overridden by chained conditions. In previous versions when you defined a `default_scope` in a model -it was overriden by chained conditions in the same field. Now it +it was overridden by chained conditions in the same field. Now it is merged like any other scope. Before: @@ -393,6 +439,28 @@ start using the more precise `:plain:`, `:html`, and `:body` options instead. Using `render :text` may pose a security risk, as the content is sent as `text/html`. +### PostgreSQL json and hstore datatypes + +Rails 4.1 will map `json` and `hstore` columns to a string-keyed Ruby `Hash`. +In earlier versions a `HashWithIndifferentAccess` was used. This means that +symbol access is no longer supported. This is also the case for +`store_accessors` based on top of `json` or `hstore` columns. Make sure to use +string keys consistently. + +### Explicit block use for `ActiveSupport::Callbacks` + +Rails 4.1 now expects an explicit block to be passed when calling +`ActiveSupport::Callbacks.set_callback`. This change stems from +`ActiveSupport::Callbacks` being largely rewritten for the 4.1 release. + +```ruby +# Previously in Rails 4.0 +set_callback :save, :around, ->(r, &block) { stuff; result = block.call; stuff } + +# Now in Rails 4.1 +set_callback :save, :around, ->(r, block) { stuff; result = block.call; stuff } +``` + Upgrading from Rails 3.2 to Rails 4.0 ------------------------------------- @@ -480,7 +548,7 @@ def update respond_to do |format| format.json do # perform a partial update - @post.update params[:post] + @article.update params[:article] end format.json_patch do @@ -712,17 +780,18 @@ config.assets.js_compressor = :uglifier 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. +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.17, -the last 3.2.x version of Rails. +The following changes are meant for upgrading your application to the latest +3.2.x version of Rails. ### Gemfile Make the following changes to your `Gemfile`. ```ruby -gem 'rails', '3.2.17' +gem 'rails', '3.2.18' group :assets do gem 'sass-rails', '~> 3.2.6' @@ -883,7 +952,7 @@ AppName::Application.config.session_store :cookie_store, key: 'SOMETHINGNEW' or ```bash -$ rake db:sessions:clear +$ bin/rake db:sessions:clear ``` ### Remove :cache and :concat options in asset helpers references in views diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md index aba3c9ed61..7c3fd9f69d 100644 --- a/guides/source/working_with_javascript_in_rails.md +++ b/guides/source/working_with_javascript_in_rails.md @@ -158,7 +158,7 @@ is a helper that assists with writing forms. `form_for` takes a `:remote` option. It works like this: ```erb -<%= form_for(@post, remote: true) do |f| %> +<%= form_for(@article, remote: true) do |f| %> ... <% end %> ``` @@ -166,7 +166,7 @@ option. It works like this: This will generate the following HTML: ```html -<form accept-charset="UTF-8" action="/posts" class="new_post" data-remote="true" id="new_post" method="post"> +<form accept-charset="UTF-8" action="/articles" class="new_article" data-remote="true" id="new_article" method="post"> ... </form> ``` @@ -180,10 +180,10 @@ bind to the `ajax:success` event. On failure, use `ajax:error`. Check it out: ```coffeescript $(document).ready -> - $("#new_post").on("ajax:success", (e, data, status, xhr) -> - $("#new_post").append xhr.responseText + $("#new_article").on("ajax:success", (e, data, status, xhr) -> + $("#new_article").append xhr.responseText ).on "ajax:error", (e, xhr, status, error) -> - $("#new_post").append "<p>ERROR</p>" + $("#new_article").append "<p>ERROR</p>" ``` Obviously, you'll want to be a bit more sophisticated than that, but it's a @@ -196,7 +196,7 @@ is very similar to `form_for`. It has a `:remote` option that you can use like this: ```erb -<%= form_tag('/posts', remote: true) do %> +<%= form_tag('/articles', remote: true) do %> ... <% end %> ``` @@ -204,7 +204,7 @@ this: This will generate the following HTML: ```html -<form accept-charset="UTF-8" action="/posts" data-remote="true" method="post"> +<form accept-charset="UTF-8" action="/articles" data-remote="true" method="post"> ... </form> ``` @@ -219,21 +219,21 @@ is a helper that assists with generating links. It has a `:remote` option you can use like this: ```erb -<%= link_to "a post", @post, remote: true %> +<%= link_to "an article", @article, remote: true %> ``` which generates ```html -<a href="/posts/1" data-remote="true">a post</a> +<a href="/articles/1" data-remote="true">an article</a> ``` You can bind to the same Ajax events as `form_for`. Here's an example. Let's -assume that we have a list of posts that can be deleted with just one +assume that we have a list of articles that can be deleted with just one click. We would generate some HTML like this: ```erb -<%= link_to "Delete post", @post, remote: true, method: :delete %> +<%= link_to "Delete article", @article, remote: true, method: :delete %> ``` and write some CoffeeScript like this: @@ -241,7 +241,7 @@ and write some CoffeeScript like this: ```coffeescript $ -> $("a[data-remote]").on "ajax:success", (e, data, status, xhr) -> - alert "The post was deleted." + alert "The article was deleted." ``` ### button_to @@ -249,14 +249,14 @@ $ -> [`button_to`](http://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-button_to) is a helper that helps you create buttons. It has a `:remote` option that you can call like this: ```erb -<%= button_to "A post", @post, remote: true %> +<%= button_to "An article", @article, remote: true %> ``` this generates ```html -<form action="/posts/1" class="button_to" data-remote="true" method="post"> - <div><input type="submit" value="A post"></div> +<form action="/articles/1" class="button_to" data-remote="true" method="post"> + <div><input type="submit" value="An article"></div> </form> ``` diff --git a/guides/w3c_validator.rb b/guides/w3c_validator.rb index 6ef3df45a9..71f044b9c4 100644 --- a/guides/w3c_validator.rb +++ b/guides/w3c_validator.rb @@ -60,6 +60,8 @@ module RailsGuides def guides_to_validate guides = Dir["./output/*.html"] guides.delete("./output/layout.html") + guides.delete("./output/_license.html") + guides.delete("./output/_welcome.html") ENV.key?('ONLY') ? select_only(guides) : guides end diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 6a31a923a7..e9abfac7a0 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,61 @@ +* Add `Rails::Application.config_for` to load a configuration for the current + environment. + + # config/exception_notification.yml: + production: + url: http://127.0.0.1:8080 + namespace: my_app_production + development: + url: http://localhost:3001 + namespace: my_app_development + + # config/production.rb + MyApp::Application.configure do + config.middleware.use ExceptionNotifier, config_for(:exception_notification) + end + + *Rafael Mendonça França*, *DHH* + +* Deprecate `Rails::Rack::LogTailer` without replacement. + + *Rafael Mendonça França* + +* Add a generic --skip-gems options to generator + + This option is useful if users want to remove some gems like jbuilder, + turbolinks, coffee-rails, etc that don't have specific options on the + generator. + + rails new my_app --skip-gems turbolinks coffee-rails + + *Rafael Mendonça França* + +* Invalid `bin/rails generate` commands will now show spelling suggestions. + + *Richard Schneeman* + +* Add `bin/setup` script to bootstrap an application. + + *Yves Senn* + +* Replace double quotes with single quotes while adding an entry into Gemfile. + + *Alexander Belaev* + +* Default `config.assets.digest` to `true` in development. + + *Dan Kang* + +* Load database configuration from the first `database.yml` available in paths. + + *Pier-Olivier Thibault* + +* Reading name and email from git for plugin gemspec. + + Fixes #9589. + + *Arun Agrawal*, *Abd ar-Rahman Hamidi*, *Roman Shmatov* + * Fix `console` and `generators` blocks defined at different environments. Fixes #14748. @@ -8,10 +66,6 @@ *Matthew Draper* -* Do not set the Rails environment to test by default when using test_unit Railtie. - - *Konstantin Shabanov* - * Remove sqlite3 lines from `.gitignore` if the application is not using sqlite3. *Dmitrii Golub* diff --git a/railties/RDOC_MAIN.rdoc b/railties/RDOC_MAIN.rdoc index eccdee7b07..8d847eaa1c 100644 --- a/railties/RDOC_MAIN.rdoc +++ b/railties/RDOC_MAIN.rdoc @@ -1,7 +1,7 @@ == Welcome to \Rails \Rails is a web-application framework that includes everything needed to create -database-backed web applications according to the {Model-View-Controller (MVC)}[http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller] pattern. +database-backed web applications according to the {Model-View-Controller (MVC)}[http://en.wikipedia.org/wiki/Model-view-controller] pattern. Understanding the MVC pattern is key to understanding \Rails. MVC divides your application into three layers, each with a specific responsibility. diff --git a/railties/README.rdoc b/railties/README.rdoc index 6248b5feed..a25658668c 100644 --- a/railties/README.rdoc +++ b/railties/README.rdoc @@ -31,7 +31,11 @@ API documentation is at * http://api.rubyonrails.org -Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here: +Bug reports can be filed for the Ruby on Rails project here: * https://github.com/rails/rails/issues +Feature requests should be discussed on the rails-core mailing list here: + +* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core + diff --git a/railties/lib/rails/app_rails_loader.rb b/railties/lib/rails/app_rails_loader.rb index 56f05b3844..39d8007333 100644 --- a/railties/lib/rails/app_rails_loader.rb +++ b/railties/lib/rails/app_rails_loader.rb @@ -2,6 +2,8 @@ require 'pathname' module Rails module AppRailsLoader + extend self + RUBY = Gem.ruby EXECUTABLES = ['bin/rails', 'script/rails'] BUNDLER_WARNING = <<EOS @@ -26,7 +28,7 @@ generate it and add it to source control: EOS - def self.exec_app_rails + def exec_app_rails original_cwd = Dir.pwd loop do @@ -54,7 +56,7 @@ EOS end end - def self.find_executable + def find_executable EXECUTABLES.find { |exe| File.file?(exe) } end end diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 2fde974732..c5fd08e743 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -187,6 +187,38 @@ module Rails end end + # Convenience for loading config/foo.yml for the current Rails env. + # + # Example: + # + # # config/exception_notification.yml: + # production: + # url: http://127.0.0.1:8080 + # namespace: my_app_production + # development: + # url: http://localhost:3001 + # namespace: my_app_development + # + # # config/production.rb + # MyApp::Application.configure do + # config.middleware.use ExceptionNotifier, config_for(:exception_notification) + # end + def config_for(name) + yaml = Pathname.new("#{paths["config"].existent.first}/#{name}.yml") + + if yaml.exist? + require "yaml" + require "erb" + (YAML.load(ERB.new(yaml.read).result) || {})[Rails.env] || {} + else + raise "Could not load configuration. No such file - #{yaml}" + end + rescue Psych::SyntaxError => e + raise "YAML syntax error occurred while parsing #{yaml}. " \ + "Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \ + "Error: #{e.message}" + end + # Stores some of the Rails initial environment parameters which # will be used by middlewares and engines to configure themselves. def env_config @@ -364,6 +396,10 @@ module Rails end end + def migration_railties # :nodoc: + (ordered_railties & railties_without_main_app).reverse + end + protected alias :build_middleware_stack :app @@ -394,6 +430,11 @@ module Rails super end + def railties_without_main_app # :nodoc: + @railties_without_main_app ||= Rails::Railtie.subclasses.map(&:instance) + + Rails::Engine.subclasses.map(&:instance) + end + # Returns the ordered railties for this application considering railties_order. def ordered_railties #:nodoc: @ordered_railties ||= begin diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 4c449d2c57..5e8f4de847 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -92,7 +92,7 @@ module Rails # Loads and returns the entire raw configuration of database from # values stored in `config/database.yml`. def database_configuration - yaml = Pathname.new(paths["config/database"].first || "") + yaml = Pathname.new(paths["config/database"].existent.first || "") config = if yaml.exist? require "yaml" diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb index 5b8509b2e9..7a1bb1e25c 100644 --- a/railties/lib/rails/application/finisher.rb +++ b/railties/lib/rails/application/finisher.rb @@ -22,8 +22,6 @@ module Rails initializer :add_builtin_route do |app| if Rails.env.development? app.routes.append do - get '/rails/mailers' => "rails/mailers#index" - get '/rails/mailers/*path' => "rails/mailers#preview" get '/rails/info/properties' => "rails/info#properties" get '/rails/info/routes' => "rails/info#routes" get '/rails/info' => "rails/info#index" diff --git a/railties/lib/rails/commands/dbconsole.rb b/railties/lib/rails/commands/dbconsole.rb index f6d8aec30d..1a2613a8d0 100644 --- a/railties/lib/rails/commands/dbconsole.rb +++ b/railties/lib/rails/commands/dbconsole.rb @@ -20,7 +20,7 @@ module Rails ENV['RAILS_ENV'] = options[:environment] || environment case config["adapter"] - when /^mysql/ + when /^(jdbc)?mysql/ args = { 'host' => '--host', 'port' => '--port', diff --git a/railties/lib/rails/commands/server.rb b/railties/lib/rails/commands/server.rb index 6146b6c1db..c3b7bb6f84 100644 --- a/railties/lib/rails/commands/server.rb +++ b/railties/lib/rails/commands/server.rb @@ -9,7 +9,17 @@ module Rails def parse!(args) args, options = args.dup, {} - opt_parser = OptionParser.new do |opts| + option_parser(options).parse! args + + options[:log_stdout] = options[:daemonize].blank? && (options[:environment] || Rails.env) == "development" + options[:server] = args.shift + options + end + + private + + def option_parser(options) + OptionParser.new do |opts| opts.banner = "Usage: rails server [mongrel, thin, etc] [options]" opts.on("-p", "--port=port", Integer, "Runs Rails on the specified port.", "Default: 3000") { |v| options[:Port] = v } @@ -37,12 +47,6 @@ module Rails opts.on("-h", "--help", "Show this help message.") { puts opts; exit } end - - opt_parser.parse! args - - options[:log_stdout] = options[:daemonize].blank? && (options[:environment] || Rails.env) == "development" - options[:server] = args.shift - options end end diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index dce734b54e..bf2390cb7e 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -156,10 +156,20 @@ module Rails args << "--help" if args.empty? && klass.arguments.any? { |a| a.required? } klass.start(args, config) else - puts "Could not find generator #{namespace}." + options = sorted_groups.map(&:last).flatten + suggestions = options.sort_by {|suggested| levenshtein_distance(namespace.to_s, suggested) }.first(3) + msg = "Could not find generator '#{namespace}'. " + msg << "Maybe you meant #{ suggestions.map {|s| "'#{s}'"}.join(" or ") }\n" + msg << "Run `rails generate --help` for more options." + puts msg end end + # Returns an array of generator namespaces that are hidden. + # Generator namespaces may be hidden for a variety of reasons. + # Some are aliased such as "rails:migration" and can be + # invoked with the shorter "migration", others are private to other generators + # such as "css:scaffold". def self.hidden_namespaces @hidden_namespaces ||= begin orm = options[:rails][:orm] @@ -199,17 +209,6 @@ module Rails # Show help message with available generators. def self.help(command = 'generate') - lookup! - - namespaces = subclasses.map{ |k| k.namespace } - namespaces.sort! - - groups = Hash.new { |h,k| h[k] = [] } - namespaces.each do |namespace| - base = namespace.split(':').first - groups[base] << namespace - end - puts "Usage: rails #{command} GENERATOR [args] [options]" puts puts "General options:" @@ -222,20 +221,74 @@ module Rails puts "Please choose a generator below." puts - # Print Rails defaults first. + print_generators + end + + def self.public_namespaces + lookup! + subclasses.map { |k| k.namespace } + end + + def self.print_generators + sorted_groups.each { |b, n| print_list(b, n) } + end + + def self.sorted_groups + namespaces = public_namespaces + namespaces.sort! + groups = Hash.new { |h,k| h[k] = [] } + namespaces.each do |namespace| + base = namespace.split(':').first + groups[base] << namespace + end rails = groups.delete("rails") rails.map! { |n| n.sub(/^rails:/, '') } rails.delete("app") rails.delete("plugin") - print_list("rails", rails) hidden_namespaces.each { |n| groups.delete(n.to_s) } - groups.sort.each { |b, n| print_list(b, n) } + [["rails", rails]] + groups.sort.to_a end protected + # This code is based directly on the Text gem implementation + # Returns a value representing the "cost" of transforming str1 into str2 + def self.levenshtein_distance str1, str2 + s = str1 + t = str2 + n = s.length + m = t.length + max = n/2 + + return m if (0 == n) + return n if (0 == m) + return n if (n - m).abs > max + + d = (0..m).to_a + x = nil + + str1.each_char.each_with_index do |char1,i| + e = i+1 + + str2.each_char.each_with_index do |char2,j| + cost = (char1 == char2) ? 0 : 1 + x = [ + d[j+1] + 1, # insertion + e + 1, # deletion + d[j] + cost # substitution + ].min + d[j] = e + e = x + end + + d[m] = x + end + + return x + end + # Prints a list of generators. def self.print_list(base, namespaces) #:nodoc: namespaces = namespaces.reject do |n| diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb index 625f031c94..a239874df0 100644 --- a/railties/lib/rails/generators/actions.rb +++ b/railties/lib/rails/generators/actions.rb @@ -20,9 +20,9 @@ module Rails # Set the message to be shown in logs. Uses the git repo if one is given, # otherwise use name (version). - parts, message = [ name.inspect ], name + parts, message = [ quote(name) ], name if version ||= options.delete(:version) - parts << version.inspect + parts << quote(version) message << " (#{version})" end message = options[:git] if options[:git] @@ -30,7 +30,7 @@ module Rails log :gemfile, message options.each do |option, value| - parts << "#{option}: #{value.inspect}" + parts << "#{option}: #{quote(value)}" end in_root do @@ -68,7 +68,7 @@ module Rails log :source, source in_root do - prepend_file "Gemfile", "source #{source.inspect}\n", verbose: false + prepend_file "Gemfile", "source #{quote(source)}\n", verbose: false end end @@ -255,6 +255,15 @@ module Rails end end + # Surround string with single quotes if there is no quotes. + # Otherwise fall back to double quotes + def quote(str) + if str.include?("'") + str.inspect + else + "'#{str}'" + end + end end end end diff --git a/railties/lib/rails/generators/actions/create_migration.rb b/railties/lib/rails/generators/actions/create_migration.rb index cf3b7acfff..682092fdf2 100644 --- a/railties/lib/rails/generators/actions/create_migration.rb +++ b/railties/lib/rails/generators/actions/create_migration.rb @@ -55,7 +55,8 @@ module Rails else say_status :conflict, :red raise Error, "Another migration is already named #{migration_file_name}: " + - "#{existing_migration}. Use --force to replace this migration file." + "#{existing_migration}. Use --force to replace this migration " + + "or --skip to ignore conflicted file." end end diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 569afe8104..7f5a916c5d 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -41,6 +41,9 @@ module Rails class_option :skip_active_record, type: :boolean, aliases: '-O', default: false, desc: 'Skip Active Record files' + class_option :skip_gems, type: :array, default: [], + desc: 'Skip the provided gems files' + class_option :skip_action_view, type: :boolean, aliases: '-V', default: false, desc: 'Skip Action View files' @@ -79,7 +82,7 @@ module Rails end def initialize(*args) - @gem_filter = lambda { |gem| true } + @gem_filter = lambda { |gem| !options[:skip_gems].include?(gem.name) } @extra_entries = [] super convert_database_option_for_jruby @@ -104,14 +107,15 @@ module Rails end def gemfile_entries - [ rails_gemfile_entry, - database_gemfile_entry, - assets_gemfile_entry, - javascript_gemfile_entry, - jbuilder_gemfile_entry, - sdoc_gemfile_entry, - spring_gemfile_entry, - @extra_entries].flatten.find_all(&@gem_filter) + [rails_gemfile_entry, + database_gemfile_entry, + assets_gemfile_entry, + javascript_gemfile_entry, + jbuilder_gemfile_entry, + sdoc_gemfile_entry, + spring_gemfile_entry, + psych_gemfile_entry, + @extra_entries].flatten.find_all(&@gem_filter) end def add_gem_entry_filter @@ -200,10 +204,14 @@ module Rails def rails_gemfile_entry if options.dev? [GemfileEntry.path('rails', Rails::Generators::RAILS_DEV_PATH), - GemfileEntry.github('arel', 'rails/arel')] + GemfileEntry.github('arel', 'rails/arel'), + GemfileEntry.github('rack', 'rack/rack'), + GemfileEntry.github('i18n', 'svenfuchs/i18n')] elsif options.edge? [GemfileEntry.github('rails', 'rails/rails'), - GemfileEntry.github('arel', 'rails/arel')] + GemfileEntry.github('arel', 'rails/arel'), + GemfileEntry.github('rack', 'rack/rack'), + GemfileEntry.github('i18n', 'svenfuchs/i18n')] else [GemfileEntry.version('rails', Rails::VERSION::STRING, @@ -308,6 +316,14 @@ module Rails GemfileEntry.new('spring', nil, comment, group: :development) end + def psych_gemfile_entry + return [] unless defined?(Rubinius) + + comment = 'Use Psych as the YAML engine, instead of Syck, so serialized ' \ + 'data can be read safely from different rubies (see http://git.io/uuLVag)' + GemfileEntry.new('psych', '~> 2.0', comment, platforms: :rbx) + end + def bundle_command(command) say_status :run, "bundle #{command}" diff --git a/railties/lib/rails/generators/base.rb b/railties/lib/rails/generators/base.rb index 67bab96a22..9af6435f23 100644 --- a/railties/lib/rails/generators/base.rb +++ b/railties/lib/rails/generators/base.rb @@ -83,7 +83,7 @@ module Rails # # The first and last part used to find the generator to be invoked are # guessed based on class invokes hook_for, as noticed in the example above. - # This can be customized with two options: :base and :as. + # This can be customized with two options: :in and :as. # # Let's suppose you are creating a generator that needs to invoke the # controller generator from test unit. Your first attempt is: @@ -108,7 +108,7 @@ module Rails # "test_unit:controller", "test_unit" # # Similarly, if you want it to also lookup in the rails namespace, you just - # need to provide the :base value: + # need to provide the :in value: # # class AwesomeGenerator < Rails::Generators::Base # hook_for :test_framework, in: :rails, as: :controller diff --git a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb index 10f80abb15..da99e74435 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb @@ -21,13 +21,8 @@ <%%= f.label :password_confirmation %><br> <%%= f.password_field :password_confirmation %> <% else -%> - <%- if attribute.reference? -%> <%%= f.label :<%= attribute.column_name %> %><br> <%%= f.<%= attribute.field_type %> :<%= attribute.column_name %> %> - <%- else -%> - <%%= f.label :<%= attribute.name %> %><br> - <%%= f.<%= attribute.field_type %> :<%= attribute.name %> %> - <%- end -%> <% end -%> </div> <% end -%> diff --git a/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb index e58b9fbd08..5620fcc850 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb @@ -1,4 +1,4 @@ -<h1>Editing <%= singular_table_name %></h1> +<h1>Editing <%= singular_table_name.titleize %></h1> <%%= render 'form' %> diff --git a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb index 814d6fdb0e..5e194783ff 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb @@ -1,4 +1,6 @@ -<h1>Listing <%= plural_table_name %></h1> +<p id="notice"><%%= notice %></p> + +<h1>Listing <%= plural_table_name.titleize %></h1> <table> <thead> diff --git a/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb index 02ae4d015e..db13a5d870 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb @@ -1,4 +1,4 @@ -<h1>New <%= singular_table_name %></h1> +<h1>New <%= singular_table_name.titleize %></h1> <%%= render 'form' %> diff --git a/railties/lib/rails/generators/named_base.rb b/railties/lib/rails/generators/named_base.rb index 5a92ab3e95..b7da44ca2d 100644 --- a/railties/lib/rails/generators/named_base.rb +++ b/railties/lib/rails/generators/named_base.rb @@ -30,7 +30,12 @@ module Rails protected attr_reader :file_name - alias :singular_name :file_name + + # FIXME: We are avoiding to use alias because a bug on thor that make + # this method public and add it to the task list. + def singular_name + file_name + end # Wrap block with namespace of current application # if namespace exists and is not skipped diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 8675d8bc1e..188e62b6c8 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -86,6 +86,16 @@ module Rails end end + def config_when_updating + cookie_serializer_config_exist = File.exist?('config/initializers/cookies_serializer.rb') + + config + + unless cookie_serializer_config_exist + gsub_file 'config/initializers/cookies_serializer.rb', /json/, 'marshal' + end + end + def database_yml template "config/databases/#{options[:database]}.yml", "config/database.yml" end @@ -188,6 +198,11 @@ module Rails build(:config) end + def update_config_files + build(:config_when_updating) + end + remove_task :update_config_files + def create_boot_file template "config/boot.rb" end diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index 448b6f4845..5bdbd58097 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -16,7 +16,7 @@ source 'https://rubygems.org' # Use ActiveModel has_secure_password # gem 'bcrypt', '~> 3.1.7' -# Use unicorn as the app server +# Use Unicorn as the app server # gem 'unicorn' # Use Capistrano for deployment diff --git a/railties/lib/rails/generators/rails/app/templates/bin/setup b/railties/lib/rails/generators/rails/app/templates/bin/setup new file mode 100644 index 0000000000..0e22b3fa5c --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/bin/setup @@ -0,0 +1,28 @@ +require 'pathname' + +# path to your application root. +APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) + +Dir.chdir APP_ROOT do + # This script is a starting point to setup your application. + # Add necessary setup steps to this file: + + puts "== Installing dependencies ==" + system "gem install bundler --conservative" + system "bundle check || bundle install" + + # puts "\n== Copying sample files ==" + # unless File.exist?("config/database.yml") + # system "cp config/database.yml.sample config/database.yml" + # end + + puts "\n== Preparing database ==" + system "bin/rake db:setup" + + puts "\n== Removing old logs and tempfiles ==" + system "rm -f log/*" + system "rm -rf tmp/cache" + + puts "\n== Restarting application server ==" + system "touch tmp/restart.txt" +end 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 5e5f0c1fac..6b750f00b1 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,3 @@ -# Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) -require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) +require 'bundler/setup' # Set up gems listed in the Gemfile. diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml index d088dd62bf..187ff01bac 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml @@ -1,7 +1,7 @@ # IBM Dataservers # # Home Page -# http://rubyforge.org/projects/rubyibm/ +# https://github.com/dparnell/ibm_db # # To install the ibm_db gem: # @@ -31,8 +31,6 @@ # Configure Using Gemfile # gem 'ibm_db' # -# For more details on the installation and the connection parameters below, -# please refer to the latest documents at http://rubyforge.org/docman/?group_id=2361 # default: &default adapter: ibm_db diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml index 10ab4c02e2..9aedcc15cb 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml @@ -1,7 +1,7 @@ # Oracle/OCI 8i, 9, 10g # # Requires Ruby/OCI8: -# http://rubyforge.org/projects/ruby-oci8/ +# https://github.com/kubo/ruby-oci8 # # Specify your database using any valid connection syntax, such as a # tnsnames.ora service name, or an SQL connect string of the form: diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt index de12565a73..bbb409616d 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 @@ -30,6 +30,9 @@ Rails.application.configure do # number of complex assets. config.assets.debug = true + # Generate digests for assets URLs. + config.assets.digest = true + # Adds additional error checking when serving assets at runtime. # Checks for improperly declared sprockets dependencies. # Raises helpful error messages. diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt index 9ed71687ea..5e52f97249 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt @@ -16,10 +16,10 @@ Rails.application.configure do # Enable Rack::Cache to put a simple HTTP cache in front of your application # Add `rack-cache` to your Gemfile before enabling this. - # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. + # For large-scale production use, consider using a caching reverse proxy like NGINX, varnish or squid. # config.action_dispatch.rack_cache = true - # Disable Rails's static asset server (Apache or nginx will already do this). + # Disable Rails's static asset server (Apache or NGINX will already do this). config.serve_static_assets = false <%- unless options.skip_sprockets? -%> @@ -33,12 +33,12 @@ Rails.application.configure do # Generate digests for assets URLs. config.assets.digest = true - # `config.assets.precompile` has moved to config/initializers/assets.rb + # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb <%- end -%> # Specifies the header that your server uses for sending files. - # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache - # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true diff --git a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb index 6b011e577a..87b8fe3516 100644 --- a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb +++ b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb @@ -5,9 +5,6 @@ require 'rails/test_help' class ActiveSupport::TestCase <% unless options[:skip_active_record] -%> # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. - # - # Note: You'll currently still have to declare fixtures explicitly in integration tests - # -- they do not yet inherit this setting fixtures :all <% end -%> diff --git a/railties/lib/rails/generators/rails/controller/controller_generator.rb b/railties/lib/rails/generators/rails/controller/controller_generator.rb index 7588a558e7..fbecab1823 100644 --- a/railties/lib/rails/generators/rails/controller/controller_generator.rb +++ b/railties/lib/rails/generators/rails/controller/controller_generator.rb @@ -2,6 +2,8 @@ module Rails module Generators class ControllerGenerator < NamedBase # :nodoc: argument :actions, type: :array, default: [], banner: "action action" + class_option :skip_routes, type: :boolean, desc: "Dont' add routes to config/routes.rb." + check_class_collision suffix: "Controller" def create_controller_files @@ -9,8 +11,10 @@ module Rails end def add_routes - actions.reverse.each do |action| - route generate_routing_code(action) + unless options[:skip_routes] + actions.reverse.each do |action| + route generate_routing_code(action) + end end end diff --git a/railties/lib/rails/generators/rails/model/USAGE b/railties/lib/rails/generators/rails/model/USAGE index 833b7beb7f..2a6b8700e3 100644 --- a/railties/lib/rails/generators/rails/model/USAGE +++ b/railties/lib/rails/generators/rails/model/USAGE @@ -6,6 +6,11 @@ Description: model's attributes. Timestamps are added by default, so you don't have to specify them by hand as 'created_at:datetime updated_at:datetime'. + As a special case, specifying 'password:digest' will generate a + password_digest field of string type, and configure your generated model and + tests for use with ActiveModel has_secure_password (assuming the default ORM + and test framework are being used). + You don't have to think up every attribute up front, but it helps to sketch out a few so you can start working with the model immediately. @@ -27,7 +32,8 @@ Available field types: `rails generate model post title:string body:text` will generate a title column with a varchar type and a body column with a text - type. You can use the following types: + type. If no type is specified the string type will be used by default. + You can use the following types: integer primary_key @@ -73,6 +79,10 @@ Available field types: `rails generate model user username:string{30}:uniq` `rails generate model product supplier:references{polymorphic}:index` + If you require a `password_digest` string column for use with + has_secure_password, you should specify `password:digest`: + + `rails generate model user password:digest` Examples: `rails generate model account` diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb index f6f529b80a..584f776c01 100644 --- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb +++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb @@ -288,6 +288,10 @@ task default: :test options[:mountable] end + def skip_git? + options[:skip_git] + end + def with_dummy_app? options[:skip_test_unit].blank? || options[:dummy_path] != 'test/dummy' end @@ -304,6 +308,24 @@ task default: :test @camelized ||= name.gsub(/\W/, '_').squeeze('_').camelize end + def author + default = "TODO: Write your name" + if skip_git? + @author = default + else + @author = `git config user.name`.chomp rescue default + end + end + + def email + default = "TODO: Write your email address" + if skip_git? + @email = default + else + @email = `git config user.email`.chomp rescue default + end + end + def valid_const? if original_name =~ /[^0-9a-zA-Z_]+/ raise Error, "Invalid plugin name #{original_name}. Please give a name which use only alphabetic or numeric or \"_\" characters." diff --git a/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec b/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec index 5fdf0e1554..919c349470 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec +++ b/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec @@ -7,8 +7,8 @@ require "<%= name %>/version" Gem::Specification.new do |s| s.name = "<%= name %>" s.version = <%= camelized %>::VERSION - s.authors = ["TODO: Your name"] - s.email = ["TODO: Your email"] + s.authors = ["<%= author %>"] + s.email = ["<%= email %>"] s.homepage = "TODO" s.summary = "TODO: Summary of <%= camelized %>." s.description = "TODO: Description of <%= camelized %>." diff --git a/railties/lib/rails/generators/rails/plugin/templates/Gemfile b/railties/lib/rails/generators/rails/plugin/templates/Gemfile index 1f704db510..796587f316 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/Gemfile +++ b/railties/lib/rails/generators/rails/plugin/templates/Gemfile @@ -1,7 +1,7 @@ -source "https://rubygems.org" +source 'https://rubygems.org' <% if options[:skip_gemspec] -%> -<%= '# ' if options.dev? || options.edge? -%>gem "rails", "~> <%= Rails::VERSION::STRING %>" +<%= '# ' if options.dev? || options.edge? -%>gem 'rails', '~> <%= Rails::VERSION::STRING %>' <% else -%> # Declare your gem's dependencies in <%= name %>.gemspec. # Bundler will treat runtime dependencies like base dependencies, and @@ -11,7 +11,7 @@ gemspec <% if options[:skip_gemspec] -%> group :development do - gem "<%= gem_for_database %>" + gem '<%= gem_for_database %>' end <% else -%> # Declare any dependencies that are still in development here instead of in diff --git a/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE b/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE index d7a9109894..ff2fb3ba4e 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE +++ b/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright <%= Date.today.year %> YOURNAME +Copyright <%= Date.today.year %> <%= author %> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/railties/lib/rails/generators/rails/plugin/templates/Rakefile b/railties/lib/rails/generators/rails/plugin/templates/Rakefile index 0ba899176c..c338a0bdb1 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/Rakefile +++ b/railties/lib/rails/generators/rails/plugin/templates/Rakefile @@ -19,6 +19,10 @@ APP_RAKEFILE = File.expand_path("../<%= dummy_path -%>/Rakefile", __FILE__) load 'rails/tasks/engine.rake' <% end %> +<% if engine? -%> +load 'rails/tasks/statistics.rake' +<% end %> + <% unless options[:skip_gemspec] -%> Bundler::GemHelper.install_tasks diff --git a/railties/lib/rails/generators/rails/scaffold/USAGE b/railties/lib/rails/generators/rails/scaffold/USAGE index 4a3eb2c7c7..1b2a944103 100644 --- a/railties/lib/rails/generators/rails/scaffold/USAGE +++ b/railties/lib/rails/generators/rails/scaffold/USAGE @@ -9,11 +9,16 @@ Description: Attributes are field arguments specifying the model's attributes. You can optionally pass the type and an index to each field. For instance: - "title body:text tracking_id:integer:uniq" will generate a title field of + 'title body:text tracking_id:integer:uniq' will generate a title field of string type, a body with text type and a tracking_id as an integer with an unique index. "index" could also be given instead of "uniq" if one desires a non unique index. + As a special case, specifying 'password:digest' will generate a + password_digest field of string type, and configure your generated model, + controller, views, and test suite for use with ActiveModel + has_secure_password (assuming they are using Rails defaults). + Timestamps are added by default, so you don't have to specify them by hand as 'created_at:datetime updated_at:datetime'. @@ -33,3 +38,4 @@ Examples: `rails generate scaffold post` `rails generate scaffold post title body:text published:boolean` `rails generate scaffold purchase amount:decimal tracking_id:integer:uniq` + `rails generate scaffold user email:uniq password:digest` diff --git a/railties/lib/rails/generators/testing/assertions.rb b/railties/lib/rails/generators/testing/assertions.rb index 2e877f8762..bd069e4bd0 100644 --- a/railties/lib/rails/generators/testing/assertions.rb +++ b/railties/lib/rails/generators/testing/assertions.rb @@ -1,3 +1,5 @@ +require 'shellwords' + module Rails module Generators module Testing diff --git a/railties/lib/rails/generators/testing/behaviour.rb b/railties/lib/rails/generators/testing/behaviour.rb index 7576eba6e0..e0600d0b59 100644 --- a/railties/lib/rails/generators/testing/behaviour.rb +++ b/railties/lib/rails/generators/testing/behaviour.rb @@ -100,6 +100,23 @@ module Rails dirname, file_name = File.dirname(absolute), File.basename(absolute).sub(/\.rb$/, '') Dir.glob("#{dirname}/[0-9]*_*.rb").grep(/\d+_#{file_name}.rb$/).first end + + def capture(stream) + stream = stream.to_s + captured_stream = Tempfile.new(stream) + stream_io = eval("$#{stream}") + origin_stream = stream_io.dup + stream_io.reopen(captured_stream) + + yield + + stream_io.rewind + return captured_stream.read + ensure + captured_stream.close + captured_stream.unlink + stream_io.reopen(origin_stream) + end end end end diff --git a/railties/lib/rails/info_controller.rb b/railties/lib/rails/info_controller.rb index 908c4ce65e..49e5431a16 100644 --- a/railties/lib/rails/info_controller.rb +++ b/railties/lib/rails/info_controller.rb @@ -5,7 +5,7 @@ class Rails::InfoController < Rails::ApplicationController # :nodoc: prepend_view_path ActionDispatch::DebugExceptions::RESCUES_TEMPLATE_PATH layout -> { request.xhr? ? false : 'application' } - before_filter :require_local! + before_action :require_local! def index redirect_to action: :routes diff --git a/railties/lib/rails/mailers_controller.rb b/railties/lib/rails/mailers_controller.rb index dd318f52e5..32740d66da 100644 --- a/railties/lib/rails/mailers_controller.rb +++ b/railties/lib/rails/mailers_controller.rb @@ -3,8 +3,8 @@ require 'rails/application_controller' class Rails::MailersController < Rails::ApplicationController # :nodoc: prepend_view_path ActionDispatch::DebugExceptions::RESCUES_TEMPLATE_PATH - before_filter :require_local! - before_filter :find_preview, only: :preview + before_action :require_local! + before_action :find_preview, only: :preview def index @previews = ActionMailer::Preview.all @@ -70,4 +70,4 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: @email end end -end
\ No newline at end of file +end diff --git a/railties/lib/rails/rack/log_tailer.rb b/railties/lib/rails/rack/log_tailer.rb index 50d0eb96fc..bc26421a9e 100644 --- a/railties/lib/rails/rack/log_tailer.rb +++ b/railties/lib/rails/rack/log_tailer.rb @@ -1,7 +1,11 @@ +require 'active_support/deprecation' + module Rails module Rack class LogTailer def initialize(app, log = nil) + ActiveSupport::Deprecation.warn "LogTailer is deprecated and will be removed on Rails 5" + @app = app path = Pathname.new(log || "#{::File.expand_path(Rails.root)}/log/#{Rails.env}.log").cleanpath diff --git a/railties/lib/rails/rack/logger.rb b/railties/lib/rails/rack/logger.rb index 3b35798679..9962e6d943 100644 --- a/railties/lib/rails/rack/logger.rb +++ b/railties/lib/rails/rack/logger.rb @@ -34,7 +34,7 @@ module Rails instrumenter = ActiveSupport::Notifications.instrumenter instrumenter.start 'request.action_dispatch', request: request - logger.info started_request_message(request) + logger.info { started_request_message(request) } resp = @app.call(env) resp[2] = ::Rack::BodyProxy.new(resp[2]) { finish(request) } resp diff --git a/railties/lib/rails/ruby_version_check.rb b/railties/lib/rails/ruby_version_check.rb index 3b7f358a5b..df74643a59 100644 --- a/railties/lib/rails/ruby_version_check.rb +++ b/railties/lib/rails/ruby_version_check.rb @@ -2,7 +2,7 @@ if RUBY_VERSION < '1.9.3' desc = defined?(RUBY_DESCRIPTION) ? RUBY_DESCRIPTION : "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE})" abort <<-end_message - Rails 4 prefers to run on Ruby 2.0. + Rails 4 prefers to run on Ruby 2.1 or newer. You're running #{desc} diff --git a/railties/lib/rails/tasks/framework.rake b/railties/lib/rails/tasks/framework.rake index 3c8f8c6b87..a1c805f8aa 100644 --- a/railties/lib/rails/tasks/framework.rake +++ b/railties/lib/rails/tasks/framework.rake @@ -55,7 +55,7 @@ namespace :rails do # desc "Update config/boot.rb from your current rails install" task :configs do invoke_from_app_generator :create_boot_file - invoke_from_app_generator :create_config_files + invoke_from_app_generator :update_config_files end # desc "Adds new executables to the application bin/ directory" diff --git a/railties/lib/rails/tasks/statistics.rake b/railties/lib/rails/tasks/statistics.rake index c1674c72ad..ae5a7d2759 100644 --- a/railties/lib/rails/tasks/statistics.rake +++ b/railties/lib/rails/tasks/statistics.rake @@ -1,3 +1,6 @@ +# while having global constant is not good, +# many 3rd party tools depend on it, like rspec-rails, cucumber-rails, etc +# so if will be removed - deprecation warning is needed STATS_DIRECTORIES = [ %w(Controllers app/controllers), %w(Helpers app/helpers), @@ -13,10 +16,12 @@ STATS_DIRECTORIES = [ %w(Integration\ tests test/integration), %w(Functional\ tests\ (old) test/functional), %w(Unit\ tests \ (old) test/unit) -].collect { |name, dir| [ name, "#{Rails.root}/#{dir}" ] }.select { |name, dir| File.directory?(dir) } +].collect do |name, dir| + [ name, "#{File.dirname(Rake.application.rakefile_location)}/#{dir}" ] +end.select { |name, dir| File.directory?(dir) } -desc "Report code statistics (KLOCs, etc) from the application" +desc "Report code statistics (KLOCs, etc) from the application or engine" task :stats do require 'rails/code_statistics' CodeStatistics.new(*STATS_DIRECTORIES).to_s -end +end
\ No newline at end of file diff --git a/railties/lib/rails/test_unit/railtie.rb b/railties/lib/rails/test_unit/railtie.rb index 878b9b7930..75180ff978 100644 --- a/railties/lib/rails/test_unit/railtie.rb +++ b/railties/lib/rails/test_unit/railtie.rb @@ -1,4 +1,4 @@ -if defined?(Rake.application) && Rake.application.top_level_tasks.grep(/^test(?::|$)/).any? +if defined?(Rake.application) && Rake.application.top_level_tasks.grep(/^(default$|test(:|$))/).any? ENV['RAILS_ENV'] ||= 'test' end diff --git a/railties/lib/rails/test_unit/sub_test_task.rb b/railties/lib/rails/test_unit/sub_test_task.rb index d9bffba4d7..6fa96d2ced 100644 --- a/railties/lib/rails/test_unit/sub_test_task.rb +++ b/railties/lib/rails/test_unit/sub_test_task.rb @@ -7,7 +7,7 @@ module Rails # # This class takes a TestInfo class and defines the appropriate rake task # based on the information, then invokes it. - class TestCreator + class TestCreator # :nodoc: def initialize(info) @info = info end @@ -41,7 +41,7 @@ module Rails # to test files (or can be transformed into test files). Calling <tt>files</tt> # provides the set of test files and is used when initializing tests after # a call to <tt>rake test</tt>. - class TestInfo + class TestInfo # :nodoc: def initialize(tasks) @tasks = tasks @files = nil diff --git a/railties/test/abstract_unit.rb b/railties/test/abstract_unit.rb index 9ccc286b4e..b6533a5fb2 100644 --- a/railties/test/abstract_unit.rb +++ b/railties/test/abstract_unit.rb @@ -26,3 +26,26 @@ end def jruby_skip(message = '') skip message if defined?(JRUBY_VERSION) end + +class ActiveSupport::TestCase + private + + unless defined?(:capture) + def capture(stream) + stream = stream.to_s + captured_stream = Tempfile.new(stream) + stream_io = eval("$#{stream}") + origin_stream = stream_io.dup + stream_io.reopen(captured_stream) + + yield + + stream_io.rewind + return captured_stream.read + ensure + captured_stream.close + captured_stream.unlink + stream_io.reopen(origin_stream) + end + end +end diff --git a/railties/test/app_rails_loader_test.rb b/railties/test/app_rails_loader_test.rb index 1d3b80253a..d4885447e6 100644 --- a/railties/test/app_rails_loader_test.rb +++ b/railties/test/app_rails_loader_test.rb @@ -3,13 +3,27 @@ require 'abstract_unit' require 'rails/app_rails_loader' class AppRailsLoaderTest < ActiveSupport::TestCase + def loader + @loader ||= Class.new do + extend Rails::AppRailsLoader + + def self.exec_arguments + @exec_arguments + end + + def self.exec(*args) + @exec_arguments = args + end + end + end + def write(filename, contents=nil) FileUtils.mkdir_p(File.dirname(filename)) File.write(filename, contents) end def expects_exec(exe) - Rails::AppRailsLoader.expects(:exec).with(Rails::AppRailsLoader::RUBY, exe) + assert_equal [Rails::AppRailsLoader::RUBY, exe], loader.exec_arguments end setup do @@ -22,30 +36,30 @@ 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(:file?).with('bin/rails').returns(false) - File.stubs(:file?).with('script/rails').returns(false) + def loader.find_executables; end - assert !Rails::AppRailsLoader.exec_app_rails + assert !loader.exec_app_rails end test "is not in a Rails application if #{exe} exists but is a folder" do FileUtils.mkdir_p(exe) - assert !Rails::AppRailsLoader.exec_app_rails + assert !loader.exec_app_rails end ['APP_PATH', 'ENGINE_PATH'].each do |keyword| test "is in a Rails application if #{exe} exists and contains #{keyword}" do write exe, keyword + loader.exec_app_rails + expects_exec exe - Rails::AppRailsLoader.exec_app_rails end test "is not in a Rails application if #{exe} exists but doesn't contain #{keyword}" do write exe - assert !Rails::AppRailsLoader.exec_app_rails + assert !loader.exec_app_rails end test "is in a Rails application if parent directory has #{exe} containing #{keyword} and chdirs to the root directory" do @@ -54,8 +68,9 @@ class AppRailsLoaderTest < ActiveSupport::TestCase Dir.chdir('foo/bar') + loader.exec_app_rails + expects_exec exe - Rails::AppRailsLoader.exec_app_rails # Compare the realpath in case either of them has symlinks. # diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb index 410b0f7d70..8f091cfdbf 100644 --- a/railties/test/application/assets_test.rb +++ b/railties/test/application/assets_test.rb @@ -50,6 +50,8 @@ module ApplicationTests end RUBY + add_to_env_config "development", "config.assets.digest = false" + require "#{app_path}/config/environment" get "/assets/demo.js" @@ -189,7 +191,6 @@ module ApplicationTests end test "asset pipeline should use a Sprockets::Index when config.assets.digest is true" do - add_to_config "config.assets.digest = true" add_to_config "config.action_controller.perform_caching = false" ENV["RAILS_ENV"] = "production" @@ -202,8 +203,6 @@ module ApplicationTests app_file "app/assets/images/rails.png", "notactuallyapng" app_file "app/assets/stylesheets/application.css.erb", "<%= asset_path('rails.png') %>" app_file "app/assets/javascripts/application.js", "alert();" - # digest is default in false, we must enable it for test environment - add_to_config "config.assets.digest = true" precompile! manifest = Dir["#{app_path}/public/assets/manifest-*.json"].first @@ -215,8 +214,6 @@ module ApplicationTests test "the manifest file should be saved by default in the same assets folder" do app_file "app/assets/javascripts/application.js", "alert();" - # digest is default in false, we must enable it for test environment - add_to_config "config.assets.digest = true" add_to_config "config.assets.prefix = '/x'" precompile! @@ -249,7 +246,6 @@ module ApplicationTests test "precompile properly refers files referenced with asset_path and runs in the provided RAILS_ENV" do app_file "app/assets/images/rails.png", "notactuallyapng" app_file "app/assets/stylesheets/application.css.erb", "<%= asset_path('rails.png') %>" - # digest is default in false, we must enable it for test environment add_to_env_config "test", "config.assets.digest = true" precompile!('RAILS_ENV=test') @@ -281,12 +277,9 @@ module ApplicationTests test "precompile appends the md5 hash to files referenced with asset_path and run in production with digest true" do app_file "app/assets/images/rails.png", "notactuallyapng" app_file "app/assets/stylesheets/application.css.erb", "<%= asset_path('rails.png') %>" - add_to_config "config.assets.compile = true" - add_to_config "config.assets.digest = true" - ENV["RAILS_ENV"] = nil - - precompile!('RAILS_GROUPS=assets') + ENV["RAILS_ENV"] = "production" + precompile! file = Dir["#{app_path}/public/assets/application-*.css"].first assert_match(/\/assets\/rails-([0-z]+)\.png/, File.read(file)) @@ -342,6 +335,8 @@ module ApplicationTests end RUBY + add_to_env_config "development", "config.assets.digest = false" + require "#{app_path}/config/environment" class ::OmgController < ActionController::Base @@ -366,6 +361,8 @@ module ApplicationTests app_file "app/assets/javascripts/demo.js", "alert();" + add_to_env_config "development", "config.assets.digest = false" + require "#{app_path}/config/environment" get "/assets/demo.js" @@ -395,7 +392,6 @@ module ApplicationTests app_file "app/assets/javascripts/application.js", "//= require_tree ." app_file "app/assets/javascripts/xmlhr.js.erb", "<%= Post.name %>" - add_to_config "config.assets.digest = false" precompile! assert_equal "Post;\n", File.read(Dir["#{app_path}/public/assets/application-*.js"].first) end @@ -415,7 +411,6 @@ module ApplicationTests test "digested assets are not mistakenly removed" do app_file "app/assets/application.js", "alert();" add_to_config "config.assets.compile = true" - add_to_config "config.assets.digest = true" precompile! @@ -438,6 +433,7 @@ module ApplicationTests test "asset urls should use the request's protocol by default" do app_with_assets_in_view add_to_config "config.asset_host = 'example.com'" + add_to_env_config "development", "config.assets.digest = false" require "#{app_path}/config/environment" class ::PostsController < ActionController::Base; end @@ -452,6 +448,7 @@ module ApplicationTests app_file "app/assets/javascripts/image_loader.js.erb", "var src='<%= image_path('rails.png') %>';" add_to_config "config.assets.precompile = %w{rails.png image_loader.js}" add_to_config "config.asset_host = 'example.com'" + add_to_env_config "development", "config.assets.digest = false" precompile! assert_match "src='//example.com/assets/rails.png'", File.read(Dir["#{app_path}/public/assets/image_loader-*.js"].first) @@ -460,9 +457,9 @@ module ApplicationTests test "asset paths should use RAILS_RELATIVE_URL_ROOT by default" do ENV["RAILS_RELATIVE_URL_ROOT"] = "/sub/uri" app_file "app/assets/images/rails.png", "notreallyapng" - app_file "app/assets/javascripts/app.js.erb", "var src='<%= image_path('rails.png') %>';" add_to_config "config.assets.precompile = %w{rails.png app.js}" + add_to_env_config "development", "config.assets.digest = false" precompile! assert_match "src='/sub/uri/assets/rails.png'", File.read(Dir["#{app_path}/public/assets/app-*.js"].first) diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 09aba1c2e9..21244188dd 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -8,6 +8,12 @@ end class ::MyOtherMailInterceptor < ::MyMailInterceptor; end +class ::MyPreviewMailInterceptor + def self.previewing_email(email); email; end +end + +class ::MyOtherPreviewMailInterceptor < ::MyPreviewMailInterceptor; end + class ::MyMailObserver def self.delivered_email(email); email; end end @@ -60,13 +66,25 @@ module ApplicationTests config.action_dispatch.show_exceptions = true RUBY + app_file 'db/migrate/20140708012246_create_user.rb', <<-RUBY + class CreateUser < ActiveRecord::Migration + def change + create_table :users + end + end + RUBY + require "#{app_path}/config/environment" - ActiveRecord::Migrator.stubs(:needs_migration?).returns(true) - ActiveRecord::NullMigration.any_instance.stubs(:mtime).returns(1) - get "/foo" - assert_equal 500, last_response.status - assert_match "ActiveRecord::PendingMigrationError", last_response.body + ActiveRecord::Migrator.migrations_paths = ["#{app_path}/db/migrate"] + + begin + get "/foo" + assert_equal 500, last_response.status + assert_match "ActiveRecord::PendingMigrationError", last_response.body + ensure + ActiveRecord::Migrator.migrations_paths = nil + end end test "Rails.groups returns available groups" do @@ -360,12 +378,14 @@ module ApplicationTests test "default method for update can be changed" do app_file 'app/models/post.rb', <<-RUBY class Post - extend ActiveModel::Naming + include ActiveModel::Model def to_key; [1]; end def persisted?; true; end end RUBY + token = "cf50faa3fe97702ca1ae" + app_file 'app/controllers/posts_controller.rb', <<-RUBY class PostsController < ApplicationController def show @@ -375,6 +395,10 @@ module ApplicationTests def update render text: "update" end + + private + + def form_authenticity_token; token; end # stub the authenticy token end RUBY @@ -386,8 +410,6 @@ module ApplicationTests require "#{app_path}/config/environment" - token = "cf50faa3fe97702ca1ae" - PostsController.any_instance.stubs(:form_authenticity_token).returns(token) params = {authenticity_token: token} get "/posts/1" @@ -460,6 +482,32 @@ module ApplicationTests assert_equal [::MyMailInterceptor, ::MyOtherMailInterceptor], ::Mail.send(:class_variable_get, "@@delivery_interceptors") end + test "registers preview interceptors with ActionMailer" do + add_to_config <<-RUBY + config.action_mailer.preview_interceptors = MyPreviewMailInterceptor + RUBY + + require "#{app_path}/config/environment" + require "mail" + + _ = ActionMailer::Base + + assert_equal [::MyPreviewMailInterceptor], ActionMailer::Base.preview_interceptors + end + + test "registers multiple preview interceptors with ActionMailer" do + add_to_config <<-RUBY + config.action_mailer.preview_interceptors = [MyPreviewMailInterceptor, "MyOtherPreviewMailInterceptor"] + RUBY + + require "#{app_path}/config/environment" + require "mail" + + _ = ActionMailer::Base + + assert_equal [MyPreviewMailInterceptor, MyOtherPreviewMailInterceptor], ActionMailer::Base.preview_interceptors + end + test "registers observers with ActionMailer" do add_to_config <<-RUBY config.action_mailer.observers = MyMailObserver @@ -682,6 +730,44 @@ module ApplicationTests assert_match "We're sorry, but something went wrong", last_response.body end + test "config.action_controller.always_permitted_parameters are: controller, action by default" do + require "#{app_path}/config/environment" + assert_equal %w(controller action), ActionController::Parameters.always_permitted_parameters + end + + test "config.action_controller.always_permitted_parameters = ['controller', 'action', 'format']" do + add_to_config <<-RUBY + config.action_controller.always_permitted_parameters = %w( controller action format ) + RUBY + require "#{app_path}/config/environment" + assert_equal %w( controller action format ), ActionController::Parameters.always_permitted_parameters + end + + test "config.action_controller.always_permitted_parameters = ['controller','action','format'] does not raise exeception" do + app_file 'app/controllers/posts_controller.rb', <<-RUBY + class PostsController < ActionController::Base + def create + render text: params.permit(post: [:title]) + end + end + RUBY + + add_to_config <<-RUBY + routes.prepend do + resources :posts + end + config.action_controller.always_permitted_parameters = %w( controller action format ) + config.action_controller.action_on_unpermitted_parameters = :raise + RUBY + + require "#{app_path}/config/environment" + + assert_equal :raise, ActionController::Parameters.action_on_unpermitted_parameters + + post "/posts", {post: {"title" =>"zomg"}, format: "json"} + assert_equal 200, last_response.status + end + test "config.action_controller.action_on_unpermitted_parameters is :log by default on development" do ENV["RAILS_ENV"] = "development" @@ -879,5 +965,129 @@ module ApplicationTests Rails.application.load_runner assert $ran_block end + + test "loading the first existing database configuration available" do + app_file 'config/environments/development.rb', <<-RUBY + + Rails.application.configure do + config.paths.add 'config/database', with: 'config/nonexistant.yml' + config.paths['config/database'] << 'config/database.yml' + end + RUBY + + require "#{app_path}/config/environment" + + db_config = Rails.application.config.database_configuration + + assert db_config.is_a?(Hash) + end + + test 'config.action_mailer.show_previews defaults to true in development' do + Rails.env = "development" + require "#{app_path}/config/environment" + + assert Rails.application.config.action_mailer.show_previews + end + + test 'config.action_mailer.show_previews defaults to false in production' do + Rails.env = "production" + require "#{app_path}/config/environment" + + assert_equal Rails.application.config.action_mailer.show_previews, false + end + + test 'config.action_mailer.show_previews can be set in the configuration file' do + Rails.env = "production" + add_to_config <<-RUBY + config.action_mailer.show_previews = true + RUBY + require "#{app_path}/config/environment" + + assert_equal Rails.application.config.action_mailer.show_previews, true + end + + test "config_for loads custom configuration from yaml files" do + app_file 'config/custom.yml', <<-RUBY + development: + key: 'custom key' + RUBY + + add_to_config <<-RUBY + config.my_custom_config = config_for('custom') + RUBY + + require "#{app_path}/config/environment" + + assert_equal 'custom key', Rails.application.config.my_custom_config['key'] + end + + test "config_for raises an exception if the file does not exist" do + add_to_config <<-RUBY + config.my_custom_config = config_for('custom') + RUBY + + exception = assert_raises(RuntimeError) do + require "#{app_path}/config/environment" + end + + assert_equal "Could not load configuration. No such file - #{app_path}/config/custom.yml", exception.message + end + + test "config_for without the environment configured returns an empty hash" do + app_file 'config/custom.yml', <<-RUBY + test: + key: 'custom key' + RUBY + + add_to_config <<-RUBY + config.my_custom_config = config_for('custom') + RUBY + require "#{app_path}/config/environment" + + assert_equal({}, Rails.application.config.my_custom_config) + end + + test "config_for with empty file returns an empty hash" do + app_file 'config/custom.yml', <<-RUBY + RUBY + + add_to_config <<-RUBY + config.my_custom_config = config_for('custom') + RUBY + require "#{app_path}/config/environment" + + assert_equal({}, Rails.application.config.my_custom_config) + end + + test "config_for containing ERB tags should evaluate" do + app_file 'config/custom.yml', <<-RUBY + development: + key: <%= 'custom key' %> + RUBY + + add_to_config <<-RUBY + config.my_custom_config = config_for('custom') + RUBY + require "#{app_path}/config/environment" + + assert_equal 'custom key', Rails.application.config.my_custom_config['key'] + end + + test "config_for with syntax error show a more descritive exception" do + app_file 'config/custom.yml', <<-RUBY + development: + key: foo: + RUBY + + add_to_config <<-RUBY + config.my_custom_config = config_for('custom') + RUBY + + exception = assert_raises(RuntimeError) do + require "#{app_path}/config/environment" + end + + assert_match 'YAML syntax error occurred while parsing', exception.message + end end end diff --git a/railties/test/application/initializers/i18n_test.rb b/railties/test/application/initializers/i18n_test.rb index bc34897cdf..9ee54796a4 100644 --- a/railties/test/application/initializers/i18n_test.rb +++ b/railties/test/application/initializers/i18n_test.rb @@ -184,28 +184,13 @@ en: assert_fallbacks ca: [:ca, :"es-ES", :es, :'en-US', :en] end - test "config.i18n.enforce_available_locales is set to true by default and avoids I18n warnings" do - add_to_config <<-RUBY - config.i18n.default_locale = :it - RUBY - - output = capture(:stderr) { load_app } - assert_no_match %r{deprecated.*enforce_available_locales}, output - assert_equal true, I18n.enforce_available_locales - - assert_raise I18n::InvalidLocale do - I18n.locale = :es - end - end - test "disable config.i18n.enforce_available_locales" do add_to_config <<-RUBY config.i18n.enforce_available_locales = false config.i18n.default_locale = :fr RUBY - output = capture(:stderr) { load_app } - assert_no_match %r{deprecated.*enforce_available_locales}, output + load_app assert_equal false, I18n.enforce_available_locales assert_nothing_raised do @@ -220,8 +205,7 @@ en: config.i18n.default_locale = :fr RUBY - output = capture(:stderr) { load_app } - assert_no_match %r{deprecated.*enforce_available_locales}, output + load_app assert_equal false, I18n.enforce_available_locales assert_nothing_raised do diff --git a/railties/test/application/mailer_previews_test.rb b/railties/test/application/mailer_previews_test.rb index c588fd7012..8b91a1171f 100644 --- a/railties/test/application/mailer_previews_test.rb +++ b/railties/test/application/mailer_previews_test.rb @@ -26,6 +26,20 @@ module ApplicationTests assert_equal 404, last_response.status end + test "/rails/mailers is accessible with correct configuraiton" do + add_to_config "config.action_mailer.show_previews = true" + app("production") + get "/rails/mailers" + assert_equal 200, last_response.status + end + + test "/rails/mailers is not accessible with show_previews = false" do + add_to_config "config.action_mailer.show_previews = false" + app("development") + get "/rails/mailers" + assert_equal 404, last_response.status + end + test "mailer previews are loaded from the default preview_path" do mailer 'notifier', <<-RUBY class Notifier < ActionMailer::Base diff --git a/railties/test/application/rack/logger_test.rb b/railties/test/application/rack/logger_test.rb index 701843a6fd..0082ec9cd2 100644 --- a/railties/test/application/rack/logger_test.rb +++ b/railties/test/application/rack/logger_test.rb @@ -11,10 +11,12 @@ module ApplicationTests def setup build_app + add_to_config <<-RUBY + config.logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + RUBY + require "#{app_path}/config/environment" super - @logger = MockLogger.new - Rails.stubs(:logger).returns(@logger) end def teardown @@ -23,7 +25,7 @@ module ApplicationTests end def logs - @logs ||= @logger.logged(:info).join("\n") + @logs ||= Rails.logger.logged(:info).join("\n") end test "logger logs proper HTTP GET verb and path" do diff --git a/railties/test/application/rake/migrations_test.rb b/railties/test/application/rake/migrations_test.rb index b7fd5d02c5..a3819b93b2 100644 --- a/railties/test/application/rake/migrations_test.rb +++ b/railties/test/application/rake/migrations_test.rb @@ -58,7 +58,7 @@ module ApplicationTests end test 'migration status when schema migrations table is not present' do - output = Dir.chdir(app_path){ `rake db:migrate:status` } + output = Dir.chdir(app_path){ `rake db:migrate:status 2>&1` } assert_equal "Schema migrations table does not exist yet.\n", output end @@ -184,6 +184,21 @@ module ApplicationTests assert_match(/create_table "books"/, structure_dump) end end + + test 'test migration status migrated file is deleted' do + Dir.chdir(app_path) do + `rails generate model user username:string password:string; + rails generate migration add_email_to_users email:string; + rake db:migrate + rm db/migrate/*email*.rb` + + output = `rake db:migrate:status` + File.write('test.txt', output) + + assert_match(/up\s+\d{14}\s+Create users/, output) + assert_match(/up\s+\d{14}\s+\** NO FILE \**/, output) + end + end end end end diff --git a/railties/test/commands/console_test.rb b/railties/test/commands/console_test.rb index 1273f9d4c2..4aea3e980f 100644 --- a/railties/test/commands/console_test.rb +++ b/railties/test/commands/console_test.rb @@ -6,7 +6,13 @@ class Rails::ConsoleTest < ActiveSupport::TestCase include EnvHelpers class FakeConsole - def self.start; end + def self.started? + @started + end + + def self.start + @started = true + end end def test_sandbox_option @@ -25,17 +31,18 @@ class Rails::ConsoleTest < ActiveSupport::TestCase end def test_start - FakeConsole.expects(:start) start + + assert app.console.started? assert_match(/Loading \w+ environment \(Rails/, output) end def test_start_with_sandbox - app.expects(:sandbox=).with(true) - FakeConsole.expects(:start) - start ["--sandbox"] + + assert app.console.started? + assert app.sandbox assert_match(/Loading \w+ environment in sandbox \(Rails/, output) end @@ -51,9 +58,12 @@ class Rails::ConsoleTest < ActiveSupport::TestCase end def test_start_with_debugger - rails_console = Rails::Console.new(app, parse_arguments(["--debugger"])) - rails_console.expects(:require_debugger).returns(nil) + stubbed_console = Class.new(Rails::Console) do + def require_debugger + end + end + rails_console = stubbed_console.new(app, parse_arguments(["--debugger"])) silence_stream(STDOUT) { rails_console.start } end end @@ -64,7 +74,7 @@ class Rails::ConsoleTest < ActiveSupport::TestCase end def test_console_defaults_to_IRB - app = build_app(console: nil) + app = build_app(nil) assert_equal IRB, Rails::Console.new(app).console end @@ -115,8 +125,12 @@ class Rails::ConsoleTest < ActiveSupport::TestCase end def test_rails_env_is_dev_when_argument_is_dev_and_dev_env_is_present - Rails::Console.stubs(:available_environments).returns(['dev']) - options = Rails::Console.parse_arguments(['dev']) + stubbed_console = Class.new(Rails::Console) do + def available_environments + ['dev'] + end + end + options = stubbed_console.parse_arguments(['dev']) assert_match('dev', options[:environment]) end @@ -131,15 +145,29 @@ class Rails::ConsoleTest < ActiveSupport::TestCase end def app - @app ||= build_app(console: FakeConsole) + @app ||= build_app(FakeConsole) end - def build_app(config) - config = mock("config", config) - app = mock("app", config: config) - app.stubs(:sandbox=).returns(nil) - app.expects(:load_console) - app + def build_app(console) + mocked_console = Class.new do + attr_reader :sandbox, :console + + def initialize(console) + @console = console + end + + def config + self + end + + def sandbox=(arg) + @sandbox = arg + end + + def load_console + end + end + mocked_console.new(console) end def parse_arguments(args) diff --git a/railties/test/commands/dbconsole_test.rb b/railties/test/commands/dbconsole_test.rb index 24db395e6e..ede08e7b86 100644 --- a/railties/test/commands/dbconsole_test.rb +++ b/railties/test/commands/dbconsole_test.rb @@ -1,4 +1,5 @@ require 'abstract_unit' +require 'minitest/mock' require 'rails/commands/dbconsole' class Rails::DBConsoleTest < ActiveSupport::TestCase @@ -26,20 +27,21 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase "timeout"=> "3000" } } - app_db_config(config_sample) - assert_equal Rails::DBConsole.new.config, config_sample["test"] + app_db_config(config_sample) do + assert_equal Rails::DBConsole.new.config, config_sample["test"] + end end def test_config_with_no_db_config - app_db_config(nil) - assert_raise(ActiveRecord::AdapterNotSpecified) { - Rails::DBConsole.new.config - } + app_db_config(nil) do + assert_raise(ActiveRecord::AdapterNotSpecified) { + Rails::DBConsole.new.config + } + end end def test_config_with_database_url_only ENV['DATABASE_URL'] = 'postgresql://foo:bar@localhost:9000/foo_test?pool=5&timeout=3000' - app_db_config(nil) expected = { "adapter" => "postgresql", "host" => "localhost", @@ -50,7 +52,10 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase "pool" => "5", "timeout" => "3000" }.sort - assert_equal expected, Rails::DBConsole.new.config.sort + + app_db_config(nil) do + assert_equal expected, Rails::DBConsole.new.config.sort + end end def test_config_choose_database_url_if_exists @@ -68,8 +73,9 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase "timeout" => "3000" } } - app_db_config(sample_config) - assert_equal host, Rails::DBConsole.new.config["host"] + app_db_config(sample_config) do + assert_equal host, Rails::DBConsole.new.config["host"] + end end def test_env @@ -78,58 +84,65 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase ENV['RAILS_ENV'] = nil ENV['RACK_ENV'] = nil - Rails.stubs(:respond_to?).with(:env).returns(false) - assert_equal Rails::DBConsole.new.environment, "development" + Rails.stub(:respond_to?, false) do + assert_equal Rails::DBConsole.new.environment, "development" - ENV['RACK_ENV'] = "rack_env" - assert_equal Rails::DBConsole.new.environment, "rack_env" + ENV['RACK_ENV'] = "rack_env" + assert_equal Rails::DBConsole.new.environment, "rack_env" - ENV['RAILS_ENV'] = "rails_env" - assert_equal Rails::DBConsole.new.environment, "rails_env" + ENV['RAILS_ENV'] = "rails_env" + assert_equal Rails::DBConsole.new.environment, "rails_env" + end ensure ENV['RAILS_ENV'] = "test" end def test_rails_env_is_development_when_argument_is_dev - Rails::DBConsole.stubs(:available_environments).returns(['development', 'test']) - options = Rails::DBConsole.new.send(:parse_arguments, ['dev']) - assert_match('development', options[:environment]) + dbconsole = Rails::DBConsole.new + + dbconsole.stub(:available_environments, ['development', 'test']) do + options = dbconsole.send(:parse_arguments, ['dev']) + assert_match('development', options[:environment]) + end end def test_rails_env_is_dev_when_argument_is_dev_and_dev_env_is_present - Rails::DBConsole.stubs(:available_environments).returns(['dev']) - options = Rails::DBConsole.new.send(:parse_arguments, ['dev']) - assert_match('dev', options[:environment]) + dbconsole = Rails::DBConsole.new + + dbconsole.stub(:available_environments, ['dev']) do + options = dbconsole.send(:parse_arguments, ['dev']) + assert_match('dev', options[:environment]) + end end def test_mysql - dbconsole.expects(:find_cmd_and_exec).with(%w[mysql mysql5], 'db') start(adapter: 'mysql', database: 'db') assert !aborted + assert_equal [%w[mysql mysql5], 'db'], dbconsole.find_cmd_and_exec_args end def test_mysql_full - dbconsole.expects(:find_cmd_and_exec).with(%w[mysql mysql5], '--host=locahost', '--port=1234', '--socket=socket', '--user=user', '--default-character-set=UTF-8', '-p', 'db') start(adapter: 'mysql', database: 'db', host: 'locahost', port: 1234, socket: 'socket', username: 'user', password: 'qwerty', encoding: 'UTF-8') assert !aborted + assert_equal [%w[mysql mysql5], '--host=locahost', '--port=1234', '--socket=socket', '--user=user', '--default-character-set=UTF-8', '-p', 'db'], dbconsole.find_cmd_and_exec_args end def test_mysql_include_password - dbconsole.expects(:find_cmd_and_exec).with(%w[mysql mysql5], '--user=user', '--password=qwerty', 'db') start({adapter: 'mysql', database: 'db', username: 'user', password: 'qwerty'}, ['-p']) assert !aborted + assert_equal [%w[mysql mysql5], '--user=user', '--password=qwerty', 'db'], dbconsole.find_cmd_and_exec_args end def test_postgresql - dbconsole.expects(:find_cmd_and_exec).with('psql', 'db') start(adapter: 'postgresql', database: 'db') assert !aborted + assert_equal ['psql', 'db'], dbconsole.find_cmd_and_exec_args end def test_postgresql_full - dbconsole.expects(:find_cmd_and_exec).with('psql', 'db') start(adapter: 'postgresql', database: 'db', username: 'user', password: 'q1w2e3', host: 'host', port: 5432) assert !aborted + assert_equal ['psql', 'db'], dbconsole.find_cmd_and_exec_args assert_equal 'user', ENV['PGUSER'] assert_equal 'host', ENV['PGHOST'] assert_equal '5432', ENV['PGPORT'] @@ -137,60 +150,60 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase end def test_postgresql_include_password - dbconsole.expects(:find_cmd_and_exec).with('psql', 'db') start({adapter: 'postgresql', database: 'db', username: 'user', password: 'q1w2e3'}, ['-p']) assert !aborted + assert_equal ['psql', 'db'], dbconsole.find_cmd_and_exec_args assert_equal 'user', ENV['PGUSER'] assert_equal 'q1w2e3', ENV['PGPASSWORD'] end def test_sqlite - dbconsole.expects(:find_cmd_and_exec).with('sqlite', 'db') start(adapter: 'sqlite', database: 'db') assert !aborted + assert_equal ['sqlite', 'db'], dbconsole.find_cmd_and_exec_args end def test_sqlite3 - dbconsole.expects(:find_cmd_and_exec).with('sqlite3', Rails.root.join('db.sqlite3').to_s) start(adapter: 'sqlite3', database: 'db.sqlite3') assert !aborted + assert_equal ['sqlite3', Rails.root.join('db.sqlite3').to_s], dbconsole.find_cmd_and_exec_args end def test_sqlite3_mode - dbconsole.expects(:find_cmd_and_exec).with('sqlite3', '-html', Rails.root.join('db.sqlite3').to_s) start({adapter: 'sqlite3', database: 'db.sqlite3'}, ['--mode', 'html']) assert !aborted + assert_equal ['sqlite3', '-html', Rails.root.join('db.sqlite3').to_s], dbconsole.find_cmd_and_exec_args end def test_sqlite3_header - dbconsole.expects(:find_cmd_and_exec).with('sqlite3', '-header', Rails.root.join('db.sqlite3').to_s) start({adapter: 'sqlite3', database: 'db.sqlite3'}, ['--header']) + assert_equal ['sqlite3', '-header', Rails.root.join('db.sqlite3').to_s], dbconsole.find_cmd_and_exec_args end def test_sqlite3_db_absolute_path - dbconsole.expects(:find_cmd_and_exec).with('sqlite3', '/tmp/db.sqlite3') start(adapter: 'sqlite3', database: '/tmp/db.sqlite3') assert !aborted + assert_equal ['sqlite3', '/tmp/db.sqlite3'], dbconsole.find_cmd_and_exec_args end def test_sqlite3_db_without_defined_rails_root - Rails.stubs(:respond_to?) - Rails.expects(:respond_to?).with(:root).once.returns(false) - dbconsole.expects(:find_cmd_and_exec).with('sqlite3', Rails.root.join('../config/db.sqlite3').to_s) - start(adapter: 'sqlite3', database: 'config/db.sqlite3') - assert !aborted + Rails.stub(:respond_to?, false) do + start(adapter: 'sqlite3', database: 'config/db.sqlite3') + assert !aborted + assert_equal ['sqlite3', Rails.root.join('../config/db.sqlite3').to_s], dbconsole.find_cmd_and_exec_args + end end def test_oracle - dbconsole.expects(:find_cmd_and_exec).with('sqlplus', 'user@db') start(adapter: 'oracle', database: 'db', username: 'user', password: 'secret') assert !aborted + assert_equal ['sqlplus', 'user@db'], dbconsole.find_cmd_and_exec_args end def test_oracle_include_password - dbconsole.expects(:find_cmd_and_exec).with('sqlplus', 'user/secret@db') start({adapter: 'oracle', database: 'db', username: 'user', password: 'secret'}, ['-p']) assert !aborted + assert_equal ['sqlplus', 'user/secret@db'], dbconsole.find_cmd_and_exec_args end def test_unknown_command_line_client @@ -223,16 +236,27 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase private def app_db_config(results) - Rails.application.config.stubs(:database_configuration).returns(results || {}) + Rails.application.config.stub(:database_configuration, results || {}) do + yield + end end def dbconsole - @dbconsole ||= Rails::DBConsole.new(nil) + @dbconsole ||= Class.new(Rails::DBConsole) do + attr_reader :find_cmd_and_exec_args + + def find_cmd_and_exec(*args) + @find_cmd_and_exec_args = args + end + end.new(nil) end def start(config = {}, argv = []) - dbconsole.stubs(config: config.stringify_keys, arguments: argv) - capture_abort { dbconsole.start } + dbconsole.stub(:config, config.stringify_keys) do + dbconsole.stub(:arguments, argv) do + capture_abort { dbconsole.start } + end + end end def capture_abort diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb index 0db40c1d32..a4337926d1 100644 --- a/railties/test/generators/actions_test.rb +++ b/railties/test/generators/actions_test.rb @@ -1,6 +1,7 @@ require 'generators/generators_test_helper' require 'rails/generators/rails/app/app_generator' require 'env_helpers' +require 'mocha/setup' # FIXME: stop using mocha class ActionsTest < Rails::Generators::TestCase include GeneratorsTestHelper @@ -41,13 +42,13 @@ class ActionsTest < Rails::Generators::TestCase def test_add_source_adds_source_to_gemfile run_generator action :add_source, 'http://gems.github.com' - assert_file 'Gemfile', /source "http:\/\/gems\.github\.com"/ + assert_file 'Gemfile', /source 'http:\/\/gems\.github\.com'/ end def test_gem_should_put_gem_dependency_in_gemfile run_generator action :gem, 'will-paginate' - assert_file 'Gemfile', /gem "will\-paginate"/ + assert_file 'Gemfile', /gem 'will\-paginate'/ end def test_gem_with_version_should_include_version_in_gemfile @@ -55,7 +56,7 @@ class ActionsTest < Rails::Generators::TestCase action :gem, 'rspec', '>=2.0.0.a5' - assert_file 'Gemfile', /gem "rspec", ">=2.0.0.a5"/ + assert_file 'Gemfile', /gem 'rspec', '>=2.0.0.a5'/ end def test_gem_should_insert_on_separate_lines @@ -66,8 +67,8 @@ class ActionsTest < Rails::Generators::TestCase action :gem, 'rspec' action :gem, 'rspec-rails' - assert_file 'Gemfile', /^gem "rspec"$/ - assert_file 'Gemfile', /^gem "rspec-rails"$/ + assert_file 'Gemfile', /^gem 'rspec'$/ + assert_file 'Gemfile', /^gem 'rspec-rails'$/ end def test_gem_should_include_options @@ -75,7 +76,15 @@ class ActionsTest < Rails::Generators::TestCase action :gem, 'rspec', github: 'dchelimsky/rspec', tag: '1.2.9.rc1' - assert_file 'Gemfile', /gem "rspec", github: "dchelimsky\/rspec", tag: "1\.2\.9\.rc1"/ + assert_file 'Gemfile', /gem 'rspec', github: 'dchelimsky\/rspec', tag: '1\.2\.9\.rc1'/ + end + + def test_gem_falls_back_to_inspect_if_string_contains_single_quote + run_generator + + action :gem, 'rspec', ">=2.0'0" + + assert_file 'Gemfile', /^gem 'rspec', ">=2\.0'0"$/ end def test_gem_group_should_wrap_gems_in_a_group @@ -89,7 +98,7 @@ class ActionsTest < Rails::Generators::TestCase gem 'fakeweb' end - assert_file 'Gemfile', /\ngroup :development, :test do\n gem "rspec-rails"\nend\n\ngroup :test do\n gem "fakeweb"\nend/ + assert_file 'Gemfile', /\ngroup :development, :test do\n gem 'rspec-rails'\nend\n\ngroup :test do\n gem 'fakeweb'\nend/ end def test_environment_should_include_data_in_environment_initializer_block @@ -234,7 +243,7 @@ class ActionsTest < Rails::Generators::TestCase protected def action(*args, &block) - silence(:stdout){ generator.send(*args, &block) } + capture(:stdout){ generator.send(*args, &block) } end end diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 007dd886da..aff484f3eb 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -1,6 +1,7 @@ require 'generators/generators_test_helper' require 'rails/generators/rails/app/app_generator' require 'generators/shared_generator_tests' +require 'mocha/setup' # FIXME: stop using mocha DEFAULT_APP_FILES = %w( .gitignore @@ -21,6 +22,7 @@ DEFAULT_APP_FILES = %w( bin/bundle bin/rails bin/rake + bin/setup config/environments config/initializers config/locales @@ -119,7 +121,7 @@ class AppGeneratorTest < Rails::Generators::TestCase generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_moved_root, shell: @shell generator.send(:app_const) - quietly { generator.send(:create_config_files) } + quietly { generator.send(:update_config_files) } assert_file "myapp_moved/config/environment.rb", /Rails\.application\.initialize!/ assert_file "myapp_moved/config/initializers/session_store.rb", /_myapp_session/ end @@ -134,10 +136,46 @@ class AppGeneratorTest < Rails::Generators::TestCase generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell generator.send(:app_const) - quietly { generator.send(:create_config_files) } + quietly { generator.send(:update_config_files) } assert_file "myapp/config/initializers/session_store.rb", /_myapp_session/ end + def test_new_application_use_json_serialzier + run_generator + + assert_file("config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :json/) + end + + def test_rails_update_keep_the_cookie_serializer_if_it_is_already_configured + app_root = File.join(destination_root, 'myapp') + run_generator [app_root] + + Rails.application.config.root = app_root + Rails.application.class.stubs(:name).returns("Myapp") + Rails.application.stubs(:is_a?).returns(Rails::Application) + + generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_file("#{app_root}/config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :json/) + end + + def test_rails_update_set_the_cookie_serializer_to_marchal_if_it_is_not_already_configured + app_root = File.join(destination_root, 'myapp') + run_generator [app_root] + + FileUtils.rm("#{app_root}/config/initializers/cookies_serializer.rb") + + Rails.application.config.root = app_root + Rails.application.class.stubs(:name).returns("Myapp") + Rails.application.stubs(:is_a?).returns(Rails::Application) + + generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_file("#{app_root}/config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :marshal/) + end + def test_application_names_are_not_singularized run_generator [File.join(destination_root, "hats")] assert_file "hats/config/environment.rb", /Rails\.application\.initialize!/ @@ -411,6 +449,21 @@ class AppGeneratorTest < Rails::Generators::TestCase end end + def test_generator_if_skip_gems_is_given + run_generator [destination_root, "--skip-gems", "turbolinks", "coffee-rails"] + + assert_file "Gemfile" do |content| + assert_no_match(/turbolinks/, content) + assert_no_match(/coffee-rails/, content) + end + assert_file "app/views/layouts/application.html.erb" do |content| + assert_no_match(/data-turbolinks-track/, content) + end + assert_file "app/assets/javascripts/application.js" do |content| + assert_no_match(/turbolinks/, content) + end + end + def test_gitignore_when_sqlite3 run_generator @@ -435,10 +488,23 @@ class AppGeneratorTest < Rails::Generators::TestCase end end + def test_psych_gem + run_generator + gem_regex = /gem 'psych',\s+'~> 2.0', \s+platforms: :rbx/ + + assert_file "Gemfile" do |content| + if defined?(Rubinius) + assert_match(gem_regex, content) + else + assert_no_match(gem_regex, content) + end + end + end + protected def action(*args, &block) - silence(:stdout) { generator.send(*args, &block) } + capture(:stdout) { generator.send(*args, &block) } end def assert_gem(gem) diff --git a/railties/test/generators/argv_scrubber_test.rb b/railties/test/generators/argv_scrubber_test.rb index a94350cbd7..31c2d846e2 100644 --- a/railties/test/generators/argv_scrubber_test.rb +++ b/railties/test/generators/argv_scrubber_test.rb @@ -1,5 +1,5 @@ -require 'active_support/test_case' require 'active_support/testing/autorun' +require 'active_support/test_case' require 'rails/generators/rails/app/app_generator' require 'tempfile' @@ -16,7 +16,7 @@ module Rails output = nil exit_code = nil scrubber.extend(Module.new { - define_method(:puts) { |str| output = str } + define_method(:puts) { |string| output = string } define_method(:exit) { |code| exit_code = code } }) scrubber.prepare! diff --git a/railties/test/generators/controller_generator_test.rb b/railties/test/generators/controller_generator_test.rb index 4b2f8539d0..28b527cb0e 100644 --- a/railties/test/generators/controller_generator_test.rb +++ b/railties/test/generators/controller_generator_test.rb @@ -70,6 +70,13 @@ class ControllerGeneratorTest < Rails::Generators::TestCase assert_file "config/routes.rb", /get 'account\/foo'/, /get 'account\/bar'/ end + def test_skip_routes + run_generator ["account", "foo", "--skip-routes"] + assert_file "config/routes.rb" do |routes| + assert_no_match(/get 'account\/foo'/, routes) + end + end + def test_invokes_default_template_engine_even_with_no_action run_generator ["account"] assert_file "app/views/account" diff --git a/railties/test/generators/generator_test.rb b/railties/test/generators/generator_test.rb index 7871399dd7..b136239795 100644 --- a/railties/test/generators/generator_test.rb +++ b/railties/test/generators/generator_test.rb @@ -1,5 +1,5 @@ -require 'active_support/test_case' require 'active_support/testing/autorun' +require 'active_support/test_case' require 'rails/generators/app_base' module Rails diff --git a/railties/test/generators/generators_test_helper.rb b/railties/test/generators/generators_test_helper.rb index 77ec2f1c0c..de1e56e7b3 100644 --- a/railties/test/generators/generators_test_helper.rb +++ b/railties/test/generators/generators_test_helper.rb @@ -41,4 +41,12 @@ module GeneratorsTestHelper FileUtils.mkdir_p(destination) FileUtils.cp routes, destination end + + def quietly + silence_stream(STDOUT) do + silence_stream(STDERR) do + yield + end + end + end end diff --git a/railties/test/generators/named_base_test.rb b/railties/test/generators/named_base_test.rb index ac5cfff229..4199e00b0d 100644 --- a/railties/test/generators/named_base_test.rb +++ b/railties/test/generators/named_base_test.rb @@ -1,5 +1,6 @@ require 'generators/generators_test_helper' require 'rails/generators/rails/scaffold_controller/scaffold_controller_generator' +require 'mocha/setup' # FIXME: stop using mocha # Mock out what we need from AR::Base. module ActiveRecord diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index 853af80111..0d01931daa 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -1,6 +1,7 @@ require 'generators/generators_test_helper' require 'rails/generators/rails/plugin/plugin_generator' require 'generators/shared_generator_tests' +require 'mocha/setup' # FIXME: stop using mocha DEFAULT_PLUGIN_FILES = %w( .gitignore @@ -312,7 +313,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_no_file "bukkits.gemspec" assert_file "Gemfile" do |contents| assert_no_match('gemspec', contents) - assert_match(/gem "rails", "~> #{Rails.version}"/, contents) + assert_match(/gem 'rails', '~> #{Rails.version}'/, contents) assert_match_sqlite3(contents) assert_no_match(/# gem "jquery-rails"/, contents) end @@ -323,7 +324,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_no_file "bukkits.gemspec" assert_file "Gemfile" do |contents| assert_no_match('gemspec', contents) - assert_match(/gem "rails", "~> #{Rails.version}"/, contents) + assert_match(/gem 'rails', '~> #{Rails.version}'/, contents) assert_match_sqlite3(contents) end end @@ -371,6 +372,40 @@ class PluginGeneratorTest < Rails::Generators::TestCase end end + def test_git_name_and_email_in_gemspec_file + name = `git config user.name`.chomp rescue "TODO: Write your name" + email = `git config user.email`.chomp rescue "TODO: Write your email address" + + run_generator [destination_root] + assert_file "bukkits.gemspec" do |contents| + assert_match(/#{Regexp.escape(name)}/, contents) + assert_match(/#{Regexp.escape(email)}/, contents) + end + end + + def test_git_name_in_license_file + name = `git config user.name`.chomp rescue "TODO: Write your name" + + run_generator [destination_root] + assert_file "MIT-LICENSE" do |contents| + assert_match(/#{Regexp.escape(name)}/, contents) + end + end + + def test_no_details_from_git_when_skip_git + name = "TODO: Write your name" + email = "TODO: Write your email address" + + run_generator [destination_root, '--skip-git'] + assert_file "MIT-LICENSE" do |contents| + assert_match(/#{Regexp.escape(name)}/, contents) + end + assert_file "bukkits.gemspec" do |contents| + assert_match(/#{Regexp.escape(name)}/, contents) + assert_match(/#{Regexp.escape(email)}/, contents) + end + end + protected def action(*args, &block) silence(:stdout){ generator.send(*args, &block) } @@ -382,9 +417,9 @@ protected def assert_match_sqlite3(contents) unless defined?(JRUBY_VERSION) - assert_match(/group :development do\n gem "sqlite3"\nend/, contents) + assert_match(/group :development do\n gem 'sqlite3'\nend/, contents) else - assert_match(/group :development do\n gem "activerecord-jdbcsqlite3-adapter"\nend/, contents) + 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 3c1123b53d..46eacd2845 100644 --- a/railties/test/generators/scaffold_controller_generator_test.rb +++ b/railties/test/generators/scaffold_controller_generator_test.rb @@ -93,6 +93,14 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase assert_no_file "app/views/layouts/users.html.erb" end + def test_index_page_have_notice + run_generator + + %w(index show).each do |view| + assert_file "app/views/users/#{view}.html.erb", /notice/ + end + end + def test_functional_tests run_generator ["User", "name:string", "age:integer", "organization:references{polymorphic}"] diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb index 8e198d5fe1..b998fef42e 100644 --- a/railties/test/generators/shared_generator_tests.rb +++ b/railties/test/generators/shared_generator_tests.rb @@ -78,9 +78,12 @@ module SharedGeneratorTests end def test_template_raises_an_error_with_invalid_path - content = capture(:stderr){ run_generator([destination_root, "-m", "non/existent/path"]) } - assert_match(/The template \[.*\] could not be loaded/, content) - assert_match(/non\/existent\/path/, content) + quietly do + content = capture(:stderr){ run_generator([destination_root, "-m", "non/existent/path"]) } + + assert_match(/The template \[.*\] could not be loaded/, content) + assert_match(/non\/existent\/path/, content) + end end def test_template_is_executed_when_supplied @@ -89,7 +92,7 @@ module SharedGeneratorTests template.instance_eval "def read; self; end" # Make the string respond to read generator([destination_root], template: path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template) - assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) + quietly { assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) } end def test_template_is_executed_when_supplied_an_https_path @@ -98,7 +101,7 @@ module SharedGeneratorTests template.instance_eval "def read; self; end" # Make the string respond to read generator([destination_root], template: path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template) - assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) + quietly { assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) } end def test_dev_option diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb index eac28badfe..b5765c391e 100644 --- a/railties/test/generators_test.rb +++ b/railties/test/generators_test.rb @@ -1,6 +1,7 @@ require 'generators/generators_test_helper' require 'rails/generators/rails/model/model_generator' require 'rails/generators/test_unit/model/model_generator' +require 'mocha/setup' # FIXME: stop using mocha class GeneratorsTest < Rails::Generators::TestCase include GeneratorsTestHelper @@ -21,8 +22,16 @@ class GeneratorsTest < Rails::Generators::TestCase end def test_invoke_when_generator_is_not_found - output = capture(:stdout){ Rails::Generators.invoke :unknown } - assert_equal "Could not find generator unknown.\n", output + name = :unknown + output = capture(:stdout){ Rails::Generators.invoke name } + assert_match "Could not find generator '#{name}'", output + assert_match "`rails generate --help`", output + end + + def test_generator_suggestions + name = :migrationz + output = capture(:stdout){ Rails::Generators.invoke name } + assert_match "Maybe you meant 'migration'", output end def test_help_when_a_generator_with_required_arguments_is_invoked_without_arguments diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 6c50911666..92d6a1729c 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -291,6 +291,33 @@ class ActiveSupport::TestCase include TestHelpers::Paths include TestHelpers::Rack include TestHelpers::Generation + + private + + def capture(stream) + stream = stream.to_s + captured_stream = Tempfile.new(stream) + stream_io = eval("$#{stream}") + origin_stream = stream_io.dup + stream_io.reopen(captured_stream) + + yield + + stream_io.rewind + return captured_stream.read + ensure + captured_stream.close + captured_stream.unlink + stream_io.reopen(origin_stream) + end + + def quietly + silence_stream(STDOUT) do + silence_stream(STDERR) do + yield + end + end + end end # Create a scope and build a fixture rails app diff --git a/railties/test/paths_test.rb b/railties/test/paths_test.rb index ed4559ec6f..1aeb9ec339 100644 --- a/railties/test/paths_test.rb +++ b/railties/test/paths_test.rb @@ -1,5 +1,6 @@ require 'abstract_unit' require 'rails/paths' +require 'mocha/setup' # FIXME: stop using mocha class PathsTest < ActiveSupport::TestCase def setup diff --git a/railties/test/rack_logger_test.rb b/railties/test/rack_logger_test.rb index 6ebd47fff9..fcc79b57fb 100644 --- a/railties/test/rack_logger_test.rb +++ b/railties/test/rack_logger_test.rb @@ -39,11 +39,11 @@ module Rails def setup @subscriber = Subscriber.new @notifier = ActiveSupport::Notifications.notifier - notifier.subscribe 'request.action_dispatch', subscriber + @subscription = notifier.subscribe 'request.action_dispatch', subscriber end def teardown - notifier.unsubscribe subscriber + notifier.unsubscribe @subscription end def test_notification diff --git a/railties/test/rails_info_controller_test.rb b/railties/test/rails_info_controller_test.rb index a9b237d0a5..8d61af4972 100644 --- a/railties/test/rails_info_controller_test.rb +++ b/railties/test/rails_info_controller_test.rb @@ -1,4 +1,5 @@ require 'abstract_unit' +require 'mocha/setup' # FIXME: stop using mocha module ActionController class Base diff --git a/railties/test/rails_info_test.rb b/railties/test/rails_info_test.rb index 44a5fd1904..4bec302ff8 100644 --- a/railties/test/rails_info_test.rb +++ b/railties/test/rails_info_test.rb @@ -66,16 +66,6 @@ class InfoTest < ActiveSupport::TestCase end protected - def svn_info=(info) - Rails::Info.module_eval do - class << self - def svn_info - info - end - end - end - end - def properties Rails::Info.properties end diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb index c4b18e9ea5..ec64ce5941 100644 --- a/railties/test/railties/engine_test.rb +++ b/railties/test/railties/engine_test.rb @@ -34,6 +34,7 @@ module RailtiesTest test "serving sprocket's assets" do @plugin.write "app/assets/javascripts/engine.js.erb", "<%= :alert %>();" + add_to_env_config "development", "config.assets.digest = false" boot_rails require 'rack/test' @@ -111,6 +112,38 @@ module RailtiesTest end end + test 'respects the order of railties when installing migrations' do + @blog = engine "blog" do |plugin| + plugin.write "lib/blog.rb", <<-RUBY + module Blog + class Engine < ::Rails::Engine + end + end + RUBY + end + + @plugin.write "db/migrate/1_create_users.rb", <<-RUBY + class CreateUsers < ActiveRecord::Migration + end + RUBY + + @blog.write "db/migrate/2_create_blogs.rb", <<-RUBY + class CreateBlogs < ActiveRecord::Migration + end + RUBY + + add_to_config("config.railties_order = [Bukkits::Engine, Blog::Engine, :all, :main_app]") + + boot_rails + + Dir.chdir(app_path) do + output = `bundle exec rake railties:install:migrations`.split("\n") + + assert_match(/Copied migration \d+_create_users.bukkits.rb from bukkits/, output.first) + assert_match(/Copied migration \d+_create_blogs.blog_engine.rb from blog_engine/, output.last) + end + end + test "mountable engine should copy migrations within engine_path" do @plugin.write "lib/bukkits.rb", <<-RUBY module Bukkits @@ -592,11 +625,15 @@ YAML @plugin.write "app/models/bukkits/post.rb", <<-RUBY module Bukkits class Post - extend ActiveModel::Naming + include ActiveModel::Model def to_param "1" end + + def persisted? + true + end end end RUBY @@ -704,8 +741,7 @@ YAML @plugin.write "app/models/bukkits/post.rb", <<-RUBY module Bukkits class Post - extend ActiveModel::Naming - include ActiveModel::Conversion + include ActiveModel::Model attr_accessor :title def to_param @@ -1077,6 +1113,7 @@ YAML RUBY add_to_config("config.railties_order = [:all, :main_app, Blog::Engine]") + add_to_env_config "development", "config.assets.digest = false" boot_rails diff --git a/railties/test/railties/mounted_engine_test.rb b/railties/test/railties/mounted_engine_test.rb index 0ef2ff2e2e..fb2071c7c3 100644 --- a/railties/test/railties/mounted_engine_test.rb +++ b/railties/test/railties/mounted_engine_test.rb @@ -88,18 +88,14 @@ module ApplicationTests @plugin.write "app/models/blog/post.rb", <<-RUBY module Blog class Post - extend ActiveModel::Naming + include ActiveModel::Model def id 44 end - def to_param - id.to_s - end - - def new_record? - false + def persisted? + true end end end diff --git a/railties/test/railties/railtie_test.rb b/railties/test/railties/railtie_test.rb index a458240d2f..5042d628cf 100644 --- a/railties/test/railties/railtie_test.rb +++ b/railties/test/railties/railtie_test.rb @@ -73,7 +73,7 @@ module RailtiesTest end test "railtie have access to application in before_configuration callbacks" do - $after_initialize = false + $before_configuration = false class Foo < Rails::Railtie ; config.before_configuration { $before_configuration = Rails.root.to_path } ; end assert_not $before_configuration require "#{app_path}/config/environment" |