diff options
528 files changed, 11700 insertions, 6402 deletions
diff --git a/.travis.yml b/.travis.yml index 16086e2663..e5aaf57f93 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,3 @@ -services: memcache script: 'ci/travis.rb' before_install: - travis_retry gem install bundler @@ -6,7 +5,8 @@ before_install: rvm: - 1.9.3 - 2.0.0 - - 2.1.1 + - 2.1 + - ruby-head - rbx-2 - jruby env: @@ -20,6 +20,7 @@ matrix: allow_failures: - rvm: rbx-2 - rvm: jruby + - rvm: ruby-head fast_finish: true notifications: email: false @@ -11,7 +11,9 @@ gem 'rack-cache', '~> 1.2' gem 'jquery-rails', '~> 3.1.0' gem 'turbolinks' gem 'coffee-rails', '~> 4.0.0' -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) @@ -24,7 +26,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 @@ -37,6 +39,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 @@ -84,7 +89,7 @@ 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 diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 951a3e5fb5..5852aeaec2 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -154,7 +154,7 @@ module ActionMailer # * signup_notification.text.erb # * signup_notification.html.erb # * signup_notification.xml.builder - # * signup_notification.yaml.erb + # * signup_notification.yml.erb # # Each would be rendered and added as a separate part to the message, with the corresponding content # type. The content type for the entire message is automatically set to <tt>multipart/alternative</tt>, @@ -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 # @@ -325,7 +326,7 @@ module ActionMailer # directory can be configured using the <tt>preview_path</tt> option which has a default # of <tt>test/mailers/previews</tt>: # - # config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews" + # config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews" # # An overview of all previews is accessible at <tt>http://localhost:3000/rails/mailers</tt> # on a running development server instance. 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/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..16e8638542 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,18 +93,16 @@ 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 @@ -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/i18n_with_controller_test.rb b/actionmailer/test/i18n_with_controller_test.rb index 14a1b11b6d..d502d42ffd 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 @@ -32,16 +32,19 @@ class ActionMailerI18nWithControllerTest < ActionDispatch::IntegrationTest Routes end - def setup - I18n.backend.store_translations('de', email_subject: '[Signed up] Welcome') + def test_send_mail + with_translation 'de', email_subject: '[Anmeldung] Willkommen' do + get '/test/send_mail' + assert_equal "Mail sent - Subject: [Anmeldung] Willkommen", @response.body + end end - def teardown - I18n.locale = :en - end + protected - def test_send_mail - get '/test/send_mail' - assert_equal "Mail sent", @response.body + 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/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 513f2e66fb..7da58ca4e7 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,105 @@ +* Deprecate all *_filter callbacks in favor of *_action callbacks. + + *Rafael Mendonça França* + +* 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 + cookies are marshal-encoded. This is not the case when `secret_token` is + used in conjunction with the `:json` or `:hybrid` serializer. + + In those case, when upgrading to use `secret_key_base`, this would cause a + `TypeError: incompatible marshal file format` and a 500 error for the user. + + Fixes #14774. + + *Godfrey Chan* + +* Make URL escaping more consistent: + + 1. Escape '%' characters in URLs - only unescaped data should be passed to URL helpers + 2. Add an `escape_segment` helper to `Router::Utils` that escapes '/' characters + 3. Use `escape_segment` rather than `escape_fragment` in optimized URL generation + 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 + 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. + + Fixes #14629, #14636 and #14070. + + *Andrew White*, *Edho Arief* + +* Add alias `ActionDispatch::Http::UploadedFile#to_io` to + `ActionDispatch::Http::UploadedFile#tempfile`. + + *Tim Linquist* + +* Returns null type format when format is not know and controller is using `any` + format block. + + Fixes #14462. + + *Rafael Mendonça França* + * Improve routing error page with fuzzy matching search. *Winston* @@ -8,7 +110,7 @@ *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* @@ -38,4 +140,5 @@ *Tony Wooster* + Please check [4-1-stable](https://github.com/rails/rails/blob/4-1-stable/actionpack/CHANGELOG.md) for previous changes. diff --git a/actionpack/RUNNING_UNIT_TESTS.rdoc b/actionpack/RUNNING_UNIT_TESTS.rdoc index ad1448f61b..2f923136d9 100644 --- a/actionpack/RUNNING_UNIT_TESTS.rdoc +++ b/actionpack/RUNNING_UNIT_TESTS.rdoc @@ -8,10 +8,10 @@ Rake can be found at http://rake.rubyforge.org. == Running by hand -To run a single test suite +Run a single test suite: rake test TEST=path/to/test.rb -which can be further narrowed down to one test: +Run one test in a test suite: rake test TEST=path/to/test.rb TESTOPTS="--name=test_something" diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index af5de815bb..acdfb33efa 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 !~ Regexp.new(File::SEPARATOR) + end end end diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index d6c941832f..252e297c69 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -1,3 +1,5 @@ +require 'active_support/deprecation' + module AbstractController module Callbacks extend ActiveSupport::Concern @@ -42,21 +44,23 @@ 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 + def skip_filter(*names) + ActiveSupport::Deprecation.warn("#{callback}_filter is deprecated and will removed in Rails 5. Use #{callback}_action instead.") + skip_action_callback(*names) + end # Take callback names and an optional callback proc, normalize them, # then call the block with each callback. This allows us to abstract @@ -85,7 +89,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 +96,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 +103,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 +110,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 +117,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 +124,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 +131,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 +138,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 +145,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 +152,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 +159,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,46 +166,51 @@ 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. [:before, :after, :around].each do |callback| - class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 - # Append a before, after or around callback. See _insert_callbacks - # for details on the allowed parameters. - def #{callback}_action(*names, &blk) # def before_action(*names, &blk) - _insert_callbacks(names, blk) do |name, options| # _insert_callbacks(names, blk) do |name, options| - set_callback(:process_action, :#{callback}, name, options) # set_callback(:process_action, :before, name, options) - end # end - end # end - - alias_method :#{callback}_filter, :#{callback}_action - - # Prepend a before, after or around callback. See _insert_callbacks - # for details on the allowed parameters. - def prepend_#{callback}_action(*names, &blk) # def prepend_before_action(*names, &blk) - _insert_callbacks(names, blk) do |name, options| # _insert_callbacks(names, blk) do |name, options| - set_callback(:process_action, :#{callback}, name, options.merge(:prepend => true)) # set_callback(:process_action, :before, name, options.merge(:prepend => true)) - end # end - end # end - - alias_method :prepend_#{callback}_filter, :prepend_#{callback}_action - - # Skip a before, after or around callback. See _insert_callbacks - # for details on the allowed parameters. - def skip_#{callback}_action(*names) # def skip_before_action(*names) - _insert_callbacks(names) do |name, options| # _insert_callbacks(names) do |name, options| - skip_callback(:process_action, :#{callback}, name, options) # skip_callback(:process_action, :before, name, options) - end # end - 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 - RUBY_EVAL + define_method "#{callback}_action" do |*names, &blk| + _insert_callbacks(names, blk) do |name, options| + set_callback(:process_action, callback, name, options) + end + end + + define_method "#{callback}_filter" do |*names, &blk| + ActiveSupport::Deprecation.warn("#{callback}_filter is deprecated and will removed in Rails 5. Use #{callback}_action instead.") + send("#{callback}_action", *names, &blk) + end + + define_method "prepend_#{callback}_action" do |*names, &blk| + _insert_callbacks(names, blk) do |name, options| + set_callback(:process_action, callback, name, options.merge(:prepend => true)) + end + end + + define_method "prepend_#{callback}_filter" do |*names, &blk| + ActiveSupport::Deprecation.warn("prepend_#{callback}_filter is deprecated and will removed in Rails 5. Use prepend_#{callback}_action instead.") + send("prepend_#{callback}_action", *names, &blk) + end + + # Skip a before, after or around callback. See _insert_callbacks + # for details on the allowed parameters. + define_method "skip_#{callback}_action" do |*names| + _insert_callbacks(names) do |name, options| + skip_callback(:process_action, callback, name, options) + end + end + + define_method "skip_#{callback}_filter" do |*names, &blk| + ActiveSupport::Deprecation.warn("skip_#{callback}_filter is deprecated and will removed in Rails 5. Use skip_#{callback}_action instead.") + send("skip_#{callback}_action", *names, &blk) + end + + # *_action is the same as append_*_action + alias_method :"append_#{callback}_action", :"#{callback}_action" # alias_method :append_before_action, :before_action + define_method "append_#{callback}_filter" do |*names, &blk| + ActiveSupport::Deprecation.warn("append_#{callback}_filter is deprecated and will removed in Rails 5. Use append_#{callback}_action instead.") + send("append_#{callback}_action", *names, &blk) + end 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/metal.rb b/actionpack/lib/action_controller/metal.rb index b84c9e78c3..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 @@ -70,7 +68,8 @@ module ActionController # can do the following: # # class HelloController < ActionController::Metal - # include ActionController::Rendering + # include AbstractController::Rendering + # include ActionView::Layouts # append_view_path "#{Rails.root}/app/views" # # def index @@ -222,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..84a9112144 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -27,7 +27,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..3111992f82 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) diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index acf40b2e16..4c0554d27b 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -102,7 +102,8 @@ module ActionController end end - @stream.write "data: #{json}\n\n" + message = json.gsub("\n", "\ndata: ") + @stream.write "data: #{message}\n\n" end end 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..136e086d0d 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,8 +62,10 @@ 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 AbstractController::DoubleRenderError if response_body diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 6c7b4652d4..46405cef55 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}" @@ -42,8 +47,8 @@ module ActionController nil end - # Hash of available renderers, mapping a renderer name to its proc. - # Default keys are <tt>:json</tt>, <tt>:js</tt>, <tt>:xml</tt>. + # A Set containing renderer names that correspond to available renderer procs. + # Default values are <tt>:json</tt>, <tt>:js</tt>, <tt>:xml</tt>. RENDERERS = Set.new # Adds a new renderer to call within controller actions. @@ -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 diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index e3b1f5ae7c..1355fe87d0 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -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/responder.rb b/actionpack/lib/action_controller/metal/responder.rb index e24b56fa91..5096558c67 100644 --- a/actionpack/lib/action_controller/metal/responder.rb +++ b/actionpack/lib/action_controller/metal/responder.rb @@ -22,7 +22,7 @@ module ActionController #:nodoc: # # 3) if the responder does not <code>respond_to :to_xml</code>, call <code>#to_format</code> on it. # - # === Builtin HTTP verb semantics + # === Built-in HTTP verb semantics # # The default \Rails responder holds semantics for each HTTP verb. Depending on the # content type, verb and the resource status, it will behave differently. diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index 754249cbc8..07265be3fe 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -23,16 +23,16 @@ 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"])) || + if (same_origin = _routes.equal?(env["action_dispatch.routes".freeze])) || (script_name = env["ROUTES_#{_routes.object_id}_SCRIPT_NAME"]) || - (original_script_name = env['ORIGINAL_SCRIPT_NAME']) + (original_script_name = env['ORIGINAL_SCRIPT_NAME'.freeze]) @_url_options.dup.tap do |options| if original_script_name diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index df57efaa97..e6695ffc90 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -17,8 +17,9 @@ module ActionController @_templates = Hash.new(0) @_layouts = Hash.new(0) @_files = Hash.new(0) + @_subscribers = [] - ActiveSupport::Notifications.subscribe("render_template.action_view") do |_name, _start, _finish, _id, payload| + @_subscribers << ActiveSupport::Notifications.subscribe("render_template.action_view") do |_name, _start, _finish, _id, payload| path = payload[:layout] if path @_layouts[path] += 1 @@ -28,7 +29,7 @@ module ActionController end end - ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload| + @_subscribers << ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload| path = payload[:virtual_path] next unless path partial = path =~ /^.*\/_[^\/]*$/ @@ -41,7 +42,7 @@ module ActionController @_templates[path] += 1 end - ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload| + @_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 path = payload[:identifier] @@ -53,8 +54,9 @@ module ActionController end def teardown_subscriptions - ActiveSupport::Notifications.unsubscribe("render_template.action_view") - ActiveSupport::Notifications.unsubscribe("!render_template.action_view") + @_subscribers.each do |subscriber| + ActiveSupport::Notifications.unsubscribe(subscriber) + end end def process(*args) @@ -197,7 +199,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) @@ -206,7 +208,7 @@ module ActionController value = value.to_param end - path_parameters[key.to_s] = value + path_parameters[key] = value end end @@ -548,6 +550,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 @@ -556,6 +583,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. @@ -574,7 +602,6 @@ module ActionController @request.env['REQUEST_METHOD'] = http_method - parameters ||= {} controller_class_name = @controller.class.anonymous? ? "anonymous" : @controller.class.controller_path @@ -668,7 +695,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) @@ -679,7 +706,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/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb index 289e204ac8..2b851cc28d 100644 --- a/actionpack/lib/action_dispatch/http/filter_parameters.rb +++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb @@ -6,8 +6,8 @@ module ActionDispatch module Http # Allows you to specify sensitive parameters which will be replaced from # the request log by looking in the query string of the request and all - # subhashes of the params hash to filter. If a block is given, each key and - # value of the params hash and all subhashes is passed to it, the value + # sub-hashes of the params hash to filter. If a block is given, each key and + # value of the params hash and all sub-hashes is passed to it, the value # or key can be replaced using String#replace or similar method. # # env["action_dispatch.parameter_filter"] = [:password] diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb index 2666cd4b0a..3e607bbde1 100644 --- a/actionpack/lib/action_dispatch/http/headers.rb +++ b/actionpack/lib/action_dispatch/http/headers.rb @@ -1,5 +1,10 @@ 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 @@ -14,21 +19,32 @@ module ActionDispatch 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 +53,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 +71,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 b803ce8b6f..0b2b60d2e4 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -129,7 +129,7 @@ module ActionDispatch end end - order.include?(Mime::ALL) ? formats.first : nil + order.include?(Mime::ALL) ? format : nil end protected diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index dcb299ed03..1ab11392ce 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -4,10 +4,7 @@ require 'active_support/core_ext/hash/indifferent_access' 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 @@ -24,14 +21,13 @@ module ActionDispatch 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 + path_parameters end # Returns a hash with the \parameters used to form the \path of the request. @@ -41,11 +37,7 @@ module ActionDispatch # # 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 diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index daa06e96e6..dfe258e463 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -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 = {} diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 3d27ff2b24..eaea93b730 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -296,6 +296,9 @@ module ActionDispatch # :nodoc: cookies end + def _status_code + @status + end private def before_committed diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb index a8d2dc3950..45bf751d09 100644 --- a/actionpack/lib/action_dispatch/http/upload.rb +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -18,6 +18,7 @@ module ActionDispatch # A +Tempfile+ object with the actual uploaded file. Note that some of # its interface is available directly. attr_accessor :tempfile + alias :to_io :tempfile # A string with the headers of the multipart request. attr_accessor :headers diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index 6f5a52c568..4cba4f5f37 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -29,54 +29,69 @@ module ActionDispatch 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 - else - result << path + def url_for(options) + unless options[:host] || options[:only_path] + raise ArgumentError, 'Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true' + end + + path = options[:script_name].to_s.chomp("/") + path << options[:path].to_s + + add_trailing_slash(path) if options[:trailing_slash] + + result = path + + unless options[:only_path] + result.prepend build_host_url(options) + end + + if options.key? :params + params = options[:params].is_a?(Hash) ? + options[:params] : + { params: options[:params] } + + params.reject! { |_,v| v.to_param.nil? } + result << "?#{params.to_query}" unless params.empty? 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 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(options) - if options[:host].blank? && options[:only_path].blank? - raise ArgumentError, 'Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true' + 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 = "" + options[:protocol] = normalize_protocol(options) + options[:host] = normalize_host(options) + options[:port] = normalize_port(options) - 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 - - options[:protocol] = normalize_protocol(options) - options[:host] = normalize_host(options) - options[:port] = normalize_port(options) - - result << options[:protocol] - result << rewrite_authentication(options) - result << options[:host] - result << ":#{options[:port]}" if options[:port] + result = options[:protocol] + + if options[:user] && options[:password] + result << "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@" end + + result << options[:host] + result << ":#{options[:port]}" if options[:port] + result end @@ -88,14 +103,6 @@ module ActionDispatch (options[:subdomain] == true || !options.key?(:subdomain)) && options[:domain].nil? end - def rewrite_authentication(options) - if options[:user] && options[:password] - "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@" - else - "" - end - end - def normalize_protocol(options) case options[:protocol] when nil @@ -130,7 +137,7 @@ module ActionDispatch case options[:protocol] when "//" - nil + options[:port] when "https://" options[:port].to_i == 443 ? nil : options[:port] else diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb index 57f0963731..6d58323789 100644 --- a/actionpack/lib/action_dispatch/journey/formatter.rb +++ b/actionpack/lib/action_dispatch/journey/formatter.rb @@ -12,7 +12,7 @@ module ActionDispatch @cache = nil end - def generate(type, name, options, recall = {}, parameterize = nil) + def generate(name, options, recall = {}, parameterize = nil) constraints = recall.merge(options) missing_keys = [] @@ -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/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..cb0a02c298 100644 --- a/actionpack/lib/action_dispatch/journey/path/pattern.rb +++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb @@ -30,6 +30,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 c8eb0f6f2d..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 @@ -101,8 +86,12 @@ module ActionDispatch end end + def glob? + !path.spec.grep(Nodes::Star).empty? + 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..74fa9ee3a2 100644 --- a/actionpack/lib/action_dispatch/journey/router.rb +++ b/actionpack/lib/action_dispatch/journey/router.rb @@ -20,60 +20,31 @@ 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 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 +54,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 +95,30 @@ 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) + + if req.env["REQUEST_METHOD"] === "HEAD" + routes.concat get_routes_as_head(routes) + end routes.sort_by!(&:precedence).select! { |r| r.matches?(req) } 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 +129,6 @@ module ActionDispatch route.precedence = r.precedence + precedence end } - routes.flatten! - routes end end end diff --git a/actionpack/lib/action_dispatch/journey/router/utils.rb b/actionpack/lib/action_dispatch/journey/router/utils.rb index d1a004af50..ac4ecb1e65 100644 --- a/actionpack/lib/action_dispatch/journey/router/utils.rb +++ b/actionpack/lib/action_dispatch/journey/router/utils.rb @@ -1,5 +1,3 @@ -require 'uri' - module ActionDispatch module Journey # :nodoc: class Router # :nodoc: @@ -25,31 +23,67 @@ module ActionDispatch # URI path and fragment escaping # http://tools.ietf.org/html/rfc3986 - module UriEscape # :nodoc: - # Symbol captures can generate multiple path segments, so include /. - reserved_segment = '/' - reserved_fragment = '/?' - reserved_pchar = ':@&=+$,;%' - - safe_pchar = "#{URI::REGEXP::PATTERN::UNRESERVED}#{reserved_pchar}" - safe_segment = "#{safe_pchar}#{reserved_segment}" - safe_fragment = "#{safe_pchar}#{reserved_fragment}" - UNSAFE_SEGMENT = Regexp.new("[^#{safe_segment}]", false).freeze - UNSAFE_FRAGMENT = Regexp.new("[^#{safe_fragment}]", false).freeze + 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) } + + ALPHA = "a-zA-Z".freeze + DIGIT = "0-9".freeze + UNRESERVED = "#{ALPHA}#{DIGIT}\\-\\._~".freeze + SUB_DELIMS = "!\\$&'\\(\\)\\*\\+,;=".freeze + + ESCAPED = /%[a-zA-Z0-9]{2}/.freeze + + FRAGMENT = /[^#{UNRESERVED}#{SUB_DELIMS}:@\/\?]/.freeze + SEGMENT = /[^#{UNRESERVED}#{SUB_DELIMS}:@]/.freeze + PATH = /[^#{UNRESERVED}#{SUB_DELIMS}:@\/]/.freeze + + def escape_fragment(fragment) + escape(fragment, FRAGMENT) + end + + def escape_path(path) + escape(path, PATH) + end + + def escape_segment(segment) + escape(segment, SEGMENT) + end + + def unescape_uri(uri) + uri.gsub(ESCAPED) { [$&[1, 2].hex].pack('C') }.force_encoding(uri.encoding) + end + + protected + def escape(component, pattern) + component.gsub(pattern){ |unsafe| percent_encode(unsafe) }.force_encoding(ENCODING) + end + + def percent_encode(unsafe) + safe = EMPTY.dup + unsafe.each_byte { |b| safe << DEC2HEX[b] } + safe + end end - Parser = URI::Parser.new + ENCODER = UriEncoder.new def self.escape_path(path) - Parser.escape(path.to_s, UriEscape::UNSAFE_SEGMENT) + ENCODER.escape_path(path.to_s) + end + + def self.escape_segment(segment) + ENCODER.escape_segment(segment.to_s) end def self.escape_fragment(fragment) - Parser.escape(fragment.to_s, UriEscape::UNSAFE_FRAGMENT) + ENCODER.escape_fragment(fragment.to_s) end def self.unescape_uri(uri) - Parser.unescape(uri) + ENCODER.unescape_uri(uri) end end end diff --git a/actionpack/lib/action_dispatch/journey/visitors.rb b/actionpack/lib/action_dispatch/journey/visitors.rb index daade5bb74..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,77 +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 visit(node, optional = false) - case node.type - when :LITERAL, :SLASH, :DOT - node.left - when :STAR - visit(node.left) - when :GROUP - visit(node.left, true) - when :CAT - visit_CAT(node, optional) - when :SYMBOL - visit_SYMBOL(node) - end - end - - def 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_SYMBOL(node) - if value = options[node.to_sym] - Router::Utils.escape_path(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 c0039fa3f5..22b16b628d 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -176,11 +176,11 @@ module ActionDispatch module VerifyAndUpgradeLegacySignedMessage def initialize(*args) super - @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token]) + @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: NullSerializer) end def verify_and_upgrade_legacy_signed_message(name, signed_message) - @legacy_verifier.verify(signed_message).tap do |value| + deserialize(name, @legacy_verifier.verify(signed_message)).tap do |value| self[name] = { value: value } end rescue ActiveSupport::MessageVerifier::InvalidSignature 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..cbb066b092 100644 --- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb +++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb @@ -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 diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb index f154021ae6..f154021ae6 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb new file mode 100644 index 0000000000..603de54b8b --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb @@ -0,0 +1,9 @@ +<%= @exception.class.to_s %><% + if @request.parameters['controller'] +%> in <%= @request.parameters['controller'].camelize %>Controller<% if @request.parameters['action'] %>#<%= @request.parameters['action'] %><% end %> +<% end %> + +<%= @exception.message %> +<%= render template: "rescues/_source" %> +<%= render template: "rescues/_trace" %> +<%= render template: "rescues/_request_and_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..84a08770f5 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -6,6 +6,7 @@ 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' module ActionDispatch module Routing @@ -15,39 +16,46 @@ 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 @@ -57,12 +65,18 @@ module ActionDispatch WILDCARD_PATH = %r{\*([^/\)]+)\)?$} attr_reader :scope, :path, :options, :requirements, :conditions, :defaults + attr_reader :to, :default_controller, :default_action def initialize(set, scope, path, options) - @set, @scope, @path, @options = set, scope, path, options + @set, @scope, @path = set, scope, path @requirements, @conditions, @defaults = {}, {}, {} - normalize_options! + options = scope[:options].merge(options) if scope[:options] + @to = options[:to] + @default_controller = options[:controller] || scope[:controller] + @default_action = options[:action] || scope[:action] + + @options = normalize_options!(options) normalize_path! normalize_requirements! normalize_conditions! @@ -94,14 +108,13 @@ module ActionDispatch options[:format] != false && !path.include?(':format') && !path.end_with?('/') end - def normalize_options! - @options.reverse_merge!(scope[:options]) if scope[:options] + def normalize_options!(options) path_without_format = path.sub(/\(\.:format\)$/, '') # 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 path_without_format.match(WILDCARD_PATH) && options[:format] != false + options[$1.to_sym] ||= /.+?/ end if path_without_format.match(':controller') @@ -111,10 +124,10 @@ module ActionDispatch # 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) + options.merge!(default_controller_and_action) end def normalize_requirements! @@ -159,6 +172,8 @@ module ActionDispatch @defaults[key] ||= default end end + elsif options[:constraints] + verify_callable_constraint(options[:constraints]) end if Regexp === options[:format] @@ -168,6 +183,12 @@ module ActionDispatch end 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 + def normalize_conditions! @conditions[:path_info] = path @@ -202,7 +223,17 @@ module ActionDispatch end def app - Constraints.new(endpoint, blocks, @set.request_class) + return to if Redirect === to + + if to.respond_to?(:call) + Constraints.new(to, blocks, false) + else + if blocks.any? + Constraints.new(dispatcher, blocks, true) + else + dispatcher + end + end end def default_controller_and_action @@ -288,24 +319,8 @@ module ActionDispatch Journey::Router::Strexp.compile(path, requirements, SEPARATORS) end - def endpoint - to.respond_to?(:call) ? to : dispatcher - end - def dispatcher - Routing::RouteSet::Dispatcher.new(:defaults => defaults) - end - - def to - options[:to] - end - - def default_controller - options[:controller] || scope[:controller] - end - - def default_action - options[:action] || scope[:action] + Routing::RouteSet::Dispatcher.new(defaults) end end @@ -340,18 +355,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 +395,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 @@ -397,7 +428,7 @@ module ActionDispatch # [: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 +447,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 +474,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 +490,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 +499,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+ @@ -554,18 +585,17 @@ module ActionDispatch _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 diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index 2fb03f2712..bd3696cda1 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -101,53 +101,45 @@ module ActionDispatch # polymorphic_url(Comment) # same as comments_url() # def polymorphic_url(record_or_hash_or_array, options = {}) - 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) - proxy = 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 - 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 + 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 - args.delete_if {|arg| arg.is_a?(Symbol) || arg.is_a?(String)} - named_route = build_named_route_call(record_or_hash_or_array, inflection, options) - - 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 + opts = options.dup + action = opts.delete :action + type = opts.delete(:routing_type) || :url - args.collect! { |a| convert_to_model(a) } + HelperMethodBuilder.polymorphic_method self, + record_or_hash_or_array, + action, + type, + opts - (proxy || self).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 = {}) @@ -165,54 +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 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 self.singular(prefix, suffix) + new(->(name) { name.singular_route_key }, prefix, suffix) end - def routing_type(options) - options[:routing_type] || :url + def self.plural(prefix, suffix) + new(->(name) { name.route_key }, prefix, suffix) end - def build_named_route_call(records, 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.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 - record = extract_record(records) - 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..b1b39a5496 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) diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index a03fb4cee7..3ca5abf0fd 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] - - # 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?) + def dispatcher?; true; end - 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 @@ -155,7 +143,7 @@ module ActionDispatch end def self.optimize_helper?(route) - route.requirements.except(:controller, :action).empty? + !route.glob? && route.path.requirements.empty? end class OptimizedUrlHelper < UrlHelper # :nodoc: @@ -163,16 +151,13 @@ module ActionDispatch def initialize(route, options) 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) + options = t.url_options.merge @options options[:path] = optimized_helper(args) ActionDispatch::Http::URL.url_for(options) else @@ -183,18 +168,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_fragment(params[segment]) : segment + @route.format params end def optimize_routes_generation?(t) @@ -202,7 +183,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) @@ -225,20 +208,23 @@ module ActionDispatch end def call(t, args) - t.url_for(handle_positional_args(t, args, @options, @segment_keys)) + controller_options = t.url_options + options = controller_options.merge @options + hash = handle_positional_args(controller_options, args, options, @segment_keys) + t._routes.url_for(hash) end - def handle_positional_args(t, args, options, keys) + def handle_positional_args(controller_options, args, result, path_params) inner_options = args.extract_options! - result = options.dup 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) @@ -302,9 +288,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 @@ -393,6 +377,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 @@ -594,7 +580,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? @@ -639,41 +625,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*. + # The +options+ argument must be a hash whose keys are *symbols*. def url_for(options) - options = default_url_options.merge(options || {}) + options = default_url_options.merge options + + user = password = nil - user, password = extract_authentication(options) - recall = options.delete(:_recall) + if options[:user] && options[:password] + user = options.delete :user + password = options.delete :password + end - original_script_name = options.delete(:original_script_name).presence - script_name = options.delete(:script_name).presence || _generate_prefix(options) + recall = options.delete(:_recall) { {} } + + 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(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 - })) + ActionDispatch::Http::URL.url_for(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 +684,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 +693,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 +710,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..e624fe3c4a 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -155,10 +155,14 @@ module ActionDispatch _routes.url_for(options.symbolize_keys.reverse_merge!(url_options)) 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/assertions/selector.rb b/actionpack/lib/action_dispatch/testing/assertions/selector.rb index 8a128427bf..12023e6f77 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/selector.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/selector.rb @@ -267,7 +267,7 @@ module ActionDispatch text.strip! unless NO_STRIP.include?(match.name) text.sub!(/\A\n/, '') if match.name == "textarea" unless match_with.is_a?(Regexp) ? (text =~ match_with) : (text == match_with.to_s) - content_mismatch ||= sprintf("<%s> expected but was\n<%s>.", match_with, text) + content_mismatch ||= sprintf("<%s> expected but was\n<%s>", match_with, text) true end end @@ -276,7 +276,7 @@ module ActionDispatch html = match.children.map(&:to_s).join html.strip! unless NO_STRIP.include?(match.name) unless match_with.is_a?(Regexp) ? (html =~ match_with) : (html == match_with.to_s) - content_mismatch ||= sprintf("<%s> expected but was\n<%s>.", match_with, html) + content_mismatch ||= sprintf("<%s> expected but was\n<%s>", match_with, html) true end end @@ -289,7 +289,7 @@ module ActionDispatch # FIXME: minitest provides messaging when we use assert_operator, # so is this custom message really needed? - message = message || %(Expected #{count_description(min, max, count)} matching "#{selector.to_s}", found #{matches.size}.) + message = message || %(Expected #{count_description(min, max, count)} matching "#{selector.to_s}", found #{matches.size}) if count assert_equal count, matches.size, message else diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index cc6b763093..107e62d20f 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: @@ -300,13 +299,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 +312,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 diff --git a/actionpack/test/abstract/callbacks_test.rb b/actionpack/test/abstract/callbacks_test.rb index 8cba049485..07571602e4 100644 --- a/actionpack/test/abstract/callbacks_test.rb +++ b/actionpack/test/abstract/callbacks_test.rb @@ -267,9 +267,11 @@ module AbstractController end class AliasedCallbacks < ControllerWithCallbacks - before_filter :first - after_filter :second - around_filter :aroundz + ActiveSupport::Deprecation.silence do + before_filter :first + after_filter :second + around_filter :aroundz + end def first @text = "Hello world" 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..46de36317e 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -320,8 +320,8 @@ module ActionDispatch 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, recall = {}) + set.url_for options.merge(:only_path => true, :_recall => recall) end end diff --git a/actionpack/test/controller/assert_select_test.rb b/actionpack/test/controller/assert_select_test.rb index 114bbf3c22..580604df3a 100644 --- a/actionpack/test/controller/assert_select_test.rb +++ b/actionpack/test/controller/assert_select_test.rb @@ -79,13 +79,13 @@ class AssertSelectTest < ActionController::TestCase def test_assert_select render_html %Q{<div id="1"></div><div id="2"></div>} assert_select "div", 2 - assert_failure(/Expected at least 1 element matching \"p\", found 0/) { assert_select "p" } + assert_failure(/\AExpected at least 1 element matching \"p\", found 0\.$/) { assert_select "p" } end def test_equality_integer render_html %Q{<div id="1"></div><div id="2"></div>} - assert_failure(/Expected exactly 3 elements matching \"div\", found 2/) { assert_select "div", 3 } - assert_failure(/Expected exactly 0 elements matching \"div\", found 2/) { assert_select "div", 0 } + assert_failure(/\AExpected exactly 3 elements matching \"div\", found 2\.$/) { assert_select "div", 3 } + assert_failure(/\AExpected exactly 0 elements matching \"div\", found 2\.$/) { assert_select "div", 0 } end def test_equality_true_false @@ -100,13 +100,14 @@ class AssertSelectTest < ActionController::TestCase def test_equality_false_message render_html %Q{<div id="1"></div><div id="2"></div>} - assert_failure(/Expected exactly 0 elements matching \"div\", found 2/) { assert_select "div", false } + assert_failure(/\AExpected exactly 0 elements matching \"div\", found 2\.$/) { assert_select "div", false } end def test_equality_string_and_regexp render_html %Q{<div id="1">foo</div><div id="2">foo</div>} assert_nothing_raised { assert_select "div", "foo" } assert_raise(Assertion) { assert_select "div", "bar" } + assert_failure(/\A<bar> expected but was\n<foo>\.$/) { assert_select "div", "bar" } assert_nothing_raised { assert_select "div", :text=>"foo" } assert_raise(Assertion) { assert_select "div", :text=>"bar" } assert_nothing_raised { assert_select "div", /(foo|bar)/ } @@ -124,6 +125,7 @@ class AssertSelectTest < ActionController::TestCase assert_raise(Assertion) { assert_select "p", html } assert_nothing_raised { assert_select "p", :html=>html } assert_raise(Assertion) { assert_select "p", :html=>text } + assert_failure(/\A<#{text}> expected but was\n<#{html}>\.$/) { assert_select "p", :html=>text } # No stripping for pre. render_html %Q{<pre>\n<em>"This is <strong>not</strong> a big problem,"</em> he said.\n</pre>} text = "\n\"This is not a big problem,\" he said.\n" @@ -144,29 +146,29 @@ class AssertSelectTest < ActionController::TestCase def test_counts render_html %Q{<div id="1">foo</div><div id="2">foo</div>} assert_nothing_raised { assert_select "div", 2 } - assert_failure(/Expected exactly 3 elements matching \"div\", found 2/) do + assert_failure(/\AExpected exactly 3 elements matching \"div\", found 2\.$/) do assert_select "div", 3 end assert_nothing_raised { assert_select "div", 1..2 } - assert_failure(/Expected between 3 and 4 elements matching \"div\", found 2/) do + assert_failure(/\AExpected between 3 and 4 elements matching \"div\", found 2\.$/) do assert_select "div", 3..4 end assert_nothing_raised { assert_select "div", :count=>2 } - assert_failure(/Expected exactly 3 elements matching \"div\", found 2/) do + assert_failure(/\AExpected exactly 3 elements matching \"div\", found 2\.$/) do assert_select "div", :count=>3 end assert_nothing_raised { assert_select "div", :minimum=>1 } assert_nothing_raised { assert_select "div", :minimum=>2 } - assert_failure(/Expected at least 3 elements matching \"div\", found 2/) do + assert_failure(/\AExpected at least 3 elements matching \"div\", found 2\.$/) do assert_select "div", :minimum=>3 end assert_nothing_raised { assert_select "div", :maximum=>2 } assert_nothing_raised { assert_select "div", :maximum=>3 } - assert_failure(/Expected at most 1 element matching \"div\", found 2/) do + assert_failure(/\AExpected at most 1 element matching \"div\", found 2\.$/) do assert_select "div", :maximum=>1 end assert_nothing_raised { assert_select "div", :minimum=>1, :maximum=>2 } - assert_failure(/Expected between 3 and 4 elements matching \"div\", found 2/) do + assert_failure(/\AExpected between 3 and 4 elements matching \"div\", found 2\.$/) do assert_select "div", :minimum=>3, :maximum=>4 end end @@ -204,7 +206,7 @@ class AssertSelectTest < ActionController::TestCase end end - assert_failure(/Expected at least 1 element matching \"#4\", found 0\./) do + assert_failure(/\AExpected at least 1 element matching \"#4\", found 0\.$/) do assert_select "div" do assert_select "#4" 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/integration_test.rb b/actionpack/test/controller/integration_test.rb index e851cc6a63..214eab2f0d 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -374,6 +374,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 +515,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 @@ -769,3 +775,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..1dca36374a 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 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/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 499c62cc35..41503e11a8 100644 --- a/actionpack/test/controller/mime/respond_to_test.rb +++ b/actionpack/test/controller/mime/respond_to_test.rb @@ -258,8 +258,6 @@ class RespondToController < ActionController::Base end class RespondToControllerTest < ActionController::TestCase - tests RespondToController - def setup super @request.host = "www.example.com" @@ -492,6 +490,11 @@ class RespondToControllerTest < ActionController::TestCase assert_equal 'Whatever you ask for, I got it', @response.body end + def test_handle_any_any_unkown_format + get :handle_any_any, { format: 'php' } + assert_equal 'Whatever you ask for, I got it', @response.body + end + def test_browser_check_with_any_any @request.accept = "application/json, application/xml" get :json_xml_or_html 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..2ddc07ef72 100644 --- a/actionpack/test/controller/new_base/bare_metal_test.rb +++ b/actionpack/test/controller/new_base/bare_metal_test.rb @@ -81,8 +81,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_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/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/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/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index 99229b3baf..2a5aad9c0e 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -127,25 +127,26 @@ 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 assert_not_blocked do get :index end - assert_select 'form>div>input[name=?][value=?]', 'custom_authenticity_token', @token + assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token end def test_should_render_button_to_with_token_tag assert_not_blocked do get :show_button end - assert_select 'form>div>input[name=?][value=?]', 'custom_authenticity_token', @token + assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token end def test_should_render_form_without_token_tag_if_remote @@ -175,7 +176,7 @@ module RequestForgeryProtectionTests assert_not_blocked do get :form_for_remote_with_external_token end - assert_select 'form>div>input[name=?][value=?]', 'custom_authenticity_token', 'external_token' + assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', 'external_token' ensure ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original end @@ -185,21 +186,21 @@ module RequestForgeryProtectionTests assert_not_blocked do get :form_for_remote_with_external_token end - assert_select 'form>div>input[name=?][value=?]', 'custom_authenticity_token', 'external_token' + assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', 'external_token' end def test_should_render_form_with_token_tag_if_remote_and_authenticity_token_requested assert_not_blocked do get :form_for_remote_with_token end - assert_select 'form>div>input[name=?][value=?]', 'custom_authenticity_token', @token + assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token end def test_should_render_form_with_token_tag_with_authenticity_token_requested assert_not_blocked do get :form_for_with_token end - assert_select 'form>div>input[name=?][value=?]', 'custom_authenticity_token', @token + assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token end def test_should_allow_get @@ -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/routing_test.rb b/actionpack/test/controller/routing_test.rb index df453a0251..9dc6d77012 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -87,7 +87,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase 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 +99,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 +111,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 +124,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 +136,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 @@ -419,14 +419,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 = {}) diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb index 4df2f8b98d..2c833aa2e5 100644 --- a/actionpack/test/controller/send_file_test.rb +++ b/actionpack/test/controller/send_file_test.rb @@ -30,17 +30,20 @@ class SendFileWithActionControllerLive < SendFileController end class SendFileTest < ActionController::TestCase - tests SendFileController include TestFileUtils - Mime::Type.register "image/png", :png unless defined? Mime::PNG - def setup + @mime_type_defined = defined? Mime::PNG + Mime::Type.register "image/png", :png unless @mime_type_defined @controller = SendFileController.new @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new end + teardown do + Mime::Type.unregister :png unless @mime_type_defined + end + def test_file_nostream @controller.options = { :stream => false } response = nil @@ -201,8 +204,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/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index fbc10baf21..1141feeff7 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 diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb index a8035e5bd7..f52f8be101 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 @@ -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'} diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index ba7aaa338d..0f145666d1 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -681,6 +681,123 @@ class CookiesTest < ActionController::TestCase assert_equal 'bar', encryptor.decrypt_and_verify(@response.cookies["foo"]) end + def test_legacy_json_signed_cookie_is_read_and_transparently_upgraded_by_signed_json_cookie_jar_if_both_secret_token_and_secret_key_base_are_set + @request.env["action_dispatch.cookies_serializer"] = :json + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + + legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate(45) + + @request.headers["Cookie"] = "user_id=#{legacy_value}" + get :get_signed_cookie + + assert_equal 45, @controller.send(:cookies).signed[:user_id] + + key_generator = @request.env["action_dispatch.key_generator"] + secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) + verifier = ActiveSupport::MessageVerifier.new(secret, serializer: JSON) + assert_equal 45, verifier.verify(@response.cookies["user_id"]) + end + + def test_legacy_json_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_json_cookie_jar_if_both_secret_token_and_secret_key_base_are_set + @request.env["action_dispatch.cookies_serializer"] = :json + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + @request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee" + @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9" + + legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate('bar') + + @request.headers["Cookie"] = "foo=#{legacy_value}" + get :get_encrypted_cookie + + assert_equal 'bar', @controller.send(:cookies).encrypted[:foo] + + key_generator = @request.env["action_dispatch.key_generator"] + secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"]) + sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"]) + encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON) + assert_equal 'bar', encryptor.decrypt_and_verify(@response.cookies["foo"]) + end + + def test_legacy_json_signed_cookie_is_read_and_transparently_upgraded_by_signed_json_hybrid_jar_if_both_secret_token_and_secret_key_base_are_set + @request.env["action_dispatch.cookies_serializer"] = :hybrid + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + + legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate(45) + + @request.headers["Cookie"] = "user_id=#{legacy_value}" + get :get_signed_cookie + + assert_equal 45, @controller.send(:cookies).signed[:user_id] + + key_generator = @request.env["action_dispatch.key_generator"] + secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) + verifier = ActiveSupport::MessageVerifier.new(secret, serializer: JSON) + assert_equal 45, verifier.verify(@response.cookies["user_id"]) + end + + def test_legacy_json_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_hybrid_cookie_jar_if_both_secret_token_and_secret_key_base_are_set + @request.env["action_dispatch.cookies_serializer"] = :hybrid + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + @request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee" + @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9" + + legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate('bar') + + @request.headers["Cookie"] = "foo=#{legacy_value}" + get :get_encrypted_cookie + + assert_equal 'bar', @controller.send(:cookies).encrypted[:foo] + + key_generator = @request.env["action_dispatch.key_generator"] + secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"]) + sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"]) + encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON) + assert_equal 'bar', encryptor.decrypt_and_verify(@response.cookies["foo"]) + end + + def test_legacy_marshal_signed_cookie_is_read_and_transparently_upgraded_by_signed_json_hybrid_jar_if_both_secret_token_and_secret_key_base_are_set + @request.env["action_dispatch.cookies_serializer"] = :hybrid + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + + legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate(45) + + @request.headers["Cookie"] = "user_id=#{legacy_value}" + get :get_signed_cookie + + assert_equal 45, @controller.send(:cookies).signed[:user_id] + + key_generator = @request.env["action_dispatch.key_generator"] + secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) + verifier = ActiveSupport::MessageVerifier.new(secret, serializer: JSON) + assert_equal 45, verifier.verify(@response.cookies["user_id"]) + end + + def test_legacy_marshal_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_hybrid_cookie_jar_if_both_secret_token_and_secret_key_base_are_set + @request.env["action_dispatch.cookies_serializer"] = :hybrid + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + @request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee" + @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9" + + legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate('bar') + + @request.headers["Cookie"] = "foo=#{legacy_value}" + get :get_encrypted_cookie + + assert_equal 'bar', @controller.send(:cookies).encrypted[:foo] + + key_generator = @request.env["action_dispatch.key_generator"] + secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"]) + sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"]) + encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON) + assert_equal 'bar', encryptor.decrypt_and_verify(@response.cookies["foo"]) + end + def test_legacy_signed_cookie_is_treated_as_nil_by_signed_cookie_jar_if_tampered @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb index 0dba651139..8660deb634 100644 --- a/actionpack/test/dispatch/debug_exceptions_test.rb +++ b/actionpack/test/dispatch/debug_exceptions_test.rb @@ -147,9 +147,10 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest get "/", {}, xhr_request_env assert_response 500 + assert_no_match(/<header>/, body) assert_no_match(/<body>/, body) assert_equal response.content_type, "text/plain" - assert_match(/puke/, body) + assert_match(/RuntimeError\npuke/, body) get "/not_found", {}, xhr_request_env assert_response 404 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/prefix_generation_test.rb b/actionpack/test/dispatch/prefix_generation_test.rb index 08501d19c0..cd31e8e326 100644 --- a/actionpack/test/dispatch/prefix_generation_test.rb +++ b/actionpack/test/dispatch/prefix_generation_test.rb @@ -15,6 +15,9 @@ module TestGenerationPrefix ActiveModel::Name.new(klass) end + + def to_model; self; end + def persisted?; true; end end class WithMountedEngine < ActionDispatch::IntegrationTest diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index 6e21b4a258..b48e8ab974 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 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 f74a0ef945..c9777ae71f 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -1723,7 +1723,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 +1735,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', @@ -2287,12 +2287,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) @@ -3368,12 +3368,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 @@ -3476,6 +3478,33 @@ class TestNamespaceWithControllerOption < ActionDispatch::IntegrationTest @app.draw(&block) end + def test_missing_controller + ex = assert_raises(ArgumentError) { + draw do + get '/foo/bar', :to => :index + end + } + assert_match(/Missing :controller/, ex.message) + end + + def test_missing_action + ex = assert_raises(ArgumentError) { + draw do + get '/foo/bar', :to => 'foo' + 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 draw do namespace :admin do @@ -3492,7 +3521,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 @@ -3553,6 +3582,7 @@ class TestHttpMethods < ActionDispatch::IntegrationTest RFC3648 = %w(ORDERPATCH) RFC3744 = %w(ACL) RFC5323 = %w(SEARCH) + RFC4791 = %w(MKCALENDAR) RFC5789 = %w(PATCH) def simple_app(response) @@ -3564,13 +3594,13 @@ class TestHttpMethods < ActionDispatch::IntegrationTest @app = ActionDispatch::Routing::RouteSet.new @app.draw do - (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC5789).each do |method| + (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 @@ -3596,8 +3626,8 @@ class TestUriPathEscaping < ActionDispatch::IntegrationTest include Routes.url_helpers def app; Routes end - test 'escapes generated path segment' do - assert_equal '/a%20b/c+d', segment_path(:segment => 'a b/c+d') + test 'escapes slash in generated path segment' do + assert_equal '/a%20b%2Fc+d', segment_path(:segment => 'a b/c+d') end test 'unescapes recognized path segment' do @@ -3605,7 +3635,7 @@ class TestUriPathEscaping < ActionDispatch::IntegrationTest assert_equal 'a b/c+d', @response.body end - test 'escapes generated path splat' do + test 'does not escape slash in generated path splat' do assert_equal '/a%20b/c+d', splat_path(:splat => 'a b/c+d') end @@ -3790,6 +3820,8 @@ class TestOptimizedNamedRoutes < ActionDispatch::IntegrationTest get '/post(/:action(/:id))' => ok, as: :posts get '/:foo/:foo_type/bars/:id' => ok, as: :bar get '/projects/:id.:format' => ok, as: :project + get '/pages/:id' => ok, as: :page + get '/wiki/*page' => ok, as: :wiki end end @@ -3822,6 +3854,26 @@ class TestOptimizedNamedRoutes < ActionDispatch::IntegrationTest assert_equal '/projects/1.json', Routes.url_helpers.project_path(1, :json) assert_equal '/projects/1.json', project_path(1, :json) end + + test 'segments with question marks are escaped' do + assert_equal '/pages/foo%3Fbar', Routes.url_helpers.page_path('foo?bar') + assert_equal '/pages/foo%3Fbar', page_path('foo?bar') + end + + test 'segments with slashes are escaped' do + assert_equal '/pages/foo%2Fbar', Routes.url_helpers.page_path('foo/bar') + assert_equal '/pages/foo%2Fbar', page_path('foo/bar') + end + + test 'glob segments with question marks are escaped' do + assert_equal '/wiki/foo%3Fbar', Routes.url_helpers.wiki_path('foo?bar') + assert_equal '/wiki/foo%3Fbar', wiki_path('foo?bar') + end + + test 'glob segments with slashes are not escaped' do + assert_equal '/wiki/foo/bar', Routes.url_helpers.wiki_path('foo/bar') + assert_equal '/wiki/foo/bar', wiki_path('foo/bar') + end end class TestNamedRouteUrlHelpers < ActionDispatch::IntegrationTest @@ -4089,6 +4141,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 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/uploaded_file_test.rb b/actionpack/test/dispatch/uploaded_file_test.rb index 72f3d1db0d..9f6381f118 100644 --- a/actionpack/test/dispatch/uploaded_file_test.rb +++ b/actionpack/test/dispatch/uploaded_file_test.rb @@ -33,6 +33,12 @@ module ActionDispatch assert_equal 'foo', uf.tempfile end + def test_to_io_returns_the_tempfile + tf = Object.new + uf = Http::UploadedFile.new(:tempfile => tf) + assert_equal tf, uf.to_io + end + def test_delegates_path_to_tempfile tf = Class.new { def path; 'thunderhorse' end } uf = Http::UploadedFile.new(:tempfile => tf.new) diff --git a/actionpack/test/dispatch/url_generation_test.rb b/actionpack/test/dispatch/url_generation_test.rb index fdea27e2d2..a4dfd0a63d 100644 --- a/actionpack/test/dispatch/url_generation_test.rb +++ b/actionpack/test/dispatch/url_generation_test.rb @@ -15,6 +15,8 @@ module TestUrlGeneration Routes.draw do get "/foo", :to => "my_route_generating#index", :as => :foo + resources :bars + mount MyRouteGeneratingController.action(:index), at: '/bar' end @@ -64,18 +66,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 +111,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/journey/router/utils_test.rb b/actionpack/test/journey/router/utils_test.rb index 93348f4647..584fd56a5c 100644 --- a/actionpack/test/journey/router/utils_test.rb +++ b/actionpack/test/journey/router/utils_test.rb @@ -5,11 +5,15 @@ module ActionDispatch class Router class TestUtils < ActiveSupport::TestCase def test_path_escape - assert_equal "a/b%20c+d", Utils.escape_path("a/b c+d") + assert_equal "a/b%20c+d%25", Utils.escape_path("a/b c+d%") + end + + def test_segment_escape + assert_equal "a%2Fb%20c+d%25", Utils.escape_segment("a/b c+d%") end def test_fragment_escape - assert_equal "a/b%20c+d?e", Utils.escape_fragment("a/b c+d?e") + assert_equal "a/b%20c+d%25?e", Utils.escape_fragment("a/b c+d%?e") end def test_uri_unescape diff --git a/actionpack/test/journey/router_test.rb b/actionpack/test/journey/router_test.rb index a286f77633..f298808be8 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,7 +30,7 @@ module ActionDispatch end def test_dashes - router = Router.new(routes, {}) + router = Router.new(routes) exp = Router::Strexp.new '/foo-bar-baz', {}, ['/.?'] path = Path::Pattern.new exp @@ -48,14 +39,14 @@ module ActionDispatch 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', {}, ['/.?'] @@ -65,7 +56,7 @@ module ActionDispatch 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,7 +64,7 @@ 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/ } @@ -82,8 +73,8 @@ module ActionDispatch 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,7 +84,7 @@ 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/ } @@ -102,8 +93,8 @@ module ActionDispatch 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', {}, ['/.?'] 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 @@ -144,7 +140,7 @@ module ActionDispatch 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 @@ -160,7 +156,7 @@ module ActionDispatch ] assert_raises(ActionController::UrlGenerationError) do - @formatter.generate(:path_info, nil, { :id => '10' }, { }) + @formatter.generate(nil, { :id => '10' }, { }) end end @@ -169,11 +165,11 @@ module ActionDispatch Router::Strexp.new("/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 @@ -182,13 +178,13 @@ module ActionDispatch Router::Strexp.new("/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 @@ -199,7 +195,7 @@ module ActionDispatch @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,20 +203,23 @@ 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 + route_set = Routing::RouteSet.new + mapper = Routing::Mapper.new route_set + strexp = Router::Strexp.new("/", {}, ['/', '.', '?'], false) path = Path::Pattern.new strexp 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 @@ -230,12 +229,12 @@ module ActionDispatch @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 @@ -287,14 +286,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,7 +303,7 @@ 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 @@ -315,7 +314,7 @@ module ActionDispatch 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 @@ -327,7 +326,7 @@ module ActionDispatch @router.routes.add_route @app, path, {}, {}, {} - path, _ = @formatter.generate(:path_info, nil, Hash[params], {}) + path, _ = @formatter.generate(nil, Hash[params], {}) assert_equal '/', path end @@ -340,7 +339,6 @@ module ActionDispatch [:action, "show"] ] @formatter.generate( - :path_info, nil, Hash[params], {}, @@ -354,7 +352,7 @@ module ActionDispatch @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 @@ -363,18 +361,29 @@ module ActionDispatch path = Path::Pattern.new '/: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/b%20c+d', path + assert_equal '/tasks/a%2Fb%20c+d', path + end + + def test_generate_escapes_with_namespaced_controller + path = Path::Pattern.new '/:controller(/:action)' + @router.routes.add_route @app, path, {}, {}, {} + + path, _ = @formatter.generate( + nil, { :controller => "admin/tasks", + :action => "a/b c+d", + }, {}) + assert_equal '/admin/tasks/a%2Fb%20c+d', path end def test_generate_extra_params path = Path::Pattern.new '/: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", @@ -388,7 +397,7 @@ module ActionDispatch path = Path::Pattern.new '/: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"}) @@ -400,7 +409,7 @@ module ActionDispatch path = Path::Pattern.new '/: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"}) @@ -421,7 +430,7 @@ module ActionDispatch 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 @@ -443,7 +452,7 @@ module ActionDispatch 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 @@ -471,7 +480,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 @@ -487,7 +496,7 @@ module ActionDispatch 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 +517,7 @@ module ActionDispatch "REQUEST_METHOD" => "HEAD" called = false - @router.recognize(env) do |r, _, params| + @router.recognize(env) do |r, params| called = true end @@ -532,7 +541,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 @@ -549,10 +558,8 @@ module ActionDispatch 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/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 8c6db33be7..147e5b47db 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,75 @@ +* 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" /> + + After: + + #=> favicon_link_tag 'myicon.ico' + <link href="/assets/myicon.ico" rel="shortcut icon" type="image/x-icon" /> + + *Geoffroy Lorieux* + +* 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 + for Content Security Policy. + + *Joost Baaij* + +* `collection_check_boxes` respects `:index` option for the hidden filed name. + + Fixes #14147. + + *Vasiliy Ermolovich* + * `date_select` helper with option `with_css_classes: true` does not overwrite other classes. *Izumi Wong-Horiuchi* @@ -28,5 +100,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/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index aa49f1edc1..f53cce32b0 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -11,7 +11,7 @@ module ActionView # the assets exist before linking to them: # # image_tag("rails.png") - # # => <img alt="Rails" src="/assets/rails.png" /> + # # => <img alt="Rails" src="/images/rails.png" /> # stylesheet_link_tag("application") # # => <link href="/assets/application.css?body=1" media="screen" rel="stylesheet" /> module AssetTagHelper @@ -20,7 +20,8 @@ module ActionView include AssetUrlHelper include TagHelper - # Returns an HTML script tag for each of the +sources+ provided. + # Returns an HTML script tag for each of the +sources+ provided. If + # you don't specify an extension, <tt>.js</tt> will be appended automatically. # # Sources may be paths to JavaScript files. Relative paths are assumed to be relative # to <tt>assets/javascripts</tt>, full paths are assumed to be relative to the document @@ -33,19 +34,19 @@ module ActionView # last argument. # # When the Asset Pipeline is enabled, you can pass the name of your manifest as - # source, and include other JavaScript or CoffeeScript files inside the manifest. + # source and include other JavaScript or CoffeeScript files inside the manifest. # # javascript_include_tag "xmlhr" - # # => <script src="/assets/xmlhr.js?1284139606"></script> + # # => <script src="/javascripts/xmlhr.js?1284139606"></script> # # javascript_include_tag "template.jst", extname: false - # # => <script src="/assets/template.jst?1284139606"></script> + # # => <script src="/javascripts/template.jst?1284139606"></script> # # javascript_include_tag "xmlhr.js" - # # => <script src="/assets/xmlhr.js?1284139606"></script> + # # => <script src="/javascripts/xmlhr.js?1284139606"></script> # # javascript_include_tag "common.javascript", "/elsewhere/cools" - # # => <script src="/assets/common.javascript?1284139606"></script> + # # => <script src="/javascripts/common.javascript?1284139606"></script> # # <script src="/elsewhere/cools.js?1423139606"></script> # # javascript_include_tag "http://www.example.com/xmlhr" @@ -72,22 +73,22 @@ module ActionView # apply to all media types. # # stylesheet_link_tag "style" - # # => <link href="/assets/style.css" media="screen" rel="stylesheet" /> + # # => <link href="/stylesheets/style.css" media="screen" rel="stylesheet" /> # # stylesheet_link_tag "style.css" - # # => <link href="/assets/style.css" media="screen" rel="stylesheet" /> + # # => <link href="/stylesheets/style.css" media="screen" rel="stylesheet" /> # # stylesheet_link_tag "http://www.example.com/style.css" # # => <link href="http://www.example.com/style.css" media="screen" rel="stylesheet" /> # # stylesheet_link_tag "style", media: "all" - # # => <link href="/assets/style.css" media="all" rel="stylesheet" /> + # # => <link href="/stylesheets/style.css" media="all" rel="stylesheet" /> # # stylesheet_link_tag "style", media: "print" - # # => <link href="/assets/style.css" media="print" rel="stylesheet" /> + # # => <link href="/stylesheets/style.css" media="print" rel="stylesheet" /> # # stylesheet_link_tag "random.styles", "/css/stylish" - # # => <link href="/assets/random.styles" media="screen" rel="stylesheet" /> + # # => <link href="/stylesheets/random.styles" media="screen" rel="stylesheet" /> # # <link href="/css/stylish.css" media="screen" rel="stylesheet" /> def stylesheet_link_tag(*sources) options = sources.extract_options!.stringify_keys @@ -142,30 +143,37 @@ module ActionView ) end - # Returns a link loading a favicon file. You may specify a different file - # in the first argument. The helper accepts an additional options hash where - # you can override "rel" and "type". + # Returns a link tag for a favicon managed by the asset pipeline. # - # ==== Options + # If a page has no link like the one generated by this helper, browsers + # ask for <tt>/favicon.ico</tt> automatically, and cache the file if the + # request succeeds. If the favicon changes it is hard to get it updated. # - # * <tt>:rel</tt> - Specify the relation of this link, defaults to 'shortcut icon' - # * <tt>:type</tt> - Override the auto-generated mime type, defaults to 'image/vnd.microsoft.icon' + # To have better control applications may let the asset pipeline manage + # their favicon storing the file under <tt>app/assets/images</tt>, and + # using this helper to generate its corresponding link tag. # - # ==== Examples + # The helper gets the name of the favicon file as first argument, which + # defaults to "favicon.ico", and also supports +:rel+ and +:type+ options + # to override their defaults, "shortcut icon" and "image/x-icon" + # respectively: + # + # favicon_link_tag + # # => <link href="/images/favicon.ico" rel="shortcut icon" type="image/x-icon" /> # # favicon_link_tag 'myicon.ico' - # # => <link href="/assets/myicon.ico" rel="shortcut icon" type="image/vnd.microsoft.icon" /> + # # => <link href="/images/myicon.ico" rel="shortcut icon" type="image/x-icon" /> # - # Mobile Safari looks for a different <link> tag, pointing to an image that - # will be used if you add the page to the home screen of an iPod Touch, iPhone, or iPad. + # Mobile Safari looks for a different link tag, pointing to an image that + # will be used if you add the page to the home screen of an iOS device. # The following call would generate such a tag: # # favicon_link_tag 'mb-icon.png', rel: 'apple-touch-icon', type: 'image/png' - # # => <link href="/assets/mb-icon.png" rel="apple-touch-icon" type="image/png" /> + # # => <link href="/images/mb-icon.png" rel="apple-touch-icon" type="image/png" /> def favicon_link_tag(source='favicon.ico', options={}) tag('link', { :rel => 'shortcut icon', - :type => 'image/vnd.microsoft.icon', + :type => 'image/x-icon', :href => path_to_image(source) }.merge!(options.symbolize_keys)) end @@ -251,19 +259,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="/images/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"]) @@ -282,11 +290,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..d86e7e490c 100644 --- a/actionview/lib/action_view/helpers/asset_url_helper.rb +++ b/actionview/lib/action_view/helpers/asset_url_helper.rb @@ -7,10 +7,10 @@ module ActionView # urls. # # image_path("rails.png") - # # => "/assets/rails.png" + # # => "/images/rails.png" # # image_url("rails.png") - # # => "http://www.example.com/assets/rails.png" + # # => "http://www.example.com/images/rails.png" # # === Using asset hosts # @@ -113,13 +113,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 +134,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 +147,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/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,7 +198,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 = options[:host] + host ||= config.asset_host if defined? config.asset_host host ||= request.base_url if request && options[:protocol] == :request if host.respond_to?(:call) @@ -223,7 +231,7 @@ module ActionView # 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 @@ -243,7 +251,7 @@ 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. # @@ -268,9 +276,9 @@ module ActionView # Full paths from the document root will be passed through. # Used internally by +image_tag+ to build the image path: # - # image_path("edit") # => "/assets/edit" - # image_path("edit.png") # => "/assets/edit.png" - # image_path("icons/edit.png") # => "/assets/icons/edit.png" + # image_path("edit") # => "/images/edit" + # image_path("edit.png") # => "/images/edit.png" + # image_path("icons/edit.png") # => "/images/icons/edit.png" # image_path("/icons/edit.png") # => "/icons/edit.png" # image_path("http://www.example.com/img/edit.png") # => "http://www.example.com/img/edit.png" # @@ -334,9 +342,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/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 1ff090f244..180c4a62bf 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -449,7 +449,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! @@ -746,6 +750,7 @@ module ActionView # label(:post, :terms) do # 'Accept <a href="/terms">Terms</a>.'.html_safe # end + # # => <label for="post_terms">Accept <a href="/terms">Terms</a>.</label> def label(object_name, method, content_or_options = nil, options = nil, &block) Tags::Label.new(object_name, method, self, content_or_options, options).render(&block) end diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb index 0bbe08166b..1e818083cc 100644 --- a/actionview/lib/action_view/helpers/form_tag_helper.rb +++ b/actionview/lib/action_view/helpers/form_tag_helper.rb @@ -83,13 +83,17 @@ module ActionView # * <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>: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,8 +109,8 @@ 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 @@ -550,6 +554,19 @@ module ActionView # # ==== Options # * Accepts the same options as text_field_tag. + # + # ==== Examples + # color_field_tag 'name' + # # => <input id="name" name="name" type="color" /> + # + # color_field_tag 'color', '#DEF726' + # # => <input id="color" name="color" type="color" value="#DEF726" /> + # + # color_field_tag 'color', nil, class: 'special_input' + # # => <input class="special_input" id="color" name="color" type="color" /> + # + # color_field_tag 'color', '#DEF726', class: 'special_input', disabled: true + # # => <input disabled="disabled" class="special_input" id="color" name="color" type="color" value="#DEF726" /> def color_field_tag(name, value = nil, options = {}) text_field_tag(name, value, options.stringify_keys.update("type" => "color")) end @@ -558,6 +575,19 @@ module ActionView # # ==== Options # * Accepts the same options as text_field_tag. + # + # ==== Examples + # search_field_tag 'name' + # # => <input id="name" name="name" type="search" /> + # + # search_field_tag 'search', 'Enter your search query here' + # # => <input id="search" name="search" type="search" value="Enter your search query here" /> + # + # search_field_tag 'search', nil, class: 'special_input' + # # => <input class="special_input" id="search" name="search" type="search" /> + # + # search_field_tag 'search', 'Enter your search query here', class: 'special_input', disabled: true + # # => <input disabled="disabled" class="special_input" id="search" name="search" type="search" value="Enter your search query here" /> def search_field_tag(name, value = nil, options = {}) text_field_tag(name, value, options.stringify_keys.update("type" => "search")) end @@ -566,6 +596,19 @@ module ActionView # # ==== Options # * Accepts the same options as text_field_tag. + # + # ==== Examples + # telephone_field_tag 'name' + # # => <input id="name" name="name" type="tel" /> + # + # telephone_field_tag 'tel', '0123456789' + # # => <input id="tel" name="tel" type="tel" value="0123456789" /> + # + # telephone_field_tag 'tel', nil, class: 'special_input' + # # => <input class="special_input" id="tel" name="tel" type="tel" /> + # + # telephone_field_tag 'tel', '0123456789', class: 'special_input', disabled: true + # # => <input disabled="disabled" class="special_input" id="tel" name="tel" type="tel" value="0123456789" /> def telephone_field_tag(name, value = nil, options = {}) text_field_tag(name, value, options.stringify_keys.update("type" => "tel")) end @@ -575,6 +618,19 @@ module ActionView # # ==== Options # * Accepts the same options as text_field_tag. + # + # ==== Examples + # date_field_tag 'name' + # # => <input id="name" name="name" type="date" /> + # + # date_field_tag 'date', '01/01/2014' + # # => <input id="date" name="date" type="date" value="01/01/2014" /> + # + # date_field_tag 'date', nil, class: 'special_input' + # # => <input class="special_input" id="date" name="date" type="date" /> + # + # date_field_tag 'date', '01/01/2014', class: 'special_input', disabled: true + # # => <input disabled="disabled" class="special_input" id="date" name="date" type="date" value="01/01/2014" /> def date_field_tag(name, value = nil, options = {}) text_field_tag(name, value, options.stringify_keys.update("type" => "date")) end @@ -638,6 +694,19 @@ module ActionView # # ==== Options # * Accepts the same options as text_field_tag. + # + # ==== Examples + # url_field_tag 'name' + # # => <input id="name" name="name" type="url" /> + # + # url_field_tag 'url', 'http://rubyonrails.org' + # # => <input id="url" name="url" type="url" value="http://rubyonrails.org" /> + # + # url_field_tag 'url', nil, class: 'special_input' + # # => <input class="special_input" id="url" name="url" type="url" /> + # + # url_field_tag 'url', 'http://rubyonrails.org', class: 'special_input', disabled: true + # # => <input disabled="disabled" class="special_input" id="url" name="url" type="url" value="http://rubyonrails.org" /> def url_field_tag(name, value = nil, options = {}) text_field_tag(name, value, options.stringify_keys.update("type" => "url")) end @@ -646,6 +715,19 @@ module ActionView # # ==== Options # * Accepts the same options as text_field_tag. + # + # ==== Examples + # email_field_tag 'name' + # # => <input id="name" name="name" type="email" /> + # + # email_field_tag 'email', 'email@example.com' + # # => <input id="email" name="email" type="email" value="email@example.com" /> + # + # email_field_tag 'email', nil, class: 'special_input' + # # => <input class="special_input" id="email" name="email" type="email" /> + # + # email_field_tag 'email', 'email@example.com', class: 'special_input', disabled: true + # # => <input disabled="disabled" class="special_input" id="email" name="email" type="email" value="email@example.com" /> def email_field_tag(name, value = nil, options = {}) text_field_tag(name, value, options.stringify_keys.update("type" => "email")) end @@ -657,12 +739,40 @@ module ActionView # * <tt>:max</tt> - The maximum acceptable value. # * <tt>:in</tt> - A range specifying the <tt>:min</tt> and # <tt>:max</tt> values. + # * <tt>:within</tt> - Same as <tt>:in</tt>. # * <tt>:step</tt> - The acceptable value granularity. # * Otherwise accepts the same options as text_field_tag. # # ==== Examples + # number_field_tag 'quantity' + # # => <input id="quantity" name="quantity" type="number" /> + # + # number_field_tag 'quantity', '1' + # # => <input id="quantity" name="quantity" type="number" value="1" /> + # + # number_field_tag 'quantity', nil, class: 'special_input' + # # => <input class="special_input" id="quantity" name="quantity" type="number" /> + # + # number_field_tag 'quantity', nil, min: 1 + # # => <input id="quantity" name="quantity" min="1" type="number" /> + # + # number_field_tag 'quantity', nil, max: 9 + # # => <input id="quantity" name="quantity" max="9" type="number" /> + # # number_field_tag 'quantity', nil, in: 1...10 # # => <input id="quantity" name="quantity" min="1" max="9" type="number" /> + # + # number_field_tag 'quantity', nil, within: 1...10 + # # => <input id="quantity" name="quantity" min="1" max="9" type="number" /> + # + # number_field_tag 'quantity', nil, min: 1, max: 10 + # # => <input id="quantity" name="quantity" min="1" max="9" type="number" /> + # + # number_field_tag 'quantity', nil, min: 1, max: 10, step: 2 + # # => <input id="quantity" name="quantity" min="1" max="9" step="2" type="number" /> + # + # number_field_tag 'quantity', '1', class: 'special_input', disabled: true + # # => <input disabled="disabled" class="special_input" id="quantity" name="quantity" type="number" value="1" /> def number_field_tag(name, value = nil, options = {}) options = options.stringify_keys options["type"] ||= "number" @@ -726,9 +836,11 @@ module ActionView method_tag(method) + token_tag(authenticity_token) end - enforce_utf8 = html_options.delete("enforce_utf8") { true } - tags = (enforce_utf8 ? utf8_enforcer_tag : ''.html_safe) << method_tag - content_tag(:div, tags, :style => 'display:none') + if html_options.delete("enforce_utf8") { true } + utf8_enforcer_tag + method_tag + else + method_tag + end end def form_tag_html(html_options) 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/tags/collection_check_boxes.rb b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb index 8b28e4fc33..6242a2a085 100644 --- a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb +++ b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb @@ -28,10 +28,7 @@ module ActionView # Append a hidden field to make sure something will be sent back to the # server if all check boxes are unchecked. if @options.fetch(:include_hidden, true) - hidden_name = @html_options[:name] || "#{tag_name}[]" - hidden = @template_object.hidden_field_tag(hidden_name, "", :id => nil) - - rendered_collection + hidden + rendered_collection + hidden_field else rendered_collection end @@ -42,6 +39,18 @@ module ActionView def render_component(builder) builder.check_box + builder.label end + + def hidden_field + hidden_name = @html_options[:name] + + hidden_name ||= if @options.has_key?(:index) + "#{tag_name_with_index(@options[:index])}[]" + else + "#{tag_name}[]" + end + + @template_object.hidden_field_tag(hidden_name, "", id: nil) + 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/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 89c196e578..894616a449 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -323,7 +323,7 @@ module ActionView inner_tags.safe_concat tag(:input, type: "hidden", name: param_name, value: value.to_param) end end - content_tag('form', content_tag('div', inner_tags), form_options) + content_tag('form', inner_tags, form_options) end # Creates a link tag of the given +name+ using a URL created by the set of diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb index 855fed0190..5fff6b0771 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -114,7 +114,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 +134,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 +228,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 +239,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/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 33be06cbf7..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 @@ -77,16 +79,20 @@ module ActionView case options when String options - when nil, Hash - options ||= {} - options = { :only_path => options[:host].nil? }.merge!(options.symbolize_keys) - super + when nil + super({:only_path => true}) + when Hash + 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/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb index 403824bd8e..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 @@ -228,7 +242,7 @@ module ActionView File.mtime(p) end - # Extract handler and formats from path. If a format cannot be a found neither + # Extract handler, formats and variant from path. If a format cannot be found neither # from the path, or the handler, we should return the array of formats given # to the resolver. def extract_handler_and_format_and_variant(path, default_formats) diff --git a/actionview/lib/action_view/test_case.rb b/actionview/lib/action_view/test_case.rb index 3145446114..9e8e6f43d5 100644 --- a/actionview/lib/action_view/test_case.rb +++ b/actionview/lib/action_view/test_case.rb @@ -235,7 +235,8 @@ module ActionView :@options, :@test_passed, :@view, - :@view_context_class + :@view_context_class, + :@_subscribers ] def _user_defined_ivars 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/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/render_test.rb b/actionview/test/actionpack/controller/render_test.rb index 45b8049b83..ab7b961ed2 100644 --- a/actionview/test/actionpack/controller/render_test.rb +++ b/actionview/test/actionpack/controller/render_test.rb @@ -720,6 +720,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 @@ -1332,4 +1337,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/activerecord/form_helper_activerecord_test.rb b/actionview/test/activerecord/form_helper_activerecord_test.rb index 0a9628da8d..0a62f49f35 100644 --- a/actionview/test/activerecord/form_helper_activerecord_test.rb +++ b/actionview/test/activerecord/form_helper_activerecord_test.rb @@ -59,12 +59,13 @@ class FormHelperActiveRecordTest < ActionView::TestCase protected def hidden_fields(method = nil) - txt = %{<div style="display:none">} - txt << %{<input name="utf8" type="hidden" value="✓" />} + txt = %{<input name="utf8" type="hidden" value="✓" />} + if method && !%w(get post).include?(method.to_s) txt << %{<input name="_method" type="hidden" value="#{method}" />} end - txt << %{</div>} + + txt end def form_text(action = "/", id = nil, html_class = nil, remote = nil, multipart = nil, method = nil) @@ -88,4 +89,4 @@ class FormHelperActiveRecordTest < ActionView::TestCase form_text(action, id, html_class, remote, multipart, method) + hidden_fields(method) + contents + "</form>" end -end
\ No newline at end of file +end 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/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb index 3ca71d3376..343681b5a9 100644 --- a/actionview/test/template/asset_tag_helper_test.rb +++ b/actionview/test/template/asset_tag_helper_test.rb @@ -195,9 +195,9 @@ class AssetTagHelperTest < ActionView::TestCase } FaviconLinkToTag = { - %(favicon_link_tag) => %(<link href="/images/favicon.ico" rel="shortcut icon" type="image/vnd.microsoft.icon" />), - %(favicon_link_tag 'favicon.ico') => %(<link href="/images/favicon.ico" rel="shortcut icon" type="image/vnd.microsoft.icon" />), - %(favicon_link_tag 'favicon.ico', :rel => 'foo') => %(<link href="/images/favicon.ico" rel="foo" type="image/vnd.microsoft.icon" />), + %(favicon_link_tag) => %(<link href="/images/favicon.ico" rel="shortcut icon" type="image/x-icon" />), + %(favicon_link_tag 'favicon.ico') => %(<link href="/images/favicon.ico" rel="shortcut icon" type="image/x-icon" />), + %(favicon_link_tag 'favicon.ico', :rel => 'foo') => %(<link href="/images/favicon.ico" rel="foo" type="image/x-icon" />), %(favicon_link_tag 'favicon.ico', :rel => 'foo', :type => 'bar') => %(<link href="/images/favicon.ico" rel="foo" type="bar" />), %(favicon_link_tag 'mb-icon.png', :rel => 'apple-touch-icon', :type => 'image/png') => %(<link href="/images/mb-icon.png" rel="apple-touch-icon" type="image/png" />) } @@ -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") @@ -588,6 +596,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 +763,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/form_collections_helper_test.rb b/actionview/test/template/form_collections_helper_test.rb index 8107529149..5e991d87ad 100644 --- a/actionview/test/template/form_collections_helper_test.rb +++ b/actionview/test/template/form_collections_helper_test.rb @@ -221,6 +221,13 @@ class FormCollectionsHelperTest < ActionView::TestCase assert_select "input[type=hidden][name='user[other_category_ids][]'][value=]", :count => 1 end + test 'collection check boxes generates a hidden field with index if it was provided' do + collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] + with_collection_check_boxes :user, :category_ids, collection, :id, :name, { index: 322 } + + assert_select "input[type=hidden][name='user[322][category_ids][]'][value=]", count: 1 + end + test 'collection check boxes does not generate a hidden field if include_hidden option is false' do collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] with_collection_check_boxes :user, :category_ids, collection, :id, :name, include_hidden: false diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb index b5e9801776..7b680aac08 100644 --- a/actionview/test/template/form_helper_test.rb +++ b/actionview/test/template/form_helper_test.rb @@ -19,6 +19,9 @@ class FormHelperTest < ActionView::TestCase attributes: { post: { cost: "Total cost" + }, + :"post/language" => { + spanish: "Espanol" } } }, @@ -154,6 +157,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)) @@ -265,6 +274,13 @@ class FormHelperTest < ActionView::TestCase ) end + def test_label_with_block_and_html + assert_dom_equal( + '<label for="post_terms">Accept <a href="/terms">Terms</a>.</label>', + label(:post, :terms) { 'Accept <a href="/terms">Terms</a>.'.html_safe } + ) + end + def test_label_with_block_and_options assert_dom_equal( '<label for="my_for">The title, please:</label>', @@ -1420,7 +1436,7 @@ class FormHelperTest < ActionView::TestCase expected = whole_form("/posts", "new_post", "new_post") do "<input checked='checked' id='post_1_tag_ids_1' name='post[1][tag_ids][]' type='checkbox' value='1' />" + "<label for='post_1_tag_ids_1'>Tag 1</label>" + - "<input name='post[tag_ids][]' type='hidden' value='' />" + "<input name='post[1][tag_ids][]' type='hidden' value='' />" end assert_dom_equal expected, output_buffer @@ -3020,12 +3036,13 @@ class FormHelperTest < ActionView::TestCase protected def hidden_fields(method = nil) - txt = %{<div style="display:none">} - txt << %{<input name="utf8" type="hidden" value="✓" />} + txt = %{<input name="utf8" type="hidden" value="✓" />} + if method && !%w(get post).include?(method.to_s) txt << %{<input name="_method" type="hidden" value="#{method}" />} end - txt << %{</div>} + + txt end def form_text(action = "/", id = nil, html_class = nil, remote = nil, multipart = nil, method = nil) diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb index cf824e2733..18c739674a 100644 --- a/actionview/test/template/form_tag_helper_test.rb +++ b/actionview/test/template/form_tag_helper_test.rb @@ -14,12 +14,15 @@ class FormTagHelperTest < ActionView::TestCase method = options[:method] enforce_utf8 = options.fetch(:enforce_utf8, true) - txt = %{<div style="display:none">} - txt << %{<input name="utf8" type="hidden" value="✓" />} if enforce_utf8 - if method && !%w(get post).include?(method.to_s) - txt << %{<input name="_method" type="hidden" value="#{method}" />} + ''.tap do |txt| + if enforce_utf8 + txt << %{<input name="utf8" type="hidden" value="✓" />} + end + + if method && !%w(get post).include?(method.to_s) + txt << %{<input name="_method" type="hidden" value="#{method}" />} + end end - txt << %{</div>} end def form_text(action = "http://www.example.com", options = {}) 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/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/translation_helper_test.rb b/actionview/test/template/translation_helper_test.rb index c4770840fb..a9d5ea7345 100644 --- a/actionview/test/template/translation_helper_test.rb +++ b/actionview/test/template/translation_helper_test.rb @@ -151,4 +151,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/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb index 7e978e15d2..35279a4558 100644 --- a/actionview/test/template/url_helper_test.rb +++ b/actionview/test/template/url_helper_test.rb @@ -53,12 +53,12 @@ class UrlHelperTest < ActiveSupport::TestCase end def test_button_to_with_straight_url - assert_dom_equal %{<form method="post" action="http://www.example.com" class="button_to"><div><input type="submit" value="Hello" /></div></form>}, button_to("Hello", "http://www.example.com") + assert_dom_equal %{<form method="post" action="http://www.example.com" class="button_to"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com") end def test_button_to_with_path assert_dom_equal( - %{<form method="post" action="/article/Hello" class="button_to"><div><input type="submit" value="Hello" /></div></form>}, + %{<form method="post" action="/article/Hello" class="button_to"><input type="submit" value="Hello" /></form>}, button_to("Hello", article_path("Hello".html_safe)) ) end @@ -67,7 +67,7 @@ class UrlHelperTest < ActiveSupport::TestCase self.request_forgery = true assert_dom_equal( - %{<form method="post" action="http://www.example.com" class="button_to"><div><input type="submit" value="Hello" /><input name="form_token" type="hidden" value="secret" /></div></form>}, + %{<form method="post" action="http://www.example.com" class="button_to"><input type="submit" value="Hello" /><input name="form_token" type="hidden" value="secret" /></form>}, button_to("Hello", "http://www.example.com") ) ensure @@ -75,102 +75,102 @@ class UrlHelperTest < ActiveSupport::TestCase end def test_button_to_with_form_class - assert_dom_equal %{<form method="post" action="http://www.example.com" class="custom-class"><div><input type="submit" value="Hello" /></div></form>}, button_to("Hello", "http://www.example.com", form_class: 'custom-class') + assert_dom_equal %{<form method="post" action="http://www.example.com" class="custom-class"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com", form_class: 'custom-class') end def test_button_to_with_form_class_escapes - assert_dom_equal %{<form method="post" action="http://www.example.com" class="<script>evil_js</script>"><div><input type="submit" value="Hello" /></div></form>}, button_to("Hello", "http://www.example.com", form_class: '<script>evil_js</script>') + assert_dom_equal %{<form method="post" action="http://www.example.com" class="<script>evil_js</script>"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com", form_class: '<script>evil_js</script>') end def test_button_to_with_query - assert_dom_equal %{<form method="post" action="http://www.example.com/q1=v1&q2=v2" class="button_to"><div><input type="submit" value="Hello" /></div></form>}, button_to("Hello", "http://www.example.com/q1=v1&q2=v2") + assert_dom_equal %{<form method="post" action="http://www.example.com/q1=v1&q2=v2" class="button_to"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com/q1=v1&q2=v2") end def test_button_to_with_html_safe_URL - assert_dom_equal %{<form method="post" action="http://www.example.com/q1=v1&q2=v2" class="button_to"><div><input type="submit" value="Hello" /></div></form>}, button_to("Hello", "http://www.example.com/q1=v1&q2=v2".html_safe) + assert_dom_equal %{<form method="post" action="http://www.example.com/q1=v1&q2=v2" class="button_to"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com/q1=v1&q2=v2".html_safe) end def test_button_to_with_query_and_no_name - assert_dom_equal %{<form method="post" action="http://www.example.com?q1=v1&q2=v2" class="button_to"><div><input type="submit" value="http://www.example.com?q1=v1&q2=v2" /></div></form>}, button_to(nil, "http://www.example.com?q1=v1&q2=v2") + assert_dom_equal %{<form method="post" action="http://www.example.com?q1=v1&q2=v2" class="button_to"><input type="submit" value="http://www.example.com?q1=v1&q2=v2" /></form>}, button_to(nil, "http://www.example.com?q1=v1&q2=v2") end def test_button_to_with_javascript_confirm assert_dom_equal( - %{<form method="post" action="http://www.example.com" class="button_to"><div><input data-confirm="Are you sure?" type="submit" value="Hello" /></div></form>}, + %{<form method="post" action="http://www.example.com" class="button_to"><input data-confirm="Are you sure?" type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com", data: { confirm: "Are you sure?" }) ) end def test_button_to_with_javascript_disable_with assert_dom_equal( - %{<form method="post" action="http://www.example.com" class="button_to"><div><input data-disable-with="Greeting..." type="submit" value="Hello" /></div></form>}, + %{<form method="post" action="http://www.example.com" class="button_to"><input data-disable-with="Greeting..." type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com", data: { disable_with: "Greeting..." }) ) end def test_button_to_with_remote_and_form_options assert_dom_equal( - %{<form method="post" action="http://www.example.com" class="custom-class" data-remote="true" data-type="json"><div><input type="submit" value="Hello" /></div></form>}, + %{<form method="post" action="http://www.example.com" class="custom-class" data-remote="true" data-type="json"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com", remote: true, form: { class: "custom-class", "data-type" => "json" }) ) end def test_button_to_with_remote_and_javascript_confirm assert_dom_equal( - %{<form method="post" action="http://www.example.com" class="button_to" data-remote="true"><div><input data-confirm="Are you sure?" type="submit" value="Hello" /></div></form>}, + %{<form method="post" action="http://www.example.com" class="button_to" data-remote="true"><input data-confirm="Are you sure?" type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com", remote: true, data: { confirm: "Are you sure?" }) ) end def test_button_to_with_remote_and_javascript_disable_with assert_dom_equal( - %{<form method="post" action="http://www.example.com" class="button_to" data-remote="true"><div><input data-disable-with="Greeting..." type="submit" value="Hello" /></div></form>}, + %{<form method="post" action="http://www.example.com" class="button_to" data-remote="true"><input data-disable-with="Greeting..." type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com", remote: true, data: { disable_with: "Greeting..." }) ) end def test_button_to_with_remote_false assert_dom_equal( - %{<form method="post" action="http://www.example.com" class="button_to"><div><input type="submit" value="Hello" /></div></form>}, + %{<form method="post" action="http://www.example.com" class="button_to"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com", remote: false) ) end def test_button_to_enabled_disabled assert_dom_equal( - %{<form method="post" action="http://www.example.com" class="button_to"><div><input type="submit" value="Hello" /></div></form>}, + %{<form method="post" action="http://www.example.com" class="button_to"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com", disabled: false) ) assert_dom_equal( - %{<form method="post" action="http://www.example.com" class="button_to"><div><input disabled="disabled" type="submit" value="Hello" /></div></form>}, + %{<form method="post" action="http://www.example.com" class="button_to"><input disabled="disabled" type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com", disabled: true) ) end def test_button_to_with_method_delete assert_dom_equal( - %{<form method="post" action="http://www.example.com" class="button_to"><div><input type="hidden" name="_method" value="delete" /><input type="submit" value="Hello" /></div></form>}, + %{<form method="post" action="http://www.example.com" class="button_to"><input type="hidden" name="_method" value="delete" /><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com", method: :delete) ) end def test_button_to_with_method_get assert_dom_equal( - %{<form method="get" action="http://www.example.com" class="button_to"><div><input type="submit" value="Hello" /></div></form>}, + %{<form method="get" action="http://www.example.com" class="button_to"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com", method: :get) ) end def test_button_to_with_block assert_dom_equal( - %{<form method="post" action="http://www.example.com" class="button_to"><div><button type="submit"><span>Hello</span></button></div></form>}, + %{<form method="post" action="http://www.example.com" class="button_to"><button type="submit"><span>Hello</span></button></form>}, button_to("http://www.example.com") { content_tag(:span, 'Hello') } ) end def test_button_to_with_params assert_dom_equal( - %{<form action="http://www.example.com" class="button_to" method="post"><div><input type="submit" value="Hello" /><input type="hidden" name="foo" value="bar" /><input type="hidden" name="baz" value="quux" /></div></form>}, + %{<form action="http://www.example.com" class="button_to" method="post"><input type="submit" value="Hello" /><input type="hidden" name="foo" value="bar" /><input type="hidden" name="baz" value="quux" /></form>}, button_to("Hello", "http://www.example.com", params: {foo: :bar, baz: "quux"}) ) end diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 0db220ab8f..b94558b65c 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,6 +1,11 @@ +* 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/lib/active_model.rb b/activemodel/lib/active_model.rb index 3e49c34182..feb3d9371d 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -48,6 +48,7 @@ module ActiveModel eager_autoload do autoload :Errors + autoload :StrictValidationFailed, 'active_model/errors' end module Serializers 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/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index 826e89bf9d..4033eb5808 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -52,8 +52,6 @@ module ActiveModel raise end - attr_reader :password - include InstanceMethodsOnActivation if options.fetch(:validations, true) @@ -68,6 +66,7 @@ module ActiveModel 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 +90,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/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/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index a45d912017..c3c61cf50e 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,324 @@ +* 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* + +* When using a custom `join_table` name on a `habtm`, rails was not saving it + on Reflections. This causes a problem when rails loads fixtures, because it + uses the reflections to set database with fixtures. + + Fixes #14845. + + *Kassio Borges* + +* Reset the cache when modifying a Relation with cached Arel. + Additionally display a warning message to make the user aware. + + *Yves Senn* + +* PostgreSQL should internally use `:datetime` consistently for TimeStamp. Assures + different spellings of timestamps are treated the same. + + Example: + + mytimestamp.simplified_type('timestamp without time zone') + # => :datetime + mytimestamp.simplified_type('timestamp(6) without time zone') + # => also :datetime (previously would be :timestamp) + + See #14513. + + *Jefferson Lai* + +* `ActiveRecord::Base.no_touching` no longer triggers callbacks or start empty transactions. + + Fixes #14841. + + *Lucas Mazza* + +* Fix name collision with `Array#select!` with `Relation#select!`. + + Fixes #14752. + + *Earl St Sauver* + +* Fixed unexpected behavior for `has_many :through` associations going through a scoped `has_many`. + + If a `has_many` association is adjusted using a scope, and another `has_many :through` + uses this association, then the scope adjustment is unexpectedly neglected. + + Fixes #14537. + + *Jan Habermann* + +* `@destroyed` should always be set to `false` when an object is duped. + + *Kuldeep Aggarwal* + +* Fixed has_many association to make it support irregular inflections. + + Fixes #8928. + + *arthurnn*, *Javier Goizueta* + +* Fixed a problem where count used with a grouping was not returning a Hash. + + Fixes #14721. + + *Eric Chahin* + +* `sanitize_sql_like` helper method to escape a string for safe use in a SQL + LIKE statement. + + Example: + + class Article + def self.search(term) + where("title LIKE ?", sanitize_sql_like(term)) + end + end + + Article.search("20% _reduction_") + # => Query looks like "... title LIKE '20\% \_reduction\_' ..." + + *Rob Gilson*, *Yves Senn* + +* Do not quote uuid default value on `change_column`. + + Fixes #14604. + + *Eric Chahin* + * The comparison between `Relation` and `CollectionProxy` should be consistent. Example: @@ -53,7 +374,7 @@ * Auto-generate stable fixture UUIDs on PostgreSQL. - Fixes: #11524 + Fixes #11524. *Roderick van Domburg* @@ -171,12 +492,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`. @@ -255,6 +570,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: diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc index e04abe9b37..c813b22f3b 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 @@ -19,9 +19,11 @@ 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,11 +31,9 @@ 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') 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/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 0d5313956b..45c275a017 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -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) @@ -249,10 +249,10 @@ module ActiveRecord 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/association_relation.rb b/activerecord/lib/active_record/association_relation.rb index 45f1b07f69..5a84792f45 100644 --- a/activerecord/lib/active_record/association_relation.rb +++ b/activerecord/lib/active_record/association_relation.rb @@ -9,14 +9,6 @@ module ActiveRecord @association end - def size - @association.size - end - - def empty? - @association.empty? - end - def ==(other) other == to_a end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 4abe2ad0a0..07dfc448e7 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, @@ -773,11 +778,16 @@ module ActiveRecord # like this can have unintended consequences. # 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 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: + # 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 = '1'") + # + # 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' @@ -1567,14 +1577,22 @@ module ActiveRecord scope = nil end + habtm_reflection = ActiveRecord::Reflection::AssociationReflection.new(:has_and_belongs_to_many, 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 @@ -1590,11 +1608,12 @@ module ActiveRecord hm_options[:through] = middle_reflection.name hm_options[:source] = join_model.right_reflection.name - [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate].each do |k| + [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table].each do |k| hm_options[k] = options[k] if options.key? k 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..4a04303fb8 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: diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 27fd9e35db..572f556999 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -1,12 +1,34 @@ module ActiveRecord module Associations class AssociationScope #:nodoc: - INSTANCE = new - def self.scope(association, connection) INSTANCE.scope association, connection end + class BindSubstitution + def initialize(block) + @block = block + end + + def bind_value(scope, column, value, alias_tracker) + substitute = alias_tracker.connection.substitute_at( + column, scope.bind_values.length) + scope.bind_values += [[column, @block.call(value)]] + substitute + end + end + + def self.create(&block) + block = block ? block : lambda { |val| val } + new BindSubstitution.new(block) + end + + def initialize(bind_substitution) + @bind_substitution = bind_substitution + end + + INSTANCE = create + def scope(association, connection) klass = association.klass reflection = association.reflection @@ -22,6 +44,23 @@ module ActiveRecord Arel::Nodes::InnerJoin end + def self.get_bind_values(owner, chain) + bvs = [] + chain.each_with_index do |reflection, i| + if reflection == chain.last + bvs << reflection.join_id_for(owner) + if reflection.type + bvs << owner.class.base_class.name + end + else + if reflection.type + bvs << chain[i + 1].klass.base_class.name + end + end + end + bvs + end + private def construct_tables(chain, klass, refl, alias_tracker) @@ -49,10 +88,7 @@ module ActiveRecord end def bind_value(scope, column, value, alias_tracker) - substitute = alias_tracker.connection.substitute_at( - column, scope.bind_values.length) - scope.bind_values += [[column, value]] - substitute + @bind_substitution.bind_value scope, column, value, alias_tracker end def bind(scope, table_name, column_name, value, tracker) @@ -120,6 +156,7 @@ module ActiveRecord end scope.where_values += item.where_values + scope.bind_values += item.bind_values scope.order_values |= item.order_values end end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 8272a5584c..1edd4fa3aa 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -31,6 +31,14 @@ module ActiveRecord @updated end + def decrement_counters # :nodoc: + with_cache_name { |name| decrement_counter name } + end + + def increment_counters # :nodoc: + with_cache_name { |name| increment_counter name } + end + private def find_target? @@ -51,13 +59,15 @@ module ActiveRecord end end - def decrement_counters - with_cache_name { |name| decrement_counter name } + def decrement_counter(counter_cache_name) + if foreign_key_present? + klass.decrement_counter(counter_cache_name, target_id) + end end - def decrement_counter counter_cache_name + def increment_counter(counter_cache_name) if foreign_key_present? - klass.decrement_counter(counter_cache_name, target_id) + klass.increment_counter(counter_cache_name, target_id) end end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 5ccaa55a32..3998aca23e 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -26,28 +26,9 @@ module ActiveRecord::Associations::Builder private def self.add_counter_cache_methods(mixin) - return if mixin.method_defined? :belongs_to_counter_cache_after_create + return if mixin.method_defined? :belongs_to_counter_cache_after_update mixin.class_eval do - def belongs_to_counter_cache_after_create(reflection) - if record = send(reflection.name) - cache_column = reflection.counter_cache_column - record.class.increment_counter(cache_column, record.id) - @_after_create_counter_called = true - end - end - - def belongs_to_counter_cache_before_destroy(reflection) - foreign_key = reflection.foreign_key.to_sym - unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key - record = send reflection.name - if record && !self.destroyed? - cache_column = reflection.counter_cache_column - record.class.decrement_counter(cache_column, record.id) - end - end - end - def belongs_to_counter_cache_after_update(reflection) foreign_key = reflection.foreign_key cache_column = reflection.counter_cache_column @@ -73,14 +54,6 @@ module ActiveRecord::Associations::Builder def self.add_counter_cache_callbacks(model, reflection) cache_column = reflection.counter_cache_column - model.after_create lambda { |record| - record.belongs_to_counter_cache_after_create(reflection) - } - - model.before_destroy lambda { |record| - record.belongs_to_counter_cache_before_destroy(reflection) - } - model.after_update lambda { |record| record.belongs_to_counter_cache_after_update(reflection) } @@ -130,7 +103,7 @@ 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 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..0ad5206980 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,7 +11,7 @@ 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 @@ -23,7 +23,13 @@ module ActiveRecord::Associations::Builder KnownTable.new options[:join_table].to_s else class_name = options.fetch(:class_name) { - name.to_s.camelize.singularize + model_name = name.to_s.camelize.singularize + + if lhs_class.parent_name + model_name.prepend("#{lhs_class.parent_name}::") + end + + model_name } KnownClass.new lhs_class, class_name end @@ -60,13 +66,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_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index 7909b93622..4c8c826f76 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -5,7 +5,7 @@ module ActiveRecord::Associations::Builder end def valid_options - super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache] + super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table] end def self.valid_dependent_options diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 803e3ab9ab..c5f7bcae7d 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -194,7 +194,7 @@ module ActiveRecord options[:dependent] end - delete(:all, dependent: dependent).tap do + delete_or_nullify_all_records(dependent).tap do reset loaded! end @@ -244,19 +244,12 @@ 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] - if records.first == :all - if (loaded? || dependent == :destroy) && dependent != :delete_all - delete_or_destroy(load_target, dependent) - else - delete_records(:all, dependent) - end - else - records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) } - delete_or_destroy(records, dependent) - end + records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) } + delete_or_destroy(records, dependent) end # Deletes the +records+ and removes them from this association calling @@ -265,6 +258,7 @@ module ActiveRecord # 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 @@ -412,9 +406,23 @@ module ActiveRecord end private + def get_records + return scope.to_a if reflection.scope_chain.any?(&:any?) + + conn = klass.connection + sc = reflection.association_scope_cache(conn, owner) do + StatementCache.create(conn) { |params| + as = AssociationScope.create { params.bind } + target_scope.merge as.scope(self, conn) + } + end + + binds = AssociationScope.get_bind_values(owner, reflection.chain) + sc.execute binds, klass, klass.connection + end def find_target - records = scope.to_a + records = get_records records.each { |record| set_inverse_instance(record) } records 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..2727e23870 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -100,28 +100,33 @@ 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| + 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 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 73baefb8e1..35ad512537 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 @@ -72,13 +73,11 @@ 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 @@ -131,13 +130,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) @@ -202,7 +201,7 @@ module ActiveRecord def find_target return [] unless target_reflection_has_associated_record? - scope.to_a + get_records end # NOTE - not sure that we can actually cope with inverses here diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 94f69d4c2d..01173b68f3 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -164,17 +164,17 @@ module ActiveRecord def make_outer_joins(parent, child) tables = table_aliases_for(parent, child) join_type = Arel::Nodes::OuterJoin - joins = make_constraints parent, child, tables, join_type + info = make_constraints parent, child, tables, join_type - joins.concat child.children.flat_map { |c| make_outer_joins(child, c) } + [info] + child.children.flat_map { |c| make_outer_joins(child, c) } end def make_inner_joins(parent, child) tables = child.tables join_type = Arel::Nodes::InnerJoin - joins = make_constraints parent, child, tables, join_type + info = make_constraints parent, child, tables, join_type - joins.concat child.children.flat_map { |c| make_inner_joins(child, c) } + [info] + child.children.flat_map { |c| make_inner_joins(child, c) } end def table_aliases_for(parent, node) @@ -207,7 +207,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,6 +215,7 @@ module ActiveRecord associations.map do |name, right| reflection = find_reflection base_klass, name reflection.check_validity! + reflection.check_eager_loadable! if reflection.options[:polymorphic] raise EagerLoadPolymorphicError.new(reflection) 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 1d923ecc09..a0e83c0a02 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -21,8 +21,11 @@ module ActiveRecord super && reflection == other.reflection end + JoinInformation = Struct.new :joins, :binds + def join_constraints(foreign_table, foreign_klass, node, join_type, tables, scope_chain, chain) joins = [] + bind_values = [] tables = tables.reverse scope_chain_index = 0 @@ -60,21 +63,27 @@ module ActiveRecord left.merge right end - if reflection.type - constraint = constraint.and table[reflection.type].eq foreign_klass.base_class.name - end - if rel && !rel.arel.constraints.empty? + bind_values.concat rel.bind_values constraint = constraint.and rel.arel.constraints end + if reflection.type + value = foreign_klass.base_class.name + column = klass.columns_hash[column.to_s] + + substitute = klass.connection.substitute_at(column, bind_values.length) + bind_values.push [column, value] + constraint = constraint.and table[reflection.type].eq substitute + end + joins << table.create_join(table, table.create_on(constraint), join_type) # The current table in this iteration becomes the foreign table in the next foreign_table, foreign_klass = table, klass end - joins + JoinInformation.new joins, bind_values end # Builds equality condition. diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 31ddf4e0fc..7519fec10a 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -80,7 +80,7 @@ module ActiveRecord # { author: :avatar } # [ :books, { author: :avatar } ] - NULL_RELATION = Struct.new(:values).new({}) + NULL_RELATION = Struct.new(:values, :bind_values).new({}, []) def preload(records, associations, preload_scope = nil) records = Array.wrap(records).compact.uniq @@ -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 69b65982b3..63773bd5e1 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.column_types[association_key_name.to_s].type + end + + def owner_key_type + @model.column_types[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 @@ -111,12 +132,15 @@ module ActiveRecord scope = klass.unscoped values = reflection_scope.values + reflection_binds = reflection_scope.bind_values preload_values = preload_scope.values + preload_binds = preload_scope.bind_values scope.where_values = Array(values[:where]) + Array(preload_values[:where]) scope.references_values = Array(values[:references]) + Array(preload_values[:references]) + scope.bind_values = (reflection_binds + preload_binds) - scope.select! preload_values[:select] || values[:select] || table[Arel.star] + scope._select! preload_values[:select] || values[:select] || table[Arel.star] scope.includes! preload_values[:includes] || values[:includes] if preload_values.key? :order diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 70e97432e4..1fed7f74e7 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -84,7 +84,7 @@ module ActiveRecord end scope.references! reflection_scope.values[:references] - scope.order! reflection_scope.values[:order] if scope.eager_loading? + scope = scope.order reflection_scope.values[:order] if scope.eager_loading? end scope diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index 747bb5f1d6..f2e3a4e40f 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -38,8 +38,23 @@ module ActiveRecord scope.scope_for_create.stringify_keys.except(klass.primary_key) end + def get_records + return scope.limit(1).to_a if reflection.scope_chain.any?(&:any?) + + conn = klass.connection + sc = reflection.association_scope_cache(conn, owner) do + StatementCache.create(conn) { |params| + as = AssociationScope.create { params.bind } + target_scope.merge(as.scope(self, conn)).limit(1) + } + end + + binds = AssociationScope.get_bind_values(owner, reflection.chain) + sc.execute binds, klass, klass.connection + end + def find_target - if record = scope.take + if record = get_records.first set_inverse_instance record end end diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index ba7d2a3782..f8a85b8a6f 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -14,9 +14,11 @@ module ActiveRecord def target_scope scope = super chain.drop(1).each do |reflection| + relation = reflection.klass.all + relation.merge!(reflection.scope) if reflection.scope + scope.merge!( - reflection.klass.all. - except(:select, :create_with, :includes, :preload, :joins, :eager_load) + relation.except(:select, :create_with, :includes, :preload, :joins, :eager_load) ) end scope diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index 30fa2c8ba5..816fb51942 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" } + # cat.assign_attributes(status: "sleeping") + # cat.attributes # => { "name" => "Gorby", "status" => "sleeping" } + # + # 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." @@ -140,7 +149,7 @@ 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 the column is a time set the values to their defaults as January 1, 1970, but only if they're nil diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 4b1733619a..a0a0214eae 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -48,7 +48,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,6 +70,7 @@ module ActiveRecord # Generates all the attribute related methods for columns in the database # accessors, mutators and query methods. def define_attribute_methods # :nodoc: + return false if @attribute_methods_generated # Use a mutex; we don't want two thread simultaneously trying to define # attribute methods. generated_attribute_methods.synchronize do @@ -200,6 +205,7 @@ module ActiveRecord # 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 + return super unless respond_to_missing?(method, true) send(method, *args, &block) end else @@ -326,13 +332,13 @@ 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?) @@ -359,6 +365,8 @@ module ActiveRecord # "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 @@ -453,7 +461,7 @@ module ActiveRecord end def pk_attribute?(name) - column_for_attribute(name).primary + name == self.class.primary_key end def typecasted_attribute_value(name) diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index d01e9aea59..979dfb207e 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -35,7 +35,7 @@ module ActiveRecord extend ActiveSupport::Concern - ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date] + ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :time, :date] included do class_attribute :attribute_types_cached_by_default, instance_writer: false @@ -52,7 +52,7 @@ module ActiveRecord end # Returns the attributes which are cached. By default time related columns - # with datatype <tt>:datetime, :timestamp, :time, :date</tt> are cached. + # with datatype <tt>:datetime, :time, :date</tt> are cached. def cached_attributes @cached_attributes ||= columns.select { |c| cacheable_column?(c) }.map { |col| col.name }.to_set end diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 67abbbc2a0..47c6f94ba7 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -30,7 +30,8 @@ module ActiveRecord # ==== Parameters # # * +attr_name+ - The field name that should be serialized. - # * +class_name+ - Optional, class name that the object type should be equal to. + # * +class_name_or_coder+ - Optional, a coder object, which responds to `.load` / `.dump` + # or a class name that the object type should be equal to. # # ==== Example # @@ -38,13 +39,23 @@ module ActiveRecord # class User < ActiveRecord::Base # serialize :preferences # end - def serialize(attr_name, class_name = Object) + # + # # Serialize preferences using JSON as coder. + # class User < ActiveRecord::Base + # serialize :preferences, JSON + # end + # + # # Serialize preferences as Hash using YAML coder. + # class User < ActiveRecord::Base + # serialize :preferences, Hash + # end + def serialize(attr_name, class_name_or_coder = Object) include Behavior - coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) } - class_name + coder = if [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) } + class_name_or_coder else - Coders::YAMLColumn.new(class_name) + Coders::YAMLColumn.new(class_name_or_coder) end # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy @@ -54,6 +65,8 @@ module ActiveRecord end class Type # :nodoc: + delegate :type, :type_cast_for_database, to: :@column + def initialize(column) @column = column end @@ -66,10 +79,6 @@ module ActiveRecord end end - def type - @column.type - end - def accessor ActiveRecord::Store::IndifferentHashAccessor end @@ -131,6 +140,14 @@ module ActiveRecord end end + def raw_type_cast_attribute_for_write(column, value) + if column && coder = self.class.serialized_attributes[column.name] + Attribute.new(coder, value, :serialized) + else + super + end + end + def _field_changed?(attr, old, value) if self.class.serialized_attributes.include?(attr) old != value 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..6149ac4906 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -2,6 +2,8 @@ module ActiveRecord module AttributeMethods module TimeZoneConversion class Type # :nodoc: + delegate :type, :type_cast_for_database, to: :@column + def initialize(column) @column = column end @@ -10,10 +12,6 @@ module ActiveRecord value = @column.type_cast(value) value.acts_like?(:time) ? value.in_time_zone : value end - - def type - @column.type - end end extend ActiveSupport::Concern @@ -28,7 +26,7 @@ module ActiveRecord module ClassMethods protected - # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. + # Defined for all +datetime+ attributes when +time_zone_aware_attributes+ are enabled. # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone. def define_method_attribute=(attr_name) if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) @@ -51,7 +49,7 @@ module ActiveRecord def create_time_zone_conversion_attribute?(name, column) time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && - (:datetime == column.type || :timestamp == column.type) + (:datetime == column.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..56441d7324 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -55,6 +55,27 @@ module ActiveRecord # specified +value+. Empty strings for fixnum and float columns are # turned into +nil+. def write_attribute(attr_name, value) + write_attribute_with_type_cast(attr_name, value, :type_cast_attribute_for_write) + end + + def raw_write_attribute(attr_name, value) + write_attribute_with_type_cast(attr_name, value, :raw_type_cast_attribute_for_write) + end + + private + # Handle *= for method_missing. + def attribute=(attribute_name, value) + write_attribute(attribute_name, value) + end + + def type_cast_attribute_for_write(column, value) + return value unless column + + column.type_cast_for_write value + end + alias_method :raw_type_cast_attribute_for_write, :type_cast_attribute_for_write + + def write_attribute_with_type_cast(attr_name, value, type_cast_method) 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) @@ -67,24 +88,11 @@ module ActiveRecord end if column || @attributes.has_key?(attr_name) - @attributes[attr_name] = type_cast_attribute_for_write(column, value) + @attributes[attr_name] = send(type_cast_method, column, value) else raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'" end end - alias_method :raw_write_attribute, :write_attribute - - private - # Handle *= for method_missing. - def attribute=(attribute_name, value) - write_attribute(attribute_name, value) - end - - def type_cast_attribute_for_write(column, value) - return value unless column - - column.type_cast_for_write value - end end end end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index f149d8f127..74e2a8e6b9 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,28 @@ 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) } + # 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) } 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 +272,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 +305,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}" @@ -380,15 +382,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 1d47cba234..8b0fffcf06 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -19,6 +19,7 @@ require 'active_record/errors' require 'active_record/log_subscriber' require 'active_record/explain_subscriber' require 'active_record/relation/delegation' +require 'active_record/properties' module ActiveRecord #:nodoc: # = Active Record @@ -296,7 +297,6 @@ module ActiveRecord #:nodoc: include Core include Persistence - include NoTouching include ReadonlyAttributes include ModelSchema include Inheritance @@ -318,9 +318,11 @@ module ActiveRecord #:nodoc: include NestedAttributes include Aggregations include Transactions + include NoTouching include Reflection include Serialization include Store + include Properties end ActiveSupport.run_load_hooks(:active_record, Base) 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 47f2ad9b10..7ff5001796 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -9,15 +9,23 @@ module ActiveRecord # Converts an arel AST to SQL def to_sql(arel, binds = []) if arel.respond_to?(:ast) - binds = binds.dup - visitor.accept(arel.ast) do - quote(*binds.shift.reverse) - end + collected = visitor.accept(arel.ast, collector) + collected.compile(binds.dup, self) else arel end end + # This is used in the StatementCache object. It returns an object that + # can be used to query the database repeatedly. + def cacheable_query(arel) # :nodoc: + if prepared_statements + ActiveRecord::StatementCache.query visitor, arel.ast + else + ActiveRecord::StatementCache.partial_query visitor, arel.ast, collector + end + end + # Returns an ActiveRecord::Result instance. def select_all(arel, name = nil, binds = []) arel, binds = binds_from_relation arel, binds @@ -219,6 +227,10 @@ module ActiveRecord end end + def open_transactions + @transaction.number + end + def current_transaction #:nodoc: @transaction end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 75501852ed..f836e60988 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -9,26 +9,22 @@ module ActiveRecord # records are quoted as their primary key return value.quoted_id if value.respond_to?(:quoted_id) + # FIXME: The only case we get an object other than nil or a real column + # is `SchemaStatements#add_column` with a PG array that has a non-empty default + # value. Is this really the only case? Are we missing tests for other types? + # We should have a real column object passed (or nil) here, and check for that + # instead + if column.respond_to?(:type_cast_for_database) + value = column.type_cast_for_database(value) + end + 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. + "'#{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)}'" @@ -47,30 +43,25 @@ 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 + # FIXME: The only case we get an object other than nil or a real column + # is `SchemaStatements#add_column` with a PG array that has a non-empty default + # value. Is this really the only case? Are we missing tests for other types? + # We should have a real column object passed (or nil) here, and check for that + # instead + if column.respond_to?(:type_cast_for_database) + value = column.type_cast_for_database(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 + case value + when Symbol, ActiveSupport::Multibyte::Chars + value.to_s + when true then unquoted_true + when false then unquoted_false + # BigDecimals need to be put in a non-normalized form and quoted. when BigDecimal then value.to_s('F') - when Numeric then value when Date, Time then quoted_date(value) - when Symbol then value.to_s + when *types_which_need_no_typecasting + value else to_type = column ? " to #{column.type}" : "" raise TypeError, "can't cast #{value.class}#{to_type}" @@ -109,10 +100,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 +123,12 @@ module ActiveRecord value.to_s(:db) end + + private + + def types_which_need_no_typecasting + [nil, Numeric, String] + 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..117c0f0969 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -99,6 +99,8 @@ 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 @@ -123,17 +125,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 +165,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 +215,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 @@ -264,6 +262,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,6 +293,12 @@ module ActiveRecord def native @native end + + def aliased_types + HashWithIndifferentAccess.new( + timestamp: :datetime, + ) + end end class AlterTable # :nodoc: 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..ac14740cfe 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -20,15 +20,8 @@ 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 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..ffa6af6d99 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]) && diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index ffd5055dec..a8e0cb93a0 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -1,11 +1,14 @@ 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' require 'active_record/connection_adapters/abstract/schema_creation' require 'monitor' +require 'arel/collectors/bind' +require 'arel/collectors/sql_string' module ActiveRecord module ConnectionAdapters # :nodoc: @@ -90,6 +93,8 @@ module ActiveRecord end end + attr_reader :prepared_statements + def initialize(connection, logger = nil, pool = nil) #:nodoc: super() @@ -103,6 +108,26 @@ module ActiveRecord @prepared_statements = false end + class BindCollector < Arel::Collectors::Bind + def compile(bvs, conn) + super(bvs.map { |bv| conn.quote(*bv.reverse) }) + end + end + + class SQLString < Arel::Collectors::SQLString + def compile(bvs, conn) + super(bvs) + end + end + + def collector + if prepared_statements + SQLString.new + else + BindCollector.new + end + end + def valid_type?(type) true end @@ -128,16 +153,11 @@ module ActiveRecord @owner = nil end - def unprepared_visitor - self.class::BindSubstitution.new self - end - def unprepared_statement old_prepared_statements, @prepared_statements = @prepared_statements, false - old_visitor, @visitor = @visitor, unprepared_visitor yield ensure - @visitor, @prepared_statements = old_visitor, old_prepared_statements + @prepared_statements = old_prepared_statements end # Returns the human-readable name of the adapter. Use mixed case - one @@ -305,10 +325,6 @@ module ActiveRecord @connection end - def open_transactions - @transaction.number - end - def create_savepoint(name = nil) end @@ -318,13 +334,14 @@ module ActiveRecord def release_savepoint(name = nil) end - def case_sensitive_modifier(node) + def case_sensitive_modifier(node, table_attribute) node end def case_sensitive_comparison(table, attribute, column, value) - value = case_sensitive_modifier(value) unless value.nil? - table[attribute].eq(value) + table_attr = table[attribute] + value = case_sensitive_modifier(value, table_attr) unless value.nil? + table_attr.eq(value) end def case_insensitive_comparison(table, attribute, column, value) @@ -340,8 +357,76 @@ module ActiveRecord pool.checkin self end + def type_map # :nodoc: + @type_map ||= Type::TypeMap.new.tap do |mapping| + initialize_type_map(mapping) + end + end + protected + def lookup_cast_type(sql_type) # :nodoc: + type_map.lookup(sql_type) + end + + 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 @@ -368,7 +453,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 20eea208ec..82e62786ca 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -56,11 +56,11 @@ 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) end def extract_default(default) @@ -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, @@ -173,7 +131,6 @@ module ActiveRecord :float => { :name => "float" }, :decimal => { :name => "decimal" }, :datetime => { :name => "datetime" }, - :timestamp => { :name => "datetime" }, :time => { :name => "time" }, :date => { :name => "date" }, :binary => { :name => "blob" }, @@ -183,21 +140,18 @@ module ActiveRecord INDEX_TYPES = [:fulltext, :spatial] INDEX_USINGS = [:btree, :hash] - class BindSubstitution < Arel::Visitors::MySQL # :nodoc: - include Arel::Visitors::BindVisitor - end - # FIXME: Make the first parameter more similar for the two adapters def initialize(connection, logger, connection_options, config) super(connection, logger) @connection_options, @config = connection_options, config @quoted_column_names, @quoted_table_names = {}, {} + @visitor = Arel::Visitors::MySQL.new self + if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @prepared_statements = true - @visitor = Arel::Visitors::MySQL.new self else - @visitor = unprepared_visitor + @prepared_statements = false end end @@ -224,17 +178,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: # @@ -263,9 +206,9 @@ 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, sql_type, null, collation, extra = "") # :nodoc: + cast_type = lookup_cast_type(sql_type) + 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 @@ -280,8 +223,6 @@ module ActiveRecord 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") else super end @@ -299,10 +240,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: @@ -318,6 +267,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) } @@ -610,7 +564,8 @@ module ActiveRecord pk_and_sequence && pk_and_sequence.first end - def case_sensitive_modifier(node) + def case_sensitive_modifier(node, table_attribute) + node = Arel::Nodes.build_quoted node, table_attribute Arel::Nodes::Bin.new(node) end @@ -644,6 +599,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) @@ -713,15 +696,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 @@ -759,30 +740,23 @@ module ActiveRecord 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 - 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' + if strict_mode? && !variables.has_key?('sql_mode') + variables['sql_mode'] = 'STRICT_ALL_TABLES' end # NAMES does not have an equals sign, see diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 187eefb9e4..a62617ab47 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -13,107 +13,33 @@ 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, :default, :cast_type, :null, :sql_type, :default_function - alias :encoded? :coder + delegate :type, :precision, :scale, :limit, :klass, :text?, :number?, :binary?, + :type_cast, :type_cast_for_write, :type_cast_for_database, 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_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 @@ -125,174 +51,6 @@ module ActiveRecord def extract_default(default) type_cast(default) end - - class << self - # Used to convert from BLOBs to Strings - def binary_to_string(value) - value - 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 a8ab52be74..b79d1a4458 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -62,8 +62,8 @@ module ActiveRecord # Converts the query parameters of the URI into a hash. # - # "localhost?pool=5&reap_frequency=2" - # # => { "pool" => "5", "reap_frequency" => "2" } + # "localhost?pool=5&reaping_frequency=2" + # # => { "pool" => "5", "reaping_frequency" => "2" } # # returns empty hash if no query present. # diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 5e82fdcbe0..0a14cdfe89 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -29,18 +29,11 @@ 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) super - @visitor = BindSubstitution.new self + @prepared_statements = false configure_connection end @@ -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 diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index e6aa2ba921..909bba8c7d 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 + 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 + 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| 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 index 551a9289c3..f7bad20f00 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb @@ -1,32 +1,12 @@ module ActiveRecord module ConnectionAdapters - class PostgreSQLColumn < Column - module Cast - def point_to_string(point) + module PostgreSQL + module Cast # :nodoc: + def point_to_string(point) # :nodoc: "(#{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) + def string_to_bit(value) # :nodoc: case value when /^0x/i value[2..-1].hex.to_s(2) # Hexadecimal notation @@ -35,7 +15,7 @@ module ActiveRecord end end - def hstore_to_string(object, array_member = false) + def hstore_to_string(object, array_member = false) # :nodoc: if Hash === object string = object.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(',') string = escape_hstore(string) if array_member @@ -45,7 +25,7 @@ module ActiveRecord end end - def string_to_hstore(string) + def string_to_hstore(string) # :nodoc: if string.nil? nil elsif String === string @@ -59,7 +39,7 @@ module ActiveRecord end end - def json_to_string(object) + def json_to_string(object) # :nodoc: if Hash === object || Array === object ActiveSupport::JSON.encode(object) else @@ -67,7 +47,7 @@ module ActiveRecord end end - def array_to_string(value, column, adapter) + def array_to_string(value, column, adapter) # :nodoc: casted_values = value.map do |val| if String === val if val == "NULL" @@ -82,13 +62,13 @@ module ActiveRecord "{#{casted_values.join(',')}}" end - def range_to_string(object) + def range_to_string(object) # :nodoc: 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) + def string_to_json(string) # :nodoc: if String === string ActiveSupport::JSON.decode(string) else @@ -96,7 +76,7 @@ module ActiveRecord end end - def string_to_cidr(string) + def string_to_cidr(string) # :nodoc: if string.nil? nil elsif String === string @@ -110,7 +90,7 @@ module ActiveRecord end end - def cidr_to_string(object) + def cidr_to_string(object) # :nodoc: if IPAddr === object "#{object.to_s}/#{object.instance_variable_get(:@mask_addr).to_s(2).count('1')}" else @@ -118,7 +98,7 @@ module ActiveRecord end end - def string_to_array(string, oid) + def string_to_array(string, oid) # :nodoc: parse_pg_array(string).map {|val| type_cast_array(oid, val)} 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..9a5e2d05ef 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -1,158 +1,44 @@ +require 'active_record/connection_adapters/postgresql/cast' + module ActiveRecord module ConnectionAdapters # PostgreSQL-specific extensions to column definitions in a table. 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) + super(name, default, cast_type, sql_type, null) end - @default_function = default if has_default_function?(default_value, default) - end - - def number? - !array && super - end - - def text? - !array && super + @default_function = default_function 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 + include PostgreSQL::Cast - 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 + # Loads pg_array_parser if available. String parsing can be + # performed quicker by a native extension, which will not create + # a large amount of Ruby objects that will need to be garbage + # collected. pg_array_parser has a C and Java extension + begin + require 'pg_array_parser' + include PgArrayParser + rescue LoadError + require 'active_record/connection_adapters/postgresql/array_parser' + include PostgreSQL::ArrayParser end end - - 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 - end - - def type_cast(value) - return if value.nil? - return super if encoded? - - @oid_type.type_cast value - end + # :startdoc: def accessor - @oid_type.accessor + cast_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 a5fb048b1e..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)}" @@ -44,10 +44,32 @@ module ActiveRecord end end + def select_value(arel, name = nil, binds = []) + arel, binds = binds_from_relation arel, binds + sql = to_sql(arel, binds) + execute_and_clear(sql, name, binds) do |result| + result.getvalue(0, 0) if result.ntuples > 0 && result.nfields > 0 + end + end + + def select_values(arel, name = nil) + arel, binds = binds_from_relation arel, [] + sql = to_sql(arel, binds) + execute_and_clear(sql, name, binds) do |result| + if result.nfields > 0 + result.column_values(0) + else + [] + end + end + end + # Executes a SELECT query and returns an array of rows. Each row is an # array of field values. def select_rows(sql, name = nil, binds = []) - exec_query(sql, name, binds).rows + execute_and_clear(sql, name, binds) do |result| + result.values + end end # Executes an INSERT query and returns the new record's ID @@ -72,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 @@ -134,28 +161,20 @@ module ActiveRecord end def exec_query(sql, name = 'SQL', binds = []) - result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) : - exec_cache(sql, name, binds) - - types = {} - fields = result.fields - fields.each_with_index do |fname, i| - ftype = result.ftype i - fmod = result.fmod i - types[fname] = get_oid_type(ftype, fmod, fname) + execute_and_clear(sql, name, binds) do |result| + types = {} + fields = result.fields + fields.each_with_index do |fname, i| + ftype = result.ftype i + fmod = result.fmod i + types[fname] = get_oid_type(ftype, fmod, fname) + end + ActiveRecord::Result.new(fields, result.values, types) end - - ret = ActiveRecord::Result.new(fields, result.values, types) - result.clear - return ret end def exec_delete(sql, name = 'SQL', binds = []) - result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) : - exec_cache(sql, name, binds) - affected = result.cmd_tuples - result.clear - affected + execute_and_clear(sql, name, binds) {|result| result.cmd_tuples } end alias :exec_update :exec_delete diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 9e898015a6..2494e19f84 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -1,445 +1,32 @@ -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/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/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/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) - case sql_type - when /^timestamp with(?:out)? time zone$/ - :datetime - else - :timestamp - end - 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..0e9dcd8c0c --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -0,0 +1,24 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Array < Type::Value + attr_reader :subtype + delegate :type, to: :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 + 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..9b2d887d07 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb @@ -0,0 +1,17 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Bit < Type::String + def type_cast(value) + if ::String === value + ConnectionAdapters::PostgreSQLColumn.string_to_bit value + else + value + end + 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..36c53d8732 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Bytea < Type::Binary + def cast_value(value) + PGconn.unescape_bytea value + 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..507c3a62b0 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb @@ -0,0 +1,17 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Cidr < Type::Value + def type + :cidr + end + + def cast_value(value) + ConnectionAdapters::PostgreSQLColumn.string_to_cidr value + 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..3c30ad5fec --- /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 + 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..9ccbf71159 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb @@ -0,0 +1,26 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class DateTime < Type::DateTime + 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$/ + super("-" + value.sub(/ BC$/, "")) + 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..ed4b8911d9 --- /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 + 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..5fed8b0f89 --- /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 + 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..9753d71461 --- /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 + include Infinity + + def type_cast(value) + case value + when nil then nil + 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..bf680b6624 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb @@ -0,0 +1,31 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Hstore < Type::Value + def type + :hstore + end + + def type_cast_for_write(value) + # roundtrip to ensure uniform uniform types + # TODO: This is not an efficient solution. + cast_value(type_cast_for_database(value)) + end + + def type_cast_for_database(value) + ConnectionAdapters::PostgreSQLColumn.hstore_to_string(value) + end + + def cast_value(value) + ConnectionAdapters::PostgreSQLColumn.string_to_hstore value + end + + def accessor + ActiveRecord::Store::StringKeyedHashAccessor + 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..7ed8f5f031 --- /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 + 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..d438ffa140 --- /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 + 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..388d3dd9ed --- /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 + 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..42a5110ffd --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb @@ -0,0 +1,31 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Json < Type::Value + def type + :json + end + + def type_cast_for_write(value) + # roundtrip to ensure uniform uniform types + # TODO: This is not an efficient solution. + cast_value(type_cast_for_database(value)) + end + + def type_cast_for_database(value) + ConnectionAdapters::PostgreSQLColumn.json_to_string(value) + end + + def cast_value(value) + ConnectionAdapters::PostgreSQLColumn.string_to_json value + end + + def accessor + ActiveRecord::Store::StringKeyedHashAccessor + 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..697dceb7c2 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb @@ -0,0 +1,39 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Money < Type::Decimal + include Infinity + + class_attribute :precision + + 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..f9531ddee3 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb @@ -0,0 +1,20 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Point < Type::String + def type_cast(value) + if ::String === value + if value[0] == '(' && value[-1] == ')' + value = value[1...-1] + end + value.split(',').map{ |v| Float(v) } + else + value + end + 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..c2262c1599 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -0,0 +1,56 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Range < Type::Value + attr_reader :subtype, :type + + def initialize(subtype, type) + @subtype = subtype + @type = type + 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 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 + 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..7b1ca16bc4 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb @@ -0,0 +1,19 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class SpecializedString < Type::String + attr_reader :type + + def initialize(type) + @type = type + end + + def text? + false + 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..ea1f599b0f --- /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 + 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..28f7a4eafb --- /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) + 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..0ed5491887 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb @@ -0,0 +1,17 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Uuid < Type::Value + def type + :uuid + end + + def type_cast(value) + value.presence + 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..2f7d1be197 --- /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 + 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/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index ac3b0f713d..ad12298013 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) @@ -150,13 +150,11 @@ 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) + schema, table = Utils.extract_schema_and_table(name.to_s) + if schema + "#{quote_column_name(schema)}.#{quote_column_name(table)}" else - table_name, name_part = extract_pg_identifier_from_name(name_part) - "#{quote_column_name(schema)}.#{quote_column_name(table_name)}" + quote_column_name(table) end end @@ -182,6 +180,15 @@ module ActiveRecord end result end + + # Does not quote function default values for UUID columns + def quote_default_value(value, column) #:nodoc: + if column.type == :uuid && value =~ /\(\)/ + value + else + quote(value, column) + end + end 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..bcfd605165 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -0,0 +1,134 @@ +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 citext(name, options = {}) + column(name, 'citext', 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 1229b4851a..484c44dc8d 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 @@ -33,10 +33,6 @@ module ActiveRecord end end - def schema_creation - SchemaCreation.new self - end - module SchemaStatements # Drops the database specified on the +name+ attribute # and creates it again using the provided +options+. @@ -182,8 +178,10 @@ 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(default) + default_function = extract_default_function(default_value, default) + PostgreSQLColumn.new(column_name, default_value, oid, type, notnull == 'f', default_function) end end @@ -404,13 +402,16 @@ module ActiveRecord # Changes the default value of a table column. def change_column_default(table_name, column_name, default) clear_cache! - execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}" + 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 end def change_column_null(table_name, column_name, null, default = nil) clear_cache! unless null || default.nil? - execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") + column = column_for(table_name, column_name) + execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_value(default, column)} WHERE #{quote_column_name(column_name)} IS NULL") if column end execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL") end 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..60ffd3a114 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb @@ -0,0 +1,25 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module Utils # :nodoc: + 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> + # * <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 + 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 0485093123..027169ae3c 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" }, @@ -238,10 +106,12 @@ module ActiveRecord citext: { name: "citext" } } - 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 +119,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 @@ -338,19 +212,15 @@ module ActiveRecord end end - class BindSubstitution < Arel::Visitors::PostgreSQL # :nodoc: - include Arel::Visitors::BindVisitor - end - # Initializes and connects a PostgreSQL adapter. def initialize(connection, logger, connection_parameters, config) super(connection, logger) + @visitor = Arel::Visitors::PostgreSQL.new self if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @prepared_statements = true - @visitor = Arel::Visitors::PostgreSQL.new self else - @visitor = unprepared_visitor + @prepared_statements = false end @connection_parameters, @config = connection_parameters, config @@ -367,7 +237,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 @@ -498,25 +368,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 @@ -526,7 +377,7 @@ module ActiveRecord end def update_table_definition(table_name, base) #:nodoc: - Table.new(table_name, base) + PostgreSQL::Table.new(table_name, base) end protected @@ -555,46 +406,169 @@ 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 + m.register_type 'bit', OID::Bit.new + m.alias_type 'varbit', 'bit' + 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 'cidr', OID::Cidr.new + m.register_type 'inet', OID::Inet.new + m.register_type 'uuid', OID::Uuid.new + m.register_type 'xml', OID::SpecializedString.new(:xml) + 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 + + 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 - vector = OID::Vector.new row['typdelim'], type_map[row['typelem'].to_i] + # Extracts the value from a PostgreSQL column default definition. + def extract_value_from_default(default) # :nodoc: + # This is a performance optimization for Ruby 1.9.2 in development. + # If the value is nil, we return nil straight away without checking + # the regular expressions. If we check each regular expression, + # Regexp#=== will call NilClass#to_str, which will trigger + # method_missing (defined by whiny nil in ActiveSupport) which + # makes this method very very slow. + return default unless default + + 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 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 @@ -612,56 +586,21 @@ 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: + def execute_and_clear(sql, name, binds) + result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) : + exec_cache(sql, name, binds) + ret = yield result + result.clear + ret + end + def exec_no_cache(sql, name, binds) log(sql, name, binds) { @connection.async_exec(sql) } end @@ -721,11 +660,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 @@ -734,7 +668,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 @@ -816,7 +750,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 @@ -828,23 +762,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 cd1f7a16c6..a5e2619cb8 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -30,7 +30,7 @@ module ActiveRecord db.busy_timeout(ConnectionAdapters::SQLite3Adapter.type_cast_config_to_integer(config[:timeout])) if config[:timeout] - ConnectionAdapters::SQLite3Adapter.new(db, logger, config) + ConnectionAdapters::SQLite3Adapter.new(db, logger, nil, config) rescue Errno::ENOENT => error if error.message.include?("No such file or directory") raise ActiveRecord::NoDatabaseError.new(error.message, error) @@ -41,14 +41,12 @@ 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 @@ -69,7 +67,6 @@ module ActiveRecord float: { name: "float" }, decimal: { name: "decimal" }, datetime: { name: "datetime" }, - timestamp: { name: "datetime" }, time: { name: "time" }, date: { name: "date" }, binary: { name: "blob" }, @@ -123,11 +120,7 @@ module ActiveRecord end end - class BindSubstitution < Arel::Visitors::SQLite # :nodoc: - include Arel::Visitors::BindVisitor - end - - def initialize(connection, logger, config) + def initialize(connection, logger, connection_options, config) super(connection, logger) @active = nil @@ -135,11 +128,12 @@ module ActiveRecord self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })) @config = config + @visitor = Arel::Visitors::SQLite.new self + if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @prepared_statements = true - @visitor = Arel::Visitors::SQLite.new self else - @visitor = unprepared_visitor + @prepared_statements = false end end @@ -273,7 +267,7 @@ module ActiveRecord def explain(arel, binds = []) sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}" - ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) + ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', [])) end class ExplainPrettyPrinter @@ -299,9 +293,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] ||= { @@ -394,7 +391,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) + Column.new(field['name'], field['dflt_value'], cast_type, sql_type, field['notnull'].to_i == 0) end end @@ -495,14 +494,18 @@ 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) + 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 4e53f66005..07eafef788 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -94,6 +94,7 @@ module ActiveRecord end class_attribute :default_connection_handler, instance_writer: false + class_attribute :find_by_statement_cache def self.connection_handler ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler @@ -107,6 +108,71 @@ module ActiveRecord end module ClassMethods + def initialize_find_by_cache + self.find_by_statement_cache = {}.extend(Mutex_m) + end + + def inherited(child_class) + child_class.initialize_find_by_cache + super + end + + def find(*ids) + # We don't have cache keys for this stuff yet + return super unless ids.length == 1 + return super if block_given? || + primary_key.nil? || + default_scopes.any? || + columns_hash.include?(inheritance_column) || + ids.first.kind_of?(Array) + + id = ids.first + if ActiveRecord::Base === id + id = id.id + ActiveSupport::Deprecation.warn "You are passing an instance of ActiveRecord::Base to `find`." \ + "Please pass the id of the object by calling `.id`" + end + key = primary_key + + s = find_by_statement_cache[key] || find_by_statement_cache.synchronize { + find_by_statement_cache[key] ||= StatementCache.create(connection) { |params| + where(key => params.bind).limit(1) + } + } + record = s.execute([id], self, connection).first + unless record + raise RecordNotFound, "Couldn't find #{name} with '#{primary_key}'=#{id}" + end + record + end + + def find_by(*args) + return super if current_scope || args.length > 1 || reflect_on_all_aggregations.any? + + hash = args.first + + return super if hash.values.any? { |v| + v.nil? || Array === v || Hash === v + } + + key = hash.keys + + klass = self + s = find_by_statement_cache[key] || find_by_statement_cache.synchronize { + find_by_statement_cache[key] ||= StatementCache.create(connection) { |params| + wheres = key.each_with_object({}) { |param,o| + o[param] = params.bind + } + klass.where(wheres).limit(1) + } + } + begin + s.execute(hash.values, self, connection).first + rescue TypeError => e + raise ActiveRecord::StatementInvalid.new(e.message, e) + end + end + def initialize_generated_modules super @@ -220,6 +286,8 @@ module ActiveRecord @new_record = false + self.class.define_attribute_methods + run_callbacks :find run_callbacks :initialize @@ -267,6 +335,7 @@ module ActiveRecord @attributes_cache = {} @new_record = true + @destroyed = false super end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index dcbdf75627..05c4b13016 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 }, primary_key) connection.update stmt end @@ -118,5 +122,54 @@ module ActiveRecord update_counters(id, counter_name => -1) end end + + protected + + def actually_destroyed? + @_actually_destroyed + end + + def clear_destroy_state + @_actually_destroyed = nil + end + + private + + def _create_record(*) + id = super + + each_counter_cached_associations do |association| + if send(association.reflection.name) + association.increment_counters + @_after_create_counter_called = true + end + end + + id + end + + def destroy_row + affected_rows = super + + if affected_rows > 0 + each_counter_cached_associations do |association| + foreign_key = association.reflection.foreign_key.to_sym + unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key + if send(association.reflection.name) + association.decrement_counters + end + end + end + end + + affected_rows + end + + def each_counter_cached_associations + _reflections.each do |name, reflection| + yield association(name) if reflection.belongs_to? && reflection.counter_cache_column + end + end + end end diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 18f1ca26de..c7ec093824 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: # diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 71efbb8f93..2ccb1b0702 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -217,6 +217,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/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 47d32fae05..d40bea5ea7 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -649,7 +649,7 @@ 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 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..8fe32bcb6c 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 # @@ -640,7 +640,7 @@ 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 end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index dc5ff02882..a4e10ed2e7 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 = "" @@ -153,6 +157,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 +198,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,20 +217,6 @@ 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] }] - end - def column_types # :nodoc: @column_types ||= decorate_columns(columns_hash.dup) end @@ -263,7 +257,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 @@ -300,8 +294,6 @@ module ActiveRecord @arel_engine = nil @column_defaults = nil @column_names = nil - @columns = nil - @columns_hash = nil @column_types = nil @content_columns = nil @dynamic_methods_hash = nil @@ -337,7 +329,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 9d92e747d4..1242f49e28 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) @@ -485,10 +485,10 @@ module ActiveRecord end # Takes in a limit and checks if the attributes_collection has too many - # records. The method will take limits in the form of symbols, procs, and - # number-like objects (anything that can be compared with an integer). + # records. It accepts limit in the form of symbol, proc, or + # number-like object (anything that can be compared with an integer). # - # Will raise an TooManyRecords error if the attributes_collection is + # Raises TooManyRecords error if the attributes_collection is # larger than the limit. def check_record_limit!(limit, attributes_collection) if limit @@ -516,10 +516,10 @@ 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(hash['_destroy']) end - # Determines if a new record should be build by checking for + # Determines if a new record should be rejected by checking # has_destroy_flag? or if a <tt>:reject_if</tt> proc exists for this # association and evaluates to +true+. def reject_new_record?(association_name, attributes) @@ -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 5b255c3fe5..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? @@ -43,18 +43,32 @@ module ActiveRecord end def count(*) - 0 + calculate :count, nil 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 - 0 + 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..b74e340b3e 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) + def instantiate(attributes, column_types = {}) + klass = discriminate_class_for_record(attributes) column_types = klass.decorate_columns(column_types.dup) - klass.allocate.init_with('attributes' => record, 'column_types' => column_types) + klass.allocate.init_with('attributes' => attributes, 'column_types' => column_types) end private diff --git a/activerecord/lib/active_record/properties.rb b/activerecord/lib/active_record/properties.rb new file mode 100644 index 0000000000..c99594c823 --- /dev/null +++ b/activerecord/lib/active_record/properties.rb @@ -0,0 +1,102 @@ +module ActiveRecord + module Properties # :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 + # Defines or overrides a property on this model. This allows customization of + # Active Record's type casting behavior, as well as adding support for user defined + # types. + # + # ==== Examples + # + # The type detected by Active Record can be overriden. + # + # # 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 + # property :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::Properties::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 + # property :price_in_cents, MoneyType.new + # end + # + # store_listing = StoreListing.new(price_in_cents: '$10.00') + # store_listing.price_in_cents # => 1000 + def property(name, cast_type) + name = name.to_s + # Assign a new hash to ensure that subclasses do not share a hash + self.user_provided_columns = user_provided_columns.merge(name => ConnectionAdapters::Column.new(name, nil, 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 + + @columns = nil + @columns_hash = nil + 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 + end + end +end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 6b0459ea37..fa25ceaefa 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -83,8 +83,7 @@ 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 + 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 } @@ -278,7 +277,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 +366,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 1724ea95b0..dd80ec6274 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,12 +1,14 @@ +require 'thread' + module ActiveRecord # = Active Record Reflection module Reflection # :nodoc: extend ActiveSupport::Concern included do - class_attribute :reflections + class_attribute :_reflections class_attribute :aggregate_reflections - self.reflections = {} + self._reflections = {} self.aggregate_reflections = {} end @@ -22,7 +24,7 @@ module ActiveRecord 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) @@ -51,6 +53,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>, @@ -61,6 +81,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 @@ -71,11 +92,19 @@ 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 @@ -127,6 +156,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. @@ -191,14 +224,27 @@ module ActiveRecord end attr_reader :type, :foreign_type + attr_accessor :parent_reflection # [:name, Reflection] def initialize(macro, name, scope, options, active_record) super - @collection = :has_many == macro + @collection = [:has_many, :has_and_belongs_to_many].include?(macro) @automatic_inverse_of = nil @type = options[:as] && "#{options[:as]}_type" @foreign_type = options[:foreign_type] || "#{name}_type" @constructable = calculate_constructable(macro, options) + @association_scope_cache = {} + @scope_lock = Mutex.new + end + + def association_scope_cache(conn, owner) + key = conn.prepared_statements + if options[:polymorphic] + key = [key, owner.read_attribute(@foreign_type)] + end + @association_scope_cache[key] ||= @scope_lock.synchronize { + @association_scope_cache[key] ||= yield + } end # Returns a new, unsaved instance of the associated class. +attributes+ will @@ -264,6 +310,25 @@ module ActiveRecord 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 @@ -297,12 +362,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) @@ -403,7 +468,7 @@ module ActiveRecord inverse_name = ActiveSupport::Inflector.underscore(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. @@ -449,9 +514,9 @@ module ActiveRecord end def derive_class_name - class_name = name.to_s.camelize + class_name = name.to_s class_name = class_name.singularize if collection? - class_name + class_name.camelize end def derive_foreign_key @@ -502,7 +567,7 @@ module ActiveRecord # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:belongs_to, @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 @@ -518,7 +583,7 @@ module ActiveRecord # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @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 @@ -625,7 +690,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 diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 709edbee88..d92ff781ee 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +require 'arel/collectors/bind' module ActiveRecord # = Active Record Relation @@ -11,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 @@ -223,6 +225,7 @@ module ActiveRecord # Please see further details in the # {Active Record Query Interface guide}[http://guides.rubyonrails.org/active_record_querying.html#running-explain]. def explain + #TODO: Fix for binds. exec_explain(collecting_queries_for_explain { exec_queries }) end @@ -323,7 +326,8 @@ module ActiveRecord stmt.wheres = arel.constraints end - @klass.connection.update stmt, 'SQL', bind_values + bvs = bind_values + arel.bind_values + @klass.connection.update stmt, 'SQL', bvs end # Updates an object (or multiple objects) and saves it to the database, if validations pass. @@ -427,12 +431,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 @@ -516,11 +529,11 @@ module ActiveRecord find_with_associations { |rel| relation = rel } end - ast = relation.arel.ast - binds = relation.bind_values.dup - visitor.accept(ast) do - connection.quote(*binds.shift.reverse) - end + arel = relation.arel + binds = (arel.bind_values + relation.bind_values).dup + binds.map! { |bv| connection.quote(*bv.reverse) } + collect = visitor.accept(arel.ast, Arel::Collectors::Bind.new) + collect.substitute_binds(binds).join end end @@ -537,7 +550,13 @@ module ActiveRecord Hash[equalities.map { |where| name = where.left.name - [name, binds.fetch(name.to_s) { where.right }] + [name, binds.fetch(name.to_s) { + case where.right + when Array then where.right.map(&:val) + else + where.right.val + end + }] }] end @@ -601,7 +620,7 @@ module ActiveRecord private def exec_queries - @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values) + @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, arel.bind_values + bind_values) preload = preload_values preload += includes_values unless eager_loading? diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 812e3e800a..d155517b18 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -19,6 +19,14 @@ module ActiveRecord # # Person.group(:city).count # # => { 'Rome' => 5, 'Paris' => 3 } + # + # 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 databsae is thrown. def count(column_name = nil, options = {}) # TODO: Remove options argument as soon we remove support to # activerecord-deprecated_finders. @@ -188,7 +196,7 @@ module ActiveRecord private def has_include?(column_name) - eager_loading? || (includes_values.present? && (column_name || references_eager_loaded_tables?)) + eager_loading? || (includes_values.present? && ((column_name && column_name != :all) || references_eager_loaded_tables?)) end def perform_calculation(operation, column_name, options = {}) @@ -235,11 +243,14 @@ module ActiveRecord column_alias = column_name + bind_values = nil + if operation == "count" && (relation.limit_value || relation.offset_value) # Shortcut when limit is zero. return 0 if relation.limit_value == 0 query_builder = build_count_subquery(relation, column_name, distinct) + bind_values = query_builder.bind_values + relation.bind_values else column = aggregate_column(column_name) @@ -249,9 +260,10 @@ module ActiveRecord relation.select_values = [select_value] query_builder = relation.arel + bind_values = query_builder.bind_values + relation.bind_values end - result = @klass.connection.select_all(query_builder, nil, relation.bind_values) + result = @klass.connection.select_all(query_builder, nil, bind_values) row = result.first value = row && row.values.first column = result.column_types.fetch(column_alias) do @@ -265,7 +277,7 @@ module ActiveRecord group_attrs = group_values if group_attrs.first.respond_to?(:to_sym) - association = @klass.reflect_on_association(group_attrs.first.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 group_fields = Array(associated ? association.foreign_key : group_attrs) else @@ -385,9 +397,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 21beed332f..50f4d5c7ab 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -40,10 +40,10 @@ module ActiveRecord BLACKLISTED_ARRAY_METHODS = [ :compact!, :flatten!, :reject!, :reverse!, :rotate!, :map!, :shuffle!, :slice!, :sort!, :sort_by!, :delete_if, - :keep_if, :pop, :shift, :delete_at, :compact + :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 7af4b29ebc..47e90e9021 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -242,7 +242,7 @@ module ActiveRecord # If no order is defined it will order by primary key. # # Person.forty_two # returns the forty-second object fetched by SELECT * FROM people - # Person.offset(3).forty_two # returns the fifth object from OFFSET 3 (which is OFFSET 44) + # Person.offset(3).forty_two # returns the forty-second object from OFFSET 3 (which is OFFSET 44) # Person.where(["user_name = :u", { u: user_name }]).forty_two def forty_two find_nth(41, offset_index) @@ -299,11 +299,8 @@ module ActiveRecord when Array, Hash relation = relation.where(conditions) else - if conditions != :none - column = columns_hash[primary_key] - substitute = connection.substitute_at(column, bind_values.length) - relation = where(table[primary_key].eq(substitute)) - relation.bind_values += [[column, conditions]] + unless conditions == :none + relation = where(primary_key => conditions) end end @@ -339,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 @@ -351,7 +357,8 @@ module ActiveRecord if ActiveRecord::NullRelation === relation [] else - rows = connection.select_all(relation.arel, 'SQL', relation.bind_values.dup) + arel = relation.arel + rows = connection.select_all(arel, 'SQL', arel.bind_values + relation.bind_values) join_dependency.instantiate(rows, aliases) end end diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index be44fccad5..ac41d0aa80 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -30,6 +30,8 @@ module ActiveRecord else other.joins!(*v) end + elsif k == :select + other._select!(v) else other.send("#{k}!", v) end @@ -62,7 +64,13 @@ module ActiveRecord # expensive), most of the time the value is going to be `nil` or `.blank?`, the only catch is that # `false.blank?` returns `true`, so there needs to be an extra check so that explicit `false` values # don't fall through the cracks. - relation.send("#{name}!", *value) unless value.nil? || (value.blank? && false != value) + unless value.nil? || (value.blank? && false != value) + if name == :select + relation._select!(*value) + else + relation.send("#{name}!", *value) + end + end end merge_multi_values @@ -148,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 1252af7635..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 @@ -113,13 +113,14 @@ module ActiveRecord register_handler(Relation, RelationHandler.new) register_handler(Array, ArrayHandler.new) - private - def self.build(attribute, value) - handler_for(value).call(attribute, value) - end + def self.build(attribute, value) + handler_for(value).call(attribute, value) + end + private_class_method :build - def self.handler_for(object) - @handlers.detect { |klass, _| klass === object }.last - end + def self.handler_for(object) + @handlers.detect { |klass, _| klass === object }.last + end + private_class_method :handler_for end 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 4287304945..1262b2c291 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -64,6 +64,7 @@ module ActiveRecord # def #{name}_values=(values) # def select_values=(values) raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded + check_cached_relation @values[:#{name}] = values # @values[:select] = values end # end CODE @@ -81,11 +82,22 @@ module ActiveRecord class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{name}_value=(value) # def readonly_value=(value) raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded + check_cached_relation @values[:#{name}] = value # @values[:readonly] = value end # end CODE end + def check_cached_relation # :nodoc: + if defined?(@arel) && @arel + @arel = nil + ActiveSupport::Deprecation.warn <<-WARNING +Modifying already cached Relation. The cache will be reset. +Use a cloned Relation to prevent this warning. +WARNING + end + end + def create_with_value # :nodoc: @values[:create_with] || {} end @@ -235,11 +247,11 @@ module ActiveRecord to_a.select { |*block_args| yield(*block_args) } else raise ArgumentError, 'Call this with at least one field' if fields.empty? - spawn.select!(*fields) + spawn._select!(*fields) end end - def select!(*fields) # :nodoc: + def _select!(*fields) # :nodoc: fields.flatten! fields.map! do |field| klass.attribute_alias?(field) ? klass.attribute_alias(field) : field @@ -561,15 +573,11 @@ module ActiveRecord 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. @@ -843,7 +851,7 @@ module ActiveRecord build_joins(arel, joins_values.flatten) unless joins_values.empty? - collapse_wheres(arel, (where_values - ['']).uniq) + collapse_wheres(arel, (where_values - [''])) #TODO: Add uniq with real value comparison / ignore uniqs that have binds arel.having(*having_values.uniq.reject(&:blank?)) unless having_values.empty? @@ -860,6 +868,15 @@ module ActiveRecord arel.from(build_from) if from_value arel.lock(lock_value) if lock_value + # Reorder bind indexes if joins produced bind values + if arel.bind_values.any? + bvs = arel.bind_values + bind_values + arel.ast.grep(Arel::Nodes::BindParam).each_with_index do |bp, i| + column = bvs[i].first + bp.replace connection.substitute_at(column, i) + end + end + arel end @@ -874,6 +891,8 @@ module ActiveRecord case scope when :order result = [] + when :where + self.bind_values = [] else result = [] unless single_val_method end @@ -924,18 +943,15 @@ module ActiveRecord def build_where(opts, other = []) case opts when String, Array - #TODO: Remove duplication with: /activerecord/lib/active_record/sanitization.rb:113 - values = Hash === other.first ? other.first.values : other - - values.grep(ActiveRecord::Relation) do |rel| - self.bind_values += rel.bind_values - end - [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] when Hash opts = PredicateBuilder.resolve_column_aliases(klass, opts) - attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts) + bv_len = bind_values.length + tmp_opts, bind_values = create_binds(opts, bv_len) + self.bind_values += bind_values + + attributes = @klass.send(:expand_hash_conditions_for_aggregates, tmp_opts) attributes.values.grep(ActiveRecord::Relation) do |rel| self.bind_values += rel.bind_values end @@ -946,6 +962,29 @@ module ActiveRecord end end + def create_binds(opts, idx) + bindable, non_binds = opts.partition do |column, value| + case value + when String, Integer, ActiveRecord::StatementCache::Substitute + @klass.columns_hash.include? column.to_s + else + false + end + end + + new_opts = {} + binds = [] + + bindable.each_with_index do |(column,value), index| + binds.push [@klass.columns_hash[column.to_s], value] + new_opts[column] = connection.substitute_at(column, index + idx) + end + + non_binds.each { |column,value| new_opts[column] = value } + + [new_opts, binds] + end + def build_from opts, name = from_value case opts @@ -987,9 +1026,12 @@ module ActiveRecord join_list ) - joins = join_dependency.join_constraints stashed_association_joins + join_infos = join_dependency.join_constraints stashed_association_joins - joins.each { |join| manager.from(join) } + join_infos.each do |info| + info.joins.each { |join| manager.from(join) } + manager.bind_values.concat info.binds + end manager.join_sources.concat(join_list) diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 2552cbd234..57d66bce4b 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -58,6 +58,9 @@ module ActiveRecord # Post.order('id asc').only(:where) # discards the order condition # Post.order('id asc').only(:where, :order) # uses the specified order def only(*onlies) + if onlies.any? { |o| o == :where } + onlies << :bind + end relation_with values.slice(*onlies) end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 5a71c13d91..1aa93ffbb3 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -92,7 +92,7 @@ module ActiveRecord table = Arel::Table.new(table_name, arel_engine).alias(default_table_name) PredicateBuilder.build_from_hash(self, attrs, table).map { |b| - connection.visitor.accept b + connection.visitor.compile b }.join(' AND ') end alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions @@ -107,6 +107,13 @@ module ActiveRecord end.join(', ') end + # Sanitizes a +string+ so that it is safe to use within a 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, "%", "_") + string.gsub(pattern) { |x| [escape_character, x].join } + end + # Accepts an array of conditions. The array has each value # sanitized and interpolated into the SQL statement. # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'" diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb index dd4ee0c4a0..aece446384 100644 --- a/activerecord/lib/active_record/statement_cache.rb +++ b/activerecord/lib/active_record/statement_cache.rb @@ -14,13 +14,87 @@ module ActiveRecord # The relation returned by the block is cached, and for each +execute+ call the cached relation gets duped. # Database is queried when +to_a+ is called on the relation. class StatementCache - def initialize - @relation = yield - raise ArgumentError.new("Statement cannot be nil") if @relation.nil? + class Substitute; end + + class Query + def initialize(sql) + @sql = sql + end + + def sql_for(binds, connection) + @sql + end + end + + class PartialQuery < Query + def initialize values + @values = values + @indexes = values.each_with_index.find_all { |thing,i| + Arel::Nodes::BindParam === thing + }.map(&:last) + end + + def sql_for(binds, connection) + val = @values.dup + binds = binds.dup + @indexes.each { |i| val[i] = connection.quote(*binds.shift.reverse) } + val.join + end + end + + def self.query(visitor, ast) + Query.new visitor.accept(ast, Arel::Collectors::SQLString.new).value + end + + def self.partial_query(visitor, ast, collector) + collected = visitor.accept(ast, collector).value + PartialQuery.new collected + end + + class Params + def bind; Substitute.new; end end - def execute - @relation.dup.to_a + class BindMap + def initialize(bind_values) + @indexes = [] + @bind_values = bind_values + + bind_values.each_with_index do |(_, value), i| + if Substitute === value + @indexes << i + end + end + end + + def bind(values) + bvs = @bind_values.map { |pair| pair.dup } + @indexes.each_with_index { |offset,i| bvs[offset][1] = values[i] } + bvs + end + end + + attr_reader :bind_map, :query_builder + + def self.create(connection, block = Proc.new) + relation = block.call Params.new + bind_map = BindMap.new relation.bind_values + query_builder = connection.cacheable_query relation.arel + new query_builder, bind_map + end + + def initialize(query_builder, bind_map) + @query_builder = query_builder + @bind_map = bind_map + end + + def execute(params, klass, connection) + bind_values = bind_map.bind params + + sql = query_builder.sql_for bind_values, connection + + klass.find_by_sql sql, bind_values end + alias :call :execute end end diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 79a6ccbda0..7014bc6d45 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,9 +94,9 @@ 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 @@ -105,6 +106,14 @@ module ActiveRecord 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 diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 6ce0495f6f..168b338b97 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -28,7 +28,7 @@ module ActiveRecord # Example usage of +DatabaseTasks+ outside Rails could look as such: # # include ActiveRecord::Tasks - # DatabaseTasks.database_configuration = YAML.load(File.read('my_database_config.yml')) + # DatabaseTasks.database_configuration = YAML.load_file('my_database_config.yml') # DatabaseTasks.db_dir = 'db' # # other settings... # diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 17f76b63b3..17d1ae1ba0 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 @@ -355,7 +353,12 @@ module ActiveRecord # 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. diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb new file mode 100644 index 0000000000..2c26477201 --- /dev/null +++ b/activerecord/lib/active_record/type.rb @@ -0,0 +1,18 @@ +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/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..e34b7bb268 --- /dev/null +++ b/activerecord/lib/active_record/type/binary.rb @@ -0,0 +1,17 @@ +module ActiveRecord + module Type + class Binary < Value # :nodoc: + def type + :binary + end + + def binary? + true + end + + def klass + ::String + 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..45c69460ef --- /dev/null +++ b/activerecord/lib/active_record/type/date.rb @@ -0,0 +1,42 @@ +module ActiveRecord + module Type + class Date < Value # :nodoc: + def type + :date + end + + def klass + ::Date + 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..560d63c101 --- /dev/null +++ b/activerecord/lib/active_record/type/date_time.rb @@ -0,0 +1,33 @@ +module ActiveRecord + module Type + class DateTime < Value # :nodoc: + include TimeValue + + def type + :datetime + 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..1c0147a797 --- /dev/null +++ b/activerecord/lib/active_record/type/decimal.rb @@ -0,0 +1,25 @@ +module ActiveRecord + module Type + class Decimal < Value # :nodoc: + include Numeric + + def type + :decimal + end + + def klass + ::BigDecimal + end + + private + + def cast_value(value) + if value.respond_to?(:to_d) + value.to_d + else + value.to_s.to_d + 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..dc50dae328 --- /dev/null +++ b/activerecord/lib/active_record/type/float.rb @@ -0,0 +1,23 @@ +module ActiveRecord + module Type + class Float < Value # :nodoc: + include Numeric + + def type + :float + end + + def klass + ::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..1e2147dec9 --- /dev/null +++ b/activerecord/lib/active_record/type/integer.rb @@ -0,0 +1,27 @@ +module ActiveRecord + module Type + class Integer < Value # :nodoc: + include Numeric + + def type + :integer + end + + def klass + ::Fixnum + 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/numeric.rb b/activerecord/lib/active_record/type/numeric.rb new file mode 100644 index 0000000000..464d631d80 --- /dev/null +++ b/activerecord/lib/active_record/type/numeric.rb @@ -0,0 +1,18 @@ +module ActiveRecord + module Type + module Numeric # :nodoc: + def number? + true + end + + def type_cast_for_write(value) + case value + when true then 1 + when false then 0 + when ::String then value.presence + else super + end + 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..b3f6ca7691 --- /dev/null +++ b/activerecord/lib/active_record/type/string.rb @@ -0,0 +1,27 @@ +module ActiveRecord + module Type + class String < Value # :nodoc: + def type + :string + end + + def text? + true + end + + def klass + ::String + end + + private + + def cast_value(value) + case value + when true then "1" + when false then "0" + else 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..6cc19b6379 --- /dev/null +++ b/activerecord/lib/active_record/type/time_value.rb @@ -0,0 +1,34 @@ +module ActiveRecord + module Type + module TimeValue # :nodoc: + def klass + ::Time + 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..72d27197d5 --- /dev/null +++ b/activerecord/lib/active_record/type/value.rb @@ -0,0 +1,59 @@ +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 that this object represents. Subclasses + # should override this method. + def type; end + + # Takes an input from the database, or from attribute setters, + # and casts it to a type appropriate for this object. This method + # should not be overriden by subclasses. Instead, override `cast_value`. + def type_cast(value) + cast_value(value) unless value.nil? + end + + def type_cast_for_write(value) + value + end + + def type_cast_for_database(value) + type_cast_for_write(value) + end + + def text? + false + end + + def number? + false + end + + def binary? + false + end + + def klass + ::Object + end + + private + + # Responsible for casting values from external sources to the appropriate + # type. Called by `type_cast` for all values except `nil`. + def cast_value(value) # :api: public + value + end + end + end +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..b6fccc9b94 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -47,7 +47,7 @@ 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? end @@ -74,7 +74,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 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..778c4ed7e5 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' diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb index 412efa22ff..4c90d06732 100644 --- a/activerecord/test/cases/adapters/mysql/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql/connection_test.rb @@ -151,6 +151,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/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/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 182d9409c7..65f50e77bb 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -77,6 +77,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/explain_test.rb b/activerecord/test/cases/adapters/mysql2/explain_test.rb index 1cd356e868..675703caa1 100644 --- a/activerecord/test/cases/adapters/mysql2/explain_test.rb +++ b/activerecord/test/cases/adapters/mysql2/explain_test.rb @@ -9,15 +9,15 @@ module ActiveRecord def test_explain_for_one_query explain = Developer.where(:id => 1).explain - assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain + assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain assert_match %r(developers |.* const), explain end def test_explain_with_eager_loading explain = Developer.where(:id => 1).includes(:audit_logs).explain - assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain + assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain assert_match %r(developers |.* const), explain - assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` IN (1)), explain + assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` IN (1)), explain assert_match %r(audit_logs |.* ALL), explain end end diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index 18dd4a6de8..34c2008ab4 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -16,7 +16,7 @@ class PostgresqlArrayTest < ActiveRecord::TestCase t.integer 'ratings', array: true end end - @column = PgArray.columns.find { |c| c.name == 'tags' } + @column = PgArray.columns_hash['tags'] end teardown do @@ -61,10 +61,10 @@ 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 @@ -80,17 +80,16 @@ 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, [] + + PgArray.reset_column_information + column = PgArray.columns_hash['tags'] + assert_equal [], column.default + end + def test_type_cast_array + assert_equal(['1', '2', '3'], @column.type_cast('{1,2,3}')) assert_equal([], @column.type_cast('{}')) assert_equal([nil], @column.type_cast('{NULL}')) end @@ -101,17 +100,32 @@ class PostgresqlArrayTest < ActiveRecord::TestCase 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 @@ -175,6 +189,14 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert_equal [], pg_array.reload.tags end + def test_escaping + unknown = 'foo\\",bar,baz,\\' + tags = ["hello_#{unknown}"] + ar = PgArray.create!(tags: tags) + ar.reload + assert_equal tags, ar.tags + end + private def assert_cycle field, array # test creation diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb index e3478856c8..fadadfa57c 100644 --- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb @@ -19,7 +19,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 diff --git a/activerecord/test/cases/adapters/postgresql/citext_test.rb b/activerecord/test/cases/adapters/postgresql/citext_test.rb index 948bf49a54..8493050726 100644 --- a/activerecord/test/cases/adapters/postgresql/citext_test.rb +++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb @@ -50,7 +50,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..ecccbf10e6 100644 --- a/activerecord/test/cases/adapters/postgresql/composite_test.rb +++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb @@ -1,19 +1,19 @@ # -*- coding: utf-8 -*- require "cases/helper" +require 'support/connection_helper' require 'active_record/base' require 'active_record/connection_adapters/postgresql_adapter' -class PostgresqlCompositeTest < ActiveRecord::TestCase +module PostgresqlCompositeBehavior + include ConnectionHelper + 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,9 +29,27 @@ 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? @@ -41,6 +59,8 @@ class PostgresqlCompositeTest < ActiveRecord::TestCase 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 +70,61 @@ 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(value) + if value =~ /\("?([^",]*)"?,"?([^",]*)"?\)/ + FullAddress.new($1, $2) + end + end + + def type_cast_for_write(value) + "(#{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.text? + 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/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index e7dda1a1af..0dad89c67a 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,12 @@ 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 +22,26 @@ 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, PostgresqlBitString, PostgresqlOid].each(&:delete_all) end def test_data_type_of_number_types @@ -103,12 +54,6 @@ 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 @@ -118,42 +63,12 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase 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,15 +76,6 @@ 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 @@ -179,38 +85,6 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase 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,20 +103,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' @@ -260,20 +120,6 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase 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 +127,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/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb index 0b61f61572..416f84cb38 100644 --- a/activerecord/test/cases/adapters/postgresql/explain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb @@ -9,7 +9,7 @@ module ActiveRecord def test_explain_for_one_query explain = Developer.where(:id => 1).explain - assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = 1), explain + assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = $1), explain assert_match %(QUERY PLAN), explain assert_match %(Index Scan using developers_pkey on developers), explain end @@ -17,9 +17,9 @@ module ActiveRecord def test_explain_with_eager_loading explain = Developer.where(:id => 1).includes(:audit_logs).explain assert_match %(QUERY PLAN), explain - assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = 1), explain + assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = $1), explain assert_match %(Index Scan using developers_pkey on developers), explain - assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain + assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain assert_match %(Seq Scan on audit_logs), explain end end 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..91058f8681 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb @@ -0,0 +1,65 @@ +require "cases/helper" +require "active_record/base" +require "active_record/connection_adapters/postgresql_adapter" + +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..4442abcbc4 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/full_text_test.rb @@ -0,0 +1,30 @@ +# encoding: utf-8 + +require "cases/helper" +require 'active_record/base' +require 'active_record/connection_adapters/postgresql_adapter' + +class PostgresqlFullTextTest < ActiveRecord::TestCase + class PostgresqlTsvector < ActiveRecord::Base; end + + 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.text? + 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/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index c24c4b0d56..1fef4899be 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 @@ -78,7 +78,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 @@ -142,6 +142,16 @@ 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_gen1 assert_equal(%q(" "=>""), @column.class.hstore_to_string({' '=>''})) end diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index ee793ffff2..03b546119d 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -23,7 +23,7 @@ class PostgresqlJSONTest < ActiveRecord::TestCase 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 @@ -57,7 +57,7 @@ class PostgresqlJSONTest < ActiveRecord::TestCase t.json 'users', default: '{}' end JsonDataType.reset_column_information - column = JsonDataType.columns.find { |c| c.name == 'users' } + column = JsonDataType.columns_hash['users'] assert_equal :json, column.type raise ActiveRecord::Rollback # reset the schema change @@ -139,6 +139,14 @@ class PostgresqlJSONTest < ActiveRecord::TestCase assert_equal "640×1136", x.resolution end + 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_update_all json = JsonDataType.create! payload: { "one" => "two" } 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..e109f1682b --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/money_test.rb @@ -0,0 +1,54 @@ +# encoding: utf-8 + +require "cases/helper" +require 'active_record/base' +require 'active_record/connection_adapters/postgresql_adapter' + +class PostgresqlMoneyTest < ActiveRecord::TestCase + class PostgresqlMoney < ActiveRecord::Base; end + + setup do + @connection = ActiveRecord::Base.connection + @connection.execute("set lc_monetary = 'C'") + end + + def test_column + column = PostgresqlMoney.columns_hash["wealth"] + assert_equal :decimal, column.type + assert_equal "money", column.sql_type + assert_equal 2, column.scale + assert column.number? + assert_not column.text? + assert_not column.binary? + assert_not column.array + 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("$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_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 +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..e99af07970 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/network_test.rb @@ -0,0 +1,77 @@ +# encoding: utf-8 + +require "cases/helper" +require 'active_record/base' +require 'active_record/connection_adapters/postgresql_adapter' + +class PostgresqlNetworkTest < ActiveRecord::TestCase + class PostgresqlNetworkAddress < ActiveRecord::Base + 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.text? + 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.text? + 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.text? + 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..49f5ec250f 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -396,6 +396,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..218c59247e 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 diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 11ec7599a3..b6c6e38f62 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" diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb index 4d29a20e66..d4102bf7be 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 @@ -84,18 +125,18 @@ class TimestampTest < ActiveRecord::TestCase 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/utils_test.rb b/activerecord/test/cases/adapters/postgresql/utils_test.rb index 9e7b08ef34..e6d7868e9a 100644 --- a/activerecord/test/cases/adapters/postgresql/utils_test.rb +++ b/activerecord/test/cases/adapters/postgresql/utils_test.rb @@ -1,7 +1,7 @@ require 'cases/helper' class PostgreSQLUtilsTest < ActiveSupport::TestCase - include ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::Utils + include ActiveRecord::ConnectionAdapters::PostgreSQL::Utils def test_extract_schema_and_table { diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index 9e03ea6bee..40ed0f64a4 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -40,6 +40,21 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase drop_table "uuid_data_type" end + 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_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_hash['thingy'] + assert_equal "uuid_generate_v4()", column.default_function + ensure + UUIDType.reset_column_information + end + def test_data_type_of_uuid_types column = UUIDType.columns_hash["guid"] assert_equal :uuid, column.type diff --git a/activerecord/test/cases/adapters/postgresql/xml_test.rb b/activerecord/test/cases/adapters/postgresql/xml_test.rb index ae299697b1..c1c85f8c92 100644 --- a/activerecord/test/cases/adapters/postgresql/xml_test.rb +++ b/activerecord/test/cases/adapters/postgresql/xml_test.rb @@ -20,7 +20,7 @@ class PostgresqlXMLTest < ActiveRecord::TestCase 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 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/explain_test.rb b/activerecord/test/cases/adapters/sqlite3/explain_test.rb index b227bce680..f1d6119d2e 100644 --- a/activerecord/test/cases/adapters/sqlite3/explain_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/explain_test.rb @@ -9,15 +9,15 @@ module ActiveRecord def test_explain_for_one_query explain = Developer.where(:id => 1).explain - assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = 1), explain + assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = ?), explain assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain) end def test_explain_with_eager_loading explain = Developer.where(:id => 1).includes(:audit_logs).explain - assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = 1), explain + assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = ?), explain assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain) - assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain + assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain assert_match(/(SCAN )?TABLE audit_logs/, explain) end end diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index ba89487838..8c9a051eea 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -15,10 +15,10 @@ 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") + cast_type = Type::String.new binary = SecureRandom.hex expected = binary.dup.encode!(Encoding::UTF_8) - assert_equal expected, @conn.type_cast(binary, column) + assert_equal expected, @conn.type_cast(binary, cast_type) end def test_type_cast_symbol @@ -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,7 +100,7 @@ module ActiveRecord def quoted_id "'zomg'" end - } + }.new assert_raise(TypeError) { @conn.type_cast(quoted_id_obj, nil) } 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 14aad61ce2..e55525177f 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -182,7 +182,7 @@ module ActiveRecord def test_quote_binary_column_escapes_it DualEncoding.connection.execute(<<-eosql) - CREATE TABLE dual_encodings ( + CREATE TABLE IF NOT EXISTS dual_encodings ( id integer PRIMARY KEY AUTOINCREMENT, name varchar(255), data binary @@ -192,9 +192,8 @@ module ActiveRecord binary = DualEncoding.new name: 'いただきます!', data: str binary.save! assert_equal str, binary.data - ensure - DualEncoding.connection.drop_table('dual_encodings') + DualEncoding.connection.execute('DROP TABLE IF EXISTS dual_encodings') end def test_type_cast_should_not_mutate_encoding @@ -417,6 +416,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/associations/association_scope_test.rb b/activerecord/test/cases/associations/association_scope_test.rb index c78b036f53..3e0032ec73 100644 --- a/activerecord/test/cases/associations/association_scope_test.rb +++ b/activerecord/test/cases/associations/association_scope_test.rb @@ -9,7 +9,12 @@ module ActiveRecord scope = AssociationScope.scope(Author.new.association(:welcome_posts), Author.connection) wheres = scope.where_values.map(&:right) + binds = scope.bind_values.map(&:last) + wheres = scope.where_values.map(&:right).reject { |node| + Arel::Nodes::BindParam === node + } assert_equal wheres.uniq, wheres + assert_equal binds.uniq, binds 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 bcd2b6dfaa..9c92dc1141 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]) @@ -502,6 +509,27 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 4, topic.replies.size end + def test_concurrent_counter_cache_double_destroy + topic = Topic.create :title => "Zoom-zoom-zoom" + + 5.times do + topic.replies.create(:title => "re: zoom", :content => "speedy quick!") + end + + assert_equal 5, topic.reload[:replies_count] + assert_equal 5, topic.replies.size + + reply = topic.replies.first + reply_clone = Reply.find(reply.id) + + reply.destroy + assert_equal 4, topic.reload[:replies_count] + + reply_clone.destroy + assert_equal 4, topic.reload[:replies_count] + assert_equal 4, topic.replies.size + end + def test_custom_counter_cache reply = Reply.create(:title => "re: zoom", :content => "speedy quick!") assert_equal 0, reply[:replies_count] @@ -542,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 diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb index cf71bc1597..5b7e462f64 100644 --- a/activerecord/test/cases/associations/callbacks_test.rb +++ b/activerecord/test/cases/associations/callbacks_test.rb @@ -150,7 +150,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase "after_removing#{jamis.id}"], activerecord.developers_log end - def test_has_and_belongs_to_many_remove_callback_on_clear + def test_has_and_belongs_to_many_does_not_fire_callbacks_on_clear activerecord = projects(:active_record) assert activerecord.developers_log.empty? if activerecord.developers_with_callbacks.size == 0 @@ -159,9 +159,9 @@ 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_equal log_array, activerecord.developers_log.sort + assert_predicate activerecord.developers_log, :empty? end def test_has_many_and_belongs_to_many_callbacks_for_save_on_parent diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 7eaa5adc86..4bd4486b41 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,23 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal 2, author.posts.size } 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 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 366472c6fd..8d8201ddae 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 @@ -21,6 +21,10 @@ require 'models/membership' 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 @@ -66,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 @@ -82,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 @@ -216,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") } @@ -786,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 @@ -819,4 +855,23 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 1, treasure.rich_people.size assert_equal person_first_name, rich_person.first_name, 'should not run associated person validation on update when validate: false' end + + def test_custom_join_table + assert_equal 'edges', Vertex.reflect_on_association(:sources).join_table + end + + def test_namespaced_habtm + magazine = Publisher::Magazine.create + article = Publisher::Article.create + magazine.articles << article + magazine.save + + assert_includes magazine.articles, article + end + + def test_redefine_habtm + child = SubDeveloper.new("name" => "Aredridel") + child.special_projects << SpecialProject.new("name" => "Special Project") + assert_equal true, child.save + 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 c79c0c87c5..5f01352ab4 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 @@ -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' @@ -131,6 +139,19 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal(expected_sql, loaded_sql) end + def test_delete_all_on_association_with_nil_dependency_is_the_same_as_not_loaded + author = authors :david + author.posts.create!(:title => "test", :body => "body") + author.reload + expected_sql = capture_sql { author.posts.delete_all } + + author.posts.create!(:title => "test", :body => "body") + author.reload + author.posts.to_a + loaded_sql = capture_sql { author.posts.delete_all } + assert_equal(expected_sql, loaded_sql) + end + def test_building_the_associated_object_with_implicit_sti_base_class firm = DependentFirm.new company = firm.companies.build @@ -1864,4 +1885,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 6675e19dd9..2e62189e7a 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -698,9 +698,6 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase [:added, :before, "Roger"], [:added, :after, "Roger"] ], log.last(4) - - post.people_with_callbacks.clear - assert_equal((%w(Michael David Julian Roger) * 2).sort, log.last(8).collect(&:last).sort) end def test_dynamic_find_should_respect_association_include @@ -1082,7 +1079,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal ["parrot", "bulbul"], owner.toys.map { |r| r.pet.name } end - test "has many through associations on new records use null relations" do + def test_has_many_through_associations_on_new_records_use_null_relations person = Person.new assert_no_queries do @@ -1094,7 +1091,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end end - test "has many through with default scope on the target" do + def test_has_many_through_with_default_scope_on_the_target person = people(:michael) assert_equal [posts(:thinking)], person.first_posts @@ -1102,11 +1099,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal [posts(:thinking)], person.reload.first_posts end - test "has many through with includes in through association scope" do + def test_has_many_through_with_includes_in_through_association_scope assert_not_empty posts(:welcome).author_address_extra_with_address end - test "insert records via has_many_through association with scope" do + def test_insert_records_via_has_many_through_association_with_scope club = Club.create! member = Member.create! Membership.create!(club: club, member: member) @@ -1117,4 +1114,16 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase club.reload assert_equal [member], club.favourites end + + def test_has_many_through_unscope_default_scope + post = Post.create!(:title => 'Beaches', :body => "I like beaches!") + Reader.create! :person => people(:david), :post => post + LazyReader.create! :person => people(:susan), :post => post + + assert_equal 2, post.people.to_a.size + assert_equal 1, post.lazy_people.to_a.size + + assert_equal 2, post.lazy_readers_unscope_skimmers.to_a.size + assert_equal 2, post.lazy_people_unscope_skimmers.to_a.size + end end diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index 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/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 38e93288e4..4c96c2f4fd 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -516,7 +516,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase end def test_only_time_related_columns_are_meant_to_be_cached_by_default - expected = %w(datetime timestamp time date).sort + expected = %w(datetime time date).sort assert_equal expected, ActiveRecord::Base.attribute_types_cached_by_default.map(&:to_s).sort end @@ -843,6 +843,18 @@ 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 + private def new_topic_like_ar_class(&block) 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..d65c4b0638 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' + + property :world_population, Type::Integer.new + property :my_house_population, Type::Integer.new + property :atoms_in_universe, Type::Integer.new end def test_big_decimal_conditions diff --git a/activerecord/test/cases/binary_test.rb b/activerecord/test/cases/binary_test.rb index 9a486cf8b8..b41b95309b 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 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..b9445ee072 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' + + property :world_population, Type::Integer.new + property :my_house_population, Type::Integer.new + property :atoms_in_universe, Type::Integer.new end class CalculationsTest < ActiveRecord::TestCase 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/abstract_adapter_test.rb b/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb deleted file mode 100644 index deed226eab..0000000000 --- a/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb +++ /dev/null @@ -1,56 +0,0 @@ -require "cases/helper" - -module ActiveRecord - module ConnectionAdapters - class ConnectionPool - def insert_connection_for_test!(c) - synchronize do - @connections << c - @available.add c - end - end - end - - class AbstractAdapterTest < ActiveRecord::TestCase - attr_reader :adapter - - def setup - @adapter = AbstractAdapter.new nil, nil - end - - def test_in_use? - assert_not adapter.in_use?, 'adapter is not in use' - assert adapter.lease, 'lease adapter' - assert adapter.in_use?, 'adapter is in use' - end - - def test_lease_twice - assert adapter.lease, 'should lease adapter' - assert_not adapter.lease, 'should not lease adapter' - end - - def test_expire_mutates_in_use - assert adapter.lease, 'lease adapter' - assert adapter.in_use?, 'adapter is in use' - adapter.expire - assert_not adapter.in_use?, 'adapter is in use' - end - - def test_close - pool = ConnectionPool.new(ConnectionSpecification.new({}, nil)) - pool.insert_connection_for_test! adapter - adapter.pool = pool - - # Make sure the pool marks the connection in use - assert_equal adapter, pool.connection - assert adapter.in_use? - - # Close should put the adapter back in the pool - adapter.close - assert_not adapter.in_use? - - assert_equal adapter, pool.connection - end - end - end -end diff --git a/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb new file mode 100644 index 0000000000..662e19f35e --- /dev/null +++ b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb @@ -0,0 +1,54 @@ +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + class AdapterLeasingTest < ActiveRecord::TestCase + class Pool < ConnectionPool + def insert_connection_for_test!(c) + synchronize do + @connections << c + @available.add c + end + end + end + + def setup + @adapter = AbstractAdapter.new nil, nil + end + + def test_in_use? + assert_not @adapter.in_use?, 'adapter is not in use' + assert @adapter.lease, 'lease adapter' + assert @adapter.in_use?, 'adapter is in use' + end + + def test_lease_twice + assert @adapter.lease, 'should lease adapter' + assert_not @adapter.lease, 'should not lease adapter' + end + + def test_expire_mutates_in_use + assert @adapter.lease, 'lease adapter' + assert @adapter.in_use?, 'adapter is in use' + @adapter.expire + assert_not @adapter.in_use?, 'adapter is in use' + end + + def test_close + pool = Pool.new(ConnectionSpecification.new({}, nil)) + pool.insert_connection_for_test! @adapter + @adapter.pool = pool + + # Make sure the pool marks the connection in use + assert_equal @adapter, pool.connection + assert @adapter.in_use? + + # Close should put the adapter back in the pool + @adapter.close + assert_not @adapter.in_use? + + assert_equal @adapter, pool.connection + end + end + end +end diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index f2d18e812d..3e33b30144 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -2,199 +2,6 @@ require "cases/helper" module ActiveRecord module ConnectionAdapters - - class MergeAndResolveDefaultUrlConfigTest < ActiveRecord::TestCase - - def klass - ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig - end - - def setup - @previous_database_url = ENV.delete("DATABASE_URL") - end - - teardown do - ENV["DATABASE_URL"] = @previous_database_url - end - - def resolve(spec, config) - ConnectionSpecification::Resolver.new(klass.new(config).resolve).resolve(spec) - end - - def spec(spec, config) - ConnectionSpecification::Resolver.new(klass.new(config).resolve).spec(spec) - end - - def test_resolver_with_database_uri_and_current_env_symbol_key - ENV['DATABASE_URL'] = "postgres://localhost/foo" - config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } - actual = resolve(:default_env, config) - expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" } - assert_equal expected, actual - end - - def test_resolver_with_database_uri_and_and_current_env_string_key - ENV['DATABASE_URL'] = "postgres://localhost/foo" - config = { "default_env" => { "adapter" => "not_postgres", "database" => "not_foo" } } - actual = assert_deprecated { resolve("default_env", config) } - expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" } - assert_equal expected, actual - end - - def test_resolver_with_database_uri_and_known_key - ENV['DATABASE_URL'] = "postgres://localhost/foo" - config = { "production" => { "adapter" => "not_postgres", "database" => "not_foo", "host" => "localhost" } } - actual = resolve(:production, config) - expected = { "adapter"=>"not_postgres", "database"=>"not_foo", "host"=>"localhost" } - assert_equal expected, actual - end - - def test_resolver_with_database_uri_and_unknown_symbol_key - ENV['DATABASE_URL'] = "postgres://localhost/foo" - config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } - assert_raises AdapterNotSpecified do - resolve(:production, config) - end - end - - def test_resolver_with_database_uri_and_unknown_string_key - ENV['DATABASE_URL'] = "postgres://localhost/foo" - config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } - assert_deprecated do - assert_raises AdapterNotSpecified do - spec("production", config) - end - end - end - - def test_resolver_with_database_uri_and_supplied_url - ENV['DATABASE_URL'] = "not-postgres://not-localhost/not_foo" - config = { "production" => { "adapter" => "also_not_postgres", "database" => "also_not_foo" } } - actual = resolve("postgres://localhost/foo", config) - expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" } - assert_equal expected, actual - end - - def test_jdbc_url - config = { "production" => { "url" => "jdbc:postgres://localhost/foo" } } - actual = klass.new(config).resolve - assert_equal config, actual - end - - def test_environment_does_not_exist_in_config_url_does_exist - ENV['DATABASE_URL'] = "postgres://localhost/foo" - config = { "not_default_env" => { "adapter" => "not_postgres", "database" => "not_foo" } } - actual = klass.new(config).resolve - expect_prod = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" } - assert_equal expect_prod, actual["default_env"] - end - - def test_url_with_hyphenated_scheme - ENV['DATABASE_URL'] = "ibm-db://localhost/foo" - config = { "default_env" => { "adapter" => "not_postgres", "database" => "not_foo", "host" => "localhost" } } - actual = resolve(:default_env, config) - expected = { "adapter"=>"ibm_db", "database"=>"foo", "host"=>"localhost" } - assert_equal expected, actual - end - - def test_string_connection - config = { "default_env" => "postgres://localhost/foo" } - actual = klass.new(config).resolve - expected = { "default_env" => - { "adapter" => "postgresql", - "database" => "foo", - "host" => "localhost" - } - } - assert_equal expected, actual - end - - def test_url_sub_key - config = { "default_env" => { "url" => "postgres://localhost/foo" } } - actual = klass.new(config).resolve - expected = { "default_env" => - { "adapter" => "postgresql", - "database" => "foo", - "host" => "localhost" - } - } - assert_equal expected, actual - end - - def test_hash - config = { "production" => { "adapter" => "postgres", "database" => "foo" } } - actual = klass.new(config).resolve - assert_equal config, actual - end - - def test_blank - config = {} - actual = klass.new(config).resolve - assert_equal config, actual - end - - def test_blank_with_database_url - ENV['DATABASE_URL'] = "postgres://localhost/foo" - - config = {} - actual = klass.new(config).resolve - expected = { "adapter" => "postgresql", - "database" => "foo", - "host" => "localhost" } - assert_equal expected, actual["default_env"] - assert_equal nil, actual["production"] - assert_equal nil, actual["development"] - assert_equal nil, actual["test"] - assert_equal nil, actual[:production] - assert_equal nil, actual[:development] - assert_equal nil, actual[:test] - end - - def test_url_sub_key_with_database_url - ENV['DATABASE_URL'] = "NOT-POSTGRES://localhost/NOT_FOO" - - config = { "default_env" => { "url" => "postgres://localhost/foo" } } - actual = klass.new(config).resolve - expected = { "default_env" => - { "adapter" => "postgresql", - "database" => "foo", - "host" => "localhost" - } - } - assert_equal expected, actual - end - - def test_merge_no_conflicts_with_database_url - ENV['DATABASE_URL'] = "postgres://localhost/foo" - - config = {"default_env" => { "pool" => "5" } } - actual = klass.new(config).resolve - expected = { "default_env" => - { "adapter" => "postgresql", - "database" => "foo", - "host" => "localhost", - "pool" => "5" - } - } - assert_equal expected, actual - end - - def test_merge_conflicts_with_database_url - ENV['DATABASE_URL'] = "postgres://localhost/foo" - - config = {"default_env" => { "adapter" => "NOT-POSTGRES", "database" => "NOT-FOO", "pool" => "5" } } - actual = klass.new(config).resolve - expected = { "default_env" => - { "adapter" => "postgresql", - "database" => "foo", - "host" => "localhost", - "pool" => "5" - } - } - assert_equal expected, actual - end - end - class ConnectionHandlerTest < ActiveRecord::TestCase def setup @klass = Class.new(Base) { def self.name; 'klass'; 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 new file mode 100644 index 0000000000..da852aaa02 --- /dev/null +++ b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb @@ -0,0 +1,192 @@ +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + class MergeAndResolveDefaultUrlConfigTest < ActiveRecord::TestCase + def setup + @previous_database_url = ENV.delete("DATABASE_URL") + end + + teardown do + ENV["DATABASE_URL"] = @previous_database_url + end + + def resolve_config(config) + ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig.new(config).resolve + end + + def resolve_spec(spec, config) + ConnectionSpecification::Resolver.new(resolve_config(config)).resolve(spec) + end + + def test_resolver_with_database_uri_and_current_env_symbol_key + ENV['DATABASE_URL'] = "postgres://localhost/foo" + config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } + actual = resolve_spec(:default_env, config) + expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" } + assert_equal expected, actual + end + + def test_resolver_with_database_uri_and_and_current_env_string_key + ENV['DATABASE_URL'] = "postgres://localhost/foo" + config = { "default_env" => { "adapter" => "not_postgres", "database" => "not_foo" } } + actual = assert_deprecated { resolve_spec("default_env", config) } + expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" } + assert_equal expected, actual + end + + def test_resolver_with_database_uri_and_known_key + ENV['DATABASE_URL'] = "postgres://localhost/foo" + config = { "production" => { "adapter" => "not_postgres", "database" => "not_foo", "host" => "localhost" } } + actual = resolve_spec(:production, config) + expected = { "adapter"=>"not_postgres", "database"=>"not_foo", "host"=>"localhost" } + assert_equal expected, actual + end + + def test_resolver_with_database_uri_and_unknown_symbol_key + ENV['DATABASE_URL'] = "postgres://localhost/foo" + config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } + assert_raises AdapterNotSpecified do + resolve_spec(:production, config) + end + end + + def test_resolver_with_database_uri_and_unknown_string_key + ENV['DATABASE_URL'] = "postgres://localhost/foo" + config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } + assert_deprecated do + assert_raises AdapterNotSpecified do + resolve_spec("production", config) + end + end + end + + def test_resolver_with_database_uri_and_supplied_url + ENV['DATABASE_URL'] = "not-postgres://not-localhost/not_foo" + config = { "production" => { "adapter" => "also_not_postgres", "database" => "also_not_foo" } } + actual = resolve_spec("postgres://localhost/foo", config) + expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" } + assert_equal expected, actual + end + + def test_jdbc_url + config = { "production" => { "url" => "jdbc:postgres://localhost/foo" } } + actual = resolve_config(config) + assert_equal config, actual + end + + def test_environment_does_not_exist_in_config_url_does_exist + ENV['DATABASE_URL'] = "postgres://localhost/foo" + config = { "not_default_env" => { "adapter" => "not_postgres", "database" => "not_foo" } } + actual = resolve_config(config) + expect_prod = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" } + assert_equal expect_prod, actual["default_env"] + end + + def test_url_with_hyphenated_scheme + ENV['DATABASE_URL'] = "ibm-db://localhost/foo" + config = { "default_env" => { "adapter" => "not_postgres", "database" => "not_foo", "host" => "localhost" } } + actual = resolve_spec(:default_env, config) + expected = { "adapter"=>"ibm_db", "database"=>"foo", "host"=>"localhost" } + assert_equal expected, actual + end + + def test_string_connection + config = { "default_env" => "postgres://localhost/foo" } + actual = resolve_config(config) + expected = { "default_env" => + { "adapter" => "postgresql", + "database" => "foo", + "host" => "localhost" + } + } + assert_equal expected, actual + end + + def test_url_sub_key + config = { "default_env" => { "url" => "postgres://localhost/foo" } } + actual = resolve_config(config) + expected = { "default_env" => + { "adapter" => "postgresql", + "database" => "foo", + "host" => "localhost" + } + } + assert_equal expected, actual + end + + def test_hash + config = { "production" => { "adapter" => "postgres", "database" => "foo" } } + actual = resolve_config(config) + assert_equal config, actual + end + + def test_blank + config = {} + actual = resolve_config(config) + assert_equal config, actual + end + + def test_blank_with_database_url + ENV['DATABASE_URL'] = "postgres://localhost/foo" + + config = {} + actual = resolve_config(config) + expected = { "adapter" => "postgresql", + "database" => "foo", + "host" => "localhost" } + assert_equal expected, actual["default_env"] + assert_equal nil, actual["production"] + assert_equal nil, actual["development"] + assert_equal nil, actual["test"] + assert_equal nil, actual[:production] + assert_equal nil, actual[:development] + assert_equal nil, actual[:test] + end + + def test_url_sub_key_with_database_url + ENV['DATABASE_URL'] = "NOT-POSTGRES://localhost/NOT_FOO" + + config = { "default_env" => { "url" => "postgres://localhost/foo" } } + actual = resolve_config(config) + expected = { "default_env" => + { "adapter" => "postgresql", + "database" => "foo", + "host" => "localhost" + } + } + assert_equal expected, actual + end + + def test_merge_no_conflicts_with_database_url + ENV['DATABASE_URL'] = "postgres://localhost/foo" + + config = {"default_env" => { "pool" => "5" } } + actual = resolve_config(config) + expected = { "default_env" => + { "adapter" => "postgresql", + "database" => "foo", + "host" => "localhost", + "pool" => "5" + } + } + assert_equal expected, actual + end + + def test_merge_conflicts_with_database_url + ENV['DATABASE_URL'] = "postgres://localhost/foo" + + config = {"default_env" => { "adapter" => "NOT-POSTGRES", "database" => "NOT-FOO", "pool" => "5" } } + actual = resolve_config(config) + expected = { "default_env" => + { "adapter" => "postgresql", + "database" => "foo", + "host" => "localhost", + "pool" => "5" + } + } + assert_equal expected, actual + end + end + end +end 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/type_lookup_test.rb b/activerecord/test/cases/connection_adapters/type_lookup_test.rb new file mode 100644 index 0000000000..3958c3bfff --- /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(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/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index ee3d8a81c2..ab2a749ba8 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -51,6 +51,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 +164,10 @@ 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 'replies_count'", e.message + assert_equal "'Topic' has no association called 'undefined_count'", e.message end end diff --git a/activerecord/test/cases/custom_properties_test.rb b/activerecord/test/cases/custom_properties_test.rb new file mode 100644 index 0000000000..397a8e0692 --- /dev/null +++ b/activerecord/test/cases/custom_properties_test.rb @@ -0,0 +1,88 @@ +require 'cases/helper' + +class OverloadedType < ActiveRecord::Base + property :overloaded_float, Type::Integer.new + property :overloaded_string_with_limit, Type::String.new(limit: 50) + property :non_existent_decimal, Type::Decimal.new +end + +class ChildOfOverloadedType < OverloadedType +end + +class GrandchildOfOverloadedType < ChildOfOverloadedType + property :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_equal 2.0, 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_property + 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_overloaded_properties_have_no_default + data = OverloadedType.new + unoverloaded_data = UnoverloadedType.new + + assert_nil data.overloaded_float + assert unoverloaded_data.overloaded_float + 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 non_existent_decimal), column_names + end + end +end diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index 7d438803a1..f885a8cbc0 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 diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb index 1e6ccecfab..409d9a82e2 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 diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb index 6dac5db111..9d25bdd82a 100644 --- a/activerecord/test/cases/explain_test.rb +++ b/activerecord/test/cases/explain_test.rb @@ -26,8 +26,12 @@ if ActiveRecord::Base.connection.supports_explain? sql, binds = queries[0] assert_match "SELECT", sql - assert_match "honda", sql - assert_equal [], binds + if binds.any? + assert_equal 1, binds.length + assert_equal "honda", binds.flatten.last + else + assert_match 'honda', sql + end end def test_exec_explain_with_no_binds diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index eaf2cada9d..937646b09a 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' diff --git a/activerecord/test/cases/hot_compatibility_test.rb b/activerecord/test/cases/hot_compatibility_test.rb index 367d04a154..b4617cf6f9 100644 --- a/activerecord/test/cases/hot_compatibility_test.rb +++ b/activerecord/test/cases/hot_compatibility_test.rb @@ -15,7 +15,7 @@ class HotCompatibilityTest < ActiveRecord::TestCase end teardown do - @klass.connection.drop_table :hot_compatibilities + ActiveRecord::Base.connection.drop_table :hot_compatibilities end test "insert after remove_column" do 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..93fd3b9605 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -339,8 +339,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 +365,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..9b26c30d14 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 @@ -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 diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index 6a02873cba..984d1c2597 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -62,7 +62,7 @@ module ActiveRecord # 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 + 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)" @@ -160,8 +160,8 @@ module ActiveRecord 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 + 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 diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb index 2d7a7ec73a..a7c287515d 100644 --- a/activerecord/test/cases/migration/columns_test.rb +++ b/activerecord/test/cases/migration/columns_test.rb @@ -274,6 +274,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/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/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index 2a7fafc559..e0b03f4735 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 diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 455ec78f68..aa679d4a35 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) + property :value_of_e, Type::Integer.new + end + property :world_population, Type::Integer.new + property :my_house_population, Type::Integer.new +end class Reminder < ActiveRecord::Base; end diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb index c77a818b93..9568aa2217 100644 --- a/activerecord/test/cases/migrator_test.rb +++ b/activerecord/test/cases/migrator_test.rb @@ -92,7 +92,7 @@ module ActiveRecord def test_relative_migrations list = Dir.chdir(MIGRATIONS_ROOT) do - ActiveRecord::Migrator.migrations("valid/") + ActiveRecord::Migrator.migrations("valid") end migration_proxy = list.find { |item| 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 046fe83e54..bc5ccd0fe9 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' @@ -233,6 +234,15 @@ class PersistenceTest < ActiveRecord::TestCase assert_nothing_raised { Minimalistic.create!(:id => 2) } end + def test_save_with_duping_of_destroyed_object + developer = Developer.first + developer.destroy + new_developer = developer.dup + new_developer.save + assert new_developer.persisted? + assert_not new_developer.destroyed? + end + def test_create_many topics = Topic.create([ { "title" => "first" }, { "title" => "second" }]) assert_equal 2, topics.size @@ -820,4 +830,28 @@ 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 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..c719918fd7 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -149,38 +149,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 +187,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..bbd5298da1 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 @@ -101,12 +93,12 @@ module ActiveRecord def test_quote_true assert_equal @quoter.quoted_true, @quoter.quote(true, nil) - assert_equal '1', @quoter.quote(true, Struct.new(:type).new(:integer)) + assert_equal '1', @quoter.quote(true, Type::Integer.new) 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)) + assert_equal '0', @quoter.quote(false, Type::Integer.new) end def test_quote_float @@ -166,26 +158,26 @@ module ActiveRecord 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)) + assert_equal "1", @quoter.quote('1', Type::Integer.new) + assert_equal "1", @quoter.quote('1.2', Type::Integer.new) 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)) + assert_equal "1.0", @quoter.quote('1', Type::Float.new) + assert_equal "1.2", @quoter.quote('1.2', Type::Float.new) 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)) + assert_equal "'lo\\\\l'", @quoter.quote(string, Type::Binary.new) end def test_quote_binary_without_string_to_binary - assert_equal "'lo\\\\l'", @quoter.quote('lo\l', FakeColumn.new(:binary)) + assert_equal "'lo\\\\l'", @quoter.quote('lo\l', Type::Binary.new) 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 @@ -193,7 +185,7 @@ module ActiveRecord end def test_quote_duration_int_column - assert_equal "7200", @quoter.quote(2.hours, FakeColumn.new(:integer)) + assert_equal "7200", @quoter.quote(2.hours, Type::Integer.new) end end end diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index fed199f6e9..e6603f28be 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -87,6 +87,14 @@ class ReflectionTest < ActiveRecord::TestCase end end + def test_irregular_reflection_class_name + ActiveSupport::Inflector.inflections do |inflect| + inflect.irregular 'plural_irregular', 'plurales_irregulares' + end + reflection = AssociationReflection.new(: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 @@ -192,7 +200,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 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 23500bf5d8..2b5c2fd5a4 100644 --- a/activerecord/test/cases/relation/merging_test.rb +++ b/activerecord/test/cases/relation/merging_test.rb @@ -17,10 +17,9 @@ class RelationMergingTest < ActiveRecord::TestCase end def test_relation_to_sql - sql = Post.connection.unprepared_statement do - Post.first.comments.to_sql - end - 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 @@ -81,31 +80,20 @@ class RelationMergingTest < ActiveRecord::TestCase left = Post.where(title: "omg").where(comments_count: 1) right = Post.where(title: "wtf").where(title: "bbq") - expected = [left.where_values[1]] + right.where_values + expected = [left.bind_values[1]] + right.bind_values merged = left.merge(right) - assert_equal expected, merged.where_values + assert_equal expected, merged.bind_values assert !merged.to_sql.include?("omg") assert merged.to_sql.include?("wtf") assert merged.to_sql.include?("bbq") end - def test_merging_removes_rhs_bind_parameters - left = Post.where(id: Arel::Nodes::BindParam.new('?')) - column = Post.columns_hash['id'] - left.bind_values += [[column, 20]] - right = Post.where(id: 10) - - merged = left.merge(right) - assert_equal [], merged.bind_values - end - def test_merging_keeps_lhs_bind_parameters column = Post.columns_hash['id'] binds = [[column, 20]] - right = Post.where(id: Arel::Nodes::BindParam.new('?')) - right.bind_values += binds + right = Post.where(id: 20) left = Post.where(id: 10) merged = left.merge(right) @@ -113,21 +101,18 @@ class RelationMergingTest < ActiveRecord::TestCase end def test_merging_reorders_bind_params - post = Post.first - id_column = Post.columns_hash['id'] - title_column = Post.columns_hash['title'] - - bv = Post.connection.substitute_at id_column, 0 - - right = Post.where(id: bv) - right.bind_values += [[id_column, post.id]] - - left = Post.where(title: bv) - left.bind_values += [[title_column, post.title]] + post = Post.first + right = Post.where(id: 1) + left = Post.where(title: post.title) 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/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index c81a3002d6..1da5c36e1c 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -24,13 +24,18 @@ module ActiveRecord @relation ||= Relation.new FakeKlass.new('posts'), Post.arel_table end - (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope]).each do |method| + (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope, :select]).each do |method| test "##{method}!" do assert relation.public_send("#{method}!", :foo).equal?(relation) assert_equal [:foo], relation.public_send("#{method}_values") end end + test "#_select!" do + assert relation.public_send("_select!", :foo).equal?(relation) + assert_equal [:foo], relation.public_send("select_values") + end + test '#order!' do assert relation.order!('name ASC').equal?(relation) assert_equal ['name ASC'], relation.order_values diff --git a/activerecord/test/cases/relation/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb index c628ca44ff..b9e69bdb08 100644 --- a/activerecord/test/cases/relation/where_chain_test.rb +++ b/activerecord/test/cases/relation/where_chain_test.rb @@ -12,9 +12,15 @@ module ActiveRecord end def test_not_eq - expected = Post.arel_table[@name].not_eq('hello') relation = Post.where.not(title: 'hello') - assert_equal([expected], relation.where_values) + + assert_equal 1, relation.where_values.length + + value = relation.where_values.first + bind = relation.bind_values.first + + assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual + assert_equal 'hello', bind.last end def test_not_null @@ -44,21 +50,29 @@ module ActiveRecord def test_not_eq_with_preceding_where relation = Post.where(title: 'hello').where.not(title: 'world') - expected = Post.arel_table[@name].eq('hello') - assert_equal(expected, relation.where_values.first) + value = relation.where_values.first + bind = relation.bind_values.first + assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::Equality + assert_equal 'hello', bind.last - expected = Post.arel_table[@name].not_eq('world') - assert_equal(expected, relation.where_values.last) + value = relation.where_values.last + bind = relation.bind_values.last + assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual + assert_equal 'world', bind.last end def test_not_eq_with_succeeding_where relation = Post.where.not(title: 'hello').where(title: 'world') - expected = Post.arel_table[@name].not_eq('hello') - assert_equal(expected, relation.where_values.first) + value = relation.where_values.first + bind = relation.bind_values.first + assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual + assert_equal 'hello', bind.last - expected = Post.arel_table[@name].eq('world') - assert_equal(expected, relation.where_values.last) + value = relation.where_values.last + bind = relation.bind_values.last + assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::Equality + assert_equal 'world', bind.last end def test_not_eq_with_string_parameter @@ -79,38 +93,61 @@ module ActiveRecord expected = Post.arel_table['author_id'].not_in([1, 2]) assert_equal(expected, relation.where_values[0]) - expected = Post.arel_table[@name].not_eq('ruby on rails') - assert_equal(expected, relation.where_values[1]) + value = relation.where_values[1] + bind = relation.bind_values.first + + 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') - expected = Post.arel_table[@name].eq('alone') assert_equal 1, relation.where_values.size - assert_equal expected, relation.where_values.first + value = relation.where_values.first + bind = relation.bind_values.first + assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::Equality + assert_equal 'alone', bind.last end def test_rewhere_with_multiple_overwriting_conditions relation = Post.where(title: 'hello').where(body: 'world').rewhere(title: 'alone', body: 'again') - title_expected = Post.arel_table['title'].eq('alone') - body_expected = Post.arel_table['body'].eq('again') - assert_equal 2, relation.where_values.size - assert_equal title_expected, relation.where_values.first - assert_equal body_expected, relation.where_values.second + + value = relation.where_values.first + bind = relation.bind_values.first + assert_bound_ast value, Post.arel_table['title'], Arel::Nodes::Equality + assert_equal 'alone', bind.last + + value = relation.where_values[1] + bind = relation.bind_values[1] + assert_bound_ast value, Post.arel_table['body'], Arel::Nodes::Equality + assert_equal 'again', bind.last + end + + def assert_bound_ast value, table, type + assert_equal table, value.left + assert_kind_of type, value + assert_kind_of Arel::Nodes::BindParam, value.right end def test_rewhere_with_one_overwriting_condition_and_one_unrelated relation = Post.where(title: 'hello').where(body: 'world').rewhere(title: 'alone') - title_expected = Post.arel_table['title'].eq('alone') - body_expected = Post.arel_table['body'].eq('world') - assert_equal 2, relation.where_values.size - assert_equal body_expected, relation.where_values.first - assert_equal title_expected, relation.where_values.second + + value = relation.where_values.first + bind = relation.bind_values.first + + assert_bound_ast value, Post.arel_table['body'], Arel::Nodes::Equality + assert_equal 'world', bind.last + + value = relation.where_values.second + bind = relation.bind_values.second + + assert_bound_ast value, Post.arel_table['title'], Arel::Nodes::Equality + assert_equal 'alone', bind.last end end end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 562cfe6796..88df997a2f 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -14,6 +14,7 @@ require 'models/car' require 'models/engine' require 'models/tyre' require 'models/minivan' +require 'models/aircraft' class RelationTest < ActiveRecord::TestCase @@ -365,6 +366,60 @@ 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 + assert_equal Hash.new, ac.engines.group(:id).count + assert_equal 0, ac.engines.count + ac.save + assert_equal Hash.new, ac.engines.group(:id).count + 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 @@ -688,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 @@ -820,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 @@ -864,6 +930,17 @@ class RelationTest < ActiveRecord::TestCase assert_equal 9, posts.where(:comments_count => 0).count end + def test_count_on_association_relation + author = Author.last + another_author = Author.first + posts = Post.where(author_id: author.id) + + assert_equal author.posts.where(author_id: author.id).size, posts.count + + assert_equal 0, author.posts.where(author_id: another_author.id).size + assert author.posts.where(author_id: another_author.id).empty? + end + def test_count_with_distinct posts = Post.all @@ -874,6 +951,14 @@ class RelationTest < ActiveRecord::TestCase assert_equal 11, posts.distinct(false).select(:comments_count).count end + def test_update_all_with_scope + tag = Tag.first + Post.tagged_with(tag.id).update_all title: "rofl" + list = Post.tagged_with(tag.id).all.to_a + assert_operator list.length, :>, 0 + list.each { |post| assert_equal 'rofl', post.title } + end + def test_count_explicit_columns Post.update_all(:comments_count => nil) posts = Post.all @@ -1621,10 +1706,8 @@ class RelationTest < ActiveRecord::TestCase end def test_merging_removes_rhs_bind_parameters - left = Post.where(id: Arel::Nodes::BindParam.new('?')) - column = Post.columns_hash['id'] - left.bind_values += [[column, 20]] - right = Post.where(id: 10) + left = Post.where(id: 20) + right = Post.where(id: [1,2,3,4]) merged = left.merge(right) assert_equal [], merged.bind_values @@ -1634,8 +1717,7 @@ class RelationTest < ActiveRecord::TestCase column = Post.columns_hash['id'] binds = [[column, 20]] - right = Post.where(id: Arel::Nodes::BindParam.new('?')) - right.bind_values += binds + right = Post.where(id: 20) left = Post.where(id: 10) merged = left.merge(right) @@ -1643,19 +1725,15 @@ class RelationTest < ActiveRecord::TestCase end def test_merging_reorders_bind_params - post = Post.first - id_column = Post.columns_hash['id'] - title_column = Post.columns_hash['title'] - - bv = Post.connection.substitute_at id_column, 0 - - right = Post.where(id: bv) - right.bind_values += [[id_column, post.id]] - - left = Post.where(title: bv) - left.bind_values += [[title_column, post.title]] + post = Post.first + right = Post.where(id: post.id) + left = Post.where(title: post.title) 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/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb index 954eab8022..dca85fb5eb 100644 --- a/activerecord/test/cases/sanitize_test.rb +++ b/activerecord/test/cases/sanitize_test.rb @@ -51,4 +51,31 @@ class SanitizeTest < ActiveRecord::TestCase select_author_sql = Post.send(:sanitize_sql_array, ['']) assert_equal('', select_author_sql) end + + def test_sanitize_sql_like + assert_equal '100\%', Binary.send(:sanitize_sql_like, '100%') + assert_equal 'snake\_cased\_string', Binary.send(:sanitize_sql_like, 'snake_cased_string') + assert_equal 'C:\\\\Programs\\\\MsPaint', Binary.send(:sanitize_sql_like, 'C:\\Programs\\MsPaint') + assert_equal 'normal string 42', Binary.send(:sanitize_sql_like, 'normal string 42') + end + + def test_sanitize_sql_like_with_custom_escape_character + assert_equal '100!%', Binary.send(:sanitize_sql_like, '100%', '!') + assert_equal 'snake!_cased!_string', Binary.send(:sanitize_sql_like, 'snake_cased_string', '!') + assert_equal 'great!!', Binary.send(:sanitize_sql_like, 'great!', '!') + assert_equal 'C:\\Programs\\MsPaint', Binary.send(:sanitize_sql_like, 'C:\\Programs\\MsPaint', '!') + assert_equal 'normal string 42', Binary.send(:sanitize_sql_like, 'normal string 42', '!') + end + + def test_sanitize_sql_like_example_use_case + searchable_post = Class.new(Post) do + def self.search(term) + where("title LIKE ?", sanitize_sql_like(term, '!')) + end + end + + assert_sql(/LIKE '20!% !_reduction!_!!'/) do + searchable_post.search("20% _reduction_!").to_a + end + end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index fd0ef2f89f..9602252b2e 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -1,10 +1,8 @@ require "cases/helper" class SchemaDumperTest < ActiveRecord::TestCase - def setup - super + setup do ActiveRecord::SchemaMigration.create_table - @stream = StringIO.new end def standard_dump @@ -25,7 +23,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 @@ -353,9 +352,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 diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index c46060a646..7dd1f10ce9 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,20 @@ class SerializationTest < ActiveRecord::TestCase ensure ActiveRecord::Base.include_root_in_json = original_root_in_json 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..c8f9d7cf87 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -244,4 +244,20 @@ class SerializedAttributeTest < ActiveRecord::TestCase type = Topic.column_types["content"] assert !type.instance_variable_get("@column").is_a?(ActiveRecord::AttributeMethods::Serialization::Type) end + + def test_serialized_column_should_unserialize_after_update_column + t = Topic.create(content: "first") + assert_equal("first", t.content) + + t.update_column(:content, Topic.serialized_attributes["content"].dump("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/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb index 76da49707f..a704b861cb 100644 --- a/activerecord/test/cases/statement_cache_test.rb +++ b/activerecord/test/cases/statement_cache_test.rb @@ -10,27 +10,61 @@ module ActiveRecord @connection = ActiveRecord::Base.connection end + #Cache v 1.1 tests + def test_statement_cache + Book.create(name: "my book") + Book.create(name: "my other book") + + cache = StatementCache.create(Book.connection) do |params| + Book.where(:name => params.bind) + end + + b = cache.execute([ "my book" ], Book, Book.connection) + assert_equal "my book", b[0].name + b = cache.execute([ "my other book" ], Book, Book.connection) + assert_equal "my other book", b[0].name + end + + + def test_statement_cache_id + b1 = Book.create(name: "my book") + b2 = Book.create(name: "my other book") + + cache = StatementCache.create(Book.connection) do |params| + Book.where(id: params.bind) + end + + b = cache.execute([ b1.id ], Book, Book.connection) + assert_equal b1.name, b[0].name + b = cache.execute([ b2.id ], Book, Book.connection) + assert_equal b2.name, b[0].name + end + + def test_find_or_create_by + Book.create(name: "my book") + + a = Book.find_or_create_by(name: "my book") + b = Book.find_or_create_by(name: "my other book") + + assert_equal("my book", a.name) + assert_equal("my other book", b.name) + end + + #End + def test_statement_cache_with_simple_statement - cache = ActiveRecord::StatementCache.new do + cache = ActiveRecord::StatementCache.create(Book.connection) do |params| Book.where(name: "my book").where("author_id > 3") end Book.create(name: "my book", author_id: 4) - books = cache.execute + books = cache.execute([], Book, Book.connection) assert_equal "my book", books[0].name end - def test_statement_cache_with_nil_statement_raises_error - assert_raise(ArgumentError) do - ActiveRecord::StatementCache.new do - nil - end - end - end - def test_statement_cache_with_complex_statement - cache = ActiveRecord::StatementCache.new do + cache = ActiveRecord::StatementCache.create(Book.connection) do |params| Liquid.joins(:molecules => :electrons).where('molecules.name' => 'dioxane', 'electrons.name' => 'lepton') end @@ -38,12 +72,12 @@ module ActiveRecord molecule = salty.molecules.create(name: 'dioxane') molecule.electrons.create(name: 'lepton') - liquids = cache.execute + liquids = cache.execute([], Book, Book.connection) assert_equal "salty", liquids[0].name end def test_statement_cache_values_differ - cache = ActiveRecord::StatementCache.new do + cache = ActiveRecord::StatementCache.create(Book.connection) do |params| Book.where(name: "my book") end @@ -51,13 +85,13 @@ module ActiveRecord Book.create(name: "my book") end - first_books = cache.execute + first_books = cache.execute([], Book, Book.connection) 3.times do Book.create(name: "my book") end - additional_books = cache.execute + additional_books = cache.execute([], Book, Book.connection) assert first_books != additional_books end end diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index 978cee9cfb..6a34c55011 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -163,6 +163,22 @@ class StoreTest < ActiveRecord::TestCase assert_equal [:width, :height], second_model.stored_attributes[:data] 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 + + 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 + test "YAML coder initializes the store when a Nil value is given" do assert_equal({}, @john.params) end diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index 803a054d7e..b6c5511849 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -10,13 +10,7 @@ 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_sql diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 594b4fb07b..77ab427be0 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -112,7 +112,7 @@ class TimestampTest < ActiveRecord::TestCase previous_starting = task.starting previous_ending = task.ending task.touch(:starting, :ending) - + assert_not_equal previous_starting, task.starting assert_not_equal previous_ending, task.ending assert_in_delta Time.now, task.starting, 1 @@ -170,6 +170,25 @@ class TimestampTest < ActiveRecord::TestCase assert !@developer.no_touching? end + def test_no_touching_with_callbacks + klass = Class.new(ActiveRecord::Base) do + self.table_name = "developers" + + attr_accessor :after_touch_called + + after_touch do |user| + user.after_touch_called = true + end + end + + developer = klass.first + + klass.no_touching do + developer.touch + assert_not developer.after_touch_called + end + end + def test_saving_a_record_with_a_belongs_to_that_specifies_touching_the_parent_should_update_the_parent_updated_at pet = Pet.first owner = pet.owner diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index e6ed85394b..de1f624191 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 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..5d5f442d3a --- /dev/null +++ b/activerecord/test/cases/types_test.rb @@ -0,0 +1,159 @@ +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('').nil? + assert type.type_cast(nil).nil? + + assert type.type_cast(true) + assert type.type_cast(1) + assert type.type_cast('1') + assert type.type_cast('t') + assert type.type_cast('T') + assert type.type_cast('true') + assert type.type_cast('TRUE') + assert type.type_cast('on') + assert type.type_cast('ON') + + # explicitly check for false vs nil + assert_equal false, type.type_cast(false) + assert_equal false, type.type_cast(0) + assert_equal false, type.type_cast('0') + assert_equal false, type.type_cast('f') + assert_equal false, type.type_cast('F') + assert_equal false, type.type_cast('false') + assert_equal false, type.type_cast('FALSE') + assert_equal false, type.type_cast('off') + assert_equal false, type.type_cast('OFF') + assert_equal false, type.type_cast(' ') + assert_equal false, type.type_cast("\u3000\r\n") + assert_equal false, type.type_cast("\u0000") + assert_equal false, type.type_cast('SOMETHING RANDOM') + end + + def test_type_cast_string + type = Type::String.new + assert_equal "1", type.type_cast(true) + assert_equal "0", type.type_cast(false) + assert_equal "123", type.type_cast(123) + end + + def test_type_cast_integer + type = Type::Integer.new + assert_equal 1, type.type_cast(1) + assert_equal 1, type.type_cast('1') + assert_equal 1, type.type_cast('1ignore') + assert_equal 0, type.type_cast('bad1') + assert_equal 0, type.type_cast('bad') + assert_equal 1, type.type_cast(1.7) + assert_equal 0, type.type_cast(false) + assert_equal 1, type.type_cast(true) + assert_nil type.type_cast(nil) + end + + def test_type_cast_non_integer_to_integer + type = Type::Integer.new + assert_nil type.type_cast([1,2]) + assert_nil type.type_cast({1 => 2}) + assert_nil type.type_cast((1..2)) + end + + def test_type_cast_activerecord_to_integer + type = Type::Integer.new + firm = Firm.create(:name => 'Apple') + assert_nil type.type_cast(firm) + end + + def test_type_cast_object_without_to_i_to_integer + type = Type::Integer.new + assert_nil type.type_cast(Object.new) + end + + def test_type_cast_nan_and_infinity_to_integer + type = Type::Integer.new + assert_nil type.type_cast(Float::NAN) + assert_nil type.type_cast(1.0/0.0) + end + + def test_type_cast_float + type = Type::Float.new + assert_equal 1.0, type.type_cast("1") + end + + def test_type_cast_decimal + type = Type::Decimal.new + assert_equal BigDecimal.new("0"), type.type_cast(BigDecimal.new("0")) + assert_equal BigDecimal.new("123"), type.type_cast(123.0) + assert_equal BigDecimal.new("1"), type.type_cast(:"1") + end + + def test_type_cast_binary + type = Type::Binary.new + assert_equal nil, type.type_cast(nil) + assert_equal "1", type.type_cast("1") + assert_equal 1, type.type_cast(1) + end + + def test_type_cast_time + type = Type::Time.new + assert_equal nil, type.type_cast(nil) + assert_equal nil, type.type_cast('') + assert_equal nil, type.type_cast('ABC') + + time_string = Time.now.utc.strftime("%T") + assert_equal time_string, type.type_cast(time_string).strftime("%T") + end + + def test_type_cast_datetime_and_timestamp + type = Type::DateTime.new + assert_equal nil, type.type_cast(nil) + assert_equal nil, type.type_cast('') + assert_equal nil, type.type_cast(' ') + assert_equal nil, type.type_cast('ABC') + + datetime_string = Time.now.utc.strftime("%FT%T") + assert_equal datetime_string, type.type_cast(datetime_string).strftime("%FT%T") + end + + def test_type_cast_date + type = Type::Date.new + assert_equal nil, type.type_cast(nil) + assert_equal nil, type.type_cast('') + assert_equal nil, type.type_cast(' ') + assert_equal nil, type.type_cast('ABC') + + date_string = Time.now.utc.strftime("%F") + assert_equal date_string, type.type_cast(date_string).strftime("%F") + end + + def test_type_cast_duration_to_integer + type = Type::Integer.new + assert_equal 1800, type.type_cast(30.minutes) + assert_equal 7200, type.type_cast(2.hours) + end + + def test_string_to_time_with_timezone + [:utc, :local].each do |zone| + with_timezone_config default: zone do + type = Type::DateTime.new + assert_equal Time.utc(2013, 9, 4, 0, 0, 0), type.type_cast("Wed, 04 Sep 2013 03:00:00 EAT") + 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(utf8_string) + + assert_equal Encoding::ASCII_8BIT, type_cast.encoding + end + end + end + end +end diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb index 602f633c45..e4edc437e6 100644 --- a/activerecord/test/cases/validations/association_validation_test.rb +++ b/activerecord/test/cases/validations/association_validation_test.rb @@ -1,44 +1,14 @@ -# encoding: utf-8 require "cases/helper" require 'models/topic' require 'models/reply' -require 'models/owner' -require 'models/pet' require 'models/man' require 'models/interest' class AssociationValidationTest < ActiveRecord::TestCase - fixtures :topics, :owners + fixtures :topics repair_validations(Topic, Reply) - def test_validates_size_of_association - repair_validations Owner do - assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 } - o = Owner.new('name' => 'nopets') - assert !o.save - assert o.errors[:pets].any? - o.pets.build('name' => 'apet') - assert o.valid? - end - end - - def test_validates_size_of_association_using_within - repair_validations Owner do - assert_nothing_raised { Owner.validates_size_of :pets, :within => 1..2 } - o = Owner.new('name' => 'nopets') - assert !o.save - assert o.errors[:pets].any? - - o.pets.build('name' => 'apet') - assert o.valid? - - 2.times { o.pets.build('name' => 'apet') } - assert !o.save - assert o.errors[:pets].any? - end - end - def test_validates_associated_many Topic.validates_associated(:replies) Reply.validates_presence_of(:content) @@ -94,17 +64,6 @@ class AssociationValidationTest < ActiveRecord::TestCase assert r.valid? end - def test_validates_size_of_association_utf8 - repair_validations Owner do - assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 } - o = Owner.new('name' => 'あいうえおかきくけこ') - assert !o.save - assert o.errors[:pets].any? - o.pets.build('name' => 'あいうえおかきくけこ') - assert o.valid? - end - end - def test_validates_presence_of_belongs_to_association__parent_is_new_record repair_validations(Interest) do # Note that Interest and Man have the :inverse_of option set diff --git a/activerecord/test/cases/validations/length_validation_test.rb b/activerecord/test/cases/validations/length_validation_test.rb new file mode 100644 index 0000000000..4a92da38ce --- /dev/null +++ b/activerecord/test/cases/validations/length_validation_test.rb @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +require "cases/helper" +require 'models/owner' +require 'models/pet' + +class LengthValidationTest < ActiveRecord::TestCase + fixtures :owners + repair_validations(Owner) + + def test_validates_size_of_association + repair_validations Owner do + assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 } + o = Owner.new('name' => 'nopets') + assert !o.save + assert o.errors[:pets].any? + o.pets.build('name' => 'apet') + assert o.valid? + end + end + + def test_validates_size_of_association_using_within + repair_validations Owner do + assert_nothing_raised { Owner.validates_size_of :pets, :within => 1..2 } + o = Owner.new('name' => 'nopets') + assert !o.save + assert o.errors[:pets].any? + + o.pets.build('name' => 'apet') + assert o.valid? + + 2.times { o.pets.build('name' => 'apet') } + assert !o.save + assert o.errors[:pets].any? + end + end + + def test_validates_size_of_association_utf8 + repair_validations Owner do + assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 } + o = Owner.new('name' => 'あいうえおかきくけこ') + assert !o.save + assert o.errors[:pets].any? + o.pets.build('name' => 'あいうえおかきくけこ') + assert o.valid? + end + end +end diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index d80da06e27..a6e1dc72e5 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 @@ -100,7 +100,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..1a690c01a6 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) 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/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/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/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/pirate.rb b/activerecord/test/models/pirate.rb index 7bb0caf44b..90a3c3ecee 100644 --- a/activerecord/test/models/pirate.rb +++ b/activerecord/test/models/pirate.rb @@ -18,7 +18,6 @@ class Pirate < ActiveRecord::Base has_many :treasures, :as => :looter has_many :treasure_estimates, :through => :treasures, :source => :price_estimates - # These both have :autosave enabled because accepts_nested_attributes_for is used on them. has_one :ship has_one :update_only_ship, :class_name => 'Ship' has_one :non_validated_ship, :class_name => 'Ship' @@ -85,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 099e039255..5f01ab0a82 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -40,6 +40,8 @@ class Post < ActiveRecord::Base scope :with_comments, -> { preload(:comments) } scope :with_tags, -> { preload(:taggings) } + scope :tagged_with, ->(id) { joins(:taggings).where(taggings: { tag_id: id }) } + has_many :comments do def find_most_recent order("id DESC").first @@ -145,6 +147,10 @@ class Post < ActiveRecord::Base has_many :lazy_readers has_many :lazy_readers_skimmers_or_not, -> { where(skimmer: [ true, false ]) }, :class_name => 'LazyReader' + has_many :lazy_people, :through => :lazy_readers, :source => :person + has_many :lazy_readers_unscope_skimmers, -> { skimmers_or_not }, :class_name => 'LazyReader' + has_many :lazy_people_unscope_skimmers, :through => :lazy_readers_unscope_skimmers, :source => :person + def self.top(limit) ranked_by_comments.limit_by(limit) end @@ -161,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 @@ -206,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..03a277bbdd --- /dev/null +++ b/activerecord/test/models/publisher/article.rb @@ -0,0 +1,3 @@ +class Publisher::Article < ActiveRecord::Base + has_and_belongs_to_many :magazines +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/reader.rb b/activerecord/test/models/reader.rb index 3a6b7fad34..91afc1898c 100644 --- a/activerecord/test/models/reader.rb +++ b/activerecord/test/models/reader.rb @@ -16,6 +16,8 @@ class LazyReader < ActiveRecord::Base self.table_name = "readers" default_scope -> { where(skimmer: true) } + scope :skimmers_or_not, -> { unscope(:where => :skimmer) } + belongs_to :post belongs_to :person 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/schema/schema.rb b/activerecord/test/schema/schema.rb index da3074e90f..4cce58f4f4 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,14 @@ 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 :audit_logs, force: true do |t| t.column :message, :string, null: false t.column :developer_id, :integer, null: false @@ -103,6 +103,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 @@ -187,6 +188,9 @@ ActiveRecord::Schema.define do t.integer :taggings_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 +389,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 @@ -848,6 +855,12 @@ ActiveRecord::Schema.define do 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'})" end + + 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 + end end Course.connection.create_table :courses, force: true do |t| diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 5e430d20fa..6915a491b0 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,77 @@ +* 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 `SecureRandom::uuid_v3` and `SecureRandom::uuid_v5` to support stable UUID fixtures on PostgreSQL. diff --git a/activesupport/lib/active_support/backtrace_cleaner.rb b/activesupport/lib/active_support/backtrace_cleaner.rb index d58578b7bc..1fec1bea0d 100644 --- a/activesupport/lib/active_support/backtrace_cleaner.rb +++ b/activesupport/lib/active_support/backtrace_cleaner.rb @@ -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/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index 05ca943776..06505bddf9 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -297,14 +297,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 @@ -724,12 +724,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/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/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb index 3d41aa8572..28536e32a4 100644 --- a/activesupport/lib/active_support/core_ext/hash/keys.rb +++ b/activesupport/lib/active_support/core_ext/hash/keys.rb @@ -75,34 +75,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 +106,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 +125,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/load_error.rb b/activesupport/lib/active_support/core_ext/load_error.rb index fe24f3716d..768b980f21 100644 --- a/activesupport/lib/active_support/core_ext/load_error.rb +++ b/activesupport/lib/active_support/core_ext/load_error.rb @@ -7,6 +7,7 @@ class LoadError ] unless method_defined?(:path) + # Returns the path which was unable to be loaded. def path @path ||= begin REGEXPS.find do |regex| @@ -17,9 +18,11 @@ class LoadError end end + # Returns true if the given path name (except perhaps for the ".rb" + # extension) is the missing file which caused the exception to be raised. def is_missing?(location) location.sub(/\.rb$/, '') == path.sub(/\.rb$/, '') end end -MissingSourceFile = LoadError
\ No newline at end of file +MissingSourceFile = LoadError 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/object/duplicable.rb b/activesupport/lib/active_support/core_ext/object/duplicable.rb index 9cd7485e2e..3d2c809c9f 100644 --- a/activesupport/lib/active_support/core_ext/object/duplicable.rb +++ b/activesupport/lib/active_support/core_ext/object/duplicable.rb @@ -78,6 +78,9 @@ end require 'bigdecimal' class BigDecimal + # Needed to support Ruby 1.9.x, as it doesn't allow dup on BigDecimal, instead + # raises TypeError exception. Checking here on the runtime whether BigDecimal + # will allow dup or not. begin BigDecimal.new('4.56').dup 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/string/access.rb b/activesupport/lib/active_support/core_ext/string/access.rb index 6018fd9641..ebd0dd3fc7 100644 --- a/activesupport/lib/active_support/core_ext/string/access.rb +++ b/activesupport/lib/active_support/core_ext/string/access.rb @@ -64,7 +64,7 @@ class String # Returns the first character. If a limit is supplied, returns a substring # from the beginning of the string until it reaches the limit value. If the - # given limit is greater than or equal to the string length, returns self. + # given limit is greater than or equal to the string length, returns a copy of self. # # str = "hello" # str.first # => "h" @@ -76,7 +76,7 @@ class String if limit == 0 '' elsif limit >= size - self + self.dup else to(limit - 1) end @@ -84,7 +84,7 @@ class String # Returns the last character of the string. If a limit is supplied, returns a substring # from the end of the string until it reaches the limit value (counting backwards). If - # the given limit is greater than or equal to the string length, returns self. + # the given limit is greater than or equal to the string length, returns a copy of self. # # str = "hello" # str.last # => "o" @@ -96,7 +96,7 @@ class String if limit == 0 '' elsif limit >= size - self + self.dup else from(-limit) end diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb index 18273573e0..a943752f17 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -31,7 +31,7 @@ class String def pluralize(count = nil, locale = :en) locale = count if count.is_a?(Symbol) if count == 1 - self + self.dup else ActiveSupport::Inflector.pluralize(self, locale) end diff --git a/activesupport/lib/active_support/core_ext/thread.rb b/activesupport/lib/active_support/core_ext/thread.rb index ac1ffa4128..4cd6634558 100644 --- a/activesupport/lib/active_support/core_ext/thread.rb +++ b/activesupport/lib/active_support/core_ext/thread.rb @@ -62,6 +62,13 @@ class Thread _locals.has_key?(key.to_sym) end + # Freezes the thread so that thread local variables cannot be set via + # Thread#thread_variable_set, nor can fiber local variables be set. + # + # me = Thread.current + # me.freeze + # me.thread_variable_set(:oliver, "a") #=> RuntimeError: can't modify frozen thread locals + # me[:oliver] = "a" #=> RuntimeError: can't modify frozen thread locals def freeze _locals.freeze super diff --git a/activesupport/lib/active_support/core_ext/time/conversions.rb b/activesupport/lib/active_support/core_ext/time/conversions.rb index 9fd26156c7..dbf1f2f373 100644 --- a/activesupport/lib/active_support/core_ext/time/conversions.rb +++ b/activesupport/lib/active_support/core_ext/time/conversions.rb @@ -20,7 +20,7 @@ class Time :iso8601 => lambda { |time| time.iso8601 } } - # Converts to a formatted string. See DATE_FORMATS for builtin formats. + # Converts to a formatted string. See DATE_FORMATS for built-in formats. # # This method is aliased to <tt>to_s</tt>. # diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index 59675d744e..8a545e4386 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -180,10 +180,11 @@ 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" 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/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index 6229d15619..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 @@ -155,7 +175,7 @@ module ActiveSupport # # Singular names are not handled correctly: # - # 'business'.classify # => "Busines" + # 'calculus'.classify # => "Calculu" def classify(table_name) # strip out any leading schema name camelize(singularize(table_name.to_s.sub(/.*\./, ''))) @@ -173,7 +193,7 @@ module ActiveSupport # 'ActiveRecord::CoreExtensions::String::Inflections'.demodulize # => "Inflections" # 'Inflections'.demodulize # => "Inflections" # '::Inflections'.demodulize # => "Inflections" - # ''.demodulize # => '' + # ''.demodulize # => "" # # See also +deconstantize+. def demodulize(path) @@ -230,7 +250,7 @@ module ActiveSupport def constantize(camel_cased_word) names = camel_cased_word.split('::') - # Trigger a builtin NameError exception including the ill-formed constant in the message. + # Trigger a built-in NameError exception including the ill-formed constant in the message. Object.const_get(camel_cased_word) if names.empty? # Remove the first blank element in case of '::ClassName' notation. @@ -244,8 +264,8 @@ module ActiveSupport next candidate if constant.const_defined?(name, false) next candidate unless Object.const_defined?(name) - # Go down the ancestors to check it it's owned - # directly before we reach Object or the end of ancestors. + # Go down the ancestors to check if it is owned directly. The check + # stops when we reach Object or the end of ancestors tree. constant = constant.ancestors.inject do |const, ancestor| break const if ancestor == Object break ancestor if ancestor.const_defined?(name, false) 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/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..e6c125bfdd 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' 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..f2a2f3c3db 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) 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/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_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb index 0a40aeb96c..224172e39f 100644 --- a/activesupport/test/core_ext/date_time_ext_test.rb +++ b/activesupport/test/core_ext/date_time_ext_test.rb @@ -162,6 +162,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) diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index 69a380c7cb..cb706d77c2 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 @@ -84,6 +88,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 +116,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 +144,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 +172,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 +220,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 +248,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,7 +657,7 @@ class HashExtTest < ActiveSupport::TestCase assert_equal 1, h['first'] end - def test_indifferent_subhashes + 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"}} @@ -697,6 +719,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" } } }) @@ -905,11 +937,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 +950,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) 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/object/duplicable_test.rb b/activesupport/test/core_ext/object/duplicable_test.rb index e0566e012c..84512380cf 100644 --- a/activesupport/test/core_ext/object/duplicable_test.rb +++ b/activesupport/test/core_ext/object/duplicable_test.rb @@ -5,34 +5,27 @@ require 'active_support/core_ext/numeric/time' class DuplicableTest < ActiveSupport::TestCase RAISE_DUP = [nil, false, true, :symbol, 1, 2.3, 5.seconds] - YES = ['1', Object.new, /foo/, [], {}, Time.now, Class.new, Module.new] - NO = [] + ALLOW_DUP = ['1', Object.new, /foo/, [], {}, Time.now, Class.new, Module.new] + # Needed to support Ruby 1.9.x, as it doesn't allow dup on BigDecimal, instead + # raises TypeError exception. Checking here on the runtime whether BigDecimal + # will allow dup or not. begin bd = BigDecimal.new('4.56') - YES << bd.dup + ALLOW_DUP << bd.dup rescue TypeError RAISE_DUP << bd end - def test_duplicable - (RAISE_DUP + NO).each do |v| + RAISE_DUP.each do |v| assert !v.duplicable? + assert_raises(TypeError, v.class.name) { v.dup } end - YES.each do |v| - assert v.duplicable?, "#{v.class} should be duplicable" - end - - (YES + NO).each do |v| + ALLOW_DUP.each do |v| + assert v.duplicable?, "#{ v.class } should be duplicable" assert_nothing_raised { v.dup } end - - RAISE_DUP.each do |v| - assert_raises(TypeError, v.class.name) do - v.dup - end - end 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..7457c4655a 100644 --- a/activesupport/test/core_ext/object/to_query_test.rb +++ b/activesupport/test/core_ext/object/to_query_test.rb @@ -49,13 +49,15 @@ class ToQueryTest < ActiveSupport::TestCase 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/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index ea12f1ced5..95df173880 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -58,6 +58,11 @@ class StringInflectionsTest < ActiveSupport::TestCase assert_equal("blargles", "blargle".pluralize(2)) end + test 'pluralize with count = 1 still returns new string' do + name = "Kuldeep" + assert_not_same name.pluralize(1), name + end + def test_singularize SingularToPlural.each do |singular, plural| assert_equal(singular, plural.singularize) @@ -296,6 +301,12 @@ class StringAccessTest < ActiveSupport::TestCase assert_equal 'x', 'x'.first(4) end + test "#first with Fixnum >= string length still returns a new string" do + string = "hello" + different_string = string.first(5) + assert_not_same different_string, string + end + test "#last returns the last character" do assert_equal "o", "hello".last assert_equal 'x', 'x'.last @@ -308,6 +319,12 @@ class StringAccessTest < ActiveSupport::TestCase assert_equal 'x', 'x'.last(4) end + test "#last with Fixnum >= string length still returns a new string" do + string = "hello" + different_string = string.last(5) + assert_not_same different_string, string + end + test "access returns a real string" do hash = {} hash["h"] = true diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index b0b4738eb3..eb8b0d878e 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 @@ -516,9 +516,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/multibyte_conformance.rb b/activesupport/test/multibyte_conformance_test.rb index 2baf724da4..6ab8fa28ee 100644 --- a/activesupport/test/multibyte_conformance.rb +++ b/activesupport/test/multibyte_conformance_test.rb @@ -20,8 +20,8 @@ class Downloader target.write l end end - end - end + end + end end end diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb index 9bdb92024e..a7a0ae02e7 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 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/time_zone_test.rb b/activesupport/test/time_zone_test.rb index 79ec57af2b..127bcc2b4d 100644 --- a/activesupport/test/time_zone_test.rb +++ b/activesupport/test/time_zone_test.rb @@ -254,6 +254,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)'] 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..4cd4a9d70c 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/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/code/getting_started/Gemfile b/guides/code/getting_started/Gemfile index c3d7e96c4d..13a0ef44a9 100644 --- a/guides/code/getting_started/Gemfile +++ b/guides/code/getting_started/Gemfile @@ -2,11 +2,11 @@ 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 'rails', '4.1.1' +# Use SQLite3 as the database for Active Record gem 'sqlite3' # Use SCSS for stylesheets -gem 'sass-rails', '~> 4.0.1' +gem 'sass-rails', '~> 4.0.3' # Use Uglifier as compressor for JavaScript assets gem 'uglifier', '>= 1.3.0' # Use CoffeeScript for .js.coffee assets and views @@ -14,7 +14,7 @@ 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 +# 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' @@ -29,7 +29,7 @@ gem 'spring', group: :development # 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/guides/code/getting_started/Gemfile.lock b/guides/code/getting_started/Gemfile.lock index a2ab76c908..f26cf58e6a 100644 --- a/guides/code/getting_started/Gemfile.lock +++ b/guides/code/getting_started/Gemfile.lock @@ -1,34 +1,33 @@ GEM remote: https://rubygems.org/ specs: - actionmailer (4.1.0) - actionpack (= 4.1.0) - actionview (= 4.1.0) + actionmailer (4.1.1) + actionpack (= 4.1.1) + actionview (= 4.1.1) mail (~> 2.5.4) - actionpack (4.1.0) - actionview (= 4.1.0) - activesupport (= 4.1.0) + actionpack (4.1.1) + actionview (= 4.1.1) + activesupport (= 4.1.1) rack (~> 1.5.2) rack-test (~> 0.6.2) - actionview (4.1.0) - activesupport (= 4.1.0) + actionview (4.1.1) + activesupport (= 4.1.1) builder (~> 3.1) erubis (~> 2.7.0) - activemodel (4.1.0) - activesupport (= 4.1.0) + activemodel (4.1.1) + activesupport (= 4.1.1) builder (~> 3.1) - activerecord (4.1.0) - activemodel (= 4.1.0) - activesupport (= 4.1.0) + activerecord (4.1.1) + activemodel (= 4.1.1) + activesupport (= 4.1.1) arel (~> 5.0.0) - activesupport (4.1.0) + activesupport (4.1.1) 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) + arel (5.0.1.20140414130214) builder (3.2.2) coffee-rails (4.0.1) coffee-script (>= 2.2.0) @@ -52,52 +51,52 @@ GEM 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) + minitest (5.3.4) + multi_json (1.10.1) + polyglot (0.3.4) 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) + rails (4.1.1) + actionmailer (= 4.1.1) + actionpack (= 4.1.1) + actionview (= 4.1.1) + activemodel (= 4.1.1) + activerecord (= 4.1.1) + activesupport (= 4.1.1) 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) + railties (= 4.1.1) + sprockets-rails (~> 2.0) + railties (4.1.1) + actionpack (= 4.1.1) + activesupport (= 4.1.1) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rake (10.1.1) + rake (10.3.2) rdoc (4.1.1) json (~> 1.4) - sass (3.2.13) - sass-rails (4.0.1) + sass (3.2.19) + sass-rails (4.0.3) railties (>= 4.0.0, < 5.0) - sass (>= 3.1.10) - sprockets-rails (~> 2.0.0) + sass (~> 3.2.0) + sprockets (~> 2.8, <= 2.11.0) + sprockets-rails (~> 2.0) sdoc (0.4.0) json (~> 1.8) rdoc (~> 4.0, < 5.0) spring (1.0.0) - sprockets (2.10.1) + sprockets (2.11.0) hike (~> 1.2) multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) - sprockets-rails (2.0.1) + sprockets-rails (2.1.3) actionpack (>= 3.0) activesupport (>= 3.0) sprockets (~> 2.8) sqlite3 (1.3.8) - thor (0.18.1) - thread_safe (0.1.3) - atomic + thor (0.19.1) + thread_safe (0.3.3) tilt (1.4.1) treetop (1.4.15) polyglot @@ -117,8 +116,8 @@ DEPENDENCIES coffee-rails (~> 4.0.0) jbuilder (~> 2.0) jquery-rails - rails (= 4.1.0) - sass-rails (~> 4.0.1) + rails (= 4.1.1) + sass-rails (~> 4.0.3) sdoc (~> 0.4.0) spring sqlite3 diff --git a/guides/code/getting_started/app/assets/javascripts/posts.js.coffee b/guides/code/getting_started/app/assets/javascripts/articles.js.coffee index 24f83d18bb..24f83d18bb 100644 --- a/guides/code/getting_started/app/assets/javascripts/posts.js.coffee +++ b/guides/code/getting_started/app/assets/javascripts/articles.js.coffee diff --git a/guides/code/getting_started/app/assets/stylesheets/posts.css.scss b/guides/code/getting_started/app/assets/stylesheets/articles.css.scss index 1a7e15390c..cca548710d 100644 --- a/guides/code/getting_started/app/assets/stylesheets/posts.css.scss +++ b/guides/code/getting_started/app/assets/stylesheets/articles.css.scss @@ -1,3 +1,3 @@ -// Place all the styles related to the posts controller here. +// Place all the styles related to the articles 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/articles_controller.rb b/guides/code/getting_started/app/controllers/articles_controller.rb new file mode 100644 index 0000000000..275b84e8b7 --- /dev/null +++ b/guides/code/getting_started/app/controllers/articles_controller.rb @@ -0,0 +1,53 @@ +class ArticlesController < ApplicationController + + http_basic_authenticate_with name: "dhh", password: "secret", except: [:index, :show] + + def index + @articles = Article.all + end + + def show + @article = Article.find(params[:id]) + end + + def edit + @article = Article.find(params[:id]) + end + + def update + @article = Article.find(params[:id]) + + if @article.update(article_params) + redirect_to action: :show, id: @article.id + else + render 'edit' + end + end + + def new + @article = Article.new + end + + def create + @article = Article.new(article_params) + + if @article.save + redirect_to action: :show, id: @article.id + else + render 'new' + end + end + + def destroy + @article = Article.find(params[:id]) + @article.destroy + + redirect_to action: :index + end + + private + + def article_params + params.require(:article).permit(:title, :text) + end +end diff --git a/guides/code/getting_started/app/controllers/comments_controller.rb b/guides/code/getting_started/app/controllers/comments_controller.rb index b2d9bcdf7f..61813b1003 100644 --- a/guides/code/getting_started/app/controllers/comments_controller.rb +++ b/guides/code/getting_started/app/controllers/comments_controller.rb @@ -3,16 +3,16 @@ 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) + @article = Article.find(params[:article_id]) + @comment = @article.comments.create(comment_params) + redirect_to article_path(@article) end def destroy - @post = Post.find(params[:post_id]) - @comment = @post.comments.find(params[:id]) + @article = Article.find(params[:article_id]) + @comment = @article.comments.find(params[:id]) @comment.destroy - redirect_to post_path(@post) + redirect_to article_path(@article) end private 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/helpers/articles_helper.rb b/guides/code/getting_started/app/helpers/articles_helper.rb new file mode 100644 index 0000000000..2968277595 --- /dev/null +++ b/guides/code/getting_started/app/helpers/articles_helper.rb @@ -0,0 +1,2 @@ +module ArticlesHelper +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/models/post.rb b/guides/code/getting_started/app/models/article.rb index 64e0d721fd..6fc7888be2 100644 --- a/guides/code/getting_started/app/models/post.rb +++ b/guides/code/getting_started/app/models/article.rb @@ -1,6 +1,6 @@ -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base has_many :comments, dependent: :destroy - + validates :title, presence: true, length: { minimum: 5 } diff --git a/guides/code/getting_started/app/models/comment.rb b/guides/code/getting_started/app/models/comment.rb index 4e76c5b5b0..e2646a324f 100644 --- a/guides/code/getting_started/app/models/comment.rb +++ b/guides/code/getting_started/app/models/comment.rb @@ -1,3 +1,3 @@ class Comment < ActiveRecord::Base - belongs_to :post + belongs_to :article end diff --git a/guides/code/getting_started/app/views/posts/_form.html.erb b/guides/code/getting_started/app/views/articles/_form.html.erb index f2f83585e1..87e3353ed2 100644 --- a/guides/code/getting_started/app/views/posts/_form.html.erb +++ b/guides/code/getting_started/app/views/articles/_form.html.erb @@ -1,10 +1,10 @@ -<%= form_for @post do |f| %> - <% if @post.errors.any? %> +<%= form_for @article do |f| %> + <% 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> diff --git a/guides/code/getting_started/app/views/posts/edit.html.erb b/guides/code/getting_started/app/views/articles/edit.html.erb index 393e7430d0..14236e2a98 100644 --- a/guides/code/getting_started/app/views/posts/edit.html.erb +++ b/guides/code/getting_started/app/views/articles/edit.html.erb @@ -1,5 +1,5 @@ -<h1>Edit post</h1> - +<h1>Edit article</h1> + <%= render 'form' %> - + <%= link_to 'Back', action: :index %> diff --git a/guides/code/getting_started/app/views/articles/index.html.erb b/guides/code/getting_started/app/views/articles/index.html.erb new file mode 100644 index 0000000000..80e9c8c60c --- /dev/null +++ b/guides/code/getting_started/app/views/articles/index.html.erb @@ -0,0 +1,21 @@ +<h1>Listing Articles</h1> +<table> + <tr> + <th>Title</th> + <th>Text</th> + <th></th> + <th></th> + <th></th> + </tr> + +<% @articles.each do |article| %> + <tr> + <td><%= article.title %></td> + <td><%= article.text %></td> + <td><%= link_to 'Show', action: :show, id: article.id %></td> + <td><%= link_to 'Edit', action: :edit, id: article.id %></td> + <td><%= link_to 'Destroy', { action: :destroy, id: article.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/articles/new.html.erb index efa81038ec..652b1c9c0b 100644 --- a/guides/code/getting_started/app/views/posts/new.html.erb +++ b/guides/code/getting_started/app/views/articles/new.html.erb @@ -1,5 +1,5 @@ -<h1>New post</h1> - +<h1>New article</h1> + <%= render 'form' %> - + <%= link_to 'Back', action: :index %> diff --git a/guides/code/getting_started/app/views/articles/show.html.erb b/guides/code/getting_started/app/views/articles/show.html.erb new file mode 100644 index 0000000000..6959c80bdb --- /dev/null +++ b/guides/code/getting_started/app/views/articles/show.html.erb @@ -0,0 +1,18 @@ +<p> + <strong>Title:</strong> + <%= @article.title %> +</p> + +<p> + <strong>Text:</strong> + <%= @article.text %> +</p> + +<h2>Comments</h2> +<%= render @article.comments %> + +<h2>Add a comment:</h2> +<%= render "comments/form" %> + +<%= link_to 'Edit Article', edit_article_path(@article) %> | +<%= link_to 'Back to Articles', articles_path %> diff --git a/guides/code/getting_started/app/views/comments/_comment.html.erb b/guides/code/getting_started/app/views/comments/_comment.html.erb index 593493339e..f7cbfaebfa 100644 --- a/guides/code/getting_started/app/views/comments/_comment.html.erb +++ b/guides/code/getting_started/app/views/comments/_comment.html.erb @@ -2,14 +2,14 @@ <strong>Commenter:</strong> <%= comment.commenter %> </p> - + <p> <strong>Comment:</strong> <%= comment.body %> </p> <p> - <%= link_to 'Destroy Comment', [comment.post, comment], + <%= link_to 'Destroy Comment', [comment.article, 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 index 00cb3a08f0..5850c41a17 100644 --- a/guides/code/getting_started/app/views/comments/_form.html.erb +++ b/guides/code/getting_started/app/views/comments/_form.html.erb @@ -1,4 +1,4 @@ -<%= form_for([@post, @post.comments.build]) do |f| %> +<%= form_for([@article, @article.comments.build]) do |f| %> <p> <%= f.label :commenter %><br /> <%= f.text_field :commenter %> 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/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 index 56be8dd3cc..1cabd0d217 100644 --- a/guides/code/getting_started/app/views/welcome/index.html.erb +++ b/guides/code/getting_started/app/views/welcome/index.html.erb @@ -1,4 +1,4 @@ <h1>Hello, Rails!</h1> -<%= link_to "My Blog", controller: "posts" %> -<%= link_to "New Post", new_post_path %> +<%= link_to "My Blog", controller: "articles" %> +<%= link_to "New Article", new_article_path %> diff --git a/guides/code/getting_started/config/environments/development.rb b/guides/code/getting_started/config/environments/development.rb index ae9ffe209a..5c1c600feb 100644 --- a/guides/code/getting_started/config/environments/development.rb +++ b/guides/code/getting_started/config/environments/development.rb @@ -27,4 +27,12 @@ Rails.application.configure do # Debug mode disables concatenation and preprocessing of 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. + config.assets.raise_runtime_errors = true end diff --git a/guides/code/getting_started/config/routes.rb b/guides/code/getting_started/config/routes.rb index 65d273b58d..97abca99b9 100644 --- a/guides/code/getting_started/config/routes.rb +++ b/guides/code/getting_started/config/routes.rb @@ -1,5 +1,5 @@ Rails.application.routes.draw do - resources :posts do + resources :articles do resources :comments end diff --git a/guides/code/getting_started/db/migrate/20130122042648_create_posts.rb b/guides/code/getting_started/db/migrate/20130122042648_create_articles.rb index 602bef31ab..6bb255e89f 100644 --- a/guides/code/getting_started/db/migrate/20130122042648_create_posts.rb +++ b/guides/code/getting_started/db/migrate/20130122042648_create_articles.rb @@ -1,6 +1,6 @@ -class CreatePosts < ActiveRecord::Migration +class CreateArticles < ActiveRecord::Migration def change - create_table :posts do |t| + create_table :articles do |t| t.string :title t.text :text diff --git a/guides/code/getting_started/db/migrate/20130122045842_create_comments.rb b/guides/code/getting_started/db/migrate/20130122045842_create_comments.rb index 3e51f9c0f7..1f765839ac 100644 --- a/guides/code/getting_started/db/migrate/20130122045842_create_comments.rb +++ b/guides/code/getting_started/db/migrate/20130122045842_create_comments.rb @@ -3,7 +3,7 @@ class CreateComments < ActiveRecord::Migration create_table :comments do |t| t.string :commenter t.text :body - t.references :post, index: true + t.references :article, index: true t.timestamps end diff --git a/guides/code/getting_started/db/schema.rb b/guides/code/getting_started/db/schema.rb index 101fe712a1..be40f7cb0e 100644 --- a/guides/code/getting_started/db/schema.rb +++ b/guides/code/getting_started/db/schema.rb @@ -13,21 +13,21 @@ ActiveRecord::Schema.define(version: 20130122045842) do - create_table "comments", force: true do |t| - t.string "commenter" - t.text "body" - t.integer "post_id" + create_table "articles", force: true do |t| + t.string "title" + t.text "text" 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" + create_table "comments", force: true do |t| + t.string "commenter" + t.text "body" + t.integer "article_id" t.datetime "created_at" t.datetime "updated_at" end + add_index "comments", ["article_id"], name: "index_comments_on_article_id" + end diff --git a/guides/code/getting_started/test/controllers/posts_controller_test.rb b/guides/code/getting_started/test/controllers/articles_controller_test.rb index 7a6ee4f1db..361aa0f47f 100644 --- a/guides/code/getting_started/test/controllers/posts_controller_test.rb +++ b/guides/code/getting_started/test/controllers/articles_controller_test.rb @@ -1,6 +1,6 @@ require 'test_helper' -class PostsControllerTest < ActionController::TestCase +class ArticlesControllerTest < ActionController::TestCase # test "the truth" do # assert true # end diff --git a/guides/code/getting_started/test/fixtures/posts.yml b/guides/code/getting_started/test/fixtures/articles.yml index 46b01c3bb4..46b01c3bb4 100644 --- a/guides/code/getting_started/test/fixtures/posts.yml +++ b/guides/code/getting_started/test/fixtures/articles.yml diff --git a/guides/code/getting_started/test/fixtures/comments.yml b/guides/code/getting_started/test/fixtures/comments.yml index 9e409d8a61..05ad26f051 100644 --- a/guides/code/getting_started/test/fixtures/comments.yml +++ b/guides/code/getting_started/test/fixtures/comments.yml @@ -3,9 +3,9 @@ one: commenter: MyString body: MyText - post_id: + article_id: two: commenter: MyString body: MyText - post_id: + article_id: diff --git a/guides/code/getting_started/test/helpers/articles_helper_test.rb b/guides/code/getting_started/test/helpers/articles_helper_test.rb new file mode 100644 index 0000000000..b341344067 --- /dev/null +++ b/guides/code/getting_started/test/helpers/articles_helper_test.rb @@ -0,0 +1,4 @@ +require 'test_helper' + +class ArticlesHelperTest < 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/models/post_test.rb b/guides/code/getting_started/test/models/article_test.rb index 6d9d463a71..11c8abe5f4 100644 --- a/guides/code/getting_started/test/models/post_test.rb +++ b/guides/code/getting_started/test/models/article_test.rb @@ -1,6 +1,6 @@ require 'test_helper' -class PostTest < ActiveSupport::TestCase +class ArticleTest < ActiveSupport::TestCase # test "the truth" do # assert true # end diff --git a/guides/code/getting_started/test/test_helper.rb b/guides/code/getting_started/test/test_helper.rb index f91a4375dc..ecbaf77ea7 100644 --- a/guides/code/getting_started/test/test_helper.rb +++ b/guides/code/getting_started/test/test_helper.rb @@ -6,9 +6,6 @@ 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... 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/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..2302a618b6 100644 --- a/guides/source/2_3_release_notes.md +++ b/guides/source/2_3_release_notes.md @@ -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..db34fa401f 100644 --- a/guides/source/3_0_release_notes.md +++ b/guides/source/3_0_release_notes.md @@ -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/_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..6ec3aa78a4 100644 --- a/guides/source/_welcome.html.erb +++ b/guides/source/_welcome.html.erb @@ -15,11 +15,5 @@ </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.1/">Rails 4.1.1</a>, <a href="http://guides.rubyonrails.org/v4.0.5/">Rails 4.0.5</a>, <a href="http://guides.rubyonrails.org/v3.2.18/">Rails 3.2.18</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 ee2b00aedb..1735188f27 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -260,7 +260,7 @@ used: params.require(:log_entry).permit! ``` -This will mark the `:log_entry` parameters hash and any subhash of it +This will mark the `:log_entry` parameters hash and any sub-hash of it permitted. Extreme care should be taken when using `permit!` as it will allow all current and future model attributes to be mass-assigned. diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index 6dc7fb1606..c67f6188c4 100644 --- a/guides/source/action_mailer_basics.md +++ b/guides/source/action_mailer_basics.md @@ -30,7 +30,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 @@ -146,8 +146,8 @@ 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 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_record_basics.md b/guides/source/active_record_basics.md index a184f0753d..21022f1abb 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,10 +309,10 @@ 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 +Validation is a very important issue to consider when persisting to the database, so the methods `create`, `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 +perform any operation on the database. All of these have a bang counterpart (that is, `create!`, `save!` and `update!`), which are stricter in that they raise the exception `ActiveRecord::RecordInvalid` if validation fails. A quick example to illustrate: diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index fbcce325ed..f0ae3c729e 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -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/active_record_postgresql.md b/guides/source/active_record_postgresql.md new file mode 100644 index 0000000000..b69c8dbb7a --- /dev/null +++ b/guides/source/active_record_postgresql.md @@ -0,0 +1,433 @@ +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. + +-------------------------------------------------------------------------------- + +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 :book do |t| + t.string 'title' + t.string 'tags', array: true + t.integer 'ratings', array: true +end + +# 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 are mapped to normal text. + + +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") +``` + +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! +p 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..ee8cf4ade6 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -472,8 +472,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 +511,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. @@ -690,32 +690,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').except(: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: ```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 +723,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 +741,27 @@ 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 +797,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 +824,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 +965,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 +988,7 @@ class Guest < ActiveRecord::Base end class Tag < ActiveRecord::Base - belongs_to :post + belongs_to :article end ``` @@ -995,64 +997,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 +1123,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 +1143,18 @@ 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. ```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. +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 +1164,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 +1172,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 +1182,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 +1191,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 +1206,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 +1214,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 +1230,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 @@ -1591,20 +1593,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 +1696,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 +1725,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 +1742,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..cb459626d5 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? @@ -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 834c94e2ec..4f37bf971a 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 ``` @@ -761,7 +761,7 @@ Arguments may be bare constant names: Math.qualified_const_get("E") # => 2.718281828459045 ``` -These methods are analogous to their builtin counterparts. In particular, +These methods are analogous to their built-in counterparts. In particular, `qualified_constant_defined?` accepts an optional second argument to be able to say whether you want the predicate to look in the ancestors. This flag is taken into account for each constant in the expression while @@ -792,7 +792,7 @@ N.qualified_const_defined?("C::X") # => true As the last example implies, the second argument defaults to true, as in `const_defined?`. -For coherence with the builtin methods only relative paths are accepted. +For coherence with the built-in methods only relative paths are accepted. Absolute qualified constant names like `::Math::PI` raise `NameError`. NOTE: Defined in `active_support/core_ext/module/qualified_const.rb`. @@ -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 @@ -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 @@ -2910,7 +2925,7 @@ NOTE: Defined in `active_support/core_ext/hash/indifferent_access.rb`. ### Compacting -The methods `compact` and `compact!` return a Hash without items with `nil` value. +The methods `compact` and `compact!` return a Hash without items with `nil` value. ```ruby {a: 1, b: 2, c: nil}.compact # => {a: 1, b: 2} @@ -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! @@ -3837,7 +3852,7 @@ rescue NameError => e end ``` -NOTE: Defined in `actionpack/lib/abstract_controller/helpers.rb`. +NOTE: Defined in `active_support/core_ext/name_error.rb`. Extensions to `LoadError` ------------------------- @@ -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! @@ -3860,4 +3875,4 @@ rescue NameError => e end ``` -NOTE: Defined in `actionpack/lib/abstract_controller/helpers.rb`. +NOTE: Defined in `active_support/core_ext/load_error.rb`. diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md index 6c77a40d42..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 { @@ -457,6 +457,7 @@ Most times you only care about the data itself. Here is a shortcut to just get t ActiveSupport::Notifications.subscribe "process_action.action_controller" do |*args| data = args.extract_options! data # { extra: :information } +end ``` You may also subscribe to events matching a regular expression. This enables you to subscribe to diff --git a/guides/source/api_documentation_guidelines.md b/guides/source/api_documentation_guidelines.md index 261538d0be..2a3bb4e34d 100644 --- a/guides/source/api_documentation_guidelines.md +++ b/guides/source/api_documentation_guidelines.md @@ -110,14 +110,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 +175,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 +286,36 @@ 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. + +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 d3dc790500..984480c70f 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -56,11 +56,11 @@ the comment operator on that line to later enable the asset pipeline: To set asset compression methods, set the appropriate configuration options in `production.rb` - `config.assets.css_compressor` for your CSS and -`config.assets.js_compressor` for your Javascript: +`config.assets.js_compressor` for your JavaScript: ```ruby config.assets.css_compressor = :yui -config.assets.js_compressor = :uglify +config.assets.js_compressor = :uglifier ``` NOTE: The `sass-rails` gem is automatically used for CSS compression if included @@ -198,12 +198,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 @@ -581,8 +578,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.digests` 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.digests = false +``` + +When this option is true, digests will be generated for asset URLs. ### Turning Debugging Off @@ -676,7 +686,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. @@ -766,7 +776,7 @@ exception indicating the name of the missing file(s). #### Far-future Expires Header -Precompiled assets exist on the filesystem and are served directly by your web +Precompiled assets exist on the file system and are served directly by your web server. They do not have far-future headers by default, so to get the benefit of fingerprinting you'll have to update your server configuration to add those headers. @@ -913,6 +923,14 @@ 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 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 ------------------------ diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index df38bd7321..22f6f5e7d6 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -1725,58 +1725,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? diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md index b6423dd44e..c652aa6a80 100644 --- a/guides/source/caching_with_rails.md +++ b/guides/source/caching_with_rails.md @@ -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 %> diff --git a/guides/source/command_line.md b/guides/source/command_line.md index 57283f7c40..6efc64c385 100644 --- a/guides/source/command_line.md +++ b/guides/source/command_line.md @@ -60,7 +60,7 @@ 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 => Call with -d to detach @@ -77,7 +77,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 +89,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 +114,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] ... @@ -141,7 +141,7 @@ Example: 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 +182,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 +193,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 +216,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 @@ -254,10 +254,10 @@ $ rails generate scaffold HighScore game:string score:integer The generator checks that there exist the directories for models, controllers, helpers, layouts, functional and unit tests, stylesheets, creates the views, controller, model and database migration for HighScore (creating the `high_scores` table and fields), takes care of the route for the **resource**, and new tests for everything. -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. +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 +269,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,13 +283,13 @@ 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 +$ bin/rails console --sandbox Loading development environment in sandbox (Rails 4.0.0) Any modifications you make will be rolled back on exit irb(main):001:0> @@ -306,7 +306,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 +314,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 +324,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 +333,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 +353,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 +373,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.1.1 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.1.1 +Action Pack version 4.1.1 +Action View version 4.1.1 +Action Mailer version 4.1.1 +Active Support version 4.1.1 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 +394,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 +420,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 +440,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 +452,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 +464,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 +536,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 f8f9e9cbd9..7a9e1beb23 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -56,7 +56,7 @@ These configuration methods are to be called on a `Rails::Railtie` object, such end ``` -* `config.asset_host` sets the host for the assets. Useful when CDNs are used for hosting assets, or when you want to work around the concurrency constraints builtin in browsers using different domain aliases. Shorter version of `config.action_controller.asset_host`. +* `config.asset_host` sets the host for the assets. Useful when CDNs are used for hosting assets, or when you want to work around the concurrency constraints built-in in browsers using different domain aliases. Shorter version of `config.action_controller.asset_host`. * `config.autoload_once_paths` accepts an array of paths from which Rails will autoload constants that won't be wiped per request. Relevant if `config.cache_classes` is false, which is the case in development mode by default. Otherwise, all autoloading happens only once. All elements of this array must also be in `autoload_paths`. Default is an empty array. @@ -388,13 +388,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 @@ -552,7 +552,7 @@ development: $ echo $DATABASE_URL postgresql://localhost/my_database -$ rails runner 'puts ActiveRecord::Base.connections' +$ bin/rails runner 'puts ActiveRecord::Base.connections' {"development"=>{"adapter"=>"postgresql", "host"=>"localhost", "database"=>"my_database"}} ``` @@ -569,7 +569,7 @@ development: $ echo $DATABASE_URL postgresql://localhost/my_database -$ rails runner 'puts ActiveRecord::Base.connections' +$ bin/rails runner 'puts ActiveRecord::Base.connections' {"development"=>{"adapter"=>"postgresql", "host"=>"localhost", "database"=>"my_database", "pool"=>5}} ``` @@ -585,7 +585,7 @@ development: $ echo $DATABASE_URL postgresql://localhost/my_database -$ rails runner 'puts ActiveRecord::Base.connections' +$ bin/rails runner 'puts ActiveRecord::Base.connections' {"development"=>{"adapter"=>"sqlite3", "database"=>"NOT_my_database"}} ``` @@ -644,11 +644,9 @@ development: encoding: unicode database: blog_development pool: 5 - username: blog - password: ``` -Prepared Statements can be disabled thus: +Prepared Statements are enabled by default on PostgreSQL. You can be disable prepared statements by setting `prepared_statements` to `false`: ```yaml production: @@ -656,6 +654,16 @@ production: prepared_statements: false ``` +If enabled, Active Record will create up to `1000` prepared statements per database connection by default. To modify this behavior you can set `statement_limit` to a different value: + +``` +production: + adapter: postgresql + statement_limit: 200 +``` + +The more prepared statements in use: the more memory your database will require. If your PostgreSQL database is hitting memory limits, try lowering `statement_limit` or disabling prepared statements. + #### Configuring an SQLite3 Database for JRuby Platform If you choose to use SQLite3 and are using JRuby, your `config/database.yml` will look a little different. Here's the development section: @@ -721,13 +729,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 36e3862c6b..133ef58fd6 100644 --- a/guides/source/contributing_to_ruby_on_rails.md +++ b/guides/source/contributing_to_ruby_on_rails.md @@ -24,9 +24,9 @@ NOTE: Bugs in the most recent released version of Ruby on Rails are likely to ge ### Creating a Bug Report -If you've found a problem in Ruby on Rails which is not a security risk, do a search in GitHub under [Issues](https://github.com/rails/rails/issues) in case it was already reported. If you find no issue addressing it you can [add a new one](https://github.com/rails/rails/issues/new). (See the next section for reporting security issues.) +If you've found a problem in Ruby on Rails which is not a security risk, do a search in GitHub under [Issues](https://github.com/rails/rails/issues) in case it has already been reported. If you do not find any issue addressing it you may proceed to [open a new one](https://github.com/rails/rails/issues/new). (See the next section for reporting security issues.) -At the minimum, your issue report needs a title and descriptive text. But that's only a minimum. You should include as much relevant information as possible. You need at least to post the code sample that has the issue. Even better is to include a unit test that shows how the expected behavior is not occurring. Your goal should be to make it easy for yourself - and others - to replicate the bug and figure out a fix. +Your issue report should contain a title and a clear description of the issue at the bare minimum. You should include as much relevant information as possible and should at least post a code sample that demonstrates the issue. It would be even better if you could include a unit test that shows how the expected behavior is not occurring. Your goal should be to make it easy for yourself - and others - to replicate the bug and figure out a fix. Then, don't get your hopes up! Unless you have a "Code Red, Mission Critical, the World is Coming to an End" kind of bug, you're creating this issue report in the hope that others with the same problem will be able to collaborate with you on solving it. Do not expect that the issue report will automatically see any activity or that others will jump to fix it. Creating an issue like this is mostly to help yourself start on the path of fixing the problem and for others to confirm it with an "I'm having this problem too" comment. @@ -50,7 +50,7 @@ Please don't put "feature request" items into GitHub Issues. If there's a new feature that you want to see added to Ruby on Rails, you'll need to write the code yourself - or convince someone else to partner with you to write the code. Later in this guide you'll find detailed instructions for proposing a patch to -Ruby on Rails. If you enter a wishlist item in GitHub Issues with no code, you +Ruby on Rails. If you enter a wish list item in GitHub Issues with no code, you can expect it to be marked "invalid" as soon as it's reviewed. Sometimes, the line between 'bug' and 'feature' is a hard one to draw. @@ -69,89 +69,6 @@ won't be accepted." But it's the proper place to discuss new ideas. GitHub Issues are not a particularly good venue for the sometimes long and involved discussions new features require. -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 set up the tests on your own computer. - -### The Easy Way - -The easiest and recommended way to get a development environment ready to hack is to use the [Rails development box](https://github.com/rails/rails-dev-box). - -### The Hard Way - -In case you can't use the Rails development box, see section above, check [this other guide](development_dependencies_install.html). - - -Running an Application Against Your Local Branch ------------------------------------------------- - -The `--dev` flag of `rails new` generates an application that uses your local -branch: - -```bash -$ cd rails -$ 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. - - -Testing Active Record ---------------------- - -This is how you run the Active Record test suite only for SQLite3: - -```bash -$ cd activerecord -$ 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 -``` - -Finally, - -```bash -$ bundle exec rake test -``` - -will now run the four of them in turn. - -You can also run any single test separately: - -```bash -$ 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. - -### 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. - -If you are sure about what you are doing and would like to have a more clear output, there's a way to override the flag: - -```bash -$ RUBYOPT=-W0 bundle exec rake test -``` - -### 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: - -```bash -$ git branch --track 3-0-stable origin/3-0-stable -$ git checkout 3-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. Helping to Resolve Existing Issues ---------------------------------- @@ -227,9 +144,21 @@ WARNING: Docrails has a very strict policy: no code can be touched whatsoever, n Contributing to the Rails Code ------------------------------ +### 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. + +#### The Easy Way + +The easiest and recommended way to get a development environment ready to hack is to use the [Rails development box](https://github.com/rails/rails-dev-box). + +#### The Hard Way + +In case you can't use the Rails development box, see [this other guide](development_dependencies_install.html). + ### Clone the Rails Repository -The first thing you need to do to be able to contribute code is to clone the repository: +To be able to contribute code, you need to clone the Rails repository: ```bash $ git clone git://github.com/rails/rails.git @@ -244,29 +173,31 @@ $ 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 + +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: + +```bash +$ cd rails +$ 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 -Now get busy and add or 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. * Include tests that fail without your code, and pass with it. * Update the (surrounding) documentation, examples elsewhere, and the guides: whatever is affected by your contribution. -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 -workflow with the [rails-dev-box](https://github.com/rails/rails-dev-box). - -As a compromise, test what your code obviously affects, and if the change is -not in railties, run the whole test suite of the affected component. If all -tests are passing, that's enough to propose your contribution. We have -[Travis CI](https://travis-ci.org/rails/rails) as a safety net for catching -unexpected breakages elsewhere. TIP: Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability of Rails will generally not be accepted. -### Follow the Coding Conventions +#### Follow the Coding Conventions Rails follows a simple set of coding style conventions: @@ -284,13 +215,130 @@ Rails follows a simple set of coding style conventions: The above are guidelines - please use your best judgment in using them. +### 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 +workflow with the [rails-dev-box](https://github.com/rails/rails-dev-box). + +As a compromise, test what your code obviously affects, and if the change is +not in railties, run the whole test suite of the affected component. If all +tests are passing, that's enough to propose your contribution. We have +[Travis CI](https://travis-ci.org/rails/rails) as a safety net for catching +unexpected breakages elsewhere. + +#### Entire Rails: + +To run all the tests, do: + +```bash +$ cd rails +$ bundle exec rake test +``` + +#### 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 +``` + +You can now run the tests as you did for `sqlite3`. The tasks are respectively + +```bash +test:mysql +test:mysql2 +test:postgresql +``` + +Finally, + +```bash +$ bundle exec rake test +``` + +will now run the four of them in turn. + +You can also run any single test separately: + +```bash +$ 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. + +### 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. + +If you are sure about what you are doing and would like to have a more clear output, there's a way to override the flag: + +```bash +$ RUBYOPT=-W0 bundle exec rake test +``` + ### Updating the CHANGELOG The CHANGELOG is an important part of every release. It keeps the list of changes for every Rails version. You should add an entry to the CHANGELOG of the framework that you modified if you're adding or removing a feature, committing a bug fix or adding deprecation notices. Refactorings and documentation changes generally should not go to the CHANGELOG. -A CHANGELOG entry should summarize what was changed and should end with author's name and it should go on top of a CHANGELOG. You can use multiple lines if you need more space and you can attach code examples indented with 4 spaces. If a change is related to a specific issue, you should attach issue's number. Here is an example CHANGELOG entry: +A CHANGELOG entry should summarize what was changed and should end with author's name and it should go on top of a CHANGELOG. You can use multiple lines if you need more space and you can attach code examples indented with 4 spaces. If a change is related to a specific issue, you should attach the issue's number. Here is an example CHANGELOG entry: ``` * Summary of a change that briefly describes what was changed. You can use multiple @@ -314,9 +362,9 @@ Your name can be added directly after the last word if you don't provide any cod 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 your work. If you don't know anyone else using Rails, try hopping into the IRC -room or posting about your idea to the rails-core mailing list. Doing this in -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 +room or posting about your idea to the rails-core mailing list. Doing this in +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 @@ -343,9 +391,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 @@ -467,11 +515,11 @@ the same way that you appreciate feedback on your patches. ### Iterate as Necessary -It's entirely possible that the feedback you get will suggest changes. Don't get discouraged: the whole point of contributing to an active open source project is to tap into community knowledge. If people are encouraging you to tweak your code, then it's worth making the tweaks and resubmitting. If the feedback is that your code doesn't belong in the core, you might still think about releasing it as a gem. +It's entirely possible that the feedback you get will suggest changes. Don't get discouraged: the whole point of contributing to an active open source project is to tap into the knowledge of the community. If people are encouraging you to tweak your code, then it's worth making the tweaks and resubmitting. If the feedback is that your code doesn't belong in the core, you might still think about releasing it as a gem. #### Squashing commits -One of the things that we may ask you to do is "squash your commits," which +One of the things that we may ask you to do is to "squash your commits", which will combine all of your commits into a single commit. We prefer pull requests that are a single commit. This makes it easier to backport changes to stable branches, squashing makes it easier to revert bad commits, and the git history @@ -507,7 +555,18 @@ $ 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. -### Backporting +### 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 4-0-stable branch: + +```bash +$ 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. + +#### Backporting Changes that are merged into master are intended for the next major release of Rails. Sometimes, it might be beneficial for your changes to propagate back to the maintenance releases for older stable branches. Generally, security fixes and bug fixes are good candidates for a backport, while new features and patches that introduce a change in behavior will not be accepted. When in doubt, it is best to consult a Rails team member before backporting your changes to avoid wasted effort. diff --git a/guides/source/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md index b067d9efb7..5f738b76af 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,20 @@ 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!!!", - "published"=>false, "created_at"=>nil} -Post should be valid: true - Post Create (0.000443) INSERT INTO "posts" ("updated_at", "title", "body", "published", + "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} 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 +285,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 +308,7 @@ For example: ```bash => Booting WEBrick -=> Rails 4.1.0 application starting in development on http://0.0.0.0:3000 +=> Rails 4.1.1 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 +319,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 +364,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 +385,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 +418,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.1.1/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.1.1/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.1.1/lib/action_controller/metal/rendering.rb:10 ... ``` @@ -438,7 +437,7 @@ context. ``` (byebug) frame 2 -[184, 193] in /PathToGems/actionpack-4.1.0/lib/abstract_controller/base.rb +[184, 193] in /PathToGems/actionpack-4.1.1/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 +486,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 +511,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 +529,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 +563,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 +580,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 +610,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 +633,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 +654,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.1.1/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 +692,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 +715,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..e365435b50 100644 --- a/guides/source/documents.yaml +++ b/guides/source/documents.yaml @@ -162,7 +162,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 bbd63bb892..1321fa3870 100644 --- a/guides/source/engines.md +++ b/guides/source/engines.md @@ -36,14 +36,14 @@ 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 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. @@ -73,13 +73,13 @@ 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, @@ -197,12 +197,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 +253,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 +283,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 +362,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 +382,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. +a 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 +412,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: @@ -471,20 +471,20 @@ called `Blorgh::Comment`. Now run the migration to create our blorgh_comments table: ```bash -$ rake db:migrate +$ bin/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 +494,7 @@ Turning the model into this: ```ruby module Blorgh - class Post < ActiveRecord::Base + class Article < ActiveRecord::Base has_many :comments end end @@ -505,9 +505,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 a 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 +519,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 +529,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 +545,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 +567,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 +590,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 +612,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 +625,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,20 +676,20 @@ 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: ```bash -$ rake blorgh:install:migrations +$ bin/rake blorgh:install:migrations ``` If you have multiple engines that need migrations copied over, use `railties:install:migrations` instead: ```bash -$ rake railties:install:migrations +$ bin/rake railties:install:migrations ``` This command, when run for the first time, will copy over all the migrations @@ -698,7 +698,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 +709,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,11 +734,11 @@ 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 +authors for a 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. @@ -753,14 +753,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 +770,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 +803,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 @@ -822,41 +822,41 @@ This migration will need to be run on the application. To do that, it must first be copied using this command: ```bash -$ rake blorgh:install:migrations +$ bin/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: ```bash -$ rake db:migrate +$ bin/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 +925,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 +960,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 +985,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 @@ -1052,6 +1052,16 @@ This tells the application that you still want to perform a `GET` request to the `index` action of this controller, but you want to use the engine's route to get there, rather than the application's one. +Another way to do this is to assign the `@routes` instance variable to `Engine.routes` in your test setup: + +```ruby +setup do + @routes = Engine.routes +end +``` + +This will also ensure url helpers for the engine will work as expected in your tests. + Improving engine functionality ------------------------------ @@ -1097,12 +1107,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 @@ -1110,20 +1120,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 @@ -1131,9 +1141,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}" @@ -1149,13 +1159,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 @@ -1168,22 +1178,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" @@ -1214,25 +1224,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 %> ``` @@ -1249,30 +1259,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 205e0f6b62..048eb9a6e3 100644 --- a/guides/source/form_helpers.md +++ b/guides/source/form_helpers.md @@ -1,23 +1,22 @@ Form Helpers ============ -Forms in web applications are an essential interface for user input. However, form markup can quickly become tedious to write and maintain because of form control naming and their numerous attributes. Rails does away with these complexities by providing view helpers for generating form markup. However, since they have different use-cases, developers are required to know all the differences between similar helper methods before putting them to use. +Forms in web applications are an essential interface for user input. However, form markup can quickly become tedious to write and maintain because of the need to handle form control naming and its numerous attributes. Rails does away with this complexity by providing view helpers for generating form markup. However, since these helpers have different use cases, developers need to know the differences between the helper methods before putting them to use. After reading this guide, you will know: * How to create search forms and similar kind of generic forms not representing any specific model in your application. -* How to make model-centric forms for creation and editing of specific database records. +* How to make model-centric forms for creating and editing specific database records. * How to generate select boxes from multiple types of data. -* The date and time helpers Rails provides. +* What date and time helpers Rails provides. * What makes a file upload form different. -* Some cases of building forms to external resources. +* How to post forms to external resources and specify setting an `authenticity_token`. * How to build complex forms. -------------------------------------------------------------------------------- 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, 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,14 +429,19 @@ 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. You can add arbitrary attributes to the options using hashes: ```html+erb -<%= options_for_select([['Lisbon', 1, {'data-size' => '2.8 million'}], ['Madrid', 2, {'data-size' => '3.2 million'}]], 2) %> +<%= options_for_select( + [ + ['Lisbon', 1, { 'data-size' => '2.8 million' }], + ['Madrid', 2, { 'data-size' => '3.2 million' }] + ], 2 +) %> output: @@ -484,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] } %> @@ -535,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 %> @@ -549,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) @@ -594,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) %> @@ -624,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 @@ -635,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. @@ -646,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| %> @@ -662,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 @@ -680,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 ------------------------------------------ @@ -819,21 +818,21 @@ As a shortcut you can append [] to the name and omit the `:index` option. This i produces exactly the same output as the previous example. -Forms to external resources +Forms to External Resources --------------------------- -If you need to post some data to an external resource it is still great to build your form using rails form helpers. But sometimes you need to set an `authenticity_token` for this resource. You can do it by passing an `authenticity_token: 'your_external_token'` parameter to the `form_tag` options: +Rails' form helpers can also be used to build a form for posting data to an external resource. However, at times it can be necessary to set an `authenticity_token` for the resource; this can be done by passing an `authenticity_token: 'your_external_token'` parameter to the `form_tag` options: ```erb -<%= form_tag 'http://farfar.away/form', authenticity_token: 'external_token') do %> +<%= form_tag 'http://farfar.away/form', authenticity_token: 'external_token' do %> Form contents <% end %> ``` -Sometimes when you submit data to an external resource, like payment gateway, fields you can use in your form are limited by an external API. So you may want not to generate an `authenticity_token` hidden field at all. For doing this just pass `false` to the `:authenticity_token` option: +Sometimes when submitting data to an external resource, like a payment gateway, the fields that can be used in the form are limited by an external API and it may be undesirable to generate an `authenticity_token`. To not send a token, simply pass `false` to the `:authenticity_token` option: ```erb -<%= form_tag 'http://farfar.away/form', authenticity_token: false) do %> +<%= form_tag 'http://farfar.away/form', authenticity_token: false do %> Form contents <% end %> ``` @@ -857,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 @@ -1008,4 +1007,4 @@ As a convenience you can instead pass the symbol `:all_blank` which will create ### Adding Fields on the Fly -Rather than rendering multiple sets of fields ahead of time you may wish to add them only when a user clicks on an 'Add new address' button. Rails does not provide any builtin support for this. When generating new sets of fields you must ensure the key of the associated array is unique - the current JavaScript date (milliseconds after the epoch) is a common choice. +Rather than rendering multiple sets of fields ahead of time you may wish to add them only when a user clicks on an 'Add new address' button. Rails does not provide any built-in support for this. When generating new sets of fields you must ensure the key of the associated array is unique - the current JavaScript date (milliseconds after the epoch) is a common choice. diff --git a/guides/source/generators.md b/guides/source/generators.md index 4a5377c206..25c67de993 100644 --- a/guides/source/generators.md +++ b/guides/source/generators.md @@ -23,13 +23,13 @@ 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 @@ -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 @@ -224,7 +224,7 @@ If we generate another resource with the scaffold generator, we can see that sty 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 bafb75c668..530232f3f3 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -89,9 +89,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 @@ -99,7 +99,7 @@ ruby 2.0.0p353 ``` If you don't have Ruby installed have a look at -[ruby-lang.org](https://www.ruby-lang.org/en/downloads/) for possible ways to +[ruby-lang.org](https://www.ruby-lang.org/en/installation/) for possible ways to install Ruby on your platform. Many popular UNIX-like OSes ship with an acceptable version of SQLite3. Windows @@ -122,10 +122,10 @@ To verify that you have everything installed correctly, you should be able to run the following: ```bash -$ rails --version +$ bin/rails --version ``` -If it says something like "Rails 4.1.0", you are ready to continue. +If it says something like "Rails 4.1.1", you are ready to continue. ### Creating the Blog Application @@ -190,7 +190,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 +243,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 +267,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 +359,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 +397,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` @@ -427,19 +428,22 @@ are generated in Rails they are empty by default, unless you tell it your wanted actions during the generation process. To manually define an action inside a controller, all you need to do is to -define a new method inside the controller. -Open `app/controllers/articles_controller.rb` and inside the `ArticlesController` -class, define a `new` method like this: +define a new method inside the controller. Open +`app/controllers/articles_controller.rb` and inside the `ArticlesController` +class, define a `new` method so that the controller now looks like this: ```ruby -def new +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) +![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 @@ -552,7 +556,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 @@ -565,18 +569,18 @@ 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 article, so go ahead and do that. When you submit the form, you should see a familiar error: -![Unknown action create for ArticlesController](images/getting_started/unknown_action_create_for_articles.png) +![Unknown action create for ArticlesController] +(images/getting_started/unknown_action_create_for_articles.png) You now need to create the `create` action within the `ArticlesController` for this to work. @@ -585,7 +589,7 @@ this to work. To make the "Unknown action" go away, you can define a `create` action within the `ArticlesController` class in `app/controllers/articles_controller.rb`, -underneath the `new` action: +underneath the `new` action, as shown: ```ruby class ArticlesController < ApplicationController @@ -612,7 +616,7 @@ def create end ``` -The `render` method here is taking a very simple hash with a key of `text` and +The `render` method here is taking a very simple hash with a key of `plain` and value of `params[:article].inspect`. The `params` method is the object which represents the parameters (or fields) coming in from the form. The `params` method returns an `ActiveSupport::HashWithIndifferentAccess` object, which @@ -632,13 +636,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 @@ -646,26 +650,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: @@ -690,13 +691,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 @@ -733,49 +734,47 @@ 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: 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) +![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: +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 @@ -792,13 +791,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: @@ -813,10 +813,24 @@ parameter, which in our case will be the id of the article. 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. + +Given that, let's add the `show` action, as follows: + ```ruby -def show - @article = Article.find(params[:id]) -end +class ArticlesController < ApplicationController + def show + @article = Article.find(params[:id]) + end + + def new + end + + # snipped for brevity ``` A couple of things to note. We use `Article.find` to find the article we're @@ -855,15 +869,27 @@ articles GET /articles(.:format) articles#index ``` Add the corresponding `index` action for that route inside the -`ArticlesController` in the `app/controllers/articles_controller.rb` file: +`ArticlesController` in the `app/controllers/articles_controller.rb` file. +When we write an `index` action, the usual practice is to place it as the +first method in the controller. Let's do it: ```ruby -def index - @articles = Article.all -end +class ArticlesController < ApplicationController + def index + @articles = Article.all + end + + def show + @article = Article.find(params[:id]) + end + + def new + end + + # snipped for brevity ``` -And then finally, add view for this action, located at +And then finally, add the view for this action, located at `app/views/articles/index.html.erb`: ```html+erb @@ -913,8 +939,8 @@ Let's add links to the other views as well, starting with adding this This link will allow you to bring up the form that lets you create a new article. -Also add a link in `app/views/articles/new.html.erb`, underneath the form, to -go back to the `index` action: +Now, add another link in `app/views/articles/new.html.erb`, underneath the +form, to go back to the `index` action: ```erb <%= form_for :article, url: articles_path do |f| %> @@ -924,7 +950,7 @@ go back to the `index` action: <%= link_to 'Back', articles_path %> ``` -Finally, add another link to the `app/views/articles/show.html.erb` template to +Finally, add a link to the `app/views/articles/show.html.erb` template to go back to the `index` action as well, so that people who are viewing a single article can go back and view the whole list again: @@ -942,9 +968,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 @@ -1028,17 +1054,21 @@ something went wrong. To do that, you'll modify ```html+erb <%= form_for :article, url: articles_path do |f| %> + <% if @article.errors.any? %> - <div id="error_explanation"> - <h2><%= pluralize(@article.errors.count, "error") %> prohibited - this article from being saved:</h2> - <ul> - <% @article.errors.full_messages.each do |msg| %> - <li><%= msg %></li> - <% end %> - </ul> - </div> + <div id="error_explanation"> + <h2> + <%= pluralize(@article.errors.count, "error") %> prohibited + this article from being saved: + </h2> + <ul> + <% @article.errors.full_messages.each do |msg| %> + <li><%= msg %></li> + <% end %> + </ul> + </div> <% end %> + <p> <%= f.label :title %><br> <%= f.text_field :title %> @@ -1052,6 +1082,7 @@ something went wrong. To do that, you'll modify <p> <%= f.submit %> </p> + <% end %> <%= link_to 'Back', articles_path %> @@ -1084,12 +1115,27 @@ you attempt to do just that on the new article form We've covered the "CR" part of CRUD. Now let's focus on the "U" part, updating articles. -The first step we'll take is adding an `edit` action to the `ArticlesController`. +The first step we'll take is adding an `edit` action to the `ArticlesController`, +generally between the `new` and `create` actions, as shown: ```ruby +def new + @article = Article.new +end + def edit @article = Article.find(params[:id]) end + +def create + @article = Article.new(article_params) + + if @article.save + redirect_to @article + else + render 'new' + end +end ``` The view will contain a form similar to the one we used when creating @@ -1100,17 +1146,21 @@ it look as follows: <h1>Editing article</h1> <%= form_for :article, url: article_path(@article), method: :patch do |f| %> + <% if @article.errors.any? %> - <div id="error_explanation"> - <h2><%= pluralize(@article.errors.count, "error") %> prohibited - this article from being saved:</h2> - <ul> - <% @article.errors.full_messages.each do |msg| %> - <li><%= msg %></li> - <% end %> - </ul> - </div> + <div id="error_explanation"> + <h2> + <%= pluralize(@article.errors.count, "error") %> prohibited + this article from being saved: + </h2> + <ul> + <% @article.errors.full_messages.each do |msg| %> + <li><%= msg %></li> + <% end %> + </ul> + </div> <% end %> + <p> <%= f.label :title %><br> <%= f.text_field :title %> @@ -1124,6 +1174,7 @@ it look as follows: <p> <%= f.submit %> </p> + <% end %> <%= link_to 'Back', articles_path %> @@ -1136,16 +1187,28 @@ The `method: :patch` option tells Rails that we want this form to be submitted via the `PATCH` HTTP method which is the HTTP method you're expected to use to **update** resources according to the REST protocol. -The first parameter of the `form_tag` can be an object, say, `@article` which would +The first parameter of `form_for` can be an object, say, `@article` which would cause the helper to fill in the form with the fields of the object. Passing in a -symbol (`:article`) with the same name as the instance variable (`@article`) also -automagically leads to the same behavior. This is what is happening here. More details -can be found in [form_for documentation](http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for). +symbol (`:article`) with the same name as the instance variable (`@article`) +also automagically leads to the same behavior. This is what is happening here. +More details can be found in [form_for documentation] +(http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for). -Next we need to create the `update` action in -`app/controllers/articles_controller.rb`: +Next, we need to create the `update` action in +`app/controllers/articles_controller.rb`. +Add it between the `create` action and the `private` method: ```ruby +def create + @article = Article.new(article_params) + + if @article.save + redirect_to @article + else + render 'new' + end +end + def update @article = Article.find(params[:id]) @@ -1187,14 +1250,14 @@ it appear next to the "Show" link: <th colspan="2"></th> </tr> -<% @articles.each do |article| %> - <tr> - <td><%= article.title %></td> - <td><%= article.text %></td> - <td><%= link_to 'Show', article_path(article) %></td> - <td><%= link_to 'Edit', edit_article_path(article) %></td> - </tr> -<% end %> + <% @articles.each do |article| %> + <tr> + <td><%= article.title %></td> + <td><%= article.text %></td> + <td><%= link_to 'Show', article_path(article) %></td> + <td><%= link_to 'Edit', edit_article_path(article) %></td> + </tr> + <% end %> </table> ``` @@ -1205,8 +1268,8 @@ bottom of the template: ```html+erb ... -<%= link_to 'Back', articles_path %> -| <%= link_to 'Edit', edit_article_path(@article) %> +<%= link_to 'Back', articles_path %> | +<%= link_to 'Edit', edit_article_path(@article) %> ``` And here's how our app looks so far: @@ -1215,10 +1278,10 @@ And here's how our app looks so far: ### Using partials to clean up duplication in views -Our `edit` page looks very similar to the `new` page, in fact they -both share the same code for displaying the form. Let's remove some duplication -by using a view partial. By convention, partial files are prefixed by an -underscore. +Our `edit` page looks very similar to the `new` page; in fact, they +both share the same code for displaying the form. Let's remove this +duplication by using a view partial. By convention, partial files are +prefixed by an underscore. TIP: You can read more about partials in the [Layouts and Rendering in Rails](layouts_and_rendering.html) guide. @@ -1228,17 +1291,21 @@ content: ```html+erb <%= form_for @article do |f| %> + <% if @article.errors.any? %> - <div id="error_explanation"> - <h2><%= pluralize(@article.errors.count, "error") %> prohibited - this article from being saved:</h2> - <ul> - <% @article.errors.full_messages.each do |msg| %> - <li><%= msg %></li> - <% end %> - </ul> - </div> + <div id="error_explanation"> + <h2> + <%= pluralize(@article.errors.count, "error") %> prohibited + this article from being saved: + </h2> + <ul> + <% @article.errors.full_messages.each do |msg| %> + <li><%= msg %></li> + <% end %> + </ul> + </div> <% end %> + <p> <%= f.label :title %><br> <%= f.text_field :title %> @@ -1252,6 +1319,7 @@ content: <p> <%= f.submit %> </p> + <% end %> ``` @@ -1260,8 +1328,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: @@ -1302,9 +1370,11 @@ people to craft malicious URLs like this: <a href='http://example.com/articles/1/destroy'>look at this cat!</a> ``` -We use the `delete` method for destroying resources, and this route is mapped to -the `destroy` action inside `app/controllers/articles_controller.rb`, which -doesn't exist yet, but is provided below: +We use the `delete` method for destroying resources, and this route is mapped +to the `destroy` action inside `app/controllers/articles_controller.rb`, which +doesn't exist yet. The `destroy` method is generally the last CRUD action in +the controller, and like the other public CRUD actions, it must be placed +before any `private` or `protected` methods. Let's add it: ```ruby def destroy @@ -1315,13 +1385,67 @@ def destroy end ``` +The complete `ArticlesController` in the +`app/controllers/articles_controller.rb` file should now look like this: + +```ruby +class ArticlesController < ApplicationController + def index + @articles = Article.all + end + + def show + @article = Article.find(params[:id]) + end + + def new + @article = Article.new + end + + def edit + @article = Article.find(params[:id]) + end + + def create + @article = Article.new(article_params) + + if @article.save + redirect_to @article + else + render 'new' + end + end + + def update + @article = Article.find(params[:id]) + + if @article.update(article_params) + redirect_to @article + else + render 'edit' + end + end + + def destroy + @article = Article.find(params[:id]) + @article.destroy + + redirect_to articles_path + end + + private + def article_params + params.require(:article).permit(:title, :text) + end +end +``` + You can call `destroy` on Active Record objects when you want to delete 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> @@ -1333,16 +1457,17 @@ together. <th colspan="3"></th> </tr> -<% @articles.each do |article| %> - <tr> - <td><%= article.title %></td> - <td><%= article.text %></td> - <td><%= link_to 'Show', article_path(article) %></td> - <td><%= link_to 'Edit', edit_article_path(article) %></td> - <td><%= link_to 'Destroy', article_path(article), - method: :delete, data: { confirm: 'Are you sure?' } %></td> - </tr> -<% end %> + <% @articles.each do |article| %> + <tr> + <td><%= article.title %></td> + <td><%= article.text %></td> + <td><%= link_to 'Show', article_path(article) %></td> + <td><%= link_to 'Edit', edit_article_path(article) %></td> + <td><%= link_to 'Destroy', article_path(article), + method: :delete, + data: { confirm: 'Are you sure?' } %></td> + </tr> + <% end %> </table> ``` @@ -1360,9 +1485,8 @@ Without this file, the confirmation dialog box wouldn't appear. Congratulations, you can now create, show, list, update and destroy articles. -TIP: In general, Rails encourages the use of resources objects in place -of declaring routes manually. -For more information about routing, see +TIP: In general, Rails encourages using resources objects instead of +declaring routes manually. For more information about routing, see [Rails Routing from the Outside In](routing.html). Adding a Second Model @@ -1378,7 +1502,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: @@ -1426,7 +1550,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 @@ -1502,7 +1626,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: @@ -1552,8 +1676,8 @@ So first, we'll wire up the Article show template </p> <% end %> -<%= link_to 'Back', articles_path %> -| <%= link_to 'Edit', edit_article_path(@article) %> +<%= link_to 'Back', articles_path %> | +<%= link_to 'Edit', edit_article_path(@article) %> ``` This adds a form on the `Article` show page that creates a new comment by @@ -1823,8 +1947,8 @@ database and send us back to the show action for the article. ### Deleting Associated Objects -If you delete an article then its associated comments will also need to be -deleted. Otherwise they would simply occupy space in the database. Rails allows +If you delete an article, its associated comments will also need to be +deleted, otherwise they would simply occupy space in the database. Rails allows you to use the `dependent` option of an association to achieve this. Modify the Article model, `app/models/article.rb`, as follows: @@ -1841,21 +1965,21 @@ Security ### Basic Authentication -If you were to publish your blog online, anybody would be able to add, edit and +If you were to publish your blog online, anyone would be able to add, edit and delete articles or delete comments. Rails provides a very simple HTTP authentication system that will work nicely in this situation. -In the `ArticlesController` we need to have a way to block access to the various -actions if the person is not authenticated, here we can use the Rails -`http_basic_authenticate_with` method, allowing access to the requested +In the `ArticlesController` we need to have a way to block access to the +various actions if the person is not authenticated. Here we can use the Rails +`http_basic_authenticate_with` method, which allows access to the requested action if that method allows it. To use the authentication system, we specify it at the top of our -`ArticlesController`, in this case, we want the user to be authenticated on -every action, except for `index` and `show`, so we write that in -`app/controllers/articles_controller.rb`: +`ArticlesController` in `app/controllers/articles_controller.rb`. In our case, +we want the user to be authenticated on every action except `index` and `show`, +so we write that: ```ruby class ArticlesController < ApplicationController @@ -1879,7 +2003,7 @@ class CommentsController < ApplicationController def create @article = Article.find(params[:article_id]) - ... + # ... end # snipped for brevity diff --git a/guides/source/i18n.md b/guides/source/i18n.md index 466ffe7907..0eba3af6e8 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -92,7 +92,7 @@ Rails adds all `.rb` and `.yml` files from the `config/locales` directory to you The default `en.yml` locale in this directory contains a sample pair of translation strings: -```ruby +```yaml en: hello: "Hello world" ``` @@ -179,7 +179,7 @@ end # in your /etc/hosts file to try this out locally def extract_locale_from_tld parsed_locale = request.host.split('.').last - I18n.available_locales.include?(parsed_locale.to_sym) ? parsed_locale : nil + I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil end ``` @@ -192,7 +192,7 @@ We can also set the locale from the _subdomain_ in a very similar way: # in your /etc/hosts file to try this out locally def extract_locale_from_subdomain parsed_locale = request.subdomains.first - I18n.available_locales.include?(parsed_locale.to_sym) ? parsed_locale : nil + I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil end ``` @@ -369,7 +369,7 @@ NOTE: Rails adds a `t` (`translate`) helper method to your views so that you do So let's add the missing translations into the dictionary files (i.e. do the "localization" part): -```ruby +```yaml # config/locales/en.yml en: hello_world: Hello world! @@ -421,7 +421,7 @@ OK! Now let's add a timestamp to the view, so we can demo the **date/time locali And in our pirate translations file let's add a time format (it's already there in Rails' defaults for English): -```ruby +```yaml # config/locales/pirate.yml pirate: time: @@ -680,62 +680,13 @@ NOTE: Automatic conversion to HTML safe translate text is only available from th ![i18n demo html safe](images/i18n/demo_html_safe.png) -How to Store your Custom Translations -------------------------------------- - -The Simple backend shipped with Active Support allows you to store translations in both plain Ruby and YAML format.[^2] - -For example a Ruby Hash providing translations can look like this: - -```ruby -{ - pt: { - foo: { - bar: "baz" - } - } -} -``` - -The equivalent YAML file would look like this: - -```ruby -pt: - foo: - bar: baz -``` - -As you see, in both cases the top level key is the locale. `:foo` is a namespace key and `:bar` is the key for the translation "baz". - -Here is a "real" example from the Active Support `en.yml` translations YAML file: - -```ruby -en: - date: - formats: - default: "%Y-%m-%d" - short: "%b %d" - long: "%B %d, %Y" -``` - -So, all of the following equivalent lookups will return the `:short` date format `"%b %d"`: - -```ruby -I18n.t 'date.formats.short' -I18n.t 'formats.short', scope: :date -I18n.t :short, scope: 'date.formats' -I18n.t :short, scope: [:date, :formats] -``` - -Generally we recommend using YAML as a format for storing translations. There are cases, though, where you want to store Ruby lambdas as part of your locale data, e.g. for special date formats. - ### Translations for Active Record Models You can use the methods `Model.model_name.human` and `Model.human_attribute_name(attribute)` to transparently look up translations for your model and attribute names. For example when you add the following translations: -```ruby +```yaml en: activerecord: models: @@ -750,7 +701,7 @@ Then `User.model_name.human` will return "Dude" and `User.human_attribute_name(" You can also set a plural form for model names, adding as following: -```ruby +```yaml en: activerecord: models: @@ -761,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. @@ -920,6 +884,55 @@ Rails uses fixed strings and other localizations, such as format strings and oth * `Array#to_sentence` uses format settings as given in the [support.array](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L33) scope. +How to Store your Custom Translations +------------------------------------- + +The Simple backend shipped with Active Support allows you to store translations in both plain Ruby and YAML format.[^2] + +For example a Ruby Hash providing translations can look like this: + +```yaml +{ + pt: { + foo: { + bar: "baz" + } + } +} +``` + +The equivalent YAML file would look like this: + +```yaml +pt: + foo: + bar: baz +``` + +As you see, in both cases the top level key is the locale. `:foo` is a namespace key and `:bar` is the key for the translation "baz". + +Here is a "real" example from the Active Support `en.yml` translations YAML file: + +```yaml +en: + date: + formats: + default: "%Y-%m-%d" + short: "%b %d" + long: "%B %d, %Y" +``` + +So, all of the following equivalent lookups will return the `:short` date format `"%b %d"`: + +```ruby +I18n.t 'date.formats.short' +I18n.t 'formats.short', scope: :date +I18n.t :short, scope: 'date.formats' +I18n.t :short, scope: [:date, :formats] +``` + +Generally we recommend using YAML as a format for storing translations. There are cases, though, where you want to store Ruby lambdas as part of your locale data, e.g. for special date formats. + Customize your I18n Setup ------------------------- 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 77f3615ca0..00b2761716 100644 --- a/guides/source/initialization.md +++ b/guides/source/initialization.md @@ -587,7 +587,7 @@ def run_initializers(group=:default, *args) end ``` -The run_initializers code itself is tricky. What Rails is doing here is +The `run_initializers` code itself is tricky. What Rails is doing here is traversing all the class ancestors looking for those that respond to an `initializers` method. It then sorts the ancestors by name, and runs them. For example, the `Engine` class will make all the engines available by @@ -642,7 +642,7 @@ def build_app(app) end ``` -Remember, `build_app` was called (by wrapped_app) in the last line of `Server#start`. +Remember, `build_app` was called (by `wrapped_app`) in the last line of `Server#start`. Here's how it looked like when we left: ```ruby 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..5b75540c05 100644 --- a/guides/source/layouts_and_rendering.md +++ b/guides/source/layouts_and_rendering.md @@ -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/migrations.md b/guides/source/migrations.md index bfee55a95d..6742c05946 100644 --- a/guides/source/migrations.md +++ b/guides/source/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 @@ -307,7 +307,7 @@ braces. You can use the following modifiers: 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 @@ -495,6 +495,7 @@ class ExampleMigration < ActiveRecord::Migration add_column :users, :home_page_url, :string rename_column :users, :email, :email_address end +end ``` Using `reversible` will ensure that the instructions are executed in the @@ -652,7 +653,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 @@ -669,7 +670,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` @@ -677,7 +678,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. @@ -687,7 +688,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 @@ -717,7 +718,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 @@ -733,7 +734,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 @@ -901,6 +902,11 @@ 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 --------------------------------------- diff --git a/guides/source/nested_model_forms.md b/guides/source/nested_model_forms.md index 855fab18e3..4f0634d955 100644 --- a/guides/source/nested_model_forms.md +++ b/guides/source/nested_model_forms.md @@ -17,9 +17,9 @@ Model setup To be able to use the nested model functionality in your forms, the model will need to support some basic operations. -First of all, it needs to define a writer method for the attribute that corresponds to the association you are building a nested model form for. The `fields_for` form helper will look for this method to decide whether or not a nested model form should be build. +First of all, it needs to define a writer method for the attribute that corresponds to the association you are building a nested model form for. The `fields_for` form helper will look for this method to decide whether or not a nested model form should be built. -If the associated object is an array a form builder will be yielded for each object, else only a single form builder will be yielded. +If the associated object is an array, a form builder will be yielded for each object, else only a single form builder will be yielded. Consider a Person model with an associated Address. When asked to yield a nested FormBuilder for the `:address` attribute, the `fields_for` form helper will look for a method on the Person instance named `address_attributes=`. @@ -220,6 +220,6 @@ As you can see it has generated 2 `project name` inputs, one for each new `proje You can basically see the `projects_attributes` hash as an array of attribute hashes, one for each model instance. -NOTE: The reason that `fields_for` constructed a form which would result in a hash instead of an array is that it won't work for any forms nested deeper than one level deep. +NOTE: The reason that `fields_for` constructed a hash instead of an array is that it won't work for any form nested deeper than one level deep. TIP: You _can_ however pass an array to the writer method generated by `accepts_nested_attributes_for` if you're using plain Ruby or some other API access. See (TODO) for more info and example. 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..9053f31b8e 100644 --- a/guides/source/rails_on_rack.md +++ b/guides/source/rails_on_rack.md @@ -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 f6b1bc1509..574a1f9c68 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', 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] @@ -1091,7 +1091,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 15b28664b7..75d8c8e4c8 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 <a href="#additional-resources">Additional Resources</a> chapter). It is done manually because that's how you find the nasty logical security problems. Sessions -------- @@ -60,7 +60,7 @@ Many web applications have an authentication system: a user provides a user name Hence, the cookie serves as temporary authentication for the web application. Anyone who seizes a cookie from someone else, may use the web application as this user - with possibly severe consequences. Here are some ways to hijack a session, and their countermeasures: -* Sniff the cookie in an insecure network. A wireless LAN can be an example of such a network. In an unencrypted wireless LAN it is especially easy to listen to the traffic of all connected clients. This is one more reason not to work from a coffee shop. For the web application builder this means to _provide a secure connection over SSL_. In Rails 3.1 and later, this could be accomplished by always forcing SSL connection in your application config file: +* Sniff the cookie in an insecure network. A wireless LAN can be an example of such a network. In an unencrypted wireless LAN it is especially easy to listen to the traffic of all connected clients. For the web application builder this means to _provide a secure connection over SSL_. In Rails 3.1 and later, this could be accomplished by always forcing SSL connection in your application config file: ```ruby config.force_ssl = true @@ -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. @@ -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 @@ -239,24 +239,23 @@ Or the attacker places the code into the onmouseover event handler of an image: There are many other possibilities, like using a `<script>` tag to make a cross-site request to a URL with a JSONP or JavaScript response. The response is executable code that the attacker can find a way to run, possibly extracting sensitive data. To protect against this data leakage, we disallow cross-site `<script>` tags. Only Ajax requests may have JavaScript responses since XmlHttpRequest is subject to the browser Same-Origin policy - meaning only your site can initiate the request. -To protect against all other forged requests, we introduce a _required security token_ that our site knows but other sites don't know. We include the security token in requests and verify it on the server. This is a one-liner in your application controller: +To protect against all other forged requests, we introduce a _required security token_ that our site knows but other sites don't know. We include the security token in requests and verify it on the server. This is a one-liner in your application controller, and is the default for newly created rails applications: ```ruby -protect_from_forgery +protect_from_forgery with: :exception ``` -This will automatically include a security token in all forms and Ajax requests generated by Rails. If the security token doesn't match what was expected, the session will be reset. +This will automatically include a security token in all forms and Ajax requests generated by Rails. If the security token doesn't match what was expected, an exception will be thrown. It is common to use persistent cookies to store user information, with `cookies.permanent` for example. In this case, the cookies will not be cleared and the out of the box CSRF protection will not be effective. If you are using a different cookie store than the session for this information, you must handle what to do with it yourself: ```ruby -def handle_unverified_request - super - sign_out_user # Example method that will destroy the user cookies. +rescue_from ActionController::InvalidAuthenticityToken do |exception| + sign_out_user # Example method that will destroy the user cookies end ``` -The above method can be placed in the `ApplicationController` and will be called when a CSRF token is not present on a non-GET request. +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. @@ -375,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. @@ -407,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 @@ -733,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) @@ -813,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 aa37115d14..4149146c4c 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -134,27 +134,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) 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,10 +170,10 @@ 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. @@ -220,7 +220,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 +231,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 +243,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 +270,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 +280,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 +295,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 +320,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 +345,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 @@ -422,26 +422,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 +457,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 +576,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 +653,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 +699,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 +710,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 +725,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 @@ -796,7 +791,7 @@ when you initiate a Rails project. Brief Note About `MiniTest` ----------------------------- -Ruby ships with a boat load of libraries. Ruby 1.8 provides `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 +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. @@ -807,43 +802,43 @@ For more information on `MiniTest`, refer to [Minitest](http://www.ruby-doc.org/ 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 +850,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 +889,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 ``` @@ -1011,7 +1006,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 diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index da161f84c9..eab5779533 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -82,10 +82,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. @@ -393,6 +393,14 @@ 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. + Upgrading from Rails 3.2 to Rails 4.0 ------------------------------------- @@ -480,7 +488,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 @@ -883,7 +891,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 8d22f8bc48..8a41f160fa 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,10 +1,30 @@ -* Move configuration of asset precompile list and version to an initializer. +* Replace double quotes with single quotes while adding an entry into Gemfile. - *Matthew Draper* + *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. -* Do not set the Rails environment to test by default when using test_unit Railtie. + Fixes #9589. - *Konstantin Shabanov* + *Arun Agrawal*, *Abd ar-Rahman Hamidi*, *Roman Shmatov* + +* Fix `console` and `generators` blocks defined at different environments. + + Fixes #14748. + + *Rafael Mendonça França* + +* Move configuration of asset precompile list and version to an initializer. + + *Matthew Draper* * Remove sqlite3 lines from `.gitignore` if the application is not using sqlite3. diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 8d080feb04..362713eb75 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -231,6 +231,18 @@ module Rails self.class.runner(&blk) end + # Sends any console called in the instance of a new application up + # to the +console+ method defined in Rails::Railtie. + def console(&blk) + self.class.console(&blk) + end + + # Sends any generators called in the instance of a new application up + # to the +generators+ method defined in Rails::Railtie. + def generators(&blk) + self.class.generators(&blk) + end + # Sends the +isolate_namespace+ method up to the class method. def isolate_namespace(mod) self.class.isolate_namespace(mod) @@ -352,6 +364,10 @@ module Rails end end + def migration_railties # :nodoc: + (ordered_railties & railties_without_main_app).reverse + end + protected alias :build_middleware_stack :app @@ -382,6 +398,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/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/generators.rb b/railties/lib/rails/generators.rb index dce734b54e..04ce38f841 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -156,7 +156,8 @@ module Rails args << "--help" if args.empty? && klass.arguments.any? { |a| a.required? } klass.start(args, config) else - puts "Could not find generator #{namespace}." + puts "Could not find generator '#{namespace}'. Please choose a generator below." + print_generators end end @@ -199,17 +200,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,6 +212,20 @@ module Rails puts "Please choose a generator below." puts + print_generators + end + + def self.print_generators + 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 # Print Rails defaults first. rails = groups.delete("rails") rails.map! { |n| n.sub(/^rails:/, '') } 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/app_base.rb b/railties/lib/rails/generators/app_base.rb index c066f748ee..569afe8104 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -79,7 +79,6 @@ module Rails end def initialize(*args) - @original_wd = Dir.pwd @gem_filter = lambda { |gem| true } @extra_entries = [] super @@ -203,7 +202,8 @@ module Rails [GemfileEntry.path('rails', Rails::Generators::RAILS_DEV_PATH), GemfileEntry.github('arel', 'rails/arel')] elsif options.edge? - [GemfileEntry.github('rails', 'rails/rails')] + [GemfileEntry.github('rails', 'rails/rails'), + GemfileEntry.github('arel', 'rails/arel')] else [GemfileEntry.version('rails', Rails::VERSION::STRING, 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/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/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/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/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/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/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/test/application/assets_test.rb b/railties/test/application/assets_test.rb index 51f55a560f..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 @@ -450,8 +446,9 @@ module ApplicationTests test "asset urls should be protocol-relative if no request is in scope" do app_file "app/assets/images/rails.png", "notreallyapng" app_file "app/assets/javascripts/image_loader.js.erb", "var src='<%= image_path('rails.png') %>';" - add_to_config "config.assets.precompile = %w{image_loader.js}" + 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{app.js}" + 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 b11fd55170..e95c3fa20d 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -360,7 +360,7 @@ 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 @@ -489,7 +489,7 @@ module ApplicationTests test "valid timezone is setup correctly" do add_to_config <<-RUBY config.root = "#{app_path}" - config.time_zone = "Wellington" + config.time_zone = "Wellington" RUBY require "#{app_path}/config/environment" @@ -500,7 +500,7 @@ module ApplicationTests test "raises when an invalid timezone is defined in the config" do add_to_config <<-RUBY config.root = "#{app_path}" - config.time_zone = "That big hill over yonder hill" + config.time_zone = "That big hill over yonder hill" RUBY assert_raise(ArgumentError) do @@ -511,7 +511,7 @@ module ApplicationTests test "valid beginning of week is setup correctly" do add_to_config <<-RUBY config.root = "#{app_path}" - config.beginning_of_week = :wednesday + config.beginning_of_week = :wednesday RUBY require "#{app_path}/config/environment" @@ -522,7 +522,7 @@ module ApplicationTests test "raises when an invalid beginning of week is defined in the config" do add_to_config <<-RUBY config.root = "#{app_path}" - config.beginning_of_week = :invalid + config.beginning_of_week = :invalid RUBY assert_raise(ArgumentError) do @@ -803,5 +803,97 @@ module ApplicationTests assert_not_nil SourceAnnotationExtractor::Annotation.extensions[/\.(coffee)$/] end + + test "rake_tasks block works at instance level" do + $ran_block = false + + app_file "config/environments/development.rb", <<-RUBY + Rails.application.configure do + rake_tasks do + $ran_block = true + end + end + RUBY + + require "#{app_path}/config/environment" + + assert !$ran_block + require 'rake' + require 'rake/testtask' + require 'rdoc/task' + + Rails.application.load_tasks + assert $ran_block + end + + test "generators block works at instance level" do + $ran_block = false + + app_file "config/environments/development.rb", <<-RUBY + Rails.application.configure do + generators do + $ran_block = true + end + end + RUBY + + require "#{app_path}/config/environment" + + assert !$ran_block + Rails.application.load_generators + assert $ran_block + end + + test "console block works at instance level" do + $ran_block = false + + app_file "config/environments/development.rb", <<-RUBY + Rails.application.configure do + console do + $ran_block = true + end + end + RUBY + + require "#{app_path}/config/environment" + + assert !$ran_block + Rails.application.load_console + assert $ran_block + end + + test "runner block works at instance level" do + $ran_block = false + + app_file "config/environments/development.rb", <<-RUBY + Rails.application.configure do + runner do + $ran_block = true + end + end + RUBY + + require "#{app_path}/config/environment" + + assert !$ran_block + 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 end end diff --git a/railties/test/application/multiple_applications_test.rb b/railties/test/application/multiple_applications_test.rb index 42b319178d..f8d8a673ae 100644 --- a/railties/test/application/multiple_applications_test.rb +++ b/railties/test/application/multiple_applications_test.rb @@ -122,6 +122,26 @@ module ApplicationTests assert_equal 3, $run_count, "There should have been three initializers that incremented the count" end + def test_consoles_run_on_different_applications_go_to_the_same_class + $run_count = 0 + AppTemplate::Application.console { $run_count += 1 } + AppTemplate::Application.new.console { $run_count += 1 } + + assert_equal 0, $run_count, "Without loading the consoles, the count should be 0" + Rails.application.load_console + assert_equal 2, $run_count, "There should have been two consoles that increment the count" + end + + def test_generators_run_on_different_applications_go_to_the_same_class + $run_count = 0 + AppTemplate::Application.generators { $run_count += 1 } + AppTemplate::Application.new.generators { $run_count += 1 } + + assert_equal 0, $run_count, "Without loading the generators, the count should be 0" + Rails.application.load_generators + assert_equal 2, $run_count, "There should have been two generators that increment the count" + end + def test_runners_run_on_different_applications_go_to_the_same_class $run_count = 0 AppTemplate::Application.runner { $run_count += 1 } diff --git a/railties/test/application/rake/migrations_test.rb b/railties/test/application/rake/migrations_test.rb index b7fd5d02c5..a6900a57c4 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 diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb index 0db40c1d32..6d6de0fb52 100644 --- a/railties/test/generators/actions_test.rb +++ b/railties/test/generators/actions_test.rb @@ -41,13 +41,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 +55,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 +66,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 +75,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 +97,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 diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 007dd886da..1cbbf62459 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -119,7 +119,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 +134,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!/ 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/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/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index 853af80111..7180efee41 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -312,7 +312,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 +323,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 +371,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 +416,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_test.rb b/railties/test/generators_test.rb index eac28badfe..8d6dbf80c2 100644 --- a/railties/test/generators_test.rb +++ b/railties/test/generators_test.rb @@ -21,8 +21,10 @@ 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 "scaffold", output end def test_help_when_a_generator_with_required_arguments_is_invoked_without_arguments 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 |