diff options
536 files changed, 10421 insertions, 3530 deletions
diff --git a/.travis.yml b/.travis.yml index c8afb46187..3ddaf86fb2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,9 +5,9 @@ before_install: rvm: - 1.9.3 - 2.0.0 - - 2.1.0-preview2 - - rbx-2.2.1 - - jruby-19mode + - 2.1.0 + - rbx + - jruby env: - "GEM=railties" - "GEM=ap,am,amo,as,av" @@ -17,8 +17,9 @@ env: - "GEM=ar:postgresql" matrix: allow_failures: - - rvm: rbx-2.2.1 - - rvm: jruby-19mode + - rvm: rbx + - rvm: jruby + fast_finish: true notifications: email: false irc: @@ -32,3 +33,6 @@ notifications: rooms: - secure: "YA1alef1ESHWGFNVwvmVGCkMe4cUy4j+UcNvMUESraceiAfVyRMAovlQBGs6\n9kBRm7DHYBUXYC2ABQoJbQRLDr/1B5JPf/M8+Qd7BKu8tcDC03U01SMHFLpO\naOs/HLXcDxtnnpL07tGVsm0zhMc5N8tq4/L3SHxK7Vi+TacwQzI=" bundler_args: --path vendor/bundle --without test +services: + - memcached + @@ -18,7 +18,7 @@ gem 'coffee-rails', '~> 4.0.0' gem 'uglifier', '>= 1.3.0', require: false group :doc do - gem 'sdoc' + gem 'sdoc', '~> 0.4.0' gem 'redcarpet', '~> 2.2.2', platforms: :ruby gem 'w3c_validators' gem 'kindlerb' @@ -36,9 +36,9 @@ group :test do gem 'ruby-prof', '~> 0.11.2' end - platforms :mri_19, :mri_20 do - gem 'debugger' - end + # platforms :mri_19, :mri_20 do + # gem 'debugger' + # end gem 'benchmark-ips' end diff --git a/RAILS_VERSION b/RAILS_VERSION index 14b6456d31..78dae579e8 100644 --- a/RAILS_VERSION +++ b/RAILS_VERSION @@ -1 +1 @@ -4.1.0.beta +4.1.0.beta1 @@ -59,7 +59,8 @@ independently outside Rails. Run with `--help` or `-h` for options. -4. Using a browser, go to http://localhost:3000 and you'll see: "Welcome aboard: You're riding Ruby on Rails!" +4. Using a browser, go to `http://localhost:3000` and you'll see: +"Welcome aboard: You're riding Ruby on Rails!" 5. Follow the guidelines to start developing your application. You may find the following resources handy: diff --git a/RELEASING_RAILS.rdoc b/RELEASING_RAILS.rdoc index aafbd25486..664505f60d 100644 --- a/RELEASING_RAILS.rdoc +++ b/RELEASING_RAILS.rdoc @@ -110,7 +110,7 @@ what to do in case anything goes wrong: $ rake all:build $ git commit -am'updating RAILS_VERSION' - $ git tag -m'tagging rc release' v3.0.10.rc1 + $ git tag -m 'v3.0.10.rc1 release' v3.0.10.rc1 $ git push $ git push --tags $ for i in $(ls pkg); do gem push $i; done diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index 857cde399a..c264c710f6 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -1,6 +1,32 @@ +* Support the use of underscored symbols when registering interceptors and + observers like we do elsewhere within Rails. + + *Andrew White* + +* Add the ability to intercept emails before previewing in a similar fashion + to how emails can be intercepted before delivery, e.g: + + class CSSInlineStyler + def self.previewing_email(message) + # inline CSS styles + end + end + + ActionMailer::Base.register_preview_interceptor CSSInlineStyler + + Fixes #13622. + + *Andrew White* + +* Add mailer previews feature based on 37 Signals mail_view gem + + *Andrew White* + * Calling `mail()` without arguments serves as getter for the current mail message and keeps previously set headers. + Fixes #13090. + Example: class MailerWithCallback < ActionMailer::Base diff --git a/actionmailer/MIT-LICENSE b/actionmailer/MIT-LICENSE index 5c668d9624..d58dd9ed9b 100644 --- a/actionmailer/MIT-LICENSE +++ b/actionmailer/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2013 David Heinemeier Hansson +Copyright (c) 2004-2014 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb index 5b6960c8fc..83969d4074 100644 --- a/actionmailer/lib/action_mailer.rb +++ b/actionmailer/lib/action_mailer.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2013 David Heinemeier Hansson +# Copyright (c) 2004-2014 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -41,6 +41,8 @@ module ActionMailer autoload :Base autoload :DeliveryMethods autoload :MailHelper + autoload :Preview + autoload :Previews, 'action_mailer/preview' autoload :TestCase autoload :TestHelper end diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 5723b2cc1b..388f694590 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -50,8 +50,8 @@ module ActionMailer # # * <tt>mail</tt> - Allows you to specify email to be sent. # - # The hash passed to the mail method allows you to specify any header that a Mail::Message - # will accept (any valid Email header including optional fields). + # The hash passed to the mail method allows you to specify any header that a <tt>Mail::Message</tt> + # will accept (any valid email header including optional fields). # # The mail method, if not passed a block, will inspect your views and send all the views with # the same name as the method, so the above action would send the +welcome.text.erb+ view @@ -229,7 +229,7 @@ module ActionMailer # An interceptor class must implement the <tt>:delivering_email(message)</tt> method which will be # called before the email is sent, allowing you to make modifications to the email before it hits # the delivery agents. Your class should make any needed modifications directly to the passed - # in Mail::Message instance. + # in <tt>Mail::Message</tt> instance. # # = Default Hash # @@ -308,6 +308,43 @@ module ActionMailer # 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. # + # = Previewing emails + # + # You can preview your email templates visually by adding a mailer preview file to the + # <tt>ActionMailer::Base.preview_path</tt>. Since most emails do something interesting + # with database data, you'll need to write some scenarios to load messages with fake data: + # + # class NotifierPreview < ActionMailer::Preview + # def welcome + # Notifier.welcome(User.first) + # end + # end + # + # Methods must return a <tt>Mail::Message</tt> object which can be generated by calling the mailer + # method without the additional <tt>deliver</tt>. The location of the mailer previews + # 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" + # + # An overview of all previews is accessible at <tt>http://localhost:3000/rails/mailers</tt> + # on a running development server instance. + # + # Previews can also be intercepted in a similar manner as deliveries can be by registering + # a preview interceptor that has a <tt>previewing_email</tt> method: + # + # class CssInlineStyler + # def self.previewing_email(message) + # # inline CSS styles + # end + # end + # + # config.action_mailer.register_preview_interceptor :css_inline_styler + # + # Note that interceptors need to be registered both with <tt>register_interceptor</tt> + # and <tt>register_preview_interceptor</tt> if they should operate on both sending and + # previewing emails. + # # = Configuration options # # These options are specified on the class level, like @@ -317,7 +354,7 @@ module ActionMailer # per the above section. # # * <tt>logger</tt> - the logger is used for generating information on the mailing run if available. - # Can be set to nil for no logging. Compatible with both Ruby's own Logger and Log4r loggers. + # Can be set to +nil+ for no logging. Compatible with both Ruby's own +Logger+ and Log4r loggers. # # * <tt>smtp_settings</tt> - Allows detailed configuration for <tt>:smtp</tt> delivery method: # * <tt>:address</tt> - Allows you to use a remote mail server. Just change it from its default @@ -335,8 +372,9 @@ module ActionMailer # and starts to use it. # * <tt>:openssl_verify_mode</tt> - When using TLS, you can set how OpenSSL checks the certificate. This is # really useful if you need to validate a self-signed and/or a wildcard certificate. You can use the name - # of an OpenSSL verify constant ('none', 'peer', 'client_once', 'fail_if_no_peer_cert') or directly the - # constant (OpenSSL::SSL::VERIFY_NONE, OpenSSL::SSL::VERIFY_PEER, ...). + # of an OpenSSL verify constant (<tt>'none'</tt>, <tt>'peer'</tt>, <tt>'client_once'</tt>, + # <tt>'fail_if_no_peer_cert'</tt>) or directly the constant (<tt>OpenSSL::SSL::VERIFY_NONE</tt>, + # <tt>OpenSSL::SSL::VERIFY_PEER</tt>, ...). # # * <tt>sendmail_settings</tt> - Allows you to override options for the <tt>:sendmail</tt> delivery method. # * <tt>:location</tt> - The location of the sendmail executable. Defaults to <tt>/usr/sbin/sendmail</tt>. @@ -351,7 +389,7 @@ module ActionMailer # # * <tt>delivery_method</tt> - Defines a delivery method. Possible values are <tt>:smtp</tt> (default), # <tt>:sendmail</tt>, <tt>:test</tt>, and <tt>:file</tt>. Or you may provide a custom delivery method - # object e.g. MyOwnDeliveryMethodClass. See the Mail gem documentation on the interface you need to + # object e.g. +MyOwnDeliveryMethodClass+. See the Mail gem documentation on the interface you need to # implement for a custom delivery agent. # # * <tt>perform_deliveries</tt> - Determines whether emails are actually sent from Action Mailer when you @@ -362,6 +400,7 @@ module ActionMailer # <tt>delivery_method :test</tt>. Most useful for unit and functional testing. class Base < AbstractController::Base include DeliveryMethods + include Previews abstract! @@ -405,18 +444,30 @@ module ActionMailer end # Register an Observer which will be notified when mail is delivered. - # Either a class or a string can be passed in as the Observer. If a string is passed in - # it will be +constantize+d. + # Either a class, string or symbol can be passed in as the Observer. + # If a string or symbol is passed in it will be camelized and constantized. def register_observer(observer) - delivery_observer = (observer.is_a?(String) ? observer.constantize : observer) + delivery_observer = case observer + when String, Symbol + observer.to_s.camelize.constantize + else + observer + end + Mail.register_observer(delivery_observer) end # Register an Interceptor which will be called before mail is sent. - # Either a class or a string can be passed in as the Interceptor. If a string is passed in - # it will be <tt>constantize</tt>d. + # Either a class, string or symbol can be passed in as the Interceptor. + # If a string or symbol is passed in it will be camelized and constantized. def register_interceptor(interceptor) - delivery_interceptor = (interceptor.is_a?(String) ? interceptor.constantize : interceptor) + delivery_interceptor = case interceptor + when String, Symbol + interceptor.to_s.camelize.constantize + else + interceptor + end + Mail.register_interceptor(delivery_interceptor) end @@ -464,11 +515,11 @@ module ActionMailer end end - # Wraps an email delivery inside of ActiveSupport::Notifications instrumentation. + # Wraps an email delivery inside of <tt>ActiveSupport::Notifications</tt> instrumentation. # - # This method is actually called by the Mail::Message object itself - # through a callback when you call +:deliver+ on the Mail::Message, - # calling +deliver_mail+ directly and passing a Mail::Message will do + # This method is actually called by the <tt>Mail::Message</tt> object itself + # through a callback when you call <tt>:deliver</tt> on the <tt>Mail::Message</tt>, + # calling +deliver_mail+ directly and passing a <tt>Mail::Message</tt> will do # nothing except tell the logger you sent the email. def deliver_mail(mail) #:nodoc: ActiveSupport::Notifications.instrument("deliver.action_mailer") do |payload| @@ -544,18 +595,18 @@ module ActionMailer self.class.mailer_name end - # Allows you to pass random and unusual headers to the new Mail::Message + # Allows you to pass random and unusual headers to the new <tt>Mail::Message</tt> # object which will add them to itself. # # headers['X-Special-Domain-Specific-Header'] = "SecretValue" # # You can also pass a hash into headers of header field names and values, - # which will then be set on the Mail::Message object: + # which will then be set on the <tt>Mail::Message</tt> object: # # headers 'X-Special-Domain-Specific-Header' => "SecretValue", # 'In-Reply-To' => incoming.message_id # - # The resulting Mail::Message will have the following in its header: + # The resulting <tt>Mail::Message</tt> will have the following in its header: # # X-Special-Domain-Specific-Header: SecretValue def headers(args = nil) @@ -644,13 +695,13 @@ module ActionMailer # templates in the view paths using by default the mailer name and the # method name that it is being called from, it will then create parts for # each of these templates intelligently, making educated guesses on correct - # content type and sequence, and return a fully prepared Mail::Message - # ready to call +:deliver+ on to send. + # content type and sequence, and return a fully prepared <tt>Mail::Message</tt> + # ready to call <tt>:deliver</tt> on to send. # # For example: # # class Notifier < ActionMailer::Base - # default from: 'no-reply@test.lindsaar.net', + # default from: 'no-reply@test.lindsaar.net' # # def welcome # mail(to: 'mikel@test.lindsaar.net') diff --git a/actionmailer/lib/action_mailer/preview.rb b/actionmailer/lib/action_mailer/preview.rb new file mode 100644 index 0000000000..fde0e86f5b --- /dev/null +++ b/actionmailer/lib/action_mailer/preview.rb @@ -0,0 +1,104 @@ +require 'active_support/descendants_tracker' + +module ActionMailer + module Previews #:nodoc: + extend ActiveSupport::Concern + + included do + # Set the location of mailer previews through app configuration: + # + # config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews" + # + mattr_accessor :preview_path, instance_writer: false + + # :nodoc: + mattr_accessor :preview_interceptors, instance_writer: false + self.preview_interceptors = [] + + # Register one or more Interceptors which will be called before mail is previewed. + def register_preview_interceptors(*interceptors) + interceptors.flatten.compact.each { |interceptor| register_preview_interceptor(interceptor) } + end + + # Register am Interceptor which will be called before mail is previewed. + # Either a class or a string can be passed in as the Interceptor. If a + # string is passed in it will be <tt>constantize</tt>d. + def register_preview_interceptor(interceptor) + preview_interceptor = case interceptor + when String, Symbol + interceptor.to_s.camelize.constantize + else + interceptor + end + + unless preview_interceptors.include?(preview_interceptor) + preview_interceptors << preview_interceptor + end + end + end + end + + class Preview + extend ActiveSupport::DescendantsTracker + + class << self + # Returns all mailer preview classes + def all + load_previews if descendants.empty? + descendants + end + + # Returns the mail object for the given email name. The registered preview + # interceptors will be informed so that they can transform the message + # as they would if the mail was actually being delivered. + def call(email) + preview = self.new + message = preview.public_send(email) + inform_preview_interceptors(message) + message + end + + # Returns all of the available email previews + def emails + public_instance_methods(false).map(&:to_s).sort + end + + # Returns true if the email exists + def email_exists?(email) + emails.include?(email) + end + + # Returns true if the preview exists + def exists?(preview) + all.any?{ |p| p.preview_name == preview } + end + + # Find a mailer preview by its underscored class name + def find(preview) + all.find{ |p| p.preview_name == preview } + end + + # Returns the underscored name of the mailer preview without the suffix + def preview_name + name.sub(/Preview$/, '').underscore + end + + protected + def load_previews #:nodoc: + if preview_path + Dir["#{preview_path}/**/*_preview.rb"].each{ |file| require_dependency file } + end + end + + def preview_path #:nodoc: + Base.preview_path + end + + def inform_preview_interceptors(message) #:nodoc: + Base.preview_interceptors.each do |interceptor| + interceptor.previewing_email(message) + end + end + end + end +end diff --git a/actionmailer/lib/action_mailer/railtie.rb b/actionmailer/lib/action_mailer/railtie.rb index 7677ff3a7c..8d1e40297b 100644 --- a/actionmailer/lib/action_mailer/railtie.rb +++ b/actionmailer/lib/action_mailer/railtie.rb @@ -19,6 +19,10 @@ module ActionMailer options.javascripts_dir ||= paths["public/javascripts"].first options.stylesheets_dir ||= paths["public/stylesheets"].first + if Rails.env.development? + options.preview_path ||= defined?(Rails.root) ? "#{Rails.root}/test/mailers/previews" : nil + end + # make sure readers methods get compiled options.asset_host ||= app.config.asset_host options.relative_url_root ||= app.config.relative_url_root @@ -40,5 +44,11 @@ module ActionMailer config.compile_methods! if config.respond_to?(:compile_methods!) end end + + config.after_initialize do + if ActionMailer::Base.preview_path + ActiveSupport::Dependencies.autoload_paths << ActionMailer::Base.preview_path + end + end end end diff --git a/actionmailer/lib/action_mailer/version.rb b/actionmailer/lib/action_mailer/version.rb index 9d00091972..46eb763c26 100644 --- a/actionmailer/lib/action_mailer/version.rb +++ b/actionmailer/lib/action_mailer/version.rb @@ -1,7 +1,7 @@ module ActionMailer # Returns the version of the currently loaded ActionMailer as a Gem::Version def self.version - Gem::Version.new "4.1.0.beta" + Gem::Version.new "4.1.0.beta1" end module VERSION #:nodoc: diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb index aa18c512c7..93d16f491d 100644 --- a/actionmailer/test/abstract_unit.rb +++ b/actionmailer/test/abstract_unit.rb @@ -20,6 +20,9 @@ ActionMailer::Base.send(:include, ActionView::Layouts) # Show backtraces for deprecated behavior for quicker cleanup. ActiveSupport::Deprecation.debug = true +# Disable available locale checks to avoid warnings running the test suite. +I18n.enforce_available_locales = false + # Bogus template processors ActionView::Template.register_template_handler :haml, lambda { |template| "Look its HAML!".inspect } ActionView::Template.register_template_handler :bak, lambda { |template| "Lame backup".inspect } @@ -59,3 +62,12 @@ end def restore_delivery_method ActionMailer::Base.delivery_method = @old_delivery_method end + +# Skips the current run on Rubinius using Minitest::Assertions#skip +def rubinius_skip(message = '') + skip message if RUBY_ENGINE == 'rbx' +end +# Skips the current run on JRuby using Minitest::Assertions#skip +def jruby_skip(message = '') + skip message if defined?(JRUBY_VERSION) +end diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index c1759d9b92..02707d0b5f 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -530,6 +530,13 @@ class BaseTest < ActiveSupport::TestCase mail.deliver 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 + 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 @@ -539,12 +546,18 @@ class BaseTest < ActiveSupport::TestCase end class MyInterceptor - def self.delivering_email(mail) - end + def self.delivering_email(mail); end + def self.previewing_email(mail); end end class MySecondInterceptor - def self.delivering_email(mail) + def self.delivering_email(mail); end + def self.previewing_email(mail); end + end + + class BaseMailerPreview < ActionMailer::Preview + def welcome + BaseMailer.welcome end end @@ -562,6 +575,13 @@ class BaseTest < ActiveSupport::TestCase mail.deliver 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 + 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 @@ -570,6 +590,39 @@ class BaseTest < ActiveSupport::TestCase 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.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.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.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.stubs(:welcome).returns(mail) + MyInterceptor.expects(:previewing_email).with(mail) + MySecondInterceptor.expects(:previewing_email).with(mail) + BaseMailerPreview.call(:welcome) + end + test "being able to put proc's into the defaults hash and they get evaluated on mail sending" do mail1 = ProcMailer.welcome['X-Proc-Method'] yesterday = 1.day.ago diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 38d20afc83..f836b69042 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,178 @@ +* Add `:serializer` option for `config.session_store :cookie_store`. This + changes default serializer when using `:cookie_store` to + `ActionDispatch::Session::MarshalSerializer` which is wrapper on Marshal. + + It is also possible to pass: + + * `:json_serializer` which is secure wrapper on JSON using `JSON.parse` and + `JSON.generate` methods with quirks mode; + * any other Symbol or String like `:my_custom_serializer` which will be + camelized and constantized in `ActionDispatch::Session` namespace; + * serializer object with `load` and `dump` methods defined. + + *Łukasz Sarnacki + Matt Aimonetti* + +* Ensure that `request.filtered_parameters` is reset between calls to `process` + in `ActionController::TestCase`. + + Fixes #13803. + + *Andrew White* + +* Fix `rake routes` error when `Rails::Engine` with empty routes is mounted. + + Fixes #13810. + + *Maurizio De Santis* + +* Log which keys were affected by deep munge. + + Deep munge solves CVE-2013-0155 security vulnerability, but its + behaviour is definately confusing, so now at least information + about for which keys values were set to nil is visible in logs. + + *Łukasz Sarnacki* + +* Automatically convert dashes to underscores for shorthand routes, e.g: + + get '/our-work/latest' + + When running `rake routes` you will get the following output: + + Prefix Verb URI Pattern Controller#Action + our_work_latest GET /our-work/latest(.:format) our_work#latest + + *Mikko Johansson* + +* Automatically convert dashes to underscores for url helpers, e.g: + + get '/contact-us' => 'pages#contact' + get '/about-us' => 'pages#about_us' + + When running `rake routes` you will get the following output: + + Prefix Verb URI Pattern Controller#Action + contact_us GET /contact-us(.:format) pages#contact + about_us GET /about-us(.:format) pages#about_us + + *Amr Tamimi* + +* Fix stream closing when sending file with `ActionController::Live` included. + + Fixes #12381 + + *Alessandro Diaferia* + +* Allow an absolute controller path inside a module scope. Fixes #12777. + + Example: + + namespace :foo do + # will route to BarController without the namespace. + get '/special', to: '/bar#index' + end + + +* Unique the segment keys array for non-optimized url helpers + + In Rails 3.2 you only needed pass an argument for dynamic segment once so + unique the segment keys array to match the number of args. Since the number + of args is less than required parts the non-optimized code path is selected. + This means to benefit from optimized url generation the arg needs to be + specified as many times as it appears in the path. + + Fixes #12808. + + *Andrew White* + +* Show full route constraints in error message + + When an optimized helper fails to generate, show the full route constraints + in the error message. Previously it would only show the contraints that were + required as part of the path. + + Fixes #13592. + + *Andrew White* + +* Use a custom route visitor for optimized url generation. Fixes #13349. + + *Andrew White* + +* Allow engine root relative redirects using an empty string. + + Example: + + # application routes.rb + mount BlogEngine => '/blog' + + # engine routes.rb + get '/welcome' => redirect('') + + This now redirects to the path `/blog`, whereas before it would redirect + to the application root path. In the case of a path redirect or a custom + redirect if the path returned contains a host then the path is treated as + absolute. Similarly for option redirects, if the options hash returned + contains a `:host` or `:domain` key then the path is treated as absolute. + + Fixes #7977. + + *Andrew White* + +* Fix `Encoding::CompatibilityError` when public path is UTF-8 + + In #5337 we forced the path encoding to ASCII-8BIT to prevent static file handling + from blowing up before an application has had chance to deal with possibly invalid + urls. However this has a negative side effect of making it an incompatible encoding + if the application's public path has UTF-8 characters in it. + + To work around the problem we check to see if the path has a valid encoding once + it has been unescaped. If it is not valid then we can return early since it will + not match any file anyway. + + Fixes #13518. + + *Andrew White* + +* `ActionController::Parameters#permit!` permits hashes in array values. + + *Xavier Noria* + +* Converts hashes in arrays of unfiltered params to unpermitted params. + + Fixes #13382. + + *Xavier Noria* + +* New config option to opt out of params "deep munging" that was used to + address security vulnerability CVE-2013-0155. In your app config: + + config.action_dispatch.perform_deep_munge = false + + Take care to understand the security risk involved before disabling this. + [Read more.](https://groups.google.com/forum/#!topic/rubyonrails-security/t1WFuuQyavI) + + *Bernard Potocki* + +* `rake routes` shows routes defined under assets prefix. + + *Ryunosuke SATO* + +* Extend cross-site request forgery (CSRF) protection to GET requests with + JavaScript responses, protecting apps from cross-origin `<script>` tags. + + *Jeremy Kemper* + +* Fix generating a path for engine inside a resources block. + + Fixes #8533. + + *Piotr Sarnacki* + +* Add `Mime::Type.register "text/vcard", :vcf` to the default list of mime types. + + *DHH* + * Remove deprecated `ActionController::RecordIdentifier`, use `ActionView::RecordIdentifier` instead. @@ -18,7 +193,7 @@ The request variant is a specialization of the request format, like `:tablet`, `:phone`, or `:desktop`. - You can set the variant in a before_action: + You can set the variant in a `before_action`: request.variant = :tablet if request.user_agent =~ /iPad/ @@ -37,16 +212,43 @@ app/views/projects/show.html+tablet.erb app/views/projects/show.html+phone.erb + You can also simplify the variants definition using the inline syntax: + + respond_to do |format| + format.js { render "trash" } + format.html.phone { redirect_to progress_path } + format.html.none { render "trash" } + end + + Variants also support common `any`/`all` block that formats have. + + It works for both inline: + + respond_to do |format| + format.html.any { render text: "any" } + format.html.phone { render text: "phone" } + end + + and block syntax: + + respond_to do |format| + format.html do |variant| + variant.any(:tablet, :phablet){ render text: "any" } + variant.phone { render text: "phone" } + end + end + *Łukasz Strzałkowski* -* Fix header `Content-Type: #<Mime::NullType:...>` in localized template. +* Fix render of localized templates without an explicit format using wrong + content header and not passing correct formats to template due to the + introduction of the `NullType` for mimes. - When localized template has no format in the template name, - the response now has the default and correct `content-type`. + Templates like `hello.it.erb` were subject to this issue. Fixes #13064. - *Angelo Capilleri* + *Angelo Capilleri*, *Carlos Antonio da Silva* * Try to escape each part of a url correctly when using a redirect route. @@ -77,9 +279,7 @@ * Add `session#fetch` method - fetch behaves similarly to [Hash#fetch](http://www.ruby-doc.org/core-1.9.3/Hash.html#method-i-fetch), - with the exception that the returned value is always saved into the session. - + fetch behaves like [Hash#fetch](http://www.ruby-doc.org/core-1.9.3/Hash.html#method-i-fetch). It returns a value from the hash for the given key. If the key can’t be found, there are several options: @@ -290,10 +490,4 @@ *Piotr Sarnacki*, *Łukasz Strzałkowski* -* Fix removing trailing slash for mounted apps. - - Fixes #3215. - - *Piotr Sarnacki* - Please check [4-0-stable](https://github.com/rails/rails/blob/4-0-stable/actionpack/CHANGELOG.md) for previous changes. diff --git a/actionpack/MIT-LICENSE b/actionpack/MIT-LICENSE index 5c668d9624..d58dd9ed9b 100644 --- a/actionpack/MIT-LICENSE +++ b/actionpack/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2013 David Heinemeier Hansson +Copyright (c) 2004-2014 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index fce61cddfd..7be61d94c9 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -1,5 +1,6 @@ require 'active_support/concern' require 'active_support/core_ext/class/attribute' +require 'action_view/view_paths' require 'set' module AbstractController @@ -21,7 +22,7 @@ module AbstractController def render(*args, &block) options = _normalize_render(*args, &block) self.response_body = render_to_body(options) - _process_format(rendered_format) + _process_format(rendered_format) if rendered_format self.response_body end @@ -46,7 +47,7 @@ module AbstractController def render_to_body(options = {}) end - # Return Content-Type of rendered content + # Returns Content-Type of rendered content # :api: public def rendered_format Mime::TEXT diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 85f0f50807..c0f10da23a 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -1,3 +1,4 @@ +require 'action_view' require "action_controller/log_subscriber" require "action_controller/metal/params_wrapper" diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index 9279d8bcea..823a1050b5 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -53,6 +53,15 @@ module ActionController debug("Unpermitted parameters: #{unpermitted_keys.join(", ")}") end + def deep_munge(event) + message = "Value for params[:#{event.payload[:keys].join('][:')}] was set"\ + "to nil, because it was one of [], [null] or [null, null, ...]."\ + "Go to http://guides.rubyonrails.org/security.html#unsafe-query-generation"\ + "for more information."\ + + debug(message) + end + %w(write_fragment read_fragment exist_fragment? expire_fragment expire_page write_page).each do |method| class_eval <<-METHOD, __FILE__, __LINE__ + 1 diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index 424473801d..43407f5b78 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -1,6 +1,6 @@ module ActionController module Head - # Return a response that has no content (merely headers). The options + # Returns a response that has no content (merely headers). The options # argument is interpreted to be a hash of header names and values. # This allows you to easily return a response that consists only of # significant headers: diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb index d3aa8f90c5..b0e164bc57 100644 --- a/actionpack/lib/action_controller/metal/instrumentation.rb +++ b/actionpack/lib/action_controller/metal/instrumentation.rb @@ -67,7 +67,7 @@ module ActionController private - # A hook invoked everytime a before callback is halted. + # A hook invoked every time a before callback is halted. def halted_callback_hook(filter) ActiveSupport::Notifications.instrument("halted_callback.action_controller", :filter => filter) end diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index 0dd788645b..33014b97ca 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -48,7 +48,7 @@ module ActionController # the server will receive a +Last-Event-ID+ header with value equal to +id+. # # After setting an option in the constructor of the SSE object, all future - # SSEs sent accross the stream will use those options unless overridden. + # SSEs sent across the stream will use those options unless overridden. # # Example Usage: # @@ -234,7 +234,7 @@ module ActionController def response_body=(body) super - response.stream.close if response + response.close if response end def set_response!(request) diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 54d3be68f0..d5e08b7034 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -184,7 +184,7 @@ module ActionController #:nodoc: # Formats can have different variants. # # The request variant is a specialization of the request format, like <tt>:tablet</tt>, - # <tt>:phone</tt>, or <tt>:desktop<tt>. + # <tt>:phone</tt>, or <tt>:desktop</tt>. # # We often want to render different html/json/xml templates for phones, # tablets, and desktop browsers. Variants make it easy. @@ -196,9 +196,10 @@ module ActionController #:nodoc: # Respond to variants in the action just like you respond to formats: # # respond_to do |format| - # format.html do |html| - # html.tablet # renders app/views/projects/show.html+tablet.erb - # html.phone { extra_setup; render ... } + # format.html do |variant| + # variant.tablet # renders app/views/projects/show.html+tablet.erb + # variant.phone { extra_setup; render ... } + # variant.none { special_setup } # executed only if there is no variant set # end # end # @@ -208,13 +209,40 @@ module ActionController #:nodoc: # app/views/projects/show.html+tablet.erb # app/views/projects/show.html+phone.erb # + # When you're not sharing any code within the format, you can simplify defining variants + # using the inline syntax: + # + # respond_to do |format| + # format.js { render "trash" } + # format.html.phone { redirect_to progress_path } + # format.html.none { render "trash" } + # end + # + # Variants also support common `any`/`all` block that formats have. + # + # It works for both inline: + # + # respond_to do |format| + # format.html.any { render text: "any" } + # format.html.phone { render text: "phone" } + # end + # + # and block syntax: + # + # respond_to do |format| + # format.html do |variant| + # variant.any(:tablet, :phablet){ render text: "any" } + # variant.phone { render text: "phone" } + # end + # end + # # Be sure to check the documentation of +respond_with+ and # <tt>ActionController::MimeResponds.respond_to</tt> for more examples. def respond_to(*mimes, &block) raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given? if collector = retrieve_collector_from_mimes(mimes, &block) - response = collector.response(request.variant) + response = collector.response response ? response.call : render({}) end end @@ -356,7 +384,7 @@ module ActionController #:nodoc: if collector = retrieve_collector_from_mimes(&block) options = resources.size == 1 ? {} : resources.extract_options! options = options.clone - options[:default_response] = collector.response(request.variant) + options[:default_response] = collector.response (options.delete(:responder) || self.class.responder).call(self, resources, options) end end @@ -389,7 +417,7 @@ module ActionController #:nodoc: # is available. def retrieve_collector_from_mimes(mimes=nil, &block) #:nodoc: mimes ||= collect_mimes_from_class_level - collector = Collector.new(mimes) + collector = Collector.new(mimes, request.variant) block.call(collector) if block_given? format = collector.negotiate_format(request) @@ -427,9 +455,11 @@ module ActionController #:nodoc: include AbstractController::Collector attr_accessor :format - def initialize(mimes) + def initialize(mimes, variant = nil) @responses = {} - mimes.each { |mime| send(mime) } + @variant = variant + + mimes.each { |mime| @responses["Mime::#{mime.upcase}".constantize] = nil } end def any(*args, &block) @@ -443,15 +473,23 @@ module ActionController #:nodoc: def custom(mime_type, &block) mime_type = Mime::Type.lookup(mime_type.to_s) unless mime_type.is_a?(Mime::Type) - @responses[mime_type] ||= block + @responses[mime_type] ||= if block_given? + block + else + VariantCollector.new(@variant) + end end - def response(variant) + def response response = @responses.fetch(format, @responses[Mime::ALL]) - if response.nil? || response.arity == 0 + if response.is_a?(VariantCollector) # `format.html.phone` - variant inline syntax + response.variant + elsif response.nil? || response.arity == 0 # `format.html` - just a format, call its block response - else - lambda { response.call VariantFilter.new(variant) } + else # `format.html{ |variant| variant.phone }` - variant block syntax + variant_collector = VariantCollector.new(@variant) + response.call(variant_collector) #call format block with variants collector + variant_collector.variant end end @@ -459,13 +497,37 @@ module ActionController #:nodoc: @format = request.negotiate_mime(@responses.keys) end - class VariantFilter #:nodoc: - def initialize(variant) + class VariantCollector #:nodoc: + def initialize(variant = nil) @variant = variant + @variants = {} end - def method_missing(name) - yield if name == @variant + def any(*args, &block) + if block_given? + if args.any? && args.none?{ |a| a == @variant } + args.each{ |v| @variants[v] = block } + else + @variants[:any] = block + end + end + end + alias :all :any + + def method_missing(name, *args, &block) + @variants[name] = block if block_given? + end + + def variant + key = if @variant.nil? + :none + elsif @variants.has_key?(@variant) + @variant + else + :any + end + + @variants[key] end end end diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index ab14a61b97..2812038938 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -58,7 +58,7 @@ module ActionController # redirect_to post_url(@post), alert: "Watch it, mister!" # redirect_to post_url(@post), status: :found, notice: "Pay attention to the road" # redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id } - # redirect_to { action: 'atom' }, alert: "Something serious happened" + # 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. diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 62a3844b04..6c7b4652d4 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -43,7 +43,7 @@ module ActionController end # Hash of available renderers, mapping a renderer name to its proc. - # Default keys are :json, :js, :xml. + # Default keys are <tt>:json</tt>, <tt>:js</tt>, <tt>:xml</tt>. RENDERERS = Set.new # Adds a new renderer to call within controller actions. diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index 66d34f3b67..5c48b4ab98 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -34,8 +34,7 @@ module ActionController def _process_format(format) super - # format is a Mime::NullType instance here then this condition can't be changed to `if format` - self.content_type ||= format.to_s unless format.nil? + self.content_type ||= format.to_s end # Normalize arguments by catching blocks and setting them on :update. diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index bd64b1f812..c88074d4c6 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -5,14 +5,24 @@ module ActionController #:nodoc: class InvalidAuthenticityToken < ActionControllerError #:nodoc: end + class InvalidCrossOriginRequest < ActionControllerError #:nodoc: + end + # Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks # by including a token in the rendered html for your application. This token is # stored as a random string in the session, to which an attacker does not have # access. When a request reaches your application, \Rails verifies the received # token with the token in the session. Only HTML and JavaScript requests are checked, # so this will not protect your XML API (presumably you'll have a different - # authentication scheme there anyway). Also, GET requests are not protected as these - # should be idempotent. + # authentication scheme there anyway). + # + # GET requests are not protected since they don't have side effects like writing + # to the database and don't leak sensitive information. JavaScript requests are + # an exception: a third-party site can use a <script> tag to reference a JavaScript + # URL on your site. When your JavaScript response loads on their site, it executes. + # With carefully crafted JavaScript on their end, sensitive data in your JavaScript + # response may be extracted. To prevent this, only XmlHttpRequest (known as XHR or + # Ajax) requests are allowed to make GET requests for JavaScript responses. # # It's important to remember that XML or JSON requests are also affected and if # you're building an API you'll need something like: @@ -65,17 +75,16 @@ module ActionController #:nodoc: module ClassMethods # Turn on request forgery protection. Bear in mind that only non-GET, HTML/JavaScript requests are checked. # + # class ApplicationController < ActionController::Base + # protect_from_forgery + # end + # # class FooController < ApplicationController # protect_from_forgery except: :index # - # You can disable csrf protection on controller-by-controller basis: - # + # You can disable CSRF protection on controller by skipping the verification before_action: # skip_before_action :verify_authenticity_token # - # It can also be disabled for specific controller actions: - # - # skip_before_action :verify_authenticity_token, except: [:create] - # # Valid Options: # # * <tt>:only/:except</tt> - Passed to the <tt>before_action</tt> call. Set which actions are verified. @@ -89,6 +98,7 @@ module ActionController #:nodoc: self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session) self.request_forgery_protection_token ||= :authenticity_token prepend_before_action :verify_authenticity_token, options + append_after_action :verify_same_origin_request end private @@ -169,18 +179,61 @@ module ActionController #:nodoc: end protected + # The actual before_action that is used to verify the CSRF token. + # Don't override this directly. Provide your own forgery protection + # strategy instead. If you override, you'll disable same-origin + # `<script>` verification. + # + # Lean on the protect_from_forgery declaration to mark which actions are + # due for same-origin request verification. If protect_from_forgery is + # enabled on an action, this before_action flags its after_action to + # verify that JavaScript responses are for XHR requests, ensuring they + # follow the browser's same-origin policy. + def verify_authenticity_token + mark_for_same_origin_verification! + + if !verified_request? + logger.warn "Can't verify CSRF token authenticity" if logger + handle_unverified_request + end + end + def handle_unverified_request forgery_protection_strategy.new(self).handle_unverified_request end - # The actual before_action that is used. Modify this to change how you handle unverified requests. - def verify_authenticity_token - unless verified_request? - logger.warn "Can't verify CSRF token authenticity" if logger - handle_unverified_request + CROSS_ORIGIN_JAVASCRIPT_WARNING = "Security warning: an embedded " \ + "<script> tag on another site requested protected JavaScript. " \ + "If you know what you're doing, go ahead and disable forgery " \ + "protection on this action to permit cross-origin JavaScript embedding." + private_constant :CROSS_ORIGIN_JAVASCRIPT_WARNING + + # If `verify_authenticity_token` was run (indicating that we have + # forgery protection enabled for this request) then also verify that + # we aren't serving an unauthorized cross-origin response. + def verify_same_origin_request + if marked_for_same_origin_verification? && non_xhr_javascript_response? + logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING if logger + raise ActionController::InvalidCrossOriginRequest, CROSS_ORIGIN_JAVASCRIPT_WARNING end end + # GET requests are checked for cross-origin JavaScript after rendering. + def mark_for_same_origin_verification! + @marked_for_same_origin_verification = request.get? + end + + # If the `verify_authenticity_token` before_action ran, verify that + # JavaScript responses are only served to same-origin GET requests. + def marked_for_same_origin_verification? + @marked_for_same_origin_verification ||= false + end + + # Check for cross-origin JavaScript responses. + def non_xhr_javascript_response? + content_type =~ %r(\Atext/javascript) && !request.xhr? + end + # Returns true or false if a request is verified. Checks: # # * is it a GET or HEAD request? Gets should be safe and idempotent diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb index b4ba169e8f..e24b56fa91 100644 --- a/actionpack/lib/action_controller/metal/responder.rb +++ b/actionpack/lib/action_controller/metal/responder.rb @@ -270,7 +270,7 @@ module ActionController #:nodoc: resource.respond_to?(:errors) && !resource.errors.empty? end - # Check whether the neceessary Renderer is available + # Check whether the necessary Renderer is available def has_renderer? Renderers::RENDERERS.include?(format) end diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index b4948d99a8..48a916f2b1 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -3,6 +3,7 @@ require 'active_support/core_ext/array/wrap' require 'active_support/rescuable' require 'action_dispatch/http/upload' require 'stringio' +require 'set' module ActionController # Raised when a required parameter is missing. @@ -125,6 +126,13 @@ module ActionController @permitted = self.class.permit_all_parameters end + # Attribute that keeps track of converted arrays, if any, to avoid double + # looping in the common use case permit + mass-assignment. Defined in a + # method to instantiate it only if needed. + def converted_arrays + @converted_arrays ||= Set.new + end + # Returns +true+ if the parameter is permitted, +false+ otherwise. # # params = ActionController::Parameters.new @@ -149,8 +157,10 @@ module ActionController # Person.new(params) # => #<Person id: nil, name: "Francesco"> def permit! each_pair do |key, value| - convert_hashes_to_parameters(key, value) - self[key].permit! if self[key].respond_to? :permit! + value = convert_hashes_to_parameters(key, value) + Array.wrap(value).each do |_| + _.permit! if _.respond_to? :permit! + end end @permitted = true @@ -284,14 +294,7 @@ module ActionController # params.fetch(:none, 'Francesco') # => "Francesco" # params.fetch(:none) { 'Francesco' } # => "Francesco" def fetch(key, *args) - value = super - # Don't rely on +convert_hashes_to_parameters+ - # so as to not mutate via a +fetch+ - if value.is_a?(Hash) - value = self.class.new(value) - value.permit! if permitted? - end - value + convert_hashes_to_parameters(key, super, false) rescue KeyError raise ActionController::ParameterMissing.new(key) end @@ -329,12 +332,21 @@ module ActionController end private - def convert_hashes_to_parameters(key, value) - if value.is_a?(Parameters) || !value.is_a?(Hash) + def convert_hashes_to_parameters(key, value, assign_if_converted=true) + converted = convert_value_to_parameters(value) + self[key] = converted if assign_if_converted && !converted.equal?(value) + converted + end + + def convert_value_to_parameters(value) + if value.is_a?(Array) && !converted_arrays.member?(value) + converted = value.map { |_| convert_value_to_parameters(_) } + converted_arrays << converted + converted + elsif value.is_a?(Parameters) || !value.is_a?(Hash) value else - # Convert to Parameters on first access - self[key] = self.class.new(value) + self.class.new(value) end end diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index 0833e65d23..a2fc814221 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -3,6 +3,7 @@ require "action_controller" require "action_dispatch/railtie" require "abstract_controller/railties/routes_helpers" require "action_controller/railties/helpers" +require "action_view/railtie" module ActionController class Railtie < Rails::Railtie #:nodoc: diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 5ed3d2ebc1..cf11ce1a9b 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -213,6 +213,9 @@ module ActionController # Clear the combined params hash in case it was already referenced. @env.delete("action_dispatch.request.parameters") + # Clear the filter cache variables so they're not stale + @filtered_parameters = @filtered_env = @filtered_path = nil + params = self.request_parameters.dup %w(controller action only_path).each do |k| params.delete(k) diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index 24a3d4741e..36dcca2905 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2013 David Heinemeier Hansson +# Copyright (c) 2004-2014 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -82,10 +82,12 @@ module ActionDispatch end module Session - autoload :AbstractStore, 'action_dispatch/middleware/session/abstract_store' - autoload :CookieStore, 'action_dispatch/middleware/session/cookie_store' - autoload :MemCacheStore, 'action_dispatch/middleware/session/mem_cache_store' - autoload :CacheStore, 'action_dispatch/middleware/session/cache_store' + autoload :AbstractStore, 'action_dispatch/middleware/session/abstract_store' + autoload :CookieStore, 'action_dispatch/middleware/session/cookie_store' + autoload :MemCacheStore, 'action_dispatch/middleware/session/mem_cache_store' + autoload :CacheStore, 'action_dispatch/middleware/session/cache_store' + autoload :JsonSerializer, 'action_dispatch/middleware/session/json_serializer' + autoload :MarshalSerializer, 'action_dispatch/middleware/session/marshal_serializer' end mattr_accessor :test_app diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index 346598b6de..c33ba201e1 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -50,7 +50,7 @@ module ActionDispatch # GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first # def format(view_path = []) - formats.first + formats.first || Mime::NullType.instance end def formats diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index 2a8ff0a5d2..3d2dd2d632 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -28,7 +28,7 @@ module Mime class << self def [](type) return type if type.is_a?(Type) - Type.lookup_by_extension(type) || NullType.instance + Type.lookup_by_extension(type) end def fetch(type) diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb index a6b3aee5e7..0e4da36038 100644 --- a/actionpack/lib/action_dispatch/http/mime_types.rb +++ b/actionpack/lib/action_dispatch/http/mime_types.rb @@ -7,6 +7,7 @@ Mime::Type.register "text/javascript", :js, %w( application/javascript applicati Mime::Type.register "text/css", :css Mime::Type.register "text/calendar", :ics Mime::Type.register "text/csv", :csv +Mime::Type.register "text/vcard", :vcf Mime::Type.register "image/png", :png, [], %w(png) Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe pjpeg) diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb index 7764763791..4410c1b5d5 100644 --- a/actionpack/lib/action_dispatch/journey/formatter.rb +++ b/actionpack/lib/action_dispatch/journey/formatter.rb @@ -33,8 +33,8 @@ module ActionDispatch return [route.format(parameterized_parts), params] end - message = "No route matches #{constraints.inspect}" - message << " missing required keys: #{missing_keys.inspect}" if name + message = "No route matches #{Hash[constraints.sort].inspect}" + message << " missing required keys: #{missing_keys.sort.inspect}" if name raise ActionController::UrlGenerationError, message end diff --git a/actionpack/lib/action_dispatch/journey/parser.rb b/actionpack/lib/action_dispatch/journey/parser.rb index bb4cbb00e2..430812fafe 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 grammer file "". +# from Racc grammar file "". # require 'racc/parser.rb' diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb index da32f1bfe7..419e665d12 100644 --- a/actionpack/lib/action_dispatch/journey/router.rb +++ b/actionpack/lib/action_dispatch/journey/router.rb @@ -54,7 +54,7 @@ module ActionDispatch end def call(env) - env['PATH_INFO'] = normalize_path(env['PATH_INFO']) + 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', @@ -103,12 +103,6 @@ module ActionDispatch private - def normalize_path(path) - path = "/#{path}" - path.squeeze!('/') - path - end - def partitioned_routes routes.partitioned_routes end diff --git a/actionpack/lib/action_dispatch/journey/visitors.rb b/actionpack/lib/action_dispatch/journey/visitors.rb index 9e66cab052..daade5bb74 100644 --- a/actionpack/lib/action_dispatch/journey/visitors.rb +++ b/actionpack/lib/action_dispatch/journey/visitors.rb @@ -77,12 +77,32 @@ module ActionDispatch end end - class OptimizedPath < String # :nodoc: + class OptimizedPath < Visitor # :nodoc: + def accept(node) + Array(visit(node)) + end + private - def visit_GROUP(node) - "" - end + 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) diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 3ccd0c9ee8..f9f034952e 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -30,7 +30,7 @@ module ActionDispatch # cookies[:login] = { value: "XJ-122", expires: 1.hour.from_now } # # # Sets a signed cookie, which prevents users from tampering with its value. - # # The cookie is signed by your app's <tt>config.secret_key_base</tt> value. + # # The cookie is signed by your app's <tt>secrets.secret_key_base</tt> value. # # It can be read using the signed method <tt>cookies.signed[:name]</tt> # cookies.signed[:user_id] = current_user.id # @@ -89,6 +89,7 @@ module ActionDispatch ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze SECRET_TOKEN = "action_dispatch.secret_token".freeze SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze + SESSION_SERIALIZER = "action_dispatch.session_serializer".freeze # Cookies can typically store 4096 bytes. MAX_COOKIE_SIZE = 4096 @@ -117,10 +118,10 @@ module ActionDispatch # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed # cookie was tampered with by the user (or a 3rd party), nil will be returned. # - # If +config.secret_key_base+ and +config.secret_token+ (deprecated) are both set, + # If +secrets.secret_key_base+ and +config.secret_token+ (deprecated) are both set, # legacy cookies signed with the old key generator will be transparently upgraded. # - # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+. + # This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+. # # Example: # @@ -140,10 +141,10 @@ module ActionDispatch # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read. # If the cookie was tampered with by the user (or a 3rd party), nil will be returned. # - # If +config.secret_key_base+ and +config.secret_token+ (deprecated) are both set, + # If +secrets.secret_key_base+ and +config.secret_token+ (deprecated) are both set, # legacy cookies signed with the old key generator will be transparently upgraded. # - # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+. + # This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+. # # Example: # @@ -210,7 +211,8 @@ module ActionDispatch encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT] || '', secret_token: env[SECRET_TOKEN], secret_key_base: env[SECRET_KEY_BASE], - upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present? + upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?, + session_serializer: env[SESSION_SERIALIZER] } end @@ -409,7 +411,7 @@ module ActionDispatch end # UpgradeLegacySignedCookieJar is used instead of SignedCookieJar if - # config.secret_token and config.secret_key_base are both set. It reads + # config.secret_token and secrets.secret_key_base are both set. It reads # legacy cookies signed with the old dummy key generator and re-saves # them using the new key generator to provide a smooth upgrade path. class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc: @@ -427,7 +429,7 @@ module ActionDispatch def initialize(parent_jar, key_generator, options = {}) if ActiveSupport::LegacyKeyGenerator === key_generator - raise "You didn't set config.secret_key_base, which is required for this cookie jar. " + + raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " + "Read the upgrade documentation to learn more about this new config option." end @@ -435,7 +437,7 @@ module ActionDispatch @options = options secret = key_generator.generate_key(@options[:encrypted_cookie_salt]) sign_secret = key_generator.generate_key(@options[:encrypted_signed_cookie_salt]) - @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret) + @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: serializer) end def [](name) @@ -462,10 +464,20 @@ module ActionDispatch rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage nil end + + def serializer + serializer = @options[:session_serializer] || :marshal_serializer + case serializer + when Symbol, String + ActionDispatch::Session.const_get(serializer.to_s.camelize) + else + serializer + end + end end # UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore - # instead of EncryptedCookieJar if config.secret_token and config.secret_key_base + # instead of EncryptedCookieJar if config.secret_token and secrets.secret_key_base # are both set. It reads legacy cookies signed with the old dummy key generator and # encrypts and re-saves them using the new key generator to provide a smooth upgrade path. class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc: diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb index 2f6968eb2e..15b5a48535 100644 --- a/actionpack/lib/action_dispatch/middleware/reloader.rb +++ b/actionpack/lib/action_dispatch/middleware/reloader.rb @@ -1,3 +1,5 @@ +require 'active_support/deprecation/reporting' + module ActionDispatch # ActionDispatch::Reloader provides prepare and cleanup callbacks, # intended to assist with code reloading during development. @@ -25,19 +27,26 @@ module ActionDispatch # class Reloader include ActiveSupport::Callbacks + include ActiveSupport::Deprecation::Reporting - define_callbacks :prepare, :scope => :name - define_callbacks :cleanup, :scope => :name + define_callbacks :prepare + define_callbacks :cleanup # Add a prepare callback. Prepare callbacks are run before each request, prior # to ActionDispatch::Callback's before callbacks. def self.to_prepare(*args, &block) + unless block_given? + warn "to_prepare without a block is deprecated. Please use a block" + end set_callback(:prepare, *args, &block) end # Add a cleanup callback. Cleanup callbacks are run after each request is # complete (after #close is called on the response body). def self.to_cleanup(*args, &block) + unless block_given? + warn "to_cleanup without a block is deprecated. Please use a block" + end set_callback(:cleanup, *args, &block) end diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index 11b42ee5be..1ebc189c28 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -31,9 +31,10 @@ module ActionDispatch # # Myapp::Application.config.session_store :cookie_store, key: '_your_app_session' # - # Configure your secret key in config/initializers/secret_token.rb: + # Configure your secret key in config/secrets.yml: # - # Myapp::Application.config.secret_key_base 'secret key' + # development: + # secret_key_base: 'secret key' # # To generate a secret key for an existing application, run `rake secret`. # diff --git a/actionpack/lib/action_dispatch/middleware/session/json_serializer.rb b/actionpack/lib/action_dispatch/middleware/session/json_serializer.rb new file mode 100644 index 0000000000..d341853f7a --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/session/json_serializer.rb @@ -0,0 +1,13 @@ +module ActionDispatch + module Session + class JsonSerializer + def self.load(value) + JSON.parse(value, quirks_mode: true) + end + + def self.dump(value) + JSON.generate(value, quirks_mode: true) + end + end + end +end diff --git a/actionpack/lib/action_dispatch/middleware/session/marshal_serializer.rb b/actionpack/lib/action_dispatch/middleware/session/marshal_serializer.rb new file mode 100644 index 0000000000..26622f682d --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/session/marshal_serializer.rb @@ -0,0 +1,14 @@ +module ActionDispatch + module Session + class MarshalSerializer + def self.load(value) + Marshal.load(value) + end + + def self.dump(value) + Marshal.dump(value) + end + end + end +end + diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index c6a7d9c415..2764584fe9 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -11,9 +11,10 @@ module ActionDispatch end def match?(path) - path = path.dup + path = unescape_path(path) + return false unless path.valid_encoding? - full_path = path.empty? ? @root : File.join(@root, escape_glob_chars(unescape_path(path))) + full_path = path.empty? ? @root : File.join(@root, escape_glob_chars(path)) paths = "#{full_path}#{ext}" matches = Dir[paths] @@ -40,7 +41,6 @@ module ActionDispatch end def escape_glob_chars(path) - path.force_encoding('binary') if path.respond_to? :force_encoding path.gsub(/[*?{}\[\]]/, "\\\\\\&") end end 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 95461fa693..323873ba4b 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb @@ -89,8 +89,8 @@ } // takes an array of elements with a data-regexp attribute and - // passes their their parent <tr> into the callback function - // if the regexp matchs a given path + // passes their parent <tr> into the callback function + // if the regexp matches a given path function eachElemsForPath(elems, path, func) { each(elems, function(e){ var reg = e.getAttribute("data-regexp"); diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index 2dfaab3587..ddeea24bb3 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -16,6 +16,7 @@ module ActionDispatch config.action_dispatch.signed_cookie_salt = 'signed cookie' config.action_dispatch.encrypted_cookie_salt = 'encrypted cookie' config.action_dispatch.encrypted_signed_cookie_salt = 'signed encrypted cookie' + config.action_dispatch.perform_deep_munge = true config.action_dispatch.default_headers = { 'X-Frame-Options' => 'SAMEORIGIN', @@ -28,6 +29,7 @@ module ActionDispatch initializer "action_dispatch.configure" do |app| ActionDispatch::Http::URL.tld_length = app.config.action_dispatch.tld_length ActionDispatch::Request.ignore_accept_header = app.config.action_dispatch.ignore_accept_header + ActionDispatch::Request::Utils.perform_deep_munge = app.config.action_dispatch.perform_deep_munge ActionDispatch::Response.default_charset = app.config.action_dispatch.default_charset || app.config.encoding ActionDispatch::Response.default_headers = app.config.action_dispatch.default_headers diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb index 6d911a75f1..973627f106 100644 --- a/actionpack/lib/action_dispatch/request/session.rb +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -7,6 +7,9 @@ module ActionDispatch ENV_SESSION_KEY = Rack::Session::Abstract::ENV_SESSION_KEY # :nodoc: ENV_SESSION_OPTIONS_KEY = Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY # :nodoc: + # Singleton object used to determine if an optional param wasn't specified + Unspecified = Object.new + def self.create(store, env, default_options) session_was = find env session = Request::Session.new(store, env) @@ -127,15 +130,12 @@ module ActionDispatch @delegate.delete key.to_s end - def fetch(key, default=nil) - if self.key?(key) - self[key] - elsif default - self[key] = default - elsif block_given? - self[key] = yield(key) + def fetch(key, default=Unspecified, &block) + load_for_read! + if default == Unspecified + @delegate.fetch(key.to_s, &block) else - raise KeyError + @delegate.fetch(key.to_s, default, &block) end end diff --git a/actionpack/lib/action_dispatch/request/utils.rb b/actionpack/lib/action_dispatch/request/utils.rb index 8b43cdada8..9d4f1aa3c5 100644 --- a/actionpack/lib/action_dispatch/request/utils.rb +++ b/actionpack/lib/action_dispatch/request/utils.rb @@ -1,18 +1,29 @@ module ActionDispatch class Request < Rack::Request class Utils # :nodoc: + + mattr_accessor :perform_deep_munge + self.perform_deep_munge = true + class << self # Remove nils from the params hash - def deep_munge(hash) + def deep_munge(hash, keys = []) + return hash unless perform_deep_munge + hash.each do |k, v| + keys << k case v when Array - v.grep(Hash) { |x| deep_munge(x) } + v.grep(Hash) { |x| deep_munge(x, keys) } v.compact! - hash[k] = nil if v.empty? + if v.empty? + hash[k] = nil + ActiveSupport::Notifications.instrument("deep_munge.action_controller", keys: keys) + end when Hash - deep_munge(v) + deep_munge(v, keys) end + keys.pop end hash diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index 120bc54333..71a0c5e826 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -69,7 +69,7 @@ module ActionDispatch end def internal? - controller.to_s =~ %r{\Arails/(info|welcome)} || path =~ %r{\A#{Rails.application.config.assets.prefix}} + controller.to_s =~ %r{\Arails/(info|mailers|welcome)} || path =~ %r{\A#{Rails.application.config.assets.prefix}\z} end def engine? @@ -194,9 +194,9 @@ module ActionDispatch end def widths(routes) - [routes.map { |r| r[:name].length }.max, - routes.map { |r| r[:verb].length }.max, - routes.map { |r| r[:path].length }.max] + [routes.map { |r| r[:name].length }.max || 0, + routes.map { |r| r[:verb].length }.max || 0, + routes.map { |r| r[:path].length }.max || 0] end end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 846a6345cb..d5eb770cb1 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -3,6 +3,7 @@ require 'active_support/core_ext/hash/reverse_merge' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/enumerable' 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' @@ -217,8 +218,12 @@ module ActionDispatch controller ||= default_controller action ||= default_action - unless controller.is_a?(Regexp) - controller = [@scope[:module], controller].compact.join("/").presence + if @scope[:module] && !controller.is_a?(Regexp) + if controller =~ %r{\A/} + controller = controller[1..-1] + else + controller = [@scope[:module], controller].compact.join("/").presence + end end if controller.is_a?(String) && controller =~ %r{\A/} @@ -502,11 +507,12 @@ module ActionDispatch raise "A rack application must be specified" unless path options[:as] ||= app_name(app) + target_as = name_for_action(options[:as], path) options[:via] ||= :all match(path, options.merge(:to => app, :anchor => false, :format => false)) - define_generate_prefix(app, options[:as]) + define_generate_prefix(app, target_as) self end @@ -545,11 +551,11 @@ module ActionDispatch _routes = @set app.routes.define_mounted_helper(name) app.routes.singleton_class.class_eval do - define_method :mounted? do + redefine_method :mounted? do true end - define_method :_generate_prefix do |options| + redefine_method :_generate_prefix do |options| 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) } @@ -1404,6 +1410,7 @@ module ActionDispatch path_without_format = _path.to_s.sub(/\(\.:format\)$/, '') if using_match_shorthand?(path_without_format, route_options) route_options[:to] ||= path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1') + route_options[:to].tr!("-", "_") end decomposed_match(_path, route_options) @@ -1434,8 +1441,8 @@ module ActionDispatch path = path_for_action(action, options.delete(:path)) action = action.to_s.dup - if action =~ /^[\w\/]+$/ - options[:action] ||= action unless action.include?("/") + if action =~ /^[\w\-\/]+$/ + options[:action] ||= action.tr('-', '_') unless action.include?("/") else action = nil end @@ -1600,10 +1607,11 @@ module ActionDispatch def prefix_name_for_action(as, action) #:nodoc: if as - as.to_s + prefix = as elsif !canonical_action?(action, @scope[:scope_level]) - action.to_s + prefix = action end + prefix.to_s.tr('-', '_') if prefix end def name_for_action(as, action) #:nodoc: diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb index cbf4c5aa8b..b08e62543b 100644 --- a/actionpack/lib/action_dispatch/routing/redirection.rb +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -26,14 +26,19 @@ module ActionDispatch end uri = URI.parse(path(req.symbolized_path_parameters, req)) + + unless uri.host + if relative_path?(uri.path) + uri.path = "#{req.script_name}/#{uri.path}" + elsif uri.path.empty? + uri.path = req.script_name.empty? ? "/" : req.script_name + end + end + uri.scheme ||= req.scheme uri.host ||= req.host uri.port ||= req.port unless req.standard_port? - if relative_path?(uri.path) - uri.path = "#{req.script_name}/#{uri.path}" - end - body = %(<html><body>You are being <a href="#{ERB::Util.h(uri.to_s)}">redirected</a>.</body></html>) headers = { @@ -112,11 +117,16 @@ module ActionDispatch url_options[:path] = (url_options[:path] % escape_path(params)) end - if relative_path?(url_options[:path]) - url_options[:path] = "/#{url_options[:path]}" - url_options[:script_name] = request.script_name + unless options[:host] || options[:domain] + if relative_path?(url_options[:path]) + url_options[:path] = "/#{url_options[:path]}" + url_options[:script_name] = request.script_name + elsif url_options[:path].empty? + url_options[:path] = request.script_name.empty? ? "/" : "" + url_options[:script_name] = request.script_name + end end - + ActionDispatch::Http::URL.url_for url_options end diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index b8abdabca5..a03fb4cee7 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -163,9 +163,10 @@ module ActionDispatch def initialize(route, options) super - @path_parts = @route.required_parts - @arg_size = @path_parts.size - @string_route = @route.optimized_path + @klass = Journey::Router::Utils + @required_parts = @route.required_parts + @arg_size = @required_parts.size + @optimized_path = @route.optimized_path end def call(t, args) @@ -182,43 +183,36 @@ module ActionDispatch private def optimized_helper(args) - path = @string_route.dup - klass = Journey::Router::Utils + params = Hash[parameterize_args(args)] + missing_keys = missing_keys(params) - @path_parts.zip(args) do |part, arg| - parameterized_arg = arg.to_param + unless missing_keys.empty? + raise_generation_error(params, missing_keys) + end - if parameterized_arg.nil? || parameterized_arg.empty? - raise_generation_error(args) - end + @optimized_path.map{ |segment| replace_segment(params, segment) }.join + end - # Replace each route parameter - # e.g. :id for regular parameter or *path for globbing - # with ruby string interpolation code - path.gsub!(/(\*|:)#{part}/, klass.escape_fragment(parameterized_arg)) - end - path + def replace_segment(params, segment) + Symbol === segment ? @klass.escape_fragment(params[segment]) : segment end def optimize_routes_generation?(t) t.send(:optimize_routes_generation?) end - def raise_generation_error(args) - parts, missing_keys = [], [] - - @path_parts.zip(args) do |part, arg| - parameterized_arg = arg.to_param - - if parameterized_arg.nil? || parameterized_arg.empty? - missing_keys << part - end + def parameterize_args(args) + @required_parts.zip(args.map(&:to_param)) + end - parts << [part, arg] - end + def missing_keys(args) + args.select{ |part, arg| arg.nil? || arg.empty? }.keys + end - message = "No route matches #{Hash[parts].inspect}" - message << " missing required keys: #{missing_keys.inspect}" + def raise_generation_error(args, missing_keys) + constraints = Hash[@route.requirements.merge(args).sort] + message = "No route matches #{constraints.inspect}" + message << " missing required keys: #{missing_keys.sort.inspect}" raise ActionController::UrlGenerationError, message end @@ -226,7 +220,7 @@ module ActionDispatch def initialize(route, options) @options = options - @segment_keys = route.segment_keys + @segment_keys = route.segment_keys.uniq @route = route end @@ -361,7 +355,7 @@ module ActionDispatch include UrlFor end - # Contains all the mounted helpers accross different + # Contains all the mounted helpers across different # engines and the `main_app` helper for the application. # You can include this in your classes if you want to # access routes for other engines. diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index 496682e8bd..f1f998d932 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -211,7 +211,7 @@ module ActionDispatch def fail_on(exception_class) yield rescue exception_class => e - raise MiniTest::Assertion, e.message + raise Minitest::Assertion, e.message end end end diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 9beb30307b..cc6b763093 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -137,7 +137,7 @@ module ActionDispatch class Session DEFAULT_HOST = "www.example.com" - include MiniTest::Assertions + include Minitest::Assertions include TestProcess, RequestHelpers, Assertions %w( status status_message headers body redirect? ).each do |method| @@ -242,7 +242,7 @@ module ActionDispatch @https = flag end - # Return +true+ if the session is mimicking a secure HTTPS request. + # Returns +true+ if the session is mimicking a secure HTTPS request. # # if session.https? # ... diff --git a/actionpack/lib/action_pack.rb b/actionpack/lib/action_pack.rb index ad5acd8080..77f656d6f1 100644 --- a/actionpack/lib/action_pack.rb +++ b/actionpack/lib/action_pack.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2013 David Heinemeier Hansson +# Copyright (c) 2004-2014 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actionpack/lib/action_pack/version.rb b/actionpack/lib/action_pack/version.rb index fd08f392aa..a51f6a434a 100644 --- a/actionpack/lib/action_pack/version.rb +++ b/actionpack/lib/action_pack/version.rb @@ -1,7 +1,7 @@ module ActionPack # Returns the version of the currently loaded ActionPack as a Gem::Version def self.version - Gem::Version.new "4.1.0.beta" + Gem::Version.new "4.1.0.beta1" end module VERSION #:nodoc: diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 0e163a93eb..03a4741f42 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -43,6 +43,9 @@ Thread.abort_on_exception = true # Show backtraces for deprecated behavior for quicker cleanup. ActiveSupport::Deprecation.debug = true +# Disable available locale checks to avoid warnings running the test suite. +I18n.enforce_available_locales = false + # Register danish language for testing I18n.backend.store_translations 'da', {} I18n.backend.store_translations 'pt-BR', {} @@ -331,7 +334,6 @@ class ThreadsController < ResourcesController; end class MessagesController < ResourcesController; end class CommentsController < ResourcesController; end class ReviewsController < ResourcesController; end -class AuthorsController < ResourcesController; end class LogosController < ResourcesController; end class AccountsController < ResourcesController; end @@ -342,8 +344,6 @@ class PreferencesController < ResourcesController; end module Backoffice class ProductsController < ResourcesController; end - class TagsController < ResourcesController; end - class ManufacturersController < ResourcesController; end class ImagesController < ResourcesController; end module Admin @@ -351,3 +351,12 @@ module Backoffice class ImagesController < ResourcesController; end end end + +# Skips the current run on Rubinius using Minitest::Assertions#skip +def rubinius_skip(message = '') + skip message if RUBY_ENGINE == 'rbx' +end +# Skips the current run on JRuby using Minitest::Assertions#skip +def jruby_skip(message = '') + skip message if defined?(JRUBY_VERSION) +end diff --git a/actionpack/test/assertions/response_assertions_test.rb b/actionpack/test/assertions/response_assertions_test.rb index 8eec98e916..5e64cae7e2 100644 --- a/actionpack/test/assertions/response_assertions_test.rb +++ b/actionpack/test/assertions/response_assertions_test.rb @@ -19,7 +19,7 @@ module ActionDispatch @response = FakeResponse.new sym assert_response sym - assert_raises(MiniTest::Assertion) { + assert_raises(Minitest::Assertion) { assert_response :unauthorized } end @@ -29,11 +29,11 @@ module ActionDispatch @response = FakeResponse.new 400 assert_response 400 - assert_raises(MiniTest::Assertion) { + assert_raises(Minitest::Assertion) { assert_response :unauthorized } - assert_raises(MiniTest::Assertion) { + assert_raises(Minitest::Assertion) { assert_response 500 } end @@ -42,11 +42,11 @@ module ActionDispatch @response = FakeResponse.new 401 assert_response :unauthorized - assert_raises(MiniTest::Assertion) { + assert_raises(Minitest::Assertion) { assert_response :ok } - assert_raises(MiniTest::Assertion) { + assert_raises(Minitest::Assertion) { assert_response :success } end diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb index ba4cffcd3e..b6b5a218cc 100644 --- a/actionpack/test/controller/action_pack_assertions_test.rb +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -444,22 +444,18 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase def test_assert_response_uses_exception_message @controller = AssertResponseWithUnexpectedErrorController.new - get :index + e = assert_raise RuntimeError, 'Expected non-success response' do + get :index + end assert_response :success - flunk 'Expected non-success response' - rescue RuntimeError => e - assert e.message.include?('FAIL') + assert_includes 'FAIL', e.message end def test_assert_response_failure_response_with_no_exception @controller = AssertResponseWithUnexpectedErrorController.new get :show - assert_response :success - flunk 'Expected non-success response' - rescue ActiveSupport::TestCase::Assertion - # success - rescue - flunk "assert_response failed to handle failure response with missing, but optional, exception." + assert_response 500 + assert_equal 'Boom', response.body end end diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb index 3b5d7ef446..d3efca5b6f 100644 --- a/actionpack/test/controller/filters_test.rb +++ b/actionpack/test/controller/filters_test.rb @@ -893,17 +893,6 @@ class ControllerWithFilterInstance < PostsController around_filter YieldingFilter.new, :only => :raises_after end -class ControllerWithFilterMethod < PostsController - class YieldingFilter < DefaultFilter - def around(controller) - yield - raise After - end - end - - around_filter YieldingFilter.new.method(:around), :only => :raises_after -end - class ControllerWithProcFilter < PostsController around_filter(:only => :no_raise) do |c,b| c.instance_variable_set(:"@before", true) diff --git a/actionpack/test/controller/http_digest_authentication_test.rb b/actionpack/test/controller/http_digest_authentication_test.rb index 9f1c168209..52a0bc9aa3 100644 --- a/actionpack/test/controller/http_digest_authentication_test.rb +++ b/actionpack/test/controller/http_digest_authentication_test.rb @@ -21,7 +21,7 @@ class HttpDigestAuthenticationTest < ActionController::TestCase def authenticate authenticate_or_request_with_http_digest("SuperSecret") do |username| - # Return the password + # Returns the password USERS[username] end end diff --git a/actionpack/test/controller/http_token_authentication_test.rb b/actionpack/test/controller/http_token_authentication_test.rb index ebf6d224aa..86b94652ce 100644 --- a/actionpack/test/controller/http_token_authentication_test.rb +++ b/actionpack/test/controller/http_token_authentication_test.rb @@ -21,7 +21,7 @@ class HttpTokenAuthenticationTest < ActionController::TestCase private def authenticate - authenticate_or_request_with_http_token do |token, options| + authenticate_or_request_with_http_token do |token, _| token == 'lifo' end end diff --git a/actionpack/test/controller/log_subscriber_test.rb b/actionpack/test/controller/log_subscriber_test.rb index 075347be52..18037b3d2f 100644 --- a/actionpack/test/controller/log_subscriber_test.rb +++ b/actionpack/test/controller/log_subscriber_test.rb @@ -137,6 +137,17 @@ class ACLogSubscriberTest < ActionController::TestCase assert_equal 'Parameters: {"id"=>"10"}', logs[1] end + def test_multiple_process_with_parameters + get :show, :id => '10' + get :show, :id => '20' + + wait + + assert_equal 6, logs.size + assert_equal 'Parameters: {"id"=>"10"}', logs[1] + assert_equal 'Parameters: {"id"=>"20"}', logs[4] + end + def test_process_action_with_wrapped_parameters @request.env['CONTENT_TYPE'] = 'application/json' post :show, :id => '10', :name => 'jose' diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb index 2b6c8739af..84e4936f31 100644 --- a/actionpack/test/controller/mime/respond_to_test.rb +++ b/actionpack/test/controller/mime/respond_to_test.rb @@ -166,6 +166,86 @@ class RespondToController < ActionController::Base end end + def variant_plus_none_for_format + respond_to do |format| + format.html do |variant| + variant.phone { render text: "phone" } + variant.none + end + end + end + + def variant_inline_syntax + respond_to do |format| + format.js { render text: "js" } + format.html.none { render text: "none" } + format.html.phone { render text: "phone" } + end + end + + def variant_inline_syntax_without_block + respond_to do |format| + format.js + format.html.none + format.html.phone + end + end + + def variant_any + respond_to do |format| + format.html do |variant| + variant.any(:tablet, :phablet){ render text: "any" } + variant.phone { render text: "phone" } + end + end + end + + def variant_any_any + respond_to do |format| + format.html do |variant| + variant.any { render text: "any" } + variant.phone { render text: "phone" } + end + end + end + + def variant_inline_any + respond_to do |format| + format.html.any(:tablet, :phablet){ render text: "any" } + format.html.phone { render text: "phone" } + end + end + + def variant_inline_any_any + respond_to do |format| + format.html.phone { render text: "phone" } + format.html.any { render text: "any" } + end + end + + def variant_any_implicit_render + respond_to do |format| + format.html.phone + format.html.any(:tablet, :phablet) + end + end + + def variant_any_with_none + respond_to do |format| + format.html.any(:none, :phone){ render text: "none or phone" } + end + end + + def format_any_variant_any + respond_to do |format| + format.html { render text: "HTML" } + format.any(:js, :xml) do |variant| + variant.phone{ render text: "phone" } + variant.any(:tablet, :phablet){ render text: "tablet" } + end + end + end + protected def set_layout case action_name @@ -544,4 +624,120 @@ class RespondToControllerTest < ActionController::TestCase assert_equal "text/html", @response.content_type assert_equal "tablet", @response.body end + + def test_no_variant_in_variant_setup + get :variant_plus_none_for_format + assert_equal "text/html", @response.content_type + assert_equal "none", @response.body + end + + def test_variant_inline_syntax + get :variant_inline_syntax, format: :js + assert_equal "text/javascript", @response.content_type + assert_equal "js", @response.body + + get :variant_inline_syntax + assert_equal "text/html", @response.content_type + assert_equal "none", @response.body + + @request.variant = :phone + get :variant_inline_syntax + assert_equal "text/html", @response.content_type + assert_equal "phone", @response.body + end + + def test_variant_inline_syntax_without_block + @request.variant = :phone + get :variant_inline_syntax_without_block + assert_equal "text/html", @response.content_type + assert_equal "phone", @response.body + end + + def test_variant_any + @request.variant = :phone + get :variant_any + assert_equal "text/html", @response.content_type + assert_equal "phone", @response.body + + @request.variant = :tablet + get :variant_any + assert_equal "text/html", @response.content_type + assert_equal "any", @response.body + + @request.variant = :phablet + get :variant_any + assert_equal "text/html", @response.content_type + assert_equal "any", @response.body + end + + def test_variant_any_any + @request.variant = :phone + get :variant_any_any + assert_equal "text/html", @response.content_type + assert_equal "phone", @response.body + + @request.variant = :yolo + get :variant_any_any + assert_equal "text/html", @response.content_type + assert_equal "any", @response.body + end + + def test_variant_inline_any + @request.variant = :phone + get :variant_any + assert_equal "text/html", @response.content_type + assert_equal "phone", @response.body + + @request.variant = :tablet + get :variant_inline_any + assert_equal "text/html", @response.content_type + assert_equal "any", @response.body + + @request.variant = :phablet + get :variant_inline_any + assert_equal "text/html", @response.content_type + assert_equal "any", @response.body + end + + def test_variant_inline_any_any + @request.variant = :phone + get :variant_inline_any_any + assert_equal "text/html", @response.content_type + assert_equal "phone", @response.body + + @request.variant = :yolo + get :variant_inline_any_any + assert_equal "text/html", @response.content_type + assert_equal "any", @response.body + end + + def test_variant_any_implicit_render + @request.variant = :tablet + get :variant_any_implicit_render + assert_equal "text/html", @response.content_type + assert_equal "tablet", @response.body + + @request.variant = :phablet + get :variant_any_implicit_render + assert_equal "text/html", @response.content_type + assert_equal "phablet", @response.body + end + + def test_variant_any_with_none + get :variant_any_with_none + assert_equal "text/html", @response.content_type + assert_equal "none or phone", @response.body + + @request.variant = :phone + get :variant_any_with_none + assert_equal "text/html", @response.content_type + assert_equal "none or phone", @response.body + end + + def test_format_any_variant_any + @request.variant = :tablet + get :format_any_variant_any, format: :js + assert_equal "text/javascript", @response.content_type + assert_equal "tablet", @response.body + end end diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb index b60c5f058d..33a91d72d9 100644 --- a/actionpack/test/controller/parameters/parameters_permit_test.rb +++ b/actionpack/test/controller/parameters/parameters_permit_test.rb @@ -8,9 +8,16 @@ class ParametersPermitTest < ActiveSupport::TestCase end setup do - @params = ActionController::Parameters.new({ person: { - age: "32", name: { first: "David", last: "Heinemeier Hansson" } - }}) + @params = ActionController::Parameters.new( + person: { + age: '32', + name: { + first: 'David', + last: 'Heinemeier Hansson' + }, + addresses: [{city: 'Chicago', state: 'Illinois'}] + } + ) @struct_fields = [] %w(0 1 12).each do |number| @@ -153,6 +160,18 @@ class ParametersPermitTest < ActiveSupport::TestCase assert_equal nil, params[:foo] end + test 'hashes in array values get wrapped' do + params = ActionController::Parameters.new(foo: [{}, {}]) + params[:foo].each do |hash| + assert !hash.permitted? + end + end + + test 'arrays are converted at most once' do + params = ActionController::Parameters.new(foo: [{}]) + assert params[:foo].equal?(params[:foo]) + end + test "fetch doesnt raise ParameterMissing exception if there is a default" do assert_equal "monkey", @params.fetch(:foo, "monkey") assert_equal "monkey", @params.fetch(:foo) { "monkey" } @@ -221,6 +240,7 @@ class ParametersPermitTest < ActiveSupport::TestCase assert @params.permitted? assert @params[:person].permitted? assert @params[:person][:name].permitted? + assert @params[:person][:addresses][0].permitted? end test "permitted takes a default value when Parameters.permit_all_parameters is set" do diff --git a/actionpack/test/controller/parameters/parameters_require_test.rb b/actionpack/test/controller/parameters/parameters_require_test.rb deleted file mode 100644 index bdaba8d2d8..0000000000 --- a/actionpack/test/controller/parameters/parameters_require_test.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'abstract_unit' -require 'action_controller/metal/strong_parameters' - -class ParametersRequireTest < ActiveSupport::TestCase - test "required parameters must be present not merely not nil" do - assert_raises(ActionController::ParameterMissing) do - ActionController::Parameters.new(person: {}).require(:person) - end - end -end diff --git a/actionpack/test/controller/render_js_test.rb b/actionpack/test/controller/render_js_test.rb index f070109b27..d550422a2f 100644 --- a/actionpack/test/controller/render_js_test.rb +++ b/actionpack/test/controller/render_js_test.rb @@ -22,7 +22,7 @@ class RenderJSTest < ActionController::TestCase tests TestController def test_render_vanilla_js - get :render_vanilla_js_hello + xhr :get, :render_vanilla_js_hello assert_equal "alert('hello')", @response.body assert_equal "text/javascript", @response.content_type end diff --git a/actionpack/test/controller/render_json_test.rb b/actionpack/test/controller/render_json_test.rb index 7c0a6bd67e..de8d1cbd9b 100644 --- a/actionpack/test/controller/render_json_test.rb +++ b/actionpack/test/controller/render_json_test.rb @@ -100,13 +100,13 @@ class RenderJsonTest < ActionController::TestCase end def test_render_json_with_callback - get :render_json_hello_world_with_callback + xhr :get, :render_json_hello_world_with_callback assert_equal 'alert({"hello":"world"})', @response.body assert_equal 'text/javascript', @response.content_type end def test_render_json_with_custom_content_type - get :render_json_with_custom_content_type + xhr :get, :render_json_with_custom_content_type assert_equal '{"hello":"world"}', @response.body assert_equal 'text/javascript', @response.content_type end diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index 727db79241..1f5fc06410 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -52,18 +52,36 @@ module RequestForgeryProtectionActions render :inline => "<%= form_for(:some_resource, :remote => true, :authenticity_token => 'external_token') {} %>" end + def same_origin_js + render js: 'foo();' + end + + def negotiate_same_origin + respond_to do |format| + format.js { same_origin_js } + end + end + + def cross_origin_js + same_origin_js + end + + def negotiate_cross_origin + negotiate_same_origin + end + def rescue_action(e) raise e end end # sample controllers class RequestForgeryProtectionControllerUsingResetSession < ActionController::Base include RequestForgeryProtectionActions - protect_from_forgery :only => %w(index meta), :with => :reset_session + protect_from_forgery :only => %w(index meta same_origin_js negotiate_same_origin), :with => :reset_session end class RequestForgeryProtectionControllerUsingException < ActionController::Base include RequestForgeryProtectionActions - protect_from_forgery :only => %w(index meta), :with => :exception + protect_from_forgery :only => %w(index meta same_origin_js negotiate_same_origin), :with => :exception end class RequestForgeryProtectionControllerUsingNullSession < ActionController::Base @@ -201,7 +219,7 @@ module RequestForgeryProtectionTests end def test_should_not_allow_post_without_token_irrespective_of_format - assert_blocked { post :index, :format=>'xml' } + assert_blocked { post :index, format: 'xml' } end def test_should_not_allow_patch_without_token @@ -271,6 +289,48 @@ module RequestForgeryProtectionTests end end + def test_should_only_allow_same_origin_js_get_with_xhr_header + assert_cross_origin_blocked { get :same_origin_js } + assert_cross_origin_blocked { get :same_origin_js, format: 'js' } + assert_cross_origin_blocked do + @request.accept = 'text/javascript' + get :negotiate_same_origin + end + + assert_cross_origin_not_blocked { xhr :get, :same_origin_js } + assert_cross_origin_not_blocked { xhr :get, :same_origin_js, format: 'js' } + assert_cross_origin_not_blocked do + @request.accept = 'text/javascript' + xhr :get, :negotiate_same_origin + end + end + + # Allow non-GET requests since GET is all a remote <script> tag can muster. + def test_should_allow_non_get_js_without_xhr_header + assert_cross_origin_not_blocked { post :same_origin_js, custom_authenticity_token: @token } + assert_cross_origin_not_blocked { post :same_origin_js, format: 'js', custom_authenticity_token: @token } + assert_cross_origin_not_blocked do + @request.accept = 'text/javascript' + post :negotiate_same_origin, custom_authenticity_token: @token + end + end + + def test_should_only_allow_cross_origin_js_get_without_xhr_header_if_protection_disabled + assert_cross_origin_not_blocked { get :cross_origin_js } + assert_cross_origin_not_blocked { get :cross_origin_js, format: 'js' } + assert_cross_origin_not_blocked do + @request.accept = 'text/javascript' + get :negotiate_cross_origin + end + + assert_cross_origin_not_blocked { xhr :get, :cross_origin_js } + assert_cross_origin_not_blocked { xhr :get, :cross_origin_js, format: 'js' } + assert_cross_origin_not_blocked do + @request.accept = 'text/javascript' + xhr :get, :negotiate_cross_origin + end + end + def assert_blocked session[:something_like_user_id] = 1 yield @@ -282,6 +342,16 @@ module RequestForgeryProtectionTests assert_nothing_raised { yield } assert_response :success end + + def assert_cross_origin_blocked + assert_raises(ActionController::InvalidCrossOriginRequest) do + yield + end + end + + def assert_cross_origin_not_blocked + assert_not_blocked { yield } + end end # OK let's get our test on @@ -305,13 +375,13 @@ class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController end end -class NullSessionDummyKeyGenerator - def generate_key(secret) - '03312270731a2ed0d11ed091c2338a06' +class RequestForgeryProtectionControllerUsingNullSessionTest < ActionController::TestCase + class NullSessionDummyKeyGenerator + def generate_key(secret) + '03312270731a2ed0d11ed091c2338a06' + end end -end -class RequestForgeryProtectionControllerUsingNullSessionTest < ActionController::TestCase def setup @request.env[ActionDispatch::Cookies::GENERATOR_KEY] = NullSessionDummyKeyGenerator.new end @@ -375,8 +445,8 @@ end class CustomAuthenticityParamControllerTest < ActionController::TestCase def setup - ActionController::Base.request_forgery_protection_token = :custom_token_name super + ActionController::Base.request_forgery_protection_token = :custom_token_name end def teardown diff --git a/actionpack/test/controller/required_params_test.rb b/actionpack/test/controller/required_params_test.rb index 343d57c300..25d0337212 100644 --- a/actionpack/test/controller/required_params_test.rb +++ b/actionpack/test/controller/required_params_test.rb @@ -25,3 +25,11 @@ class ActionControllerRequiredParamsTest < ActionController::TestCase assert_response :ok end end + +class ParametersRequireTest < ActiveSupport::TestCase + test "required parameters must be present not merely not nil" do + assert_raises(ActionController::ParameterMissing) do + ActionController::Parameters.new(person: {}).require(:person) + end + end +end diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 2c84e95c6e..df453a0251 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -1833,11 +1833,11 @@ class RackMountIntegrationTests < ActiveSupport::TestCase assert_equal({:controller => 'foo', :action => 'id_default', :id => 1 }, @routes.recognize_path('/id_default')) assert_equal({:controller => 'foo', :action => 'get_or_post'}, @routes.recognize_path('/get_or_post', :method => :get)) assert_equal({:controller => 'foo', :action => 'get_or_post'}, @routes.recognize_path('/get_or_post', :method => :post)) - assert_raise(ActionController::ActionControllerError) { @routes.recognize_path('/get_or_post', :method => :put) } - assert_raise(ActionController::ActionControllerError) { @routes.recognize_path('/get_or_post', :method => :delete) } + assert_raise(ActionController::RoutingError) { @routes.recognize_path('/get_or_post', :method => :put) } + assert_raise(ActionController::RoutingError) { @routes.recognize_path('/get_or_post', :method => :delete) } assert_equal({:controller => 'posts', :action => 'index', :optional => 'bar'}, @routes.recognize_path('/optional/bar')) - assert_raise(ActionController::ActionControllerError) { @routes.recognize_path('/optional') } + assert_raise(ActionController::RoutingError) { @routes.recognize_path('/optional') } assert_equal({:controller => 'posts', :action => 'show', :id => '1', :ws => true}, @routes.recognize_path('/ws/posts/show/1', :method => :get)) assert_equal({:controller => 'posts', :action => 'list', :ws => true}, @routes.recognize_path('/ws/posts/list', :method => :get)) @@ -1916,11 +1916,4 @@ class RackMountIntegrationTests < ActiveSupport::TestCase end extras end - - def assert_raise(e) - result = yield - flunk "Did not raise #{e}, but returned #{result.inspect}" - rescue e - assert true - end end diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb index 8ecc1c7d73..4df2f8b98d 100644 --- a/actionpack/test/controller/send_file_test.rb +++ b/actionpack/test/controller/send_file_test.rb @@ -25,6 +25,10 @@ class SendFileController < ActionController::Base end end +class SendFileWithActionControllerLive < SendFileController + include ActionController::Live +end + class SendFileTest < ActionController::TestCase tests SendFileController include TestFileUtils @@ -144,7 +148,7 @@ class SendFileTest < ActionController::TestCase } @controller.headers = {} - assert !@controller.send(:send_file_headers!, options) + assert_raise(ArgumentError) { @controller.send(:send_file_headers!, options) } end def test_send_file_headers_guess_type_from_extension @@ -196,4 +200,14 @@ class SendFileTest < ActionController::TestCase assert_equal 200, @response.status end end + + tests SendFileWithActionControllerLive + + def test_send_file_with_action_controller_live + @controller = SendFileWithActionControllerLive.new + @controller.options = { :content_type => "application/x-ruby" } + + response = process('file') + assert_equal 200, response.status + end end diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index de0476dbde..5ff4a383ec 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -706,6 +706,14 @@ XML assert @request.params[:foo].blank? end + def test_filtered_parameters_reset_between_requests + get :no_op, :foo => "bar" + assert_equal "bar", @request.filtered_parameters[:foo] + + get :no_op, :foo => "baz" + assert_equal "baz", @request.filtered_parameters[:foo] + end + def test_symbolized_path_params_reset_after_request get :test_params, :id => "foo" assert_equal "foo", @request.symbolized_path_parameters[:id] diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index 91ac13e7c6..b19ce905f5 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -379,6 +379,39 @@ class CookiesTest < ActionController::TestCase assert_equal 'bar', cookies.encrypted[:foo] end + class ActionDispatch::Session::CustomJsonSerializer + def self.load(value) + JSON.load(value) + " and loaded" + end + + def self.dump(value) + JSON.dump(value + " was dumped") + end + end + + def test_encrypted_cookie_using_custom_json_serializer + @request.env["action_dispatch.session_serializer"] = :custom_json_serializer + get :set_encrypted_cookie + assert_equal 'bar was dumped and loaded', cookies.encrypted[:foo] + end + + def test_encrypted_cookie_using_serializer_object + @request.env["action_dispatch.session_serializer"] = ActionDispatch::Session::CustomJsonSerializer + get :set_encrypted_cookie + assert_equal 'bar was dumped and loaded', cookies.encrypted[:foo] + end + + def test_encrypted_cookie_using_json_serializer + @request.env["action_dispatch.session_serializer"] = :json_serializer + get :set_encrypted_cookie + cookies = @controller.send :cookies + assert_not_equal 'bar', cookies[:foo] + assert_raises TypeError do + cookies.signed[:foo] + end + assert_equal 'bar', cookies.encrypted[:foo] + end + def test_accessing_nonexistant_encrypted_cookie_should_not_raise_invalid_message get :set_encrypted_cookie assert_nil @controller.send(:cookies).encrypted[:non_existant_attribute] diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb index 8a19129695..981cf2426e 100644 --- a/actionpack/test/dispatch/mime_type_test.rb +++ b/actionpack/test/dispatch/mime_type_test.rb @@ -31,21 +31,21 @@ class MimeTypeTest < ActiveSupport::TestCase test "parse text with trailing star at the beginning" do accept = "text/*, text/html, application/json, multipart/form-data" - expect = [Mime::HTML, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::XML, Mime::YAML, Mime::JSON, Mime::MULTIPART_FORM] + expect = [Mime::HTML, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::VCF, Mime::XML, Mime::YAML, Mime::JSON, Mime::MULTIPART_FORM] parsed = Mime::Type.parse(accept) assert_equal expect, parsed end test "parse text with trailing star in the end" do accept = "text/html, application/json, multipart/form-data, text/*" - expect = [Mime::HTML, Mime::JSON, Mime::MULTIPART_FORM, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::XML, Mime::YAML] + expect = [Mime::HTML, Mime::JSON, Mime::MULTIPART_FORM, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::VCF, Mime::XML, Mime::YAML] parsed = Mime::Type.parse(accept) assert_equal expect, parsed end test "parse text with trailing star" do accept = "text/*" - expect = [Mime::HTML, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::XML, Mime::YAML, Mime::JSON] + expect = [Mime::HTML, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::VCF, Mime::XML, Mime::YAML, Mime::JSON] parsed = Mime::Type.parse(accept) assert_equal expect, parsed end diff --git a/actionpack/test/dispatch/mount_test.rb b/actionpack/test/dispatch/mount_test.rb index 30e95a0b75..cdf00d84fb 100644 --- a/actionpack/test/dispatch/mount_test.rb +++ b/actionpack/test/dispatch/mount_test.rb @@ -5,7 +5,7 @@ class TestRoutingMount < ActionDispatch::IntegrationTest class FakeEngine def self.routes - Object.new + @routes ||= ActionDispatch::Routing::RouteSet.new end def self.call(env) @@ -27,15 +27,21 @@ class TestRoutingMount < ActionDispatch::IntegrationTest scope "/its_a" do mount SprocketsApp, :at => "/sprocket" end + + resources :users do + mount FakeEngine, :at => "/fakeengine", :as => :fake_mounted_at_resource + end end def app Router end - def test_trailing_slash_is_not_removed_from_path_info - get "/sprockets/omg/" - assert_equal "/sprockets -- /omg/", response.body + def test_app_name_is_properly_generated_when_engine_is_mounted_in_resources + assert Router.mounted_helpers.method_defined?(:user_fake_mounted_at_resource), + "A mounted helper should be defined with a parent's prefix" + assert Router.named_routes.routes[:user_fake_mounted_at_resource], + "A named route should be defined with a parent's prefix" end def test_mounting_sets_script_name diff --git a/actionpack/test/dispatch/prefix_generation_test.rb b/actionpack/test/dispatch/prefix_generation_test.rb index e519fff51e..08501d19c0 100644 --- a/actionpack/test/dispatch/prefix_generation_test.rb +++ b/actionpack/test/dispatch/prefix_generation_test.rb @@ -32,12 +32,18 @@ module TestGenerationPrefix get "/conflicting_url", :to => "inside_engine_generating#conflicting" get "/foo", :to => "never#invoked", :as => :named_helper_that_should_be_invoked_only_in_respond_to_test - get "/relative_path_redirect", :to => redirect("foo") + get "/relative_path_root", :to => redirect("") + get "/relative_path_redirect", :to => redirect("foo") + get "/relative_option_root", :to => redirect(:path => "") get "/relative_option_redirect", :to => redirect(:path => "foo") + get "/relative_custom_root", :to => redirect { |params, request| "" } get "/relative_custom_redirect", :to => redirect { |params, request| "foo" } - get "/absolute_path_redirect", :to => redirect("/foo") + get "/absolute_path_root", :to => redirect("/") + get "/absolute_path_redirect", :to => redirect("/foo") + get "/absolute_option_root", :to => redirect(:path => "/") get "/absolute_option_redirect", :to => redirect(:path => "/foo") + get "/absolute_custom_root", :to => redirect { |params, request| "/" } get "/absolute_custom_redirect", :to => redirect { |params, request| "/foo" } end @@ -190,46 +196,64 @@ module TestGenerationPrefix assert_equal "engine", last_response.body end + test "[ENGINE] relative path root uses SCRIPT_NAME from request" do + get "/awesome/blog/relative_path_root" + verify_redirect "http://example.org/awesome/blog" + end + test "[ENGINE] relative path redirect uses SCRIPT_NAME from request" do get "/awesome/blog/relative_path_redirect" - assert_equal 301, last_response.status - assert_equal "http://example.org/awesome/blog/foo", last_response.headers["Location"] - assert_equal %(<html><body>You are being <a href="http://example.org/awesome/blog/foo">redirected</a>.</body></html>), last_response.body + verify_redirect "http://example.org/awesome/blog/foo" + end + + test "[ENGINE] relative option root uses SCRIPT_NAME from request" do + get "/awesome/blog/relative_option_root" + verify_redirect "http://example.org/awesome/blog" end test "[ENGINE] relative option redirect uses SCRIPT_NAME from request" do get "/awesome/blog/relative_option_redirect" - assert_equal 301, last_response.status - assert_equal "http://example.org/awesome/blog/foo", last_response.headers["Location"] - assert_equal %(<html><body>You are being <a href="http://example.org/awesome/blog/foo">redirected</a>.</body></html>), last_response.body + verify_redirect "http://example.org/awesome/blog/foo" + end + + test "[ENGINE] relative custom root uses SCRIPT_NAME from request" do + get "/awesome/blog/relative_custom_root" + verify_redirect "http://example.org/awesome/blog" end test "[ENGINE] relative custom redirect uses SCRIPT_NAME from request" do get "/awesome/blog/relative_custom_redirect" - assert_equal 301, last_response.status - assert_equal "http://example.org/awesome/blog/foo", last_response.headers["Location"] - assert_equal %(<html><body>You are being <a href="http://example.org/awesome/blog/foo">redirected</a>.</body></html>), last_response.body + verify_redirect "http://example.org/awesome/blog/foo" + end + + test "[ENGINE] absolute path root doesn't use SCRIPT_NAME from request" do + get "/awesome/blog/absolute_path_root" + verify_redirect "http://example.org/" end test "[ENGINE] absolute path redirect doesn't use SCRIPT_NAME from request" do get "/awesome/blog/absolute_path_redirect" - assert_equal 301, last_response.status - assert_equal "http://example.org/foo", last_response.headers["Location"] - assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + verify_redirect "http://example.org/foo" + end + + test "[ENGINE] absolute option root doesn't use SCRIPT_NAME from request" do + get "/awesome/blog/absolute_option_root" + verify_redirect "http://example.org/" end test "[ENGINE] absolute option redirect doesn't use SCRIPT_NAME from request" do get "/awesome/blog/absolute_option_redirect" - assert_equal 301, last_response.status - assert_equal "http://example.org/foo", last_response.headers["Location"] - assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + verify_redirect "http://example.org/foo" + end + + test "[ENGINE] absolute custom root doesn't use SCRIPT_NAME from request" do + get "/awesome/blog/absolute_custom_root" + verify_redirect "http://example.org/" end test "[ENGINE] absolute custom redirect doesn't use SCRIPT_NAME from request" do get "/awesome/blog/absolute_custom_redirect" - assert_equal 301, last_response.status - assert_equal "http://example.org/foo", last_response.headers["Location"] - assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + verify_redirect "http://example.org/foo" end # Inside Application @@ -320,6 +344,17 @@ module TestGenerationPrefix path = engine_object.polymorphic_url(Post.new, :host => "www.example.com") assert_equal "http://www.example.com/awesome/blog/posts/1", path end + + private + def verify_redirect(url, status = 301) + assert_equal status, last_response.status + assert_equal url, last_response.headers["Location"] + assert_equal expected_redirect_body(url), last_response.body + end + + def expected_redirect_body(url) + %(<html><body>You are being <a href="#{url}">redirected</a>.</body></html>) + end end class EngineMountedAtRoot < ActionDispatch::IntegrationTest @@ -332,12 +367,18 @@ module TestGenerationPrefix routes.draw do get "/posts/:id", :to => "posts#show", :as => :post - get "/relative_path_redirect", :to => redirect("foo") + get "/relative_path_root", :to => redirect("") + get "/relative_path_redirect", :to => redirect("foo") + get "/relative_option_root", :to => redirect(:path => "") get "/relative_option_redirect", :to => redirect(:path => "foo") + get "/relative_custom_root", :to => redirect { |params, request| "" } get "/relative_custom_redirect", :to => redirect { |params, request| "foo" } - get "/absolute_path_redirect", :to => redirect("/foo") + get "/absolute_path_root", :to => redirect("/") + get "/absolute_path_redirect", :to => redirect("/foo") + get "/absolute_option_root", :to => redirect(:path => "/") get "/absolute_option_redirect", :to => redirect(:path => "/foo") + get "/absolute_custom_root", :to => redirect { |params, request| "/" } get "/absolute_custom_redirect", :to => redirect { |params, request| "/foo" } end @@ -390,46 +431,75 @@ module TestGenerationPrefix assert_equal "/posts/1", last_response.body end + test "[ENGINE] relative path root uses SCRIPT_NAME from request" do + get "/relative_path_root" + verify_redirect "http://example.org/" + end + test "[ENGINE] relative path redirect uses SCRIPT_NAME from request" do get "/relative_path_redirect" - assert_equal 301, last_response.status - assert_equal "http://example.org/foo", last_response.headers["Location"] - assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + verify_redirect "http://example.org/foo" + end + + test "[ENGINE] relative option root uses SCRIPT_NAME from request" do + get "/relative_option_root" + verify_redirect "http://example.org/" end test "[ENGINE] relative option redirect uses SCRIPT_NAME from request" do get "/relative_option_redirect" - assert_equal 301, last_response.status - assert_equal "http://example.org/foo", last_response.headers["Location"] - assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + verify_redirect "http://example.org/foo" + end + + test "[ENGINE] relative custom root uses SCRIPT_NAME from request" do + get "/relative_custom_root" + verify_redirect "http://example.org/" end test "[ENGINE] relative custom redirect uses SCRIPT_NAME from request" do get "/relative_custom_redirect" - assert_equal 301, last_response.status - assert_equal "http://example.org/foo", last_response.headers["Location"] - assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + verify_redirect "http://example.org/foo" + end + + test "[ENGINE] absolute path root doesn't use SCRIPT_NAME from request" do + get "/absolute_path_root" + verify_redirect "http://example.org/" end test "[ENGINE] absolute path redirect doesn't use SCRIPT_NAME from request" do get "/absolute_path_redirect" - assert_equal 301, last_response.status - assert_equal "http://example.org/foo", last_response.headers["Location"] - assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + verify_redirect "http://example.org/foo" + end + + test "[ENGINE] absolute option root doesn't use SCRIPT_NAME from request" do + get "/absolute_option_root" + verify_redirect "http://example.org/" end test "[ENGINE] absolute option redirect doesn't use SCRIPT_NAME from request" do get "/absolute_option_redirect" - assert_equal 301, last_response.status - assert_equal "http://example.org/foo", last_response.headers["Location"] - assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + verify_redirect "http://example.org/foo" + end + + test "[ENGINE] absolute custom root doesn't use SCRIPT_NAME from request" do + get "/absolute_custom_root" + verify_redirect "http://example.org/" end test "[ENGINE] absolute custom redirect doesn't use SCRIPT_NAME from request" do get "/absolute_custom_redirect" - assert_equal 301, last_response.status - assert_equal "http://example.org/foo", last_response.headers["Location"] - assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body - end + verify_redirect "http://example.org/foo" + end + + private + def verify_redirect(url, status = 301) + assert_equal status, last_response.status + assert_equal url, last_response.headers["Location"] + assert_equal expected_redirect_body(url), last_response.body + end + + def expected_redirect_body(url) + %(<html><body>You are being <a href="#{url}">redirected</a>.</body></html>) + end end end diff --git a/actionpack/test/dispatch/request/json_params_parsing_test.rb b/actionpack/test/dispatch/request/json_params_parsing_test.rb index dba9ab688f..c609075e6b 100644 --- a/actionpack/test/dispatch/request/json_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/json_params_parsing_test.rb @@ -23,6 +23,13 @@ class JsonParamsParsingTest < ActionDispatch::IntegrationTest ) end + test "parses boolean and number json params for application json" do + assert_parses( + {"item" => {"enabled" => false, "count" => 10}}, + "{\"item\": {\"enabled\": false, \"count\": 10}}", { 'CONTENT_TYPE' => 'application/json' } + ) + end + test "parses json params for application jsonrequest" do assert_parses( {"person" => {"name" => "David"}}, diff --git a/actionpack/test/dispatch/request/query_string_parsing_test.rb b/actionpack/test/dispatch/request/query_string_parsing_test.rb index 0ad0dbc958..d82493140f 100644 --- a/actionpack/test/dispatch/request/query_string_parsing_test.rb +++ b/actionpack/test/dispatch/request/query_string_parsing_test.rb @@ -104,6 +104,21 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest assert_parses({"action" => ['1']}, "action[]=1&action[]") end + test "perform_deep_munge" do + ActionDispatch::Request::Utils.perform_deep_munge = false + begin + assert_parses({"action" => nil}, "action") + assert_parses({"action" => {"foo" => nil}}, "action[foo]") + assert_parses({"action" => {"foo" => {"bar" => nil}}}, "action[foo][bar]") + assert_parses({"action" => {"foo" => {"bar" => [nil]}}}, "action[foo][bar][]") + assert_parses({"action" => {"foo" => [nil]}}, "action[foo][]") + assert_parses({"action" => {"foo" => [{"bar" => nil}]}}, "action[foo][][bar]") + assert_parses({"action" => ['1',nil]}, "action[]=1&action[]") + ensure + ActionDispatch::Request::Utils.perform_deep_munge = true + end + end + test "query string with empty key" do assert_parses( { "action" => "create_customer", "full_name" => "David Heinemeier Hansson" }, diff --git a/actionpack/test/dispatch/request/session_test.rb b/actionpack/test/dispatch/request/session_test.rb index a244d1364c..df55fcc8bc 100644 --- a/actionpack/test/dispatch/request/session_test.rb +++ b/actionpack/test/dispatch/request/session_test.rb @@ -68,13 +68,12 @@ module ActionDispatch assert_equal '1', session.fetch(:one) assert_equal '2', session.fetch(:two, '2') - assert_equal '2', session.fetch(:two) + assert_nil session.fetch(:two, nil) assert_equal 'three', session.fetch(:three) {|el| el.to_s } - assert_equal 'three', session.fetch(:three) assert_raise KeyError do - session.fetch(:four) + session.fetch(:three) end end diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index b308c5749a..f79fe47897 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -606,7 +606,7 @@ class RequestTest < ActiveSupport::TestCase 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest" request.expects(:parameters).at_least_once.returns({}) assert_equal [Mime::JS], request.formats - + request = stub_request 'CONTENT_TYPE' => 'application/xml; charset=UTF-8', 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest" request.expects(:parameters).at_least_once.returns({}) @@ -624,10 +624,10 @@ class RequestTest < ActiveSupport::TestCase test "format is not nil with unknown format" do request = stub_request request.expects(:parameters).at_least_once.returns({ format: :hello }) - assert_equal request.format.nil?, true - assert_equal request.format.html?, false - assert_equal request.format.xml?, false - assert_equal request.format.json?, false + assert request.format.nil? + assert_not request.format.html? + assert_not request.format.xml? + assert_not request.format.json? end test "formats with xhr request" do diff --git a/actionpack/test/dispatch/routing/inspector_test.rb b/actionpack/test/dispatch/routing/inspector_test.rb index c8038bbd7c..ff33dd5652 100644 --- a/actionpack/test/dispatch/routing/inspector_test.rb +++ b/actionpack/test/dispatch/routing/inspector_test.rb @@ -54,6 +54,27 @@ module ActionDispatch ], output end + def test_displaying_routes_for_engines_without_routes + engine = Class.new(Rails::Engine) do + def self.inspect + "Blog::Engine" + end + end + engine.routes.draw do + end + + output = draw do + mount engine => "/blog", as: "blog" + end + + assert_equal [ + "Prefix Verb URI Pattern Controller#Action", + " blog /blog Blog::Engine", + "", + "Routes for Blog::Engine:" + ], output + end + def test_cart_inspect output = draw do get '/cart', :to => 'cart#show' @@ -160,6 +181,29 @@ module ActionDispatch ], output end + def test_rake_routes_shows_routes_with_dashes + output = draw do + get 'about-us' => 'pages#about_us' + get 'our-work/latest' + + resources :photos, only: [:show] do + get 'user-favorites', on: :collection + get 'preview-photo', on: :member + get 'summary-text' + end + end + + assert_equal [ + " Prefix Verb URI Pattern Controller#Action", + " about_us GET /about-us(.:format) pages#about_us", + " our_work_latest GET /our-work/latest(.:format) our_work#latest", + "user_favorites_photos GET /photos/user-favorites(.:format) photos#user_favorites", + " preview_photo_photo GET /photos/:id/preview-photo(.:format) photos#preview_photo", + " photo_summary_text GET /photos/:photo_id/summary-text(.:format) photos#summary_text", + " photo GET /photos/:id(.:format) photos#show" + ], output + end + class RackApp def self.call(env) end @@ -203,6 +247,18 @@ module ActionDispatch assert_no_match(/\/sprockets/, output.first) end + def test_rake_routes_shows_route_defined_in_under_assets_prefix + output = draw do + scope '/sprockets' do + get '/foo' => 'foo#bar' + end + end + assert_equal [ + "Prefix Verb URI Pattern Controller#Action", + " foo GET /sprockets/foo(.:format) foo#bar" + ], output + end + def test_redirect output = draw do get "/foo" => redirect("/foo/bar"), :constraints => { :subdomain => "admin" } diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index aac808afda..26821bdb56 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -2864,6 +2864,100 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert !@request.params[:action].frozen? end + def test_multiple_positional_args_with_the_same_name + draw do + get '/downloads/:id/:id.tar' => 'downloads#show', as: :download, format: false + end + + expected_params = { + controller: 'downloads', + action: 'show', + id: '1' + } + + get '/downloads/1/1.tar' + assert_equal 'downloads#show', @response.body + assert_equal expected_params, @request.symbolized_path_parameters + assert_equal '/downloads/1/1.tar', download_path('1') + assert_equal '/downloads/1/1.tar', download_path('1', '1') + end + + def test_absolute_controller_namespace + draw do + namespace :foo do + get '/', to: '/bar#index', as: 'root' + end + end + + get '/foo' + assert_equal 'bar#index', @response.body + assert_equal '/foo', foo_root_path + end + + def test_trailing_slash + draw do + resources :streams + end + + get '/streams' + assert @response.ok?, 'route without trailing slash should work' + + get '/streams/' + assert @response.ok?, 'route with trailing slash should work' + + get '/streams?foobar' + assert @response.ok?, 'route without trailing slash and with QUERY_STRING should work' + + get '/streams/?foobar' + assert @response.ok?, 'route with trailing slash and with QUERY_STRING should work' + end + + def test_route_with_dashes_in_path + draw do + get '/contact-us', to: 'pages#contact_us' + end + + get '/contact-us' + assert_equal 'pages#contact_us', @response.body + assert_equal '/contact-us', contact_us_path + end + + def test_shorthand_route_with_dashes_in_path + draw do + get '/about-us/index' + end + + get '/about-us/index' + assert_equal 'about_us#index', @response.body + assert_equal '/about-us/index', about_us_index_path + end + + def test_resource_routes_with_dashes_in_path + draw do + resources :photos, only: [:show] do + get 'user-favorites', on: :collection + get 'preview-photo', on: :member + get 'summary-text' + end + end + + get '/photos/user-favorites' + assert_equal 'photos#user_favorites', @response.body + assert_equal '/photos/user-favorites', user_favorites_photos_path + + get '/photos/1/preview-photo' + assert_equal 'photos#preview_photo', @response.body + assert_equal '/photos/1/preview-photo', preview_photo_photo_path('1') + + get '/photos/1/summary-text' + assert_equal 'photos#summary_text', @response.body + assert_equal '/photos/1/summary-text', photo_summary_text_path('1') + + get '/photos/1' + assert_equal 'photos#show', @response.body + assert_equal '/photos/1', photo_path('1') + end + private def draw(&block) @@ -3327,6 +3421,8 @@ class TestOptimizedNamedRoutes < ActionDispatch::IntegrationTest ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } get '/foo' => ok, as: :foo get '/post(/:action(/:id))' => ok, as: :posts + get '/:foo/:foo_type/bars/:id' => ok, as: :bar + get '/projects/:id.:format' => ok, as: :project end end @@ -3349,6 +3445,16 @@ class TestOptimizedNamedRoutes < ActionDispatch::IntegrationTest assert_equal '/post', Routes.url_helpers.posts_path assert_equal '/post', posts_path end + + test 'segments with same prefix are replaced correctly' do + assert_equal '/foo/baz/bars/1', Routes.url_helpers.bar_path('foo', 'baz', '1') + assert_equal '/foo/baz/bars/1', bar_path('foo', 'baz', '1') + end + + test 'segments separated with a period are replaced correctly' do + assert_equal '/projects/1.json', Routes.url_helpers.project_path(1, :json) + assert_equal '/projects/1.json', project_path(1, :json) + end end class TestNamedRouteUrlHelpers < ActionDispatch::IntegrationTest @@ -3696,3 +3802,28 @@ class TestRedirectRouteGeneration < ActionDispatch::IntegrationTest end end end + +class TestUrlGenerationErrors < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + get "/products/:id" => 'products#show', :as => :product + end + end + + def app; Routes end + + include Routes.url_helpers + + test "url helpers raise a helpful error message whem generation fails" do + url, missing = { action: 'show', controller: 'products', id: nil }, [:id] + message = "No route matches #{url.inspect} missing required keys: #{missing.inspect}" + + # Optimized url helper + error = assert_raises(ActionController::UrlGenerationError){ product_path(nil) } + assert_equal message, error.message + + # Non-optimized url helper + error = assert_raises(ActionController::UrlGenerationError, message){ product_path(id: nil) } + assert_equal message, error.message + end +end diff --git a/actionpack/test/dispatch/static_test.rb b/actionpack/test/dispatch/static_test.rb index acccbcb2e6..5bd1806b21 100644 --- a/actionpack/test/dispatch/static_test.rb +++ b/actionpack/test/dispatch/static_test.rb @@ -37,10 +37,8 @@ module StaticTests end def test_served_static_file_with_non_english_filename - if RUBY_ENGINE == 'jruby ' - skip "Stop skipping if following bug gets fixed: " \ + jruby_skip "Stop skipping if following bug gets fixed: " \ "http://jira.codehaus.org/browse/JRUBY-7192" - end assert_html "means hello in Japanese\n", get("/foo/#{Rack::Utils.escape("こんにちは.html")}") end @@ -137,7 +135,7 @@ module StaticTests end def with_static_file(file) - path = "#{FIXTURE_LOAD_PATH}/public" + file + path = "#{FIXTURE_LOAD_PATH}/#{public_path}" + file File.open(path, "wb+") { |f| f.write(file) } yield file ensure @@ -149,11 +147,24 @@ class StaticTest < ActiveSupport::TestCase DummyApp = lambda { |env| [200, {"Content-Type" => "text/plain"}, ["Hello, World!"]] } - App = ActionDispatch::Static.new(DummyApp, "#{FIXTURE_LOAD_PATH}/public", "public, max-age=60") def setup - @app = App + @app = ActionDispatch::Static.new(DummyApp, "#{FIXTURE_LOAD_PATH}/public", "public, max-age=60") + end + + def public_path + "public" end include StaticTests end + +class StaticEncodingTest < StaticTest + def setup + @app = ActionDispatch::Static.new(DummyApp, "#{FIXTURE_LOAD_PATH}/公共", "public, max-age=60") + end + + def public_path + "公共" + end +end diff --git a/actionpack/test/dispatch/url_generation_test.rb b/actionpack/test/dispatch/url_generation_test.rb index f919592d24..fdea27e2d2 100644 --- a/actionpack/test/dispatch/url_generation_test.rb +++ b/actionpack/test/dispatch/url_generation_test.rb @@ -66,7 +66,7 @@ module TestUrlGeneration assert_equal "http://www.example.com:8080/foo", foo_url(host: "www.example.com:8080", protocol: "http://") end - test "port option overides the host" do + 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) end diff --git a/actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+phablet.erb b/actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+phablet.erb new file mode 100644 index 0000000000..e905d051bf --- /dev/null +++ b/actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+phablet.erb @@ -0,0 +1 @@ +phablet
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+tablet.erb b/actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+tablet.erb new file mode 100644 index 0000000000..65526af8cf --- /dev/null +++ b/actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+tablet.erb @@ -0,0 +1 @@ +tablet
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_to/variant_inline_syntax_without_block.html+phone.erb b/actionpack/test/fixtures/respond_to/variant_inline_syntax_without_block.html+phone.erb new file mode 100644 index 0000000000..cd222a4a49 --- /dev/null +++ b/actionpack/test/fixtures/respond_to/variant_inline_syntax_without_block.html+phone.erb @@ -0,0 +1 @@ +phone
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_to/variant_plus_none_for_format.html.erb b/actionpack/test/fixtures/respond_to/variant_plus_none_for_format.html.erb new file mode 100644 index 0000000000..c86c3f3551 --- /dev/null +++ b/actionpack/test/fixtures/respond_to/variant_plus_none_for_format.html.erb @@ -0,0 +1 @@ +none
\ No newline at end of file diff --git a/actionpack/test/fixtures/公共/foo/bar.html b/actionpack/test/fixtures/公共/foo/bar.html new file mode 100644 index 0000000000..9a35646205 --- /dev/null +++ b/actionpack/test/fixtures/公共/foo/bar.html @@ -0,0 +1 @@ +/foo/bar.html
\ No newline at end of file diff --git a/actionpack/test/fixtures/公共/foo/baz.css b/actionpack/test/fixtures/公共/foo/baz.css new file mode 100644 index 0000000000..b5173fbef2 --- /dev/null +++ b/actionpack/test/fixtures/公共/foo/baz.css @@ -0,0 +1,3 @@ +body { +background: #000; +} diff --git a/actionpack/test/fixtures/公共/foo/index.html b/actionpack/test/fixtures/公共/foo/index.html new file mode 100644 index 0000000000..497a2e898f --- /dev/null +++ b/actionpack/test/fixtures/公共/foo/index.html @@ -0,0 +1 @@ +/foo/index.html
\ No newline at end of file diff --git a/actionpack/test/fixtures/公共/foo/こんにちは.html b/actionpack/test/fixtures/公共/foo/こんにちは.html new file mode 100644 index 0000000000..1df9166522 --- /dev/null +++ b/actionpack/test/fixtures/公共/foo/こんにちは.html @@ -0,0 +1 @@ +means hello in Japanese diff --git a/actionpack/test/fixtures/公共/index.html b/actionpack/test/fixtures/公共/index.html new file mode 100644 index 0000000000..525950ba6b --- /dev/null +++ b/actionpack/test/fixtures/公共/index.html @@ -0,0 +1 @@ +/index.html
\ No newline at end of file diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 9e58c193b1..960f867d99 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,74 @@ +* Added `config.action_view.raise_on_missing_translations` to define whether an + error should be raised for missing translations. + + Fixes #13196 + + *Kassio Borges* + +* Improved ERB dependency detection. New argument types and formattings for the `render` + calls can be matched. + + Fixes #13074 and #13116 + + *João Britto* + +* Use `display:none` instead of `display:inline` for hidden fields + + Fixes #6403 + + *Gaelian Ditchburn* + +* The `video_tag` helper accepts a number as `:size` + + The `:size` option of the `video_tag` helper now can be specified + with a stringified number. The `width` and `height` attributes of + the generated tag will be the same. + + *Kuldeep Aggarwal* + +* A Cycle object should accept an array and cycle through it as it would with a set of + comma-separated objects. + + arr = [1,2,3] + cycle(arr) # => '1' + cycle(arr) # => '2' + cycle(arr) # => '3' + + Previously, it would return the array as a string, because it took the array as a + single object: + + arr = [1,2,3] + cycle(arr) # => '[1,2,3]' + cycle(arr) # => '[1,2,3]' + cycle(arr) # => '[1,2,3]' + + *Kristian Freeman* + +* Label tags generated by collection helpers only inherit the `:index` and + `:namespace` from the input, because only these attributes modifies the + `for` attribute of the label. Also, the input attributes don't have + precedence over the label attributes anymore. + + Before: + + collection = [[1, true, { class: 'foo' }]] + f.collection_check_boxes :options, collection, :second, :first do |b| + b.label(class: 'my_custom_class') + end + + # => <label class="foo" for="user_active_true">1</label> + + After: + + collection = [[1, true, { class: 'foo' }]] + f.collection_check_boxes :options, collection, :second, :first do |b| + b.label(class: 'my_custom_class') + end + + # => <label class="my_custom_class" for="user_active_true">1</label> + + *Andriel Nuernberg* + * Fixed a long-standing bug in `json_escape` that causes quotation marks to be stripped. This method also escapes the \u2028 and \u2029 unicode newline characters which are treated as \n in JavaScript. This matches the behaviour of the AS::JSON encoder. (The diff --git a/actionview/MIT-LICENSE b/actionview/MIT-LICENSE index 5c668d9624..d58dd9ed9b 100644 --- a/actionview/MIT-LICENSE +++ b/actionview/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2013 David Heinemeier Hansson +Copyright (c) 2004-2014 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionview/actionview.gemspec b/actionview/actionview.gemspec index 567e45e14f..e45dd04225 100644 --- a/actionview/actionview.gemspec +++ b/actionview/actionview.gemspec @@ -21,7 +21,7 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', version - s.add_dependency 'builder', '~> 3.1.0' + s.add_dependency 'builder', '~> 3.1' s.add_dependency 'erubis', '~> 2.7.0' s.add_development_dependency 'actionpack', version diff --git a/actionview/lib/action_view.rb b/actionview/lib/action_view.rb index 810d82e89b..5c729345dc 100644 --- a/actionview/lib/action_view.rb +++ b/actionview/lib/action_view.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2013 David Heinemeier Hansson +# Copyright (c) 2004-2014 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -23,6 +23,7 @@ require 'active_support' require 'active_support/rails' +require 'action_view/version' module ActionView extend ActiveSupport::Autoload diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb index 8eb7072d0c..455ce531ae 100644 --- a/actionview/lib/action_view/base.rb +++ b/actionview/lib/action_view/base.rb @@ -153,6 +153,10 @@ module ActionView #:nodoc: # Specify default_formats that can be rendered. cattr_accessor :default_formats + # Specify whether an error should be raised for missing translations + cattr_accessor :raise_on_missing_translations + @@raise_on_missing_translations = false + class_attribute :_routes class_attribute :logger diff --git a/actionview/lib/action_view/dependency_tracker.rb b/actionview/lib/action_view/dependency_tracker.rb index b2e8334077..0ccf2515c5 100644 --- a/actionview/lib/action_view/dependency_tracker.rb +++ b/actionview/lib/action_view/dependency_tracker.rb @@ -1,7 +1,7 @@ require 'thread_safe' module ActionView - class DependencyTracker + class DependencyTracker # :nodoc: @trackers = ThreadSafe::Cache.new def self.find_dependencies(name, template) @@ -23,24 +23,52 @@ module ActionView @trackers.delete(handler) end - class ERBTracker + class ERBTracker # :nodoc: EXPLICIT_DEPENDENCY = /# Template Dependency: (\S+)/ + # A valid ruby identifier - suitable for class, method and specially variable names + IDENTIFIER = / + [[:alpha:]_] # at least one uppercase letter, lowercase letter or underscore + [[:word:]]* # followed by optional letters, numbers or underscores + /x + + # Any kind of variable name. e.g. @instance, @@class, $global or local. + # Possibly following a method call chain + VARIABLE_OR_METHOD_CHAIN = / + (?:\$|@{1,2})? # optional global, instance or class variable indicator + (?:#{IDENTIFIER}\.)* # followed by an optional chain of zero-argument method calls + (?<dynamic>#{IDENTIFIER}) # and a final valid identifier, captured as DYNAMIC + /x + + # A simple string literal. e.g. "School's out!" + STRING = / + (?<quote>['"]) # an opening quote + (?<static>.*?) # with anything inside, captured as STATIC + \k<quote> # and a matching closing quote + /x + + # Part of any hash containing the :partial key + PARTIAL_HASH_KEY = / + (?:\bpartial:|:partial\s*=>) # partial key in either old or new style hash syntax + \s* # followed by optional spaces + /x + # Matches: - # render partial: "comments/comment", collection: commentable.comments - # render "comments/comments" - # render 'comments/comments' - # render('comments/comments') + # partial: "comments/comment", collection: @all_comments => "comments/comment" + # (object: @single_comment, partial: "comments/comment") => "comments/comment" # - # render(@topic) => render("topics/topic") - # render(topics) => render("topics/topic") - # render(message.topics) => render("topics/topic") - RENDER_DEPENDENCY = / - render\s* # render, followed by optional whitespace - \(? # start an optional parenthesis for the render call - (partial:|:partial\s+=>)?\s* # naming the partial, used with collection -- 1st capture - ([@a-z"'][@\w\/\."']+) # the template name itself -- 2nd capture - /x + # "comments/comments" + # 'comments/comments' + # ('comments/comments') + # + # (@topic) => "topics/topic" + # topics => "topics/topic" + # (message.topics) => "topics/topic" + RENDER_ARGUMENTS = /\A + (?:\s*\(?\s*) # optional opening paren surrounded by spaces + (?:.*?#{PARTIAL_HASH_KEY})? # optional hash, up to the partial key declaration + (?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest + /xm def self.call(name, template) new(name, template).dependencies @@ -68,19 +96,33 @@ module ActionView end def render_dependencies - source.scan(RENDER_DEPENDENCY). - collect(&:second).uniq. + render_dependencies = [] + render_calls = source.split(/\brender\b/).drop(1) - # render(@topic) => render("topics/topic") - # render(topics) => render("topics/topic") - # render(message.topics) => render("topics/topic") - collect { |name| name.sub(/\A@?([a-z_]+\.)*([a-z_]+)\z/) { "#{$2.pluralize}/#{$2.singularize}" } }. + render_calls.each do |arguments| + arguments.scan(RENDER_ARGUMENTS) do + add_dynamic_dependency(render_dependencies, Regexp.last_match[:dynamic]) + add_static_dependency(render_dependencies, Regexp.last_match[:static]) + end + end - # render("headline") => render("message/headline") - collect { |name| name.include?("/") ? name : "#{directory}/#{name}" }. + render_dependencies.uniq + end + + def add_dynamic_dependency(dependencies, dependency) + if dependency + dependencies << "#{dependency.pluralize}/#{dependency.singularize}" + end + end - # replace quotes from string renders - collect { |name| name.gsub(/["']/, "") } + def add_static_dependency(dependencies, dependency) + if dependency + if dependency.include?('/') + dependencies << dependency + else + dependencies << "#{directory}/#{dependency}" + end + end end def explicit_dependencies diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index 163d01c2eb..aa49f1edc1 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -176,7 +176,7 @@ module ActionView # ==== Options # # You can add HTML attributes using the +options+. The +options+ supports - # three additional keys for convenience and conformance: + # two additional keys for convenience and conformance: # # * <tt>:alt</tt> - If no alt text is given, the file name part of the # +source+ is used (capitalized and without the extension) @@ -207,14 +207,7 @@ module ActionView options[:alt] = options.fetch(:alt){ image_alt(src) } end - if size = options.delete(:size) - if size =~ %r{\A\d+x\d+\z} - options[:width], options[:height] = size.split('x') - elsif size =~ %r{\A\d+\z} - options[:width] = options[:height] = size - end - end - + options[:width], options[:height] = extract_dimensions(options.delete(:size)) if options[:size] tag("img", options) end @@ -251,9 +244,9 @@ module ActionView # # * <tt>:poster</tt> - Set an image (like a screenshot) to be shown # before the video loads. The path is calculated like the +src+ of +image_tag+. - # * <tt>:size</tt> - Supplied as "{Width}x{Height}", so "30x45" becomes - # width="30" and height="45". <tt>:size</tt> will be ignored if the - # value is not in the correct format. + # * <tt>:size</tt> - Supplied as "{Width}x{Height}" or "{Number}", so "30x45" becomes + # width="30" and height="45", and "50" becomes width="50" and height="50". + # <tt>:size</tt> will be ignored if the value is not in the correct format. # # ==== Examples # @@ -267,6 +260,8 @@ module ActionView # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/assets/screenshot.png" /> # video_tag("/trailers/hd.avi", size: "16x16") # # => <video src="/trailers/hd.avi" width="16" height="16" /> + # video_tag("/trailers/hd.avi", size: "16") + # # => <video height="16" src="/trailers/hd.avi" width="16" /> # video_tag("/trailers/hd.avi", height: '32', width: '32') # # => <video height="32" src="/trailers/hd.avi" width="32" /> # video_tag("trailer.ogg", "trailer.flv") @@ -278,10 +273,7 @@ module ActionView def video_tag(*sources) multiple_sources_tag('video', sources) do |options| options[:poster] = path_to_image(options[:poster]) if options[:poster] - - if size = options.delete(:size) - options[:width], options[:height] = size.split("x") if size =~ %r{^\d+x\d+$} - end + options[:width], options[:height] = extract_dimensions(options.delete(:size)) if options[:size] end end @@ -317,6 +309,14 @@ module ActionView content_tag(type, nil, options) end end + + def extract_dimensions(size) + if size =~ %r{\A\d+x\d+\z} + size.split('x') + elsif size =~ %r{\A\d+\z} + [size, size] + end + end end end end diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb index 523f8aed7a..3d091c4a00 100644 --- a/actionview/lib/action_view/helpers/date_helper.rb +++ b/actionview/lib/action_view/helpers/date_helper.rb @@ -170,8 +170,12 @@ module ActionView # * <tt>:use_month_names</tt> - Set to an array with 12 month names if you want to customize month names. # Note: You can also use Rails' i18n functionality for this. # * <tt>:date_separator</tt> - Specifies a string to separate the date fields. Default is "" (i.e. nothing). - # * <tt>:start_year</tt> - Set the start year for the year select. Default is <tt>Time.now.year - 5</tt>. - # * <tt>:end_year</tt> - Set the end year for the year select. Default is <tt>Time.now.year + 5</tt>. + # * <tt>:start_year</tt> - Set the start year for the year select. Default is <tt>Date.today.year - 5</tt>if + # you are creating new record. While editing existing record, <tt>:start_year</tt> defaults to + # the current selected year minus 5. + # * <tt>:end_year</tt> - Set the end year for the year select. Default is <tt>Date.today.year + 5</tt> if + # you are creating new record. While editing existing record, <tt>:end_year</tt> defaults to + # the current selected year plus 5. # * <tt>:discard_day</tt> - Set to true if you don't want to show a day select. This includes the day # as a hidden field instead of showing a select field. Also note that this implicitly sets the day to be the # first of the given month in order to not create invalid dates like 31 February. @@ -1062,7 +1066,7 @@ module ActionView # Wraps ActionView::Helpers::DateHelper#datetime_select for form builders: # # <%= form_for @person do |f| %> - # <%= f.time_select :last_request_at %> + # <%= f.datetime_select :last_request_at %> # <%= f.submit %> # <% end %> # diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 8e66fa13dc..5235962f9f 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -51,7 +51,7 @@ module ActionView # The HTML generated for this would be (modulus formatting): # # <form action="/people" class="new_person" id="new_person" method="post"> - # <div style="margin:0;padding:0;display:inline"> + # <div style="display:none"> # <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" /> # </div> # <label for="person_first_name">First name</label>: @@ -81,7 +81,7 @@ module ActionView # the code above as is would yield instead: # # <form action="/people/256" class="edit_person" id="edit_person_256" method="post"> - # <div style="margin:0;padding:0;display:inline"> + # <div style="display:none"> # <input name="_method" type="hidden" value="patch" /> # <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" /> # </div> @@ -315,7 +315,7 @@ module ActionView # The HTML generated for this would be: # # <form action='http://www.example.com' method='post' data-remote='true'> - # <div style='margin:0;padding:0;display:inline'> + # <div style='display:none'> # <input name='_method' type='hidden' value='patch' /> # </div> # ... @@ -333,7 +333,7 @@ module ActionView # The HTML generated for this would be: # # <form action='http://www.example.com' method='post' data-behavior='autosave' name='go'> - # <div style='margin:0;padding:0;display:inline'> + # <div style='display:none'> # <input name='_method' type='hidden' value='patch' /> # </div> # ... @@ -762,8 +762,8 @@ module ActionView # text_field(:post, :title, class: "create_input") # # => <input type="text" id="post_title" name="post[title]" value="#{@post.title}" class="create_input" /> # - # text_field(:session, :user, onchange: "if ($('#session_user').val() === 'admin') { alert('Your login can not be admin!'); }") - # # => <input type="text" id="session_user" name="session[user]" value="#{@session.user}" onchange="if ($('#session_user').val() === 'admin') { alert('Your login can not be admin!'); }"/> + # text_field(:session, :user, onchange: "if ($('#session_user').val() === 'admin') { alert('Your login cannot be admin!'); }") + # # => <input type="text" id="session_user" name="session[user]" value="#{@session.user}" onchange="if ($('#session_user').val() === 'admin') { alert('Your login cannot be admin!'); }"/> # # text_field(:snippet, :code, size: 20, class: 'code_input') # # => <input type="text" id="snippet_code" name="snippet[code]" size="20" value="#{@snippet.code}" class="code_input" /> diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb index 142c27ace0..80f066b3be 100644 --- a/actionview/lib/action_view/helpers/form_tag_helper.rb +++ b/actionview/lib/action_view/helpers/form_tag_helper.rb @@ -465,7 +465,7 @@ module ActionView # # <strong>Ask me!</strong> # # </button> # - # button_tag "Checkout", data: { disable_with => "Please wait..." } + # button_tag "Checkout", data: { :disable_with => "Please wait..." } # # => <button data-disable-with="Please wait..." name="button" type="submit">Checkout</button> # def button_tag(content_or_options = nil, options = nil, &block) @@ -722,7 +722,7 @@ module ActionView enforce_utf8 = html_options.delete("enforce_utf8") { true } tags = (enforce_utf8 ? utf8_enforcer_tag : ''.html_safe) << method_tag - content_tag(:div, tags, :style => 'margin:0;padding:0;display:inline') + content_tag(:div, tags, :style => 'display:none') end def form_tag_html(html_options) diff --git a/actionview/lib/action_view/helpers/tags/collection_helpers.rb b/actionview/lib/action_view/helpers/tags/collection_helpers.rb index 787039c82e..991f32cea2 100644 --- a/actionview/lib/action_view/helpers/tags/collection_helpers.rb +++ b/actionview/lib/action_view/helpers/tags/collection_helpers.rb @@ -18,7 +18,7 @@ module ActionView end def label(label_html_options={}, &block) - html_options = label_html_options.merge(@input_html_options) + html_options = @input_html_options.slice(:index, :namespace).merge(label_html_options) @template_object.label(@object_name, @sanitized_attribute_name, @text, html_options, &block) end end diff --git a/actionview/lib/action_view/helpers/tags/label.rb b/actionview/lib/action_view/helpers/tags/label.rb index 180aa9ac27..35d3ba8434 100644 --- a/actionview/lib/action_view/helpers/tags/label.rb +++ b/actionview/lib/action_view/helpers/tags/label.rb @@ -30,7 +30,6 @@ module ActionView add_default_name_and_id_for_value(tag_value, name_and_id) options.delete("index") options.delete("namespace") - options.delete("multiple") options["for"] = name_and_id["id"] unless options.key?("for") if block_given? diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb index 3478eadbf2..7cfbca5b6f 100644 --- a/actionview/lib/action_view/helpers/text_helper.rb +++ b/actionview/lib/action_view/helpers/text_helper.rb @@ -82,6 +82,9 @@ module ActionView # # => "And they f... (continued)" # # truncate("<p>Once upon a time in a world far far away</p>") + # # => "<p>Once upon a time in a wo..." + # + # truncate("<p>Once upon a time in a world far far away</p>", escape: false) # # => "<p>Once upon a time in a wo..." # # truncate("Once upon a time in a world far far away") { link_to "Continue", "#" } @@ -316,7 +319,7 @@ module ActionView options = values.extract_options! name = options.fetch(:name, 'default') - values.unshift(first_value) + values.unshift(*first_value) cycle = get_cycle(name) unless cycle && cycle.values == values diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb index 3ae1df04fe..0bc40874d9 100644 --- a/actionview/lib/action_view/helpers/translation_helper.rb +++ b/actionview/lib/action_view/helpers/translation_helper.rb @@ -38,10 +38,10 @@ module ActionView # If the user has specified rescue_format then pass it all through, otherwise use # raise and do the work ourselves - if options.key?(:raise) || options.key?(:rescue_format) - raise_error = options[:raise] || options[:rescue_format] - else - raise_error = false + options[:raise] ||= ActionView::Base.raise_on_missing_translations + + raise_error = options[:raise] || options.key?(:rescue_format) + unless raise_error options[:raise] = true end diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb index e930ba9aae..81f9c40b85 100644 --- a/actionview/lib/action_view/railtie.rb +++ b/actionview/lib/action_view/railtie.rb @@ -36,7 +36,7 @@ module ActionView end end - initializer "action_view.setup_action_pack", before: :add_view_paths do |app| + initializer "action_view.setup_action_pack" do |app| ActiveSupport.on_load(:action_controller) do ActionView::RoutingUrlFor.send(:include, ActionDispatch::Routing::UrlFor) end diff --git a/actionview/lib/action_view/renderer/streaming_template_renderer.rb b/actionview/lib/action_view/renderer/streaming_template_renderer.rb index 9cf6eb0c65..3ab2cd36fc 100644 --- a/actionview/lib/action_view/renderer/streaming_template_renderer.rb +++ b/actionview/lib/action_view/renderer/streaming_template_renderer.rb @@ -58,7 +58,7 @@ module ActionView def delayed_render(buffer, template, layout, view, locals) # Wrap the given buffer in the StreamingBuffer and pass it to the - # underlying template handler. Now, everytime something is concatenated + # underlying template handler. Now, every time something is concatenated # to the buffer, it is not appended to an array, but streamed straight # to the client. output = ActionView::StreamingBuffer.new(buffer) diff --git a/actionview/lib/action_view/vendor/html-scanner/html/node.rb b/actionview/lib/action_view/vendor/html-scanner/html/node.rb index 7e7cd4f7b6..27f0f2f6f8 100644 --- a/actionview/lib/action_view/vendor/html-scanner/html/node.rb +++ b/actionview/lib/action_view/vendor/html-scanner/html/node.rb @@ -71,12 +71,12 @@ module HTML #:nodoc: @line, @position = line, pos end - # Return a textual representation of the node. + # Returns a textual representation of the node. def to_s @children.join() end - # Return false (subclasses must override this to provide specific matching + # Returns false (subclasses must override this to provide specific matching # behavior.) +conditions+ may be of any type. def match(conditions) false diff --git a/actionview/lib/action_view/vendor/html-scanner/html/selector.rb b/actionview/lib/action_view/vendor/html-scanner/html/selector.rb index 7f8609c408..dfdd724b9b 100644 --- a/actionview/lib/action_view/vendor/html-scanner/html/selector.rb +++ b/actionview/lib/action_view/vendor/html-scanner/html/selector.rb @@ -488,7 +488,7 @@ module HTML end - # Return the next element after this one. Skips sibling text nodes. + # Returns the next element after this one. Skips sibling text nodes. # # With the +name+ argument, returns the next element with that name, # skipping other sibling elements. diff --git a/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb b/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb index 8ac8d34430..adf4e45930 100644 --- a/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb +++ b/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb @@ -30,7 +30,7 @@ module HTML #:nodoc: @current_line = 1 end - # Return the next token in the sequence, or +nil+ if there are no more tokens in + # Returns the next token in the sequence, or +nil+ if there are no more tokens in # the stream. def next return nil if @scanner.eos? diff --git a/actionview/lib/action_view/version.rb b/actionview/lib/action_view/version.rb index 094dd474df..edb6d8f116 100644 --- a/actionview/lib/action_view/version.rb +++ b/actionview/lib/action_view/version.rb @@ -1,7 +1,7 @@ module ActionView # Returns the version of the currently loaded ActionView as a Gem::Version def self.version - Gem::Version.new "4.1.0.beta" + Gem::Version.new "4.1.0.beta1" end module VERSION #:nodoc: diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb index eef0abb609..9eae3a4fbd 100644 --- a/actionview/test/abstract_unit.rb +++ b/actionview/test/abstract_unit.rb @@ -42,6 +42,9 @@ Thread.abort_on_exception = true # Show backtraces for deprecated behavior for quicker cleanup. ActiveSupport::Deprecation.debug = true +# Disable available locale checks to avoid warnings running the test suite. +I18n.enforce_available_locales = false + # Register danish language for testing I18n.backend.store_translations 'da', {} I18n.backend.store_translations 'pt-BR', {} @@ -328,3 +331,11 @@ module ActionDispatch end end +# Skips the current run on Rubinius using Minitest::Assertions#skip +def rubinius_skip(message = '') + skip message if RUBY_ENGINE == 'rbx' +end +# Skips the current run on JRuby using Minitest::Assertions#skip +def jruby_skip(message = '') + skip message if defined?(JRUBY_VERSION) +end diff --git a/actionview/test/actionpack/abstract/helper_test.rb b/actionview/test/actionpack/abstract/helper_test.rb index 89c4567715..7d346e917d 100644 --- a/actionview/test/actionpack/abstract/helper_test.rb +++ b/actionview/test/actionpack/abstract/helper_test.rb @@ -78,9 +78,9 @@ module AbstractController end def test_declare_missing_helper - AbstractHelpers.helper :missing - flunk "should have raised an exception" - rescue LoadError => e + e = assert_raise AbstractController::Helpers::MissingHelperError do + AbstractHelpers.helper :missing + end assert_equal "helpers/missing_helper.rb", e.path end diff --git a/actionview/test/actionpack/controller/layout_test.rb b/actionview/test/actionpack/controller/layout_test.rb index 5dd23c4b31..b44f57a23e 100644 --- a/actionview/test/actionpack/controller/layout_test.rb +++ b/actionview/test/actionpack/controller/layout_test.rb @@ -215,12 +215,6 @@ class LayoutSetInResponseTest < ActionController::TestCase end end -class RenderWithTemplateOptionController < LayoutTest - def hello - render :template => 'alt/hello' - end -end - class SetsNonExistentLayoutFile < LayoutTest layout "nofile" end diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb index 8c99504050..45b8049b83 100644 --- a/actionview/test/actionpack/controller/render_test.rb +++ b/actionview/test/actionpack/controller/render_test.rb @@ -44,8 +44,6 @@ module Quiz end end - class Store < Question; end - # Controller class QuestionsController < ApplicationController def new @@ -693,9 +691,9 @@ class RenderTest < ActionController::TestCase end def test_line_offset - get :render_line_offset - flunk "the action should have raised an exception" - rescue StandardError => exc + exc = assert_raises ActionView::Template::Error do + get :render_line_offset + end line = exc.backtrace.first assert(line =~ %r{:(\d+):}) assert_equal "1", $1, @@ -971,7 +969,7 @@ class RenderTest < ActionController::TestCase end def test_should_implicitly_render_js_template_without_layout - get :render_implicit_js_template_without_layout, :format => :js + xhr :get, :render_implicit_js_template_without_layout, :format => :js assert_no_match %r{<html>}, @response.body end diff --git a/actionview/test/activerecord/form_helper_activerecord_test.rb b/actionview/test/activerecord/form_helper_activerecord_test.rb index 2e302c65a7..0a9628da8d 100644 --- a/actionview/test/activerecord/form_helper_activerecord_test.rb +++ b/actionview/test/activerecord/form_helper_activerecord_test.rb @@ -59,7 +59,7 @@ class FormHelperActiveRecordTest < ActionView::TestCase protected def hidden_fields(method = nil) - txt = %{<div style="margin:0;padding:0;display:inline">} + txt = %{<div style="display:none">} txt << %{<input name="utf8" type="hidden" value="✓" />} if method && !%w(get post).include?(method.to_s) txt << %{<input name="_method" type="hidden" value="#{method}" />} diff --git a/actionview/test/fixtures/developer.rb b/actionview/test/fixtures/developer.rb index 4941463015..8b3f0a8039 100644 --- a/actionview/test/fixtures/developer.rb +++ b/actionview/test/fixtures/developer.rb @@ -4,7 +4,3 @@ class Developer < ActiveRecord::Base has_many :topics, :through => :replies accepts_nested_attributes_for :projects end - -class DeVeLoPeR < ActiveRecord::Base - self.table_name = "developers" -end diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb index 214a13654e..3ca71d3376 100644 --- a/actionview/test/template/asset_tag_helper_test.rb +++ b/actionview/test/template/asset_tag_helper_test.rb @@ -2,14 +2,6 @@ require 'zlib' require 'abstract_unit' require 'active_support/ordered_options' -class FakeController - attr_accessor :request - - def config - @config ||= ActiveSupport::InheritableOptions.new(ActionController::Base.config) - end -end - class AssetTagHelperTest < ActionView::TestCase tests ActionView::Helpers::AssetTagHelper @@ -245,7 +237,7 @@ class AssetTagHelperTest < ActionView::TestCase %(video_tag("gold.m4v", :size => "160x120")) => %(<video height="120" src="/videos/gold.m4v" width="160"></video>), %(video_tag("gold.m4v", "size" => "320x240")) => %(<video height="240" src="/videos/gold.m4v" width="320"></video>), %(video_tag("trailer.ogg", :poster => "screenshot.png")) => %(<video poster="/images/screenshot.png" src="/videos/trailer.ogg"></video>), - %(video_tag("error.avi", "size" => "100")) => %(<video src="/videos/error.avi"></video>), + %(video_tag("error.avi", "size" => "100")) => %(<video height="100" src="/videos/error.avi" width="100"></video>), %(video_tag("error.avi", "size" => "100 x 100")) => %(<video src="/videos/error.avi"></video>), %(video_tag("error.avi", "size" => "x")) => %(<video src="/videos/error.avi"></video>), %(video_tag("http://media.rubyonrails.org/video/rails_blog_2.mov")) => %(<video src="http://media.rubyonrails.org/video/rails_blog_2.mov"></video>), diff --git a/actionview/test/template/dependency_tracker_test.rb b/actionview/test/template/dependency_tracker_test.rb index 7a9b4b26ac..df3a0602d1 100644 --- a/actionview/test/template/dependency_tracker_test.rb +++ b/actionview/test/template/dependency_tracker_test.rb @@ -1,3 +1,5 @@ +# encoding: utf-8 + require 'abstract_unit' require 'action_view/dependency_tracker' @@ -52,23 +54,127 @@ class ERBTrackerTest < Minitest::Test def test_dependency_of_erb_template_with_number_in_filename template = FakeTemplate.new("<%# render 'messages/message123' %>", :erb) - tracker = make_tracker('messages/_message123', template) + tracker = make_tracker("messages/_message123", template) assert_equal ["messages/message123"], tracker.dependencies end def test_finds_dependency_in_correct_directory template = FakeTemplate.new("<%# render(message.topic) %>", :erb) - tracker = make_tracker('messages/_message', template) + tracker = make_tracker("messages/_message", template) assert_equal ["topics/topic"], tracker.dependencies end def test_finds_dependency_in_correct_directory_with_underscore template = FakeTemplate.new("<%# render(message_type.messages) %>", :erb) - tracker = make_tracker('message_types/_message_type', template) + tracker = make_tracker("message_types/_message_type", template) assert_equal ["messages/message"], tracker.dependencies end -end + def test_dependency_of_erb_template_with_no_spaces_after_render + template = FakeTemplate.new("<%# render'messages/message' %>", :erb) + tracker = make_tracker("messages/_message", template) + + assert_equal ["messages/message"], tracker.dependencies + end + + def test_finds_no_dependency_when_render_begins_the_name_of_an_identifier + template = FakeTemplate.new("<%# rendering 'it useless' %>", :erb) + tracker = make_tracker("resources/_resource", template) + + assert_equal [], tracker.dependencies + end + + def test_finds_no_dependency_when_render_ends_the_name_of_another_method + template = FakeTemplate.new("<%# surrender 'to reason' %>", :erb) + tracker = make_tracker("resources/_resource", template) + + assert_equal [], tracker.dependencies + end + + def test_finds_dependency_on_multiline_render_calls + template = FakeTemplate.new("<%# + render :object => @all_posts, + :partial => 'posts' %>", :erb) + + tracker = make_tracker("some/_little_posts", template) + + assert_equal ["some/posts"], tracker.dependencies + end + + def test_finds_multiple_unrelated_odd_dependencies + template = FakeTemplate.new(" + <%# render('shared/header', title: 'Title') %> + <h2>Section title</h2> + <%# render@section %> + ", :erb) + + tracker = make_tracker("multiple/_dependencies", template) + + assert_equal ["shared/header", "sections/section"], tracker.dependencies + end + + def test_finds_dependencies_for_all_kinds_of_identifiers + template = FakeTemplate.new(" + <%# render $globals %> + <%# render @instance_variables %> + <%# render @@class_variables %> + ", :erb) + + tracker = make_tracker("identifiers/_all", template) + + assert_equal [ + "globals/global", + "instance_variables/instance_variable", + "class_variables/class_variable" + ], tracker.dependencies + end + + def test_finds_dependencies_on_method_chains + template = FakeTemplate.new("<%# render @parent.child.grandchildren %>", :erb) + tracker = make_tracker("method/_chains", template) + + assert_equal ["grandchildren/grandchild"], tracker.dependencies + end + + def test_finds_dependencies_with_special_characters + template = FakeTemplate.new("<%# render @pokémon, partial: 'ピカチュウ' %>", :erb) + tracker = make_tracker("special/_characters", template) + + assert_equal ["special/ピカチュウ"], tracker.dependencies + end + + def test_finds_dependencies_with_quotes_within + template = FakeTemplate.new(%{ + <%# render "single/quote's" %> + <%# render 'double/quote"s' %> + }, :erb) + + tracker = make_tracker("quotes/_single_and_double", template) + + assert_equal ["single/quote's", 'double/quote"s'], tracker.dependencies + end + + def test_finds_dependencies_with_extra_spaces + template = FakeTemplate.new(%{ + <%= render "header" %> + <%= render partial: "form" %> + <%= render @message %> + <%= render ( @message.events ) %> + <%= render :collection => @message.comments, + :partial => "comments/comment" %> + }, :erb) + + tracker = make_tracker("spaces/_extra", template) + + assert_equal [ + "spaces/header", + "spaces/form", + "messages/message", + "events/event", + "comments/comment" + ], tracker.dependencies + end +end diff --git a/actionview/test/template/form_collections_helper_test.rb b/actionview/test/template/form_collections_helper_test.rb index d28e4aeb48..18632465db 100644 --- a/actionview/test/template/form_collections_helper_test.rb +++ b/actionview/test/template/form_collections_helper_test.rb @@ -60,7 +60,7 @@ class FormCollectionsHelperTest < ActionView::TestCase assert_no_select 'input[type=radio][value=other][disabled=disabled]' end - test 'collection radio accepts single disable item' do + test 'collection radio accepts single disabled item' do collection = [[1, true], [0, false]] with_collection_radio_buttons :user, :active, collection, :last, :first, :disabled => true @@ -84,6 +84,24 @@ class FormCollectionsHelperTest < ActionView::TestCase assert_select 'input[type=radio][value=false].bar#user_active_false' end + test 'collection radio sets the label class defined inside the block' do + collection = [[1, true, {class: 'foo'}], [0, false, {class: 'bar'}]] + with_collection_radio_buttons :user, :active, collection, :second, :first do |b| + b.label(class: "collection_radio_buttons") + end + + assert_select 'label.collection_radio_buttons[for=user_active_true]' + assert_select 'label.collection_radio_buttons[for=user_active_false]' + end + + test 'collection radio does not include the input class in the respective label' do + collection = [[1, true, {class: 'foo'}], [0, false, {class: 'bar'}]] + with_collection_radio_buttons :user, :active, collection, :second, :first + + assert_no_select 'label.foo[for=user_active_true]' + assert_no_select 'label.bar[for=user_active_false]' + end + test 'collection radio does not wrap input inside the label' do with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s @@ -164,7 +182,7 @@ class FormCollectionsHelperTest < ActionView::TestCase end # COLLECTION CHECK BOXES - test 'collection check boxes accepts a collection and generate a serie of checkboxes for value method' do + test 'collection check boxes accepts a collection and generate a series of checkboxes for value method' do collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] with_collection_check_boxes :user, :category_ids, collection, :id, :name @@ -186,7 +204,7 @@ class FormCollectionsHelperTest < ActionView::TestCase assert_select "input[type=hidden][name='user[other_category_ids][]'][value=]", :count => 1 end - test 'collection check boxes accepts a collection and generate a serie of checkboxes with labels for label method' do + test 'collection check boxes accepts a collection and generate a series of checkboxes with labels for label method' do collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] with_collection_check_boxes :user, :category_ids, collection, :id, :name @@ -215,6 +233,24 @@ class FormCollectionsHelperTest < ActionView::TestCase assert_select 'input[type=checkbox][value=2].bar' end + test 'collection check boxes sets the label class defined inside the block' do + collection = [[1, 'Category 1', {class: 'foo'}], [2, 'Category 2', {class: 'bar'}]] + with_collection_check_boxes :user, :active, collection, :second, :first do |b| + b.label(class: 'collection_check_boxes') + end + + assert_select 'label.collection_check_boxes[for=user_active_category_1]' + assert_select 'label.collection_check_boxes[for=user_active_category_2]' + end + + test 'collection check boxes does not include the input class in the respective label' do + collection = [[1, 'Category 1', {class: 'foo'}], [2, 'Category 2', {class: 'bar'}]] + with_collection_check_boxes :user, :active, collection, :second, :first + + assert_no_select 'label.foo[for=user_active_category_1]' + assert_no_select 'label.bar[for=user_active_category_2]' + end + test 'collection check boxes accepts selected values as :checked option' do collection = (1..3).map{|i| [i, "Category #{i}"] } with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => [1, 3] @@ -264,7 +300,7 @@ class FormCollectionsHelperTest < ActionView::TestCase assert_no_select 'input[type=checkbox][value=2][disabled=disabled]' end - test 'collection check boxes accepts single disable item' do + test 'collection check boxes accepts single disabled item' do collection = (1..3).map{|i| [i, "Category #{i}"] } with_collection_check_boxes :user, :category_ids, collection, :first, :last, :disabled => 1 diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb index 3e8a2468ed..fe82349265 100644 --- a/actionview/test/template/form_helper_test.rb +++ b/actionview/test/template/form_helper_test.rb @@ -1300,6 +1300,24 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end + def test_form_with_index_and_with_collection_radio_buttons + post = Post.new + def post.active; false; end + + form_for(post, index: '1') do |f| + concat f.collection_radio_buttons(:active, [true, false], :to_s, :to_s) + end + + expected = whole_form("/posts", "new_post", "new_post") do + "<input id='post_1_active_true' name='post[1][active]' type='radio' value='true' />" + + "<label for='post_1_active_true'>true</label>" + + "<input checked='checked' id='post_1_active_false' name='post[1][active]' type='radio' value='false' />" + + "<label for='post_1_active_false'>false</label>" + end + + assert_dom_equal expected, output_buffer + end + def test_form_for_with_collection_check_boxes post = Post.new def post.tag_ids; [1, 3]; end @@ -1397,6 +1415,24 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end + def test_form_with_index_and_with_collection_check_boxes + post = Post.new + def post.tag_ids; [1]; end + collection = [[1, "Tag 1"]] + + form_for(post, index: '1') do |f| + concat f.collection_check_boxes(:tag_ids, collection, :first, :last) + end + + 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='' />" + end + + assert_dom_equal expected, output_buffer + end + def test_form_for_with_file_field_generate_multipart Post.send :attr_accessor, :file @@ -2987,7 +3023,7 @@ class FormHelperTest < ActionView::TestCase protected def hidden_fields(method = nil) - txt = %{<div style="margin:0;padding:0;display:inline">} + txt = %{<div style="display:none">} txt << %{<input name="utf8" type="hidden" value="✓" />} if method && !%w(get post).include?(method.to_s) txt << %{<input name="_method" type="hidden" value="#{method}" />} diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb index 22bf438a56..0d5831dc6f 100644 --- a/actionview/test/template/form_tag_helper_test.rb +++ b/actionview/test/template/form_tag_helper_test.rb @@ -14,7 +14,7 @@ class FormTagHelperTest < ActionView::TestCase method = options[:method] enforce_utf8 = options.fetch(:enforce_utf8, true) - txt = %{<div style="margin:0;padding:0;display:inline">} + 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}" />} diff --git a/actionview/test/template/text_helper_test.rb b/actionview/test/template/text_helper_test.rb index 5b3bccf951..a514bba83d 100644 --- a/actionview/test/template/text_helper_test.rb +++ b/actionview/test/template/text_helper_test.rb @@ -386,6 +386,13 @@ class TextHelperTest < ActionView::TestCase assert_equal("3", cycle("one", 2, "3")) end + def test_cycle_with_array + array = [1, 2, 3] + assert_equal("1", cycle(array)) + assert_equal("2", cycle(array)) + assert_equal("3", cycle(array)) + end + def test_cycle_with_no_arguments assert_raise(ArgumentError) { cycle } end diff --git a/actionview/test/template/translation_helper_test.rb b/actionview/test/template/translation_helper_test.rb index 269714fad0..c4770840fb 100644 --- a/actionview/test/template/translation_helper_test.rb +++ b/actionview/test/template/translation_helper_test.rb @@ -53,6 +53,16 @@ class TranslationHelperTest < ActiveSupport::TestCase assert_equal false, translate(:"translations.missing", :rescue_format => nil).html_safe? end + def test_raises_missing_translation_message_with_raise_config_option + ActionView::Base.raise_on_missing_translations = true + + assert_raise(I18n::MissingTranslationData) do + translate("translations.missing") + end + ensure + ActionView::Base.raise_on_missing_translations = false + end + def test_raises_missing_translation_message_with_raise_option assert_raise(I18n::MissingTranslationData) do translate(:"translations.missing", :raise => true) diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 77d1252f1f..0148066bac 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,3 +1,29 @@ +* Ability to specify multiple contexts when defining a validation. + + Example: + + class Person + include ActiveModel::Validations + + attr_reader :name + validates_presence_of :name, on: [:verify, :approve] + end + + person = Person.new + person.valid? # => true + person.valid?(:verify) # => false + person.errors.full_messages_for(:name) # => ["Name can't be blank"] + person.valid?(:approve) # => false + person.errors.full_messages_for(:name) # => ["Name can't be blank"] + + *Vince Puzzella* + +* `attribute_changed?` now accepts parameters which check the old and new value of the attribute + + `model.name_changed?(from: "Pete", to: "Ringo")` + + *Tejas Dinkar* + * Fix `has_secure_password` to honor bcrypt-ruby's cost attribute. *T.J. Schuck* @@ -13,8 +39,8 @@ *Bogdan Gusiev* -* Fix has_secure_password. `password_confirmation` validations are triggered - even if no `password_confirmation` is set. +* Fix `has_secure_password` not to trigger `password_confirmation` validations + if no `password_confirmation` is set. *Vladimir Kiselev* @@ -27,7 +53,7 @@ *Charles Bergeron* -* Fix regression in has_secure_password. When a password is set, but a +* Fix regression in `has_secure_password`. When a password is set, but a confirmation is an empty string, it would incorrectly save. *Steve Klabnik* and *Phillip Calvin* diff --git a/activemodel/MIT-LICENSE b/activemodel/MIT-LICENSE index 5c668d9624..d58dd9ed9b 100644 --- a/activemodel/MIT-LICENSE +++ b/activemodel/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2013 David Heinemeier Hansson +Copyright (c) 2004-2014 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc index a399fe9051..4c00755532 100644 --- a/activemodel/README.rdoc +++ b/activemodel/README.rdoc @@ -109,7 +109,7 @@ behavior out of the box: attr_reader :errors def validate! - errors.add(:name, "can not be nil") if name.nil? + errors.add(:name, "cannot be nil") if name.nil? end def self.human_attribute_name(attr, options = {}) @@ -118,7 +118,7 @@ behavior out of the box: end person.errors.full_messages - # => ["Name can not be nil"] + # => ["Name cannot be nil"] {Learn more}[link:classes/ActiveModel/Errors.html] diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index 11e755649c..36e565f692 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -20,5 +20,5 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', version - s.add_dependency 'builder', '~> 3.1.0' + s.add_dependency 'builder', '~> 3.1' end diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index ef4f2514be..3e49c34182 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2013 David Heinemeier Hansson +# Copyright (c) 2004-2014 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -59,9 +59,9 @@ module ActiveModel end end - def eager_load! + def self.eager_load! super - ActiveModel::Serializer.eager_load! + ActiveModel::Serializers.eager_load! end end diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 8d6d405e96..ea07c5c039 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -14,11 +14,11 @@ module ActiveModel class MissingAttributeError < NoMethodError end - # == Active \Model Attribute Methods + # == Active \Model \Attribute \Methods # - # <tt>ActiveModel::AttributeMethods</tt> provides a way to add prefixes and - # suffixes to your methods as well as handling the creation of - # <tt>ActiveRecord::Base</tt>-like class methods such as +table_name+. + # Provides a way to add prefixes and suffixes to your methods as + # well as handling the creation of <tt>ActiveRecord::Base</tt>-like + # class methods such as +table_name+. # # The requirements to implement <tt>ActiveModel::AttributeMethods</tt> are to: # @@ -27,7 +27,9 @@ module ActiveModel # or +attribute_method_prefix+. # * Call +define_attribute_methods+ after the other methods are called. # * Define the various generic +_attribute+ methods that you have declared. - # * Define an +attributes+ method, see below. + # * Define an +attributes+ method which returns a hash with each + # attribute name in your model as hash key and the attribute value as hash value. + # Hash keys must be strings. # # A minimal implementation could be: # @@ -42,7 +44,7 @@ module ActiveModel # attr_accessor :name # # def attributes - # {'name' => @name} + # { 'name' => @name } # end # # private @@ -59,13 +61,6 @@ module ActiveModel # send("#{attr}=", 'Default Name') # end # end - # - # Note that whenever you include <tt>ActiveModel::AttributeMethods</tt> in - # your class, it requires you to implement an +attributes+ method which - # returns a hash with each attribute name in your model as hash key and the - # attribute value as hash value. - # - # Hash keys must be strings. module AttributeMethods extend ActiveSupport::Concern @@ -173,14 +168,14 @@ module ActiveModel # private # # def reset_attribute_to_default!(attr) - # ... + # send("#{attr}=", 'Default Name') # end # end # # person = Person.new # person.name # => 'Gem' # person.reset_name_to_default! - # person.name # => 'Gemma' + # person.name # => 'Default Name' def attribute_method_affix(*affixes) self.attribute_method_matchers += affixes.map! { |affix| AttributeMethodMatcher.new prefix: affix[:prefix], suffix: affix[:suffix] } undefine_attribute_methods @@ -250,7 +245,7 @@ module ActiveModel # private # # def clear_attribute(attr) - # ... + # send("#{attr}=", nil) # end # end def define_attribute_methods(*attr_names) diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index 377aa6ee27..b27a39b787 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -30,7 +30,7 @@ module ActiveModel # end # # Then in your class, you can use the +before_create+, +after_create+ and - # +around_create+ methods, just as you would in an Active Record module. + # +around_create+ methods, just as you would in an Active Record model. # # before_create :action_before_create # diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb index 21e4eb3c86..6b0a752566 100644 --- a/activemodel/lib/active_model/conversion.rb +++ b/activemodel/lib/active_model/conversion.rb @@ -1,5 +1,5 @@ module ActiveModel - # == Active \Model Conversion + # == Active \Model \Conversion # # Handles default conversions: to_model, to_key, to_param, and to_partial_path. # @@ -41,7 +41,7 @@ module ActiveModel end # Returns an Enumerable of all key attributes if any is set, regardless if - # the object is persisted or not. If there no key attributes, returns +nil+. + # the object is persisted or not. Returns +nil+ if there are no key attributes. # # class Person < ActiveRecord::Base # end diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index c5f1b3f11a..98ffffeb10 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -54,6 +54,7 @@ module ActiveModel # person.name = 'Bob' # person.changed? # => true # person.name_changed? # => true + # person.name_changed?(from: "Uncle Bob", to: "Bob") # => true # person.name_was # => "Uncle Bob" # person.name_change # => ["Uncle Bob", "Bob"] # person.name = 'Bill' @@ -135,7 +136,7 @@ module ActiveModel # person.save # person.previous_changes # => {"name" => ["bob", "robert"]} def previous_changes - @previously_changed ||= {} + @previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new end # Returns a hash of the attributes with unsaved changes indicating their original @@ -149,12 +150,15 @@ module ActiveModel end # Handle <tt>*_changed?</tt> for +method_missing+. - def attribute_changed?(attr) - changed_attributes.include?(attr) + def attribute_changed?(attr, options = {}) #:nodoc: + result = changed_attributes.include?(attr) + result &&= options[:to] == __send__(attr) if options.key?(:to) + result &&= options[:from] == changed_attributes[attr] if options.key?(:from) + result end # Handle <tt>*_was</tt> for +method_missing+. - def attribute_was(attr) + def attribute_was(attr) # :nodoc: attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr) end @@ -163,13 +167,13 @@ module ActiveModel # Removes current changes and makes them accessible through +previous_changes+. def changes_applied @previously_changed = changes - @changed_attributes = {} + @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new end # Removes all dirty data: current changes and previous changes def reset_changes - @previously_changed = {} - @changed_attributes = {} + @previously_changed = ActiveSupport::HashWithIndifferentAccess.new + @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new end # Handle <tt>*_change</tt> for +method_missing+. diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index cf7551e4f4..9c3bc913e1 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -7,7 +7,7 @@ module ActiveModel # == Active \Model \Errors # # Provides a modified +Hash+ that you can include in your object - # for handling error messages and interacting with Action Pack helpers. + # for handling error messages and interacting with Action View helpers. # # A minimal implementation could be: # @@ -23,7 +23,7 @@ module ActiveModel # attr_reader :errors # # def validate! - # errors.add(:name, "can not be nil") if name == nil + # errors.add(:name, "cannot be nil") if name == nil # end # # # The following methods are needed to be minimally implemented @@ -51,8 +51,8 @@ module ActiveModel # The above allows you to do: # # person = Person.new - # person.validate! # => ["can not be nil"] - # person.errors.full_messages # => ["name can not be nil"] + # person.validate! # => ["cannot be nil"] + # person.errors.full_messages # => ["name cannot be nil"] # # etc.. class Errors include Enumerable @@ -80,7 +80,7 @@ module ActiveModel # Clear the error messages. # - # person.errors.full_messages # => ["name can not be nil"] + # person.errors.full_messages # => ["name cannot be nil"] # person.errors.clear # person.errors.full_messages # => [] def clear @@ -90,19 +90,19 @@ module ActiveModel # Returns +true+ if the error messages include an error for the given key # +attribute+, +false+ otherwise. # - # person.errors.messages # => {:name=>["can not be nil"]} + # person.errors.messages # => {:name=>["cannot be nil"]} # person.errors.include?(:name) # => true # person.errors.include?(:age) # => false def include?(attribute) - (v = messages[attribute]) && v.any? + messages[attribute].present? end # aliases include? alias :has_key? :include? # Get messages for +key+. # - # person.errors.messages # => {:name=>["can not be nil"]} - # person.errors.get(:name) # => ["can not be nil"] + # person.errors.messages # => {:name=>["cannot be nil"]} + # person.errors.get(:name) # => ["cannot be nil"] # person.errors.get(:age) # => nil def get(key) messages[key] @@ -110,7 +110,7 @@ module ActiveModel # Set messages for +key+ to +value+. # - # person.errors.get(:name) # => ["can not be nil"] + # person.errors.get(:name) # => ["cannot be nil"] # person.errors.set(:name, ["can't be nil"]) # person.errors.get(:name) # => ["can't be nil"] def set(key, value) @@ -119,8 +119,8 @@ module ActiveModel # Delete messages for +key+. Returns the deleted messages. # - # person.errors.get(:name) # => ["can not be nil"] - # person.errors.delete(:name) # => ["can not be nil"] + # person.errors.get(:name) # => ["cannot be nil"] + # person.errors.delete(:name) # => ["cannot be nil"] # person.errors.get(:name) # => nil def delete(key) messages.delete(key) @@ -129,8 +129,8 @@ module ActiveModel # When passed a symbol or a name of a method, returns an array of errors # for the method. # - # person.errors[:name] # => ["can not be nil"] - # person.errors['name'] # => ["can not be nil"] + # person.errors[:name] # => ["cannot be nil"] + # person.errors['name'] # => ["cannot be nil"] def [](attribute) get(attribute.to_sym) || set(attribute.to_sym, []) end @@ -175,15 +175,15 @@ module ActiveModel # Returns all message values. # - # person.errors.messages # => {:name=>["can not be nil", "must be specified"]} - # person.errors.values # => [["can not be nil", "must be specified"]] + # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]} + # person.errors.values # => [["cannot be nil", "must be specified"]] def values messages.values end # Returns all message keys. # - # person.errors.messages # => {:name=>["can not be nil", "must be specified"]} + # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]} # person.errors.keys # => [:name] def keys messages.keys @@ -211,7 +211,7 @@ module ActiveModel # Returns +true+ if no errors are found, +false+ otherwise. # If the error message is a string it can be empty. # - # person.errors.full_messages # => ["name can not be nil"] + # person.errors.full_messages # => ["name cannot be nil"] # person.errors.empty? # => false def empty? all? { |k, v| v && v.empty? && !v.is_a?(String) } @@ -238,8 +238,8 @@ module ActiveModel # object. You can pass the <tt>:full_messages</tt> option. This determines # if the json object should contain full messages or not (false by default). # - # person.errors.as_json # => {:name=>["can not be nil"]} - # person.errors.as_json(full_messages: true) # => {:name=>["name can not be nil"]} + # person.errors.as_json # => {:name=>["cannot be nil"]} + # person.errors.as_json(full_messages: true) # => {:name=>["name cannot be nil"]} def as_json(options=nil) to_hash(options && options[:full_messages]) end @@ -247,8 +247,8 @@ module ActiveModel # Returns a Hash of attributes with their error messages. If +full_messages+ # is +true+, it will contain full messages (see +full_message+). # - # person.errors.to_hash # => {:name=>["can not be nil"]} - # person.errors.to_hash(true) # => {:name=>["name can not be nil"]} + # person.errors.to_hash # => {:name=>["cannot be nil"]} + # person.errors.to_hash(true) # => {:name=>["name cannot be nil"]} def to_hash(full_messages = false) if full_messages messages = {} @@ -279,7 +279,7 @@ module ActiveModel # If +message+ is a proc, it will be called, allowing for things like # <tt>Time.now</tt> to be used within an error. # - # If the <tt>:strict</tt> option is set to true will raise + # If the <tt>:strict</tt> option is set to +true+, it will raise # ActiveModel::StrictValidationFailed instead of adding the error. # <tt>:strict</tt> option can also be set to any other exception. # diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb index 46b446dc08..c6bc18b008 100644 --- a/activemodel/lib/active_model/lint.rb +++ b/activemodel/lib/active_model/lint.rb @@ -13,7 +13,7 @@ module ActiveModel # # These tests do not attempt to determine the semantic correctness of the # returned values. For instance, you could implement <tt>valid?</tt> to - # always return true, and the tests would pass. It is up to you to ensure + # always return +true+, and the tests would pass. It is up to you to ensure # that the values are semantically meaningful. # # Objects you pass in are expected to return a compliant object from a call diff --git a/activemodel/lib/active_model/model.rb b/activemodel/lib/active_model/model.rb index f048dda5c6..63716eebb1 100644 --- a/activemodel/lib/active_model/model.rb +++ b/activemodel/lib/active_model/model.rb @@ -1,6 +1,6 @@ module ActiveModel - # == Active \Model Basic \Model + # == Active \Model \Basic \Model # # Includes the required interface for an object to interact with # <tt>ActionPack</tt>, using different <tt>ActiveModel</tt> modules. diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index 7e694b5c50..01739d8ae4 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -9,7 +9,7 @@ module ActiveModel module ClassMethods # Adds methods to set and authenticate against a BCrypt password. - # This mechanism requires you to have a password_digest attribute. + # This mechanism requires you to have a +password_digest+ attribute. # # Validations for presence of password on create, confirmation of password # (using a +password_confirmation+ attribute) are automatically added. If @@ -57,11 +57,15 @@ module ActiveModel include InstanceMethodsOnActivation if options.fetch(:validations, true) - validates_confirmation_of :password, if: :should_confirm_password? - validates_presence_of :password, on: :create - validates_presence_of :password_confirmation, if: :should_confirm_password? + # This ensures the model has a password by checking whether the password_digest + # is present, so that this works with both new and existing records. However, + # when there is an error, the message is added to the password attribute instead + # so that the error message will make sense to the end-user. + validate do |record| + record.errors.add(:password, :blank) unless record.password_digest.present? + end - before_create { raise "Password digest missing on new record" if password_digest.blank? } + validates_confirmation_of :password, if: ->{ password.present? } end if respond_to?(:attributes_protected_by_default) @@ -100,7 +104,9 @@ module ActiveModel # user.password = 'mUc3m00RsqyRe' # user.password_digest # => "$2a$10$4LEA7r4YmNHtvlAvHhsYAeZmk/xeUVtMTYqwIvYY76EW5GUqDiP4." def password=(unencrypted_password) - unless unencrypted_password.blank? + if unencrypted_password.nil? + self.password_digest = nil + elsif unencrypted_password.present? @password = unencrypted_password cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost) @@ -110,12 +116,6 @@ module ActiveModel def password_confirmation=(unencrypted_password) @password_confirmation = unencrypted_password end - - private - - def should_confirm_password? - password_confirmation && password.present? - end end end end diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index fdb06aebb9..36a6c00290 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -128,7 +128,7 @@ module ActiveModel # retrieve the value for a given attribute differently: # # class MyClass - # include ActiveModel::Validations + # include ActiveModel::Serialization # # def initialize(data = {}) # @data = data diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index 05e2e089e5..c58e73f6a7 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -2,7 +2,7 @@ require 'active_support/json' module ActiveModel module Serializers - # == Active Model JSON Serializer + # == Active \Model \JSON \Serializer module JSON extend ActiveSupport::Concern include ActiveModel::Serialization diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 31c2245265..e9674d5143 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -4,7 +4,7 @@ require 'active_support/core_ext/hash/except' module ActiveModel - # == Active \Model Validations + # == Active \Model \Validations # # Provides a full validation framework to your objects. # @@ -66,8 +66,10 @@ module ActiveModel # end # # Options: - # * <tt>:on</tt> - Specifies the context where this validation is active - # (e.g. <tt>on: :create</tt> or <tt>on: :custom_validation_context</tt>) + # * <tt>:on</tt> - Specifies the contexts where this validation is active. + # You can pass a symbol or an array of symbols. + # (e.g. <tt>on: :create</tt> or <tt>on: :custom_validation_context</tt> or + # <tt>on: [:create, :custom_validation_context]</tt>) # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+. # * <tt>:allow_blank</tt> - Skip validation if attribute is blank. # * <tt>:if</tt> - Specifies a method, proc or string to call to determine @@ -124,10 +126,10 @@ module ActiveModel # end # # Options: - # * <tt>:on</tt> - Specifies the context where this validation is active - # (e.g. <tt>on: :create</tt> or <tt>on: :custom_validation_context</tt>) - # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+. - # * <tt>:allow_blank</tt> - Skip validation if attribute is blank. + # * <tt>:on</tt> - Specifies the contexts where this validation is active. + # You can pass a symbol or an array of symbols. + # (e.g. <tt>on: :create</tt> or <tt>on: :custom_validation_context</tt> or + # <tt>on: [:create, :custom_validation_context]</tt>) # * <tt>:if</tt> - Specifies a method, proc or string to call to determine # if the validation should occur (e.g. <tt>if: :allow_validation</tt>, # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method, @@ -143,7 +145,7 @@ module ActiveModel options = options.dup options[:if] = Array(options[:if]) options[:if].unshift lambda { |o| - o.validation_context == options[:on] + Array(options[:on]).include?(o.validation_context) } end args << options @@ -199,12 +201,12 @@ module ActiveModel # # #<StrictValidator:0x007fbff3204a30 @options={strict:true}> # # ] # - # If one runs Person.clear_validators! and then checks to see what + # If one runs <tt>Person.clear_validators!</tt> and then checks to see what # validators this class has, you would obtain: # # Person.validators # => [] # - # Also, the callback set by +validate :cannot_be_robot+ will be erased + # Also, the callback set by <tt>validate :cannot_be_robot</tt> will be erased # so that: # # Person._validate_callbacks.empty? # => true diff --git a/activemodel/lib/active_model/validations/absence.rb b/activemodel/lib/active_model/validations/absence.rb index 1a1863370b..9b5416fb1d 100644 --- a/activemodel/lib/active_model/validations/absence.rb +++ b/activemodel/lib/active_model/validations/absence.rb @@ -21,7 +21,7 @@ module ActiveModel # * <tt>:message</tt> - A custom error message (default is: "must be blank"). # # There is also a list of default options supported by every validator: - # +:if+, +:unless+, +:on+ and +:strict+. + # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. # See <tt>ActiveModel::Validation#validates</tt> for more information def validates_absence_of(*attr_names) validates_with AbsenceValidator, _merge_attributes(attr_names) diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index 139de16326..ac5e79859b 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -38,8 +38,6 @@ module ActiveModel # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "must be # accepted"). - # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default - # is +true+). # * <tt>:accept</tt> - Specifies value that is considered accepted. # The default value is a string "1", which makes it easy to relate to # an HTML checkbox. This should be set to +true+ if you are validating @@ -47,8 +45,8 @@ module ActiveModel # before validation. # # There is also a list of default options supported by every validator: - # +:if+, +:unless+, +:on+ and +:strict+. - # See <tt>ActiveModel::Validation#validates</tt> for more information + # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. + # See <tt>ActiveModel::Validation#validates</tt> for more information. def validates_acceptance_of(*attr_names) validates_with AcceptanceValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb index fde53b9f89..edfffdd3ce 100644 --- a/activemodel/lib/active_model/validations/callbacks.rb +++ b/activemodel/lib/active_model/validations/callbacks.rb @@ -1,6 +1,6 @@ module ActiveModel module Validations - # == Active \Model Validation Callbacks + # == Active \Model \Validation \Callbacks # # Provides an interface for any class to have +before_validation+ and # +after_validation+ callbacks. diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index b0542661af..a51523912f 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -57,7 +57,7 @@ module ActiveModel # confirmation"). # # There is also a list of default options supported by every validator: - # +:if+, +:unless+, +:on+ and +:strict+. + # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. # See <tt>ActiveModel::Validation#validates</tt> for more information def validates_confirmation_of(*attr_names) validates_with ConfirmationValidator, _merge_attributes(attr_names) diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index 48bf5cd802..f342d27275 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -34,13 +34,9 @@ module ActiveModel # <tt>Range#cover?</tt>, otherwise with <tt>include?</tt>. # * <tt>:message</tt> - Specifies a custom error message (default is: "is # reserved"). - # * <tt>:allow_nil</tt> - If set to true, skips this validation if the - # attribute is +nil+ (default is +false+). - # * <tt>:allow_blank</tt> - If set to true, skips this validation if the - # attribute is blank(default is +false+). # # There is also a list of default options supported by every validator: - # +:if+, +:unless+, +:on+ and +:strict+. + # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. # See <tt>ActiveModel::Validation#validates</tt> for more information def validates_exclusion_of(*attr_names) validates_with ExclusionValidator, _merge_attributes(attr_names) diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index f0fe22438f..ff3e95da34 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -91,10 +91,6 @@ module ActiveModel # # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "is invalid"). - # * <tt>:allow_nil</tt> - If set to true, skips this validation if the - # attribute is +nil+ (default is +false+). - # * <tt>:allow_blank</tt> - If set to true, skips this validation if the - # attribute is blank (default is +false+). # * <tt>:with</tt> - Regular expression that if the attribute matches will # result in a successful validation. This can be provided as a proc or # lambda returning regular expression which will be called at runtime. @@ -107,7 +103,7 @@ module ActiveModel # beginning or end of the string. These anchors are <tt>^</tt> and <tt>$</tt>. # # There is also a list of default options supported by every validator: - # +:if+, +:unless+, +:on+ and +:strict+. + # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. # See <tt>ActiveModel::Validation#validates</tt> for more information def validates_format_of(*attr_names) validates_with FormatValidator, _merge_attributes(attr_names) diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index 24337614c5..c84025f083 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -29,17 +29,14 @@ module ActiveModel # * <tt>:in</tt> - An enumerable object of available items. This can be # supplied as a proc, lambda or symbol which returns an enumerable. If the # enumerable is a numerical range the test is performed with <tt>Range#cover?</tt>, - # otherwise with <tt>include?</tt>. + # otherwise with <tt>include?</tt>. When using a proc or lambda the instance + # under validation is passed as an argument. # * <tt>:within</tt> - A synonym(or alias) for <tt>:in</tt> # * <tt>:message</tt> - Specifies a custom error message (default is: "is # not included in the list"). - # * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the - # attribute is +nil+ (default is +false+). - # * <tt>:allow_blank</tt> - If set to +true+, skips this validation if the - # attribute is blank (default is +false+). # # There is also a list of default options supported by every validator: - # +:if+, +:unless+, +:on+ and +:strict+. + # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. # See <tt>ActiveModel::Validation#validates</tt> for more information def validates_inclusion_of(*attr_names) validates_with InclusionValidator, _merge_attributes(attr_names) diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index ddfd8a342e..a96b30cadd 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -1,6 +1,6 @@ module ActiveModel - # == Active \Model Length \Validator + # == Active \Model Length Validator module Validations class LengthValidator < EachValidator # :nodoc: MESSAGES = { is: :wrong_length, minimum: :too_short, maximum: :too_long }.freeze diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index c8d3236463..a9fb9804d4 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -110,7 +110,7 @@ module ActiveModel # * <tt>:even</tt> - Specifies the value must be an even number. # # There is also a list of default options supported by every validator: - # +:if+, +:unless+, +:on+ and +:strict+ . + # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+ . # See <tt>ActiveModel::Validation#validates</tt> for more information # # The following checks can also be supplied with a proc or a symbol which diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb index ab8c8359fc..5d593274eb 100644 --- a/activemodel/lib/active_model/validations/presence.rb +++ b/activemodel/lib/active_model/validations/presence.rb @@ -29,7 +29,7 @@ module ActiveModel # * <tt>:message</tt> - A custom error message (default is: "can't be blank"). # # There is also a list of default options supported by every validator: - # +:if+, +:unless+, +:on+ and +:strict+. + # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. # See <tt>ActiveModel::Validation#validates</tt> for more information def validates_presence_of(*attr_names) validates_with PresenceValidator, _merge_attributes(attr_names) diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index bf588b7bd0..ae8d377fdf 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -83,7 +83,9 @@ module ActiveModel # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a +true+ or # +false+ value. - # * <tt>:strict</tt> - if the <tt>:strict</tt> option is set to true + # * <tt>:allow_nil</tt> - Skip validation if the attribute is +nil+. + # * <tt>:allow_blank</tt> - Skip validation if the attribute is blank. + # * <tt>:strict</tt> - If the <tt>:strict</tt> option is set to true # will raise ActiveModel::StrictValidationFailed instead of adding the error. # <tt>:strict</tt> option can also be set to any other exception. # diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index 690856aee1..bddacc8c45 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -61,7 +61,7 @@ module ActiveModel # end # # Note that the validator is initialized only once for the whole application - # lifecycle, and not on each validation run. + # life cycle, and not on each validation run. # # The easiest way to add custom validators for validating individual attributes # is with the convenient <tt>ActiveModel::EachValidator</tt>. @@ -83,7 +83,7 @@ module ActiveModel # end # # It can be useful to access the class that is using that validator when there are prerequisites such - # as an +attr_accessor+ being present. This class is accessable via +options[:class]+ in the constructor. + # as an +attr_accessor+ being present. This class is accessible via +options[:class]+ in the constructor. # To setup your validator override the constructor. # # class MyValidator < ActiveModel::Validator @@ -109,7 +109,7 @@ module ActiveModel deprecated_setup(options) end - # Return the kind for this validator. + # Returns the kind for this validator. # # PresenceValidator.new.kind # => :presence # UniquenessValidator.new.kind # => :uniqueness diff --git a/activemodel/lib/active_model/version.rb b/activemodel/lib/active_model/version.rb index 86340bba37..58ba3ab9b2 100644 --- a/activemodel/lib/active_model/version.rb +++ b/activemodel/lib/active_model/version.rb @@ -1,7 +1,7 @@ module ActiveModel # Returns the version of the currently loaded ActiveModel as a Gem::Version def self.version - Gem::Version.new "4.1.0.beta" + Gem::Version.new "4.1.0.beta1" end module VERSION #:nodoc: diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb index a90d0b1299..8b55901a65 100644 --- a/activemodel/test/cases/dirty_test.rb +++ b/activemodel/test/cases/dirty_test.rb @@ -67,12 +67,30 @@ class DirtyTest < ActiveModel::TestCase assert_equal [nil, "John"], @model.changes['name'] end + test "checking if an attribute has changed to a particular value" do + @model.name = "Ringo" + assert @model.name_changed?(from: nil, to: "Ringo") + assert_not @model.name_changed?(from: "Pete", to: "Ringo") + assert @model.name_changed?(to: "Ringo") + assert_not @model.name_changed?(to: "Pete") + assert @model.name_changed?(from: nil) + assert_not @model.name_changed?(from: "Pete") + end + test "changes accessible through both strings and symbols" do @model.name = "David" assert_not_nil @model.changes[:name] assert_not_nil @model.changes['name'] end + test "be consistent with symbols arguments after the changes are applied" do + @model.name = "David" + assert @model.attribute_changed?(:name) + @model.save + @model.name = 'Rafael' + assert @model.attribute_changed?(:name) + end + test "attribute mutation" do @model.instance_variable_set("@name", "Yam") assert !@model.name_changed? diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index 4e07e0e00b..def28578f8 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -11,7 +11,7 @@ class ErrorsTest < ActiveModel::TestCase attr_reader :errors def validate! - errors.add(:name, "can not be nil") if name == nil + errors.add(:name, "cannot be nil") if name == nil end def read_attribute_for_validation(attr) @@ -51,7 +51,12 @@ class ErrorsTest < ActiveModel::TestCase def test_has_key? errors = ActiveModel::Errors.new(self) errors[:foo] = 'omg' - assert errors.has_key?(:foo), 'errors should have key :foo' + assert_equal true, errors.has_key?(:foo), 'errors should have key :foo' + end + + def test_has_no_key + errors = ActiveModel::Errors.new(self) + assert_equal false, errors.has_key?(:name), 'errors should not have key :name' end test "clear errors" do @@ -104,8 +109,8 @@ class ErrorsTest < ActiveModel::TestCase test "adding errors using conditionals with Person#validate!" do person = Person.new person.validate! - assert_equal ["name can not be nil"], person.errors.full_messages - assert_equal ["can not be nil"], person.errors[:name] + assert_equal ["name cannot be nil"], person.errors.full_messages + assert_equal ["cannot be nil"], person.errors[:name] end test "assign error" do @@ -116,8 +121,8 @@ class ErrorsTest < ActiveModel::TestCase test "add an error message on a specific attribute" do person = Person.new - person.errors.add(:name, "can not be blank") - assert_equal ["can not be blank"], person.errors[:name] + person.errors.add(:name, "cannot be blank") + assert_equal ["cannot be blank"], person.errors[:name] end test "add an error with a symbol" do @@ -129,15 +134,15 @@ class ErrorsTest < ActiveModel::TestCase test "add an error with a proc" do person = Person.new - message = Proc.new { "can not be blank" } + message = Proc.new { "cannot be blank" } person.errors.add(:name, message) - assert_equal ["can not be blank"], person.errors[:name] + assert_equal ["cannot be blank"], person.errors[:name] end test "added? detects if a specific error was added to the object" do person = Person.new - person.errors.add(:name, "can not be blank") - assert person.errors.added?(:name, "can not be blank") + person.errors.add(:name, "cannot be blank") + assert person.errors.added?(:name, "cannot be blank") end test "added? handles symbol message" do @@ -148,7 +153,7 @@ class ErrorsTest < ActiveModel::TestCase test "added? handles proc messages" do person = Person.new - message = Proc.new { "can not be blank" } + message = Proc.new { "cannot be blank" } person.errors.add(:name, message) assert person.errors.added?(:name, message) end @@ -161,9 +166,9 @@ class ErrorsTest < ActiveModel::TestCase test "added? matches the given message when several errors are present for the same attribute" do person = Person.new - person.errors.add(:name, "can not be blank") + person.errors.add(:name, "cannot be blank") person.errors.add(:name, "is invalid") - assert person.errors.added?(:name, "can not be blank") + assert person.errors.added?(:name, "cannot be blank") end test "added? returns false when no errors are present" do @@ -174,52 +179,52 @@ class ErrorsTest < ActiveModel::TestCase test "added? returns false when checking a nonexisting error and other errors are present for the given attribute" do person = Person.new person.errors.add(:name, "is invalid") - assert !person.errors.added?(:name, "can not be blank") + assert !person.errors.added?(:name, "cannot be blank") end test "size calculates the number of error messages" do person = Person.new - person.errors.add(:name, "can not be blank") + person.errors.add(:name, "cannot be blank") assert_equal 1, person.errors.size end test "to_a returns the list of errors with complete messages containing the attribute names" do person = Person.new - person.errors.add(:name, "can not be blank") - person.errors.add(:name, "can not be nil") - assert_equal ["name can not be blank", "name can not be nil"], person.errors.to_a + person.errors.add(:name, "cannot be blank") + person.errors.add(:name, "cannot be nil") + assert_equal ["name cannot be blank", "name cannot be nil"], person.errors.to_a end test "to_hash returns the error messages hash" do person = Person.new - person.errors.add(:name, "can not be blank") - assert_equal({ name: ["can not be blank"] }, person.errors.to_hash) + person.errors.add(:name, "cannot be blank") + assert_equal({ name: ["cannot be blank"] }, person.errors.to_hash) end test "full_messages creates a list of error messages with the attribute name included" do person = Person.new - person.errors.add(:name, "can not be blank") - person.errors.add(:name, "can not be nil") - assert_equal ["name can not be blank", "name can not be nil"], person.errors.full_messages + person.errors.add(:name, "cannot be blank") + person.errors.add(:name, "cannot be nil") + assert_equal ["name cannot be blank", "name cannot be nil"], person.errors.full_messages end test "full_messages_for contains all the error messages for the given attribute" do person = Person.new - person.errors.add(:name, "can not be blank") - person.errors.add(:name, "can not be nil") - assert_equal ["name can not be blank", "name can not be nil"], person.errors.full_messages_for(:name) + person.errors.add(:name, "cannot be blank") + person.errors.add(:name, "cannot be nil") + assert_equal ["name cannot be blank", "name cannot be nil"], person.errors.full_messages_for(:name) end test "full_messages_for does not contain error messages from other attributes" do person = Person.new - person.errors.add(:name, "can not be blank") - person.errors.add(:email, "can not be blank") - assert_equal ["name can not be blank"], person.errors.full_messages_for(:name) + person.errors.add(:name, "cannot be blank") + person.errors.add(:email, "cannot be blank") + assert_equal ["name cannot be blank"], person.errors.full_messages_for(:name) end test "full_messages_for returns an empty list in case there are no errors for the given attribute" do person = Person.new - person.errors.add(:name, "can not be blank") + person.errors.add(:name, "cannot be blank") assert_equal [], person.errors.full_messages_for(:email) end @@ -230,22 +235,22 @@ class ErrorsTest < ActiveModel::TestCase test "full_message returns the given message with the attribute name included" do person = Person.new - assert_equal "name can not be blank", person.errors.full_message(:name, "can not be blank") - assert_equal "name_test can not be blank", person.errors.full_message(:name_test, "can not be blank") + assert_equal "name cannot be blank", person.errors.full_message(:name, "cannot be blank") + assert_equal "name_test cannot be blank", person.errors.full_message(:name_test, "cannot be blank") end test "as_json creates a json formatted representation of the errors hash" do person = Person.new person.validate! - assert_equal({ name: ["can not be nil"] }, person.errors.as_json) + assert_equal({ name: ["cannot be nil"] }, person.errors.as_json) end test "as_json with :full_messages option creates a json formatted representation of the errors containing complete messages" do person = Person.new person.validate! - assert_equal({ name: ["name can not be nil"] }, person.errors.as_json(full_messages: true)) + assert_equal({ name: ["name cannot be nil"] }, person.errors.as_json(full_messages: true)) end test "generate_message works without i18n_scope" do diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb index 7a63674757..522a7cebb4 100644 --- a/activemodel/test/cases/helper.rb +++ b/activemodel/test/cases/helper.rb @@ -7,4 +7,7 @@ require 'active_support/core_ext/string/access' # Show backtraces for deprecated behavior for quicker cleanup. ActiveSupport::Deprecation.debug = true +# Disable available locale checks to avoid warnings running the test suite. +I18n.enforce_available_locales = false + require 'active_support/testing/autorun' diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb index 41d0b2263e..82fd291064 100644 --- a/activemodel/test/cases/secure_password_test.rb +++ b/activemodel/test/cases/secure_password_test.rb @@ -1,8 +1,6 @@ require 'cases/helper' require 'models/user' -require 'models/oauthed_user' require 'models/visitor' -require 'models/administrator' class SecurePasswordTest < ActiveModel::TestCase setup do @@ -10,69 +8,152 @@ class SecurePasswordTest < ActiveModel::TestCase @user = User.new @visitor = Visitor.new - @oauthed_user = OauthedUser.new + + # Simulate loading an existing user from the DB + @existing_user = User.new + @existing_user.password_digest = BCrypt::Password.create('password', cost: BCrypt::Engine::MIN_COST) end teardown do ActiveModel::SecurePassword.min_cost = false end - test "blank password" do - @user.password = @visitor.password = '' - assert !@user.valid?(:create), 'user should be invalid' + test "create and updating without validations" do assert @visitor.valid?(:create), 'visitor should be valid' - end + assert @visitor.valid?(:update), 'visitor should be valid' + + @visitor.password = '123' + @visitor.password_confirmation = '456' - test "nil password" do - @user.password = @visitor.password = nil - assert !@user.valid?(:create), 'user should be invalid' assert @visitor.valid?(:create), 'visitor should be valid' + assert @visitor.valid?(:update), 'visitor should be valid' end - test "blank password doesn't override previous password" do - @user.password = 'test' + test "create a new user with validation and a blank password" do @user.password = '' - assert_equal @user.password, 'test' + assert !@user.valid?(:create), 'user should be invalid' + assert_equal 1, @user.errors.count + assert_equal ["can't be blank"], @user.errors[:password] + end + + test "create a new user with validation and a nil password" do + @user.password = nil + assert !@user.valid?(:create), 'user should be invalid' + assert_equal 1, @user.errors.count + assert_equal ["can't be blank"], @user.errors[:password] + end + + test "create a new user with validation and a blank password confirmation" do + @user.password = 'password' + @user.password_confirmation = '' + assert !@user.valid?(:create), 'user should be invalid' + assert_equal 1, @user.errors.count + assert_equal ["doesn't match Password"], @user.errors[:password_confirmation] end - test "password must be present" do - assert !@user.valid?(:create) - assert_equal 1, @user.errors.size + test "create a new user with validation and a nil password confirmation" do + @user.password = 'password' + @user.password_confirmation = nil + assert @user.valid?(:create), 'user should be valid' end - test "match confirmation" do - @user.password = @visitor.password = "thiswillberight" - @user.password_confirmation = @visitor.password_confirmation = "wrong" + test "create a new user with validation and an incorrect password confirmation" do + @user.password = 'password' + @user.password_confirmation = 'something else' + assert !@user.valid?(:create), 'user should be invalid' + assert_equal 1, @user.errors.count + assert_equal ["doesn't match Password"], @user.errors[:password_confirmation] + end - assert !@user.valid? - assert @visitor.valid? + test "create a new user with validation and a correct password confirmation" do + @user.password = 'password' + @user.password_confirmation = 'something else' + assert !@user.valid?(:create), 'user should be invalid' + assert_equal 1, @user.errors.count + assert_equal ["doesn't match Password"], @user.errors[:password_confirmation] + end - @user.password_confirmation = "thiswillberight" + test "update an existing user with validation and no change in password" do + assert @existing_user.valid?(:update), 'user should be valid' + end - assert @user.valid? + test "updating an existing user with validation and a blank password" do + @existing_user.password = '' + assert @existing_user.valid?(:update), 'user should be valid' end - test "authenticate" do - @user.password = "secret" + test "updating an existing user with validation and a blank password and password_confirmation" do + @existing_user.password = '' + @existing_user.password_confirmation = '' + assert @existing_user.valid?(:update), 'user should be valid' + end - assert !@user.authenticate("wrong") - assert @user.authenticate("secret") + test "updating an existing user with validation and a nil password" do + @existing_user.password = nil + assert !@existing_user.valid?(:update), 'user should be invalid' + assert_equal 1, @existing_user.errors.count + assert_equal ["can't be blank"], @existing_user.errors[:password] + end + + test "updating an existing user with validation and a blank password confirmation" do + @existing_user.password = 'password' + @existing_user.password_confirmation = '' + assert !@existing_user.valid?(:update), 'user should be invalid' + assert_equal 1, @existing_user.errors.count + assert_equal ["doesn't match Password"], @existing_user.errors[:password_confirmation] end - test "User should not be created with blank digest" do - assert_raise RuntimeError do - @user.run_callbacks :create - end - @user.password = "supersecretpassword" - assert_nothing_raised do - @user.run_callbacks :create - end + test "updating an existing user with validation and a nil password confirmation" do + @existing_user.password = 'password' + @existing_user.password_confirmation = nil + assert @existing_user.valid?(:update), 'user should be valid' end - test "Oauthed user can be created with blank digest" do - assert_nothing_raised do - @oauthed_user.run_callbacks :create - end + test "updating an existing user with validation and an incorrect password confirmation" do + @existing_user.password = 'password' + @existing_user.password_confirmation = 'something else' + assert !@existing_user.valid?(:update), 'user should be invalid' + assert_equal 1, @existing_user.errors.count + assert_equal ["doesn't match Password"], @existing_user.errors[:password_confirmation] + end + + test "updating an existing user with validation and a correct password confirmation" do + @existing_user.password = 'password' + @existing_user.password_confirmation = 'something else' + assert !@existing_user.valid?(:update), 'user should be invalid' + assert_equal 1, @existing_user.errors.count + assert_equal ["doesn't match Password"], @existing_user.errors[:password_confirmation] + end + + test "updating an existing user with validation and a blank password digest" do + @existing_user.password_digest = '' + assert !@existing_user.valid?(:update), 'user should be invalid' + assert_equal 1, @existing_user.errors.count + assert_equal ["can't be blank"], @existing_user.errors[:password] + end + + test "updating an existing user with validation and a nil password digest" do + @existing_user.password_digest = nil + assert !@existing_user.valid?(:update), 'user should be invalid' + assert_equal 1, @existing_user.errors.count + assert_equal ["can't be blank"], @existing_user.errors[:password] + end + + test "setting a blank password should not change an existing password" do + @existing_user.password = '' + assert @existing_user.password_digest == 'password' + end + + test "setting a nil password should clear an existing password" do + @existing_user.password = nil + assert_equal nil, @existing_user.password_digest + end + + test "authenticate" do + @user.password = "secret" + + assert !@user.authenticate("wrong") + assert @user.authenticate("secret") end test "Password digest cost defaults to bcrypt default cost when min_cost is false" do @@ -96,24 +177,4 @@ class SecurePasswordTest < ActiveModel::TestCase @user.password = "secret" assert_equal BCrypt::Engine::MIN_COST, @user.password_digest.cost end - - test "blank password_confirmation does not result in a confirmation error" do - @user.password = "" - @user.password_confirmation = "" - assert @user.valid?(:update), "user should be valid" - end - - test "password_confirmation validations will not be triggered if password_confirmation is not sent" do - @user.password = "password" - assert @user.valid?(:create) - end - - test "will not save if confirmation is blank but password is not" do - @user.password = "password" - @user.password_confirmation = "" - assert_not @user.valid?(:create) - - @user.password_confirmation = "password" - assert @user.valid?(:create) - end end diff --git a/activemodel/test/cases/validations/absence_validation_test.rb b/activemodel/test/cases/validations/absence_validation_test.rb index c05d71de5a..795ce16d28 100644 --- a/activemodel/test/cases/validations/absence_validation_test.rb +++ b/activemodel/test/cases/validations/absence_validation_test.rb @@ -6,9 +6,9 @@ require 'models/custom_reader' class AbsenceValidationTest < ActiveModel::TestCase teardown do - Topic.reset_callbacks(:validate) - Person.reset_callbacks(:validate) - CustomReader.reset_callbacks(:validate) + Topic.clear_validators! + Person.clear_validators! + CustomReader.clear_validators! end def test_validate_absences diff --git a/activemodel/test/cases/validations/acceptance_validation_test.rb b/activemodel/test/cases/validations/acceptance_validation_test.rb index dc413bef30..e78aa1adaf 100644 --- a/activemodel/test/cases/validations/acceptance_validation_test.rb +++ b/activemodel/test/cases/validations/acceptance_validation_test.rb @@ -8,7 +8,7 @@ require 'models/person' class AcceptanceValidationTest < ActiveModel::TestCase def teardown - Topic.reset_callbacks(:validate) + Topic.clear_validators! end def test_terms_of_service_agreement_no_acceptance @@ -63,6 +63,6 @@ class AcceptanceValidationTest < ActiveModel::TestCase p.karma = "1" assert p.valid? ensure - Person.reset_callbacks(:validate) + Person.clear_validators! end end diff --git a/activemodel/test/cases/validations/conditional_validation_test.rb b/activemodel/test/cases/validations/conditional_validation_test.rb index 5049d6dd61..1261937b56 100644 --- a/activemodel/test/cases/validations/conditional_validation_test.rb +++ b/activemodel/test/cases/validations/conditional_validation_test.rb @@ -6,7 +6,7 @@ require 'models/topic' class ConditionalValidationTest < ActiveModel::TestCase def teardown - Topic.reset_callbacks(:validate) + Topic.clear_validators! end def test_if_validation_using_method_true diff --git a/activemodel/test/cases/validations/confirmation_validation_test.rb b/activemodel/test/cases/validations/confirmation_validation_test.rb index f03de2c24a..4957ba5d0a 100644 --- a/activemodel/test/cases/validations/confirmation_validation_test.rb +++ b/activemodel/test/cases/validations/confirmation_validation_test.rb @@ -7,7 +7,7 @@ require 'models/person' class ConfirmationValidationTest < ActiveModel::TestCase def teardown - Topic.reset_callbacks(:validate) + Topic.clear_validators! end def test_no_title_confirmation @@ -49,7 +49,7 @@ class ConfirmationValidationTest < ActiveModel::TestCase p.karma = "None" assert p.valid? ensure - Person.reset_callbacks(:validate) + Person.clear_validators! end def test_title_confirmation_with_i18n_attribute diff --git a/activemodel/test/cases/validations/exclusion_validation_test.rb b/activemodel/test/cases/validations/exclusion_validation_test.rb index 81455ba519..1ce41f9bc9 100644 --- a/activemodel/test/cases/validations/exclusion_validation_test.rb +++ b/activemodel/test/cases/validations/exclusion_validation_test.rb @@ -7,7 +7,7 @@ require 'models/person' class ExclusionValidationTest < ActiveModel::TestCase def teardown - Topic.reset_callbacks(:validate) + Topic.clear_validators! end def test_validates_exclusion_of @@ -50,7 +50,7 @@ class ExclusionValidationTest < ActiveModel::TestCase p.karma = "Lifo" assert p.valid? ensure - Person.reset_callbacks(:validate) + Person.clear_validators! end def test_validates_exclusion_of_with_lambda @@ -87,6 +87,6 @@ class ExclusionValidationTest < ActiveModel::TestCase assert p.valid? ensure - Person.reset_callbacks(:validate) + Person.clear_validators! end end diff --git a/activemodel/test/cases/validations/format_validation_test.rb b/activemodel/test/cases/validations/format_validation_test.rb index 26e8dbf19c..0f91b73cd7 100644 --- a/activemodel/test/cases/validations/format_validation_test.rb +++ b/activemodel/test/cases/validations/format_validation_test.rb @@ -7,7 +7,7 @@ require 'models/person' class PresenceValidationTest < ActiveModel::TestCase def teardown - Topic.reset_callbacks(:validate) + Topic.clear_validators! end def test_validate_format @@ -68,11 +68,11 @@ class PresenceValidationTest < ActiveModel::TestCase assert t.invalid? assert_equal ["can't be Invalid title"], t.errors[:title] end - + def test_validate_format_of_with_multiline_regexp_should_raise_error assert_raise(ArgumentError) { Topic.validates_format_of(:title, with: /^Valid Title$/) } end - + def test_validate_format_of_with_multiline_regexp_and_option assert_nothing_raised(ArgumentError) do Topic.validates_format_of(:title, with: /^Valid Title$/, multiline: true) @@ -144,6 +144,6 @@ class PresenceValidationTest < ActiveModel::TestCase p.karma = "1234" assert p.valid? ensure - Person.reset_callbacks(:validate) + Person.clear_validators! end end 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 40a5aee997..93600c587a 100644 --- a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb @@ -4,7 +4,7 @@ require 'models/person' class I18nGenerateMessageValidationTest < ActiveModel::TestCase def setup - Person.reset_callbacks(:validate) + Person.clear_validators! @person = Person.new end diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb index e29771d6b7..d10010537e 100644 --- a/activemodel/test/cases/validations/i18n_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_validation_test.rb @@ -6,7 +6,7 @@ require 'models/person' class I18nValidationTest < ActiveModel::TestCase def setup - Person.reset_callbacks(:validate) + Person.clear_validators! @person = Person.new @old_load_path, @old_backend = I18n.load_path.dup, I18n.backend @@ -16,7 +16,7 @@ class I18nValidationTest < ActiveModel::TestCase end def teardown - Person.reset_callbacks(:validate) + Person.clear_validators! I18n.load_path.replace @old_load_path I18n.backend = @old_backend end diff --git a/activemodel/test/cases/validations/inclusion_validation_test.rb b/activemodel/test/cases/validations/inclusion_validation_test.rb index 8b90856869..3a8f3080e1 100644 --- a/activemodel/test/cases/validations/inclusion_validation_test.rb +++ b/activemodel/test/cases/validations/inclusion_validation_test.rb @@ -8,7 +8,7 @@ require 'models/person' class InclusionValidationTest < ActiveModel::TestCase def teardown - Topic.reset_callbacks(:validate) + Topic.clear_validators! end def test_validates_inclusion_of_range @@ -105,7 +105,7 @@ class InclusionValidationTest < ActiveModel::TestCase p.karma = "monkey" assert p.valid? ensure - Person.reset_callbacks(:validate) + Person.clear_validators! end def test_validates_inclusion_of_with_lambda @@ -142,6 +142,6 @@ class InclusionValidationTest < ActiveModel::TestCase assert p.valid? ensure - Person.reset_callbacks(:validate) + Person.clear_validators! end end diff --git a/activemodel/test/cases/validations/length_validation_test.rb b/activemodel/test/cases/validations/length_validation_test.rb index 8b2f886cc4..046ffcb16f 100644 --- a/activemodel/test/cases/validations/length_validation_test.rb +++ b/activemodel/test/cases/validations/length_validation_test.rb @@ -6,7 +6,7 @@ require 'models/person' class LengthValidationTest < ActiveModel::TestCase def teardown - Topic.reset_callbacks(:validate) + Topic.clear_validators! end def test_validates_length_of_with_allow_nil @@ -354,7 +354,7 @@ class LengthValidationTest < ActiveModel::TestCase p.karma = "The Smiths" assert p.valid? ensure - Person.reset_callbacks(:validate) + Person.clear_validators! end def test_validates_length_of_for_infinite_maxima diff --git a/activemodel/test/cases/validations/numericality_validation_test.rb b/activemodel/test/cases/validations/numericality_validation_test.rb index 84332ed014..f77cf47fb7 100644 --- a/activemodel/test/cases/validations/numericality_validation_test.rb +++ b/activemodel/test/cases/validations/numericality_validation_test.rb @@ -9,7 +9,7 @@ require 'bigdecimal' class NumericalityValidationTest < ActiveModel::TestCase def teardown - Topic.reset_callbacks(:validate) + Topic.clear_validators! end NIL = [nil] @@ -157,7 +157,7 @@ class NumericalityValidationTest < ActiveModel::TestCase p.karma = "1234" assert p.valid? ensure - Person.reset_callbacks(:validate) + Person.clear_validators! end def test_validates_numericality_with_invalid_args diff --git a/activemodel/test/cases/validations/presence_validation_test.rb b/activemodel/test/cases/validations/presence_validation_test.rb index 2f228cfa83..ecf16d1e16 100644 --- a/activemodel/test/cases/validations/presence_validation_test.rb +++ b/activemodel/test/cases/validations/presence_validation_test.rb @@ -8,9 +8,9 @@ require 'models/custom_reader' class PresenceValidationTest < ActiveModel::TestCase teardown do - Topic.reset_callbacks(:validate) - Person.reset_callbacks(:validate) - CustomReader.reset_callbacks(:validate) + Topic.clear_validators! + Person.clear_validators! + CustomReader.clear_validators! end def test_validate_presences diff --git a/activemodel/test/cases/validations/validates_test.rb b/activemodel/test/cases/validations/validates_test.rb index c1914b32bc..699a872e42 100644 --- a/activemodel/test/cases/validations/validates_test.rb +++ b/activemodel/test/cases/validations/validates_test.rb @@ -11,9 +11,9 @@ class ValidatesTest < ActiveModel::TestCase teardown :reset_callbacks def reset_callbacks - Person.reset_callbacks(:validate) - Topic.reset_callbacks(:validate) - PersonWithValidator.reset_callbacks(:validate) + Person.clear_validators! + Topic.clear_validators! + PersonWithValidator.clear_validators! end def test_validates_with_messages_empty diff --git a/activemodel/test/cases/validations/validations_context_test.rb b/activemodel/test/cases/validations/validations_context_test.rb index 5f99b320a6..005bf118c6 100644 --- a/activemodel/test/cases/validations/validations_context_test.rb +++ b/activemodel/test/cases/validations/validations_context_test.rb @@ -4,10 +4,8 @@ require 'cases/helper' require 'models/topic' class ValidationsContextTest < ActiveModel::TestCase - def teardown - Topic.reset_callbacks(:validate) - Topic._validators.clear + Topic.clear_validators! end ERROR_MESSAGE = "Validation error from validator" @@ -36,4 +34,17 @@ class ValidationsContextTest < ActiveModel::TestCase assert topic.invalid?(:create), "Validation does run on create if 'on' is set to create" assert topic.errors[:base].include?(ERROR_MESSAGE) end + + test "with a class that adds errors on multiple contexts and validating a new model" do + Topic.validates_with(ValidatorThatAddsErrors, on: [:context1, :context2]) + + topic = Topic.new + assert topic.valid?, "Validation ran with no context given when 'on' is set to context1 and context2" + + assert topic.invalid?(:context1), "Validation did not run on context1 when 'on' is set to context1 and context2" + assert topic.errors[:base].include?(ERROR_MESSAGE) + + assert topic.invalid?(:context2), "Validation did not run on context2 when 'on' is set to context1 and context2" + assert topic.errors[:base].include?(ERROR_MESSAGE) + end end diff --git a/activemodel/test/cases/validations/with_validation_test.rb b/activemodel/test/cases/validations/with_validation_test.rb index 93716f1433..736c2deea8 100644 --- a/activemodel/test/cases/validations/with_validation_test.rb +++ b/activemodel/test/cases/validations/with_validation_test.rb @@ -6,8 +6,7 @@ require 'models/topic' class ValidatesWithTest < ActiveModel::TestCase def teardown - Topic.reset_callbacks(:validate) - Topic._validators.clear + Topic.clear_validators! end ERROR_MESSAGE = "Validation error from validator" diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb index 039b6b8872..bee8ece992 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -10,17 +10,10 @@ require 'active_support/json' require 'active_support/xml_mini' class ValidationsTest < ActiveModel::TestCase - class CustomStrictValidationException < StandardError; end - def setup - Topic._validators.clear - end - - # Most of the tests mess with the validations of Topic, so lets repair it all the time. - # Other classes we mess with will be dealt with in the specific tests def teardown - Topic.reset_callbacks(:validate) + Topic.clear_validators! end def test_single_field_validation diff --git a/activemodel/test/models/administrator.rb b/activemodel/test/models/administrator.rb deleted file mode 100644 index 2f3aff290c..0000000000 --- a/activemodel/test/models/administrator.rb +++ /dev/null @@ -1,11 +0,0 @@ -class Administrator - extend ActiveModel::Callbacks - include ActiveModel::Validations - include ActiveModel::SecurePassword - - define_model_callbacks :create - - attr_accessor :name, :password_digest - - has_secure_password -end diff --git a/activemodel/test/models/oauthed_user.rb b/activemodel/test/models/oauthed_user.rb deleted file mode 100644 index 9750bc19d4..0000000000 --- a/activemodel/test/models/oauthed_user.rb +++ /dev/null @@ -1,11 +0,0 @@ -class OauthedUser - extend ActiveModel::Callbacks - include ActiveModel::Validations - include ActiveModel::SecurePassword - - define_model_callbacks :create - - has_secure_password(validations: false) - - attr_accessor :password_digest, :password_salt -end diff --git a/activemodel/test/models/user.rb b/activemodel/test/models/user.rb index 4b11df12bf..cbe259b1ad 100644 --- a/activemodel/test/models/user.rb +++ b/activemodel/test/models/user.rb @@ -7,5 +7,5 @@ class User has_secure_password - attr_accessor :password_digest, :password_salt + attr_accessor :password_digest end diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 1386c5d740..fe0d7b2b35 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -3,7 +3,566 @@ *Marc-André Lafortune* -* Polymorphic belongs_to associations with the `touch: true` option set update the timestamps of +* `enum` now raises on "dangerous" name conflicts + + Dangerous name conflicts includes instance or class method conflicts + with methods defined within `ActiveRecord::Base` but not its ancestors, + as well as conflicts with methods generated by other enums on the same + class. + + Fixes #13389. + + *Godfrey Chan* + +* `scope` now raises on "dangerous" name conflicts + + Similar to dangerous attribute methods, a scope name conflict is + dangerous if it conflicts with an existing class method defined within + `ActiveRecord::Base` but not its ancestors. + + See also #13389. + + *Godfrey Chan*, *Philippe Creux* + +* Correctly send an user provided statement to a `lock!()` call. + + person.lock! 'FOR SHARE NOWAIT' + # Before: SELECT * ... LIMIT 1 FOR UPDATE + # After: SELECT * ... LIMIT 1 FOR SHARE NOWAIT + + Fixes #13788. + + *Maurício Linhares* + +* Handle aliased attributes `select()`, `order()` and `reorder()`. + + *Tsutomu Kuroda* + +* Reset the collection association when calling `reset` on it. + + Before: + + post.comments.loaded? # => true + post.comments.reset + post.comments.loaded? # => true + + After: + + post.comments.loaded? # => true + post.comments.reset + post.comments.loaded? # => false + + Fixes #13777. + + *Kelsey Schlarman* + +* Make enum fields work as expected with the `ActiveModel::Dirty` API. + + Before this change, using the dirty API would have surprising results: + + conversation = Conversation.new + conversation.status = :active + conversation.status = :archived + conversation.status_was # => 0 + + After this change, the same code would result in: + + conversation = Conversation.new + conversation.status = :active + conversation.status = :archived + conversation.status_was # => "active" + + *Rafael Mendonça França* + +* `has_one` and `belongs_to` accessors don't add ORDER BY to the queries anymore. + + Since Rails 4.0, we add an ORDER BY in the `first` method to ensure consistent results + among different database engines. But for singular associations this behavior is not needed + since we will have one record to return. As this ORDER BY option can lead some performance + issues we are removing it for singular associations accessors. + + Fixes #12623. + + *Rafael Mendonça França* + +* Prepend table name for column names passed to `Relation#select`. + + Example: + + Post.select(:id) + # Before: => SELECT id FROM "posts" + # After: => SELECT "posts"."id" FROM "posts" + + *Yves Senn* + +* Fail early with "Primary key not included in the custom select clause" + in `find_in_batches`. + + Before this patch, the exception was raised after the first batch was + yielded to the block. This means that you only get it, when you hit the + `batch_size` treshold. This could shadow the issue in development. + + *Alexander Balashov* + +* Ensure `second` through `fifth` methods act like the `first` finder. + + The famous ordinal Array instance methods defined in ActiveSupport + (`first`, `second`, `third`, `fourth`, and `fifth`) are now available as + full-fledged finders in ActiveRecord. The biggest benefit of this is ordering + of the records returned now defaults to the table's primary key in ascending order. + + Fixes #13743. + + Example: + + User.all.second + + # Before + # => 'SELECT "users".* FROM "users"' + + # After + # => SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1 OFFSET 1' + + User.offset(3).second + + # Before + # => 'SELECT "users".* FROM "users" LIMIT -1 OFFSET 3' # sqlite3 gem + # => 'SELECT "users".* FROM "users" OFFSET 3' # pg gem + # => 'SELECT `users`.* FROM `users` LIMIT 18446744073709551615 OFFSET 3' # mysql2 gem + + # After + # => SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1 OFFSET 4' + + *Jason Meller* + +* ActiveRecord states are now correctly restored after a rollback for + models that did not define any transactional callbacks (i.e. + `after_commit`, `after_rollback` or `after_create`). + + Fixes #13744. + + *Godfrey Chan* + +* Make `touch` fire the `after_commit` and `after_rollback` callbacks. + + *Harry Brundage* + +* Enable partial indexes for sqlite >= 3.8.0 + + See http://www.sqlite.org/partialindex.html + + *Cody Cutrer* + +* Don't try to get the subclass if the inheritance column doesn't exist + + The `subclass_from_attrs` method is called even if the column specified by + the `inheritance_column` setting doesn't exist. This prevents setting associations + via the attributes hash if the association name clashes with the value of the setting, + typically `:type`. This worked previously in Rails 3.2. + + *Ujjwal Thaakar* + +* Enum mappings are now exposed via class methods instead of constants. + + Example: + + class Conversation < ActiveRecord::Base + enum status: [ :active, :archived ] + end + + Before: + + Conversation::STATUS # => { "active" => 0, "archived" => 1 } + + After: + + Conversation.statuses # => { "active" => 0, "archived" => 1 } + + *Godfrey Chan* + +* Set `NameError#name` when STI-class-lookup fails. + + *Chulki Lee* + +* Fix bug in `becomes!` when changing from the base model to a STI sub-class. + + Fixes #13272. + + *the-web-dev*, *Yves Senn* + +* Currently Active Record can be configured via the environment variable + `DATABASE_URL` or by manually injecting a hash of values which is what Rails does, + reading in `database.yml` and setting Active Record appropriately. Active Record + expects to be able to use `DATABASE_URL` without the use of Rails, and we cannot + rip out this functionality without deprecating. This presents a problem though + when both config is set, and a `DATABASE_URL` is present. Currently the + `DATABASE_URL` should "win" and none of the values in `database.yml` are + used. This is somewhat unexpected, if one were to set values such as + `pool` in the `production:` group of `database.yml` they are ignored. + + There are many ways that Active Record initiates a connection today: + + - Stand Alone (without rails) + - `rake db:<tasks>` + - `ActiveRecord.establish_connection` + + - With Rails + - `rake db:<tasks>` + - `rails <server> | <console>` + - `rails dbconsole` + + Now all of these behave exactly the same way. The best way to do + this is to put all of this logic in one place so it is guaranteed to be used. + + Here is the matrix of how this behavior works: + + ``` + No database.yml + No DATABASE_URL + => Error + ``` + + ``` + database.yml present + No DATABASE_URL + => Use database.yml configuration + ``` + + ``` + No database.yml + DATABASE_URL present + => use DATABASE_URL configuration + ``` + + ``` + database.yml present + DATABASE_URL present + => Merged into `url` sub key. If both specify `url` sub key, the `database.yml` `url` + sub key "wins". If other paramaters `adapter` or `database` are specified in YAML, + they are discarded as the `url` sub key "wins". + ``` + + Current implementation uses `ActiveRecord::Base.configurations` to resolve and merge + all connection information before returning. This is achieved through a utility + class: `ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig`. + + To understand the exact behavior of this class, it is best to review the + behavior in `activerecord/test/cases/connection_adapters/connection_handler_test.rb`. + + *Richard Schneeman* + +* Make `change_column_null` revertable. Fixes #13576. + + *Yves Senn*, *Nishant Modak*, *Prathamesh Sonpatki* + +* Don't create/drop the test database if RAILS_ENV is specified explicitly. + + Previously, when the environment was development, we would always + create or drop both the test and development databases. + + Now, if RAILS_ENV is explicitly defined as development, we don't create + the test database. + + *Damien Mathieu* + +* Initialize version on Migration objects so that it can be used in a migration, + and it will be included in the announce message. + + *Dylan Thacker-Smith* + +* `change_table` now uses the current adapter's `update_table_definition` + method to retrieve a specific table definition. + This ensures that `change_table` and `create_table` will use + similar objects. + + Fixes #13577 and #13503. + + *Nishant Modak*, *Prathamesh Sonpatki*, *Rafael Mendonça França* + +* Fixed ActiveRecord::Store nil conversion TypeError when using YAML coder. + In case the YAML passed as paramter is nil, uses an empty string. + + Fixes #13570. + + *Thales Oliveira* + +* Deprecate unused `ActiveRecord::Base.symbolized_base_class` + and `ActiveRecord::Base.symbolized_sti_name` without replacement. + + *Yves Senn* + +* Since the `test_help.rb` file in Railties now automatically maintains + your test schema, the `rake db:test:*` tasks are deprecated. This + doesn't stop you manually running other tasks on your test database + if needed: + + rake db:schema:load RAILS_ENV=test + + *Jon Leighton* + +* Fix presence validator for association when the associated record responds to `to_a`. + + *gmarik* + +* Fixed regression on preload/includes with multiple arguments failing in certain conditions, + raising a NoMethodError internally by calling `reflect_on_association` for `NilClass:Class`. + + Fixes #13437. + + *Vipul A M*, *khustochka* + +* Add the ability to nullify the `enum` column. + + Example: + + class Conversation < ActiveRecord::Base + enum gender: [:female, :male] + end + + Conversation::GENDER # => { female: 0, male: 1 } + + # conversation.update! gender: 0 + conversation.female! + conversation.female? # => true + conversation.gender # => "female" + + # conversation.update! gender: nil + conversation.gender = nil + conversation.gender.nil? # => true + conversation.gender # => nil + + *Amr Tamimi* + +* Connection specification now accepts a "url" key. The value of this + key is expected to contain a database URL. The database URL will be + expanded into a hash and merged. + + *Richard Schneeman* + +* An `ArgumentError` is now raised on a call to `Relation#where.not(nil)`. + + Example: + + User.where.not(nil) + + # Before + # => 'SELECT `users`.* FROM `users` WHERE (NOT (NULL))' + + # After + # => ArgumentError, 'Invalid argument for .where.not(), got nil.' + + *Kuldeep Aggarwal* + +* Deprecated use of string argument as a configuration lookup in + `ActiveRecord::Base.establish_connection`. Instead, a symbol must be given. + + *José Valim* + +* Fixed `update_column`, `update_columns`, and `update_all` to correctly serialize + values for `array`, `hstore` and `json` column types in PostgreSQL. + + Fixes #12261. + + *Tadas Tamosauskas*, *Carlos Antonio da Silva* + +* Do not consider PostgreSQL array columns as number or text columns. + + The code uses these checks in several places to know what to do with a + particular column, for instance AR attribute query methods has a branch + like this: + + if column.number? + !value.zero? + end + + This should never be true for array columns, since it would be the same + as running [].zero?, which results in a NoMethodError exception. + + Fixing this by ensuring that array columns in PostgreSQL never return + true for number?/text? checks. + + *Carlos Antonio da Silva* + +* When connecting to a non-existant database, the error: + `ActiveRecord::NoDatabaseError` will now be raised. When being used with Rails + the error message will include information on how to create a database: + `rake db:create`. Supported adapters: postgresql, mysql, mysql2, sqlite3 + + *Richard Schneeman* + +* Do not raise `'cannot touch on a new record object'` exception on destroying + already destroyed `belongs_to` association with `touch: true` option. + + Fixes #13445. + + Example: + + # Given Comment has belongs_to :post, touch: true + comment.post.destroy + comment.destroy # no longer raises an error + + *Paul Nikitochkin* + +* Fix a bug when assigning an array containing string numbers to a + PostgreSQL integer array column. + + Fixes #13444. + + Example: + + # Given Book#ratings is of type :integer, array: true + Book.new(ratings: [1, 2]) # worked before + Book.new(ratings: ['1', '2']) # now works as well + + *Damien Mathieu* + +* Improve the default select when `from` is used. + + Previously, if you did something like Topic.from(:temp_topics), it + would generate SQL like: + + SELECT topics.* FROM temp_topics; + + Which is will cause an error since there's not a topics table to select + from. + + Now the default if you use from is just `*`: + + SELECT * FROM temp_topics; + + *Cody Cutrer* + +* Fix `PostgreSQL` insert to properly extract table name from multiline string SQL. + + Previously, executing an insert SQL in `PostgreSQL` with a command like this: + + insert into articles( + number) + values( + 5152 + ) + + would not work because the adapter was unable to extract the correct `articles` + table name. + + *Kuldeep Aggarwal* + +* `Relation` no longer has mutator methods like `#map!` and `#delete_if`. Convert + to an `Array` by calling `#to_a` before using these methods. + + It intends to prevent odd bugs and confusion in code that call mutator + methods directly on the `Relation`. + + Example: + + # Instead of this + Author.where(name: 'Hank Moody').compact! + + # Now you have to do this + authors = Author.where(name: 'Hank Moody').to_a + authors.compact! + + *Lauro Caetano* + +* Better support for `where()` conditions that use a `belongs_to` + association name. + + Using the name of an association in `where` previously worked only + if the value was a single `ActiveRecord::Base` object. e.g. + + Post.where(author: Author.first) + + Any other values, including `nil`, would cause invalid SQL to be + generated. This change supports arguments in the `where` query + conditions where the key is a `belongs_to` association name and the + value is `nil`, an `Array` of `ActiveRecord::Base` objects, or an + `ActiveRecord::Relation` object. + + class Post < ActiveRecord::Base + belongs_to :author + end + + `nil` value finds records where the association is not set: + + Post.where(author: nil) + # SELECT "posts".* FROM "posts" WHERE "posts"."author_id" IS NULL + + `Array` values find records where the association foreign key + matches the ids of the passed ActiveRecord models, resulting + in the same query as `Post.where(author_id: [1,2])`: + + authors_array = [Author.find(1), Author.find(2)] + Post.where(author: authors_array) + # SELECT "posts".* FROM "posts" WHERE "posts"."author_id" IN (1, 2) + + `ActiveRecord::Relation` values find records using the same + query as `Post.where(author_id: Author.where(last_name: "Emde"))` + + Post.where(author: Author.where(last_name: "Emde")) + # SELECT "posts".* FROM "posts" + # WHERE "posts"."author_id" IN ( + # SELECT "authors"."id" FROM "authors" + # WHERE "authors"."last_name" = 'Emde') + + Polymorphic `belongs_to` associations will continue to be handled + appropriately, with the polymorphic `association_type` field added + to the query to match the base class of the value. This feature + previously only worked when the value was a single `ActveRecord::Base`. + + class Post < ActiveRecord::Base + belongs_to :author, polymorphic: true + end + + Post.where(author: Author.where(last_name: "Emde")) + # Generates a query similar to: + Post.where(author_id: Author.where(last_name: "Emde"), author_type: "Author") + + *Martin Emde* + +* Respect temporary option when dropping tables with MySQL. + + Normal DROP TABLE also works, but commits the transaction. + + drop_table :temporary_table, temporary: true + + *Cody Cutrer* + +* Add option to create tables from a query. + + create_table(:long_query, temporary: true, + as: "SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id") + + Generates: + + CREATE TEMPORARY TABLE long_query AS + SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id + + *Cody Cutrer* + +* `db:test:clone` and `db:test:prepare` must load Rails environment. + + `db:test:clone` and `db:test:prepare` use `ActiveRecord::Base`. configurations, + so we need to load the Rails environment, otherwise the config wont be in place. + + *arthurnn* + +* Use the right column to type cast grouped calculations with custom expressions. + + Fixes #13230. + + Example: + + # Before + Account.group(:firm_name).sum('0.01 * credit_limit') + # => { '37signals' => '0.5' } + + # After + Account.group(:firm_name).sum('0.01 * credit_limit') + # => { '37signals' => 0.5 } + + *Paul Nikitochkin* + +* Polymorphic `belongs_to` associations with the `touch: true` option set update the timestamps of the old and new owner correctly when moved between owners of different types. Example: @@ -34,7 +593,7 @@ *kostya*, *Lauro Caetano* * `type_to_sql` returns a `String` for unmapped columns. This fixes an error - when using unmapped array types in PG + when using unmapped PostgreSQL array types. Example: @@ -73,7 +632,7 @@ * Update counter cache on a `has_many` relationship regardless of default scope. - Fix #12952. + Fixes #12952. *Uku Taht* @@ -84,9 +643,10 @@ *Cody Cutrer*, *Yves Senn* -* Raise `ActiveRecord::RecordNotDestroyed` when a replaced child marked with `dependent: destroy` fails to be destroyed. +* Raise `ActiveRecord::RecordNotDestroyed` when a replaced child + marked with `dependent: destroy` fails to be destroyed. - Fix #12812 + Fixex #12812. *Brian Thomas Storti* @@ -438,12 +998,6 @@ *Paul Nikitochkin* -* Deprecate the delegation of Array bang methods for associations. - To use them, instead first call `#to_a` on the association to access the - array to be acted on. - - *Ben Woosley* - * `CollectionAssociation#first`/`#last` (e.g. `has_many`) use a `LIMIT`ed query to fetch results rather than loading the entire collection. @@ -981,7 +1535,7 @@ *John Wang* -* Fix `add_column` with `array` option when using PostgreSQL. Fixes #10432 +* Fix `add_column` with `array` option when using PostgreSQL. Fixes #10432. *Adam Anderson* @@ -998,6 +1552,7 @@ *Yves Senn* * Fix the `:primary_key` option for `has_many` associations. + Fixes #10693. *Yves Senn* @@ -1122,7 +1677,7 @@ * Trigger a save on `has_one association=(associate)` when the associate contents have changed. - Fix #8856. + Fixes #8856. *Chris Thompson* diff --git a/activerecord/MIT-LICENSE b/activerecord/MIT-LICENSE index 0d7fb865e2..2950f05b11 100644 --- a/activerecord/MIT-LICENSE +++ b/activerecord/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2013 David Heinemeier Hansson +Copyright (c) 2004-2014 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index cbac2ef3c6..f856c482d6 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2013 David Heinemeier Hansson +# Copyright (c) 2004-2014 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 74e2774626..f3f77e21c0 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -75,13 +75,13 @@ module ActiveRecord class EagerLoadPolymorphicError < ActiveRecordError #:nodoc: def initialize(reflection) - super("Can not eagerly load the polymorphic association #{reflection.name.inspect}") + super("Cannot eagerly load the polymorphic association #{reflection.name.inspect}") end end class ReadOnlyAssociation < ActiveRecordError #:nodoc: def initialize(reflection) - super("Can not add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.") + super("Cannot add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.") end end @@ -1213,7 +1213,8 @@ module ActiveRecord # Returns the associated object. +nil+ is returned if none is found. # [association=(associate)] # Assigns the associate object, extracts the primary key, sets it as the foreign key, - # and saves the associate object. + # and saves the associate object. To avoid database inconsistencies, permanently deletes an existing + # associated object when assigning a new one, even if the new one isn't saved to database. # [build_association(attributes = {})] # Returns a new object of the associated type that has been instantiated # with +attributes+ and linked to this object through a foreign key, but has not diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 02f45731c9..67ea489b22 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -69,7 +69,7 @@ module ActiveRecord # The target is stale if the target no longer points to the record(s) that the # relevant foreign_key(s) refers to. If stale, the association accessor method # on the owner will reload the target. It's up to subclasses to implement the - # state_state method if relevant. + # stale_state method if relevant. # # Note that if the target has not been loaded, it is not considered stale. def stale_target? @@ -104,11 +104,12 @@ module ActiveRecord # Set the inverse association, if possible def set_inverse_instance(record) - if record && invertible_for?(record) + if invertible_for?(record) inverse = record.association(inverse_reflection_for(record).name) inverse.target = owner inverse.inversed = true end + record end # Returns the class of the target. belongs_to polymorphic overrides this to look at the diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index e1fa5225b5..8272a5584c 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -8,13 +8,16 @@ module ActiveRecord end def replace(record) - raise_on_type_mismatch!(record) if record - - update_counters(record) - replace_keys(record) - set_inverse_instance(record) - - @updated = true if record + if record + raise_on_type_mismatch!(record) + update_counters(record) + replace_keys(record) + set_inverse_instance(record) + @updated = true + else + decrement_counters + remove_keys + end self.target = record end @@ -34,35 +37,41 @@ module ActiveRecord !loaded? && foreign_key_present? && klass end - def update_counters(record) + def with_cache_name counter_cache_name = reflection.counter_cache_column + return unless counter_cache_name && owner.persisted? + yield counter_cache_name + end - if counter_cache_name && owner.persisted? && different_target?(record) - if record - record.class.increment_counter(counter_cache_name, record.id) - end + def update_counters(record) + with_cache_name do |name| + return unless different_target? record + record.class.increment_counter(name, record.id) + decrement_counter name + end + end - if foreign_key_present? - klass.decrement_counter(counter_cache_name, target_id) - end + def decrement_counters + with_cache_name { |name| decrement_counter name } + end + + def decrement_counter counter_cache_name + if foreign_key_present? + klass.decrement_counter(counter_cache_name, target_id) end end # Checks whether record is different to the current target, without loading it def different_target?(record) - if record.nil? - owner[reflection.foreign_key] - else - record.id != owner[reflection.foreign_key] - end + record.id != owner[reflection.foreign_key] end def replace_keys(record) - if record - owner[reflection.foreign_key] = record[reflection.association_primary_key(record.class)] - else - owner[reflection.foreign_key] = nil - end + owner[reflection.foreign_key] = record[reflection.association_primary_key(record.class)] + end + + def remove_keys + owner[reflection.foreign_key] = nil end def foreign_key_present? diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index eae5eed3a1..b710cf6bdb 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -11,7 +11,12 @@ module ActiveRecord def replace_keys(record) super - owner[reflection.foreign_type] = record && record.class.base_class.name + owner[reflection.foreign_type] = record.class.base_class.name + end + + def remove_keys + super + owner[reflection.foreign_type] = nil end def different_target?(record) diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index 37ba1c73b1..3911d1b520 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/module/attribute_accessors' + # This is the parent Association class which defines the variables # used by all associations. # @@ -13,65 +15,67 @@ module ActiveRecord::Associations::Builder class Association #:nodoc: class << self attr_accessor :extensions + # TODO: This class accessor is needed to make activerecord-deprecated_finders work. + # We can move it to a constant in 5.0. + attr_accessor :valid_options end self.extensions = [] - VALID_OPTIONS = [:class_name, :class, :foreign_key, :validate] + self.valid_options = [:class_name, :class, :foreign_key, :validate] + + attr_reader :name, :scope, :options def self.build(model, name, scope, options, &block) - extension = define_extensions model, name, &block - reflection = create_reflection model, name, scope, options, extension + builder = create_builder model, name, scope, options, &block + reflection = builder.build(model) define_accessors model, reflection define_callbacks model, reflection + builder.define_extensions model reflection end - def self.create_reflection(model, name, scope, options, extension = nil) + def self.create_builder(model, name, scope, options, &block) raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol) + new(model, name, scope, options, &block) + end + + def initialize(model, name, scope, options) + # TODO: Move this to create_builder as soon we drop support to activerecord-deprecated_finders. if scope.is_a?(Hash) options = scope scope = nil end - validate_options(options) + # TODO: Remove this model argument as soon we drop support to activerecord-deprecated_finders. + @name = name + @scope = scope + @options = options - scope = build_scope(scope, extension) - - ActiveRecord::Reflection.create(macro, name, scope, options, model) - end - - def self.build_scope(scope, extension) - new_scope = scope + validate_options if scope && scope.arity == 0 - new_scope = proc { instance_exec(&scope) } - end - - if extension - new_scope = wrap_scope new_scope, extension + @scope = proc { instance_exec(&scope) } end - - new_scope end - def self.wrap_scope(scope, extension) - scope + def build(model) + ActiveRecord::Reflection.create(macro, name, scope, options, model) end - def self.macro + def macro raise NotImplementedError end - def self.valid_options(options) - VALID_OPTIONS + Association.extensions.flat_map(&:valid_options) + def valid_options + Association.valid_options + Association.extensions.flat_map(&:valid_options) end - def self.validate_options(options) - options.assert_valid_keys(valid_options(options)) + def validate_options + options.assert_valid_keys(valid_options) end - def self.define_extensions(model, name) + def define_extensions(model) end def self.define_callbacks(model, reflection) @@ -114,6 +118,8 @@ module ActiveRecord::Associations::Builder raise NotImplementedError end + private + def self.add_before_destroy_callbacks(model, reflection) unless valid_dependent_options.include? reflection.options[:dependent] raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{reflection.options[:dependent]}" diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index e8e36e7cd0..5ccaa55a32 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -1,10 +1,10 @@ module ActiveRecord::Associations::Builder class BelongsTo < SingularAssociation #:nodoc: - def self.macro + def macro :belongs_to end - def self.valid_options(options) + def valid_options super + [:foreign_type, :polymorphic, :touch, :counter_cache] end @@ -23,6 +23,8 @@ module ActiveRecord::Associations::Builder add_counter_cache_methods mixin end + private + def self.add_counter_cache_methods(mixin) return if mixin.method_defined? :belongs_to_counter_cache_after_create @@ -110,7 +112,7 @@ module ActiveRecord::Associations::Builder end record = o.send name - unless record.nil? || record.new_record? + if record && record.persisted? if touch != true record.touch touch else diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb index 2ff67f904d..bc15a49996 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -7,11 +7,22 @@ module ActiveRecord::Associations::Builder CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove] - def self.valid_options(options) + def valid_options super + [:table_name, :before_add, :after_add, :before_remove, :after_remove, :extend] end + attr_reader :block_extension + + def initialize(model, name, scope, options) + super + @mod = nil + if block_given? + @mod = Module.new(&Proc.new) + @scope = wrap_scope @scope, @mod + end + end + def self.define_callbacks(model, reflection) super name = reflection.name @@ -21,11 +32,10 @@ module ActiveRecord::Associations::Builder } end - def self.define_extensions(model, name) - if block_given? + def define_extensions(model) + if @mod extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension" - extension = Module.new(&Proc.new) - model.parent.const_set(extension_module_name, extension) + model.parent.const_set(extension_module_name, @mod) end end @@ -68,7 +78,9 @@ module ActiveRecord::Associations::Builder CODE end - def self.wrap_scope(scope, mod) + private + + def wrap_scope(scope, mod) if scope proc { |owner| instance_exec(owner, &scope).extending(mod) } else diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index 1c9c04b044..e472277374 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 @@ -20,7 +20,7 @@ module ActiveRecord::Associations::Builder def self.build(lhs_class, name, options) if options[:join_table] - KnownTable.new options[:join_table] + KnownTable.new options[:join_table].to_s else class_name = options.fetch(:class_name) { name.to_s.camelize.singularize @@ -84,11 +84,11 @@ module ActiveRecord::Associations::Builder middle_name = [lhs_model.name.downcase.pluralize, association_name].join('_').gsub(/::/, '_').to_sym middle_options = middle_options join_model - - HasMany.create_reflection(lhs_model, - middle_name, - nil, - middle_options) + hm_builder = HasMany.create_builder(lhs_model, + middle_name, + nil, + middle_options) + hm_builder.build lhs_model end private diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index 227184cd19..7909b93622 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -1,10 +1,10 @@ module ActiveRecord::Associations::Builder class HasMany < CollectionAssociation #:nodoc: - def self.macro + def macro :has_many end - def self.valid_options(options) + def valid_options super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache] end diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index 064a3c8b51..f359efd496 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -1,10 +1,10 @@ module ActiveRecord::Associations::Builder class HasOne < SingularAssociation #:nodoc: - def self.macro + def macro :has_one end - def self.valid_options(options) + def valid_options valid = super + [:order, :as] valid += [:through, :source, :source_type] if options[:through] valid @@ -14,6 +14,8 @@ module ActiveRecord::Associations::Builder [:destroy, :delete, :nullify, :restrict_with_error, :restrict_with_exception] end + private + def self.add_before_destroy_callbacks(model, reflection) super unless reflection.options[:through] end diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index a6e8300642..e655c389a6 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -2,7 +2,7 @@ module ActiveRecord::Associations::Builder class SingularAssociation < Association #:nodoc: - def self.valid_options(options) + def valid_options super + [:remote, :dependent, :primary_key, :inverse_of] end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 62f23f54f9..89b7945c78 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -4,7 +4,7 @@ module ActiveRecord # # CollectionAssociation is an abstract class that provides common stuff to # ease the implementation of association proxies that represent - # collections. See the class hierarchy in AssociationProxy. + # collections. See the class hierarchy in Association. # # CollectionAssociation: # HasManyAssociation => has_many @@ -66,11 +66,11 @@ module ActiveRecord @target = [] end - def select(select = nil) + def select(*fields) if block_given? load_target.select.each { |e| yield e } else - scope.select(select) + scope.select(*fields) end end @@ -96,11 +96,31 @@ module ActiveRecord end def first(*args) - first_or_last(:first, *args) + first_nth_or_last(:first, *args) + end + + def second(*args) + first_nth_or_last(:second, *args) + end + + def third(*args) + first_nth_or_last(:third, *args) + end + + def fourth(*args) + first_nth_or_last(:fourth, *args) + end + + def fifth(*args) + first_nth_or_last(:fifth, *args) + end + + def forty_two(*args) + first_nth_or_last(:forty_two, *args) end def last(*args) - first_or_last(:last, *args) + first_nth_or_last(:last, *args) end def build(attributes = {}, &block) @@ -193,7 +213,11 @@ module ActiveRecord # Count all records using SQL. Construct options and pass them with # scope to the target class's +count+. - def count(column_name = nil) + def count(column_name = nil, count_options = {}) + # TODO: Remove count_options argument as soon we remove support to + # activerecord-deprecated_finders. + column_name, count_options = nil, column_name if column_name.is_a?(Hash) + relation = scope if association_scope.distinct_value # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. @@ -522,7 +546,7 @@ module ActiveRecord # * target already loaded # * owner is new record # * target contains new or changed record(s) - def fetch_first_or_last_using_find?(args) + def fetch_first_nth_or_last_using_find?(args) if args.first.is_a?(Hash) true else @@ -560,10 +584,10 @@ module ActiveRecord end # Fetches the first/last using SQL if possible, otherwise from the target array. - def first_or_last(type, *args) + def first_nth_or_last(type, *args) args.shift if args.first.is_a?(Hash) && args.first.empty? - collection = fetch_first_or_last_using_find?(args) ? scope : load_target + collection = fetch_first_nth_or_last_using_find?(args) ? scope : load_target collection.send(type, *args).tap do |record| set_inverse_instance record if record.is_a? ActiveRecord::Base end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 0b37ecf5b7..eba688866c 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -75,7 +75,7 @@ module ActiveRecord # # #<Pet id: nil, name: "Choo-Choo"> # # ] # - # person.pets.select([:id, :name]) + # person.pets.select(:id, :name ) # # => [ # # #<Pet id: 1, name: "Fancy-Fancy">, # # #<Pet id: 2, name: "Spook">, @@ -84,7 +84,7 @@ module ActiveRecord # # Be careful because this also means you're initializing a model # object with only the fields that you've selected. If you attempt - # to access a field that is not in the initialized record you'll + # to access a field except +id+ that is not in the initialized record you'll # receive: # # person.pets.select(:name).first.person_id @@ -106,13 +106,13 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook">, # # #<Pet id: 3, name: "Choo-Choo"> # # ] - def select(select = nil, &block) - @association.select(select, &block) + def select(*fields, &block) + @association.select(*fields, &block) end # Finds an object in the collection responding to the +id+. Uses the same # rules as <tt>ActiveRecord::Base.find</tt>. Returns <tt>ActiveRecord::RecordNotFound</tt> - # error if the object can not be found. + # error if the object cannot be found. # # class Person < ActiveRecord::Base # has_many :pets @@ -170,6 +170,32 @@ module ActiveRecord @association.first(*args) end + # Same as +first+ except returns only the second record. + def second(*args) + @association.second(*args) + end + + # Same as +first+ except returns only the third record. + def third(*args) + @association.third(*args) + end + + # Same as +first+ except returns only the fourth record. + def fourth(*args) + @association.fourth(*args) + end + + # Same as +first+ except returns only the fifth record. + def fifth(*args) + @association.fifth(*args) + end + + # Same as +first+ except returns only the forty second record. + # Also known as accessing "the reddit". + def forty_two(*args) + @association.forty_two(*args) + end + # Returns the last record, or the last +n+ records, from the collection. # If the collection is empty, the first form returns +nil+, and the second # form returns an empty array. @@ -669,8 +695,10 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] - def count(column_name = nil) - @association.count(column_name) + def count(column_name = nil, options = {}) + # TODO: Remove options argument as soon we remove support to + # activerecord-deprecated_finders. + @association.count(column_name, options) end # Returns the size of the collection. If the collection hasn't been loaded, @@ -976,6 +1004,28 @@ module ActiveRecord proxy_association.reload self end + + # Unloads the association. Returns +self+. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets # fetches pets from the database + # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] + # + # person.pets # uses the pets cache + # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] + # + # person.pets.reset # clears the pets cache + # + # person.pets # fetches pets from the database + # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] + def reset + proxy_association.reset + proxy_association.reset_scope + self + end end end end diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 9506960be3..295dccf34e 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -114,7 +114,7 @@ module ActiveRecord walk join_root, oj.join_root else oj.join_root.children.flat_map { |child| - make_outer_joins join_root, child + make_outer_joins oj.join_root, child } end } diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index 84e18684d8..0cd2e1a816 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -52,18 +52,16 @@ module ActiveRecord end end - if reflection.type - scope_chain_items << - ActiveRecord::Relation.create(klass, table) - .where(reflection.type => foreign_klass.base_class.name) - end - scope_chain_items.concat [klass.send(:build_default_scope)].compact rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right| 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? constraint = constraint.and rel.arel.constraints end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 2393667ac8..83637a0409 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -183,7 +183,7 @@ module ActiveRecord def run(preloader); end def preloaded_records - owners.flat_map { |owner| owner.read_attribute reflection.name } + owners.flat_map { |owner| owner.association(reflection.name).target } end end diff --git a/activerecord/lib/active_record/associations/preloader/singular_association.rb b/activerecord/lib/active_record/associations/preloader/singular_association.rb index 2b5cfda8ce..f60647a81e 100644 --- a/activerecord/lib/active_record/associations/preloader/singular_association.rb +++ b/activerecord/lib/active_record/associations/preloader/singular_association.rb @@ -11,7 +11,7 @@ module ActiveRecord association = owner.association(reflection.name) association.target = record - association.set_inverse_instance(record) + association.set_inverse_instance(record) if record end end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index 02dc464536..399aff378a 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -39,7 +39,9 @@ module ActiveRecord end def find_target - scope.first.tap { |record| set_inverse_instance(record) } + if record = scope.take + set_inverse_instance record + end end def replace(record) diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 217fc52dd5..ccbff8d1ff 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -110,16 +110,34 @@ module ActiveRecord end end - # A method name is 'dangerous' if it is already defined by Active Record, but + # A method name is 'dangerous' if it is already (re)defined by Active Record, but # not by any ancestors. (So 'puts' is not dangerous but 'save' is.) def dangerous_attribute_method?(name) # :nodoc: method_defined_within?(name, Base) end - def method_defined_within?(name, klass, sup = klass.superclass) # :nodoc: + def method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc: if klass.method_defined?(name) || klass.private_method_defined?(name) - if sup.method_defined?(name) || sup.private_method_defined?(name) - klass.instance_method(name).owner != sup.instance_method(name).owner + if superklass.method_defined?(name) || superklass.private_method_defined?(name) + klass.instance_method(name).owner != superklass.instance_method(name).owner + else + true + end + else + false + end + end + + # A class method is 'dangerous' if it is already (re)defined by Active Record, but + # not by any ancestors. (So 'puts' is not dangerous but 'new' is.) + def dangerous_class_method?(method_name) + class_method_defined_within?(method_name, Base) + end + + def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc + if klass.respond_to?(name, true) + if superklass.respond_to?(name, true) + klass.method(name).owner != superklass.method(name).owner else true end @@ -330,7 +348,7 @@ module ActiveRecord end # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, - # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). It raises + # "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. # # Alias for the <tt>read_attribute</tt> method. diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 19e81abba5..8a1b199997 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -38,11 +38,37 @@ module ActiveRecord end end + def initialize_dup(other) # :nodoc: + super + init_changed_attributes + end + private + def initialize_internals_callback + super + init_changed_attributes + end + + def init_changed_attributes + @changed_attributes = nil + # Intentionally avoid using #column_defaults since overridden defaults (as is done in + # optimistic locking) won't get written unless they get marked as changed + self.class.columns.each do |c| + attr, orig_value = c.name, c.default + changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, @attributes[attr]) + end + end + # Wrap write_attribute to remember original attribute value. def write_attribute(attr, value) attr = attr.to_s + save_changed_attribute(attr, value) + + super(attr, value) + end + + def save_changed_attribute(attr, value) # The attribute already has an unsaved change. if attribute_changed?(attr) old = changed_attributes[attr] @@ -51,9 +77,6 @@ module ActiveRecord old = clone_attribute_value(:read_attribute, attr) changed_attributes[attr] = old if _field_changed?(attr, old, value) end - - # Carry on. - super(attr, value) end def update_record(*) @@ -67,7 +90,7 @@ module ActiveRecord # Serialized attributes should always be written in case they've been # changed in place. def keys_for_partial_write - changed | (attributes.keys & self.class.serialized_attributes.keys) + changed end def _field_changed?(attr, old, value) diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index c152a246b5..d01e9aea59 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -102,7 +102,7 @@ module ActiveRecord end # Returns the value of the attribute identified by <tt>attr_name</tt> after - # it has been typecast (for example, "2004-12-12" in a data column is cast + # it has been typecast (for example, "2004-12-12" in a date column is cast # to a date object, like Date.new(2004, 12, 12)). def read_attribute(attr_name) # If it's cached, just return it diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index d484659190..3227464032 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -115,6 +115,14 @@ module ActiveRecord end end + def should_record_timestamps? + super || (self.record_timestamps && (attributes.keys & self.class.serialized_attributes.keys).present?) + end + + def keys_for_partial_write + super | (attributes.keys & self.class.serialized_attributes.keys) + end + def type_cast_attribute_for_write(column, value) if column && coder = self.class.serialized_attributes[column.name] Attribute.new(coder, value, :unserialized) diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 1d3ec75aa1..9ec1feea97 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -294,6 +294,7 @@ module ActiveRecord #:nodoc: extend Enum extend Delegation::DelegateCache + include Core include Persistence include NoTouching include ReadonlyAttributes @@ -320,7 +321,6 @@ module ActiveRecord #:nodoc: include Reflection include Serialization include Store - include Core end ActiveSupport.run_load_hooks(:active_record, Base) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index cfdcae7f63..cebe741daa 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -86,7 +86,7 @@ module ActiveRecord end end - # Return the number of threads currently waiting on this + # Returns the number of threads currently waiting on this # queue. def num_waiting synchronize do diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 1a323b6a9c..c90915c509 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -286,10 +286,6 @@ module ActiveRecord # Inserts the given fixture into the table. Overridden in adapters that require # something beyond a simple insert (eg. Oracle). def insert_fixture(fixture, table_name) - execute fixture_sql(fixture, table_name), 'Fixture Insert' - end - - def fixture_sql(fixture, table_name) columns = schema_cache.columns_hash(table_name) key_list = [] @@ -298,7 +294,7 @@ module ActiveRecord quote(value, columns[name]) end - "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})" + execute "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})", 'Fixture Insert' end def empty_insert_statement_value @@ -346,7 +342,7 @@ module ActiveRecord protected - # Return a subquery for the given key using the join information. + # Returns a subquery for the given key using the join information. def subquery_for(key, select) subselect = select.clone subselect.projections = [key] diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 552a22d28a..75501852ed 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -43,7 +43,9 @@ module ActiveRecord # SQLite does not understand dates, so this method will convert a Date # to a String. def type_cast(value, column) - return value.id if value.respond_to?(:quoted_id) + if value.respond_to?(:quoted_id) && value.respond_to?(:id) + return value.id + end case value when String, ActiveSupport::Multibyte::Chars diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb index 7c330a2f25..a51691bfa8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -34,9 +34,10 @@ module ActiveRecord def visit_TableDefinition(o) create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE " - create_sql << "#{quote_table_name(o.name)} (" - create_sql << o.columns.map { |c| accept c }.join(', ') - create_sql << ") #{o.options}" + create_sql << "#{quote_table_name(o.name)} " + create_sql << "(#{o.columns.map { |c| accept c }.join(', ')}) " unless o.as + create_sql << "#{o.options}" + create_sql << " AS #{@conn.to_sql(o.as)}" if o.as create_sql 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 063b19871a..c39bf15e83 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -49,14 +49,15 @@ module ActiveRecord # An array of ColumnDefinition objects, representing the column changes # that have been defined. attr_accessor :indexes - attr_reader :name, :temporary, :options + attr_reader :name, :temporary, :options, :as - def initialize(types, name, temporary, options) + def initialize(types, name, temporary, options, as = nil) @columns_hash = {} @indexes = {} @native = types @temporary = temporary @options = options + @as = as @name = name end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 51a6929dab..88bf15bc18 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -131,6 +131,9 @@ module ActiveRecord # [<tt>:force</tt>] # Set to true to drop the table before creating it. # Defaults to false. + # [<tt>:as</tt>] + # SQL to use to generate the table. When this option is used, the block is + # ignored, as are the <tt>:id</tt> and <tt>:primary_key</tt> options. # # ====== Add a backend specific option to the generated SQL (MySQL) # @@ -169,19 +172,31 @@ module ActiveRecord # supplier_id int # ) # + # ====== Create a temporary table based on a query + # + # create_table(:long_query, temporary: true, + # as: "SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id") + # + # generates: + # + # CREATE TEMPORARY TABLE long_query AS + # SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id + # # See also TableDefinition#column for details on how to create columns. def create_table(table_name, options = {}) - td = create_table_definition table_name, options[:temporary], options[:options] + td = create_table_definition table_name, options[:temporary], options[:options], options[:as] - unless options[:id] == false - pk = options.fetch(:primary_key) { - Base.get_primary_key table_name.to_s.singularize - } + if !options[:as] + unless options[:id] == false + pk = options.fetch(:primary_key) { + Base.get_primary_key table_name.to_s.singularize + } - td.primary_key pk, options.fetch(:id, :primary_key), options - end + td.primary_key pk, options.fetch(:id, :primary_key), options + end - yield td if block_given? + yield td if block_given? + end if options[:force] && table_exists?(table_name) drop_table(table_name, options) @@ -699,7 +714,7 @@ module ActiveRecord # require the order columns appear in the SELECT. # # columns_for_distinct("posts.id", ["posts.created_at desc"]) - def columns_for_distinct(columns, orders) # :nodoc: + def columns_for_distinct(columns, orders) #:nodoc: columns end @@ -721,6 +736,10 @@ module ActiveRecord remove_column table_name, :created_at end + def update_table_definition(table_name, base) #:nodoc: + Table.new(table_name, base) + end + protected def add_index_sort_order(option_strings, column_names, options = {}) if options.is_a?(Hash) && order = options[:order] @@ -826,17 +845,13 @@ module ActiveRecord end private - def create_table_definition(name, temporary, options) - TableDefinition.new native_database_types, name, temporary, options + def create_table_definition(name, temporary, options, as = nil) + TableDefinition.new native_database_types, name, temporary, options, as end def create_alter_table(name) AlterTable.new create_table_definition(name, false, {}) end - - def update_table_definition(table_name, base) - Table.new(table_name, base) - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 2b6685499a..bc4884b538 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -23,6 +23,10 @@ module ActiveRecord @parent = nil end + def finalized? + @state + end + def committed? @state == :committed end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 8aa1ce5c04..3c94bad208 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -349,6 +349,14 @@ module ActiveRecord protected + def translate_exception_class(e, sql) + message = "#{e.class.name}: #{e.message}: #{sql}" + @logger.error message if @logger + exception = translate_exception(e, message) + exception.set_backtrace e.backtrace + exception + end + def log(sql, name = "SQL", binds = [], statement_name = nil) @instrumenter.instrument( "sql.active_record", @@ -358,11 +366,7 @@ module ActiveRecord :statement_name => statement_name, :binds => binds) { yield } rescue => e - message = "#{e.class.name}: #{e.message}: #{sql}" - @logger.error message if @logger - exception = translate_exception(e, message) - exception.set_backtrace e.backtrace - raise exception + raise translate_exception_class(e, sql) end def translate_exception(exception, message) 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 e3270e734f..7768c24967 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -148,7 +148,7 @@ module ActiveRecord QUOTED_TRUE, QUOTED_FALSE = '1', '0' NATIVE_DATABASE_TYPES = { - :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY", + :primary_key => "int(11) auto_increment PRIMARY KEY", :string => { :name => "varchar", :limit => 255 }, :text => { :name => "text" }, :integer => { :name => "int", :limit => 4 }, @@ -303,12 +303,6 @@ module ActiveRecord else log(sql, name) { @connection.query(sql) } end - rescue ActiveRecord::StatementInvalid => exception - if exception.message.split(":").first =~ /Packets out of order/ - raise ActiveRecord::StatementInvalid.new("'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings.", exception.original_exception) - else - raise - end end # MysqlAdapter has to free a result after using it, so we use this method to write @@ -492,6 +486,10 @@ module ActiveRecord rename_table_indexes(table_name, new_name) end + def drop_table(table_name, options = {}) + execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE #{quote_table_name(table_name)}" + end + def rename_index(table_name, old_name, new_name) if (version[0] == 5 && version[1] >= 7) || version[0] >= 6 execute "ALTER TABLE #{quote_table_name(table_name)} RENAME INDEX #{quote_table_name(old_name)} TO #{quote_table_name(new_name)}" diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 64fc9e95d8..3f8b14bf67 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -13,41 +13,146 @@ module ActiveRecord @config = original.config.dup end + # Expands a connection string into a hash. + class ConnectionUrlResolver # :nodoc: + + # == Example + # + # url = "postgresql://foo:bar@localhost:9000/foo_test?pool=5&timeout=3000" + # ConnectionUrlResolver.new(url).to_hash + # # => { + # "adapter" => "postgresql", + # "host" => "localhost", + # "port" => 9000, + # "database" => "foo_test", + # "username" => "foo", + # "password" => "bar", + # "pool" => "5", + # "timeout" => "3000" + # } + def initialize(url) + raise "Database URL cannot be empty" if url.blank? + @uri = URI.parse(url) + @adapter = @uri.scheme + @adapter = "postgresql" if @adapter == "postgres" + @query = @uri.query || '' + end + + # Converts the given URL to a full connection hash. + def to_hash + config = raw_config.reject { |_,value| value.blank? } + config.map { |key,value| config[key] = uri_parser.unescape(value) if value.is_a? String } + config + end + + private + + def uri + @uri + end + + def uri_parser + @uri_parser ||= URI::Parser.new + end + + # Converts the query parameters of the URI into a hash. + # + # "localhost?pool=5&reap_frequency=2" + # # => { "pool" => "5", "reap_frequency" => "2" } + # + # returns empty hash if no query present. + # + # "localhost" + # # => {} + def query_hash + Hash[@query.split("&").map { |pair| pair.split("=") }] + end + + def raw_config + query_hash.merge({ + "adapter" => @adapter, + "username" => uri.user, + "password" => uri.password, + "port" => uri.port, + "database" => database, + "host" => uri.host }) + end + + # Returns name of the database. + # Sqlite3 expects this to be a full path or `:memory:`. + def database + if @adapter == 'sqlite3' + if '/:memory:' == uri.path + ':memory:' + else + uri.path + end + else + uri.path.sub(%r{^/},"") + end + end + end + ## - # Builds a ConnectionSpecification from user input + # Builds a ConnectionSpecification from user input. class Resolver # :nodoc: - attr_reader :config, :klass, :configurations + attr_reader :configurations - def initialize(config, configurations) - @config = config + # Accepts a hash two layers deep, keys on the first layer represent + # environments such as "production". Keys must be strings. + def initialize(configurations) @configurations = configurations end - def spec - case config - when nil - raise AdapterNotSpecified unless defined?(Rails.env) - resolve_string_connection Rails.env - when Symbol, String - resolve_string_connection config.to_s - when Hash - resolve_hash_connection config + # Returns a hash with database connection information. + # + # == Examples + # + # Full hash Configuration. + # + # configurations = { "production" => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } } + # Resolver.new(configurations).resolve(:production) + # # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3"} + # + # Initialized with URL configuration strings. + # + # configurations = { "production" => "postgresql://localhost/foo" } + # Resolver.new(configurations).resolve(:production) + # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" } + # + def resolve(config) + if config + resolve_connection config + elsif env = ActiveRecord::ConnectionHandling::RAILS_ENV.call + resolve_env_connection env.to_sym + else + raise AdapterNotSpecified end end - private - def resolve_string_connection(spec) # :nodoc: - hash = configurations.fetch(spec) do |k| - connection_url_to_hash(k) + # Expands each key in @configurations hash into fully resolved hash + def resolve_all + config = configurations.dup + config.each do |key, value| + config[key] = resolve(value) if value end - - raise(AdapterNotSpecified, "#{spec} database is not configured") unless hash - - resolve_hash_connection hash + config end - def resolve_hash_connection(spec) # :nodoc: - spec = spec.symbolize_keys + # Returns an instance of ConnectionSpecification for a given adapter. + # Accepts a hash one layer deep that contains all connection information. + # + # == Example + # + # config = { "production" => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } } + # spec = Resolver.new(config).spec(:production) + # spec.adapter_method + # # => "sqlite3" + # spec.config + # # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } + # + def spec(config) + spec = resolve(config).symbolize_keys raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter) @@ -61,35 +166,87 @@ module ActiveRecord end adapter_method = "#{spec[:adapter]}_connection" - ConnectionSpecification.new(spec, adapter_method) end - def connection_url_to_hash(url) # :nodoc: - config = URI.parse url - adapter = config.scheme - adapter = "postgresql" if adapter == "postgres" - spec = { :adapter => adapter, - :username => config.user, - :password => config.password, - :port => config.port, - :database => config.path.sub(%r{^/},""), - :host => config.host } - - spec.reject!{ |_,value| value.blank? } - - uri_parser = URI::Parser.new - - spec.map { |key,value| spec[key] = uri_parser.unescape(value) if value.is_a?(String) } + private - if config.query - options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys + # Returns fully resolved connection, accepts hash, string or symbol. + # Always returns a hash. + # + # == Examples + # + # Symbol representing current environment. + # + # Resolver.new("production" => {}).resolve_connection(:production) + # # => {} + # + # One layer deep hash of connection values. + # + # Resolver.new({}).resolve_connection("adapter" => "sqlite3") + # # => { "adapter" => "sqlite3" } + # + # Connection URL. + # + # Resolver.new({}).resolve_connection("postgresql://localhost/foo") + # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" } + # + def resolve_connection(spec) + case spec + when Symbol, String + resolve_env_connection spec + when Hash + resolve_hash_connection spec + end + end - spec.merge!(options) + # Takes the environment such as `:production` or `:development`. + # This requires that the @configurations was initialized with a key that + # matches. + # + # + # Resolver.new("production" => {}).resolve_env_connection(:production) + # # => {} + # + # Takes a connection URL. + # + # Resolver.new({}).resolve_env_connection("postgresql://localhost/foo") + # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" } + # + def resolve_env_connection(spec) + # Rails has historically accepted a string to mean either + # an environment key or a URL spec, so we have deprecated + # this ambiguous behaviour and in the future this function + # can be removed in favor of resolve_string_connection and + # resolve_symbol_connection. + if config = configurations[spec.to_s] + if spec.is_a?(String) + ActiveSupport::Deprecation.warn "Passing a string to ActiveRecord::Base.establish_connection " \ + "for a configuration lookup is deprecated, please pass a symbol (#{spec.to_sym.inspect}) instead" + end + resolve_connection(config) + elsif spec.is_a?(String) + resolve_string_connection(spec) + else + raise(AdapterNotSpecified, "'#{spec}' database is not configured. Available configuration: #{configurations.inspect}") end + end + # Accepts a hash. Expands the "url" key that contains a + # URL database connection to a full connection + # hash and merges with the rest of the hash. + # Connection details inside of the "url" key win any merge conflicts + def resolve_hash_connection(spec) + if url = spec.delete("url") + connection_hash = resolve_string_connection(url) + spec.merge!(connection_hash) + end spec end + + def resolve_string_connection(url) + ConnectionUrlResolver.new(url).to_hash + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index e790f731ea..6d8e994654 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -18,6 +18,12 @@ module ActiveRecord client = Mysql2::Client.new(config) options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0] ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config) + rescue Mysql2::Error => error + if error.message.include?("Unknown database") + raise ActiveRecord::NoDatabaseError.new(error.message) + else + raise error + end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index c4dcba0501..7dbaa272a3 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -34,6 +34,12 @@ module ActiveRecord default_flags |= Mysql::CLIENT_FOUND_ROWS if Mysql.const_defined?(:CLIENT_FOUND_ROWS) options = [host, username, password, database, port, socket, default_flags] ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config) + rescue Mysql::Error => error + if error.message.include?("Unknown database") + raise ActiveRecord::NoDatabaseError.new(error.message) + else + raise error + end end end @@ -419,14 +425,19 @@ module ActiveRecord if result types = {} + fields = [] result.fetch_fields.each { |field| + field_name = field.name + fields << field_name + if field.decimals > 0 - types[field.name] = Fields::Decimal.new + types[field_name] = Fields::Decimal.new else - types[field.name] = Fields.find_type field + types[field_name] = Fields.find_type field end } - result_set = ActiveRecord::Result.new(types.keys, result.to_a, types) + + result_set = ActiveRecord::Result.new(fields, result.to_a, types) result.free else result_set = ActiveRecord::Result.new([], []) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb index bf34f2bdae..35ce881302 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb @@ -144,7 +144,7 @@ module ActiveRecord def quote_and_escape(value) case value - when "NULL" + when "NULL", Numeric value else "\"#{value.gsub(/"/,"\\\"")}\"" 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 fa173d13a2..f349c37724 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -142,7 +142,7 @@ module ActiveRecord fields.each_with_index do |fname, i| ftype = result.ftype i fmod = result.fmod i - types[fname] = OID::TYPE_MAP.fetch(ftype, fmod) { |oid, mod| + types[fname] = type_map.fetch(ftype, fmod) { |oid, mod| warn "unknown OID: #{fname}(#{oid}) (#{sql})" OID::Identity.new } diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 2a9547fd10..fae260a921 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -301,11 +301,9 @@ module ActiveRecord end end - TYPE_MAP = TypeMap.new # :nodoc: - - # When the PG adapter connects, the pg_type table is queried. The + # 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 + # 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 diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 1ec1490263..a471383041 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -46,7 +46,7 @@ module ActiveRecord # PostgreSQL-specific extensions to column definitions in a table. class PostgreSQLColumn < Column #:nodoc: attr_accessor :array - # Instantiates a new PostgreSQL column definition in a table. + def initialize(name, default, oid_type, sql_type = nil, null = true) @oid_type = oid_type default_value = self.class.extract_value_from_default(default) @@ -62,6 +62,14 @@ module ActiveRecord @default_function = default if has_default_function?(default_value, default) end + def number? + !array && super + end + + def text? + !array && super + end + # :stopdoc: class << self include ConnectionAdapters::PostgreSQLColumn::Cast @@ -713,6 +721,10 @@ module ActiveRecord !native_database_types[type].nil? end + def update_table_definition(table_name, base) #:nodoc: + Table.new(table_name, base) + end + protected # Returns the version of the connected PostgreSQL server. @@ -748,6 +760,25 @@ module ActiveRecord 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 + + 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 + end + + vector = OID::Vector.new row['typdelim'], type_map[row['typelem'].to_i] + end + + type_map[row['oid'].to_i] = vector + type_map + end + def initialize_type_map(type_map) result = execute('SELECT oid, typname, typelem, typdelim, typinput FROM pg_type', 'SCHEMA') leaves, nodes = result.partition { |row| row['typelem'] == '0' } @@ -757,19 +788,13 @@ module ActiveRecord type_map[row['oid'].to_i] = OID::NAMES[row['typname']] end + records_by_oid = result.group_by { |row| row['oid'] } + arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' } # populate composite types - nodes.find_all { |row| type_map.key? row['typelem'].to_i }.each do |row| - if OID.registered_type? row['typname'] - # this composite type is explicitly registered - vector = OID::NAMES[row['typname']] - else - # use the default for composite types - vector = OID::Vector.new row['typdelim'], type_map[row['typelem'].to_i] - end - - type_map[row['oid'].to_i] = vector + nodes.each do |row| + add_oid row, records_by_oid, type_map end # populate array types @@ -779,7 +804,7 @@ module ActiveRecord end end - FEATURE_NOT_SUPPORTED = "0A000" # :nodoc: + FEATURE_NOT_SUPPORTED = "0A000" #:nodoc: def exec_no_cache(sql, name, binds) log(sql, name, binds) { @connection.async_exec(sql) } @@ -828,7 +853,11 @@ module ActiveRecord sql_key = sql_key(sql) unless @statements.key? sql_key nextkey = @statements.next_key - @connection.prepare nextkey, sql + begin + @connection.prepare nextkey, sql + rescue => e + raise translate_exception_class(e, sql) + end # Clear the queue @connection.get_last_result @statements[sql_key] = nextkey @@ -852,6 +881,12 @@ module ActiveRecord PostgreSQLColumn.money_precision = (postgresql_version >= 80300) ? 19 : 10 configure_connection + rescue ::PG::Error => error + if error.message.include?("does not exist") + raise ActiveRecord::NoDatabaseError.new(error.message) + else + raise error + end end # Configures the encoding, verbosity, schema search path, and time zone of the connection. @@ -956,16 +991,12 @@ module ActiveRecord end def extract_table_ref_from_insert_sql(sql) - sql[/into\s+([^\(]*).*values\s*\(/i] + sql[/into\s+([^\(]*).*values\s*\(/im] $1.strip if $1 end - def create_table_definition(name, temporary, options) - TableDefinition.new native_database_types, name, temporary, options - end - - def update_table_definition(table_name, base) - Table.new(table_name, base) + def create_table_definition(name, temporary, options, as = nil) + TableDefinition.new native_database_types, name, temporary, options, as 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 2cf1015f2c..170dddb08e 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -31,6 +31,12 @@ module ActiveRecord db.busy_timeout(ConnectionAdapters::SQLite3Adapter.type_cast_config_to_integer(config[:timeout])) if config[:timeout] ConnectionAdapters::SQLite3Adapter.new(db, logger, config) + rescue Errno::ENOENT => error + if error.message.include?("No such file or directory") + raise ActiveRecord::NoDatabaseError.new(error.message) + else + raise error + end end end @@ -149,6 +155,10 @@ module ActiveRecord true end + def supports_partial_index? + sqlite_version >= '3.8.0' + end + # Returns true, since this connection adapter supports prepared statement # caching. def supports_statement_cache? @@ -391,13 +401,25 @@ module ActiveRecord # Returns an array of indexes for the given table. def indexes(table_name, name = nil) #:nodoc: exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", 'SCHEMA').map do |row| + sql = <<-SQL + SELECT sql + FROM sqlite_master + WHERE name=#{quote(row['name'])} AND type='index' + UNION ALL + SELECT sql + FROM sqlite_temp_master + WHERE name=#{quote(row['name'])} AND type='index' + SQL + index_sql = exec_query(sql).first['sql'] + match = /\sWHERE\s+(.+)$/i.match(index_sql) + where = match[1] if match IndexDefinition.new( table_name, row['name'], row['unique'] != 0, exec_query("PRAGMA index_info('#{row['name']}')", "SCHEMA").map { |col| col['name'] - }) + }, nil, nil, where) end end @@ -586,7 +608,11 @@ module ActiveRecord def translate_exception(exception, message) case exception.message - when /column(s)? .* (is|are) not unique/ + # SQLite 3.8.2 returns a newly formatted error message: + # UNIQUE constraint failed: *table_name*.*column_name* + # Older versions of SQLite return: + # column *column_name* is not unique + when /column(s)? .* (is|are) not unique/, /UNIQUE constraint failed: .*/ RecordNotUnique.new(message, exception) else super diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index a1943dfcb0..11f6a47158 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -1,5 +1,8 @@ module ActiveRecord module ConnectionHandling + RAILS_ENV = -> { Rails.env if defined?(Rails) } + DEFAULT_ENV = -> { RAILS_ENV.call || "default_env" } + # Establishes the connection to the database. Accepts a hash as input where # the <tt>:adapter</tt> key must be specified with the name of a database adapter (in lower-case) # example for regular databases (MySQL, Postgresql, etc): @@ -32,11 +35,19 @@ module ActiveRecord # "postgres://myuser:mypass@localhost/somedatabase" # ) # + # In case <tt>ActiveRecord::Base.configurations</tt> is set (Rails + # automatically loads the contents of config/database.yml into it), + # a symbol can also be given as argument, representing a key in the + # configuration hash: + # + # ActiveRecord::Base.establish_connection(:production) + # # The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError # may be returned on an error. - def establish_connection(spec = ENV["DATABASE_URL"]) - resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new spec, configurations - spec = resolver.spec + def establish_connection(spec = nil) + spec ||= DEFAULT_ENV.call.to_sym + resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new configurations + spec = resolver.spec(spec) unless respond_to?(spec.adapter_method) raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter" @@ -46,6 +57,56 @@ module ActiveRecord connection_handler.establish_connection self, spec end + class MergeAndResolveDefaultUrlConfig # :nodoc: + def initialize(raw_configurations, url = ENV['DATABASE_URL']) + @raw_config = raw_configurations.dup + @url = url + end + + # Returns fully resolved connection hashes. + # Merges connection information from `ENV['DATABASE_URL']` if available. + def resolve + ConnectionAdapters::ConnectionSpecification::Resolver.new(config).resolve_all + end + + private + def config + if @url + raw_merged_into_default + else + @raw_config + end + end + + def raw_merged_into_default + default = default_url_hash + + @raw_config.each do |env, values| + default[env] = values || {} + default[env].merge!("url" => @url) { |h, v1, v2| v1 || v2 } if default[env].is_a?(Hash) + end + default + end + + # When the raw configuration is not present and ENV['DATABASE_URL'] + # is available we return a hash with the connection information in + # the connection URL. This hash responds to any string key with + # resolved connection information. + def default_url_hash + if @raw_config.blank? + Hash.new do |hash, key| + hash[key] = if key.is_a? String + ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(@url).to_hash + else + nil + end + end + else + {} + end + end + end + # Returns the connection currently associated with the class. This can # also be used to "borrow" the connection to do database work unrelated # to any of the specific Active Records. diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 8808ad5a4c..6f02c763fe 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -42,9 +42,16 @@ module ActiveRecord # 'database' => 'db/production.sqlite3' # } # } - mattr_accessor :configurations, instance_writer: false + def self.configurations=(config) + @@configurations = ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig.new(config).resolve + end self.configurations = {} + # Returns fully resolved configurations hash + def self.configurations + @@configurations + end + ## # :singleton-method: # Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling @@ -69,6 +76,9 @@ module ActiveRecord mattr_accessor :timestamped_migrations, instance_writer: false self.timestamped_migrations = true + # :nodoc: + mattr_accessor :maintain_test_schema, instance_accessor: false + def self.disable_implicit_join_references=(value) ActiveSupport::Deprecation.warn("Implicit join references were removed with Rails 4.1." \ "Make sure to remove this configuration because it does nothing.") @@ -172,9 +182,7 @@ module ActiveRecord @column_types = self.class.column_types init_internals - init_changed_attributes - ensure_proper_type - populate_with_current_scope_attributes + initialize_internals_callback # +options+ argument is only needed to make protected_attributes gem easier to hook. # Remove it when we drop support to this gem. @@ -245,16 +253,12 @@ module ActiveRecord run_callbacks(:initialize) unless _initialize_callbacks.empty? - @changed_attributes = {} - init_changed_attributes - @aggregation_cache = {} @association_cache = {} @attributes_cache = {} @new_record = true - ensure_proper_type super end @@ -387,13 +391,10 @@ module ActiveRecord end def update_attributes_from_transaction_state(transaction_state, depth) - if transaction_state && !has_transactional_callbacks? + if transaction_state && transaction_state.finalized? && !has_transactional_callbacks? unless @reflects_state[depth] - if transaction_state.committed? - committed! - elsif transaction_state.rolledback? - rolledback! - end + restore_transaction_record_state if transaction_state.rolledback? + clear_transaction_record_state @reflects_state[depth] = true end @@ -433,13 +434,7 @@ module ActiveRecord @reflects_state = [false] end - def init_changed_attributes - # Intentionally avoid using #column_defaults since overridden defaults (as is done in - # optimistic locking) won't get written unless they get marked as changed - self.class.columns.each do |c| - attr, orig_value = c.name, c.default - changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, @attributes[attr]) - end + def initialize_internals_callback end # This method is needed to make protected_attributes gem easier to hook. diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 7e3bef9431..dcbdf75627 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -82,10 +82,10 @@ module ActiveRecord # Increment a numeric field by one, via a direct SQL update. # - # This method is used primarily for maintaining counter_cache columns used to - # store aggregate values. For example, a DiscussionBoard may cache posts_count - # and comments_count to avoid running an SQL query to calculate the number of - # posts and comments there are each time it is displayed. + # This method is used primarily for maintaining counter_cache columns that are + # used to store aggregate values. For example, a DiscussionBoard may cache + # posts_count and comments_count to avoid running an SQL query to calculate the + # number of posts and comments there are, each time it is displayed. # # ==== Parameters # diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index e650ebcf64..e94b74063e 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -6,8 +6,12 @@ module ActiveRecord # then we can remove the indirection. def respond_to?(name, include_private = false) - match = Method.match(self, name) - match && match.valid? || super + if self == Base + super + else + match = Method.match(self, name) + match && match.valid? || super + end end private @@ -84,13 +88,18 @@ module ActiveRecord "#{finder}(#{attributes_hash})" end + # The parameters in the signature may have reserved Ruby words, in order + # to prevent errors, we start each param name with `_`. + # # Extended in activerecord-deprecated_finders def signature - attribute_names.join(', ') + attribute_names.map { |name| "_#{name}" }.join(', ') end + # Given that the parameters starts with `_`, the finder needs to use the + # same parameter name. def attributes_hash - "{" + attribute_names.map { |name| ":#{name} => #{name}" }.join(',') + "}" + "{" + attribute_names.map { |name| ":#{name} => _#{name}" }.join(',') + "}" end def finder diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 5fcc0382d8..059bfe9a0f 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -1,5 +1,6 @@ module ActiveRecord - # Declare an enum attribute where the values map to integers in the database, but can be queried by name. Example: + # Declare an enum attribute where the values map to integers in the database, + # but can be queried by name. Example: # # class Conversation < ActiveRecord::Base # enum status: [ :active, :archived ] @@ -18,6 +19,15 @@ module ActiveRecord # # conversation.update! status: 1 # conversation.status = "archived" # + # # conversation.update! status: nil + # conversation.status = nil + # conversation.status.nil? # => true + # 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. + # # You can set the default value from the database declaration, like: # # create_table :conversations do |t| @@ -26,63 +36,157 @@ module ActiveRecord # # Good practice is to let the first declared status be the default. # - # Finally, it's also possible to explicitly map the relation between attribute and database integer: + # Finally, it's also possible to explicitly map the relation between attribute and + # database integer with a +Hash+: # # class Conversation < ActiveRecord::Base # enum status: { active: 0, archived: 1 } # end # + # Note that when an +Array+ is used, the implicit mapping from the values to database + # integers is derived from the order the values appear in the array. In the example, + # <tt>:active</tt> is mapped to +0+ as it's the first element, and <tt>:archived</tt> + # is mapped to +1+. In general, the +i+-th element is mapped to <tt>i-1</tt> in the + # database. + # + # Therefore, once a value is added to the enum array, its position in the array must + # be maintained, and new values should only be added to the end of the array. To + # remove unused values, the explicit +Hash+ syntax should be used. + # # In rare circumstances you might need to access the mapping directly. - # The mappings are exposed through a constant with the attributes name: + # The mappings are exposed through a class method with the pluralized attribute + # name: # - # Conversation::STATUS # => { "active" => 0, "archived" => 1 } + # Conversation.statuses # => { "active" => 0, "archived" => 1 } # - # Use that constant when you need to know the ordinal value of an enum: + # Use that class method when you need to know the ordinal value of an enum: # - # Conversation.where("status <> ?", Conversation::STATUS[:archived]) + # Conversation.where("status <> ?", Conversation.statuses[:archived]) module Enum + DEFINED_ENUMS = {} # :nodoc: + + def enum_mapping_for(attr_name) # :nodoc: + DEFINED_ENUMS[attr_name.to_s] + end + def enum(definitions) klass = self definitions.each do |name, values| - # DIRECTION = { } - enum_values = _enum_methods_module.const_set name.to_s.upcase, ActiveSupport::HashWithIndifferentAccess.new + # statuses = { } + enum_values = ActiveSupport::HashWithIndifferentAccess.new name = name.to_sym + # def self.statuses statuses end + detect_enum_conflict!(name, name.to_s.pluralize, true) + klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values } + _enum_methods_module.module_eval do - # def direction=(value) self[:direction] = DIRECTION[value] end + # def status=(value) self[:status] = statuses[value] end + klass.send(:detect_enum_conflict!, name, "#{name}=") define_method("#{name}=") { |value| - unless enum_values.has_key?(value) + if enum_values.has_key?(value) || value.blank? + self[name] = enum_values[value] + elsif enum_values.has_value?(value) + # Assigning a value directly is not a end-user feature, hence it's not documented. + # This is used internally to make building objects from the generated scopes work + # as expected, i.e. +Conversation.archived.build.archived?+ should be true. + self[name] = value + else raise ArgumentError, "'#{value}' is not a valid #{name}" end - self[name] = enum_values[value] } - # def direction() DIRECTION.key self[:direction] end + # def status() statuses.key self[:status] end + klass.send(:detect_enum_conflict!, name, name) define_method(name) { enum_values.key self[name] } + # def status_before_type_cast() statuses.key self[:status] end + klass.send(:detect_enum_conflict!, name, "#{name}_before_type_cast") + define_method("#{name}_before_type_cast") { enum_values.key self[name] } + pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index pairs.each do |value, i| enum_values[value] = i - # scope :incoming, -> { where direction: 0 } - klass.scope value, -> { klass.where name => i } - - # def incoming?() direction == 0 end + # def active?() status == 0 end + klass.send(:detect_enum_conflict!, name, "#{value}?") define_method("#{value}?") { self[name] == i } - # def incoming! update! direction: :incoming end + # def active!() update! status: :active end + klass.send(:detect_enum_conflict!, name, "#{value}!") define_method("#{value}!") { update! name => value } + + # scope :active, -> { where status: 0 } + klass.send(:detect_enum_conflict!, name, value, true) + klass.scope value, -> { klass.where name => i } end + + DEFINED_ENUMS[name.to_s] = enum_values end end end - def _enum_methods_module - @_enum_methods_module ||= begin - mod = Module.new - include mod - mod + private + def _enum_methods_module + @_enum_methods_module ||= begin + mod = Module.new do + private + def save_changed_attribute(attr_name, value) + if (mapping = self.class.enum_mapping_for(attr_name)) + if attribute_changed?(attr_name) + old = changed_attributes[attr_name] + + if mapping[old] == value + changed_attributes.delete(attr_name) + end + else + old = clone_attribute_value(:read_attribute, attr_name) + + if old != value + changed_attributes[attr_name] = mapping.key old + end + end + else + super + end + end + end + include mod + mod + end + end + + ENUM_CONFLICT_MESSAGE = \ + "You tried to define an enum named \"%{enum}\" on the model \"%{klass}\", but " \ + "this will generate a %{type} method \"%{method}\", which is already defined " \ + "by %{source}." + + def detect_enum_conflict!(enum_name, method_name, klass_method = false) + if klass_method && dangerous_class_method?(method_name) + raise ArgumentError, ENUM_CONFLICT_MESSAGE % { + enum: enum_name, + klass: self.name, + type: 'class', + method: method_name, + source: 'Active Record' + } + elsif !klass_method && dangerous_attribute_method?(method_name) + raise ArgumentError, ENUM_CONFLICT_MESSAGE % { + enum: enum_name, + klass: self.name, + type: 'instance', + method: method_name, + source: 'Active Record' + } + elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module) + raise ArgumentError, ENUM_CONFLICT_MESSAGE % { + enum: enum_name, + klass: self.name, + type: 'instance', + method: method_name, + source: 'another enum' + } + end end - end end end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 7e38719811..7f6228131f 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -94,6 +94,18 @@ module ActiveRecord class PreparedStatementInvalid < ActiveRecordError end + # Raised when a given database does not exist + class NoDatabaseError < ActiveRecordError + def initialize(message) + super extend_message(message) + end + + # can be over written to add additional error information. + def extend_message(message) + message + end + end + # Raised on attempt to save stale record. Record is stale when it's being saved in another query after # instantiation, for example, when two users edit the same wiki page and one starts editing and saves # the page before the other. @@ -188,7 +200,7 @@ module ActiveRecord end end - # Raised when a primary key is needed, but there is not one specified in the schema or model. + # Raised when a primary key is needed, but not specified in the schema or model. class UnknownPrimaryKey < ActiveRecordError attr_reader :model diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index d241788a9b..297792aeec 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -462,7 +462,7 @@ module ActiveRecord @class_names.delete_if { |k,klass| unless klass.is_a? Class klass = klass.safe_constantize - ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name will be removed in Rails 4.2, consider using the class itself instead.") + ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name to `set_fixture_class` will be removed in Rails 4.2. Use the class itself instead.") end !insert_class(@class_names, k, klass) } @@ -521,8 +521,16 @@ module ActiveRecord connection.transaction(:requires_new => true) do fixture_sets.each do |fs| conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection - fs.fixture_sql(conn).each do |stmt| - conn.execute stmt + table_rows = fs.table_rows + + table_rows.keys.each do |table| + conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete' + end + + table_rows.each do |fixture_set_name, rows| + rows.each do |row| + conn.insert_fixture(row, fixture_set_name) + end end end @@ -560,7 +568,7 @@ module ActiveRecord @model_class = nil if class_name.is_a?(String) - ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name will be removed in Rails 4.2, consider using the class itself instead.") + ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name to `FixtureSet.new` will be removed in Rails 4.2. Use the class itself instead.") end if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any? @@ -594,17 +602,7 @@ module ActiveRecord fixtures.size end - def fixture_sql(conn) - table_rows = self.table_rows - - table_rows.keys.map { |table| - "DELETE FROM #{conn.quote_table_name(table)}" - }.concat table_rows.flat_map { |fixture_set_name, rows| - rows.map { |row| conn.fixture_sql(row, fixture_set_name) } - } - end - - # Return a hash of rows to be inserted. The key is the table, the value is + # Returns a hash of rows to be inserted. The key is the table, the value is # a list of rows to insert to that table. def table_rows now = config.default_timezone == :utc ? Time.now.utc : Time.now diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 7e1e120288..08fc91c9df 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -16,15 +16,19 @@ module ActiveRecord # instance of the given subclass instead of the base class. def new(*args, &block) if abstract_class? || self == Base - raise NotImplementedError, "#{self} is an abstract class and can not be instantiated." + raise NotImplementedError, "#{self} is an abstract class and cannot be instantiated." end - if (attrs = args.first).is_a?(Hash) - if subclass = subclass_from_attrs(attrs) - return subclass.new(*args, &block) - end + + attrs = args.first + if subclass_from_attributes?(attrs) + subclass = subclass_from_attributes(attrs) + end + + if subclass + subclass.new(*args, &block) + else + super end - # Delegate to the original .new - super end # Returns +true+ if this does not need STI type condition. Returns @@ -45,10 +49,12 @@ module ActiveRecord end def symbolized_base_class + ActiveSupport::Deprecation.warn("ActiveRecord::Base.symbolized_base_class is deprecated and will be removed without replacement.") @symbolized_base_class ||= base_class.to_s.to_sym end def symbolized_sti_name + ActiveSupport::Deprecation.warn("ActiveRecord::Base.symbolized_sti_name is deprecated and will be removed without replacement.") @symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class end @@ -124,7 +130,7 @@ module ActiveRecord end end - raise NameError, "uninitialized constant #{candidates.first}" + raise NameError.new("uninitialized constant #{candidates.first}", candidates.first) end end @@ -170,7 +176,11 @@ module ActiveRecord # is not self or a valid subclass, raises ActiveRecord::SubclassNotFound # If this is a StrongParameters hash, and access to inheritance_column is not permitted, # this will ignore the inheritance column and return nil - def subclass_from_attrs(attrs) + def subclass_from_attributes?(attrs) + columns_hash.include?(inheritance_column) && attrs.is_a?(Hash) + end + + def subclass_from_attributes(attrs) subclass_name = attrs.with_indifferent_access[inheritance_column] if subclass_name.present? && subclass_name != self.name @@ -185,8 +195,18 @@ module ActiveRecord end end + def initialize_dup(other) + super + ensure_proper_type + end + private + def initialize_internals_callback + super + ensure_proper_type + end + # Sets the attribute used for single table inheritance to this class name if this is not the # ActiveRecord::Base descendant. # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 27576b1e61..31e2518540 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -98,8 +98,10 @@ module ActiveRecord super() else define_method :to_param do - if (default = super()) && (result = send(method_name).to_s).present? - "#{default}-#{result.squish.truncate(20, separator: /\s/, omission: nil).parameterize}" + if (default = super()) && + (result = send(method_name).to_s).present? && + (param = result.squish.truncate(20, separator: /\s/, omission: nil).parameterize).present? + "#{default}-#{param}" else default end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 7d7e97e6c9..b57da73969 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -389,6 +389,19 @@ module ActiveRecord raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration? end + def load_schema_if_pending! + if ActiveRecord::Migrator.needs_migration? + ActiveRecord::Tasks::DatabaseTasks.load_schema + check_pending! + end + end + + def maintain_test_schema! # :nodoc: + if ActiveRecord::Base.maintain_test_schema + suppress_messages { load_schema_if_pending! } + end + end + def method_missing(name, *args, &block) # :nodoc: (delegate || superclass.delegate).send(name, *args, &block) end @@ -746,7 +759,7 @@ module ActiveRecord def load_migration require(File.expand_path(filename)) - name.constantize.new + name.constantize.new(name, version) end end diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index 01c73be849..9139ad953c 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -74,7 +74,7 @@ module ActiveRecord :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column_default, :add_reference, :remove_reference, :transaction, :drop_join_table, :drop_table, :execute_block, :enable_extension, - :change_column, :execute, :remove_columns, # irreversible methods need to be here too + :change_column, :execute, :remove_columns, :change_column_null # irreversible methods need to be here too ].each do |method| class_eval <<-EOV, __FILE__, __LINE__ + 1 def #{method}(*args, &block) # def create_table(*args, &block) @@ -86,7 +86,7 @@ module ActiveRecord alias :remove_belongs_to :remove_reference def change_table(table_name, options = {}) - yield ConnectionAdapters::Table.new(table_name, self) + yield delegate.update_table_definition(table_name, self) end private @@ -157,11 +157,18 @@ module ActiveRecord alias :invert_add_belongs_to :invert_add_reference alias :invert_remove_belongs_to :invert_remove_reference + def invert_change_column_null(args) + args[2] = !args[2] + [:change_column_null, args] + end + # Forwards any missing method call to the \target. def method_missing(method, *args, &block) - @delegate.send(method, *args, &block) - rescue NoMethodError => e - raise e, e.message.sub(/ for #<.*$/, " via proxy for #{@delegate}") + if @delegate.respond_to?(method) + @delegate.send(method, *args, &block) + else + super + end end end end diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index 080b20134d..5b255c3fe5 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -50,8 +50,10 @@ module ActiveRecord 0 end - def calculate(_operation, _column_name) - if _operation == :count + def calculate(operation, _column_name, _options = {}) + # TODO: Remove _options argument as soon we remove support to + # activerecord-deprecated_finders. + if operation == :count 0 else nil diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 35fbad466e..b1b35ed940 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -181,6 +181,7 @@ module ActiveRecord became = klass.new became.instance_variable_set("@attributes", @attributes) became.instance_variable_set("@attributes_cache", @attributes_cache) + became.instance_variable_set("@changed_attributes", @changed_attributes) if defined?(@changed_attributes) became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) became.instance_variable_set("@errors", errors) @@ -195,7 +196,11 @@ module ActiveRecord # share the same set of attributes. def becomes!(klass) became = becomes(klass) - became.public_send("#{klass.inheritance_column}=", klass.sti_name) unless self.class.descends_from_active_record? + sti_type = nil + if !klass.descends_from_active_record? + sti_type = klass.sti_name + end + became.public_send("#{klass.inheritance_column}=", sti_type) became end @@ -264,7 +269,7 @@ module ActiveRecord # This method raises an +ActiveRecord::ActiveRecordError+ when called on new # objects, or when at least one of the attributes is marked as readonly. def update_columns(attributes) - raise ActiveRecordError, "can not update on a new record object" unless persisted? + raise ActiveRecordError, "cannot update on a new record object" unless persisted? attributes.each_key do |key| verify_readonly_attribute(key.to_s) @@ -361,7 +366,7 @@ module ActiveRecord # assert_equal 25, account.credit # check it is updated in memory # assert_equal 25, account.reload.credit # check it is also persisted # - # Another commom use case is optimistic locking handling: + # Another common use case is optimistic locking handling: # # def with_optimistic_retry # begin @@ -384,7 +389,7 @@ module ActiveRecord fresh_object = if options && options[:lock] - self.class.unscoped { self.class.lock.find(id) } + self.class.unscoped { self.class.lock(options[:lock]).find(id) } else self.class.unscoped { self.class.find(id) } end @@ -426,7 +431,7 @@ module ActiveRecord # ball.touch(:updated_at) # => raises ActiveRecordError # def touch(name = nil) - raise ActiveRecordError, "can not touch on a new record object" unless persisted? + raise ActiveRecordError, "cannot touch on a new record object" unless persisted? attributes = timestamp_attributes_for_update_in_model attributes << name if name diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index fd4c973504..ef138c6f80 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -1,6 +1,7 @@ module ActiveRecord module Querying delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, to: :all + delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, to: :all delegate :first_or_create, :first_or_create!, :first_or_initialize, to: :all delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all delegate :find_by, :find_by!, to: :all diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index eef08aea88..11b564f8f9 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -31,22 +31,16 @@ module ActiveRecord config.active_record.use_schema_cache_dump = true + config.active_record.maintain_test_schema = true config.eager_load_namespaces << ActiveRecord rake_tasks do require "active_record/base" - ActiveRecord::Tasks::DatabaseTasks.seed_loader = Rails.application - ActiveRecord::Tasks::DatabaseTasks.env = Rails.env - namespace :db do task :load_config do - ActiveRecord::Tasks::DatabaseTasks.db_dir = Rails.application.config.paths["db"].first ActiveRecord::Tasks::DatabaseTasks.database_configuration = Rails.application.config.database_configuration - ActiveRecord::Tasks::DatabaseTasks.migrations_paths = Rails.application.paths['db/migrate'].to_a - ActiveRecord::Tasks::DatabaseTasks.fixtures_path = File.join Rails.root, 'test', 'fixtures' - ActiveRecord::Tasks::DatabaseTasks.root = Rails.root if defined?(ENGINE_PATH) && engine = Rails::Engine.find(ENGINE_PATH) if engine.paths['db/migrate'].existent @@ -122,7 +116,16 @@ module ActiveRecord # and then establishes the connection. initializer "active_record.initialize_database" do |app| ActiveSupport.on_load(:active_record) do - self.configurations = app.config.database_configuration || {} + + class ActiveRecord::NoDatabaseError + remove_possible_method :extend_message + def extend_message(message) + message << "Run `$ bin/rake db:create db:migrate` to create your database" + message + end + end + + self.configurations = Rails.application.config.database_configuration establish_connection end end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 52b3d3e5e6..561387a179 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -2,7 +2,7 @@ require 'active_record' db_namespace = namespace :db do task :load_config do - ActiveRecord::Base.configurations = ActiveRecord::Tasks::DatabaseTasks.database_configuration || {} + ActiveRecord::Base.configurations = ActiveRecord::Tasks::DatabaseTasks.database_configuration || {} ActiveRecord::Migrator.migrations_paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths end @@ -12,13 +12,9 @@ db_namespace = namespace :db do end end - desc 'Create the database from DATABASE_URL or config/database.yml for the current Rails.env (use db:create:all to create all databases in the config)' + desc 'Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV it defaults to creating the development and test databases.' task :create => [:load_config] do - if ENV['DATABASE_URL'] - ActiveRecord::Tasks::DatabaseTasks.create_database_url - else - ActiveRecord::Tasks::DatabaseTasks.create_current - end + ActiveRecord::Tasks::DatabaseTasks.create_current end namespace :drop do @@ -27,13 +23,9 @@ db_namespace = namespace :db do end end - desc 'Drops the database using DATABASE_URL or the current Rails.env (use db:drop:all to drop all databases)' + desc 'Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV it defaults to dropping the development and test databases.' task :drop => [:load_config] do - if ENV['DATABASE_URL'] - ActiveRecord::Tasks::DatabaseTasks.drop_database_url - else - ActiveRecord::Tasks::DatabaseTasks.drop_current - end + ActiveRecord::Tasks::DatabaseTasks.drop_current end desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." @@ -187,9 +179,6 @@ db_namespace = namespace :db do require 'active_record/fixtures' base_dir = if ENV['FIXTURES_PATH'] - STDERR.puts "Using FIXTURES_PATH env variable is deprecated, please use " + - "ActiveRecord::Tasks::DatabaseTasks.fixtures_path = '/path/to/fixtures' " + - "instead." File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten else ActiveRecord::Tasks::DatabaseTasks.fixtures_path @@ -212,9 +201,6 @@ db_namespace = namespace :db do puts %Q(The fixture ID for "#{label}" is #{ActiveRecord::FixtureSet.identify(label)}.) if label base_dir = if ENV['FIXTURES_PATH'] - STDERR.puts "Using FIXTURES_PATH env variable is deprecated, please use " + - "ActiveRecord::Tasks::DatabaseTasks.fixtures_path = '/path/to/fixtures' " + - "instead." File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten else ActiveRecord::Tasks::DatabaseTasks.fixtures_path @@ -248,9 +234,7 @@ db_namespace = namespace :db do desc 'Load a schema.rb file into the database' task :load => [:environment, :load_config] do - file = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, 'schema.rb') - ActiveRecord::Tasks::DatabaseTasks.check_schema_file(file) - load(file) + ActiveRecord::Tasks::DatabaseTasks.load_schema(:ruby, ENV['SCHEMA']) end task :load_if_ruby => ['db:create', :environment] do @@ -295,10 +279,7 @@ db_namespace = namespace :db do # desc "Recreate the databases from the structure.sql file" task :load => [:environment, :load_config] do - filename = ENV['DB_STRUCTURE'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql") - ActiveRecord::Tasks::DatabaseTasks.check_schema_file(filename) - current_config = ActiveRecord::Tasks::DatabaseTasks.current_config - ActiveRecord::Tasks::DatabaseTasks.structure_load(current_config, filename) + ActiveRecord::Tasks::DatabaseTasks.load_schema(:sql, ENV['DB_STRUCTURE']) end task :load_if_sql => ['db:create', :environment] do @@ -308,8 +289,15 @@ db_namespace = namespace :db do namespace :test do + task :deprecated do + Rake.application.top_level_tasks.grep(/^db:test:/).each do |task| + $stderr.puts "WARNING: #{task} is deprecated. The Rails test helper now maintains " \ + "your test schema automatically, see the release notes for details." + end + end + # desc "Recreate the test database from the current schema" - task :load => 'db:test:purge' do + task :load => %w(db:test:deprecated db:test:purge) do case ActiveRecord::Base.schema_format when :ruby db_namespace["test:load_schema"].invoke @@ -319,7 +307,7 @@ db_namespace = namespace :db do end # desc "Recreate the test database from an existent schema.rb file" - task :load_schema => 'db:test:purge' do + task :load_schema => %w(db:test:deprecated db:test:purge) do begin should_reconnect = ActiveRecord::Base.connection_pool.active_connection? ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test']) @@ -333,7 +321,7 @@ db_namespace = namespace :db do end # desc "Recreate the test database from an existent structure.sql file" - task :load_structure => 'db:test:purge' do + task :load_structure => %w(db:test:deprecated db:test:purge) do begin ActiveRecord::Tasks::DatabaseTasks.current_config(:config => ActiveRecord::Base.configurations['test']) db_namespace["structure:load"].invoke @@ -343,7 +331,7 @@ db_namespace = namespace :db do end # desc "Recreate the test database from a fresh schema" - task :clone do + task :clone => %w(db:test:deprecated environment) do case ActiveRecord::Base.schema_format when :ruby db_namespace["test:clone_schema"].invoke @@ -353,18 +341,18 @@ db_namespace = namespace :db do end # desc "Recreate the test database from a fresh schema.rb file" - task :clone_schema => ["db:schema:dump", "db:test:load_schema"] + task :clone_schema => %w(db:test:deprecated db:schema:dump db:test:load_schema) # desc "Recreate the test database from a fresh structure.sql file" - task :clone_structure => [ "db:structure:dump", "db:test:load_structure" ] + task :clone_structure => %w(db:test:deprecated db:structure:dump db:test:load_structure) # desc "Empty the test database" - task :purge => [:environment, :load_config] do + task :purge => %w(db:test:deprecated environment load_config) do ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations['test'] end # desc 'Check for pending migrations and load the test schema' - task :prepare => :load_config do + task :prepare => %w(db:test:deprecated environment load_config) do unless ActiveRecord::Base.configurations.blank? db_namespace['test:load'].invoke end @@ -399,6 +387,3 @@ namespace :railties do end end end - -task 'test:prepare' => ['db:test:prepare', 'db:test:load', 'db:abort_if_pending_migrations'] - diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 745c6cf349..fb213dc6f7 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -24,6 +24,7 @@ module ActiveRecord @klass = klass @table = table @values = values + @offsets = {} @loaded = false end @@ -495,9 +496,10 @@ module ActiveRecord end def reset - @first = @last = @to_sql = @order_clause = @scope_for_create = @arel = @loaded = nil + @last = @to_sql = @order_clause = @scope_for_create = @arel = @loaded = nil @should_eager_load = @join_dependency = nil @records = [] + @offsets = {} self end @@ -533,7 +535,6 @@ module ActiveRecord } binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }] - binds.merge!(Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }]) Hash[equalities.map { |where| name = where.left.name diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 87d6f12aa5..dfcfef2ad2 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -72,6 +72,8 @@ module ActiveRecord # group.each(&:recover_from_last_night!) # end # + # To be yielded each record one by one, use #find_each instead. + # # ==== Options # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. # * <tt>:start</tt> - Specifies the starting point for the batch processing. @@ -111,16 +113,13 @@ module ActiveRecord while records.any? records_size = records.size primary_key_offset = records.last.id + raise "Primary key not included in the custom select clause" unless primary_key_offset yield records break if records_size < batch_size - if primary_key_offset - records = relation.where(table[primary_key].gt(primary_key_offset)).to_a - else - raise "Primary key not included in the custom select clause" - end + records = relation.where(table[primary_key].gt(primary_key_offset)).to_a end end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 2d267183ce..45ffb99868 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -19,16 +19,21 @@ module ActiveRecord # # Person.group(:city).count # # => { 'Rome' => 5, 'Paris' => 3 } - def count(column_name = nil) - calculate(:count, column_name) + def count(column_name = nil, options = {}) + # TODO: Remove options argument as soon we remove support to + # activerecord-deprecated_finders. + column_name, options = nil, column_name if column_name.is_a?(Hash) + calculate(:count, column_name, options) end # Calculates the average value on a given column. Returns +nil+ if there's # no row. See +calculate+ for examples with options. # # Person.average(:age) # => 35.8 - def average(column_name) - calculate(:average, column_name) + def average(column_name, options = {}) + # TODO: Remove options argument as soon we remove support to + # activerecord-deprecated_finders. + calculate(:average, column_name, options) end # Calculates the minimum value on a given column. The value is returned @@ -36,8 +41,10 @@ module ActiveRecord # +calculate+ for examples with options. # # Person.minimum(:age) # => 7 - def minimum(column_name) - calculate(:minimum, column_name) + def minimum(column_name, options = {}) + # TODO: Remove options argument as soon we remove support to + # activerecord-deprecated_finders. + calculate(:minimum, column_name, options) end # Calculates the maximum value on a given column. The value is returned @@ -45,8 +52,10 @@ module ActiveRecord # +calculate+ for examples with options. # # Person.maximum(:age) # => 93 - def maximum(column_name) - calculate(:maximum, column_name) + def maximum(column_name, options = {}) + # TODO: Remove options argument as soon we remove support to + # activerecord-deprecated_finders. + calculate(:maximum, column_name, options) end # Calculates the sum of values on a given column. The value is returned @@ -89,15 +98,17 @@ module ActiveRecord # Person.group(:last_name).having("min(age) > 17").minimum(:age) # # Person.sum("2 * age") - def calculate(operation, column_name) + def calculate(operation, column_name, options = {}) + # TODO: Remove options argument as soon we remove support to + # activerecord-deprecated_finders. if column_name.is_a?(Symbol) && attribute_alias?(column_name) column_name = attribute_alias(column_name) end if has_include?(column_name) - construct_relation_for_association_calculations.calculate(operation, column_name) + construct_relation_for_association_calculations.calculate(operation, column_name, options) else - perform_calculation(operation, column_name) + perform_calculation(operation, column_name, options) end end @@ -180,7 +191,9 @@ module ActiveRecord eager_loading? || (includes_values.present? && (column_name || references_eager_loaded_tables?)) end - def perform_calculation(operation, column_name) + def perform_calculation(operation, column_name, options = {}) + # TODO: Remove options argument as soon we remove support to + # activerecord-deprecated_finders. operation = operation.to_s.downcase # If #count is used with #distinct / #uniq it is considered distinct. (eg. relation.distinct.count) @@ -311,7 +324,9 @@ module ActiveRecord } key = key.first if key.size == 1 key = key_records[key] if associated - [key, type_cast_calculated_value(row[aggregate_alias], column_for(column_name), operation)] + + column_type = calculated_data.column_types.fetch(aggregate_alias) { column_for(column_name) } + [key, type_cast_calculated_value(row[aggregate_alias], column_type, operation)] end] end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 1e15bddcdf..21beed332f 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -1,3 +1,4 @@ +require 'set' require 'active_support/concern' require 'active_support/deprecation' @@ -36,7 +37,14 @@ module ActiveRecord # may vary depending on the klass of a relation, so we create a subclass of Relation # for each different klass, and the delegations are compiled into that subclass only. - delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :to => :to_a + BLACKLISTED_ARRAY_METHODS = [ + :compact!, :flatten!, :reject!, :reverse!, :rotate!, :map!, + :shuffle!, :slice!, :sort!, :sort_by!, :delete_if, + :keep_if, :pop, :shift, :delete_at, :compact + ].to_set # :nodoc: + + delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, to: :to_a + delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, :connection, :columns_hash, :to => :klass @@ -64,7 +72,7 @@ module ActiveRecord RUBY else define_method method do |*args, &block| - scoping { @klass.send(method, *args, &block) } + scoping { @klass.public_send(method, *args, &block) } end end end @@ -83,13 +91,10 @@ module ActiveRecord def method_missing(method, *args, &block) if @klass.respond_to?(method) self.class.delegate_to_scoped_klass(method) - scoping { @klass.send(method, *args, &block) } - elsif array_delegable?(method) - self.class.delegate method, :to => :to_a - to_a.send(method, *args, &block) + scoping { @klass.public_send(method, *args, &block) } elsif arel.respond_to?(method) self.class.delegate method, :to => :arel - arel.send(method, *args, &block) + arel.public_send(method, *args, &block) else super end @@ -109,30 +114,24 @@ module ActiveRecord end def respond_to?(method, include_private = false) - super || array_delegable?(method) || - @klass.respond_to?(method, include_private) || + super || @klass.respond_to?(method, include_private) || + array_delegable?(method) || arel.respond_to?(method, include_private) end protected def array_delegable?(method) - defined = Array.method_defined?(method) - if defined && method.to_s.ends_with?('!') - ActiveSupport::Deprecation.warn( - "Association will no longer delegate #{method} to #to_a as of Rails 4.2. You instead must first call #to_a on the association to expose the array to be acted on." - ) - end - defined + Array.method_defined?(method) && BLACKLISTED_ARRAY_METHODS.exclude?(method) end def method_missing(method, *args, &block) if @klass.respond_to?(method) - scoping { @klass.send(method, *args, &block) } + scoping { @klass.public_send(method, *args, &block) } elsif array_delegable?(method) - to_a.send(method, *args, &block) + to_a.public_send(method, *args, &block) elsif arel.respond_to?(method) - arel.send(method, *args, &block) + arel.public_send(method, *args, &block) else super end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 3963f2b3e0..01d46f7676 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -127,9 +127,9 @@ module ActiveRecord # def first(limit = nil) if limit - find_first_with_limit(limit) + find_nth_with_limit(offset_value, limit) else - find_first + find_nth(:first, offset_value) end end @@ -172,6 +172,86 @@ module ActiveRecord last or raise RecordNotFound end + # Find the second record. + # If no order is defined it will order by primary key. + # + # Person.second # returns the second object fetched by SELECT * FROM people + # Person.offset(3).second # returns the second object from OFFSET 3 (which is OFFSET 4) + # Person.where(["user_name = :u", { u: user_name }]).second + def second + find_nth(:second, offset_value ? offset_value + 1 : 1) + end + + # Same as +second+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # is found. + def second! + second or raise RecordNotFound + end + + # Find the third record. + # If no order is defined it will order by primary key. + # + # Person.third # returns the third object fetched by SELECT * FROM people + # Person.offset(3).third # returns the third object from OFFSET 3 (which is OFFSET 5) + # Person.where(["user_name = :u", { u: user_name }]).third + def third + find_nth(:third, offset_value ? offset_value + 2 : 2) + end + + # Same as +third+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # is found. + def third! + third or raise RecordNotFound + end + + # Find the fourth record. + # If no order is defined it will order by primary key. + # + # Person.fourth # returns the fourth object fetched by SELECT * FROM people + # Person.offset(3).fourth # returns the fourth object from OFFSET 3 (which is OFFSET 6) + # Person.where(["user_name = :u", { u: user_name }]).fourth + def fourth + find_nth(:fourth, offset_value ? offset_value + 3 : 3) + end + + # Same as +fourth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # is found. + def fourth! + fourth or raise RecordNotFound + end + + # Find the fifth record. + # If no order is defined it will order by primary key. + # + # Person.fifth # returns the fifth object fetched by SELECT * FROM people + # Person.offset(3).fifth # returns the fifth object from OFFSET 3 (which is OFFSET 7) + # Person.where(["user_name = :u", { u: user_name }]).fifth + def fifth + find_nth(:fifth, offset_value ? offset_value + 4 : 4) + end + + # Same as +fifth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # is found. + def fifth! + fifth or raise RecordNotFound + end + + # Find the forty-second record. Also known as accessing "the reddit". + # 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.where(["user_name = :u", { u: user_name }]).forty_two + def forty_two + find_nth(:forty_two, offset_value ? offset_value + 41 : 41) + end + + # Same as +forty_two+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # is found. + def forty_two! + forty_two or raise RecordNotFound + end + # Returns +true+ if a record exists in the table that matches the +id+ or # conditions given, or +false+ otherwise. The argument can take six forms: # @@ -195,6 +275,7 @@ module ActiveRecord # Person.exists?(5) # Person.exists?('5') # Person.exists?(['name LIKE ?', "%#{query}%"]) + # Person.exists?(id: [1, 4, 8]) # Person.exists?(name: 'David') # Person.exists?(false) # Person.exists? @@ -230,9 +311,9 @@ module ActiveRecord conditions = " [#{conditions}]" if conditions if Array(ids).size == 1 - error = "Couldn't find #{@klass.name} with #{primary_key}=#{ids}#{conditions}" + error = "Couldn't find #{@klass.name} with '#{primary_key}'=#{ids}#{conditions}" else - error = "Couldn't find all #{@klass.name.pluralize} with IDs " + error = "Couldn't find all #{@klass.name.pluralize} with '#{primary_key}': " error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})" end @@ -363,19 +444,19 @@ module ActiveRecord end end - def find_first + def find_nth(ordinal, offset) if loaded? - @records.first + @records.send(ordinal) else - @first ||= find_first_with_limit(1).first + @offsets[offset] ||= find_nth_with_limit(offset, 1).first end end - def find_first_with_limit(limit) + def find_nth_with_limit(offset, limit) if order_values.empty? && primary_key - order(arel_table[primary_key].asc).limit(limit).to_a + order(arel_table[primary_key].asc).limit(limit).offset(offset).to_a else - limit(limit).to_a + limit(limit).offset(offset).to_a end end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index c60cd27a83..1252af7635 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -55,9 +55,9 @@ module ActiveRecord # # For polymorphic relationships, find the foreign key and type: # PriceEstimate.where(estimate_of: treasure) - if klass && value.is_a?(Base) && reflection = klass.reflect_on_association(column.to_sym) - if reflection.polymorphic? - queries << build(table[reflection.foreign_type], value.class.base_class) + 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 column = reflection.foreign_key @@ -67,6 +67,18 @@ module ActiveRecord queries end + def self.polymorphic_base_class_from_value(value) + case value + when Relation + value.klass.base_class + when Array + val = value.compact.first + val.class.base_class if val.is_a?(Base) + when Base + value.class.base_class + end + end + def self.references(attributes) attributes.map do |key, value| if value.is_a?(Hash) diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index cacc787eba..88fc47fada 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -37,6 +37,8 @@ module ActiveRecord def not(opts, *rest) where_value = @scope.send(:build_where, opts, rest).map do |rel| case rel + when NilClass + raise ArgumentError, 'Invalid argument for .where.not(), got nil.' when Arel::Nodes::In Arel::Nodes::NotIn.new(rel.left, rel.right) when Arel::Nodes::Equality @@ -232,7 +234,9 @@ module ActiveRecord def select!(*fields) # :nodoc: fields.flatten! - + fields.map! do |field| + klass.attribute_alias?(field) ? klass.attribute_alias(field).to_sym : field + end self.select_values += fields self end @@ -427,7 +431,7 @@ module ActiveRecord # === string # # A single string, without additional arguments, is passed to the query - # constructor as a SQL fragment, and used in the where clause of the query. + # constructor as an SQL fragment, and used in the where clause of the query. # # Client.where("orders_count = '2'") # # SELECT * from clients where orders_count = '2'; @@ -631,12 +635,11 @@ module ActiveRecord self end - # Returns a chainable relation with zero records, specifically an - # instance of the <tt>ActiveRecord::NullRelation</tt> class. + # Returns a chainable relation with zero records. # - # The returned <tt>ActiveRecord::NullRelation</tt> inherits from Relation and implements the - # Null Object pattern. It is an object with defined null behavior and always returns an empty - # array of records without querying the database. + # The returned relation implements the Null Object pattern. It is an + # object with defined null behavior and always returns an empty array of + # records without querying the database. # # Any subsequent condition chained to the returned relation will continue # generating an empty relation and will not fire any query to the database. @@ -656,7 +659,7 @@ module ActiveRecord # when 'Reviewer' # Post.published # when 'Bad User' - # Post.none # => returning [] instead breaks the previous code + # Post.none # It can't be chained if [] is returned. # end # end # @@ -854,11 +857,11 @@ module ActiveRecord end single_val_method = Relation::SINGLE_VALUE_METHODS.include?(scope) - unscope_code = :"#{scope}_value#{'s' unless single_val_method}=" + unscope_code = "#{scope}_value#{'s' unless single_val_method}=" case scope when :order - self.send(:reverse_order_value=, false) + self.reverse_order_value = false result = [] else result = [] unless single_val_method @@ -868,17 +871,19 @@ module ActiveRecord end def where_unscoping(target_value) - target_value_sym = target_value.to_sym + target_value = target_value.to_s where_values.reject! do |rel| case rel when Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual subrelation = (rel.left.kind_of?(Arel::Attributes::Attribute) ? rel.left : rel.right) - subrelation.name.to_sym == target_value_sym + subrelation.name == target_value else - raise "unscope(where: #{target_value.inspect}) failed: unscoping #{rel.class} is unimplemented." + raise "unscope(where: #{target_value.inspect}) failed: unscoping #{rel.class} \"#{rel}\" is unimplemented." end end + + bind_values.reject! { |col,_| col.name == target_value } end def custom_join_ast(table, joins) @@ -983,8 +988,13 @@ module ActiveRecord end def build_select(arel, selects) - unless selects.empty? - arel.project(*selects) + if !selects.empty? + expanded_select = selects.map do |field| + columns_hash.key?(field.to_s) ? arel_table[field] : field + end + arel.project(*expanded_select) + elsif from_value + arel.project(Arel.star) else arel.project(@klass.arel_table[Arel.star]) end @@ -1040,9 +1050,11 @@ module ActiveRecord order_args.map! do |arg| case arg when Symbol + arg = klass.attribute_alias(arg).to_sym if klass.attribute_alias?(arg) table[arg].asc when Hash arg.map { |field, dir| + field = klass.attribute_alias(field).to_sym if klass.attribute_alias?(field) table[field].send(dir) } else diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index cab8fd745a..dacaec26b7 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -100,8 +100,9 @@ module ActiveRecord # { status: nil, group_id: 1 } # # => "status = NULL , group_id = 1" def sanitize_sql_hash_for_assignment(attrs, table) + c = connection attrs.map do |attr, value| - "#{connection.quote_table_name_for_assignment(table, attr)} = #{quote_bound_value(value)}" + "#{c.quote_table_name_for_assignment(table, attr)} = #{quote_bound_value(value, c, columns_hash[attr.to_s])}" end.join(', ') end @@ -152,8 +153,10 @@ module ActiveRecord end end - def quote_bound_value(value, c = connection) #:nodoc: - if value.respond_to?(:map) && !value.acts_like?(:string) + def quote_bound_value(value, c = connection, column = nil) #:nodoc: + if column + c.quote(value, column) + elsif value.respond_to?(:map) && !value.acts_like?(:string) if value.respond_to?(:empty?) && value.empty? c.quote(nil) else diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index 0cf3d59985..3e43591672 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -27,6 +27,11 @@ module ActiveRecord end end + def initialize_internals_callback + super + populate_with_current_scope_attributes + end + # This class stores the +:current_scope+ and +:ignore_default_scope+ values # for different classes. The registry is stored as a thread local, which is # accessed through +ScopeRegistry.current+. diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 2a5718f388..49cadb66d0 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -139,6 +139,12 @@ module ActiveRecord # Article.published.featured.latest_article # Article.featured.titles def scope(name, body, &block) + if dangerous_class_method?(name) + raise ArgumentError, "You tried to define a scope named \"#{name}\" " \ + "on the model \"#{self.name}\", but Active Record already defined " \ + "a class method with the same name." + end + extension = Module.new(&block) if block singleton_class.send(:define_method, name) do |*args| diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 186a737561..79a6ccbda0 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -178,7 +178,7 @@ module ActiveRecord end def load(yaml) - self.class.as_indifferent_hash(@coder.load(yaml)) + self.class.as_indifferent_hash(@coder.load(yaml || '')) end def self.as_indifferent_hash(obj) diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index be7d496d15..6ce0495f6f 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -36,9 +36,8 @@ module ActiveRecord module DatabaseTasks extend self - attr_writer :current_config - attr_accessor :database_configuration, :migrations_paths, :seed_loader, :db_dir, - :fixtures_path, :env, :root + attr_writer :current_config, :db_dir, :migrations_paths, :fixtures_path, :root, :env, :seed_loader + attr_accessor :database_configuration LOCAL_HOSTS = ['127.0.0.1', 'localhost'] @@ -51,16 +50,36 @@ module ActiveRecord register_task(/postgresql/, ActiveRecord::Tasks::PostgreSQLDatabaseTasks) register_task(/sqlite/, ActiveRecord::Tasks::SQLiteDatabaseTasks) + def db_dir + @db_dir ||= Rails.application.config.paths["db"].first + end + + def migrations_paths + @migrations_paths ||= Rails.application.paths['db/migrate'].to_a + end + + def fixtures_path + @fixtures_path ||= File.join(root, 'test', 'fixtures') + end + + def root + @root ||= Rails.root + end + + def env + @env ||= Rails.env + end + + def seed_loader + @seed_loader ||= Rails.application + end + def current_config(options = {}) options.reverse_merge! :env => env if options.has_key?(:config) @current_config = options[:config] else - @current_config ||= if ENV['DATABASE_URL'] - database_url_config - else - ActiveRecord::Base.configurations[options[:env]] - end + @current_config ||= ActiveRecord::Base.configurations[options[:env]] end end @@ -82,11 +101,7 @@ module ActiveRecord each_current_configuration(environment) { |configuration| create configuration } - ActiveRecord::Base.establish_connection environment - end - - def create_database_url - create database_url_config + ActiveRecord::Base.establish_connection(environment.to_sym) end def drop(*arguments) @@ -107,10 +122,6 @@ module ActiveRecord } end - def drop_database_url - drop database_url_config - end - def charset_current(environment = env) charset ActiveRecord::Base.configurations[environment] end @@ -145,6 +156,21 @@ module ActiveRecord class_for_adapter(configuration['adapter']).new(*arguments).structure_load(filename) end + def load_schema(format = ActiveRecord::Base.schema_format, file = nil) + case format + when :ruby + file ||= File.join(db_dir, "schema.rb") + check_schema_file(file) + load(file) + when :sql + file ||= File.join(db_dir, "structure.sql") + check_schema_file(file) + structure_load(current_config, file) + else + raise ArgumentError, "unknown format #{format.inspect}" + end + end + def check_schema_file(filename) unless File.exist?(filename) message = %{#{filename} doesn't exist yet. Run `rake db:migrate` to create it, then try again.} @@ -165,11 +191,6 @@ module ActiveRecord private - def database_url_config - @database_url_config ||= - ConnectionAdapters::ConnectionSpecification::Resolver.new(ENV["DATABASE_URL"], {}).spec.config.stringify_keys - end - def class_for_adapter(adapter) key = @tasks.keys.detect { |pattern| adapter[pattern] } unless key @@ -180,7 +201,8 @@ module ActiveRecord def each_current_configuration(environment) environments = [environment] - environments << 'test' if environment == 'development' + # add test environment only if no RAILS_ENV was specified. + environments << 'test' if environment == 'development' && ENV['RAILS_ENV'].nil? configurations = ActiveRecord::Base.configurations.values_at(*environments) configurations.compact.each do |configuration| diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index e0541b7681..7178bed560 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -37,8 +37,8 @@ module ActiveRecord end def initialize_dup(other) # :nodoc: - clear_timestamp_attributes super + clear_timestamp_attributes end private @@ -71,7 +71,7 @@ module ActiveRecord end def should_record_timestamps? - self.record_timestamps && (!partial_writes? || changed? || (attributes.keys & self.class.serialized_attributes.keys).present?) + self.record_timestamps && (!partial_writes? || changed?) end def timestamp_attributes_for_create_in_model diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 45313b5e75..c33ffeece0 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -6,9 +6,6 @@ module ActiveRecord extend ActiveSupport::Concern ACTIONS = [:create, :destroy, :update] - class TransactionError < ActiveRecordError # :nodoc: - end - included do define_callbacks :commit, :rollback, terminator: ->(_, result) { result == false }, @@ -243,15 +240,14 @@ module ActiveRecord def set_options_for_callbacks!(args) options = args.last if options.is_a?(Hash) && options[:on] - assert_valid_transaction_action(options[:on]) - options[:if] = Array(options[:if]) fire_on = Array(options[:on]) + assert_valid_transaction_action(fire_on) + options[:if] = Array(options[:if]) options[:if] << "transaction_include_any_action?(#{fire_on})" end end def assert_valid_transaction_action(actions) - actions = Array(actions) if (actions - ACTIONS).any? raise ArgumentError, ":on conditions for after_commit and after_rollback callbacks have to be one of #{ACTIONS.join(",")}" end @@ -277,6 +273,10 @@ module ActiveRecord with_transaction_returning_status { super } end + def touch(*) #:nodoc: + with_transaction_returning_status { super } + end + # Reset id and @new_record if the transaction rolls back. def rollback_active_record_state! remember_transaction_record_state diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb index 6b14c39686..9a19483da3 100644 --- a/activerecord/lib/active_record/validations/presence.rb +++ b/activerecord/lib/active_record/validations/presence.rb @@ -5,7 +5,7 @@ module ActiveRecord super attributes.each do |attribute| next unless record.class.reflect_on_association(attribute) - associated_records = Array(record.send(attribute)) + associated_records = Array.wrap(record.send(attribute)) # Superclass validates presence. Ensure present records aren't about to be destroyed. if associated_records.present? && associated_records.all? { |r| r.marked_for_destruction? } diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb index de5fd05468..863c3ebe4d 100644 --- a/activerecord/lib/active_record/version.rb +++ b/activerecord/lib/active_record/version.rb @@ -1,7 +1,7 @@ module ActiveRecord # Returns the version of the currently loaded ActiveRecord as a Gem::Version def self.version - Gem::Version.new "4.1.0.beta" + Gem::Version.new "4.1.0.beta1" end module VERSION #:nodoc: diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 34be68a97f..b67e70ec7e 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -92,7 +92,7 @@ module ActiveRecord ) end ensure - ActiveRecord::Base.establish_connection 'arunit' + ActiveRecord::Base.establish_connection :arunit end end end @@ -191,7 +191,7 @@ module ActiveRecord end def setup - Klass.establish_connection 'arunit' + Klass.establish_connection :arunit @connection = Klass.connection end diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb index a1b41b6991..5cd5d8ac5f 100644 --- a/activerecord/test/cases/adapters/mysql/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql/connection_test.rb @@ -71,7 +71,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase def test_exec_no_binds @connection.exec_query('drop table if exists ex') @connection.exec_query(<<-eosql) - CREATE TABLE `ex` (`id` int(11) DEFAULT NULL auto_increment PRIMARY KEY, + CREATE TABLE `ex` (`id` int(11) auto_increment PRIMARY KEY, `data` varchar(255)) eosql result = @connection.exec_query('SELECT id, data FROM ex') @@ -93,7 +93,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase def test_exec_with_binds @connection.exec_query('drop table if exists ex') @connection.exec_query(<<-eosql) - CREATE TABLE `ex` (`id` int(11) DEFAULT NULL auto_increment PRIMARY KEY, + CREATE TABLE `ex` (`id` int(11) auto_increment PRIMARY KEY, `data` varchar(255)) eosql @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') @@ -109,7 +109,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase def test_exec_typecasts_bind_vals @connection.exec_query('drop table if exists ex') @connection.exec_query(<<-eosql) - CREATE TABLE `ex` (`id` int(11) DEFAULT NULL auto_increment PRIMARY KEY, + CREATE TABLE `ex` (`id` int(11) auto_increment PRIMARY KEY, `data` varchar(255)) eosql @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb index 9ad0744aee..578f6301bd 100644 --- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb @@ -10,12 +10,20 @@ module ActiveRecord @conn.exec_query('drop table if exists ex') @conn.exec_query(<<-eosql) CREATE TABLE `ex` ( - `id` int(11) DEFAULT NULL auto_increment PRIMARY KEY, + `id` int(11) auto_increment PRIMARY KEY, `number` integer, `data` varchar(255)) eosql end + def test_bad_connection_mysql + assert_raise ActiveRecord::NoDatabaseError do + configuration = ActiveRecord::Base.configurations['arunit'].merge(database: 'inexistent_activerecord_unittest') + connection = ActiveRecord::Base.mysql_connection(configuration) + connection.exec_query('drop table if exists ex') + end + end + def test_valid_column column = @conn.columns('ex').find { |col| col.name == 'id' } assert @conn.valid_type?(column.type) @@ -75,7 +83,7 @@ module ActiveRecord @conn.exec_query('drop table if exists ex_with_non_standard_pk') @conn.exec_query(<<-eosql) CREATE TABLE `ex_with_non_standard_pk` ( - `code` INT(11) DEFAULT NULL auto_increment, + `code` INT(11) auto_increment, PRIMARY KEY (`code`)) eosql pk, seq = @conn.pk_and_sequence_for('ex_with_non_standard_pk') @@ -87,7 +95,7 @@ module ActiveRecord @conn.exec_query('drop table if exists ex_with_custom_index_type_pk') @conn.exec_query(<<-eosql) CREATE TABLE `ex_with_custom_index_type_pk` ( - `id` INT(11) DEFAULT NULL auto_increment, + `id` INT(11) auto_increment, PRIMARY KEY USING BTREE (`id`)) eosql pk, seq = @conn.pk_and_sequence_for('ex_with_custom_index_type_pk') diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 8dc1df1851..9b7202c915 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -13,6 +13,14 @@ class MysqlConnectionTest < ActiveRecord::TestCase super end + def test_bad_connection + assert_raise ActiveRecord::NoDatabaseError do + configuration = ActiveRecord::Base.configurations['arunit'].merge(database: 'inexistent_activerecord_unittest') + connection = ActiveRecord::Base.mysql2_connection(configuration) + connection.exec_query('drop table if exists ex') + end + end + def test_no_automatic_reconnection_after_timeout assert @connection.active? @connection.update('set @@wait_timeout=1') diff --git a/activerecord/test/cases/adapters/mysql2/explain_test.rb b/activerecord/test/cases/adapters/mysql2/explain_test.rb index 68ed361aeb..1cd356e868 100644 --- a/activerecord/test/cases/adapters/mysql2/explain_test.rb +++ b/activerecord/test/cases/adapters/mysql2/explain_test.rb @@ -10,15 +10,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 %(developers | const), 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 %(developers | const), 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 %(audit_logs | ALL), explain + assert_match %r(audit_logs |.* ALL), explain end end end diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb index 5db60ff8a0..43c9116b5a 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb @@ -65,6 +65,15 @@ module ActiveRecord assert_nil index_c.using assert_equal :fulltext, index_c.type end + + def test_drop_temporary_table + @connection.transaction do + @connection.create_table(:temp_table, temporary: true) + # if it doesn't properly say DROP TEMPORARY TABLE, the transaction commit + # will complain that no transaction is active + @connection.drop_table(:temp_table, temporary: true) + end + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index 9536cceb1d..d71e2aa2bb 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -10,12 +10,12 @@ class PostgresqlArrayTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection - @connection.transaction do - @connection.create_table('pg_arrays') do |t| - t.string 'tags', array: true - t.integer 'ratings', array: true - end + @connection.transaction do + @connection.create_table('pg_arrays') do |t| + t.string 'tags', array: true + t.integer 'ratings', array: true end + end @column = PgArray.columns.find { |c| c.name == 'tags' } end @@ -26,6 +26,12 @@ class PostgresqlArrayTest < ActiveRecord::TestCase def test_column assert_equal :string, @column.type assert @column.array + assert_not @column.text? + + ratings_column = PgArray.columns_hash['ratings'] + assert_equal :integer, ratings_column.type + assert ratings_column.array + assert_not ratings_column.number? end def test_change_column_with_array @@ -50,8 +56,6 @@ class PostgresqlArrayTest < ActiveRecord::TestCase end def test_type_cast_array - assert @column - 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 @@ -66,6 +70,12 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert_equal([nil], @column.type_cast('{NULL}')) end + def test_type_cast_integers + x = PgArray.new(ratings: ['1', '2']) + assert x.save! + assert_equal(['1', '2'], x.ratings) + end + def test_rewrite @connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')" x = PgArray.first @@ -118,6 +128,16 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]", record.attribute_for_inspect(:ratings)) end + def test_update_all + pg_array = PgArray.create! tags: ["one", "two", "three"] + + PgArray.update_all tags: ["four", "five"] + assert_equal ["four", "five"], pg_array.reload.tags + + PgArray.update_all tags: [] + assert_equal [], pg_array.reload.tags + end + private def assert_cycle field, array # test creation diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index 01de202d11..04a458fbce 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -3,9 +3,6 @@ require "cases/helper" class PostgresqlArray < ActiveRecord::Base end -class PostgresqlRange < ActiveRecord::Base -end - class PostgresqlTsvector < ActiveRecord::Base end @@ -46,104 +43,6 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase @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 <<_SQL if @connection.supports_ranges? - INSERT INTO postgresql_ranges ( - date_range, - num_range, - ts_range, - tstz_range, - int4_range, - int8_range - ) VALUES ( - '[''2012-01-02'', ''2012-01-04'']', - '[0.1, 0.2]', - '[''2010-01-01 14:30'', ''2011-01-01 14:30'']', - '[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']', - '[1, 10]', - '[10, 100]' - ) -_SQL - - @connection.execute <<_SQL if @connection.supports_ranges? - INSERT INTO postgresql_ranges ( - date_range, - num_range, - ts_range, - tstz_range, - int4_range, - int8_range - ) VALUES ( - '(''2012-01-02'', ''2012-01-04'')', - '[0.1, 0.2)', - '[''2010-01-01 14:30'', ''2011-01-01 14:30'')', - '[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'')', - '(1, 10)', - '(10, 100)' - ) -_SQL - - @connection.execute <<_SQL if @connection.supports_ranges? - INSERT INTO postgresql_ranges ( - date_range, - num_range, - ts_range, - tstz_range, - int4_range, - int8_range - ) VALUES ( - '(''2012-01-02'',]', - '[0.1,]', - '[''2010-01-01 14:30'',]', - '[''2010-01-01 14:30:00+05'',]', - '(1,]', - '(10,]' - ) -_SQL - - @connection.execute <<_SQL if @connection.supports_ranges? - INSERT INTO postgresql_ranges ( - date_range, - num_range, - ts_range, - tstz_range, - int4_range, - int8_range - ) VALUES ( - '[,]', - '[,]', - '[,]', - '[,]', - '[,]', - '[,]' - ) -_SQL - - @connection.execute <<_SQL if @connection.supports_ranges? - INSERT INTO postgresql_ranges ( - date_range, - num_range, - ts_range, - tstz_range, - int4_range, - int8_range - ) VALUES ( - '(''2012-01-02'', ''2012-01-02'')', - '(0.1, 0.1)', - '(''2010-01-01 14:30'', ''2010-01-01 14:30'')', - '(''2010-01-01 14:30:00+05'', ''2010-01-01 06:30:00-03'')', - '(1, 1)', - '(10, 10)' - ) -_SQL - - if @connection.supports_ranges? - @first_range = PostgresqlRange.find(1) - @second_range = PostgresqlRange.find(2) - @third_range = PostgresqlRange.find(3) - @fourth_range = PostgresqlRange.find(4) - @empty_range = PostgresqlRange.find(5) - end - @connection.execute("INSERT INTO postgresql_tsvectors (id, text_vector) VALUES (1, ' ''text'' ''vector'' ')") @first_tsvector = PostgresqlTsvector.find(1) @@ -230,187 +129,6 @@ _SQL assert_equal "'text' 'vector'", @first_tsvector.text_vector end - if ActiveRecord::Base.connection.supports_ranges? - def test_data_type_of_range_types - assert_equal :daterange, @first_range.column_for_attribute(:date_range).type - assert_equal :numrange, @first_range.column_for_attribute(:num_range).type - assert_equal :tsrange, @first_range.column_for_attribute(:ts_range).type - assert_equal :tstzrange, @first_range.column_for_attribute(:tstz_range).type - assert_equal :int4range, @first_range.column_for_attribute(:int4_range).type - assert_equal :int8range, @first_range.column_for_attribute(:int8_range).type - end - - def test_int4range_values - assert_equal 1...11, @first_range.int4_range - assert_equal 2...10, @second_range.int4_range - assert_equal 2...Float::INFINITY, @third_range.int4_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int4_range) - assert_nil @empty_range.int4_range - end - - def test_int8range_values - assert_equal 10...101, @first_range.int8_range - assert_equal 11...100, @second_range.int8_range - assert_equal 11...Float::INFINITY, @third_range.int8_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int8_range) - assert_nil @empty_range.int8_range - end - - def test_daterange_values - assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 5), @first_range.date_range - assert_equal Date.new(2012, 1, 3)...Date.new(2012, 1, 4), @second_range.date_range - assert_equal Date.new(2012, 1, 3)...Float::INFINITY, @third_range.date_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.date_range) - assert_nil @empty_range.date_range - end - - def test_numrange_values - assert_equal BigDecimal.new('0.1')..BigDecimal.new('0.2'), @first_range.num_range - assert_equal BigDecimal.new('0.1')...BigDecimal.new('0.2'), @second_range.num_range - assert_equal BigDecimal.new('0.1')...BigDecimal.new('Infinity'), @third_range.num_range - assert_equal BigDecimal.new('-Infinity')...BigDecimal.new('Infinity'), @fourth_range.num_range - assert_nil @empty_range.num_range - end - - def test_tsrange_values - tz = ::ActiveRecord::Base.default_timezone - assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)..Time.send(tz, 2011, 1, 1, 14, 30, 0), @first_range.ts_range - assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 1, 1, 14, 30, 0), @second_range.ts_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.ts_range) - assert_nil @empty_range.ts_range - end - - def test_tstzrange_values - assert_equal Time.parse('2010-01-01 09:30:00 UTC')..Time.parse('2011-01-01 17:30:00 UTC'), @first_range.tstz_range - assert_equal Time.parse('2010-01-01 09:30:00 UTC')...Time.parse('2011-01-01 17:30:00 UTC'), @second_range.tstz_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.tstz_range) - assert_nil @empty_range.tstz_range - end - - def test_create_tstzrange - tstzrange = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2011-02-02 14:30:00 CDT') - range = PostgresqlRange.new(:tstz_range => tstzrange) - assert range.save - assert range.reload - assert_equal range.tstz_range, tstzrange - assert_equal range.tstz_range, Time.parse('2010-01-01 13:30:00 UTC')...Time.parse('2011-02-02 19:30:00 UTC') - end - - def test_update_tstzrange - new_tstzrange = Time.parse('2010-01-01 14:30:00 CDT')...Time.parse('2011-02-02 14:30:00 CET') - @first_range.tstz_range = new_tstzrange - assert @first_range.save - assert @first_range.reload - assert_equal new_tstzrange, @first_range.tstz_range - @first_range.tstz_range = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2010-01-01 13:30:00 +0000') - assert @first_range.save - assert @first_range.reload - assert_nil @first_range.tstz_range - end - - def test_create_tsrange - tz = ::ActiveRecord::Base.default_timezone - tsrange = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0) - range = PostgresqlRange.new(:ts_range => tsrange) - assert range.save - assert range.reload - assert_equal range.ts_range, tsrange - end - - def test_update_tsrange - tz = ::ActiveRecord::Base.default_timezone - new_tsrange = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0) - @first_range.ts_range = new_tsrange - assert @first_range.save - assert @first_range.reload - assert_equal new_tsrange, @first_range.ts_range - @first_range.ts_range = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0) - assert @first_range.save - assert @first_range.reload - assert_nil @first_range.ts_range - end - - def test_create_numrange - numrange = BigDecimal.new('0.5')...BigDecimal.new('1') - range = PostgresqlRange.new(:num_range => numrange) - assert range.save - assert range.reload - assert_equal range.num_range, numrange - end - - def test_update_numrange - new_numrange = BigDecimal.new('0.5')...BigDecimal.new('1') - @first_range.num_range = new_numrange - assert @first_range.save - assert @first_range.reload - assert_equal new_numrange, @first_range.num_range - @first_range.num_range = BigDecimal.new('0.5')...BigDecimal.new('0.5') - assert @first_range.save - assert @first_range.reload - assert_nil @first_range.num_range - end - - def test_create_daterange - daterange = Range.new(Date.new(2012, 1, 1), Date.new(2013, 1, 1), true) - range = PostgresqlRange.new(:date_range => daterange) - assert range.save - assert range.reload - assert_equal range.date_range, daterange - end - - def test_update_daterange - new_daterange = Date.new(2012, 2, 3)...Date.new(2012, 2, 10) - @first_range.date_range = new_daterange - assert @first_range.save - assert @first_range.reload - assert_equal new_daterange, @first_range.date_range - @first_range.date_range = Date.new(2012, 2, 3)...Date.new(2012, 2, 3) - assert @first_range.save - assert @first_range.reload - assert_nil @first_range.date_range - end - - def test_create_int4range - int4range = Range.new(3, 50, true) - range = PostgresqlRange.new(:int4_range => int4range) - assert range.save - assert range.reload - assert_equal range.int4_range, int4range - end - - def test_update_int4range - new_int4range = 6...10 - @first_range.int4_range = new_int4range - assert @first_range.save - assert @first_range.reload - assert_equal new_int4range, @first_range.int4_range - @first_range.int4_range = 3...3 - assert @first_range.save - assert @first_range.reload - assert_nil @first_range.int4_range - end - - def test_create_int8range - int8range = Range.new(30, 50, true) - range = PostgresqlRange.new(:int8_range => int8range) - assert range.save - assert range.reload - assert_equal range.int8_range, int8range - end - - def test_update_int8range - new_int8range = 60000...10000000 - @first_range.int8_range = new_int8range - assert @first_range.save - assert @first_range.reload - assert_equal new_int8range, @first_range.int8_range - @first_range.int8_range = 39999...39999 - assert @first_range.save - assert @first_range.reload - assert_nil @first_range.int8_range - end - end - def test_money_values assert_equal 567.89, @first_money.wealth assert_equal(-567.89, @second_money.wealth) diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index 2845413575..d8782f5eaa 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -70,6 +70,23 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase Hstore.reset_column_information end + def test_hstore_migration + hstore_migration = Class.new(ActiveRecord::Migration) do + def change + change_table("hstores") do |t| + t.hstore :keys + end + end + end + + hstore_migration.new.suppress_messages do + hstore_migration.migrate(:up) + assert_includes @connection.columns(:hstores).map(&:name), "keys" + hstore_migration.migrate(:down) + assert_not_includes @connection.columns(:hstores).map(&:name), "keys" + end + end + def test_cast_value_on_write x = Hstore.new tags: {"bool" => true, "number" => 5} assert_equal({"bool" => "true", "number" => "5"}, x.tags) @@ -206,6 +223,16 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase def test_multiline assert_cycle("a\nb" => "c\nd") end + + def test_update_all + hstore = Hstore.create! tags: { "one" => "two" } + + Hstore.update_all tags: { "three" => "four" } + assert_equal({ "three" => "four" }, hstore.reload.tags) + + Hstore.update_all tags: { } + assert_equal({ }, hstore.reload.tags) + end end private diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index c33c7ef968..3daef399d8 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -21,7 +21,7 @@ class PostgresqlJSONTest < ActiveRecord::TestCase end end rescue ActiveRecord::StatementInvalid - return skip "do not test on PG without json" + skip "do not test on PG without json" end @column = JsonDataType.columns.find { |c| c.name == 'payload' } end @@ -121,4 +121,14 @@ class PostgresqlJSONTest < ActiveRecord::TestCase x = JsonDataType.first assert_equal "640×1136", x.resolution end + + def test_update_all + json = JsonDataType.create! payload: { "one" => "two" } + + JsonDataType.update_all payload: { "three" => "four" } + assert_equal({ "three" => "four" }, json.reload.payload) + + JsonDataType.update_all payload: { } + assert_equal({ }, json.reload.payload) + 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 8b017760b1..131080913c 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -10,6 +10,14 @@ module ActiveRecord @connection.exec_query('create table ex(id serial primary key, number integer, data character varying(255))') end + def test_bad_connection + assert_raise ActiveRecord::NoDatabaseError do + configuration = ActiveRecord::Base.configurations['arunit'].merge(database: 'should_not_exist-cinco-dog-db') + connection = ActiveRecord::Base.postgresql_connection(configuration) + connection.exec_query('drop table if exists ex') + end + end + def test_valid_column column = @connection.columns('ex').find { |col| col.name == 'id' } assert @connection.valid_type?(column.type) @@ -62,6 +70,18 @@ module ActiveRecord assert_equal expect, id end + def test_multiline_insert_sql + id = @connection.insert_sql(<<-SQL) + insert into ex( + number) + values( + 5152 + ) + SQL + expect = @connection.query('select max(id) from ex').first.first + assert_equal expect, id + end + def test_insert_sql_with_returning_disabled connection = connection_without_insert_returning id = connection.insert_sql("insert into postgresql_partitioned_table_parent (number) VALUES (1)") diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb new file mode 100644 index 0000000000..5c2d8e1c1d --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -0,0 +1,245 @@ +require "cases/helper" +require 'active_record/base' +require 'active_record/connection_adapters/postgresql_adapter' + +if ActiveRecord::Base.connection.supports_ranges? + class PostgresqlRange < ActiveRecord::Base + self.table_name = "postgresql_ranges" + end + + class PostgresqlRangeTest < ActiveRecord::TestCase + def teardown + @connection.execute 'DROP TABLE IF EXISTS postgresql_ranges' + end + + def setup + @connection = ActiveRecord::Base.connection + begin + @connection.transaction do + @connection.create_table('postgresql_ranges') do |t| + t.daterange :date_range + t.numrange :num_range + t.tsrange :ts_range + t.tstzrange :tstz_range + t.int4range :int4_range + t.int8range :int8_range + end + end + rescue ActiveRecord::StatementInvalid + skip "do not test on PG without range" + end + + insert_range(id: 101, + date_range: "[''2012-01-02'', ''2012-01-04'']", + num_range: "[0.1, 0.2]", + ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'']", + tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']", + int4_range: "[1, 10]", + int8_range: "[10, 100]") + + insert_range(id: 102, + date_range: "(''2012-01-02'', ''2012-01-04'')", + num_range: "[0.1, 0.2)", + ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'')", + tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'')", + int4_range: "(1, 10)", + int8_range: "(10, 100)") + + insert_range(id: 103, + date_range: "(''2012-01-02'',]", + num_range: "[0.1,]", + ts_range: "[''2010-01-01 14:30'',]", + tstz_range: "[''2010-01-01 14:30:00+05'',]", + int4_range: "(1,]", + int8_range: "(10,]") + + insert_range(id: 104, + date_range: "[,]", + num_range: "[,]", + ts_range: "[,]", + tstz_range: "[,]", + int4_range: "[,]", + int8_range: "[,]") + + insert_range(id: 105, + date_range: "(''2012-01-02'', ''2012-01-02'')", + num_range: "(0.1, 0.1)", + ts_range: "(''2010-01-01 14:30'', ''2010-01-01 14:30'')", + tstz_range: "(''2010-01-01 14:30:00+05'', ''2010-01-01 06:30:00-03'')", + int4_range: "(1, 1)", + int8_range: "(10, 10)") + + @new_range = PostgresqlRange.new + @first_range = PostgresqlRange.find(101) + @second_range = PostgresqlRange.find(102) + @third_range = PostgresqlRange.find(103) + @fourth_range = PostgresqlRange.find(104) + @empty_range = PostgresqlRange.find(105) + end + + def test_data_type_of_range_types + assert_equal :daterange, @first_range.column_for_attribute(:date_range).type + assert_equal :numrange, @first_range.column_for_attribute(:num_range).type + assert_equal :tsrange, @first_range.column_for_attribute(:ts_range).type + assert_equal :tstzrange, @first_range.column_for_attribute(:tstz_range).type + assert_equal :int4range, @first_range.column_for_attribute(:int4_range).type + assert_equal :int8range, @first_range.column_for_attribute(:int8_range).type + end + + def test_int4range_values + assert_equal 1...11, @first_range.int4_range + assert_equal 2...10, @second_range.int4_range + assert_equal 2...Float::INFINITY, @third_range.int4_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int4_range) + assert_nil @empty_range.int4_range + end + + def test_int8range_values + assert_equal 10...101, @first_range.int8_range + assert_equal 11...100, @second_range.int8_range + assert_equal 11...Float::INFINITY, @third_range.int8_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int8_range) + assert_nil @empty_range.int8_range + end + + def test_daterange_values + assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 5), @first_range.date_range + assert_equal Date.new(2012, 1, 3)...Date.new(2012, 1, 4), @second_range.date_range + assert_equal Date.new(2012, 1, 3)...Float::INFINITY, @third_range.date_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.date_range) + assert_nil @empty_range.date_range + end + + def test_numrange_values + assert_equal BigDecimal.new('0.1')..BigDecimal.new('0.2'), @first_range.num_range + assert_equal BigDecimal.new('0.1')...BigDecimal.new('0.2'), @second_range.num_range + assert_equal BigDecimal.new('0.1')...BigDecimal.new('Infinity'), @third_range.num_range + assert_equal BigDecimal.new('-Infinity')...BigDecimal.new('Infinity'), @fourth_range.num_range + assert_nil @empty_range.num_range + end + + def test_tsrange_values + tz = ::ActiveRecord::Base.default_timezone + assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)..Time.send(tz, 2011, 1, 1, 14, 30, 0), @first_range.ts_range + assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 1, 1, 14, 30, 0), @second_range.ts_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.ts_range) + assert_nil @empty_range.ts_range + end + + def test_tstzrange_values + assert_equal Time.parse('2010-01-01 09:30:00 UTC')..Time.parse('2011-01-01 17:30:00 UTC'), @first_range.tstz_range + assert_equal Time.parse('2010-01-01 09:30:00 UTC')...Time.parse('2011-01-01 17:30:00 UTC'), @second_range.tstz_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.tstz_range) + assert_nil @empty_range.tstz_range + end + + def test_create_tstzrange + tstzrange = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2011-02-02 14:30:00 CDT') + round_trip(@new_range, :tstz_range, tstzrange) + assert_equal @new_range.tstz_range, tstzrange + assert_equal @new_range.tstz_range, Time.parse('2010-01-01 13:30:00 UTC')...Time.parse('2011-02-02 19:30:00 UTC') + end + + def test_update_tstzrange + assert_equal_round_trip(@first_range, :tstz_range, + Time.parse('2010-01-01 14:30:00 CDT')...Time.parse('2011-02-02 14:30:00 CET')) + assert_nil_round_trip(@first_range, :tstz_range, + Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2010-01-01 13:30:00 +0000')) + end + + def test_create_tsrange + tz = ::ActiveRecord::Base.default_timezone + assert_equal_round_trip(@new_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0)) + end + + def test_update_tsrange + tz = ::ActiveRecord::Base.default_timezone + assert_equal_round_trip(@first_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0)) + assert_nil_round_trip(@first_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0)) + end + + def test_create_numrange + assert_equal_round_trip(@new_range, :num_range, + BigDecimal.new('0.5')...BigDecimal.new('1')) + end + + def test_update_numrange + assert_equal_round_trip(@first_range, :num_range, + BigDecimal.new('0.5')...BigDecimal.new('1')) + assert_nil_round_trip(@first_range, :num_range, + BigDecimal.new('0.5')...BigDecimal.new('0.5')) + end + + def test_create_daterange + assert_equal_round_trip(@new_range, :date_range, + Range.new(Date.new(2012, 1, 1), Date.new(2013, 1, 1), true)) + end + + def test_update_daterange + assert_equal_round_trip(@first_range, :date_range, + Date.new(2012, 2, 3)...Date.new(2012, 2, 10)) + assert_nil_round_trip(@first_range, :date_range, + Date.new(2012, 2, 3)...Date.new(2012, 2, 3)) + end + + def test_create_int4range + assert_equal_round_trip(@new_range, :int4_range, Range.new(3, 50, true)) + end + + def test_update_int4range + assert_equal_round_trip(@first_range, :int4_range, 6...10) + assert_nil_round_trip(@first_range, :int4_range, 3...3) + end + + def test_create_int8range + assert_equal_round_trip(@new_range, :int8_range, Range.new(30, 50, true)) + end + + def test_update_int8range + assert_equal_round_trip(@first_range, :int8_range, 60000...10000000) + assert_nil_round_trip(@first_range, :int8_range, 39999...39999) + end + + private + def assert_equal_round_trip(range, attribute, value) + round_trip(range, attribute, value) + assert_equal value, range.public_send(attribute) + end + + def assert_nil_round_trip(range, attribute, value) + round_trip(range, attribute, value) + assert_nil range.public_send(attribute) + end + + def round_trip(range, attribute, value) + range.public_send "#{attribute}=", value + assert range.save + assert range.reload + end + + def insert_range(values) + @connection.execute <<-SQL + INSERT INTO postgresql_ranges ( + id, + date_range, + num_range, + ts_range, + tstz_range, + int4_range, + int8_range + ) VALUES ( + #{values[:id]}, + '#{values[:date_range]}', + '#{values[:num_range]}', + '#{values[:ts_range]}', + '#{values[:tstz_range]}', + '#{values[:int4_range]}', + '#{values[:int8_range]}' + ) + SQL + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index e8dd188ec8..1e59bca6a7 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -115,6 +115,12 @@ class SchemaTest < ActiveRecord::TestCase end end + def test_raise_wraped_exception_on_bad_prepare + assert_raises(ActiveRecord::StatementInvalid) do + @connection.exec_query "select * from developers where id = ?", 'sql', [[nil, 1]] + end + end + def test_schema_change_with_prepared_stmt altered = false @connection.exec_query "select * from developers where id = $1", 'sql', [[nil, 1]] diff --git a/activerecord/test/cases/adapters/postgresql/xml_test.rb b/activerecord/test/cases/adapters/postgresql/xml_test.rb index bf14b378d8..dd2a727afe 100644 --- a/activerecord/test/cases/adapters/postgresql/xml_test.rb +++ b/activerecord/test/cases/adapters/postgresql/xml_test.rb @@ -18,7 +18,7 @@ class PostgresqlXMLTest < ActiveRecord::TestCase end end rescue ActiveRecord::StatementInvalid - return skip "do not test on PG without xml" + skip "do not test on PG without xml" end @column = XmlDataType.columns.find { |c| c.name == 'payload' } end diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index a7b2764fc1..ba89487838 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -95,6 +95,13 @@ module ActiveRecord end }.new assert_equal 10, @conn.type_cast(quoted_id_obj, nil) + + quoted_id_obj = Class.new { + def quoted_id + "'zomg'" + end + } + assert_raise(TypeError) { @conn.type_cast(quoted_id_obj, nil) } end end end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index ce7c869eec..02834edf7b 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -1,6 +1,7 @@ # encoding: utf-8 require "cases/helper" require 'models/owner' +require 'tempfile' module ActiveRecord module ConnectionAdapters @@ -25,6 +26,40 @@ module ActiveRecord ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber) end + def test_bad_connection + assert_raise ActiveRecord::NoDatabaseError do + connection = ActiveRecord::Base.sqlite3_connection(adapter: "sqlite3", database: "/tmp/should/_not/_exist/-cinco-dog.db") + connection.exec_query('drop table if exists ex') + end + end + + def test_connect_with_url + skip "can't establish new connection when using memory db" if in_memory_db? + begin + original_connection = ActiveRecord::Base.remove_connection + tf = Tempfile.open 'whatever' + url = "sqlite3://#{tf.path}" + ActiveRecord::Base.establish_connection(url) + assert ActiveRecord::Base.connection + ensure + tf.close + tf.unlink + ActiveRecord::Base.establish_connection(original_connection) + end + end + + def test_connect_memory_with_url + skip "can't establish new connection when using memory db" if in_memory_db? + begin + original_connection = ActiveRecord::Base.remove_connection + url = "sqlite3:///:memory:" + ActiveRecord::Base.establish_connection(url) + assert ActiveRecord::Base.connection + ensure + ActiveRecord::Base.establish_connection(original_connection) + end + end + def test_valid_column column = @conn.columns('items').find { |col| col.name == 'id' } assert @conn.valid_type?(column.type) @@ -154,7 +189,7 @@ module ActiveRecord DualEncoding.connection.execute(<<-eosql) CREATE TABLE dual_encodings ( id integer PRIMARY KEY AUTOINCREMENT, - name string, + name varchar(255), data binary ) eosql diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 7c913bc78b..2283ba66db 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -28,6 +28,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal companies(:first_firm).name, firm.name end + def test_belongs_to_does_not_use_order_by + ActiveRecord::SQLCounter.clear_log + Client.find(3).firm + ensure + assert ActiveRecord::SQLCounter.log_all.all? { |sql| /order by/i !~ sql }, 'ORDER BY was used in the query' + end + def test_belongs_to_with_primary_key client = Client.create(:name => "Primary key client", :firm_name => companies(:first_firm).name) assert_equal companies(:first_firm).name, client.firm_with_primary_key.name @@ -356,6 +363,14 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_queries(2) { line_item.destroy } end + def test_belongs_to_with_touch_option_on_destroy_with_destroyed_parent + line_item = LineItem.create! + invoice = Invoice.create!(line_items: [line_item]) + invoice.destroy + + assert_queries(1) { line_item.destroy } + end + def test_belongs_to_with_touch_option_on_touch_and_reassigned_parent line_item = LineItem.create! Invoice.create!(line_items: [line_item]) @@ -578,6 +593,19 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_nil essay.writer_id end + def test_polymorphic_assignment_with_nil + essay = Essay.new + assert_nil essay.writer_id + assert_nil essay.writer_type + + essay.writer_id = 1 + essay.writer_type = 'Author' + + essay.writer = nil + assert_nil essay.writer_id + assert_nil essay.writer_type + end + def test_belongs_to_proxy_should_not_respond_to_private_methods assert_raise(NoMethodError) { companies(:first_firm).private_method } assert_raise(NoMethodError) { companies(:second_client).firm.private_method } diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index 811d91f849..71c0609df5 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -174,4 +174,15 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase sink = Vertex.all.merge!(:includes=>{:sources=>{:sources=>{:sources=>:sources}}}, :order => 'vertices.id DESC').first assert_equal vertices(:vertex_1), assert_no_queries { sink.sources.first.sources.first.sources.first.sources.first } end + + def test_eager_association_loading_with_cascaded_interdependent_one_level_and_two_levels + authors_relation = Author.all.merge!(includes: [:comments, { posts: :categorizations }], order: "authors.id") + authors = authors_relation.to_a + assert_equal 3, authors.size + assert_equal 10, authors[0].comments.size + assert_equal 1, authors[1].comments.size + assert_equal 5, authors[0].posts.size + assert_equal 3, authors[1].posts.size + assert_equal 3, authors[0].posts.collect { |post| post.categorizations.size }.inject(0) { |sum, i| sum+i } + end end diff --git a/activerecord/test/cases/associations/eager_singularization_test.rb b/activerecord/test/cases/associations/eager_singularization_test.rb index 634f6b63ba..669569a774 100644 --- a/activerecord/test/cases/associations/eager_singularization_test.rb +++ b/activerecord/test/cases/associations/eager_singularization_test.rb @@ -1,128 +1,133 @@ require "cases/helper" -class Virus < ActiveRecord::Base - belongs_to :octopus -end -class Octopus < ActiveRecord::Base - has_one :virus -end -class Pass < ActiveRecord::Base - belongs_to :bus -end -class Bus < ActiveRecord::Base - has_many :passes -end -class Mess < ActiveRecord::Base - has_and_belongs_to_many :crises -end -class Crisis < ActiveRecord::Base - has_and_belongs_to_many :messes - has_many :analyses, :dependent => :destroy - has_many :successes, :through => :analyses - has_many :dresses, :dependent => :destroy - has_many :compresses, :through => :dresses -end -class Analysis < ActiveRecord::Base - belongs_to :crisis - belongs_to :success -end -class Success < ActiveRecord::Base - has_many :analyses, :dependent => :destroy - has_many :crises, :through => :analyses -end -class Dress < ActiveRecord::Base - belongs_to :crisis - has_many :compresses -end -class Compress < ActiveRecord::Base - belongs_to :dress -end - class EagerSingularizationTest < ActiveRecord::TestCase + class Virus < ActiveRecord::Base + belongs_to :octopus + end + + class Octopus < ActiveRecord::Base + has_one :virus + end + + class Pass < ActiveRecord::Base + belongs_to :bus + end + + class Bus < ActiveRecord::Base + has_many :passes + end + + class Mess < ActiveRecord::Base + has_and_belongs_to_many :crises + end + + class Crisis < ActiveRecord::Base + has_and_belongs_to_many :messes + has_many :analyses, :dependent => :destroy + has_many :successes, :through => :analyses + has_many :dresses, :dependent => :destroy + has_many :compresses, :through => :dresses + end + + class Analysis < ActiveRecord::Base + belongs_to :crisis + belongs_to :success + end + + class Success < ActiveRecord::Base + has_many :analyses, :dependent => :destroy + has_many :crises, :through => :analyses + end + + class Dress < ActiveRecord::Base + belongs_to :crisis + has_many :compresses + end + + class Compress < ActiveRecord::Base + belongs_to :dress + end def setup - if ActiveRecord::Base.connection.supports_migrations? - ActiveRecord::Base.connection.create_table :viri do |t| - t.column :octopus_id, :integer - t.column :species, :string - end - ActiveRecord::Base.connection.create_table :octopi do |t| - t.column :species, :string - end - ActiveRecord::Base.connection.create_table :passes do |t| - t.column :bus_id, :integer - t.column :rides, :integer - end - ActiveRecord::Base.connection.create_table :buses do |t| - t.column :name, :string - end - ActiveRecord::Base.connection.create_table :crises_messes, :id => false do |t| - t.column :crisis_id, :integer - t.column :mess_id, :integer - end - ActiveRecord::Base.connection.create_table :messes do |t| - t.column :name, :string - end - ActiveRecord::Base.connection.create_table :crises do |t| - t.column :name, :string - end - ActiveRecord::Base.connection.create_table :successes do |t| - t.column :name, :string - end - ActiveRecord::Base.connection.create_table :analyses do |t| - t.column :crisis_id, :integer - t.column :success_id, :integer - end - ActiveRecord::Base.connection.create_table :dresses do |t| - t.column :crisis_id, :integer - end - ActiveRecord::Base.connection.create_table :compresses do |t| - t.column :dress_id, :integer - end - @have_tables = true - else - @have_tables = false + skip 'Does not support migrations' unless connection.supports_migrations? + + connection.create_table :viri do |t| + t.column :octopus_id, :integer + t.column :species, :string + end + connection.create_table :octopi do |t| + t.column :species, :string + end + connection.create_table :passes do |t| + t.column :bus_id, :integer + t.column :rides, :integer + end + connection.create_table :buses do |t| + t.column :name, :string + end + connection.create_table :crises_messes, :id => false do |t| + t.column :crisis_id, :integer + t.column :mess_id, :integer + end + connection.create_table :messes do |t| + t.column :name, :string + end + connection.create_table :crises do |t| + t.column :name, :string + end + connection.create_table :successes do |t| + t.column :name, :string + end + connection.create_table :analyses do |t| + t.column :crisis_id, :integer + t.column :success_id, :integer + end + connection.create_table :dresses do |t| + t.column :crisis_id, :integer + end + connection.create_table :compresses do |t| + t.column :dress_id, :integer end end def teardown - ActiveRecord::Base.connection.drop_table :viri - ActiveRecord::Base.connection.drop_table :octopi - ActiveRecord::Base.connection.drop_table :passes - ActiveRecord::Base.connection.drop_table :buses - ActiveRecord::Base.connection.drop_table :crises_messes - ActiveRecord::Base.connection.drop_table :messes - ActiveRecord::Base.connection.drop_table :crises - ActiveRecord::Base.connection.drop_table :successes - ActiveRecord::Base.connection.drop_table :analyses - ActiveRecord::Base.connection.drop_table :dresses - ActiveRecord::Base.connection.drop_table :compresses + connection.drop_table :viri + connection.drop_table :octopi + connection.drop_table :passes + connection.drop_table :buses + connection.drop_table :crises_messes + connection.drop_table :messes + connection.drop_table :crises + connection.drop_table :successes + connection.drop_table :analyses + connection.drop_table :dresses + connection.drop_table :compresses + end + + def connection + ActiveRecord::Base.connection end def test_eager_no_extra_singularization_belongs_to - return unless @have_tables assert_nothing_raised do Virus.all.merge!(:includes => :octopus).to_a end end def test_eager_no_extra_singularization_has_one - return unless @have_tables assert_nothing_raised do Octopus.all.merge!(:includes => :virus).to_a end end def test_eager_no_extra_singularization_has_many - return unless @have_tables assert_nothing_raised do Bus.all.merge!(:includes => :passes).to_a end end def test_eager_no_extra_singularization_has_and_belongs_to_many - return unless @have_tables assert_nothing_raised do Crisis.all.merge!(:includes => :messes).to_a Mess.all.merge!(:includes => :crises).to_a @@ -130,14 +135,12 @@ class EagerSingularizationTest < ActiveRecord::TestCase end def test_eager_no_extra_singularization_has_many_through_belongs_to - return unless @have_tables assert_nothing_raised do Crisis.all.merge!(:includes => :successes).to_a end end def test_eager_no_extra_singularization_has_many_through_has_many - return unless @have_tables assert_nothing_raised do Crisis.all.merge!(:includes => :compresses).to_a end diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb index f8f2832ab1..4c1fdfdd9a 100644 --- a/activerecord/test/cases/associations/extension_test.rb +++ b/activerecord/test/cases/associations/extension_test.rb @@ -75,6 +75,7 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase private def extend!(model) - ActiveRecord::Associations::Builder::HasMany.define_extensions(model, :association_name) { } + builder = ActiveRecord::Associations::Builder::HasMany.new(model, :association_name, nil, {}) { } + builder.define_extensions(model) 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 be928ec8ee..8aee7ff40e 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 @@ -570,6 +570,13 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert !developer.special_projects.include?(other_project) end + def test_symbol_join_table + developer = Developer.first + sp = developer.sym_special_projects.create("name" => "omg") + developer.reload + assert_includes developer.sym_special_projects, sp + end + def test_update_attributes_after_push_without_duplicate_join_table_rows developer = Developer.new("name" => "Kano") project = SpecialProject.create("name" => "Special Project") diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index bfb80afa61..cf1e50890e 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -216,6 +216,31 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end assert_no_queries do + bulbs.second() + bulbs.second({}) + end + + assert_no_queries do + bulbs.third() + bulbs.third({}) + end + + assert_no_queries do + bulbs.fourth() + bulbs.fourth({}) + end + + assert_no_queries do + bulbs.fifth() + bulbs.fifth({}) + end + + assert_no_queries do + bulbs.forty_two() + bulbs.forty_two({}) + end + + assert_no_queries do bulbs.last() bulbs.last({}) end @@ -242,11 +267,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first def test_counting_with_counter_sql - assert_equal 2, Firm.all.merge!(:order => "id").first.clients.count + assert_equal 3, Firm.all.merge!(:order => "id").first.clients.count end def test_counting - assert_equal 2, Firm.all.merge!(:order => "id").first.plain_clients.count + assert_equal 3, Firm.all.merge!(:order => "id").first.plain_clients.count end def test_counting_with_single_hash @@ -254,7 +279,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_counting_with_column_name_and_hash - assert_equal 2, Firm.all.merge!(:order => "id").first.plain_clients.count(:name) + assert_equal 3, Firm.all.merge!(:order => "id").first.plain_clients.count(:name) end def test_counting_with_association_limit @@ -264,17 +289,17 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_finding - assert_equal 2, Firm.all.merge!(:order => "id").first.clients.length + assert_equal 3, Firm.all.merge!(:order => "id").first.clients.length end def test_finding_array_compatibility - assert_equal 2, Firm.order(:id).find{|f| f.id > 0}.clients.length + assert_equal 3, Firm.order(:id).find{|f| f.id > 0}.clients.length end def test_find_many_with_merged_options assert_equal 1, companies(:first_firm).limited_clients.size assert_equal 1, companies(:first_firm).limited_clients.to_a.size - assert_equal 2, companies(:first_firm).limited_clients.limit(nil).to_a.size + assert_equal 3, companies(:first_firm).limited_clients.limit(nil).to_a.size end def test_find_should_append_to_association_order @@ -283,8 +308,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_dynamic_find_should_respect_association_order - assert_equal companies(:second_client), companies(:first_firm).clients_sorted_desc.where("type = 'Client'").first - assert_equal companies(:second_client), companies(:first_firm).clients_sorted_desc.find_by_type('Client') + assert_equal companies(:another_first_firm_client), companies(:first_firm).clients_sorted_desc.where("type = 'Client'").first + assert_equal companies(:another_first_firm_client), companies(:first_firm).clients_sorted_desc.find_by_type('Client') end def test_cant_save_has_many_readonly_association @@ -297,7 +322,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_finding_with_different_class_name_and_order - assert_equal "Microsoft", Firm.all.merge!(:order => "id").first.clients_sorted_desc.first.name + assert_equal "Apex", Firm.all.merge!(:order => "id").first.clients_sorted_desc.first.name end def test_finding_with_foreign_key @@ -318,9 +343,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_belongs_to_sanity c = Client.new - assert_nil c.firm - - flunk "belongs_to failed if check" if c.firm + assert_nil c.firm, "belongs_to failed sanity check on new object" end def test_find_ids @@ -357,7 +380,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_find_all firm = Firm.all.merge!(:order => "id").first - assert_equal 2, firm.clients.where("#{QUOTED_TYPE} = 'Client'").to_a.length + assert_equal 3, firm.clients.where("#{QUOTED_TYPE} = 'Client'").to_a.length assert_equal 1, firm.clients.where("name = 'Summit'").to_a.length end @@ -366,7 +389,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert ! firm.clients.loaded? - assert_queries(3) do + assert_queries(4) do firm.clients.find_each(:batch_size => 1) {|c| assert_equal firm.id, c.firm_id } end @@ -436,15 +459,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_find_grouped all_clients_of_firm1 = Client.all.merge!(:where => "firm_id = 1").to_a grouped_clients_of_firm1 = Client.all.merge!(:where => "firm_id = 1", :group => "firm_id", :select => 'firm_id, count(id) as clients_count').to_a - assert_equal 2, all_clients_of_firm1.size + assert_equal 3, all_clients_of_firm1.size assert_equal 1, grouped_clients_of_firm1.size end def test_find_scoped_grouped assert_equal 1, companies(:first_firm).clients_grouped_by_firm_id.size assert_equal 1, companies(:first_firm).clients_grouped_by_firm_id.length - assert_equal 2, companies(:first_firm).clients_grouped_by_name.size - assert_equal 2, companies(:first_firm).clients_grouped_by_name.length + assert_equal 3, companies(:first_firm).clients_grouped_by_name.size + assert_equal 3, companies(:first_firm).clients_grouped_by_name.length end def test_find_scoped_grouped_having @@ -457,28 +480,32 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_select_query_method - assert_equal ['id'], posts(:welcome).comments.select(:id).first.attributes.keys + assert_equal ['id', 'body'], posts(:welcome).comments.select(:id, :body).first.attributes.keys end - def test_select_without_foreign_key + def test_select_with_block + assert_equal [1], posts(:welcome).comments.select { |c| c.id == 1 }.map(&:id) + end + + def test_select_without_foreign_key assert_equal companies(:first_firm).accounts.first.credit_limit, companies(:first_firm).accounts.select(:credit_limit).first.credit_limit - end + end def test_adding force_signal37_to_load_all_clients_of_firm natural = Client.new("name" => "Natural Company") companies(:first_firm).clients_of_firm << natural - assert_equal 2, companies(:first_firm).clients_of_firm.size # checking via the collection - assert_equal 2, companies(:first_firm).clients_of_firm(true).size # checking using the db + assert_equal 3, companies(:first_firm).clients_of_firm.size # checking via the collection + assert_equal 3, companies(:first_firm).clients_of_firm(true).size # checking using the db assert_equal natural, companies(:first_firm).clients_of_firm.last end def test_adding_using_create first_firm = companies(:first_firm) - assert_equal 2, first_firm.plain_clients.size - first_firm.plain_clients.create(:name => "Natural Company") - assert_equal 3, first_firm.plain_clients.length assert_equal 3, first_firm.plain_clients.size + first_firm.plain_clients.create(:name => "Natural Company") + assert_equal 4, first_firm.plain_clients.length + assert_equal 4, first_firm.plain_clients.size end def test_create_with_bang_on_has_many_when_parent_is_new_raises @@ -517,8 +544,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_adding_a_collection force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.concat([Client.new("name" => "Natural Company"), Client.new("name" => "Apple")]) - assert_equal 3, companies(:first_firm).clients_of_firm.size - assert_equal 3, companies(:first_firm).clients_of_firm(true).size + assert_equal 4, companies(:first_firm).clients_of_firm.size + assert_equal 4, companies(:first_firm).clients_of_firm(true).size end def test_transactions_when_adding_to_persisted @@ -571,7 +598,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase company = companies(:first_firm) # company already has one client company.clients_of_firm.build("name" => "Another Client") company.clients_of_firm.build("name" => "Yet Another Client") - assert_equal 3, company.clients_of_firm.size + assert_equal 4, company.clients_of_firm.size end def test_collection_not_empty_after_building @@ -647,14 +674,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase Firm.column_names Client.column_names - assert_equal 1, first_firm.clients_of_firm.size + assert_equal 2, first_firm.clients_of_firm.size first_firm.clients_of_firm.reset assert_queries(1) do first_firm.clients_of_firm.create(:name => "Superstars") end - assert_equal 2, first_firm.clients_of_firm.size + assert_equal 3, first_firm.clients_of_firm.size end def test_create @@ -667,7 +694,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_create_many companies(:first_firm).clients_of_firm.create([{"name" => "Another Client"}, {"name" => "Another Client II"}]) - assert_equal 3, companies(:first_firm).clients_of_firm(true).size + assert_equal 4, companies(:first_firm).clients_of_firm(true).size end def test_create_followed_by_save_does_not_load_target @@ -679,8 +706,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_deleting force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.delete(companies(:first_firm).clients_of_firm.first) - assert_equal 0, companies(:first_firm).clients_of_firm.size - assert_equal 0, companies(:first_firm).clients_of_firm(true).size + assert_equal 1, companies(:first_firm).clients_of_firm.size + assert_equal 1, companies(:first_firm).clients_of_firm(true).size end def test_deleting_before_save @@ -777,8 +804,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_deleting_a_collection force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.create("name" => "Another Client") - assert_equal 2, companies(:first_firm).clients_of_firm.size - companies(:first_firm).clients_of_firm.delete([companies(:first_firm).clients_of_firm[0], companies(:first_firm).clients_of_firm[1]]) + assert_equal 3, companies(:first_firm).clients_of_firm.size + companies(:first_firm).clients_of_firm.delete([companies(:first_firm).clients_of_firm[0], companies(:first_firm).clients_of_firm[1], companies(:first_firm).clients_of_firm[2]]) assert_equal 0, companies(:first_firm).clients_of_firm.size assert_equal 0, companies(:first_firm).clients_of_firm(true).size end @@ -787,7 +814,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase force_signal37_to_load_all_clients_of_firm companies(:first_firm).dependent_clients_of_firm.create("name" => "Another Client") clients = companies(:first_firm).dependent_clients_of_firm.to_a - assert_equal 2, clients.count + assert_equal 3, clients.count assert_difference "Client.count", -(clients.count) do companies(:first_firm).dependent_clients_of_firm.delete_all @@ -797,7 +824,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_delete_all_with_not_yet_loaded_association_collection force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.create("name" => "Another Client") - assert_equal 2, companies(:first_firm).clients_of_firm.size + assert_equal 3, companies(:first_firm).clients_of_firm.size companies(:first_firm).clients_of_firm.reset companies(:first_firm).clients_of_firm.delete_all assert_equal 0, companies(:first_firm).clients_of_firm.size @@ -830,7 +857,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_clearing_an_association_collection firm = companies(:first_firm) client_id = firm.clients_of_firm.first.id - assert_equal 1, firm.clients_of_firm.size + assert_equal 2, firm.clients_of_firm.size firm.clients_of_firm.clear @@ -864,7 +891,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_clearing_a_dependent_association_collection firm = companies(:first_firm) client_id = firm.dependent_clients_of_firm.first.id - assert_equal 1, firm.dependent_clients_of_firm.size + assert_equal 2, firm.dependent_clients_of_firm.size assert_equal 1, Client.find_by_id(client_id).client_of # :delete_all is called on each client since the dependent options is :destroy @@ -895,7 +922,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_clearing_an_exclusively_dependent_association_collection firm = companies(:first_firm) client_id = firm.exclusively_dependent_clients_of_firm.first.id - assert_equal 1, firm.exclusively_dependent_clients_of_firm.size + assert_equal 2, firm.exclusively_dependent_clients_of_firm.size assert_equal [], Client.destroyed_client_ids[firm.id] @@ -951,10 +978,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_delete_all_association_with_primary_key_deletes_correct_records firm = Firm.first # break the vanilla firm_id foreign key - assert_equal 2, firm.clients.count + assert_equal 3, firm.clients.count firm.clients.first.update_columns(firm_id: nil) - assert_equal 1, firm.clients(true).count - assert_equal 1, firm.clients_using_primary_key_with_delete_all.count + assert_equal 2, firm.clients(true).count + assert_equal 2, firm.clients_using_primary_key_with_delete_all.count old_record = firm.clients_using_primary_key_with_delete_all.first firm = Firm.first firm.destroy @@ -986,8 +1013,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase force_signal37_to_load_all_clients_of_firm summit = Client.find_by_name('Summit') companies(:first_firm).clients_of_firm.delete(summit) - assert_equal 1, companies(:first_firm).clients_of_firm.size - assert_equal 1, companies(:first_firm).clients_of_firm(true).size + assert_equal 2, companies(:first_firm).clients_of_firm.size + assert_equal 2, companies(:first_firm).clients_of_firm(true).size assert_equal 2, summit.client_of end @@ -1024,8 +1051,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase companies(:first_firm).clients_of_firm.destroy(companies(:first_firm).clients_of_firm.first) end - assert_equal 0, companies(:first_firm).reload.clients_of_firm.size - assert_equal 0, companies(:first_firm).clients_of_firm(true).size + assert_equal 1, companies(:first_firm).reload.clients_of_firm.size + assert_equal 1, companies(:first_firm).clients_of_firm(true).size end def test_destroying_by_fixnum_id @@ -1035,8 +1062,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase companies(:first_firm).clients_of_firm.destroy(companies(:first_firm).clients_of_firm.first.id) end - assert_equal 0, companies(:first_firm).reload.clients_of_firm.size - assert_equal 0, companies(:first_firm).clients_of_firm(true).size + assert_equal 1, companies(:first_firm).reload.clients_of_firm.size + assert_equal 1, companies(:first_firm).clients_of_firm(true).size end def test_destroying_by_string_id @@ -1046,21 +1073,21 @@ class HasManyAssociationsTest < ActiveRecord::TestCase companies(:first_firm).clients_of_firm.destroy(companies(:first_firm).clients_of_firm.first.id.to_s) end - assert_equal 0, companies(:first_firm).reload.clients_of_firm.size - assert_equal 0, companies(:first_firm).clients_of_firm(true).size + assert_equal 1, companies(:first_firm).reload.clients_of_firm.size + assert_equal 1, companies(:first_firm).clients_of_firm(true).size end def test_destroying_a_collection force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.create("name" => "Another Client") - assert_equal 2, companies(:first_firm).clients_of_firm.size + assert_equal 3, companies(:first_firm).clients_of_firm.size assert_difference "Client.count", -2 do companies(:first_firm).clients_of_firm.destroy([companies(:first_firm).clients_of_firm[0], companies(:first_firm).clients_of_firm[1]]) end - assert_equal 0, companies(:first_firm).reload.clients_of_firm.size - assert_equal 0, companies(:first_firm).clients_of_firm(true).size + assert_equal 1, companies(:first_firm).reload.clients_of_firm.size + assert_equal 1, companies(:first_firm).clients_of_firm(true).size end def test_destroy_all @@ -1076,7 +1103,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_dependence firm = companies(:first_firm) - assert_equal 2, firm.clients.size + assert_equal 3, firm.clients.size firm.destroy assert Client.all.merge!(:where => "firm_id=#{firm.id}").to_a.empty? end @@ -1089,14 +1116,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_destroy_dependent_when_deleted_from_association # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first firm = Firm.all.merge!(:order => "id").first - assert_equal 2, firm.clients.size + assert_equal 3, firm.clients.size client = firm.clients.first firm.clients.delete(client) assert_raise(ActiveRecord::RecordNotFound) { Client.find(client.id) } assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find(client.id) } - assert_equal 1, firm.clients.size + assert_equal 2, firm.clients.size end def test_three_levels_of_dependence @@ -1111,12 +1138,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_dependence_with_transaction_support_on_failure firm = companies(:first_firm) clients = firm.clients - assert_equal 2, clients.length + assert_equal 3, clients.length clients.last.instance_eval { def overwrite_to_raise() raise "Trigger rollback" end } firm.destroy rescue "do nothing" - assert_equal 2, Client.all.merge!(:where => "firm_id=#{firm.id}").to_a.size + assert_equal 3, Client.all.merge!(:where => "firm_id=#{firm.id}").to_a.size end def test_dependence_on_account @@ -1237,7 +1264,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_get_ids - assert_equal [companies(:first_client).id, companies(:second_client).id], companies(:first_firm).client_ids + assert_equal [companies(:first_client).id, companies(:second_client).id, companies(:another_first_firm_client).id], companies(:first_firm).client_ids end def test_get_ids_for_loaded_associations @@ -1252,7 +1279,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_get_ids_for_unloaded_associations_does_not_load_them company = companies(:first_firm) assert !company.clients.loaded? - assert_equal [companies(:first_client).id, companies(:second_client).id], company.client_ids + assert_equal [companies(:first_client).id, companies(:second_client).id, companies(:another_first_firm_client).id], company.client_ids assert !company.clients.loaded? end @@ -1261,7 +1288,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_get_ids_for_ordered_association - assert_equal [companies(:second_client).id, companies(:first_client).id], companies(:first_firm).clients_ordered_by_name_ids + assert_equal [companies(:another_first_firm_client).id, companies(:second_client).id, companies(:first_client).id], companies(:first_firm).clients_ordered_by_name_ids end def test_get_ids_for_association_on_new_record_does_not_try_to_find_records @@ -1355,9 +1382,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal false, firm.clients.include?(client) end - def test_calling_first_or_last_on_association_should_not_load_association + def test_calling_first_nth_or_last_on_association_should_not_load_association firm = companies(:first_firm) firm.clients.first + firm.clients.second firm.clients.last assert !firm.clients.loaded? end @@ -1382,30 +1410,33 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_queries 1 do firm.clients.first + firm.clients.second firm.clients.last end assert firm.clients.loaded? end - def test_calling_first_or_last_on_existing_record_with_create_should_not_load_association + def test_calling_first_nth_or_last_on_existing_record_with_create_should_not_load_association firm = companies(:first_firm) firm.clients.create(:name => 'Foo') assert !firm.clients.loaded? - assert_queries 2 do + assert_queries 3 do firm.clients.first + firm.clients.second firm.clients.last end assert !firm.clients.loaded? end - def test_calling_first_or_last_on_new_record_should_not_run_queries + def test_calling_first_nth_or_last_on_new_record_should_not_run_queries firm = Firm.new assert_no_queries do firm.clients.first + firm.clients.second firm.clients.last end end @@ -1492,7 +1523,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_calling_many_should_return_true_if_more_than_one firm = companies(:first_firm) assert firm.clients.many? - assert_equal 2, firm.clients.size + assert_equal 3, firm.clients.size end def test_joins_with_namespaced_model_should_use_correct_type @@ -1781,12 +1812,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [original_child], car.reload.failed_bulbs end - + test 'updates counter cache when default scope is given' do topic = DefaultRejectedTopic.create approved: true assert_difference "topic.reload.replies_count", 1 do topic.approved_replies.create! end - end + end end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 5a41461edf..d4edef03d6 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -22,6 +22,13 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal Account.find(1).credit_limit, companies(:first_firm).account.credit_limit end + def test_has_one_does_not_use_order_by + ActiveRecord::SQLCounter.clear_log + companies(:first_firm).account + ensure + assert ActiveRecord::SQLCounter.log_all.all? { |sql| /order by/i !~ sql }, 'ORDER BY was used in the query' + end + def test_has_one_cache_nils firm = companies(:another_firm) assert_queries(1) { assert_nil firm.account } diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index dffee42e7d..a9efa6d86a 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -42,7 +42,7 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase end def test_join_association_conditions_support_string_and_arel_expressions - assert_equal 0, Author.joins(:welcome_posts_with_comment).count + assert_equal 0, Author.joins(:welcome_posts_with_one_comment).count assert_equal 1, Author.joins(:welcome_posts_with_comments).count end diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 48e6fc5cd4..f663b5490c 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -255,6 +255,15 @@ class AssociationProxyTest < ActiveRecord::TestCase assert_equal man, man.interests.where("1=1").first.man end end + + def test_reset_unloads_target + david = authors(:david) + david.posts.reload + + assert david.posts.loaded? + david.posts.reset + assert !david.posts.loaded? + end end class OverridingAssociationsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 517d2674a7..703f805188 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -401,7 +401,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa assert_equal new_client, companies(:first_firm).clients_of_firm.last assert !companies(:first_firm).save assert !new_client.persisted? - assert_equal 1, companies(:first_firm).clients_of_firm(true).size + assert_equal 2, companies(:first_firm).clients_of_firm(true).size end def test_adding_before_save @@ -455,7 +455,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa company.name += '-changed' assert_queries(2) { assert company.save } assert new_client.persisted? - assert_equal 2, company.clients_of_firm(true).size + assert_equal 3, company.clients_of_firm(true).size end def test_build_many_before_save @@ -464,7 +464,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa company.name += '-changed' assert_queries(3) { assert company.save } - assert_equal 3, company.clients_of_firm(true).size + assert_equal 4, company.clients_of_firm(true).size end def test_build_via_block_before_save @@ -475,7 +475,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa company.name += '-changed' assert_queries(2) { assert company.save } assert new_client.persisted? - assert_equal 2, company.clients_of_firm(true).size + assert_equal 3, company.clients_of_firm(true).size end def test_build_many_via_block_before_save @@ -488,7 +488,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa company.name += '-changed' assert_queries(3) { assert company.save } - assert_equal 3, company.clients_of_firm(true).size + assert_equal 4, company.clients_of_firm(true).size end def test_replace_on_new_object @@ -574,6 +574,13 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase @ship = @pirate.create_ship(:name => 'Nights Dirty Lightning') end + def teardown + # We are running without transactional fixtures and need to cleanup. + Bird.delete_all + @ship.delete + @pirate.delete + end + # reload def test_a_marked_for_destruction_record_should_not_be_be_marked_after_reload @pirate.mark_for_destruction diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index cde188f6c3..8a0b0b9589 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -121,6 +121,10 @@ class BasicsTest < ActiveRecord::TestCase assert_equal 1, Topic.limit(1).to_a.length end + def test_limit_should_take_value_from_latest_limit + assert_equal 1, Topic.limit(2).limit(1).to_a.length + end + def test_invalid_limit assert_raises(ArgumentError) do Topic.limit("asdfadf").to_a @@ -317,7 +321,7 @@ class BasicsTest < ActiveRecord::TestCase def test_load topics = Topic.all.merge!(:order => 'id').to_a - assert_equal(4, topics.size) + assert_equal(5, topics.size) assert_equal(topics(:first).title, topics.first.title) end @@ -622,6 +626,7 @@ class BasicsTest < ActiveRecord::TestCase assert_equal ["EUC-JP"], Weird.columns.map {|c| c.name.encoding.name }.uniq ensure silence_warnings { Encoding.default_internal = old_default_internal } + Weird.reset_column_information end end @@ -1125,7 +1130,7 @@ class BasicsTest < ActiveRecord::TestCase k = Class.new(ak) k.table_name = "projects" orig_name = k.sequence_name - return skip "sequences not supported by db" unless orig_name + skip "sequences not supported by db" unless orig_name assert_equal k.reset_sequence_name, orig_name end @@ -1297,9 +1302,11 @@ class BasicsTest < ActiveRecord::TestCase end def test_compute_type_nonexistent_constant - assert_raises NameError do + e = assert_raises NameError do ActiveRecord::Base.send :compute_type, 'NonexistentModel' end + assert_equal 'uninitialized constant ActiveRecord::Base::NonexistentModel', e.message + assert_equal 'ActiveRecord::Base::NonexistentModel', e.name end def test_compute_type_no_method_error @@ -1373,6 +1380,8 @@ class BasicsTest < ActiveRecord::TestCase }) rd, wr = IO.pipe + rd.binmode + wr.binmode ActiveRecord::Base.connection_handler.clear_all_connections! diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index 3b94d46710..1161b57514 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -46,7 +46,9 @@ class EachTest < ActiveRecord::TestCase def test_each_should_raise_if_select_is_set_without_id assert_raise(RuntimeError) do - Post.select(:title).find_each(:batch_size => 1) { |post| post } + Post.select(:title).find_each(batch_size: 1) { |post| + flunk "should not call this block" + } end end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 2c41656b3d..db999f90ab 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -211,6 +211,10 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 19.83, NumericData.sum(:bank_balance) end + def test_should_return_type_casted_values_with_group_and_expression + assert_equal 0.5, Account.group(:firm_name).sum('0.01 * credit_limit')['37signals'] + end + def test_should_group_by_summed_field_with_conditions c = Account.where('firm_id > 1').group(:firm_id).sum(:credit_limit) assert_nil c[1] @@ -274,7 +278,7 @@ class CalculationsTest < ActiveRecord::TestCase c = Company.group("UPPER(#{QUOTED_TYPE})").count(:all) assert_equal 2, c[nil] assert_equal 1, c['DEPENDENTFIRM'] - assert_equal 4, c['CLIENT'] + assert_equal 5, c['CLIENT'] assert_equal 2, c['FIRM'] end @@ -282,7 +286,7 @@ class CalculationsTest < ActiveRecord::TestCase c = Company.group("UPPER(companies.#{QUOTED_TYPE})").count(:all) assert_equal 2, c[nil] assert_equal 1, c['DEPENDENTFIRM'] - assert_equal 4, c['CLIENT'] + assert_equal 5, c['CLIENT'] assert_equal 2, c['FIRM'] end @@ -462,14 +466,14 @@ class CalculationsTest < ActiveRecord::TestCase def test_distinct_is_honored_when_used_with_count_operation_after_group # Count the number of authors for approved topics approved_topics_count = Topic.group(:approved).count(:author_name)[true] - assert_equal approved_topics_count, 3 + assert_equal approved_topics_count, 4 # Count the number of distinct authors for approved Topics distinct_authors_for_approved_count = Topic.group(:approved).distinct.count(:author_name)[true] - assert_equal distinct_authors_for_approved_count, 2 + assert_equal distinct_authors_for_approved_count, 3 end def test_pluck - assert_equal [1,2,3,4], Topic.order(:id).pluck(:id) + assert_equal [1,2,3,4,5], Topic.order(:id).pluck(:id) end def test_pluck_without_column_names @@ -505,7 +509,7 @@ class CalculationsTest < ActiveRecord::TestCase end def test_pluck_with_qualified_column_name - assert_equal [1,2,3,4], Topic.order(:id).pluck("topics.id") + assert_equal [1,2,3,4,5], Topic.order(:id).pluck("topics.id") end def test_pluck_auto_table_name_prefix @@ -553,11 +557,13 @@ class CalculationsTest < ActiveRecord::TestCase def test_pluck_multiple_columns assert_equal [ [1, "The First Topic"], [2, "The Second Topic of the day"], - [3, "The Third Topic of the day"], [4, "The Fourth Topic of the day"] + [3, "The Third Topic of the day"], [4, "The Fourth Topic of the day"], + [5, "The Fifth Topic of the day"] ], Topic.order(:id).pluck(:id, :title) assert_equal [ [1, "The First Topic", "David"], [2, "The Second Topic of the day", "Mary"], - [3, "The Third Topic of the day", "Carl"], [4, "The Fourth Topic of the day", "Carl"] + [3, "The Third Topic of the day", "Carl"], [4, "The Fourth Topic of the day", "Carl"], + [5, "The Fifth Topic of the day", "Jason"] ], Topic.order(:id).pluck(:id, :title, :author_name) end @@ -583,7 +589,7 @@ class CalculationsTest < ActiveRecord::TestCase def test_pluck_replaces_select_clause taks_relation = Topic.select(:approved, :id).order(:id) - assert_equal [1,2,3,4], taks_relation.pluck(:id) - assert_equal [false, true, true, true], taks_relation.pluck(:approved) + assert_equal [1,2,3,4,5], taks_relation.pluck(:id) + assert_equal [false, true, true, true, true], taks_relation.pluck(:approved) 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 3e33b30144..318cc5a32c 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -2,6 +2,133 @@ 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 + + def teardown + ENV["DATABASE_URL"] = @previous_database_url + end + + def test_string_connection + config = { "production" => "postgres://localhost/foo" } + actual = klass.new(config).resolve + expected = { "production" => + { "adapter" => "postgresql", + "database" => "foo", + "host" => "localhost" + } + } + assert_equal expected, actual + end + + def test_url_sub_key + config = { "production" => { "url" => "postgres://localhost/foo" } } + actual = klass.new(config).resolve + expected = { "production" => + { "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["production"] + assert_equal expected, actual["development"] + assert_equal expected, actual["test"] + assert_equal nil, actual[:production] + assert_equal nil, actual[:development] + assert_equal nil, actual[:test] + end + + def test_sting_with_database_url + ENV['DATABASE_URL'] = "NOT-POSTGRES://localhost/NOT_FOO" + + config = { "production" => "postgres://localhost/foo" } + actual = klass.new(config).resolve + + expected = { "production" => + { "adapter" => "postgresql", + "database" => "foo", + "host" => "localhost" + } + } + assert_equal expected, actual + end + + def test_url_sub_key_with_database_url + ENV['DATABASE_URL'] = "NOT-POSTGRES://localhost/NOT_FOO" + + config = { "production" => { "url" => "postgres://localhost/foo" } } + actual = klass.new(config).resolve + expected = { "production" => + { "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 = {"production" => { "pool" => "5" } } + actual = klass.new(config).resolve + expected = { "production" => + { "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 = {"production" => { "adapter" => "NOT-POSTGRES", "database" => "NOT-FOO", "pool" => "5" } } + actual = klass.new(config).resolve + expected = { "production" => + { "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_management_test.rb b/activerecord/test/cases/connection_management_test.rb index 00667cc52e..77d9ae9b8e 100644 --- a/activerecord/test/cases/connection_management_test.rb +++ b/activerecord/test/cases/connection_management_test.rb @@ -31,6 +31,8 @@ module ActiveRecord object_id = ActiveRecord::Base.connection.object_id rd, wr = IO.pipe + rd.binmode + wr.binmode pid = fork { rd.close diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb index c8dfc3244b..fdd1914cba 100644 --- a/activerecord/test/cases/connection_specification/resolver_test.rb +++ b/activerecord/test/cases/connection_specification/resolver_test.rb @@ -4,58 +4,92 @@ module ActiveRecord module ConnectionAdapters class ConnectionSpecification class ResolverTest < ActiveRecord::TestCase - def resolve(spec) - Resolver.new(spec, {}).spec.config + def resolve(spec, config={}) + Resolver.new(config).resolve(spec) + end + + def spec(spec, config={}) + Resolver.new(config).spec(spec) end def test_url_invalid_adapter - assert_raises(LoadError) do - resolve 'ridiculous://foo?encoding=utf8' + error = assert_raises(LoadError) do + spec 'ridiculous://foo?encoding=utf8' end + + assert_match "Could not load 'active_record/connection_adapters/ridiculous_adapter'", error.message end # The abstract adapter is used simply to bypass the bit of code that # checks that the adapter file can be required in. + def test_url_from_environment + spec = resolve :production, 'production' => 'abstract://foo?encoding=utf8' + assert_equal({ + "adapter" => "abstract", + "host" => "foo", + "encoding" => "utf8" }, spec) + end + + def test_url_sub_key + spec = resolve :production, 'production' => {"url" => 'abstract://foo?encoding=utf8'} + assert_equal({ + "adapter" => "abstract", + "host" => "foo", + "encoding" => "utf8" }, spec) + end + + def test_url_sub_key_merges_correctly + hash = {"url" => 'abstract://foo?encoding=utf8&', "adapter" => "sqlite3", "host" => "bar", "pool" => "3"} + spec = resolve :production, 'production' => hash + assert_equal({ + "adapter" => "abstract", + "host" => "foo", + "encoding" => "utf8", + "pool" => "3" }, spec) + end + def test_url_host_no_db spec = resolve 'abstract://foo?encoding=utf8' assert_equal({ - adapter: "abstract", - host: "foo", - encoding: "utf8" }, spec) + "adapter" => "abstract", + "host" => "foo", + "encoding" => "utf8" }, spec) end def test_url_host_db spec = resolve 'abstract://foo/bar?encoding=utf8' assert_equal({ - adapter: "abstract", - database: "bar", - host: "foo", - encoding: "utf8" }, spec) + "adapter" => "abstract", + "database" => "bar", + "host" => "foo", + "encoding" => "utf8" }, spec) end def test_url_port spec = resolve 'abstract://foo:123?encoding=utf8' assert_equal({ - adapter: "abstract", - port: 123, - host: "foo", - encoding: "utf8" }, spec) + "adapter" => "abstract", + "port" => 123, + "host" => "foo", + "encoding" => "utf8" }, spec) end def test_encoded_password password = 'am@z1ng_p@ssw0rd#!' encoded_password = URI.encode_www_form_component(password) spec = resolve "abstract://foo:#{encoded_password}@localhost/bar" - assert_equal password, spec[:password] + assert_equal password, spec["password"] end - def test_descriptive_error_message_when_adapter_is_missing - error = assert_raise(LoadError) do - resolve(adapter: 'non-existing') - end + def test_url_host_db_for_sqlite3 + spec = resolve 'sqlite3://foo:bar@dburl:9000/foo_test' + assert_equal('/foo_test', spec["database"]) + end - assert_match "Could not load 'active_record/connection_adapters/non-existing_adapter'", error.message + def test_url_host_memory_db_for_sqlite3 + spec = resolve 'sqlite3://foo:bar@dburl:9000/:memory:' + assert_equal(':memory:', spec["database"]) end end end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 9d7f57bf85..df4183c065 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -584,6 +584,14 @@ class DirtyTest < ActiveRecord::TestCase end end + def test_datetime_attribute_doesnt_change_if_zone_is_modified_in_string + time_in_paris = Time.utc(2014, 1, 1, 12, 0, 0).in_time_zone('Paris') + pirate = Pirate.create!(:catchphrase => 'rrrr', :created_on => time_in_paris) + + pirate.created_on = pirate.created_on.in_time_zone('Tokyo').to_s + assert !pirate.created_on_changed? + end + test "partial insert" do with_partial_writes Person do jon = nil diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index 35e8a98156..5cac630a3a 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -16,7 +16,7 @@ class EnumTest < ActiveRecord::TestCase assert @book.unread? end - test "query state with symbol" do + test "query state with strings" do assert_equal "proposed", @book.status assert_equal "unread", @book.read_status end @@ -51,6 +51,77 @@ class EnumTest < ActiveRecord::TestCase assert @book.written? end + test "enum changed attributes" do + old_status = @book.status + @book.status = :published + assert_equal old_status, @book.changed_attributes[:status] + end + + test "enum changes" do + old_status = @book.status + @book.status = :published + assert_equal [old_status, 'published'], @book.changes[:status] + end + + test "enum attribute was" do + old_status = @book.status + @book.status = :published + assert_equal old_status, @book.attribute_was(:status) + end + + test "enum attribute changed" do + @book.status = :published + assert @book.attribute_changed?(:status) + end + + test "enum attribute changed to" do + @book.status = :published + assert @book.attribute_changed?(:status, to: 'published') + end + + test "enum attribute changed from" do + old_status = @book.status + @book.status = :published + assert @book.attribute_changed?(:status, from: old_status) + end + + test "enum attribute changed from old status to new status" do + old_status = @book.status + @book.status = :published + assert @book.attribute_changed?(:status, from: old_status, to: 'published') + end + + test "enum didn't change" do + old_status = @book.status + @book.status = old_status + assert_not @book.attribute_changed?(:status) + end + + test "persist changes that are dirty" do + @book.status = :published + assert @book.attribute_changed?(:status) + @book.status = :written + assert @book.attribute_changed?(:status) + end + + test "reverted changes that are not dirty" do + old_status = @book.status + @book.status = :published + assert @book.attribute_changed?(:status) + @book.status = old_status + assert_not @book.attribute_changed?(:status) + end + + test "reverted changes are not dirty going from nil to value and back" do + book = Book.create!(nullable_status: nil) + + book.nullable_status = :married + assert book.attribute_changed?(:nullable_status) + + book.nullable_status = nil + assert_not book.attribute_changed?(:nullable_status) + end + test "assign non existing value raises an error" do e = assert_raises(ArgumentError) do @book.status = :unknown @@ -58,9 +129,97 @@ class EnumTest < ActiveRecord::TestCase assert_equal "'unknown' is not a valid status", e.message end + test "assign nil value" do + @book.status = nil + assert @book.status.nil? + end + + test "assign empty string value" do + @book.status = '' + assert @book.status.nil? + end + + test "assign long empty string value" do + @book.status = ' ' + assert @book.status.nil? + end + test "constant to access the mapping" do - assert_equal 0, Book::STATUS[:proposed] - assert_equal 1, Book::STATUS["written"] - assert_equal 2, Book::STATUS[:published] + assert_equal 0, Book.statuses[:proposed] + assert_equal 1, Book.statuses["written"] + assert_equal 2, Book.statuses[:published] + end + + test "building new objects with enum scopes" do + assert Book.written.build.written? + assert Book.read.build.read? + end + + test "creating new objects with enum scopes" do + assert Book.written.create.written? + assert Book.read.create.read? + end + + test "_before_type_cast returns the enum label (required for form fields)" do + assert_equal "proposed", @book.status_before_type_cast + end + + test "reserved enum names" do + klass = Class.new(ActiveRecord::Base) do + self.table_name = "books" + enum status: [:proposed, :written, :published] + end + + conflicts = [ + :column, # generates class method .columns, which conflicts with an AR method + :logger, # generates #logger, which conflicts with an AR method + :attributes, # generates #attributes=, which conflicts with an AR method + ] + + conflicts.each_with_index do |name, i| + assert_raises(ArgumentError, "enum name `#{name}` should not be allowed") do + klass.class_eval { enum name => ["value_#{i}"] } + end + end + end + + test "reserved enum values" do + klass = Class.new(ActiveRecord::Base) do + self.table_name = "books" + enum status: [:proposed, :written, :published] + end + + conflicts = [ + :new, # generates a scope that conflicts with an AR class method + :valid, # generates #valid?, which conflicts with an AR method + :save, # generates #save!, which conflicts with an AR method + :proposed, # same value as an existing enum + ] + + conflicts.each_with_index do |value, i| + assert_raises(ArgumentError, "enum value `#{value}` should not be allowed") do + klass.class_eval { enum "status_#{i}" => [value] } + end + end + end + + test "overriding enum method should not raise" do + assert_nothing_raised do + klass = Class.new(ActiveRecord::Base) do + self.table_name = "books" + + def published! + super + "do publish work..." + end + + enum status: [:proposed, :written, :published] + + def written! + super + "do written work..." + end + end + end end end diff --git a/activerecord/test/cases/finder_respond_to_test.rb b/activerecord/test/cases/finder_respond_to_test.rb index 3ff22f222f..6ab2657c44 100644 --- a/activerecord/test/cases/finder_respond_to_test.rb +++ b/activerecord/test/cases/finder_respond_to_test.rb @@ -5,6 +5,11 @@ class FinderRespondToTest < ActiveRecord::TestCase fixtures :topics + def test_should_preserve_normal_respond_to_behaviour_on_base + assert_respond_to ActiveRecord::Base, :new + assert !ActiveRecord::Base.respond_to?(:find_by_something) + end + def test_should_preserve_normal_respond_to_behaviour_and_respond_to_newly_added_method class << Topic; self; end.send(:define_method, :method_added_for_finder_respond_to_test) { } assert_respond_to Topic, :method_added_for_finder_respond_to_test diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index b06418d9fe..b1eded6494 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -12,6 +12,7 @@ require 'models/developer' require 'models/customer' require 'models/toy' require 'models/matey' +require 'models/dog' class FinderTest < ActiveRecord::TestCase fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers, :categories, :categorizations @@ -253,6 +254,94 @@ class FinderTest < ActiveRecord::TestCase end end + def test_second + assert_equal topics(:second).title, Topic.second.title + end + + def test_second_with_offset + assert_equal topics(:fifth), Topic.offset(3).second + end + + def test_second_have_primary_key_order_by_default + expected = topics(:second) + expected.touch # PostgreSQL changes the default order if no order clause is used + assert_equal expected, Topic.second + end + + def test_model_class_responds_to_second_bang + assert Topic.second! + Topic.delete_all + assert_raises ActiveRecord::RecordNotFound do + Topic.second! + end + end + + def test_third + assert_equal topics(:third).title, Topic.third.title + end + + def test_third_with_offset + assert_equal topics(:fifth), Topic.offset(2).third + end + + def test_third_have_primary_key_order_by_default + expected = topics(:third) + expected.touch # PostgreSQL changes the default order if no order clause is used + assert_equal expected, Topic.third + end + + def test_model_class_responds_to_third_bang + assert Topic.third! + Topic.delete_all + assert_raises ActiveRecord::RecordNotFound do + Topic.third! + end + end + + def test_fourth + assert_equal topics(:fourth).title, Topic.fourth.title + end + + def test_fourth_with_offset + assert_equal topics(:fifth), Topic.offset(1).fourth + end + + def test_fourth_have_primary_key_order_by_default + expected = topics(:fourth) + expected.touch # PostgreSQL changes the default order if no order clause is used + assert_equal expected, Topic.fourth + end + + def test_model_class_responds_to_fourth_bang + assert Topic.fourth! + Topic.delete_all + assert_raises ActiveRecord::RecordNotFound do + Topic.fourth! + end + end + + def test_fifth + assert_equal topics(:fifth).title, Topic.fifth.title + end + + def test_fifth_with_offset + assert_equal topics(:fifth), Topic.offset(0).fifth + end + + def test_fifth_have_primary_key_order_by_default + expected = topics(:fifth) + expected.touch # PostgreSQL changes the default order if no order clause is used + assert_equal expected, Topic.fifth + end + + def test_model_class_responds_to_fifth_bang + assert Topic.fifth! + Topic.delete_all + assert_raises ActiveRecord::RecordNotFound do + Topic.fifth! + end + end + def test_last_bang_present assert_nothing_raised do assert_equal topics(:second), Topic.where("title = 'The Second Topic of the day'").last! @@ -266,7 +355,7 @@ class FinderTest < ActiveRecord::TestCase end def test_model_class_responds_to_last_bang - assert_equal topics(:fourth), Topic.last! + assert_equal topics(:fifth), Topic.last! assert_raises ActiveRecord::RecordNotFound do Topic.delete_all Topic.last! @@ -641,6 +730,13 @@ class FinderTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordNotFound) { Topic.find_by_title!("The First Topic!") } end + def test_find_by_on_attribute_that_is_a_reserved_word + dog_alias = 'Dog' + dog = Dog.create(alias: dog_alias) + + assert_equal dog, Dog.find_by_alias(dog_alias) + end + def test_find_by_one_attribute_that_is_an_alias assert_equal topics(:first), Topic.find_by_heading("The First Topic") assert_nil Topic.find_by_heading("The First Topic!") @@ -721,7 +817,7 @@ class FinderTest < ActiveRecord::TestCase end def test_find_last_with_offset - devs = Developer.order('id').all + devs = Developer.order('id') assert_equal devs[2], Developer.offset(2).first assert_equal devs[-3], Developer.offset(2).last @@ -804,8 +900,8 @@ class FinderTest < ActiveRecord::TestCase end def test_select_values - assert_equal ["1","2","3","4","5","6","7","8","9", "10"], Company.connection.select_values("SELECT id FROM companies ORDER BY id").map! { |i| i.to_s } - assert_equal ["37signals","Summit","Microsoft", "Flamboyant Software", "Ex Nihilo", "RailsCore", "Leetsoft", "Jadedpixel", "Odegy", "Ex Nihilo Part Deux"], Company.connection.select_values("SELECT name FROM companies ORDER BY id") + assert_equal ["1","2","3","4","5","6","7","8","9", "10", "11"], Company.connection.select_values("SELECT id FROM companies ORDER BY id").map! { |i| i.to_s } + assert_equal ["37signals","Summit","Microsoft", "Flamboyant Software", "Ex Nihilo", "RailsCore", "Leetsoft", "Jadedpixel", "Odegy", "Ex Nihilo Part Deux", "Apex"], Company.connection.select_values("SELECT name FROM companies ORDER BY id") end def test_select_rows @@ -847,7 +943,7 @@ class FinderTest < ActiveRecord::TestCase def test_with_limiting_with_custom_select posts = Post.references(:authors).merge( - :includes => :author, :select => ' posts.*, authors.id as "author_id"', + :includes => :author, :select => 'posts.*, authors.id as "author_id"', :limit => 3, :order => 'posts.id' ).to_a assert_equal 3, posts.size @@ -855,14 +951,23 @@ class FinderTest < ActiveRecord::TestCase end def test_find_one_message_with_custom_primary_key - Toy.primary_key = :name - begin - Toy.find 'Hello World!' - rescue ActiveRecord::RecordNotFound => e - assert_equal 'Couldn\'t find Toy with name=Hello World!', e.message + table_with_custom_primary_key do |model| + model.primary_key = :name + e = assert_raises(ActiveRecord::RecordNotFound) do + model.find 'Hello World!' + end + assert_equal %Q{Couldn't find MercedesCar with 'name'=Hello World!}, e.message + end + end + + def test_find_some_message_with_custom_primary_key + table_with_custom_primary_key do |model| + model.primary_key = :name + e = assert_raises(ActiveRecord::RecordNotFound) do + model.find 'Hello', 'World!' + end + assert_equal %Q{Couldn't find all MercedesCars with 'name': (Hello, World!) (found 0 results, but was looking for 2)}, e.message end - ensure - Toy.reset_primary_key end def test_find_without_primary_key @@ -884,10 +989,11 @@ class FinderTest < ActiveRecord::TestCase end end - def with_env_tz(new_tz = 'US/Eastern') - old_tz, ENV['TZ'] = ENV['TZ'], new_tz - yield - ensure - old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') + def table_with_custom_primary_key + yield(Class.new(Toy) do + def self.name + 'MercedesCar' + end + end) end end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 34e8f1be0f..3758224b0c 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -20,6 +20,9 @@ Thread.abort_on_exception = true # Show backtraces for deprecated behavior for quicker cleanup. ActiveSupport::Deprecation.debug = true +# Disable available locale checks to avoid warnings running the test suite. +I18n.enforce_available_locales = false + # Connect to the database ARTest.connect @@ -149,23 +152,6 @@ end load_schema -class << Time - unless method_defined? :now_before_time_travel - alias_method :now_before_time_travel, :now - end - - def now - (@now ||= nil) || now_before_time_travel - end - - def travel_to(time, &block) - @now = time - block.call - ensure - @now = nil - end -end - class SQLSubscriber attr_reader :logged attr_reader :payloads @@ -183,7 +169,6 @@ class SQLSubscriber def finish(name, id, payload); end end - module InTimeZone private diff --git a/activerecord/test/cases/hot_compatibility_test.rb b/activerecord/test/cases/hot_compatibility_test.rb index 96e581ab4c..367d04a154 100644 --- a/activerecord/test/cases/hot_compatibility_test.rb +++ b/activerecord/test/cases/hot_compatibility_test.rb @@ -5,7 +5,7 @@ class HotCompatibilityTest < ActiveRecord::TestCase setup do @klass = Class.new(ActiveRecord::Base) do - connection.create_table :hot_compatibilities do |t| + connection.create_table :hot_compatibilities, force: true do |t| t.string :foo t.string :bar end diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 73cf99a5d7..e2ff2aa451 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -1,10 +1,11 @@ -require "cases/helper" +require 'cases/helper' require 'models/company' require 'models/person' require 'models/post' require 'models/project' require 'models/subscriber' require 'models/vegetables' +require 'models/shop' class InheritanceTest < ActiveRecord::TestCase fixtures :companies, :projects, :subscribers, :accounts, :vegetables @@ -128,6 +129,17 @@ class InheritanceTest < ActiveRecord::TestCase assert_kind_of Cabbage, cabbage end + def test_alt_becomes_bang_resets_inheritance_type_column + vegetable = Vegetable.create!(name: "Red Pepper") + assert_nil vegetable.custom_type + + cabbage = vegetable.becomes!(Cabbage) + assert_equal "Cabbage", cabbage.custom_type + + vegetable = cabbage.becomes!(Vegetable) + assert_nil cabbage.custom_type + end + def test_inheritance_find_all companies = Company.all.merge!(:order => 'id').to_a assert_kind_of Firm, companies[0], "37signals should be a firm" @@ -176,14 +188,14 @@ class InheritanceTest < ActiveRecord::TestCase e = assert_raises(NotImplementedError) do AbstractCompany.new end - assert_equal("AbstractCompany is an abstract class and can not be instantiated.", e.message) + assert_equal("AbstractCompany is an abstract class and cannot be instantiated.", e.message) end def test_new_with_ar_base e = assert_raises(NotImplementedError) do ActiveRecord::Base.new end - assert_equal("ActiveRecord::Base is an abstract class and can not be instantiated.", e.message) + assert_equal("ActiveRecord::Base is an abstract class and cannot be instantiated.", e.message) end def test_new_with_invalid_type @@ -210,9 +222,9 @@ class InheritanceTest < ActiveRecord::TestCase end def test_inheritance_condition - assert_equal 10, Company.count + assert_equal 11, Company.count assert_equal 2, Firm.count - assert_equal 4, Client.count + assert_equal 5, Client.count end def test_alt_inheritance_condition @@ -356,4 +368,10 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase ensure ActiveRecord::Base.store_full_sti_class = true end + + def test_sti_type_from_attributes_disabled_in_non_sti_class + phone = Shop::Product::Type.new(name: 'Phone') + product = Shop::Product.new(:type => phone) + assert product.save + end end diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb index 07ffcef875..dfb8a608cb 100644 --- a/activerecord/test/cases/integration_test.rb +++ b/activerecord/test/cases/integration_test.rb @@ -1,12 +1,13 @@ +# encoding: utf-8 + require 'cases/helper' require 'models/company' require 'models/developer' -require 'models/car' -require 'models/bulb' require 'models/owner' +require 'models/pet' class IntegrationTest < ActiveRecord::TestCase - fixtures :companies, :developers, :owners + fixtures :companies, :developers, :owners, :pets def test_to_param_should_return_string assert_kind_of String, Client.first.to_param @@ -46,6 +47,12 @@ class IntegrationTest < ActiveRecord::TestCase assert_equal '4-ab-ab-ab-ab-ab-ab', firm.to_param end + def test_to_param_class_method_multibyte_character + firm = Firm.find(4) + firm.name = "戦場ヶ原 ひたぎ" + assert_equal '4', firm.to_param + end + def test_to_param_class_method_uses_default_if_blank firm = Firm.find(4) firm.name = nil @@ -83,13 +90,14 @@ class IntegrationTest < ActiveRecord::TestCase end def test_cache_key_changes_when_child_touched - car = Car.create - Bulb.create(car: car) + owner = owners(:blackbeard) + pet = pets(:parrot) + + owner.update_column :updated_at, Time.current + key = owner.cache_key - key = car.cache_key - car.bulb.touch - car.reload - assert_not_equal key, car.cache_key + assert pet.touch + assert_not_equal key, owner.reload.cache_key end def test_cache_key_format_for_existing_record_with_nil_updated_timestamps diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index a16ed963fe..c373dc1511 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -431,6 +431,17 @@ unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) || in_memory_db? assert_equal old, person.reload.first_name end + if current_adapter?(:PostgreSQLAdapter) + def test_lock_sending_custom_lock_statement + Person.transaction do + person = Person.find(1) + assert_sql(/LIMIT 1 FOR SHARE NOWAIT/) do + person.lock!('FOR SHARE NOWAIT') + end + end + end + end + if current_adapter?(:PostgreSQLAdapter, :OracleAdapter) def test_no_locks_no_wait first, second = duel { Person.find 1 } diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index e43e256d24..294f2eb9fe 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -308,6 +308,22 @@ module ActiveRecord assert_equal 2000, connection.select_values("SELECT money FROM testings").first.to_i end + def test_change_column_null + testing_table_with_only_foo_attribute do + notnull_migration = Class.new(ActiveRecord::Migration) do + def change + change_column_null :testings, :foo, false + end + end + notnull_migration.new.suppress_messages do + notnull_migration.migrate(:up) + assert_equal false, connection.columns(:testings).find{ |c| c.name == "foo"}.null + notnull_migration.migrate(:down) + assert connection.columns(:testings).find{ |c| c.name == "foo"}.null + end + end + end + def test_column_exists connection.create_table :testings do |t| t.column :foo, :string diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb index 8065541bfe..c1d7cd5874 100644 --- a/activerecord/test/cases/migration/change_table_test.rb +++ b/activerecord/test/cases/migration/change_table_test.rb @@ -5,7 +5,7 @@ module ActiveRecord class Migration class TableTest < ActiveRecord::TestCase def setup - @connection = MiniTest::Mock.new + @connection = Minitest::Mock.new end def teardown diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 1b205d372f..35b656ee43 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -4,7 +4,8 @@ module ActiveRecord class Migration class CommandRecorderTest < ActiveRecord::TestCase def setup - @recorder = CommandRecorder.new + connection = ActiveRecord::Base.connection + @recorder = CommandRecorder.new(connection) end def test_respond_to_delegates diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 7c3988859f..1bda472d23 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -79,6 +79,10 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Migrator.migrations_paths = old_path end + def test_migration_version + ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/version_check", 20131219224947) + end + def test_create_table_with_force_true_does_not_drop_nonexisting_table if Person.connection.table_exists?(:testings2) Person.connection.drop_table :testings2 @@ -324,6 +328,7 @@ class MigrationTest < ActiveRecord::TestCase end def test_proper_table_name_on_migrator + reminder_class = new_isolated_reminder_class assert_deprecated do assert_equal "table", ActiveRecord::Migrator.proper_table_name('table') end @@ -331,30 +336,30 @@ class MigrationTest < ActiveRecord::TestCase assert_equal "table", ActiveRecord::Migrator.proper_table_name(:table) end assert_deprecated do - assert_equal "reminders", ActiveRecord::Migrator.proper_table_name(Reminder) + assert_equal "reminders", ActiveRecord::Migrator.proper_table_name(reminder_class) end - Reminder.reset_table_name + reminder_class.reset_table_name assert_deprecated do - assert_equal Reminder.table_name, ActiveRecord::Migrator.proper_table_name(Reminder) + assert_equal reminder_class.table_name, ActiveRecord::Migrator.proper_table_name(reminder_class) end # Use the model's own prefix/suffix if a model is given ActiveRecord::Base.table_name_prefix = "ARprefix_" ActiveRecord::Base.table_name_suffix = "_ARsuffix" - Reminder.table_name_prefix = 'prefix_' - Reminder.table_name_suffix = '_suffix' - Reminder.reset_table_name + reminder_class.table_name_prefix = 'prefix_' + reminder_class.table_name_suffix = '_suffix' + reminder_class.reset_table_name assert_deprecated do - assert_equal "prefix_reminders_suffix", ActiveRecord::Migrator.proper_table_name(Reminder) + assert_equal "prefix_reminders_suffix", ActiveRecord::Migrator.proper_table_name(reminder_class) end - Reminder.table_name_prefix = '' - Reminder.table_name_suffix = '' - Reminder.reset_table_name + reminder_class.table_name_prefix = '' + reminder_class.table_name_suffix = '' + reminder_class.reset_table_name # Use AR::Base's prefix/suffix if string or symbol is given ActiveRecord::Base.table_name_prefix = "prefix_" ActiveRecord::Base.table_name_suffix = "_suffix" - Reminder.reset_table_name + reminder_class.reset_table_name assert_deprecated do assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name('table') end @@ -364,28 +369,29 @@ class MigrationTest < ActiveRecord::TestCase end def test_proper_table_name_on_migration + reminder_class = new_isolated_reminder_class migration = ActiveRecord::Migration.new assert_equal "table", migration.proper_table_name('table') assert_equal "table", migration.proper_table_name(:table) - assert_equal "reminders", migration.proper_table_name(Reminder) - Reminder.reset_table_name - assert_equal Reminder.table_name, migration.proper_table_name(Reminder) + assert_equal "reminders", migration.proper_table_name(reminder_class) + reminder_class.reset_table_name + assert_equal reminder_class.table_name, migration.proper_table_name(reminder_class) # Use the model's own prefix/suffix if a model is given ActiveRecord::Base.table_name_prefix = "ARprefix_" ActiveRecord::Base.table_name_suffix = "_ARsuffix" - Reminder.table_name_prefix = 'prefix_' - Reminder.table_name_suffix = '_suffix' - Reminder.reset_table_name - assert_equal "prefix_reminders_suffix", migration.proper_table_name(Reminder) - Reminder.table_name_prefix = '' - Reminder.table_name_suffix = '' - Reminder.reset_table_name + reminder_class.table_name_prefix = 'prefix_' + reminder_class.table_name_suffix = '_suffix' + reminder_class.reset_table_name + assert_equal "prefix_reminders_suffix", migration.proper_table_name(reminder_class) + reminder_class.table_name_prefix = '' + reminder_class.table_name_suffix = '' + reminder_class.reset_table_name # Use AR::Base's prefix/suffix if string or symbol is given ActiveRecord::Base.table_name_prefix = "prefix_" ActiveRecord::Base.table_name_suffix = "_suffix" - Reminder.reset_table_name + reminder_class.reset_table_name assert_equal "prefix_table_suffix", migration.proper_table_name('table', migration.table_name_options) assert_equal "prefix_table_suffix", migration.proper_table_name(:table, migration.table_name_options) end @@ -443,6 +449,32 @@ class MigrationTest < ActiveRecord::TestCase Person.connection.drop_table :binary_testings rescue nil end + def test_create_table_with_query + Person.connection.drop_table :table_from_query_testings rescue nil + Person.connection.create_table(:person, force: true) + + Person.connection.create_table :table_from_query_testings, as: "SELECT id FROM person" + + columns = Person.connection.columns(:table_from_query_testings) + assert_equal 1, columns.length + assert_equal "id", columns.first.name + + Person.connection.drop_table :table_from_query_testings rescue nil + end + + def test_create_table_with_query_from_relation + Person.connection.drop_table :table_from_query_testings rescue nil + Person.connection.create_table(:person, force: true) + + Person.connection.create_table :table_from_query_testings, as: Person.select(:id) + + columns = Person.connection.columns(:table_from_query_testings) + assert_equal 1, columns.length + assert_equal "id", columns.first.name + + Person.connection.drop_table :table_from_query_testings rescue nil + end + if current_adapter? :OracleAdapter def test_create_table_with_custom_sequence_name # table name is 29 chars, the standard sequence name will @@ -502,11 +534,13 @@ class MigrationTest < ActiveRecord::TestCase end protected - def with_env_tz(new_tz = 'US/Eastern') - old_tz, ENV['TZ'] = ENV['TZ'], new_tz - yield - ensure - old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') + # This is needed to isolate class_attribute assignments like `table_name_prefix` + # for each test case. + def new_isolated_reminder_class + Class.new(Reminder) { + def self.name; "Reminder"; end + def self.base_class; self; end + } end end @@ -743,7 +777,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps" @existing_migrations = Dir[@migrations_path + "/*.rb"] - Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do + travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") @@ -768,7 +802,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy_with_timestamps" sources[:omg] = MIGRATIONS_ROOT + "/to_copy_with_timestamps2" - Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do + travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do copied = ActiveRecord::Migration.copy(@migrations_path, sources) assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") @@ -788,7 +822,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps" @existing_migrations = Dir[@migrations_path + "/*.rb"] - Time.travel_to(Time.utc(2010, 2, 20, 10, 10, 10)) do + travel_to(Time.utc(2010, 2, 20, 10, 10, 10)) do ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) assert File.exist?(@migrations_path + "/20100301010102_people_have_hobbies.bukkits.rb") assert File.exist?(@migrations_path + "/20100301010103_people_have_descriptions.bukkits.rb") @@ -863,7 +897,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase @migrations_path = MIGRATIONS_ROOT + "/non_existing" @existing_migrations = [] - Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do + travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") @@ -878,7 +912,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase @migrations_path = MIGRATIONS_ROOT + "/empty" @existing_migrations = [] - Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do + travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb index 2e386a172a..3831de6ae3 100644 --- a/activerecord/test/cases/multiple_db_test.rb +++ b/activerecord/test/cases/multiple_db_test.rb @@ -101,7 +101,7 @@ class MultipleDbTest < ActiveRecord::TestCase College.first.courses.first end ensure - ActiveRecord::Base.establish_connection 'arunit' + ActiveRecord::Base.establish_connection :arunit end end end diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 6cd3e2154e..b9f0624f76 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -152,6 +152,20 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal original_errors, client.errors end + def test_dupd_becomes_persists_changes_from_the_original + original = topics(:first) + copy = original.dup.becomes(Reply) + copy.save! + assert_equal "The First Topic", Topic.find(copy.id).title + end + + def test_becomes_includes_changed_attributes + company = Company.new(name: "37signals") + client = company.becomes(Client) + assert_equal "37signals", client.name + assert_equal %w{name}, client.changed + end + def test_delete_many original_count = Topic.count Topic.delete(deleting = [1, 2]) @@ -726,7 +740,7 @@ class PersistenceTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordInvalid) { reply.update!(title: nil, content: "Have a nice evening") } ensure - Reply.reset_callbacks(:validate) + Reply.clear_validators! end def test_update_attributes! @@ -747,7 +761,7 @@ class PersistenceTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordInvalid) { reply.update_attributes!(title: nil, content: "Have a nice evening") } ensure - Reply.reset_callbacks(:validate) + Reply.clear_validators! end def test_destroyed_returns_boolean diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 5566563116..da8ae672fe 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -8,7 +8,7 @@ require 'rack' class QueryCacheTest < ActiveRecord::TestCase fixtures :tasks, :topics, :categories, :posts, :categories_posts - def setup + teardown do Task.connection.clear_query_cache ActiveRecord::Base.connection.disable_query_cache! end @@ -214,7 +214,7 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase Post.find(1) # change the column definition - Post.connection.change_column :posts, :title, :string, :limit => 80 + Post.connection.change_column :posts, :title, :string, limit: 80 assert_nothing_raised { Post.find(1) } # restore the old definition @@ -241,7 +241,6 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase def test_update Task.connection.expects(:clear_query_cache).times(2) - Task.cache do task = Task.find(1) task.starting = Time.now.utc @@ -251,7 +250,6 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase def test_destroy Task.connection.expects(:clear_query_cache).times(2) - Task.cache do Task.find(1).destroy end @@ -259,7 +257,6 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase def test_insert ActiveRecord::Base.connection.expects(:clear_query_cache).times(2) - Task.cache do Task.create! end diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index 13677797cf..9b2bfed039 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -6,95 +6,63 @@ module ActiveRecord class DelegationTest < ActiveRecord::TestCase fixtures :posts - def assert_responds(target, method) - assert target.respond_to?(method) - assert_nothing_raised do - method_arity = target.to_a.method(method).arity + def call_method(target, method) + method_arity = target.to_a.method(method).arity - if method_arity.zero? - target.send(method) - elsif method_arity < 0 - if method == :shuffle! - target.send(method) - else - target.send(method, 1) - end + if method_arity.zero? + target.public_send(method) + elsif method_arity < 0 + if method == :shuffle! + target.public_send(method) else - raise NotImplementedError + target.public_send(method, 1) end + elsif method_arity == 1 + target.public_send(method, 1) + else + raise NotImplementedError end end end - class DelegationAssociationTest < DelegationTest - def target - Post.first.comments - end + module DelegationWhitelistBlacklistTests + ARRAY_DELEGATES = [ + :+, :-, :|, :&, :[], + :all?, :collect, :detect, :each, :each_cons, :each_with_index, + :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 + ] - [:map, :collect].each do |method| - test "##{method} is delegated" do - assert_responds(target, method) - assert_equal(target.pluck(:body), target.send(method) {|post| post.body }) - end - - test "##{method}! is not delegated" do - assert_deprecated do - assert_responds(target, "#{method}!") - end + ARRAY_DELEGATES.each do |method| + define_method "test_delegates_#{method}_to_Array" do + assert_respond_to target, method end end - [:compact!, :flatten!, :reject!, :reverse!, :rotate!, - :shuffle!, :slice!, :sort!, :sort_by!].each do |method| - test "##{method} delegation is deprecated" do - assert_deprecated do - assert_responds(target, method) - end - end - end - - [:select!, :uniq!].each do |method| - test "##{method} is implemented" do - assert_responds(target, method) + ActiveRecord::Delegation::BLACKLISTED_ARRAY_METHODS.each do |method| + define_method "test_#{method}_is_not_delegated_to_Array" do + assert_raises(NoMethodError) { call_method(target, method) } end end end - class DelegationRelationTest < DelegationTest - fixtures :comments + class DelegationAssociationTest < DelegationTest + include DelegationWhitelistBlacklistTests def target - Comment.where(body: 'Normal type') + Post.first.comments end + end - [:map, :collect].each do |method| - test "##{method} is delegated" do - assert_responds(target, method) - assert_equal(target.pluck(:body), target.send(method) {|post| post.body }) - end - - test "##{method}! is not delegated" do - assert_deprecated do - assert_responds(target, "#{method}!") - end - end - end + class DelegationRelationTest < DelegationTest + include DelegationWhitelistBlacklistTests - [:compact!, :flatten!, :reject!, :reverse!, :rotate!, - :shuffle!, :slice!, :sort!, :sort_by!].each do |method| - test "##{method} delegation is deprecated" do - assert_deprecated do - assert_responds(target, method) - end - end - end + fixtures :comments - [:select!, :uniq!].each do |method| - test "##{method} triggers an immutable error" do - assert_raises ActiveRecord::ImmutableRelation do - assert_responds(target, method) - end - end + def target + Comment.all end end end diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index 7cb2a19bee..4fafa668fb 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -14,6 +14,10 @@ module ActiveRecord def relation_delegate_class(klass) self.class.relation_delegate_class(klass) end + + def attribute_alias?(name) + false + end end def relation diff --git a/activerecord/test/cases/relation/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb index d44b4dfe3d..fd2420cb88 100644 --- a/activerecord/test/cases/relation/where_chain_test.rb +++ b/activerecord/test/cases/relation/where_chain_test.rb @@ -23,6 +23,12 @@ module ActiveRecord assert_equal([expected], relation.where_values) end + def test_not_with_nil + assert_raise ArgumentError do + Post.where.not(nil) + end + end + def test_not_in expected = Arel::Nodes::NotIn.new(Post.arel_table[@name], %w[hello goodbye]) relation = Post.where.not(title: %w[hello goodbye]) diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index 56e4605ccc..937f226b1d 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -35,6 +35,21 @@ module ActiveRecord assert_equal Post.where(author_id: 1).to_sql, Post.where(author: author).to_sql end + def test_belongs_to_nil_where + assert_equal Post.where(author_id: nil).to_sql, Post.where(author: nil).to_sql + end + + def test_belongs_to_array_value_where + assert_equal Post.where(author_id: [1,2]).to_sql, Post.where(author: [1,2]).to_sql + end + + def test_belongs_to_nested_relation_where + expected = Post.where(author_id: Author.where(id: [1,2])).to_sql + actual = Post.where(author: Author.where(id: [1,2])).to_sql + + assert_equal expected, actual + end + def test_belongs_to_nested_where parent = Comment.new parent.id = 1 @@ -55,6 +70,25 @@ module ActiveRecord assert_equal expected.to_sql, actual.to_sql end + def test_polymorphic_nested_array_where + treasure = Treasure.new + treasure.id = 1 + hidden = HiddenTreasure.new + hidden.id = 2 + + expected = PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: [treasure, hidden]) + actual = PriceEstimate.where(estimate_of: [treasure, hidden]) + + assert_equal expected.to_sql, actual.to_sql + end + + def test_polymorphic_nested_relation_where + expected = PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: Treasure.where(id: [1,2])) + actual = PriceEstimate.where(estimate_of: Treasure.where(id: [1,2])) + + assert_equal expected.to_sql, actual.to_sql + end + def test_polymorphic_sti_shallow_where treasure = HiddenTreasure.new treasure.id = 1 diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 70d113fb39..15611656fd 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -207,6 +207,16 @@ module ActiveRecord assert_equal 3, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count.length end + def test_relation_merging_with_joins_as_join_dependency_pick_proper_parent + post = Post.create!(title: "haha", body: "huhu") + comment = post.comments.create!(body: "hu") + 3.times { comment.ratings.create! } + + relation = Post.joins(:comments).merge Comment.joins(:ratings) + + assert_equal 3, relation.where(id: post.id).pluck(:id).size + end + def test_respond_to_for_non_selected_element post = Post.select(:title).first assert_equal false, post.respond_to?(:body), "post should not respond_to?(:body) since invoking it raises exception" @@ -221,6 +231,5 @@ module ActiveRecord posts_with_special_comments_with_ratings = Post.group("posts.id").joins(:special_comments).merge(special_comments_with_ratings) assert_equal 3, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count.length end - end end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index baa3acf3fb..e390d37871 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -65,7 +65,7 @@ class RelationTest < ActiveRecord::TestCase def test_scoped topics = Topic.all assert_kind_of ActiveRecord::Relation, topics - assert_equal 4, topics.size + assert_equal 5, topics.size end def test_to_json @@ -86,14 +86,14 @@ class RelationTest < ActiveRecord::TestCase def test_scoped_all topics = Topic.all.to_a assert_kind_of Array, topics - assert_no_queries { assert_equal 4, topics.size } + assert_no_queries { assert_equal 5, topics.size } end def test_loaded_all topics = Topic.all assert_queries(1) do - 2.times { assert_equal 4, topics.to_a.size } + 2.times { assert_equal 5, topics.to_a.size } end assert topics.loaded? @@ -151,6 +151,11 @@ class RelationTest < ActiveRecord::TestCase assert_equal relation.to_a, Comment.select('a.*').from(relation, :a).to_a end + def test_finding_with_subquery_without_select + relation = Topic.where(:approved => true) + assert_equal relation.to_a, Topic.from(relation).to_a + end + def test_finding_with_conditions assert_equal ["David"], Author.where(:name => 'David').map(&:name) assert_equal ['Mary'], Author.where(["name = ?", 'Mary']).map(&:name) @@ -159,27 +164,27 @@ class RelationTest < ActiveRecord::TestCase def test_finding_with_order topics = Topic.order('id') - assert_equal 4, topics.to_a.size + assert_equal 5, topics.to_a.size assert_equal topics(:first).title, topics.first.title end def test_finding_with_arel_order topics = Topic.order(Topic.arel_table[:id].asc) - assert_equal 4, topics.to_a.size + assert_equal 5, topics.to_a.size assert_equal topics(:first).title, topics.first.title end def test_finding_with_assoc_order topics = Topic.order(:id => :desc) - assert_equal 4, topics.to_a.size - assert_equal topics(:fourth).title, topics.first.title + assert_equal 5, topics.to_a.size + assert_equal topics(:fifth).title, topics.first.title end def test_finding_with_reverted_assoc_order topics = Topic.order(:id => :asc).reverse_order - assert_equal 4, topics.to_a.size - assert_equal topics(:fourth).title, topics.first.title + assert_equal 5, topics.to_a.size + assert_equal topics(:fifth).title, topics.first.title end def test_order_with_hash_and_symbol_generates_the_same_sql @@ -192,19 +197,43 @@ class RelationTest < ActiveRecord::TestCase def test_finding_last_with_arel_order topics = Topic.order(Topic.arel_table[:id].asc) - assert_equal topics(:fourth).title, topics.last.title + assert_equal topics(:fifth).title, topics.last.title end def test_finding_with_order_concatenated topics = Topic.order('author_name').order('title') - assert_equal 4, topics.to_a.size + assert_equal 5, topics.to_a.size assert_equal topics(:fourth).title, topics.first.title end + def test_finding_with_order_by_aliased_attributes + topics = Topic.order(:heading) + assert_equal 5, topics.to_a.size + assert_equal topics(:fifth).title, topics.first.title + end + + def test_finding_with_assoc_order_by_aliased_attributes + topics = Topic.order(heading: :desc) + assert_equal 5, topics.to_a.size + assert_equal topics(:third).title, topics.first.title + end + def test_finding_with_reorder topics = Topic.order('author_name').order('title').reorder('id').to_a topics_titles = topics.map{ |t| t.title } - assert_equal ['The First Topic', 'The Second Topic of the day', 'The Third Topic of the day', 'The Fourth Topic of the day'], topics_titles + assert_equal ['The First Topic', 'The Second Topic of the day', 'The Third Topic of the day', 'The Fourth Topic of the day', 'The Fifth Topic of the day'], topics_titles + end + + def test_finding_with_reorder_by_aliased_attributes + topics = Topic.order('author_name').reorder(:heading) + assert_equal 5, topics.to_a.size + assert_equal topics(:fifth).title, topics.first.title + end + + def test_finding_with_assoc_reorder_by_aliased_attributes + topics = Topic.order('author_name').reorder(heading: :desc) + assert_equal 5, topics.to_a.size + assert_equal topics(:third).title, topics.first.title end def test_finding_with_order_and_take @@ -295,7 +324,7 @@ class RelationTest < ActiveRecord::TestCase def test_null_relation_calculations_methods assert_no_queries do assert_equal 0, Developer.none.count - assert_equal 0, Developer.none.calculate(:count, nil) + assert_equal 0, Developer.none.calculate(:count, nil, {}) assert_equal nil, Developer.none.calculate(:average, 'salary') end end @@ -770,6 +799,13 @@ class RelationTest < ActiveRecord::TestCase assert_equal david.salary, developer.salary end + def test_select_takes_an_aliased_attribute + first = topics(:first) + + topic = Topic.where(id: first.id).select(:heading).first + assert_equal first.heading, topic.heading + end + def test_select_argument_error assert_raises(ArgumentError) { Developer.select } end @@ -1500,32 +1536,63 @@ class RelationTest < ActiveRecord::TestCase end end + test "joins with select" do + posts = Post.joins(:author).select("id", "authors.author_address_id").order("posts.id").limit(3) + assert_equal [1, 2, 4], posts.map(&:id) + assert_equal [1, 1, 1], posts.map(&:author_address_id) + end + test "delegations do not leak to other classes" do Topic.all.by_lifo assert Topic.all.class.method_defined?(:by_lifo) assert !Post.all.respond_to?(:by_lifo) end - class OMGTopic < ActiveRecord::Base - self.table_name = 'topics' + def test_unscope_removes_binds + left = Post.where(id: Arel::Nodes::BindParam.new('?')) + column = Post.columns_hash['id'] + left.bind_values += [[column, 20]] - def self.__omg__ - "omgtopic" - end + relation = left.unscope(where: :id) + assert_equal [], relation.bind_values end - test "delegations do not clash across classes" do - begin - class ::Array - def __omg__ - "array" - end - 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) - assert_equal "array", Topic.all.__omg__ - assert_equal "omgtopic", OMGTopic.all.__omg__ - ensure - Array.send(:remove_method, :__omg__) - end + merged = left.merge(right) + assert_equal [], merged.bind_values + end + + def test_merging_keeps_lhs_bind_parameters + column = Post.columns_hash['id'] + binds = [[column, 20]] + + right = Post.where(id: Arel::Nodes::BindParam.new('?')) + right.bind_values += binds + left = Post.where(id: 10) + + merged = left.merge(right) + assert_equal binds, merged.bind_values + end + + def test_merging_reorders_bind_params + post = Post.first + id_column = Post.columns_hash['id'] + title_column = Post.columns_hash['title'] + + bv = Post.connection.substitute_at id_column, 0 + + right = Post.where(id: bv) + right.bind_values += [[id_column, post.id]] + + left = Post.where(title: bv) + left.bind_values += [[title_column, post.title]] + + merged = left.merge(right) + assert_equal post, merged.first end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 741827446d..c085663efb 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -190,6 +190,8 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition elsif current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter) assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition + elsif current_adapter?(:SQLite3Adapter) && ActiveRecord::Base.connection.supports_partial_index? + assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "rating > 10"', index_definition else assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index"', index_definition end diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index 72c9787b84..9dc26cfd4d 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -266,6 +266,63 @@ class NamedScopingTest < ActiveRecord::TestCase assert_equal 'lifo', topic.author_name end + def test_reserved_scope_names + klass = Class.new(ActiveRecord::Base) do + self.table_name = "topics" + + scope :approved, -> { where(approved: true) } + + class << self + public + def pub; end + + private + def pri; end + + protected + def pro; end + end + end + + subklass = Class.new(klass) + + conflicts = [ + :create, # public class method on AR::Base + :relation, # private class method on AR::Base + :new, # redefined class method on AR::Base + :all, # a default scope + ] + + non_conflicts = [ + :find_by_title, # dynamic finder method + :approved, # existing scope + :pub, # existing public class method + :pri, # existing private class method + :pro, # existing protected class method + :open, # a ::Kernel method + ] + + conflicts.each do |name| + assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do + klass.class_eval { scope name, ->{ where(approved: true) } } + end + + assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do + subklass.class_eval { scope name, ->{ where(approved: true) } } + end + end + + non_conflicts.each do |name| + assert_nothing_raised do + klass.class_eval { scope name, ->{ where(approved: true) } } + end + + assert_nothing_raised do + subklass.class_eval { scope name, ->{ where(approved: true) } } + end + end + end + # Method delegation for scope names which look like /\A[a-zA-Z_]\w*[!?]?\z/ # has been done by evaluating a string with a plain def statement. For scope # names which contain spaces this approach doesn't work. @@ -344,13 +401,13 @@ class NamedScopingTest < ActiveRecord::TestCase end def test_scopes_batch_finders - assert_equal 3, Topic.approved.count + assert_equal 4, Topic.approved.count - assert_queries(4) do + assert_queries(5) do Topic.approved.find_each(:batch_size => 1) {|t| assert t.approved? } end - assert_queries(2) do + assert_queries(3) do Topic.approved.find_in_batches(:batch_size => 2) do |group| group.each {|t| assert t.approved? } end @@ -366,7 +423,7 @@ class NamedScopingTest < ActiveRecord::TestCase def test_scopes_on_relations # Topic.replied approved_topics = Topic.all.approved.order('id DESC') - assert_equal topics(:fourth), approved_topics.first + assert_equal topics(:fifth), approved_topics.first replied_approved_topics = approved_topics.replied assert_equal topics(:third), replied_approved_topics.first diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index 0c9f7ccd55..6f632b4d8d 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -162,4 +162,8 @@ class StoreTest < ActiveRecord::TestCase assert_equal [:color], first_model.stored_attributes[:data] assert_equal [:width, :height], second_model.stored_attributes[:data] end + + test "YAML coder initializes the store when a Nil value is given" do + assert_equal({}, @john.params) + end end diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index a9a114328c..bf9e14fa4f 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -129,11 +129,22 @@ module ActiveRecord ) end - def test_creates_test_database_when_environment_is_database + def test_creates_test_and_development_databases_when_env_was_not_specified ActiveRecord::Tasks::DatabaseTasks.expects(:create). with('database' => 'dev-db') ActiveRecord::Tasks::DatabaseTasks.expects(:create). with('database' => 'test-db') + ENV.expects(:[]).with('RAILS_ENV').returns(nil) + + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new('development') + ) + end + + def test_creates_only_development_database_when_rails_env_is_development + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with('database' => 'dev-db') + ENV.expects(:[]).with('RAILS_ENV').returns('development') ActiveRecord::Tasks::DatabaseTasks.create_current( ActiveSupport::StringInquirer.new('development') @@ -143,7 +154,7 @@ module ActiveRecord def test_establishes_connection_for_the_given_environment ActiveRecord::Tasks::DatabaseTasks.stubs(:create).returns true - ActiveRecord::Base.expects(:establish_connection).with('development') + ActiveRecord::Base.expects(:establish_connection).with(:development) ActiveRecord::Tasks::DatabaseTasks.create_current( ActiveSupport::StringInquirer.new('development') @@ -239,11 +250,22 @@ module ActiveRecord ) end - def test_creates_test_database_when_environment_is_database + def test_drops_test_and_development_databases_when_env_was_not_specified ActiveRecord::Tasks::DatabaseTasks.expects(:drop). with('database' => 'dev-db') ActiveRecord::Tasks::DatabaseTasks.expects(:drop). with('database' => 'test-db') + ENV.expects(:[]).with('RAILS_ENV').returns(nil) + + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new('development') + ) + end + + def test_drops_only_development_database_when_rails_env_is_development + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with('database' => 'dev-db') + ENV.expects(:[]).with('RAILS_ENV').returns('development') ActiveRecord::Tasks::DatabaseTasks.drop_current( ActiveSupport::StringInquirer.new('development') diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index 5644a35385..7e7d95841b 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -1,9 +1,11 @@ require "cases/helper" +require 'models/owner' +require 'models/pet' require 'models/topic' class TransactionCallbacksTest < ActiveRecord::TestCase self.use_transactional_fixtures = false - fixtures :topics + fixtures :topics, :owners, :pets class ReplyWithCallbacks < ActiveRecord::Base self.table_name = :topics @@ -28,14 +30,14 @@ class TransactionCallbacksTest < ActiveRecord::TestCase has_many :replies, class_name: "ReplyWithCallbacks", foreign_key: "parent_id" - after_commit{|record| record.send(:do_after_commit, nil)} - after_commit(:on => :create){|record| record.send(:do_after_commit, :create)} - after_commit(:on => :update){|record| record.send(:do_after_commit, :update)} - after_commit(:on => :destroy){|record| record.send(:do_after_commit, :destroy)} - after_rollback{|record| record.send(:do_after_rollback, nil)} - after_rollback(:on => :create){|record| record.send(:do_after_rollback, :create)} - after_rollback(:on => :update){|record| record.send(:do_after_rollback, :update)} - after_rollback(:on => :destroy){|record| record.send(:do_after_rollback, :destroy)} + after_commit { |record| record.do_after_commit(nil) } + after_commit(on: :create) { |record| record.do_after_commit(:create) } + after_commit(on: :update) { |record| record.do_after_commit(:update) } + after_commit(on: :destroy) { |record| record.do_after_commit(:destroy) } + after_rollback { |record| record.do_after_rollback(nil) } + after_rollback(on: :create) { |record| record.do_after_rollback(:create) } + after_rollback(on: :update) { |record| record.do_after_rollback(:update) } + after_rollback(on: :destroy) { |record| record.do_after_rollback(:destroy) } def history @history ||= [] @@ -65,7 +67,7 @@ class TransactionCallbacksTest < ActiveRecord::TestCase end def setup - @first, @second = TopicWithCallbacks.find(1, 3).sort_by { |t| t.id } + @first = TopicWithCallbacks.find(1) end def test_call_after_commit_after_transaction_commits @@ -77,40 +79,25 @@ class TransactionCallbacksTest < ActiveRecord::TestCase end def test_only_call_after_commit_on_update_after_transaction_commits_for_existing_record - @first.after_commit_block(:create){|r| r.history << :commit_on_create} - @first.after_commit_block(:update){|r| r.history << :commit_on_update} - @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} - @first.after_rollback_block(:create){|r| r.history << :rollback_on_create} - @first.after_rollback_block(:update){|r| r.history << :rollback_on_update} - @first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy} + add_transaction_execution_blocks @first @first.save! assert_equal [:commit_on_update], @first.history end def test_only_call_after_commit_on_destroy_after_transaction_commits_for_destroyed_record - @first.after_commit_block(:create){|r| r.history << :commit_on_create} - @first.after_commit_block(:update){|r| r.history << :commit_on_update} - @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} - @first.after_rollback_block(:create){|r| r.history << :rollback_on_create} - @first.after_rollback_block(:update){|r| r.history << :rollback_on_update} - @first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy} + add_transaction_execution_blocks @first @first.destroy assert_equal [:commit_on_destroy], @first.history end def test_only_call_after_commit_on_create_after_transaction_commits_for_new_record - @new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today) - @new_record.after_commit_block(:create){|r| r.history << :commit_on_create} - @new_record.after_commit_block(:update){|r| r.history << :commit_on_update} - @new_record.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} - @new_record.after_rollback_block(:create){|r| r.history << :rollback_on_create} - @new_record.after_rollback_block(:update){|r| r.history << :rollback_on_update} - @new_record.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy} - - @new_record.save! - assert_equal [:commit_on_create], @new_record.history + new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today) + add_transaction_execution_blocks new_record + + new_record.save! + assert_equal [:commit_on_create], new_record.history end def test_only_call_after_commit_on_create_after_transaction_commits_for_new_record_if_create_succeeds_creating_through_association @@ -120,6 +107,13 @@ class TransactionCallbacksTest < ActiveRecord::TestCase assert_equal [], reply.history end + def test_only_call_after_commit_on_update_after_transaction_commits_for_existing_record_on_touch + add_transaction_execution_blocks @first + + @first.touch + assert_equal [:commit_on_update], @first.history + end + def test_call_after_rollback_after_transaction_rollsback @first.after_commit_block{|r| r.history << :after_commit} @first.after_rollback_block{|r| r.history << :after_rollback} @@ -133,12 +127,7 @@ class TransactionCallbacksTest < ActiveRecord::TestCase end def test_only_call_after_rollback_on_update_after_transaction_rollsback_for_existing_record - @first.after_commit_block(:create){|r| r.history << :commit_on_create} - @first.after_commit_block(:update){|r| r.history << :commit_on_update} - @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} - @first.after_rollback_block(:create){|r| r.history << :rollback_on_create} - @first.after_rollback_block(:update){|r| r.history << :rollback_on_update} - @first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy} + add_transaction_execution_blocks @first Topic.transaction do @first.save! @@ -148,13 +137,19 @@ class TransactionCallbacksTest < ActiveRecord::TestCase assert_equal [:rollback_on_update], @first.history end + def test_only_call_after_rollback_on_update_after_transaction_rollsback_for_existing_record_on_touch + add_transaction_execution_blocks @first + + Topic.transaction do + @first.touch + raise ActiveRecord::Rollback + end + + assert_equal [:rollback_on_update], @first.history + end + def test_only_call_after_rollback_on_destroy_after_transaction_rollsback_for_destroyed_record - @first.after_commit_block(:create){|r| r.history << :commit_on_create} - @first.after_commit_block(:update){|r| r.history << :commit_on_update} - @first.after_commit_block(:destroy){|r| r.history << :commit_on_update} - @first.after_rollback_block(:create){|r| r.history << :rollback_on_create} - @first.after_rollback_block(:update){|r| r.history << :rollback_on_update} - @first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy} + add_transaction_execution_blocks @first Topic.transaction do @first.destroy @@ -165,20 +160,15 @@ class TransactionCallbacksTest < ActiveRecord::TestCase end def test_only_call_after_rollback_on_create_after_transaction_rollsback_for_new_record - @new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today) - @new_record.after_commit_block(:create){|r| r.history << :commit_on_create} - @new_record.after_commit_block(:update){|r| r.history << :commit_on_update} - @new_record.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} - @new_record.after_rollback_block(:create){|r| r.history << :rollback_on_create} - @new_record.after_rollback_block(:update){|r| r.history << :rollback_on_update} - @new_record.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy} + new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today) + add_transaction_execution_blocks new_record Topic.transaction do - @new_record.save! + new_record.save! raise ActiveRecord::Rollback end - assert_equal [:rollback_on_create], @new_record.history + assert_equal [:rollback_on_create], new_record.history end def test_call_after_rollback_when_commit_fails @@ -205,23 +195,24 @@ class TransactionCallbacksTest < ActiveRecord::TestCase @first.after_rollback_block{|r| r.rollbacks(1)} @first.after_commit_block{|r| r.commits(1)} - def @second.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end - def @second.commits(i=0); @commits ||= 0; @commits += i if i; end - @second.after_rollback_block{|r| r.rollbacks(1)} - @second.after_commit_block{|r| r.commits(1)} + second = TopicWithCallbacks.find(3) + def second.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end + def second.commits(i=0); @commits ||= 0; @commits += i if i; end + second.after_rollback_block{|r| r.rollbacks(1)} + second.after_commit_block{|r| r.commits(1)} Topic.transaction do @first.save! Topic.transaction(:requires_new => true) do - @second.save! + second.save! raise ActiveRecord::Rollback end end assert_equal 1, @first.commits assert_equal 0, @first.rollbacks - assert_equal 0, @second.commits - assert_equal 1, @second.rollbacks + assert_equal 0, second.commits + assert_equal 1, second.rollbacks end def test_only_call_after_rollback_on_records_rolled_back_to_a_savepoint_when_release_savepoint_fails @@ -252,33 +243,61 @@ class TransactionCallbacksTest < ActiveRecord::TestCase def @first.last_after_transaction_error; @last_transaction_error; end @first.after_commit_block{|r| r.last_after_transaction_error = :commit; raise "fail!";} @first.after_rollback_block{|r| r.last_after_transaction_error = :rollback; raise "fail!";} - @second.after_commit_block{|r| r.history << :after_commit} - @second.after_rollback_block{|r| r.history << :after_rollback} + + second = TopicWithCallbacks.find(3) + second.after_commit_block{|r| r.history << :after_commit} + second.after_rollback_block{|r| r.history << :after_rollback} Topic.transaction do @first.save! - @second.save! + second.save! end assert_equal :commit, @first.last_after_transaction_error - assert_equal [:after_commit], @second.history + assert_equal [:after_commit], second.history - @second.history.clear + second.history.clear Topic.transaction do @first.save! - @second.save! + second.save! raise ActiveRecord::Rollback end assert_equal :rollback, @first.last_after_transaction_error - assert_equal [:after_rollback], @second.history + assert_equal [:after_rollback], second.history end def test_after_rollback_callbacks_should_validate_on_condition - assert_raise(ArgumentError) { Topic.send(:after_rollback, :on => :save) } + assert_raise(ArgumentError) { Topic.after_rollback(on: :save) } end def test_after_commit_callbacks_should_validate_on_condition - assert_raise(ArgumentError) { Topic.send(:after_commit, :on => :save) } + assert_raise(ArgumentError) { Topic.after_commit(on: :save) } end + + def test_saving_a_record_with_a_belongs_to_that_specifies_touching_the_parent_should_call_callbacks_on_the_parent_object + pet = Pet.first + owner = pet.owner + flag = false + + owner.on_after_commit do + flag = true + end + + pet.name = "Fluffy the Third" + pet.save + + assert flag + end + + private + + def add_transaction_execution_blocks(record) + record.after_commit_block(:create) { |r| r.history << :commit_on_create } + record.after_commit_block(:update) { |r| r.history << :commit_on_update } + record.after_commit_block(:destroy) { |r| r.history << :commit_on_destroy } + record.after_rollback_block(:create) { |r| r.history << :rollback_on_create } + record.after_rollback_block(:update) { |r| r.history << :rollback_on_update } + record.after_rollback_block(:destroy) { |r| r.history << :rollback_on_destroy } + end end class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/transaction_isolation_test.rb b/activerecord/test/cases/transaction_isolation_test.rb index 84c16fb109..f89c26532d 100644 --- a/activerecord/test/cases/transaction_isolation_test.rb +++ b/activerecord/test/cases/transaction_isolation_test.rb @@ -28,8 +28,8 @@ if ActiveRecord::Base.connection.supports_transaction_isolation? end setup do - Tag.establish_connection 'arunit' - Tag2.establish_connection 'arunit' + Tag.establish_connection :arunit + Tag2.establish_connection :arunit Tag.destroy_all end diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 89dab16975..1664f1a096 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -430,17 +430,26 @@ class TransactionTest < ActiveRecord::TestCase end def test_restore_active_record_state_for_all_records_in_a_transaction + topic_without_callbacks = Class.new(ActiveRecord::Base) do + self.table_name = 'topics' + end + topic_1 = Topic.new(:title => 'test_1') topic_2 = Topic.new(:title => 'test_2') + topic_3 = topic_without_callbacks.new(:title => 'test_3') + Topic.transaction do assert topic_1.save assert topic_2.save + assert topic_3.save @first.save @second.destroy assert topic_1.persisted?, 'persisted' assert_not_nil topic_1.id assert topic_2.persisted?, 'persisted' assert_not_nil topic_2.id + assert topic_3.persisted?, 'persisted' + assert_not_nil topic_3.id assert @first.persisted?, 'persisted' assert_not_nil @first.id assert @second.destroyed?, 'destroyed' @@ -451,6 +460,8 @@ class TransactionTest < ActiveRecord::TestCase assert_nil topic_1.id assert !topic_2.persisted?, 'not persisted' assert_nil topic_2.id + assert !topic_3.persisted?, 'not persisted' + assert_nil topic_3.id assert @first.persisted?, 'persisted' assert_not_nil @first.id assert !@second.destroyed?, 'not destroyed' diff --git a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb index a73c3bf1af..13d4d85afa 100644 --- a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb @@ -3,7 +3,7 @@ require 'models/topic' class I18nGenerateMessageValidationTest < ActiveRecord::TestCase def setup - Topic.reset_callbacks(:validate) + Topic.clear_validators! @topic = Topic.new I18n.backend = I18n::Backend::Simple.new end diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb index 1de8934406..3790d3c8cf 100644 --- a/activerecord/test/cases/validations/presence_validation_test.rb +++ b/activerecord/test/cases/validations/presence_validation_test.rb @@ -3,6 +3,8 @@ require "cases/helper" require 'models/man' require 'models/face' require 'models/interest' +require 'models/speedometer' +require 'models/dashboard' class PresenceValidationTest < ActiveRecord::TestCase class Boy < Man; end @@ -48,4 +50,18 @@ class PresenceValidationTest < ActiveRecord::TestCase i2.mark_for_destruction assert b.invalid? end + + def test_validates_presence_doesnt_convert_to_array + Speedometer.validates_presence_of :dashboard + + dash = Dashboard.new + + # dashboard has to_a method + def dash.to_a; ['(/)', '(\)']; end + + s = Speedometer.new + s.dashboard = dash + + assert_nothing_raised { s.valid? } + end end diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index 5f55696c1d..de618902aa 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -56,13 +56,11 @@ class ValidationsTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordInvalid) { WrongReply.create! } assert_raise(ActiveRecord::RecordInvalid) { WrongReply.new.save! } - begin - r = WrongReply.new + r = WrongReply.new + invalid = assert_raise ActiveRecord::RecordInvalid do r.save! - flunk - rescue ActiveRecord::RecordInvalid => invalid - assert_equal r, invalid.record end + assert_equal r, invalid.record end def test_exception_on_create_bang_many diff --git a/activerecord/test/fixtures/companies.yml b/activerecord/test/fixtures/companies.yml index 0766e92027..ab9d5378ad 100644 --- a/activerecord/test/fixtures/companies.yml +++ b/activerecord/test/fixtures/companies.yml @@ -57,3 +57,11 @@ odegy: id: 9 name: Odegy type: ExclusivelyDependentFirm + +another_first_firm_client: + id: 11 + type: Client + firm_id: 1 + client_of: 1 + name: Apex + firm_name: 37signals diff --git a/activerecord/test/fixtures/topics.yml b/activerecord/test/fixtures/topics.yml index 2b042bd135..bf049abbf1 100644 --- a/activerecord/test/fixtures/topics.yml +++ b/activerecord/test/fixtures/topics.yml @@ -40,3 +40,10 @@ fourth: type: Reply parent_id: 3 +fifth: + id: 5 + title: The Fifth Topic of the day + author_name: Jason + written_on: 2013-07-13t12:11:00.0099+01:00 + content: Omakase + approved: true diff --git a/activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb b/activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb new file mode 100644 index 0000000000..9d46485a31 --- /dev/null +++ b/activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb @@ -0,0 +1,8 @@ +class MigrationVersionCheck < ActiveRecord::Migration + def self.up + raise "incorrect migration version" unless version == 20131219224947 + end + + def self.down + end +end diff --git a/activerecord/test/models/admin/user.rb b/activerecord/test/models/admin/user.rb index 4c3b71e8f9..48a110bd23 100644 --- a/activerecord/test/models/admin/user.rb +++ b/activerecord/test/models/admin/user.rb @@ -14,6 +14,7 @@ class Admin::User < ActiveRecord::Base end belongs_to :account + store :params, accessors: [ :token ], coder: YAML store :settings, :accessors => [ :color, :homepage ] store_accessor :settings, :favorite_food store :preferences, :accessors => [ :remember_login ] diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 794d1af43d..c197951c71 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -29,7 +29,7 @@ class Author < ActiveRecord::Base has_many :thinking_posts, -> { where(:title => 'So I was thinking') }, :dependent => :delete_all, :class_name => 'Post' has_many :welcome_posts, -> { where(:title => 'Welcome to the weblog') }, :class_name => 'Post' - has_many :welcome_posts_with_comment, + has_many :welcome_posts_with_one_comment, -> { where(title: 'Welcome to the weblog').where('comments_count = ?', 1) }, class_name: 'Post' has_many :welcome_posts_with_comments, diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb index 4cb2c7606b..2170018068 100644 --- a/activerecord/test/models/book.rb +++ b/activerecord/test/models/book.rb @@ -9,6 +9,7 @@ class Book < ActiveRecord::Base enum status: [:proposed, :written, :published] enum read_status: {unread: 0, reading: 2, read: 3} + enum nullable_status: [:single, :married] def published! super diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb index c4a15a79e2..db0f93f63b 100644 --- a/activerecord/test/models/car.rb +++ b/activerecord/test/models/car.rb @@ -15,7 +15,6 @@ class Car < ActiveRecord::Base scope :incl_engines, -> { includes(:engines) } scope :order_using_new_style, -> { order('name asc') } - end class CoolCar < Car diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index a26de55758..2e2d8a0d37 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -36,6 +36,10 @@ class Developer < ActiveRecord::Base end has_and_belongs_to_many :special_projects, :join_table => 'developers_projects', :association_foreign_key => 'project_id' + has_and_belongs_to_many :sym_special_projects, + :join_table => :developers_projects, + :association_foreign_key => 'project_id', + :class_name => 'SpecialProject' has_many :audit_logs has_many :contracts diff --git a/activerecord/test/models/owner.rb b/activerecord/test/models/owner.rb index 1c7ed4aa3e..cf24502d3a 100644 --- a/activerecord/test/models/owner.rb +++ b/activerecord/test/models/owner.rb @@ -2,4 +2,21 @@ class Owner < ActiveRecord::Base self.primary_key = :owner_id has_many :pets, -> { order 'pets.name desc' } has_many :toys, :through => :pets + + after_commit :execute_blocks + + def blocks + @blocks ||= [] + end + + def on_after_commit(&block) + blocks << block + end + + def execute_blocks + blocks.each do |block| + block.call(self) + end + @blocks = [] + end end diff --git a/activerecord/test/models/shop.rb b/activerecord/test/models/shop.rb index 81414227ea..607a0a5b41 100644 --- a/activerecord/test/models/shop.rb +++ b/activerecord/test/models/shop.rb @@ -5,6 +5,11 @@ module Shop class Product < ActiveRecord::Base has_many :variants, :dependent => :delete_all + belongs_to :type + + class Type < ActiveRecord::Base + has_many :products + end end class Variant < ActiveRecord::Base diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index 6b7012a172..a86a188bcf 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -1,6 +1,6 @@ ActiveRecord::Schema.define do - %w(postgresql_ranges postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings postgresql_uuids postgresql_ltrees + %w(postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings postgresql_uuids postgresql_ltrees postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent postgresql_json_data_type).each do |table_name| execute "DROP TABLE IF EXISTS #{quote_table_name table_name}" end @@ -74,18 +74,6 @@ _SQL ); _SQL - execute <<_SQL if supports_ranges? - CREATE TABLE postgresql_ranges ( - id SERIAL PRIMARY KEY, - date_range daterange, - num_range numrange, - ts_range tsrange, - tstz_range tstzrange, - int4_range int4range, - int8_range int8range - ); -_SQL - execute <<_SQL CREATE TABLE postgresql_tsvectors ( id SERIAL PRIMARY KEY, @@ -221,4 +209,3 @@ _SQL t.text :text, limit: 100_000 end end - diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 92e2e4d409..99a53434f6 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -45,6 +45,7 @@ ActiveRecord::Schema.define do t.string :preferences, null: true, default: '', limit: 1024 t.string :json_data, null: true, limit: 1024 t.string :json_data_empty, null: true, default: "", limit: 1024 + t.text :params t.references :account end @@ -96,6 +97,7 @@ ActiveRecord::Schema.define do t.column :name, :string t.column :status, :integer, default: 0 t.column :read_status, :integer, default: 0 + t.column :nullable_status, :integer end create_table :booleans, force: true do |t| @@ -245,6 +247,7 @@ ActiveRecord::Schema.define do t.integer :trainer_id t.integer :breeder_id t.integer :dog_lover_id + t.string :alias end create_table :edges, force: true, id: false do |t| @@ -555,9 +558,14 @@ ActiveRecord::Schema.define do create_table :products, force: true do |t| t.references :collection + t.references :type t.string :name end + create_table :product_types, force: true do |t| + t.string :name + end + create_table :projects, force: true do |t| t.string :name t.string :type diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb index 196b3a9493..d11fd9cfc1 100644 --- a/activerecord/test/support/connection.rb +++ b/activerecord/test/support/connection.rb @@ -15,7 +15,7 @@ module ARTest puts "Using #{connection_name}" ActiveRecord::Base.logger = ActiveSupport::Logger.new("debug.log", 0, 100 * 1024 * 1024) ActiveRecord::Base.configurations = connection_config - ActiveRecord::Base.establish_connection 'arunit' - ARUnit2Model.establish_connection 'arunit2' + ActiveRecord::Base.establish_connection :arunit + ARUnit2Model.establish_connection :arunit2 end end diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index ea4aaff610..95bf5601f2 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,7 +1,141 @@ -* Ensure `config.i18n.enforce_available_locales` is set before any other - configuration option. +* Add `ActiveSupport::JSON::Encoding.time_precision` as a way to configure the + precision of encoded time values: - *Yves Senn* + Time.utc(2000, 1, 1).as_json # => "2000-01-01T00:00:00.000Z" + ActiveSupport::JSON::Encoding.time_precision = 0 + Time.utc(2000, 1, 1).as_json # => "2000-01-01T00:00:00Z" + + *Parker Selbert* + +* Maintain the current timezone when calling `change` during DST overlap + + Currently if a time is changed during DST overlap in the autumn then the method + `period_for_local` will return the DST period. However if the original time is + not DST then this can be surprising and is not what is generally wanted. This + commit changes that behavior to maintain the current period if it's in the list + of periods returned by `periods_for_local`. + + Fixes #12163. + + *Andrew White* + +* Added `Hash#compact` and `Hash#compact!` for removing items with nil value from hash. + + *Celestino Gomes* + +* Maintain proleptic gregorian in Time#advance + + `Time#advance` uses `Time#to_date` and `Date#advance` to calculate a new date. + The `Date` object returned by `Time#to_date` is constructed with the assumption + that the `Time` object represents a proleptic gregorian date, but it is + configured to observe the default julian calendar reform date (2299161j) + for purposes of calculating month, date and year: + + Time.new(1582, 10, 4).to_date.to_s # => "1582-09-24" + Time.new(1582, 10, 4).to_date.gregorian.to_s # => "1582-10-04" + + This patch ensures that when the intermediate `Date` object is advanced + to yield a new `Date` object, that the `Time` object for return is constructed + with a proleptic gregorian month, date and year. + + *Riley Lynch* + +* `MemCacheStore` should only accept a `Dalli::Client`, or create one. + + *arthurnn* + +* Don't lazy load the `tzinfo` library as it causes problems on Windows. + + Fixes #13553. + + *Andrew White* + +* Use `remove_possible_method` instead of `remove_method` to avoid + a `NameError` to be thrown on FreeBSD with the `Date` object. + + *Rafael Mendonça França*, *Robin Dupret* + +* `blank?` and `present?` commit to return singletons. + + *Xavier Noria*, *Pavel Pravosud* + +* Fixed Float related error in NumberHelper with large precisions. + + Before: + + ActiveSupport::NumberHelper.number_to_rounded '3.14159', precision: 50 + #=> "3.14158999999999988261834005243144929409027099609375" + + After: + + ActiveSupport::NumberHelper.number_to_rounded '3.14159', precision: 50 + #=> "3.14159000000000000000000000000000000000000000000000" + + *Kenta Murata*, *Akira Matsuda* + +* Default the new `I18n.enforce_available_locales` config to `true`, meaning + `I18n` will make sure that all locales passed to it must be declared in the + `available_locales` list. + + To disable it add the following configuration to your application: + + config.i18n.enforce_available_locales = false + + This also ensures I18n configuration is properly initialized taking the new + option into account, to avoid their deprecations while booting up the app. + + *Carlos Antonio da Silva*, *Yves Senn* + +* Introduce Module#concerning: a natural, low-ceremony way to separate + responsibilities within a class. + + Imported from https://github.com/37signals/concerning#readme + + class Todo < ActiveRecord::Base + concerning :EventTracking do + included do + has_many :events + end + + def latest_event + ... + end + + private + def some_internal_method + ... + end + end + + concerning :Trashable do + def trashed? + ... + end + + def latest_event + super some_option: true + end + end + end + + is equivalent to defining these modules inline, extending them into + concerns, then mixing them in to the class. + + Inline concerns tame "junk drawer" classes that intersperse many unrelated + class-level declarations, public instance methods, and private + implementation. Coalesce related bits and give them definition. + These are a stepping stone toward future growth & refactoring. + + When to move on from an inline concern: + * Encapsulating state? Extract collaborator object. + * Encompassing more public behavior or implementation? Move to separate file. + * Sharing behavior among classes? Move to separate file. + + *Jeremy Kemper* + +* Fix file descriptor being leaked on each call to `Kernel.silence_stream`. + + *Mario Visic* * Added `Date#all_week/month/quarter/year` for generating date ranges. @@ -259,19 +393,21 @@ The value of `PER_ENTRY_OVERHEAD` is 240 bytes based on an [empirical estimation](https://gist.github.com/ssimeonov/6047200) for 64-bit MRI on - 1.9.3 and 2.0. GH#11512 + 1.9.3 and 2.0. + + Fixes #11512. *Simeon Simeonov* * Only raise `Module::DelegationError` if it's the source of the exception. - Fixes #10559 + Fixes #10559. *Andrew White* * Make `Time.at_with_coercion` retain the second fraction and return local time. - Fixes #11350 + Fixes #11350. *Neer Friedman*, *Andrew White* @@ -288,7 +424,7 @@ *Arun Agrawal* -* Remove deprecated `DateTime.local_offset` in favor of `DateTime.civil_from_fromat`. +* Remove deprecated `DateTime.local_offset` in favor of `DateTime.civil_from_format`. *Arun Agrawal* @@ -337,21 +473,21 @@ * Fix return value from `BacktraceCleaner#noise` when the cleaner is configured with multiple silencers. - Fixes #11030 + Fixes #11030. *Mark J. Titorenko* * `HashWithIndifferentAccess#select` now returns a `HashWithIndifferentAccess` instance instead of a `Hash` instance. - Fixes #10723 + Fixes #10723. *Albert Llop* * Add `DateTime#usec` and `DateTime#nsec` so that `ActiveSupport::TimeWithZone` keeps sub-second resolution when wrapping a `DateTime` value. - Fixes #10855 + Fixes #10855. *Andrew White* @@ -367,7 +503,7 @@ * Prevent side effects to hashes inside arrays when `Hash#with_indifferent_access` is called. - Fixes #10526 + Fixes #10526. *Yves Senn* diff --git a/activesupport/MIT-LICENSE b/activesupport/MIT-LICENSE index 6aeeb7132d..d06d4f3b2d 100644 --- a/activesupport/MIT-LICENSE +++ b/activesupport/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2005-2013 David Heinemeier Hansson +Copyright (c) 2005-2014 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index 4fdc697a15..f3625e8b79 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -20,9 +20,9 @@ Gem::Specification.new do |s| s.rdoc_options.concat ['--encoding', 'UTF-8'] - s.add_dependency 'i18n', '~> 0.6', '>= 0.6.4' + s.add_dependency 'i18n', '~> 0.6', '>= 0.6.9' s.add_dependency 'json', '~> 1.7', '>= 1.7.7' s.add_dependency 'tzinfo', '~> 1.1' - s.add_dependency 'minitest', '~> 5.0' + s.add_dependency 'minitest', '~> 5.1' s.add_dependency 'thread_safe','~> 0.1' end diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index a40c6b559c..ab0054b339 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2005-2013 David Heinemeier Hansson +# Copyright (c) 2005-2014 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/activesupport/lib/active_support/backtrace_cleaner.rb b/activesupport/lib/active_support/backtrace_cleaner.rb index c88ae3e661..d58578b7bc 100644 --- a/activesupport/lib/active_support/backtrace_cleaner.rb +++ b/activesupport/lib/active_support/backtrace_cleaner.rb @@ -22,7 +22,7 @@ module ActiveSupport # <tt>BacktraceCleaner#remove_silencers!</tt>, which will restore the # backtrace to a pristine state. If you need to reconfigure an existing # BacktraceCleaner so that it does not filter or modify the paths of any lines - # of the backtrace, you can call <tt>BacktraceCleaner#remove_filters!<tt> + # of the backtrace, you can call <tt>BacktraceCleaner#remove_filters!</tt> # These two methods will give you a completely untouched backtrace. # # Inspired by the Quiet Backtrace gem by Thoughtbot. diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index d584d50da8..53154aef27 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -401,7 +401,7 @@ module ActiveSupport end end - # Return +true+ if the cache contains an entry for the given key. + # Returns +true+ if the cache contains an entry for the given key. # # Options are passed to the underlying cache implementation. def exist?(name, options = nil) diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index 5cd6065077..8ed60aebac 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -22,15 +22,15 @@ module ActiveSupport extend Strategy::LocalCache end - # Deletes all items from the cache. In this case it deletes all the entries in the specified - # file store directory except for .gitkeep. Be careful which directory is specified in your + # Deletes all items from the cache. In this case it deletes all the entries in the specified + # file store directory except for .gitkeep. Be careful which directory is specified in your # config file when using +FileStore+ because everything in that directory will be deleted. def clear(options = nil) root_dirs = Dir.entries(cache_path).reject {|f| (EXCLUDED_DIRS + [".gitkeep"]).include?(f)} FileUtils.rm_r(root_dirs.collect{|f| File.join(cache_path, f)}) end - # Premptively iterates through all stored keys and removes the ones which have expired. + # Preemptively iterates through all stored keys and removes the ones which have expired. def cleanup(options = nil) options = merged_options(options) search_dir(cache_path) do |fname| @@ -43,33 +43,13 @@ module ActiveSupport # Increments an already existing integer value that is stored in the cache. # If the key is not found nothing is done. def increment(name, amount = 1, options = nil) - file_name = key_file_path(namespaced_key(name, options)) - lock_file(file_name) do - options = merged_options(options) - if num = read(name, options) - num = num.to_i + amount - write(name, num, options) - num - else - nil - end - end + modify_value(name, amount, options) end # Decrements an already existing integer value that is stored in the cache. # If the key is not found nothing is done. def decrement(name, amount = 1, options = nil) - file_name = key_file_path(namespaced_key(name, options)) - lock_file(file_name) do - options = merged_options(options) - if num = read(name, options) - num = num.to_i - amount - write(name, num, options) - num - else - nil - end - end + modify_value(name, -amount, options) end def delete_matched(matcher, options = nil) @@ -184,6 +164,22 @@ module ActiveSupport end end end + + # Modifies the amount of an already existing integer value that is stored in the cache. + # If the key is not found nothing is done. + def modify_value(name, amount, options) + file_name = key_file_path(namespaced_key(name, options)) + + lock_file(file_name) do + options = merged_options(options) + + if num = read(name, options) + num = num.to_i + amount + write(name, num, options) + num + end + end + end end end end diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb index 512296554f..61b4f0b8b0 100644 --- a/activesupport/lib/active_support/cache/mem_cache_store.rb +++ b/activesupport/lib/active_support/cache/mem_cache_store.rb @@ -41,17 +41,15 @@ module ActiveSupport # # If no addresses are specified, then MemCacheStore will connect to # localhost port 11211 (the default memcached port). - # - # Instead of addresses one can pass in a MemCache-like object. For example: - # - # require 'memcached' # gem install memcached; uses C bindings to libmemcached - # ActiveSupport::Cache::MemCacheStore.new(Memcached::Rails.new("localhost:11211")) def initialize(*addresses) addresses = addresses.flatten options = addresses.extract_options! super(options) - if addresses.first.respond_to?(:get) + unless [String, Dalli::Client, NilClass].include?(addresses.first.class) + raise ArgumentError, "First argument must be an empty array, an array of hosts or a Dalli::Client instance." + end + if addresses.first.is_a?(Dalli::Client) @data = addresses.first else mem_cache_options = options.dup @@ -87,7 +85,7 @@ module ActiveSupport instrument(:increment, name, :amount => amount) do @data.incr(escape_key(namespaced_key(name, options)), amount) end - rescue Dalli::DalliError + rescue Dalli::DalliError => e logger.error("DalliError (#{e}): #{e.message}") if logger nil end @@ -101,7 +99,7 @@ module ActiveSupport instrument(:decrement, name, :amount => amount) do @data.decr(escape_key(namespaced_key(name, options)), amount) end - rescue Dalli::DalliError + rescue Dalli::DalliError => e logger.error("DalliError (#{e}): #{e.message}") if logger nil end diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb index 34ac91334a..8a0523d0e2 100644 --- a/activesupport/lib/active_support/cache/memory_store.rb +++ b/activesupport/lib/active_support/cache/memory_store.rb @@ -36,7 +36,7 @@ module ActiveSupport end end - # Premptively iterates through all stored keys and removes the ones which have expired. + # Preemptively iterates through all stored keys and removes the ones which have expired. def cleanup(options = nil) options = merged_options(options) instrument(:cleanup, :size => @data.size) do diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb index cea7eee924..4eaf57f385 100644 --- a/activesupport/lib/active_support/cache/strategy/local_cache.rb +++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/object/duplicable' require 'active_support/core_ext/string/inflections' +require 'rack/body_proxy' module ActiveSupport module Cache @@ -83,9 +84,14 @@ module ActiveSupport def call(env) LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new) - @app.call(env) - ensure + response = @app.call(env) + response[2] = ::Rack::BodyProxy.new(response[2]) do + LocalCacheRegistry.set_cache_for(local_cache_key, nil) + end + response + rescue Exception LocalCacheRegistry.set_cache_for(local_cache_key, nil) + raise end end diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index c3aac31323..e14ece7f35 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -7,14 +7,14 @@ require 'active_support/core_ext/kernel/singleton_class' require 'thread' module ActiveSupport - # Callbacks are code hooks that are run at key points in an object's lifecycle. + # Callbacks are code hooks that are run at key points in an object's life cycle. # The typical use case is to have a base class define a set of callbacks # relevant to the other functionality it supplies, so that subclasses can # install callbacks that enhance or modify the base functionality without # needing to override or redefine methods of the base class. # # Mixing in this module allows you to define the events in the object's - # lifecycle that will support callbacks (via +ClassMethods.define_callbacks+), + # life cycle that will support callbacks (via +ClassMethods.define_callbacks+), # set the instance methods, procs, or callback objects to be called (via # +ClassMethods.set_callback+), and run the installed callbacks at the # appropriate times (via +run_callbacks+). @@ -89,7 +89,7 @@ module ActiveSupport private - # A hook invoked everytime a before callback is halted. + # A hook invoked every time a before callback is halted. # This can be overridden in AS::Callback implementors in order # to provide better debugging/logging. def halted_callback_hook(filter) @@ -577,7 +577,7 @@ module ActiveSupport # The callback can be specified as a symbol naming an instance method; as a # proc, lambda, or block; as a string to be instance evaluated; or as an # object that responds to a certain method determined by the <tt>:scope</tt> - # argument to +define_callback+. + # argument to +define_callbacks+. # # If a proc, lambda, or block is given, its body is evaluated in the context # of the current object. It can also optionally accept the current object as @@ -648,7 +648,7 @@ module ActiveSupport self.set_callbacks name, callbacks.dup.clear end - # Define sets of events in the object lifecycle that support callbacks. + # Define sets of events in the object life cycle that support callbacks. # # define_callbacks :validate # define_callbacks :initialize, :save, :destroy diff --git a/activesupport/lib/active_support/configurable.rb b/activesupport/lib/active_support/configurable.rb index e0d39d509f..3dd44e32d8 100644 --- a/activesupport/lib/active_support/configurable.rb +++ b/activesupport/lib/active_support/configurable.rb @@ -107,7 +107,7 @@ module ActiveSupport options = names.extract_options! names.each do |name| - raise NameError.new('invalid config attribute name') unless name =~ /^[_A-Za-z]\w*$/ + raise NameError.new('invalid config attribute name') unless name =~ /\A[_A-Za-z]\w*\z/ reader, reader_line = "def #{name}; config.#{name}; end", __LINE__ writer, writer_line = "def #{name}=(value); config.#{name} = value; end", __LINE__ diff --git a/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb b/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb index ff870f5fd1..c2219beb5a 100644 --- a/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb +++ b/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb @@ -4,20 +4,21 @@ require 'active_support/core_ext/module/remove_method' class Class def superclass_delegating_accessor(name, options = {}) # Create private _name and _name= methods that can still be used if the public - # methods are overridden. This allows - _superclass_delegating_accessor("_#{name}") + # methods are overridden. + _superclass_delegating_accessor("_#{name}", options) - # Generate the public methods name, name=, and name? + # Generate the public methods name, name=, and name?. # These methods dispatch to the private _name, and _name= methods, making them - # overridable + # overridable. singleton_class.send(:define_method, name) { send("_#{name}") } singleton_class.send(:define_method, "#{name}?") { !!send("_#{name}") } singleton_class.send(:define_method, "#{name}=") { |value| send("_#{name}=", value) } - # If an instance_reader is needed, generate methods for name and name= on the - # class itself, so instances will be able to see them - define_method(name) { send("_#{name}") } if options[:instance_reader] != false - define_method("#{name}?") { !!send("#{name}") } if options[:instance_reader] != false + # If an instance_reader is needed, generate public instance methods name and name?. + if options[:instance_reader] != false + define_method(name) { send("_#{name}") } + define_method("#{name}?") { !!send("#{name}") } + end end private diff --git a/activesupport/lib/active_support/core_ext/date/conversions.rb b/activesupport/lib/active_support/core_ext/date/conversions.rb index 6bc8f12176..df419a6e63 100644 --- a/activesupport/lib/active_support/core_ext/date/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date/conversions.rb @@ -1,6 +1,7 @@ require 'date' require 'active_support/inflector/methods' require 'active_support/core_ext/date/zones' +require 'active_support/core_ext/module/remove_method' class Date DATE_FORMATS = { @@ -19,8 +20,10 @@ class Date # Ruby 1.9 has Date#to_time which converts to localtime only. remove_method :to_time - # Ruby 1.9 has Date#xmlschema which converts to a string without the time component. - remove_method :xmlschema + # Ruby 1.9 has Date#xmlschema which converts to a string without the time + # component. This removal may generate an issue on FreeBSD, that's why we + # need to use remove_possible_method here + remove_possible_method :xmlschema # Convert to a formatted string. See DATE_FORMATS for predefined formats. # @@ -37,12 +40,12 @@ class Date # date.to_formatted_s(:rfc822) # => "10 Nov 2007" # date.to_formatted_s(:iso8601) # => "2007-11-10" # - # == Adding your own time formats to to_formatted_s + # == Adding your own date formats to to_formatted_s # You can add your own formats to the Date::DATE_FORMATS hash. # Use the format name as the hash key and either a strftime string # or Proc instance that takes a date argument as the value. # - # # config/initializers/time_formats.rb + # # config/initializers/date_formats.rb # Date::DATE_FORMATS[:month_and_year] = '%B %Y' # Date::DATE_FORMATS[:short_ordinal] = ->(date) { date.strftime("%B #{date.day.ordinalize}") } def to_formatted_s(format = :default) 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 8e5d723074..73ad0aa097 100644 --- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb @@ -151,7 +151,11 @@ class DateTime # Layers additional behavior on DateTime#<=> so that Time and # ActiveSupport::TimeWithZone instances can be compared with a DateTime. def <=>(other) - super other.to_datetime + if other.respond_to? :to_datetime + super other.to_datetime + else + nil + end end end diff --git a/activesupport/lib/active_support/core_ext/hash.rb b/activesupport/lib/active_support/core_ext/hash.rb index 686f12c6da..f68e1662f9 100644 --- a/activesupport/lib/active_support/core_ext/hash.rb +++ b/activesupport/lib/active_support/core_ext/hash.rb @@ -1,3 +1,4 @@ +require 'active_support/core_ext/hash/compact' require 'active_support/core_ext/hash/conversions' require 'active_support/core_ext/hash/deep_merge' require 'active_support/core_ext/hash/except' diff --git a/activesupport/lib/active_support/core_ext/hash/compact.rb b/activesupport/lib/active_support/core_ext/hash/compact.rb new file mode 100644 index 0000000000..6566215a4d --- /dev/null +++ b/activesupport/lib/active_support/core_ext/hash/compact.rb @@ -0,0 +1,20 @@ +class Hash + # Returns a hash with non +nil+ values. + # + # hash = { a: true, b: false, c: nil} + # hash.compact # => { a: true, b: false} + # hash # => { a: true, b: false, c: nil} + # { c: nil }.compact # => {} + def compact + self.select { |_, value| !value.nil? } + end + + # Replaces current hash with non +nil+ values. + # + # hash = { a: true, b: false, c: nil} + # hash.compact! # => { a: true, b: false} + # hash # => { a: true, b: false} + def compact! + self.reject! { |_, value| value.nil? } + 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 2684c772ea..7bea461c77 100644 --- a/activesupport/lib/active_support/core_ext/hash/conversions.rb +++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb @@ -105,7 +105,7 @@ class Hash # hash = Hash.from_xml(xml) # # => {"hash"=>{"foo"=>1, "bar"=>2}} # - # DisallowedType is raise if the XML contains attributes with <tt>type="yaml"</tt> or + # DisallowedType is raised if the XML contains attributes with <tt>type="yaml"</tt> or # <tt>type="symbol"</tt>. Use <tt>Hash.from_trusted_xml</tt> to parse this XML. def from_xml(xml, disallowed_types = nil) ActiveSupport::XMLConverter.new(xml, disallowed_types).to_h diff --git a/activesupport/lib/active_support/core_ext/hash/except.rb b/activesupport/lib/active_support/core_ext/hash/except.rb index d90e996ad4..682d089881 100644 --- a/activesupport/lib/active_support/core_ext/hash/except.rb +++ b/activesupport/lib/active_support/core_ext/hash/except.rb @@ -1,5 +1,5 @@ class Hash - # Return a hash that includes everything but the given keys. This is useful for + # Returns a hash that includes everything but the given keys. This is useful for # limiting a set of parameters to everything but a few known toggles: # # @person.update(params[:person].except(:admin)) diff --git a/activesupport/lib/active_support/core_ext/hash/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb index 93e716585b..3d41aa8572 100644 --- a/activesupport/lib/active_support/core_ext/hash/keys.rb +++ b/activesupport/lib/active_support/core_ext/hash/keys.rb @@ -1,5 +1,5 @@ class Hash - # Return a new hash with all keys converted using the block operation. + # Returns a new hash with all keys converted using the block operation. # # hash = { name: 'Rob', age: '28' } # @@ -22,7 +22,7 @@ class Hash self end - # Return a new hash with all keys converted to strings. + # Returns a new hash with all keys converted to strings. # # hash = { name: 'Rob', age: '28' } # @@ -38,7 +38,7 @@ class Hash transform_keys!{ |key| key.to_s } end - # Return a new hash with all keys converted to symbols, as long as + # Returns a new hash with all keys converted to symbols, as long as # they respond to +to_sym+. # # hash = { 'name' => 'Rob', 'age' => '28' } @@ -73,7 +73,7 @@ class Hash end end - # Return a new hash with all keys converted by the block operation. + # Returns a new hash with all keys converted by the block operation. # This includes the keys from the root hash and from all # nested hashes. # @@ -100,7 +100,7 @@ class Hash self end - # Return a new hash with all keys converted to strings. + # Returns a new hash with all keys converted to strings. # This includes the keys from the root hash and from all # nested hashes. # @@ -119,7 +119,7 @@ class Hash deep_transform_keys!{ |key| key.to_s } end - # Return a new hash with all keys converted to symbols, as long as + # Returns a new hash with all keys converted to symbols, as long as # they respond to +to_sym+. This includes the keys from the root hash # and from all nested hashes. # diff --git a/activesupport/lib/active_support/core_ext/kernel/reporting.rb b/activesupport/lib/active_support/core_ext/kernel/reporting.rb index df11737a6b..3b5e205244 100644 --- a/activesupport/lib/active_support/core_ext/kernel/reporting.rb +++ b/activesupport/lib/active_support/core_ext/kernel/reporting.rb @@ -48,6 +48,7 @@ module Kernel yield ensure stream.reopen(old_stream) + old_stream.close end # Blocks and ignores any exception passed as argument if raised within the block. diff --git a/activesupport/lib/active_support/core_ext/module.rb b/activesupport/lib/active_support/core_ext/module.rb index f2d4887df6..b4efff8b24 100644 --- a/activesupport/lib/active_support/core_ext/module.rb +++ b/activesupport/lib/active_support/core_ext/module.rb @@ -4,6 +4,7 @@ require 'active_support/core_ext/module/anonymous' require 'active_support/core_ext/module/reachable' require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/module/attr_internal' +require 'active_support/core_ext/module/concerning' require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/module/deprecation' require 'active_support/core_ext/module/remove_method' diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb index f70a839074..d317df5079 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -49,7 +49,7 @@ class Module # include HairColors # end # - # Person.hair_colors # => [:brown, :black, :blonde, :red] + # + # Person.hair_colors # => [:brown, :black, :blonde, :red] def mattr_reader(*syms) options = syms.extract_options! syms.each do |sym| @@ -181,7 +181,7 @@ class Module # Or pass <tt>instance_accessor: false</tt>, to opt out both instance methods. # # module HairColors - # mattr_accessor :hair_colors, instance_acessor: false + # mattr_accessor :hair_colors, instance_accessor: false # end # # class Person diff --git a/activesupport/lib/active_support/core_ext/module/concerning.rb b/activesupport/lib/active_support/core_ext/module/concerning.rb new file mode 100644 index 0000000000..b22dc5ff1e --- /dev/null +++ b/activesupport/lib/active_support/core_ext/module/concerning.rb @@ -0,0 +1,135 @@ +require 'active_support/concern' + +class Module + # = Bite-sized separation of concerns + # + # We often find ourselves with a medium-sized chunk of behavior that we'd + # like to extract, but only mix in to a single class. + # + # Extracting a plain old Ruby object to encapsulate it and collaborate or + # delegate to the original object is often a good choice, but when there's + # no additional state to encapsulate or we're making DSL-style declarations + # about the parent class, introducing new collaborators can obfuscate rather + # than simplify. + # + # The typical route is to just dump everything in a monolithic class, perhaps + # with a comment, as a least-bad alternative. Using modules in separate files + # means tedious sifting to get a big-picture view. + # + # = Dissatisfying ways to separate small concerns + # + # == Using comments: + # + # class Todo + # # Other todo implementation + # # ... + # + # ## Event tracking + # has_many :events + # + # before_create :track_creation + # after_destroy :track_deletion + # + # private + # def track_creation + # # ... + # end + # end + # + # == With an inline module: + # + # Noisy syntax. + # + # class Todo + # # Other todo implementation + # # ... + # + # module EventTracking + # extend ActiveSupport::Concern + # + # included do + # has_many :events + # before_create :track_creation + # after_destroy :track_deletion + # end + # + # private + # def track_creation + # # ... + # end + # end + # include EventTracking + # end + # + # == Mix-in noise exiled to its own file: + # + # Once our chunk of behavior starts pushing the scroll-to-understand it + # boundary, we give in and move it to a separate file. At this size, the + # overhead feels in good proportion to the size of our extraction, despite + # diluting our at-a-glance sense of how things really work. + # + # class Todo + # # Other todo implementation + # # ... + # + # include TodoEventTracking + # end + # + # = Introducing Module#concerning + # + # By quieting the mix-in noise, we arrive at a natural, low-ceremony way to + # separate bite-sized concerns. + # + # class Todo + # # Other todo implementation + # # ... + # + # concerning :EventTracking do + # included do + # has_many :events + # before_create :track_creation + # after_destroy :track_deletion + # end + # + # private + # def track_creation + # # ... + # end + # end + # end + # + # Todo.ancestors + # # => Todo, Todo::EventTracking, Object + # + # This small step has some wonderful ripple effects. We can + # * grok the behavior of our class in one glance, + # * clean up monolithic junk-drawer classes by separating their concerns, and + # * stop leaning on protected/private for crude "this is internal stuff" modularity. + module Concerning + # Define a new concern and mix it in. + def concerning(topic, &block) + include concern(topic, &block) + end + + # A low-cruft shortcut to define a concern. + # + # concern :EventTracking do + # ... + # end + # + # is equivalent to + # + # module EventTracking + # extend ActiveSupport::Concern + # + # ... + # end + def concern(topic, &module_definition) + const_set topic, Module.new { + extend ::ActiveSupport::Concern + module_eval(&module_definition) + } + end + end + include Concerning +end diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index 58146cdf7a..f855833a24 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -178,30 +178,32 @@ class Module # whereas conceptually, from the user point of view, the delegator should # be doing one call. if allow_nil - module_eval(<<-EOS, file, line - 3) - 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 - EOS + 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}") - module_eval(<<-EOS, file, line - 2) - 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 - EOS + 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 + + module_eval(method_def, file, line) end end end diff --git a/activesupport/lib/active_support/core_ext/object/blank.rb b/activesupport/lib/active_support/core_ext/object/blank.rb index f9ff4d9567..38e43478df 100644 --- a/activesupport/lib/active_support/core_ext/object/blank.rb +++ b/activesupport/lib/active_support/core_ext/object/blank.rb @@ -4,36 +4,42 @@ class Object # An object is blank if it's false, empty, or a whitespace string. # For example, '', ' ', +nil+, [], and {} are all blank. # - # This simplifies: + # This simplifies # - # if address.nil? || address.empty? + # address.nil? || address.empty? # - # ...to: + # to # - # if address.blank? + # address.blank? + # + # @return [true, false] def blank? - respond_to?(:empty?) ? empty? : !self + respond_to?(:empty?) ? !!empty? : !self end - # An object is present if it's not <tt>blank?</tt>. + # An object is present if it's not blank. + # + # @return [true, false] def present? !blank? end - # Returns object if it's <tt>present?</tt> otherwise returns +nil+. - # <tt>object.presence</tt> is equivalent to <tt>object.present? ? object : nil</tt>. + # Returns the receiver if it's present otherwise returns +nil+. + # <tt>object.presence</tt> is equivalent to # - # This is handy for any representation of objects where blank is the same - # as not present at all. For example, this simplifies a common check for - # HTTP POST/query parameters: + # object.present? ? object : nil + # + # For example, something like # # state = params[:state] if params[:state].present? # country = params[:country] if params[:country].present? # region = state || country || 'US' # - # ...becomes: + # becomes # # region = params[:state].presence || params[:country].presence || 'US' + # + # @return [Object] def presence self if present? end @@ -43,6 +49,8 @@ class NilClass # +nil+ is blank: # # nil.blank? # => true + # + # @return [true] def blank? true end @@ -52,6 +60,8 @@ class FalseClass # +false+ is blank: # # false.blank? # => true + # + # @return [true] def blank? true end @@ -61,6 +71,8 @@ class TrueClass # +true+ is not blank: # # true.blank? # => false + # + # @return [false] def blank? false end @@ -71,6 +83,8 @@ class Array # # [].blank? # => true # [1,2,3].blank? # => false + # + # @return [true, false] alias_method :blank?, :empty? end @@ -79,18 +93,28 @@ class Hash # # {}.blank? # => true # { key: 'value' }.blank? # => false + # + # @return [true, false] alias_method :blank?, :empty? end class String + BLANK_RE = /\A[[:space:]]*\z/ + # A string is blank if it's empty or contains whitespaces only: # - # ''.blank? # => true - # ' '.blank? # => true - # ' '.blank? # => true - # ' something here '.blank? # => false + # ''.blank? # => true + # ' '.blank? # => true + # "\t\n\r".blank? # => true + # ' blah '.blank? # => false + # + # Unicode whitespace is supported: + # + # "\u00a0".blank? # => true + # + # @return [true, false] def blank? - self =~ /\A[[:space:]]*\z/ + BLANK_RE === self end end @@ -99,6 +123,8 @@ class Numeric #:nodoc: # # 1.blank? # => false # 0.blank? # => false + # + # @return [false] def blank? false end diff --git a/activesupport/lib/active_support/core_ext/object/json.rb b/activesupport/lib/active_support/core_ext/object/json.rb index 1675145ffe..8e08cfbf26 100644 --- a/activesupport/lib/active_support/core_ext/object/json.rb +++ b/activesupport/lib/active_support/core_ext/object/json.rb @@ -16,12 +16,12 @@ require 'active_support/core_ext/module/aliasing' # otherwise they will always use to_json gem implementation, which is backwards incompatible in # several cases (for instance, the JSON implementation for Hash does not work) with inheritance # and consequently classes as ActiveSupport::OrderedHash cannot be serialized to json. -# +# # On the other hand, we should avoid conflict with ::JSON.{generate,dump}(obj). Unfortunately, the # JSON gem's encoder relies on its own to_json implementation to encode objects. Since it always # passes a ::JSON::State object as the only argument to to_json, we can detect that and forward the # calls to the original to_json method. -# +# # It should be noted that when using ::JSON.{generate,dump} directly, ActiveSupport's encoder is # bypassed completely. This means that as_json won't be invoked and the JSON gem will simply # ignore any options it does not natively understand. This also means that ::JSON.{generate,dump} @@ -163,7 +163,7 @@ end class Time def as_json(options = nil) #:nodoc: if ActiveSupport.use_standard_json_time_format - xmlschema(3) + xmlschema(ActiveSupport::JSON::Encoding.time_precision) else %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)}) end @@ -183,7 +183,7 @@ end class DateTime def as_json(options = nil) #:nodoc: if ActiveSupport.use_standard_json_time_format - xmlschema(3) + xmlschema(ActiveSupport::JSON::Encoding.time_precision) else strftime('%Y/%m/%d %H:%M:%S %z') end diff --git a/activesupport/lib/active_support/core_ext/range/each.rb b/activesupport/lib/active_support/core_ext/range/each.rb index d51ea2e944..ecef78f55f 100644 --- a/activesupport/lib/active_support/core_ext/range/each.rb +++ b/activesupport/lib/active_support/core_ext/range/each.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/module/aliasing' -require 'active_support/core_ext/object/acts_like' class Range #:nodoc: @@ -17,7 +16,7 @@ class Range #:nodoc: private def ensure_iteration_allowed - if first.acts_like?(:time) + if first.is_a?(Time) raise TypeError, "can't iterate from #{first.class}" 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 d94e1bfca2..6018fd9641 100644 --- a/activesupport/lib/active_support/core_ext/string/access.rb +++ b/activesupport/lib/active_support/core_ext/string/access.rb @@ -59,7 +59,7 @@ class String # str.from(0).to(-1) # => "hello" # str.from(1).to(-2) # => "ell" def to(position) - self[0, position + 1] + self[0..position] end # Returns the first character. If a limit is supplied, returns a substring diff --git a/activesupport/lib/active_support/core_ext/string/output_safety.rb b/activesupport/lib/active_support/core_ext/string/output_safety.rb index 1b2098fc84..eb02b6a442 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -70,9 +70,20 @@ class ERB # them inside a script tag to avoid XSS vulnerability: # # <script> - # var currentUser = <%= json_escape current_user.to_json %>; + # var currentUser = <%= raw json_escape(current_user.to_json) %>; # </script> # + # It is necessary to +raw+ the result of +json_escape+, so that quotation marks + # don't get converted to <tt>"</tt> entities. +json_escape+ doesn't + # automatically flag the result as HTML safe, since the raw value is unsafe to + # use inside HTML attributes. + # + # If you need to output JSON elsewhere in your HTML, you can just do something + # like this, as any unsafe characters (including quotation marks) will be + # automatically escaped for you: + # + # <div data-user-info="<%= current_user.to_json %>">...</div> + # # WARNING: this helper only works with valid JSON. Using this on non-JSON values # will open up serious XSS vulnerabilities. For example, if you replace the # +current_user.to_json+ in the example above with user input instead, the browser @@ -88,17 +99,6 @@ class ERB # is recommended that you always apply this helper (other libraries, such as the # JSON gem, do not provide this kind of protection by default; also some gems # might override +to_json+ to bypass Active Support's encoder). - # - # The output of this helper method is marked as HTML safe so that you can directly - # include it inside a <tt><script></tt> tag as shown above. - # - # However, it is NOT safe to use the output of this inside an HTML attribute, - # because quotation marks are not escaped. Doing so might break your page's layout. - # If you intend to use this inside an HTML attribute, you should use the - # +html_escape+ helper (or its +h+ alias) instead: - # - # <div data-user-info="<%= h current_user.to_json %>">...</div> - # def json_escape(s) result = s.to_s.gsub(JSON_ESCAPE_REGEXP, JSON_ESCAPE) s.html_safe? ? result.html_safe : result @@ -183,15 +183,14 @@ module ActiveSupport #:nodoc: end def %(args) - args = Array(args).map do |arg| - if !html_safe? || arg.html_safe? - arg - else - ERB::Util.h(arg) - end + case args + when Hash + escaped_args = Hash[args.map { |k,arg| [k, html_escape_interpolated_argument(arg)] }] + else + escaped_args = Array(args).map { |arg| html_escape_interpolated_argument(arg) } end - self.class.new(super(args)) + self.class.new(super(escaped_args)) end def html_safe? @@ -224,6 +223,12 @@ module ActiveSupport #:nodoc: EOT end end + + private + + def html_escape_interpolated_argument(arg) + (!html_safe? || arg.html_safe?) ? arg : ERB::Util.h(arg) + end end end diff --git a/activesupport/lib/active_support/core_ext/thread.rb b/activesupport/lib/active_support/core_ext/thread.rb index d5a420301a..ac1ffa4128 100644 --- a/activesupport/lib/active_support/core_ext/thread.rb +++ b/activesupport/lib/active_support/core_ext/thread.rb @@ -33,7 +33,7 @@ class Thread _locals[key.to_sym] = value end - # Returns an an array of the names of the thread-local variables (as Symbols). + # Returns an array of the names of the thread-local variables (as Symbols). # # thr = Thread.new do # Thread.current.thread_variable_set(:cat, 'meow') diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb index 5ebafcc26e..89cd7516cd 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -91,10 +91,11 @@ class Time end end - # Uses Date to provide precise Time calculations for years, months, and days. - # The +options+ parameter takes a hash with any of these keys: <tt>:years</tt>, - # <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>, <tt>:hours</tt>, - # <tt>:minutes</tt>, <tt>:seconds</tt>. + # Uses Date to provide precise Time calculations for years, months, and days + # according to the proleptic Gregorian calendar. The +options+ parameter + # takes a hash with any of these keys: <tt>:years</tt>, <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) @@ -107,6 +108,7 @@ class Time end d = to_date.advance(options) + d = d.gregorian if d.julian? time_advanced_by_date = change(:year => d.year, :month => d.month, :day => d.day) seconds_to_advance = \ options.fetch(:seconds, 0) + diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index 19d4ff51d7..6be19771f5 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -176,14 +176,22 @@ module ActiveSupport #:nodoc: end def const_missing(const_name) - # The interpreter does not pass nesting information, and in the - # case of anonymous modules we cannot even make the trade-off of - # assuming their name reflects the nesting. Resort to Object as - # the only meaningful guess we can make. - from_mod = anonymous? ? ::Object : self + from_mod = anonymous? ? guess_for_anonymous(const_name) : self 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. + 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" + else + Object + end + end + def unloadable(const_desc = self) super(const_desc) end @@ -456,8 +464,6 @@ module ActiveSupport #:nodoc: raise ArgumentError, "A copy of #{from_mod} has been removed from the module tree but is still active!" end - raise NameError, "#{from_mod} is not missing constant #{const_name}!" if from_mod.const_defined?(const_name, false) - qualified_name = qualified_name_for from_mod, const_name path_suffix = qualified_name.underscore diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index f690eab604..594a4ca938 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -159,7 +159,7 @@ module ActiveSupport # # counters.fetch('foo') # => 1 # counters.fetch(:bar, 0) # => 0 - # counters.fetch(:bar) {|key| 0} # => 0 + # counters.fetch(:bar) { |key| 0 } # => 0 # counters.fetch(:zoo) # => KeyError: key not found: "zoo" def fetch(key, *extras) super(convert_key(key), *extras) @@ -172,7 +172,7 @@ module ActiveSupport # hash[:b] = 'y' # hash.values_at('a', 'b') # => ["x", "y"] def values_at(*indices) - indices.collect {|key| self[convert_key(key)]} + indices.collect { |key| self[convert_key(key)] } end # Returns an exact copy of the hash. @@ -228,7 +228,11 @@ module ActiveSupport def to_options!; self end def select(*args, &block) - dup.tap {|hash| hash.select!(*args, &block)} + dup.tap { |hash| hash.select!(*args, &block) } + end + + def reject(*args, &block) + dup.tap { |hash| hash.reject!(*args, &block) } end # Convert to a regular hash with string keys. diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb index dcdea70443..23cd6716e3 100644 --- a/activesupport/lib/active_support/i18n_railtie.rb +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -8,6 +8,8 @@ module I18n config.i18n.railties_load_path = [] config.i18n.load_path = [] config.i18n.fallbacks = ActiveSupport::OrderedOptions.new + # Enforce I18n to check the available locales when setting a locale. + config.i18n.enforce_available_locales = true # Set the i18n configuration after initialization since a lot of # configuration is still usually done in application initializers. @@ -31,10 +33,11 @@ module I18n fallbacks = app.config.i18n.delete(:fallbacks) - if app.config.i18n.has_key?(:enforce_available_locales) - # this option needs to be set before `default_locale=` to work properly. - I18n.enforce_available_locales = app.config.i18n.delete(:enforce_available_locales) - end + # Avoid issues with setting the default_locale by disabling available locales + # check while configuring. + enforce_available_locales = app.config.i18n.delete(:enforce_available_locales) + enforce_available_locales = I18n.enforce_available_locales unless I18n.enforce_available_locales.nil? + I18n.enforce_available_locales = false app.config.i18n.each do |setting, value| case setting @@ -49,6 +52,9 @@ module I18n init_fallbacks(fallbacks) if fallbacks && validate_fallbacks(fallbacks) + # Restore available locales check so it will take place from now on. + I18n.enforce_available_locales = enforce_available_locales + reloader = ActiveSupport::FileUpdateChecker.new(I18n.load_path.dup){ I18n.reload! } app.reloaders << reloader ActionDispatch::Reloader.to_prepare { reloader.execute_if_updated } diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index 0f7ae98a8a..cdee4c2ca5 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -117,7 +117,8 @@ module ActiveSupport result.gsub!(/([a-z\d]*)/i) { |match| "#{inflections.acronyms[match] || match.downcase}" } - options.fetch(:capitalize, true) ? result.gsub(/^\w/) { $&.upcase } : result + result.gsub!(/^\w/) { $&.upcase } if options.fetch(:capitalize, true) + result end # Capitalizes all the words and replaces some characters in the string to diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index 060dcb6995..f29d42276d 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -4,6 +4,7 @@ require 'active_support/core_ext/module/delegation' module ActiveSupport class << self delegate :use_standard_json_time_format, :use_standard_json_time_format=, + :time_precision, :time_precision=, :escape_html_entities_in_json, :escape_html_entities_in_json=, :encode_big_decimal_as_string, :encode_big_decimal_as_string=, :json_encoder, :json_encoder=, @@ -60,36 +61,31 @@ module ActiveSupport end # Mark these as private so we don't leak encoding-specific constructs - private_constant :ESCAPED_CHARS, :ESCAPE_REGEX_WITH_HTML_ENTITIES, + private_constant :ESCAPED_CHARS, :ESCAPE_REGEX_WITH_HTML_ENTITIES, :ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, :EscapedString - # Recursively turn the given object into a "jsonified" Ruby data structure - # that the JSON gem understands - i.e. we want only Hash, Array, String, - # Numeric, true, false and nil in the final tree. Calls #as_json on it if - # it's not from one of these base types. - # - # This allows developers to implement #as_json withouth having to worry - # about what base types of objects they are allowed to return and having - # to remember calling #as_json recursively. - # - # By default, the options hash is not passed to the children data structures - # to avoid undesiarable result. Develoers must opt-in by implementing - # custom #as_json methods (e.g. Hash#as_json and Array#as_json). + # Convert an object into a "JSON-ready" representation composed of + # primitives like Hash, Array, String, Numeric, and true/false/nil. + # Recursively calls #as_json to the object to recursively build a + # fully JSON-ready object. + # + # This allows developers to implement #as_json without having to + # worry about what base types of objects they are allowed to return + # or having to remember to call #as_json recursively. + # + # Note: the +options+ hash passed to +object.to_json+ is only passed + # to +object.as_json+, not any of this method's recursive +#as_json+ + # calls. def jsonify(value) - if value.is_a?(Hash) - Hash[value.map { |k, v| [jsonify(k), jsonify(v)] }] - elsif value.is_a?(Array) - value.map { |v| jsonify(v) } - elsif value.is_a?(String) + case value + when String EscapedString.new(value) - elsif value.is_a?(Numeric) + when Numeric, NilClass, TrueClass, FalseClass value - elsif value == true - true - elsif value == false - false - elsif value == nil - nil + when Hash + Hash[value.map { |k, v| [jsonify(k), jsonify(v)] }] + when Array + value.map { |v| jsonify(v) } else jsonify value.as_json end @@ -110,6 +106,10 @@ module ActiveSupport # as a safety measure. attr_accessor :escape_html_entities_in_json + # Sets the precision of encoded time values. + # Defaults to 3 (equivalent to millisecond precision) + attr_accessor :time_precision + # Sets the encoder used by Rails to encode Ruby objects into JSON strings # in +Object#to_json+ and +ActiveSupport::JSON.encode+. attr_accessor :json_encoder @@ -166,6 +166,7 @@ module ActiveSupport self.use_standard_json_time_format = true self.escape_html_entities_in_json = true self.json_encoder = JSONGemEncoder + self.time_precision = 3 end end end diff --git a/activesupport/lib/active_support/key_generator.rb b/activesupport/lib/active_support/key_generator.rb index 598c46bce5..51d2da3a79 100644 --- a/activesupport/lib/active_support/key_generator.rb +++ b/activesupport/lib/active_support/key_generator.rb @@ -57,18 +57,16 @@ module ActiveSupport # secret they've provided is at least 30 characters in length. def ensure_secret_secure(secret) if secret.blank? - raise ArgumentError, "A secret is required to generate an " + - "integrity hash for cookie session data. Use " + - "config.secret_key_base = \"some secret phrase of at " + - "least #{SECRET_MIN_LENGTH} characters\"" + - "in config/initializers/secret_token.rb" + raise ArgumentError, "A secret is required to generate an integrity hash " \ + "for cookie session data. Set a secret_key_base of at least " \ + "#{SECRET_MIN_LENGTH} characters in config/secrets.yml." end if secret.length < SECRET_MIN_LENGTH - raise ArgumentError, "Secret should be something secure, " + - "like \"#{SecureRandom.hex(16)}\". The value you " + - "provided, \"#{secret}\", is shorter than the minimum length " + - "of #{SECRET_MIN_LENGTH} characters" + raise ArgumentError, "Secret should be something secure, " \ + "like \"#{SecureRandom.hex(16)}\". The value you " \ + "provided, \"#{secret}\", is shorter than the minimum length " \ + "of #{SECRET_MIN_LENGTH} characters." end end end diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index bffdfc6201..b019ad0dec 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -12,7 +12,7 @@ module ActiveSupport # This can be used in situations similar to the <tt>MessageVerifier</tt>, but # where you don't want users to be able to determine the value of the payload. # - # salt = SecureRandom.random_bytes(64) + # salt = SecureRandom.random_bytes(64) # key = ActiveSupport::KeyGenerator.new('password').generate_key(salt) # => "\x89\xE0\x156\xAC..." # crypt = ActiveSupport::MessageEncryptor.new(key) # => #<ActiveSupport::MessageEncryptor ...> # encrypted_data = crypt.encrypt_and_sign('my secret data') # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..." @@ -76,12 +76,12 @@ module ActiveSupport encrypted_data = cipher.update(@serializer.dump(value)) encrypted_data << cipher.final - [encrypted_data, iv].map {|v| ::Base64.strict_encode64(v)}.join("--") + "#{::Base64.strict_encode64 encrypted_data}--#{::Base64.strict_encode64 iv}" end def _decrypt(encrypted_message) cipher = new_cipher - encrypted_data, iv = encrypted_message.split("--").map {|v| ::Base64.decode64(v)} + encrypted_data, iv = encrypted_message.split("--").map {|v| ::Base64.strict_decode64(v)} cipher.decrypt cipher.key = @secret @@ -91,7 +91,7 @@ module ActiveSupport decrypted_data << cipher.final @serializer.load(decrypted_data) - rescue OpenSSLCipherError, TypeError + rescue OpenSSLCipherError, TypeError, ArgumentError raise InvalidMessage end diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb index e0cd92ae3c..8e6e1dcfeb 100644 --- a/activesupport/lib/active_support/message_verifier.rb +++ b/activesupport/lib/active_support/message_verifier.rb @@ -37,7 +37,12 @@ module ActiveSupport data, digest = signed_message.split("--") if data.present? && digest.present? && secure_compare(digest, generate_digest(data)) - @serializer.load(::Base64.decode64(data)) + begin + @serializer.load(::Base64.strict_decode64(data)) + rescue ArgumentError => argument_error + raise InvalidSignature if argument_error.message =~ %r{invalid base64} + raise + end else raise InvalidSignature end diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb index 1845c6ae38..84799c2399 100644 --- a/activesupport/lib/active_support/multibyte/unicode.rb +++ b/activesupport/lib/active_support/multibyte/unicode.rb @@ -11,7 +11,7 @@ module ActiveSupport NORMALIZATION_FORMS = [:c, :kc, :d, :kd] # The Unicode version that is supported by the implementation - UNICODE_VERSION = '6.2.0' + UNICODE_VERSION = '6.3.0' # The default normalization used for operations that require # normalization. It can be set to any of the normalizations @@ -212,37 +212,43 @@ module ActiveSupport codepoints end - # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent - # resulting in a valid UTF-8 string. - # - # Passing +true+ will forcibly tidy all bytes, assuming that the string's - # encoding is entirely CP1252 or ISO-8859-1. - def tidy_bytes(string, force = false) - return string if string.empty? - - if force - return string.encode(Encoding::UTF_8, Encoding::Windows_1252, invalid: :replace, undef: :replace) + # Ruby >= 2.1 has String#scrub, which is faster than the workaround used for < 2.1. + if RUBY_VERSION >= '2.1' + # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent + # resulting in a valid UTF-8 string. + # + # Passing +true+ will forcibly tidy all bytes, assuming that the string's + # encoding is entirely CP1252 or ISO-8859-1. + def tidy_bytes(string, force = false) + return string if string.empty? + return recode_windows1252_chars(string) if force + string.scrub { |bad| recode_windows1252_chars(bad) } end + else + def tidy_bytes(string, force = false) + return string if string.empty? + return recode_windows1252_chars(string) if force + + # We can't transcode to the same format, so we choose a nearly-identical encoding. + # We're going to 'transcode' bytes from UTF-8 when possible, then fall back to + # CP1252 when we get errors. The final string will be 'converted' back to UTF-8 + # before returning. + reader = Encoding::Converter.new(Encoding::UTF_8, Encoding::UTF_8_MAC) + + source = string.dup + out = ''.force_encoding(Encoding::UTF_8_MAC) + + loop do + reader.primitive_convert(source, out) + _, _, _, error_bytes, _ = reader.primitive_errinfo + break if error_bytes.nil? + out << error_bytes.encode(Encoding::UTF_8_MAC, Encoding::Windows_1252, invalid: :replace, undef: :replace) + end - # We can't transcode to the same format, so we choose a nearly-identical encoding. - # We're going to 'transcode' bytes from UTF-8 when possible, then fall back to - # CP1252 when we get errors. The final string will be 'converted' back to UTF-8 - # before returning. - reader = Encoding::Converter.new(Encoding::UTF_8, Encoding::UTF_8_MAC) - - source = string.dup - out = ''.force_encoding(Encoding::UTF_8_MAC) + reader.finish - loop do - reader.primitive_convert(source, out) - _, _, _, error_bytes, _ = reader.primitive_errinfo - break if error_bytes.nil? - out << error_bytes.encode(Encoding::UTF_8_MAC, Encoding::Windows_1252, invalid: :replace, undef: :replace) + out.encode!(Encoding::UTF_8) end - - reader.finish - - out.encode!(Encoding::UTF_8) end # Returns the KC normalization of the string by default. NFKC is @@ -371,14 +377,8 @@ module ActiveSupport end.pack('U*') end - def tidy_byte(byte) - if byte < 160 - [database.cp1252[byte] || byte].pack("U").unpack("C*") - elsif byte < 192 - [194, byte] - else - [195, byte - 64] - end + def recode_windows1252_chars(string) + string.encode(Encoding::UTF_8, Encoding::Windows_1252, invalid: :replace, undef: :replace) end def database diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb index c6658dba96..b169e3af01 100644 --- a/activesupport/lib/active_support/number_helper.rb +++ b/activesupport/lib/active_support/number_helper.rb @@ -1,4 +1,3 @@ - module ActiveSupport module NumberHelper extend ActiveSupport::Autoload @@ -343,6 +342,5 @@ module ActiveSupport def number_to_human(number, options = {}) NumberToHumanConverter.convert(number, options) end - end end diff --git a/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb index d1335f6910..78d2c9ae6e 100644 --- a/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb @@ -1,6 +1,6 @@ module ActiveSupport module NumberHelper - class NumberToHumanSizeConverter < NumberConverter + class NumberToHumanSizeConverter < NumberConverter #:nodoc: STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb] self.namespace = :human diff --git a/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb b/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb index 4c33c30772..af2ee56d91 100644 --- a/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb @@ -1,6 +1,6 @@ module ActiveSupport module NumberHelper - class NumberToPhoneConverter < NumberConverter + class NumberToPhoneConverter < NumberConverter #:nodoc: def convert str = country_code(opts[:country_code]) str << convert_to_phone_number(number.to_s.strip) diff --git a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb index 273667fdae..c42354fc83 100644 --- a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb @@ -5,28 +5,50 @@ module ActiveSupport self.validate_float = true def convert - @number = Float(number) - precision = options.delete :precision significant = options.delete :significant + case number + when Float, String + @number = BigDecimal(number.to_s) + when Rational + if significant + @number = BigDecimal(number, digit_count(number.to_i) + precision) + else + @number = BigDecimal(number, precision) + end + else + @number = number.to_d + end + if significant && precision > 0 digits, rounded_number = digits_and_rounded_number(precision) precision -= digits precision = 0 if precision < 0 # don't let it be negative else - rounded_number = BigDecimal.new(number.to_s).round(precision).to_f + rounded_number = number.round(precision) + rounded_number = rounded_number.to_i if precision == 0 rounded_number = rounded_number.abs if rounded_number.zero? # prevent showing negative zeros end - delimited_number = NumberToDelimitedConverter.convert("%01.#{precision}f" % rounded_number, options) + formatted_string = + case rounded_number + when BigDecimal + s = rounded_number.to_s('F') + '0'*precision + a, b = s.split('.', 2) + a + '.' + b[0, precision] + else + "%01.#{precision}f" % rounded_number + end + + delimited_number = NumberToDelimitedConverter.convert(formatted_string, options) format_number(delimited_number) end private def digits_and_rounded_number(precision) - if number.zero? + if zero? [1, 0] else digits = digit_count(number) @@ -38,11 +60,11 @@ module ActiveSupport end def calculate_rounded_number(multiplier) - (BigDecimal.new(number.to_s) / BigDecimal.new(multiplier.to_f.to_s)).round.to_f * multiplier + (number / BigDecimal.new(multiplier.to_f.to_s)).round * multiplier end def digit_count(number) - (Math.log10(number.abs) + 1).floor + (Math.log10(absolute_number(number)) + 1).floor end def strip_insignificant_zeros @@ -57,6 +79,14 @@ module ActiveSupport number end end + + def absolute_number(number) + number.respond_to?(:abs) ? number.abs : number.to_d.abs + end + + def zero? + number.respond_to?(:zero?) ? number.zero? : number.to_d.zero? + end end end end diff --git a/activesupport/lib/active_support/ordered_hash.rb b/activesupport/lib/active_support/ordered_hash.rb index 1a3693f766..58a2ce2105 100644 --- a/activesupport/lib/active_support/ordered_hash.rb +++ b/activesupport/lib/active_support/ordered_hash.rb @@ -28,6 +28,10 @@ module ActiveSupport coder.represent_seq '!omap', map { |k,v| { k => v } } end + def reject(*args, &block) + dup.tap { |hash| hash.reject!(*args, &block) } + end + def nested_under_indifferent_access self end diff --git a/activesupport/lib/active_support/per_thread_registry.rb b/activesupport/lib/active_support/per_thread_registry.rb index a5e7389d16..ca2e4d5625 100644 --- a/activesupport/lib/active_support/per_thread_registry.rb +++ b/activesupport/lib/active_support/per_thread_registry.rb @@ -32,12 +32,15 @@ module ActiveSupport # # If the class has an initializer, it must accept no arguments. module PerThreadRegistry + def self.extended(object) + object.instance_variable_set '@per_thread_registry_key', object.name.freeze + end + def instance - Thread.current[name] ||= new + Thread.current[@per_thread_registry_key] ||= new end protected - def method_missing(name, *args, &block) # :nodoc: # Caches the method definition as a singleton method of the receiver. define_singleton_method(name) do |*a, &b| diff --git a/activesupport/lib/active_support/testing/deprecation.rb b/activesupport/lib/active_support/testing/deprecation.rb index a8342904dc..6c94c611b6 100644 --- a/activesupport/lib/active_support/testing/deprecation.rb +++ b/activesupport/lib/active_support/testing/deprecation.rb @@ -19,18 +19,17 @@ module ActiveSupport result end - private - def collect_deprecations - old_behavior = ActiveSupport::Deprecation.behavior - deprecations = [] - ActiveSupport::Deprecation.behavior = Proc.new do |message, callstack| - deprecations << message - end - result = yield - [result, deprecations] - ensure - ActiveSupport::Deprecation.behavior = old_behavior + def collect_deprecations + old_behavior = ActiveSupport::Deprecation.behavior + deprecations = [] + ActiveSupport::Deprecation.behavior = Proc.new do |message, callstack| + deprecations << message end + result = yield + [result, deprecations] + ensure + ActiveSupport::Deprecation.behavior = old_behavior + end end end end diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb index d5d31cecbe..908af176be 100644 --- a/activesupport/lib/active_support/testing/isolation.rb +++ b/activesupport/lib/active_support/testing/isolation.rb @@ -1,5 +1,4 @@ require 'rbconfig' -require 'minitest/parallel_each' module ActiveSupport module Testing @@ -7,11 +6,9 @@ module ActiveSupport require 'thread' def self.included(klass) #:nodoc: - klass.extend(Module.new { - def test_methods - ParallelEach.new super - end - }) + klass.class_eval do + parallelize_me! + end end def self.forking_env? @@ -40,6 +37,8 @@ module ActiveSupport module Forking def run_in_isolation(&blk) read, write = IO.pipe + read.binmode + write.binmode pid = fork do read.close diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 50db7da9d9..d459af1778 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -45,7 +45,7 @@ module ActiveSupport def initialize(utc_time, time_zone, local_time = nil, period = nil) @utc, @time_zone, @time = utc_time, time_zone, local_time - @period = @utc ? period : get_period_and_ensure_valid_local_time + @period = @utc ? period : get_period_and_ensure_valid_local_time(period) end # Returns a Time or DateTime instance that represents the time in +time_zone+. @@ -132,8 +132,8 @@ module ActiveSupport end def xmlschema(fraction_digits = 0) - fraction = if fraction_digits > 0 - (".%06i" % time.usec)[0, fraction_digits + 1] + fraction = if fraction_digits.to_i > 0 + (".%06i" % time.usec)[0, fraction_digits.to_i + 1] end "#{time.strftime("%Y-%m-%dT%H:%M:%S")}#{fraction}#{formatted_offset(true, 'Z')}" @@ -154,7 +154,7 @@ module ActiveSupport # # => "2005/02/01 05:15:10 -1000" def as_json(options = nil) if ActiveSupport::JSON::Encoding.use_standard_json_time_format - xmlschema(3) + xmlschema(ActiveSupport::JSON::Encoding.time_precision) else %(#{time.strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)}) end @@ -292,6 +292,12 @@ module ActiveSupport end end + def change(options) + new_time = time.change(options) + periods = time_zone.periods_for_local(new_time) + self.class.new(nil, time_zone, new_time, periods.include?(period) ? period : nil) + end + %w(year mon month day mday wday yday hour min sec usec nsec to_date).each do |method_name| class_eval <<-EOV, __FILE__, __LINE__ + 1 def #{method_name} # def month @@ -367,12 +373,12 @@ module ActiveSupport end private - def get_period_and_ensure_valid_local_time + def get_period_and_ensure_valid_local_time(period) # we don't want a Time.local instance enforcing its own DST rules as well, # so transfer time values to a utc constructor if necessary @time = transfer_time_values_to_utc_constructor(@time) unless @time.utc? begin - @time_zone.period_for_local(@time) + period || @time_zone.period_for_local(@time) rescue ::TZInfo::PeriodNotFound # time is in the "spring forward" hour gap, so we're moving the time forward one hour and trying again @time += 1.hour diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index a22e61d286..eb785d46ce 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -1,3 +1,4 @@ +require 'tzinfo' require 'thread_safe' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/try' @@ -208,8 +209,6 @@ module ActiveSupport # (GMT). Seconds were chosen as the offset unit because that is the unit # that Ruby uses to represent time zone offsets (see Time#utc_offset). def initialize(name, utc_offset = nil, tzinfo = nil) - self.class.send(:require_tzinfo) - @name = name @utc_offset = utc_offset @tzinfo = tzinfo || TimeZone.find_tzinfo(name) @@ -235,6 +234,7 @@ module ActiveSupport # Compare this time zone to the parameter. The two are compared first on # their offsets, and then by name. def <=>(zone) + return unless zone.respond_to? :utc_offset result = (utc_offset <=> zone.utc_offset) result = (name <=> zone.name) if result == 0 result @@ -352,6 +352,10 @@ module ActiveSupport tzinfo.period_for_local(time, dst) end + def periods_for_local(time) #:nodoc: + tzinfo.periods_for_local(time) + end + def self.find_tzinfo(name) TZInfo::TimezoneProxy.new(MAPPING[name] || name) end @@ -359,14 +363,14 @@ module ActiveSupport class << self alias_method :create, :new - # Return a TimeZone instance with the given name, or +nil+ if no + # Returns a TimeZone instance with the given name, or +nil+ if no # such TimeZone instance exists. (This exists to support the use of # this class with the +composed_of+ macro.) def new(name) self[name] end - # Return an array of all TimeZone objects. There are multiple + # Returns an array of all TimeZone objects. There are multiple # TimeZone objects per time zone, in many cases, to make it easier # for users to find their own time zone. def all @@ -389,7 +393,7 @@ module ActiveSupport case arg when String begin - lazy_zones_map[arg] ||= create(arg).tap { |tz| tz.utc_offset } + @lazy_zones_map[arg] ||= create(arg).tap { |tz| tz.utc_offset } rescue TZInfo::InvalidTimezoneIdentifier nil end @@ -406,22 +410,6 @@ module ActiveSupport def us_zones @us_zones ||= all.find_all { |z| z.name =~ /US|Arizona|Indiana|Hawaii|Alaska/ } end - - protected - - def require_tzinfo - require 'tzinfo' unless defined?(::TZInfo) - rescue LoadError - $stderr.puts "You don't have tzinfo installed in your application. Please add it to your Gemfile and run bundle install" - raise - end - - private - - def lazy_zones_map - require_tzinfo - @lazy_zones_map - end end private diff --git a/activesupport/lib/active_support/values/unicode_tables.dat b/activesupport/lib/active_support/values/unicode_tables.dat Binary files differindex 2571faa019..394ee95f4b 100644 --- a/activesupport/lib/active_support/values/unicode_tables.dat +++ b/activesupport/lib/active_support/values/unicode_tables.dat diff --git a/activesupport/lib/active_support/version.rb b/activesupport/lib/active_support/version.rb index 8762330a6e..b3f0e7198d 100644 --- a/activesupport/lib/active_support/version.rb +++ b/activesupport/lib/active_support/version.rb @@ -1,7 +1,7 @@ module ActiveSupport # Returns the version of the currently loaded ActiveSupport as a Gem::Version def self.version - Gem::Version.new "4.1.0.beta" + Gem::Version.new "4.1.0.beta1" end module VERSION #:nodoc: diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb index 65de48a7f6..0b393e0c7a 100644 --- a/activesupport/test/abstract_unit.rb +++ b/activesupport/test/abstract_unit.rb @@ -15,7 +15,6 @@ silence_warnings do end require 'active_support/testing/autorun' -require 'empty_bool' ENV['NO_RELOAD'] = '1' require 'active_support' @@ -25,6 +24,9 @@ Thread.abort_on_exception = true # Show backtraces for deprecated behavior for quicker cleanup. ActiveSupport::Deprecation.debug = true +# Disable available locale checks to avoid warnings running the test suite. +I18n.enforce_available_locales = false + # Skips the current run on Rubinius using Minitest::Assertions#skip def rubinius_skip(message = '') skip message if RUBY_ENGINE == 'rbx' @@ -32,5 +34,5 @@ end # Skips the current run on JRuby using Minitest::Assertions#skip def jruby_skip(message = '') - skip message if RUBY_ENGINE == 'jruby' + skip message if defined?(JRUBY_VERSION) end diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index 51007402a1..c3c65cf805 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -3,6 +3,42 @@ require 'abstract_unit' require 'active_support/cache' require 'dependencies_test_helpers' +module ActiveSupport + module Cache + module Strategy + module LocalCache + class MiddlewareTest < ActiveSupport::TestCase + def test_local_cache_cleared_on_close + key = "super awesome key" + assert_nil LocalCacheRegistry.cache_for key + middleware = Middleware.new('<3', key).new(->(env) { + assert LocalCacheRegistry.cache_for(key), 'should have a cache' + [200, {}, []] + }) + _, _, body = middleware.call({}) + assert LocalCacheRegistry.cache_for(key), 'should still have a cache' + body.each { } + assert LocalCacheRegistry.cache_for(key), 'should still have a cache' + body.close + assert_nil LocalCacheRegistry.cache_for(key) + end + + def test_local_cache_cleared_on_exception + key = "super awesome key" + assert_nil LocalCacheRegistry.cache_for key + middleware = Middleware.new('<3', key).new(->(env) { + assert LocalCacheRegistry.cache_for(key), 'should have a cache' + raise + }) + assert_raises(RuntimeError) { middleware.call({}) } + assert_nil LocalCacheRegistry.cache_for(key) + end + end + end + end + end +end + class CacheKeyTest < ActiveSupport::TestCase def test_entry_legacy_optional_ivars legacy = Class.new(ActiveSupport::Cache::Entry) do @@ -110,12 +146,12 @@ class CacheStoreSettingTest < ActiveSupport::TestCase assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) end - def test_mem_cache_fragment_cache_store_with_given_mem_cache_like_object + def test_mem_cache_fragment_cache_store_with_not_dalli_client Dalli::Client.expects(:new).never memcache = Object.new - def memcache.get() true end - store = ActiveSupport::Cache.lookup_store :mem_cache_store, memcache - assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + assert_raises(ArgumentError) do + ActiveSupport::Cache.lookup_store :mem_cache_store, memcache + end end def test_mem_cache_fragment_cache_store_with_multiple_servers @@ -577,6 +613,7 @@ module LocalCacheBehavior result = @cache.write('foo', 'bar') assert_equal 'bar', @cache.read('foo') # make sure 'foo' was written assert result + [200, {}, []] } app = @cache.middleware.new(app) app.call({}) diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb index f8e2ce22fa..32c2dfdfc0 100644 --- a/activesupport/test/callbacks_test.rb +++ b/activesupport/test/callbacks_test.rb @@ -1,27 +1,6 @@ require 'abstract_unit' module CallbacksTest - class Phone - include ActiveSupport::Callbacks - define_callbacks :save - - set_callback :save, :before, :before_save1 - set_callback :save, :after, :after_save1 - - def before_save1; self.history << :before; end - def after_save1; self.history << :after; end - - def save - run_callbacks :save do - raise 'boom' - end - end - - def history - @history ||= [] - end - end - class Record include ActiveSupport::Callbacks diff --git a/activesupport/test/configurable_test.rb b/activesupport/test/configurable_test.rb index d00273a028..ef847fc557 100644 --- a/activesupport/test/configurable_test.rb +++ b/activesupport/test/configurable_test.rb @@ -95,6 +95,20 @@ class ConfigurableActiveSupport < ActiveSupport::TestCase config_accessor "invalid attribute name" end end + + assert_raises NameError do + Class.new do + include ActiveSupport::Configurable + config_accessor "invalid\nattribute" + end + end + + assert_raises NameError do + Class.new do + include ActiveSupport::Configurable + config_accessor "invalid\n" + end + end end def assert_method_defined(object, method) diff --git a/activesupport/test/core_ext/blank_test.rb b/activesupport/test/core_ext/blank_test.rb index a68c074777..246bc7fa61 100644 --- a/activesupport/test/core_ext/blank_test.rb +++ b/activesupport/test/core_ext/blank_test.rb @@ -4,17 +4,29 @@ require 'abstract_unit' require 'active_support/core_ext/object/blank' class BlankTest < ActiveSupport::TestCase - BLANK = [ EmptyTrue.new, nil, false, '', ' ', " \n\t \r ", ' ', [], {} ] + class EmptyTrue + def empty? + 0 + end + end + + class EmptyFalse + def empty? + nil + end + end + + BLANK = [ EmptyTrue.new, nil, false, '', ' ', " \n\t \r ", ' ', "\u00a0", [], {} ] NOT = [ EmptyFalse.new, Object.new, true, 0, 1, 'a', [nil], { nil => 0 } ] def test_blank - BLANK.each { |v| assert v.blank?, "#{v.inspect} should be blank" } - NOT.each { |v| assert !v.blank?, "#{v.inspect} should not be blank" } + BLANK.each { |v| assert_equal true, v.blank?, "#{v.inspect} should be blank" } + NOT.each { |v| assert_equal false, v.blank?, "#{v.inspect} should not be blank" } end def test_present - BLANK.each { |v| assert !v.present?, "#{v.inspect} should not be present" } - NOT.each { |v| assert v.present?, "#{v.inspect} should be present" } + BLANK.each { |v| assert_equal false, v.present?, "#{v.inspect} should not be present" } + NOT.each { |v| assert_equal true, v.present?, "#{v.inspect} should be present" } end def test_presence diff --git a/activesupport/test/core_ext/class/delegating_attributes_test.rb b/activesupport/test/core_ext/class/delegating_attributes_test.rb index 148f82946c..0e0742d147 100644 --- a/activesupport/test/core_ext/class/delegating_attributes_test.rb +++ b/activesupport/test/core_ext/class/delegating_attributes_test.rb @@ -39,10 +39,13 @@ class DelegatingAttributesTest < ActiveSupport::TestCase end def test_simple_accessor_declaration_with_instance_reader_false + _instance_methods = single_class.public_instance_methods single_class.superclass_delegating_accessor :no_instance_reader, :instance_reader => false assert_respond_to single_class, :no_instance_reader assert_respond_to single_class, :no_instance_reader= - assert !single_class.public_instance_methods.map(&:to_s).include?("no_instance_reader") + assert !_instance_methods.include?(:no_instance_reader) + assert !_instance_methods.include?(:no_instance_reader?) + assert !_instance_methods.include?(:_no_instance_reader) end def test_working_with_simple_attributes diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb index 8eae8c832c..28ba33331e 100644 --- a/activesupport/test/core_ext/duration_test.rb +++ b/activesupport/test/core_ext/duration_test.rb @@ -53,12 +53,10 @@ class DurationTest < ActiveSupport::TestCase end def test_argument_error - 1.second.ago('') - flunk("no exception was raised") - rescue ArgumentError => e + e = assert_raise ArgumentError do + 1.second.ago('') + end assert_equal 'expected a time or date, got ""', e.message, "ensure ArgumentError is not being raised by dependencies.rb" - rescue Exception => e - flunk("ArgumentError should be raised, but we got #{e.class} instead") end def test_fractional_weeks diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index 1ee9fbf46e..40c8f03374 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -54,6 +54,8 @@ class HashExtTest < ActiveSupport::TestCase assert_respond_to h, :deep_stringify_keys! assert_respond_to h, :to_options assert_respond_to h, :to_options! + assert_respond_to h, :compact + assert_respond_to h, :compact! end def test_transform_keys @@ -865,6 +867,32 @@ class HashExtTest < ActiveSupport::TestCase original.expects(:delete).never original.except(:a) end + + 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) + end + + 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) + end end class IWriteMyOwnXML diff --git a/activesupport/test/core_ext/kernel_test.rb b/activesupport/test/core_ext/kernel_test.rb index b8951de402..18b251173f 100644 --- a/activesupport/test/core_ext/kernel_test.rb +++ b/activesupport/test/core_ext/kernel_test.rb @@ -38,6 +38,22 @@ class KernelTest < ActiveSupport::TestCase # Skip if we can't STDERR.tell end + def test_silence_stream + old_stream_position = STDOUT.tell + silence_stream(STDOUT) { STDOUT.puts 'hello world' } + assert_equal old_stream_position, STDOUT.tell + rescue Errno::ESPIPE + # Skip if we can't stream.tell + end + + def test_silence_stream_closes_file_descriptors + stream = StringIO.new + dup_stream = StringIO.new + stream.stubs(:dup).returns(dup_stream) + dup_stream.expects(:close) + silence_stream(stream) { stream.puts 'hello world' } + end + def test_quietly old_stdout_position, old_stderr_position = STDOUT.tell, STDERR.tell quietly do diff --git a/activesupport/test/core_ext/module/concerning_test.rb b/activesupport/test/core_ext/module/concerning_test.rb new file mode 100644 index 0000000000..c6863b24a4 --- /dev/null +++ b/activesupport/test/core_ext/module/concerning_test.rb @@ -0,0 +1,35 @@ +require 'abstract_unit' +require 'active_support/core_ext/module/concerning' + +class ConcerningTest < ActiveSupport::TestCase + def test_concern_shortcut_creates_a_module_but_doesnt_include_it + mod = Module.new { concern(:Foo) { } } + assert_kind_of Module, mod::Foo + assert mod::Foo.respond_to?(:included) + assert !mod.ancestors.include?(mod::Foo), mod.ancestors.inspect + end + + def test_concern_creates_a_module_extended_with_active_support_concern + klass = Class.new do + concern :Foo do + included { @foo = 1 } + def should_be_public; end + end + end + + # Declares a concern but doesn't include it + assert_kind_of Module, klass::Foo + assert !klass.ancestors.include?(klass::Foo), klass.ancestors.inspect + + # Public method visibility by default + assert klass::Foo.public_instance_methods.map(&:to_s).include?('should_be_public') + + # Calls included hook + assert_equal 1, Class.new { include klass::Foo }.instance_variable_get('@foo') + end + + def test_concerning_declares_a_concern_and_includes_it_immediately + klass = Class.new { concerning(:Foo) { } } + assert klass.ancestors.include?(klass::Foo), klass.ancestors.inspect + end +end diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb index 283b13ff8b..ff6e21854e 100644 --- a/activesupport/test/core_ext/module_test.rb +++ b/activesupport/test/core_ext/module_test.rb @@ -12,12 +12,6 @@ class Ab Constant3 = "Goodbye World" end -module Xy - class Bc - include One - end -end - module Yz module Zy class Cd @@ -251,6 +245,16 @@ class ModuleTest < ActiveSupport::TestCase end end + def test_delegation_line_number + _, line = Someone.instance_method(:foo).source_location + assert_equal Someone::FAILED_DELEGATE_LINE, line + end + + def test_delegate_line_with_nil + _, line = Someone.instance_method(:bar).source_location + assert_equal Someone::FAILED_DELEGATE_LINE_2, line + end + def test_delegation_exception_backtrace someone = Someone.new("foo", "bar") someone.foo diff --git a/activesupport/test/core_ext/name_error_test.rb b/activesupport/test/core_ext/name_error_test.rb index 03ce09f22a..7525f80cf0 100644 --- a/activesupport/test/core_ext/name_error_test.rb +++ b/activesupport/test/core_ext/name_error_test.rb @@ -3,18 +3,18 @@ require 'active_support/core_ext/name_error' class NameErrorTest < ActiveSupport::TestCase def test_name_error_should_set_missing_name - SomeNameThatNobodyWillUse____Really ? 1 : 0 - flunk "?!?!" - rescue NameError => exc + exc = assert_raise NameError do + SomeNameThatNobodyWillUse____Really ? 1 : 0 + end assert_equal "NameErrorTest::SomeNameThatNobodyWillUse____Really", exc.missing_name assert exc.missing_name?(:SomeNameThatNobodyWillUse____Really) assert exc.missing_name?("NameErrorTest::SomeNameThatNobodyWillUse____Really") end def test_missing_method_should_ignore_missing_name - some_method_that_does_not_exist - flunk "?!?!" - rescue NameError => exc + exc = assert_raise NameError do + some_method_that_does_not_exist + end assert !exc.missing_name?(:Foo) assert_nil exc.missing_name end diff --git a/activesupport/test/core_ext/object_and_class_ext_test.rb b/activesupport/test/core_ext/object_and_class_ext_test.rb index 8d748791e3..0f454fdd95 100644 --- a/activesupport/test/core_ext/object_and_class_ext_test.rb +++ b/activesupport/test/core_ext/object_and_class_ext_test.rb @@ -3,31 +3,6 @@ require 'active_support/time' require 'active_support/core_ext/object' require 'active_support/core_ext/class/subclasses' -class ClassA; end -class ClassB < ClassA; end -class ClassC < ClassB; end -class ClassD < ClassA; end - -class ClassI; end -class ClassJ < ClassI; end - -class ClassK -end -module Nested - class << self - def on_const_missing(&callback) - @on_const_missing = callback - end - private - def const_missing(mod_id) - @on_const_missing[mod_id] if @on_const_missing - super - end - end - class ClassL < ClassK - end -end - class ObjectTests < ActiveSupport::TestCase class DuckTime def acts_like_time? diff --git a/activesupport/test/core_ext/range_ext_test.rb b/activesupport/test/core_ext/range_ext_test.rb index 6d6afc85c4..150e6b65fb 100644 --- a/activesupport/test/core_ext/range_ext_test.rb +++ b/activesupport/test/core_ext/range_ext_test.rb @@ -112,4 +112,8 @@ class RangeTest < ActiveSupport::TestCase end end + def test_date_time_with_each + datetime = DateTime.now + assert ((datetime - 1.hour)..datetime).each {} + end end diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index 20e3d4802e..d4f8ba8cdd 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -166,56 +166,6 @@ class StringInflectionsTest < ActiveSupport::TestCase assert_equal 97, 'abc'.ord end - def test_access - s = "hello" - assert_equal "h", s.at(0) - - assert_equal "llo", s.from(2) - assert_equal "hel", s.to(2) - - assert_equal "h", s.first - assert_equal "he", s.first(2) - assert_equal "", s.first(0) - - assert_equal "o", s.last - assert_equal "llo", s.last(3) - assert_equal "hello", s.last(10) - assert_equal "", s.last(0) - - assert_equal 'x', 'x'.first - assert_equal 'x', 'x'.first(4) - - assert_equal 'x', 'x'.last - assert_equal 'x', 'x'.last(4) - end - - def test_access_returns_a_real_string - hash = {} - hash["h"] = true - hash["hello123".at(0)] = true - assert_equal %w(h), hash.keys - - hash = {} - hash["llo"] = true - hash["hello".from(2)] = true - assert_equal %w(llo), hash.keys - - hash = {} - hash["hel"] = true - hash["hello".to(2)] = true - assert_equal %w(hel), hash.keys - - hash = {} - hash["hello"] = true - hash["123hello".last(5)] = true - assert_equal %w(hello), hash.keys - - hash = {} - hash["hello"] = true - hash["hello123".first(5)] = true - assert_equal %w(hello), hash.keys - end - def test_starts_ends_with_alias s = "hello" assert s.starts_with?('h') @@ -295,6 +245,93 @@ class StringInflectionsTest < ActiveSupport::TestCase end end +class StringAccessTest < ActiveSupport::TestCase + test "#at with Fixnum, returns a substring of one character at that position" do + assert_equal "h", "hello".at(0) + end + + test "#at with Range, returns a substring containing characters at offsets" do + assert_equal "lo", "hello".at(-2..-1) + end + + test "#at with Regex, returns the matching portion of the string" do + assert_equal "lo", "hello".at(/lo/) + assert_equal nil, "hello".at(/nonexisting/) + end + + test "#from with positive Fixnum, returns substring from the given position to the end" do + assert_equal "llo", "hello".from(2) + end + + test "#from with negative Fixnum, position is counted from the end" do + assert_equal "lo", "hello".from(-2) + end + + test "#to with positive Fixnum, substring from the beginning to the given position" do + assert_equal "hel", "hello".to(2) + end + + test "#to with negative Fixnum, position is counted from the end" do + assert_equal "hell", "hello".to(-2) + end + + test "#from and #to can be combined" do + assert_equal "hello", "hello".from(0).to(-1) + assert_equal "ell", "hello".from(1).to(-2) + end + + test "#first returns the first character" do + assert_equal "h", "hello".first + assert_equal 'x', 'x'.first + end + + test "#first with Fixnum, returns a substring from the beginning to position" do + assert_equal "he", "hello".first(2) + assert_equal "", "hello".first(0) + assert_equal "hello", "hello".first(10) + assert_equal 'x', 'x'.first(4) + end + + test "#last returns the last character" do + assert_equal "o", "hello".last + assert_equal 'x', 'x'.last + end + + test "#last with Fixnum, returns a substring from the end to position" do + assert_equal "llo", "hello".last(3) + assert_equal "hello", "hello".last(10) + assert_equal "", "hello".last(0) + assert_equal 'x', 'x'.last(4) + end + + test "access returns a real string" do + hash = {} + hash["h"] = true + hash["hello123".at(0)] = true + assert_equal %w(h), hash.keys + + hash = {} + hash["llo"] = true + hash["hello".from(2)] = true + assert_equal %w(llo), hash.keys + + hash = {} + hash["hel"] = true + hash["hello".to(2)] = true + assert_equal %w(hel), hash.keys + + hash = {} + hash["hello"] = true + hash["123hello".last(5)] = true + assert_equal %w(hello), hash.keys + + hash = {} + hash["hello"] = true + hash["hello123".first(5)] = true + assert_equal %w(hello), hash.keys + end +end + class StringConversionsTest < ActiveSupport::TestCase def test_string_to_time with_env_tz "Europe/Moscow" do diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb index 41a1df084e..e0a4b1be3e 100644 --- a/activesupport/test/core_ext/time_ext_test.rb +++ b/activesupport/test/core_ext/time_ext_test.rb @@ -476,6 +476,13 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase assert_equal t, t.advance(:months => 0) end + def test_advance_gregorian_proleptic + assert_equal Time.local(1582,10,14,15,15,10), Time.local(1582,10,15,15,15,10).advance(:days => -1) + assert_equal Time.local(1582,10,15,15,15,10), Time.local(1582,10,14,15,15,10).advance(:days => 1) + assert_equal Time.local(1582,10,5,15,15,10), Time.local(1582,10,4,15,15,10).advance(:days => 1) + assert_equal Time.local(1582,10,4,15,15,10), Time.local(1582,10,5,15,15,10).advance(:days => -1) + end + def test_last_week with_env_tz 'US/Eastern' do assert_equal Time.local(2005,2,21), Time.local(2005,3,1,15,15,10).last_week diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index 5494824a40..8e25f1e2f2 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -1,6 +1,5 @@ require 'abstract_unit' require 'active_support/time' -require 'active_support/json' class TimeWithZoneTest < ActiveSupport::TestCase @@ -66,25 +65,6 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_equal 'EDT', ActiveSupport::TimeWithZone.new(Time.utc(2000, 6), @time_zone).zone #dst end - def test_to_json_with_use_standard_json_time_format_config_set_to_false - old, ActiveSupport.use_standard_json_time_format = ActiveSupport.use_standard_json_time_format, false - assert_equal "\"1999/12/31 19:00:00 -0500\"", ActiveSupport::JSON.encode(@twz) - ensure - ActiveSupport.use_standard_json_time_format = old - end - - def test_to_json_with_use_standard_json_time_format_config_set_to_true - old, ActiveSupport.use_standard_json_time_format = ActiveSupport.use_standard_json_time_format, true - assert_equal "\"1999-12-31T19:00:00.000-05:00\"", ActiveSupport::JSON.encode(@twz) - ensure - ActiveSupport.use_standard_json_time_format = old - end - - def test_to_json_when_wrapping_a_date_time - twz = ActiveSupport::TimeWithZone.new(DateTime.civil(2000), @time_zone) - assert_equal '"1999-12-31T19:00:00.000-05:00"', ActiveSupport::JSON.encode(twz) - end - def test_nsec local = Time.local(2011,6,7,23,59,59,Rational(999999999, 1000)) with_zone = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Hawaii"], local) @@ -131,6 +111,10 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_equal "1999-12-31T19:00:00.001234-05:00", @twz.xmlschema(12) end + def test_xmlschema_with_nil_fractional_seconds + assert_equal "1999-12-31T19:00:00-05:00", @twz.xmlschema(nil) + end + def test_to_yaml assert_match(/^--- 2000-01-01 00:00:00(\.0+)?\s*Z\n/, @twz.to_yaml) end @@ -511,6 +495,11 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_equal "Fri, 31 Dec 1999 19:00:30 EST -05:00", @twz.change(:sec => 30).inspect end + def test_change_at_dst_boundary + twz = ActiveSupport::TimeWithZone.new(Time.at(1319936400).getutc, ActiveSupport::TimeZone['Madrid']) + assert_equal twz, twz.change(:min => 0) + end + def test_advance assert_equal "Fri, 31 Dec 1999 19:00:00 EST -05:00", @twz.inspect assert_equal "Mon, 31 Dec 2001 19:00:00 EST -05:00", @twz.advance(:years => 2).inspect diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index 2392b71960..00bec5bd9d 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -73,12 +73,11 @@ class DependenciesTest < ActiveSupport::TestCase $raises_exception_load_count = 0 5.times do |count| - begin + e = assert_raise Exception, 'should have loaded dependencies/raises_exception which raises an exception' do require_dependency filename - flunk 'should have loaded dependencies/raises_exception which raises an exception' - rescue Exception => e - assert_equal 'Loading me failed, so do not add to loaded or history.', e.message end + + assert_equal 'Loading me failed, so do not add to loaded or history.', e.message assert_equal count + 1, $raises_exception_load_count assert !ActiveSupport::Dependencies.loaded.include?(filename) @@ -366,26 +365,19 @@ class DependenciesTest < ActiveSupport::TestCase def test_non_existing_const_raises_name_error_with_fully_qualified_name with_autoloading_fixtures do - begin - A::DoesNotExist.nil? - flunk "No raise!!" - rescue NameError => e - assert_equal "uninitialized constant A::DoesNotExist", e.message - end - begin - A::B::DoesNotExist.nil? - flunk "No raise!!" - rescue NameError => e - assert_equal "uninitialized constant A::B::DoesNotExist", e.message - end + e = assert_raise(NameError) { A::DoesNotExist.nil? } + assert_equal "uninitialized constant A::DoesNotExist", e.message + + e = assert_raise(NameError) { A::B::DoesNotExist.nil? } + assert_equal "uninitialized constant A::B::DoesNotExist", e.message end end def test_smart_name_error_strings - Object.module_eval "ImaginaryObject" - flunk "No raise!!" - rescue NameError => e - assert e.message.include?("uninitialized constant ImaginaryObject") + e = assert_raise NameError do + Object.module_eval "ImaginaryObject" + end + assert_includes "uninitialized constant ImaginaryObject", e.message end def test_loadable_constants_for_path_should_handle_empty_autoloads @@ -530,29 +522,21 @@ class DependenciesTest < ActiveSupport::TestCase end end - def test_const_missing_should_not_double_load - $counting_loaded_times = 0 + def test_const_missing_in_anonymous_modules_loads_top_level_constants with_autoloading_fixtures do - require_dependency '././counting_loader' - assert_equal 1, $counting_loaded_times - assert_raise(NameError) { ActiveSupport::Dependencies.load_missing_constant Object, :CountingLoader } - assert_equal 1, $counting_loaded_times + # class_eval STRING pushes the class to the nesting of the eval'ed code. + klass = Class.new.class_eval "E" + assert_equal E, klass end end - def test_const_missing_within_anonymous_module - $counting_loaded_times = 0 - m = Module.new - m.module_eval "def a() CountingLoader; end" - extend m + def test_const_missing_in_anonymous_modules_raises_if_the_constant_belongs_to_Object with_autoloading_fixtures do - kls = nil - assert_nothing_raised { kls = a } - assert_equal "CountingLoader", kls.name - assert_equal 1, $counting_loaded_times + require_dependency 'e' - assert_nothing_raised { kls = a } - assert_equal 1, $counting_loaded_times + mod = Module.new + e = assert_raise(NameError) { mod::E } + assert_equal 'E cannot be autoloaded from an anonymous class or module', e.message end end @@ -561,12 +545,10 @@ class DependenciesTest < ActiveSupport::TestCase c = ServiceOne ActiveSupport::Dependencies.clear assert ! defined?(ServiceOne) - begin + e = assert_raise ArgumentError do ActiveSupport::Dependencies.load_missing_constant(c, :FakeMissing) - flunk "Expected exception" - rescue ArgumentError => e - assert_match %r{ServiceOne has been removed from the module tree}i, e.message end + assert_match %r{ServiceOne has been removed from the module tree}i, e.message end end @@ -905,12 +887,10 @@ class DependenciesTest < ActiveSupport::TestCase with_autoloading_fixtures do Object.send(:remove_const, :RaisesNameError) if defined?(::RaisesNameError) 2.times do - begin + e = assert_raise NameError do ::RaisesNameError::FooBarBaz.object_id - flunk 'should have raised NameError when autoloaded file referenced FooBarBaz' - rescue NameError => e - assert_equal 'uninitialized constant RaisesNameError::FooBarBaz', e.message end + assert_equal 'uninitialized constant RaisesNameError::FooBarBaz', e.message assert !defined?(::RaisesNameError), "::RaisesNameError is defined but it should have failed!" end diff --git a/activesupport/test/deprecation_test.rb b/activesupport/test/deprecation_test.rb index 9674851b9d..ee1c69502e 100644 --- a/activesupport/test/deprecation_test.rb +++ b/activesupport/test/deprecation_test.rb @@ -104,14 +104,11 @@ class DeprecationTest < ActiveSupport::TestCase message = 'Revise this deprecated stuff now!' callstack = %w(foo bar baz) - begin + e = assert_raise ActiveSupport::DeprecationException do ActiveSupport::Deprecation.behavior.first.call(message, callstack) - rescue ActiveSupport::DeprecationException => e - assert_equal message, e.message - assert_equal callstack, e.backtrace - else - flunk 'the :raise deprecation behaviour should raise the expected exception' end + assert_equal message, e.message + assert_equal callstack, e.backtrace end def test_default_stderr_behavior @@ -174,7 +171,7 @@ class DeprecationTest < ActiveSupport::TestCase ActiveSupport::Deprecation.warn 'abc' ActiveSupport::Deprecation.warn 'def' end - rescue MiniTest::Assertion + rescue Minitest::Assertion flunk 'assert_deprecated should match any warning in block, not just the last one' end diff --git a/activesupport/test/empty_bool.rb b/activesupport/test/empty_bool.rb deleted file mode 100644 index 005b3523ef..0000000000 --- a/activesupport/test/empty_bool.rb +++ /dev/null @@ -1,7 +0,0 @@ -class EmptyTrue - def empty?() true; end -end - -class EmptyFalse - def empty?() false; end -end diff --git a/activesupport/test/fixtures/custom.rb b/activesupport/test/fixtures/custom.rb deleted file mode 100644 index 0eefce0c25..0000000000 --- a/activesupport/test/fixtures/custom.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Custom -end
\ No newline at end of file diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb index 78cf4819f9..c4283ee79a 100644 --- a/activesupport/test/json/encoding_test.rb +++ b/activesupport/test/json/encoding_test.rb @@ -3,6 +3,7 @@ require 'securerandom' require 'abstract_unit' require 'active_support/core_ext/string/inflections' require 'active_support/json' +require 'active_support/time' class TestJSONEncoding < ActiveSupport::TestCase class Foo @@ -226,21 +227,17 @@ class TestJSONEncoding < ActiveSupport::TestCase end def test_time_to_json_includes_local_offset - prev = ActiveSupport.use_standard_json_time_format - ActiveSupport.use_standard_json_time_format = true - with_env_tz 'US/Eastern' do - assert_equal %("2005-02-01T15:15:10.000-05:00"), ActiveSupport::JSON.encode(Time.local(2005,2,1,15,15,10)) + with_standard_json_time_format(true) do + with_env_tz 'US/Eastern' do + assert_equal %("2005-02-01T15:15:10.000-05:00"), ActiveSupport::JSON.encode(Time.local(2005,2,1,15,15,10)) + end end - ensure - ActiveSupport.use_standard_json_time_format = prev end def test_hash_with_time_to_json - prev = ActiveSupport.use_standard_json_time_format - ActiveSupport.use_standard_json_time_format = false - assert_equal '{"time":"2009/01/01 00:00:00 +0000"}', { :time => Time.utc(2009) }.to_json - ensure - ActiveSupport.use_standard_json_time_format = prev + with_standard_json_time_format(false) do + assert_equal '{"time":"2009/01/01 00:00:00 +0000"}', { :time => Time.utc(2009) }.to_json + end end def test_nested_hash_with_float @@ -453,6 +450,57 @@ EXPECTED assert_nil h.as_json_called end + def test_twz_to_json_with_use_standard_json_time_format_config_set_to_false + with_standard_json_time_format(false) do + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone) + assert_equal "\"1999/12/31 19:00:00 -0500\"", ActiveSupport::JSON.encode(time) + end + end + + def test_twz_to_json_with_use_standard_json_time_format_config_set_to_true + with_standard_json_time_format(true) do + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone) + assert_equal "\"1999-12-31T19:00:00.000-05:00\"", ActiveSupport::JSON.encode(time) + end + end + + def test_twz_to_json_with_custom_time_precision + with_standard_json_time_format(true) do + ActiveSupport::JSON::Encoding.time_precision = 0 + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone) + assert_equal "\"1999-12-31T19:00:00-05:00\"", ActiveSupport::JSON.encode(time) + end + ensure + ActiveSupport::JSON::Encoding.time_precision = 3 + end + + def test_time_to_json_with_custom_time_precision + with_standard_json_time_format(true) do + ActiveSupport::JSON::Encoding.time_precision = 0 + assert_equal "\"2000-01-01T00:00:00Z\"", ActiveSupport::JSON.encode(Time.utc(2000)) + end + ensure + ActiveSupport::JSON::Encoding.time_precision = 3 + end + + def test_datetime_to_json_with_custom_time_precision + with_standard_json_time_format(true) do + ActiveSupport::JSON::Encoding.time_precision = 0 + assert_equal "\"2000-01-01T00:00:00+00:00\"", ActiveSupport::JSON.encode(DateTime.new(2000)) + end + ensure + ActiveSupport::JSON::Encoding.time_precision = 3 + end + + def test_twz_to_json_when_wrapping_a_date_time + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + time = ActiveSupport::TimeWithZone.new(DateTime.new(2000), zone) + assert_equal '"1999-12-31T19:00:00.000-05:00"', ActiveSupport::JSON.encode(time) + end + protected def object_keys(json_object) @@ -465,4 +513,11 @@ EXPECTED ensure old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') end + + def with_standard_json_time_format(boolean = true) + old, ActiveSupport.use_standard_json_time_format = ActiveSupport.use_standard_json_time_format, boolean + yield + ensure + ActiveSupport.use_standard_json_time_format = old + end end diff --git a/activesupport/test/message_encryptor_test.rb b/activesupport/test/message_encryptor_test.rb index 203156baa1..b6c0a08b05 100644 --- a/activesupport/test/message_encryptor_test.rb +++ b/activesupport/test/message_encryptor_test.rb @@ -66,6 +66,17 @@ class MessageEncryptorTest < ActiveSupport::TestCase ActiveSupport.use_standard_json_time_format = prev end + def test_message_obeys_strict_encoding + bad_encoding_characters = "\n!@#" + message, iv = @encryptor.encrypt_and_sign("This is a very \n\nhumble string"+bad_encoding_characters) + + assert_not_decrypted("#{::Base64.encode64 message.to_s}--#{::Base64.encode64 iv.to_s}") + assert_not_verified("#{::Base64.encode64 message.to_s}--#{::Base64.encode64 iv.to_s}") + + assert_not_decrypted([iv, message] * bad_encoding_characters) + assert_not_verified([iv, message] * bad_encoding_characters) + end + private def assert_not_decrypted(value) @@ -81,7 +92,7 @@ class MessageEncryptorTest < ActiveSupport::TestCase end def munge(base64_string) - bits = ::Base64.decode64(base64_string) + bits = ::Base64.strict_decode64(base64_string) bits.reverse! ::Base64.strict_encode64(bits) end diff --git a/activesupport/test/message_verifier_test.rb b/activesupport/test/message_verifier_test.rb index f208814468..a5748d28ba 100644 --- a/activesupport/test/message_verifier_test.rb +++ b/activesupport/test/message_verifier_test.rb @@ -55,6 +55,20 @@ class MessageVerifierTest < ActiveSupport::TestCase ActiveSupport.use_standard_json_time_format = prev end + def test_raise_error_when_argument_class_is_not_loaded + # To generate the valid message below: + # + # AutoloadClass = Struct.new(:foo) + # valid_message = @verifier.generate(foo: AutoloadClass.new('foo')) + # + valid_message = "BAh7BjoIZm9vbzonTWVzc2FnZVZlcmlmaWVyVGVzdDo6QXV0b2xvYWRDbGFzcwY6CUBmb29JIghmb28GOgZFVA==--f3ef39a5241c365083770566dc7a9eb5d6ace914" + exception = assert_raise(ArgumentError, NameError) do + @verifier.verify(valid_message) + end + assert_includes ["uninitialized constant MessageVerifierTest::AutoloadClass", + "undefined class/module MessageVerifierTest::AutoloadClass"], exception.message + end + def assert_not_verified(message) assert_raise(ActiveSupport::MessageVerifier::InvalidSignature) do @verifier.verify(message) diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb index 2bf73291a2..659fceb852 100644 --- a/activesupport/test/multibyte_chars_test.rb +++ b/activesupport/test/multibyte_chars_test.rb @@ -221,9 +221,9 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase end def test_include_raises_when_nil_is_passed - @chars.include?(nil) - flunk "Expected chars.include?(nil) to raise TypeError or NoMethodError" - rescue Exception + assert_raises TypeError, NoMethodError, "Expected chars.include?(nil) to raise TypeError or NoMethodError" do + @chars.include?(nil) + end end def test_index_should_return_character_offset diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb index 111db59b2b..6d8d835de7 100644 --- a/activesupport/test/number_helper_test.rb +++ b/activesupport/test/number_helper_test.rb @@ -123,6 +123,12 @@ module ActiveSupport assert_equal("10.00", number_helper.number_to_rounded(9.995, :precision => 2)) assert_equal("11.00", number_helper.number_to_rounded(10.995, :precision => 2)) assert_equal("0.00", number_helper.number_to_rounded(-0.001, :precision => 2)) + + assert_equal("111.23460000000000000000", number_helper.number_to_rounded(111.2346, :precision => 20)) + assert_equal("111.23460000000000000000", number_helper.number_to_rounded(Rational(1112346, 10000), :precision => 20)) + assert_equal("111.23460000000000000000", number_helper.number_to_rounded('111.2346', :precision => 20)) + assert_equal("111.23460000000000000000", number_helper.number_to_rounded(BigDecimal(111.2346, Float::DIG), :precision => 20)) + assert_equal("111.2346" + "0"*96, number_helper.number_to_rounded('111.2346', :precision => 100)) end end @@ -155,6 +161,14 @@ module ActiveSupport assert_equal "10.0", number_helper.number_to_rounded(9.995, :precision => 3, :significant => true) assert_equal "9.99", number_helper.number_to_rounded(9.994, :precision => 3, :significant => true) assert_equal "11.0", number_helper.number_to_rounded(10.995, :precision => 3, :significant => true) + + assert_equal "9775.0000000000000000", number_helper.number_to_rounded(9775, :precision => 20, :significant => true ) + assert_equal "9775.0000000000000000", number_helper.number_to_rounded(9775.0, :precision => 20, :significant => true ) + assert_equal "9775.0000000000000000", number_helper.number_to_rounded(Rational(9775, 1), :precision => 20, :significant => true ) + assert_equal "97.750000000000000000", number_helper.number_to_rounded(Rational(9775, 100), :precision => 20, :significant => true ) + assert_equal "9775.0000000000000000", number_helper.number_to_rounded(BigDecimal(9775), :precision => 20, :significant => true ) + assert_equal "9775.0000000000000000", number_helper.number_to_rounded("9775", :precision => 20, :significant => true ) + assert_equal "9775." + "0"*96, number_helper.number_to_rounded("9775", :precision => 100, :significant => true ) end end diff --git a/activesupport/test/safe_buffer_test.rb b/activesupport/test/safe_buffer_test.rb index 047b89be2a..efa9d5e61f 100644 --- a/activesupport/test/safe_buffer_test.rb +++ b/activesupport/test/safe_buffer_test.rb @@ -140,4 +140,29 @@ class SafeBufferTest < ActiveSupport::TestCase # should still be unsafe assert !y.html_safe?, "should not be safe" end + + test 'Should work with interpolation (array argument)' do + x = 'foo %s bar'.html_safe % ['qux'] + assert_equal 'foo qux bar', x + end + + test 'Should work with interpolation (hash argument)' do + x = 'foo %{x} bar'.html_safe % { x: 'qux' } + assert_equal 'foo qux bar', x + end + + test 'Should escape unsafe interpolated args' do + x = 'foo %{x} bar'.html_safe % { x: '<br/>' } + assert_equal 'foo <br/> bar', x + end + + test 'Should not escape safe interpolated args' do + x = 'foo %{x} bar'.html_safe % { x: '<br/>'.html_safe } + assert_equal 'foo <br/> bar', x + end + + test 'Should interpolate to a safe string' do + x = 'foo %{x} bar'.html_safe % { x: 'qux' } + assert x.html_safe?, 'should be safe' + end end diff --git a/activesupport/test/test_test.rb b/activesupport/test/test_test.rb index 5ed2da7e8b..8a71ef4324 100644 --- a/activesupport/test/test_test.rb +++ b/activesupport/test/test_test.rb @@ -21,10 +21,10 @@ class AssertDifferenceTest < ActiveSupport::TestCase assert_equal true, assert_not(nil) assert_equal true, assert_not(false) - e = assert_raises(MiniTest::Assertion) { assert_not true } + e = assert_raises(Minitest::Assertion) { assert_not true } assert_equal 'Expected true to be nil or false', e.message - e = assert_raises(MiniTest::Assertion) { assert_not true, 'custom' } + e = assert_raises(Minitest::Assertion) { assert_not true, 'custom' } assert_equal 'custom', e.message end @@ -73,7 +73,7 @@ class AssertDifferenceTest < ActiveSupport::TestCase end def test_array_of_expressions_identify_failure - assert_raises(MiniTest::Assertion) do + assert_raises(Minitest::Assertion) do assert_difference ['@object.num', '1 + 1'] do @object.increment end @@ -81,7 +81,7 @@ class AssertDifferenceTest < ActiveSupport::TestCase end def test_array_of_expressions_identify_failure_when_message_provided - assert_raises(MiniTest::Assertion) do + assert_raises(Minitest::Assertion) do assert_difference ['@object.num', '1 + 1'], 1, 'something went wrong' do @object.increment end diff --git a/activesupport/test/testing/constant_lookup_test.rb b/activesupport/test/testing/constant_lookup_test.rb index aca2951450..71a9561189 100644 --- a/activesupport/test/testing/constant_lookup_test.rb +++ b/activesupport/test/testing/constant_lookup_test.rb @@ -6,7 +6,6 @@ class Bar < Foo def index; end def self.index; end end -class Baz < Bar; end module FooBar; end class ConstantLookupTest < ActiveSupport::TestCase diff --git a/guides/assets/images/getting_started/rails_welcome.jpg b/guides/assets/images/getting_started/rails_welcome.jpg Binary files differnew file mode 100644 index 0000000000..65a44cdfe5 --- /dev/null +++ b/guides/assets/images/getting_started/rails_welcome.jpg diff --git a/guides/assets/images/getting_started/rails_welcome.png b/guides/assets/images/getting_started/rails_welcome.png Binary files differdeleted file mode 100644 index 569dd846a8..0000000000 --- a/guides/assets/images/getting_started/rails_welcome.png +++ /dev/null diff --git a/guides/bug_report_templates/action_controller_gem.rb b/guides/bug_report_templates/action_controller_gem.rb index 89ac28671a..9387e3dc1d 100644 --- a/guides/bug_report_templates/action_controller_gem.rb +++ b/guides/bug_report_templates/action_controller_gem.rb @@ -29,7 +29,10 @@ end require 'minitest/autorun' require 'rack/test' -class BugTest < MiniTest::Unit::TestCase +# Ensure backward compatibility with Minitest 4 +Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test) + +class BugTest < Minitest::Test include Rack::Test::Methods def test_returns_success @@ -41,4 +44,4 @@ class BugTest < MiniTest::Unit::TestCase def app Rails.application end -end
\ No newline at end of file +end diff --git a/guides/bug_report_templates/active_record_gem.rb b/guides/bug_report_templates/active_record_gem.rb index a8868a1877..d72633d0b2 100644 --- a/guides/bug_report_templates/active_record_gem.rb +++ b/guides/bug_report_templates/active_record_gem.rb @@ -4,6 +4,9 @@ require 'active_record' require 'minitest/autorun' require 'logger' +# Ensure backward compatibility with Minitest 4 +Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test) + # This connection will do for database-independent bug reports. ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') ActiveRecord::Base.logger = Logger.new(STDOUT) @@ -25,7 +28,7 @@ class Comment < ActiveRecord::Base belongs_to :post end -class BugTest < MiniTest::Unit::TestCase +class BugTest < Minitest::Test def test_association_stuff post = Post.create! post.comments << Comment.create! diff --git a/guides/code/getting_started/Gemfile b/guides/code/getting_started/Gemfile index eef6180804..ecb6e7aa1a 100644 --- a/guides/code/getting_started/Gemfile +++ b/guides/code/getting_started/Gemfile @@ -1,43 +1,40 @@ source 'https://rubygems.org' -gem 'rails', '4.0.0' +# Bundle edge Rails instead: gem 'rails', github: 'rails/rails' +gem 'rails', '4.1.0' # Use sqlite3 as the database for Active Record gem 'sqlite3' - # Use SCSS for stylesheets -gem 'sass-rails' - +gem 'sass-rails', '~> 4.0.1' +# Use Uglifier as compressor for JavaScript assets +gem 'uglifier', '>= 1.3.0' # Use CoffeeScript for .js.coffee assets and views -gem 'coffee-rails' - +gem 'coffee-rails', '~> 4.0.0' # See https://github.com/sstephenson/execjs#readme for more supported runtimes -# gem 'therubyracer', platforms: :ruby - -# Use Uglifier as compressor for JavaScript assets -gem 'uglifier', '>= 1.0.3' +# gem 'therubyracer', platforms: :ruby +# Use jquery as the JavaScript library gem 'jquery-rails' - # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks gem 'turbolinks' - -group :doc do - # bundle exec rake doc:rails generates the API under doc/api. - gem 'sdoc', require: false -end - # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder -gem 'jbuilder', '~> 1.2' +gem 'jbuilder', '~> 2.0' +# bundle exec rake doc:rails generates the API under doc/api. +gem 'sdoc', '~> 0.4.0', group: :doc -# To use ActiveModel has_secure_password +# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring +gem 'spring', group: :development + +# Use ActiveModel has_secure_password # gem 'bcrypt-ruby', '~> 3.1.2' # Use unicorn as the app server # gem 'unicorn' -# Deploy with Capistrano -# gem 'capistrano', group: :development +# Use Capistrano for deployment +# gem 'capistrano-rails', group: :development + +# Use debugger +# gem 'debugger', group: [:development, :test] -# To use debugger -# gem 'debugger' diff --git a/guides/code/getting_started/Gemfile.lock b/guides/code/getting_started/Gemfile.lock index 2d5c50ef5c..a2ab76c908 100644 --- a/guides/code/getting_started/Gemfile.lock +++ b/guides/code/getting_started/Gemfile.lock @@ -1,120 +1,126 @@ GEM remote: https://rubygems.org/ specs: - actionmailer (4.0.0) - actionpack (= 4.0.0) - mail (~> 2.5.3) - actionpack (4.0.0) - activesupport (= 4.0.0) - builder (~> 3.1.0) - erubis (~> 2.7.0) + actionmailer (4.1.0) + actionpack (= 4.1.0) + actionview (= 4.1.0) + mail (~> 2.5.4) + actionpack (4.1.0) + actionview (= 4.1.0) + activesupport (= 4.1.0) rack (~> 1.5.2) rack-test (~> 0.6.2) - activemodel (4.0.0) - activesupport (= 4.0.0) - builder (~> 3.1.0) - activerecord (4.0.0) - activemodel (= 4.0.0) - activerecord-deprecated_finders (~> 1.0.2) - activesupport (= 4.0.0) - arel (~> 4.0.0) - activerecord-deprecated_finders (1.0.3) - activesupport (4.0.0) - i18n (~> 0.6, >= 0.6.4) - minitest (~> 4.2) - multi_json (~> 1.3) + actionview (4.1.0) + activesupport (= 4.1.0) + builder (~> 3.1) + erubis (~> 2.7.0) + activemodel (4.1.0) + activesupport (= 4.1.0) + builder (~> 3.1) + activerecord (4.1.0) + activemodel (= 4.1.0) + activesupport (= 4.1.0) + arel (~> 5.0.0) + activesupport (4.1.0) + i18n (~> 0.6, >= 0.6.9) + json (~> 1.7, >= 1.7.7) + minitest (~> 5.1) thread_safe (~> 0.1) - tzinfo (~> 0.3.37) - arel (4.0.0) - atomic (1.1.10) - builder (3.1.4) - coffee-rails (4.0.0) + tzinfo (~> 1.1) + arel (5.0.0) + atomic (1.1.14) + builder (3.2.2) + coffee-rails (4.0.1) coffee-script (>= 2.2.0) - railties (>= 4.0.0.beta, < 5.0) + railties (>= 4.0.0, < 5.0) coffee-script (2.2.0) coffee-script-source execjs coffee-script-source (1.6.3) erubis (2.7.0) - execjs (1.4.0) - multi_json (~> 1.0) + execjs (2.0.2) hike (1.2.3) - i18n (0.6.4) - jbuilder (1.4.2) + i18n (0.6.9) + jbuilder (2.0.2) activesupport (>= 3.0.0) multi_json (>= 1.2.0) - jquery-rails (3.0.2) + jquery-rails (3.0.4) railties (>= 3.0, < 5.0) thor (>= 0.14, < 2.0) - json (1.8.0) + json (1.8.1) mail (2.5.4) mime-types (~> 1.16) treetop (~> 1.4.8) - mime-types (1.23) - minitest (4.7.5) - multi_json (1.7.7) + mime-types (1.25.1) + minitest (5.2.1) + multi_json (1.8.4) polyglot (0.3.3) rack (1.5.2) rack-test (0.6.2) rack (>= 1.0) - rails (4.0.0) - actionmailer (= 4.0.0) - actionpack (= 4.0.0) - activerecord (= 4.0.0) - activesupport (= 4.0.0) + rails (4.1.0) + actionmailer (= 4.1.0) + actionpack (= 4.1.0) + actionview (= 4.1.0) + activemodel (= 4.1.0) + activerecord (= 4.1.0) + activesupport (= 4.1.0) bundler (>= 1.3.0, < 2.0) - railties (= 4.0.0) + railties (= 4.1.0) sprockets-rails (~> 2.0.0) - railties (4.0.0) - actionpack (= 4.0.0) - activesupport (= 4.0.0) + railties (4.1.0) + actionpack (= 4.1.0) + activesupport (= 4.1.0) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rake (10.1.0) - rdoc (3.12.2) + rake (10.1.1) + rdoc (4.1.1) json (~> 1.4) - sass (3.2.9) - sass-rails (4.0.0) - railties (>= 4.0.0.beta, < 5.0) + sass (3.2.13) + sass-rails (4.0.1) + railties (>= 4.0.0, < 5.0) sass (>= 3.1.10) sprockets-rails (~> 2.0.0) - sdoc (0.3.20) - json (>= 1.1.3) - rdoc (~> 3.10) - sprockets (2.10.0) + sdoc (0.4.0) + json (~> 1.8) + rdoc (~> 4.0, < 5.0) + spring (1.0.0) + sprockets (2.10.1) hike (~> 1.2) multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) - sprockets-rails (2.0.0) + sprockets-rails (2.0.1) actionpack (>= 3.0) activesupport (>= 3.0) sprockets (~> 2.8) - sqlite3 (1.3.7) + sqlite3 (1.3.8) thor (0.18.1) - thread_safe (0.1.0) + thread_safe (0.1.3) atomic tilt (1.4.1) - treetop (1.4.14) + treetop (1.4.15) polyglot polyglot (>= 0.3.1) - turbolinks (1.2.0) + turbolinks (2.2.0) coffee-rails - tzinfo (0.3.37) - uglifier (2.1.1) + tzinfo (1.1.0) + thread_safe (~> 0.1) + uglifier (2.4.0) execjs (>= 0.3.0) - multi_json (~> 1.0, >= 1.0.2) + json (>= 1.8.0) PLATFORMS ruby DEPENDENCIES - coffee-rails - jbuilder (~> 1.2) + coffee-rails (~> 4.0.0) + jbuilder (~> 2.0) jquery-rails - rails (= 4.0.0) - sass-rails - sdoc + rails (= 4.1.0) + sass-rails (~> 4.0.1) + sdoc (~> 0.4.0) + spring sqlite3 turbolinks - uglifier (>= 1.0.3) + uglifier (>= 1.3.0) diff --git a/guides/code/getting_started/app/views/layouts/application.html.erb b/guides/code/getting_started/app/views/layouts/application.html.erb index 95368c37a3..d0ba8415e6 100644 --- a/guides/code/getting_started/app/views/layouts/application.html.erb +++ b/guides/code/getting_started/app/views/layouts/application.html.erb @@ -2,8 +2,8 @@ <html> <head> <title>Blog</title> - <%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %> - <%= javascript_include_tag "application", "data-turbolinks-track" => true %> + <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> + <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> <%= csrf_meta_tags %> </head> <body> diff --git a/guides/code/getting_started/config/environments/production.rb b/guides/code/getting_started/config/environments/production.rb index 368a735122..8c514e065e 100644 --- a/guides/code/getting_started/config/environments/production.rb +++ b/guides/code/getting_started/config/environments/production.rb @@ -5,7 +5,7 @@ Blog::Application.configure do config.cache_classes = true # Eager load code on boot. This eager loads most of Rails and - # your application in memory, allowing both thread web servers + # your application in memory, allowing both threaded web servers # and those relying on copy on write to perform better. # Rake tasks automatically ignore this option for performance. config.eager_load = true @@ -66,7 +66,7 @@ Blog::Application.configure do # config.action_mailer.raise_delivery_errors = false # Enable locale fallbacks for I18n (makes lookups for any locale fall back to - # the I18n.default_locale when a translation can not be found). + # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true # Send deprecation notices to registered listeners. diff --git a/guides/code/getting_started/config/environments/test.rb b/guides/code/getting_started/config/environments/test.rb index 00adaa5015..34ab1530d1 100644 --- a/guides/code/getting_started/config/environments/test.rb +++ b/guides/code/getting_started/config/environments/test.rb @@ -14,7 +14,7 @@ Blog::Application.configure do # Configure static asset server for tests with Cache-Control for performance. config.serve_static_assets = true - config.static_cache_control = "public, max-age=3600" + config.static_cache_control = 'public, max-age=3600' # Show full error reports and disable caching. config.consider_all_requests_local = true diff --git a/guides/source/3_0_release_notes.md b/guides/source/3_0_release_notes.md index cf9d694de7..dd81ec58f9 100644 --- a/guides/source/3_0_release_notes.md +++ b/guides/source/3_0_release_notes.md @@ -574,7 +574,7 @@ The following methods have been removed because they are no longer used in the f Action Mailer ------------- -Action Mailer has been given a new API with TMail being replaced out with the new [Mail](http://github.com/mikel/mail) as the Email library. Action Mailer itself has been given an almost complete re-write with pretty much every line of code touched. The result is that Action Mailer now simply inherits from Abstract Controller and wraps the Mail gem in a Rails DSL. This reduces the amount of code and duplication of other libraries in Action Mailer considerably. +Action Mailer has been given a new API with TMail being replaced out with the new [Mail](http://github.com/mikel/mail) as the email library. Action Mailer itself has been given an almost complete re-write with pretty much every line of code touched. The result is that Action Mailer now simply inherits from Abstract Controller and wraps the Mail gem in a Rails DSL. This reduces the amount of code and duplication of other libraries in Action Mailer considerably. * All mailers are now in `app/mailers` by default. * Can now send email using new API with three methods: `attachments`, `headers` and `mail`. diff --git a/guides/source/3_1_release_notes.md b/guides/source/3_1_release_notes.md index 5c99892e39..485f8c756b 100644 --- a/guides/source/3_1_release_notes.md +++ b/guides/source/3_1_release_notes.md @@ -286,7 +286,7 @@ Action Pack end ``` - You can restrict it to some actions by using `:only` or `:except`. Please read the docs at [`ActionController::Streaming`](http://api.rubyonrails.org/classes/ActionController/Streaming.html) for more information. + You can restrict it to some actions by using `:only` or `:except`. Please read the docs at [`ActionController::Streaming`](http://api.rubyonrails.org/v3.1.0/classes/ActionController/Streaming.html) for more information. * The redirect route method now also accepts a hash of options which will only change the parts of the url in question, or an object which responds to call, allowing for redirects to be reused. diff --git a/guides/source/3_2_release_notes.md b/guides/source/3_2_release_notes.md index a9484cf97a..ce811a583b 100644 --- a/guides/source/3_2_release_notes.md +++ b/guides/source/3_2_release_notes.md @@ -238,7 +238,7 @@ Action Pack end ``` - In the example above, Posts controller will no longer automatically look up for a posts layout. If you need this functionality you could either remove `layout "application"` from `ApplicationController` or explicitly set it to `nil` in `PostsController`. + In the example above, `PostsController` will no longer automatically look up for a posts layout. If you need this functionality you could either remove `layout "application"` from `ApplicationController` or explicitly set it to `nil` in `PostsController`. * Deprecated `ActionController::UnknownAction` in favor of `AbstractController::ActionNotFound`. diff --git a/guides/source/4_0_release_notes.md b/guides/source/4_0_release_notes.md index c0eb77c1e7..19c690233c 100644 --- a/guides/source/4_0_release_notes.md +++ b/guides/source/4_0_release_notes.md @@ -15,7 +15,7 @@ These release notes cover only the major changes. To know about various bug fixe Upgrading to Rails 4.0 ---------------------- -If you're upgrading an existing application, it's a great idea to have good test coverage before going in. You should also first upgrade to Rails 3.2 in case you haven't and make sure your application still runs as expected before attempting an update to Rails 4.0. A list of things to watch out for when upgrading is available in the [Upgrading to Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-3-2-to-rails-4-0) guide. +If you're upgrading an existing application, it's a great idea to have good test coverage before going in. You should also first upgrade to Rails 3.2 in case you haven't and make sure your application still runs as expected before attempting an update to Rails 4.0. A list of things to watch out for when upgrading is available in the [Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-3-2-to-rails-4-0) guide. Creating a Rails 4.0 application @@ -90,7 +90,7 @@ Major Features * **match do not catch all** ([commit](https://github.com/rails/rails/commit/90d2802b71a6e89aedfe40564a37bd35f777e541)) - In the routing DSL, match requires the HTTP verb or verbs to be specified. * **html entities escaped by default** ([commit](https://github.com/rails/rails/commit/5f189f41258b83d49012ec5a0678d827327e7543)) - Strings rendered in erb are escaped unless wrapped with `raw` or `html_safe` is called. * **New security headers** ([commit](https://github.com/rails/rails/commit/6794e92b204572d75a07bd6413bdae6ae22d5a82)) - Rails sends the following headers with every HTTP request: `X-Frame-Options` (prevents clickjacking by forbidding the browser from embedding the page in a frame), `X-XSS-Protection` (asks the browser to halt script injection) and `X-Content-Type-Options` (prevents the browser from opening a jpeg as an exe). - + Extraction of features to gems --------------------------- @@ -268,7 +268,7 @@ Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/a * `scoped_by_...` can be rewritten using `where(...)`. * `find_or_initialize_by_...` can be rewritten using `find_or_initialize_by(...)`. * `find_or_create_by_...` can be rewritten using `find_or_create_by(...)`. - * `find_or_create_by_...!` can be rewritten using `find_or_create_by!(...)`. + * `find_or_create_by_...!` can be rewritten using `find_or_create_by!(...)`. Credits ------- diff --git a/guides/source/4_1_release_notes.md b/guides/source/4_1_release_notes.md index 5c50ed83ea..477268f4bc 100644 --- a/guides/source/4_1_release_notes.md +++ b/guides/source/4_1_release_notes.md @@ -3,9 +3,10 @@ Ruby on Rails 4.1 Release Notes Highlights in Rails 4.1: -* Variants -* Spring -* Action View extracted from Action Pack +* Spring application preloader +* `config/secrets.yml` +* Action Pack variants +* Action Mailer previews These release notes cover only the major changes. To know about various bug fixes and changes, please refer to the change logs or check out the @@ -22,22 +23,83 @@ coverage before going in. You should also first upgrade to Rails 4.0 in case you haven't and make sure your application still runs as expected before attempting an update to Rails 4.1. A list of things to watch out for when upgrading is available in the -[Upgrading to Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-4-0-to-rails-4-1) +[Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-4-0-to-rails-4-1) guide. Major Features -------------- -### Variants +### Spring Application Preloader -We often want to render different html/json/xml templates for phones, -tablets, and desktop browsers. Variants makes it easy. +Spring is a Rails application preloader. It speeds up development by keeping +your application running in the background so you don't need to boot it every +time you run a test, rake task or migration. + +New Rails 4.1 applications will ship with "springified" binstubs. This means +that `bin/rails` and `bin/rake` will automatically take advantage of preloaded +spring environments. + +**Running rake tasks:** + +``` +bin/rake test:models +``` + +**Running a Rails command:** + +``` +bin/rails console +``` + +**Spring introspection:** + +``` +$ bin/spring status +Spring is running: + + 1182 spring server | my_app | started 29 mins ago + 3656 spring app | my_app | started 23 secs ago | test mode + 3746 spring app | my_app | started 10 secs ago | development mode +``` + +Have a look at the +[Spring README](https://github.com/rails/spring/blob/master/README.md) to +see all available features. + +See the [Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#spring) +guide on how to migrate existing applications to use this feature. + +### `config/secrets.yml` + +Rails 4.1 generates a new `secrets.yml` file in the `config` folder. By default, +this file contains the application's `secret_key_base`, but it could also be +used to store other secrets such as access keys for external APIs. + +The secrets added to this file are accessible via `Rails.application.secrets`. +For example, with the following `config/secrets.yml`: + +```yaml +development: + secret_key_base: 3b7cd727ee24e8444053437c36cc66c3 + some_api_key: SOMEKEY +``` + +`Rails.application.secrets.some_api_key` returns `SOMEKEY` in the development +environment. + +See the [Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#config-secrets-yml) +guide on how to migrate existing applications to use this feature. + +### Action Pack Variants + +We often want to render different HTML/JSON/XML templates for phones, +tablets, and desktop browsers. Variants make it easy. The request variant is a specialization of the request format, like `:tablet`, `:phone`, or `:desktop`. -You can set the variant in a before_action: +You can set the variant in a `before_action`: ```ruby request.variant = :tablet if request.user_agent =~ /iPad/ @@ -62,46 +124,41 @@ app/views/projects/show.html+tablet.erb app/views/projects/show.html+phone.erb ``` -### Spring - -New Rails 4.1 applications will ship with "springified" binstubs. This means -that `bin/rails` and `bin/rake` will automatically take advantage preloaded -spring environments. - -**running rake tasks:** +You can also simplify the variants definition using the inline syntax: -``` -bin/rake routes +```ruby +respond_to do |format| + format.js { render "trash" } + format.html.phone { redirect_to progress_path } + format.html.none { render "trash" } +end ``` -**running tests:** +### Action Mailer Previews -``` -bin/rake test -bin/rake test test/models -bin/rake test test/models/user_test.rb -``` +Action Mailer previews provide a way to visually see how emails look by visiting +a special URL that renders them. -**running a console:** +You implement a preview class whose methods return the mail object you'd like +to check: -``` -bin/rails console +```ruby +class NotifierPreview < ActionMailer::Preview + def welcome + Notifier.welcome(User.first) + end +end ``` -**spring introspection:** +The preview is available in http://localhost:3000/rails/mailers/notifier/welcome, +and a list of them in http://localhost:3000/rails/mailers. -``` -$ bundle exec spring status -Spring is running: +By default, these preview classes live in `test/mailers/previews`. +This can be configured using the `preview_path` option. - 1182 spring server | my_app | started 29 mins ago - 3656 spring app | my_app | started 23 secs ago | test mode - 3746 spring app | my_app | started 10 secs ago | development mode -``` - -Have a look at the -[Spring README](https://github.com/jonleighton/spring/blob/master/README.md) to -see a all available features. +See its +[documentation](http://api.rubyonrails.org/v4.1.0/classes/ActionMailer/Base.html) +for a detailed write up. ### Active Record enums @@ -113,31 +170,76 @@ class Conversation < ActiveRecord::Base enum status: [ :active, :archived ] end -conversation.archive! +conversation.archived! conversation.active? # => false conversation.status # => "archived" Conversation.archived # => Relation for all archived Conversations ``` -See -[active_record/enum.rb](https://github.com/rails/rails/blob/4-1-stable/activerecord/lib/active_record/enum.rb#L2-L42) +See its +[documentation](http://api.rubyonrails.org/v4.1.0/classes/ActiveRecord/Enum.html) for a detailed write up. -### Application message verifier. +### Message Verifiers + +Message verifiers can be used to generate and verify signed messages. This can +be useful to safely transport sensitive data like remember-me tokens and +friends. -Create a message verifier that can be used to generate and verify signed -messages in the application. +The method `Rails.application.message_verifier` returns a new message verifier +that signs messages with a key derived from secret_key_base and the given +message verifier name: ```ruby -message = Rails.application.message_verifier('salt').generate('my sensible data') -Rails.application.message_verifier('salt').verify(message) -# => 'my sensible data' +signed_token = Rails.application.message_verifier(:remember_me).generate(token) +Rails.application.message_verifier(:remember_me).verify(signed_token) # => token + +Rails.application.message_verifier(:remember_me).verify(tampered_token) +# raises ActiveSupport::MessageVerifier::InvalidSignature ``` -Documentation -------------- +### Module#concerning + +A natural, low-ceremony way to separate responsibilities within a class: + +```ruby +class Todo < ActiveRecord::Base + concerning :EventTracking do + included do + has_many :events + end + + def latest_event + ... + end + + private + def some_internal_method + ... + end + end +end +``` + +This example is equivalent to defining a `EventTracking` module inline, +extending it with `ActiveSupport::Concern`, then mixing it in to the +`Todo` class. + +See its +[documentation](http://api.rubyonrails.org/v4.1.0/classes/Module/Concerning.html) +for a detailed write up and the intended use cases. + +### CSRF protection from remote `<script>` tags +Cross-site request forgery (CSRF) protection now covers GET requests with +JavaScript responses, too. That prevents a third-party site from referencing +your JavaScript URL and attempting to run it to extract sensitive data. + +This means any of your tests that hit `.js` URLs will now fail CSRF protection +unless they use `xhr`. Upgrade your tests to be explicit about expecting +XmlHttpRequests. Instead of `post :create, format: :js`, switch to the explicit +`xhr :post, :create, format: :js`. Railties -------- @@ -165,7 +267,7 @@ for detailed changes. ### Notable changes * The [Spring application - preloader](https://github.com/jonleighton/spring) is now installed + preloader](https://github.com/rails/spring) is now installed by default for new applications. It uses the development group of the Gemfile, so will not be installed in production. ([Pull Request](https://github.com/rails/rails/pull/12958)) @@ -179,137 +281,12 @@ for detailed changes. * Add `Application#message_verifier` method to return a message verifier. ([Pull Request](https://github.com/rails/rails/pull/12995)) -Action Mailer -------------- - -Please refer to the -[Changelog](https://github.com/rails/rails/blob/4-1-stable/actionmailer/CHANGELOG.md) -for detailed changes. - -### Notable changes - -* Instrument the generation of Action Mailer messages. The time it takes to - generate a message is written to the log. ([Pull Request](https://github.com/rails/rails/pull/12556)) - - -Active Model ------------- - -Please refer to the -[Changelog](https://github.com/rails/rails/blob/4-1-stable/activemodel/CHANGELOG.md) -for detailed changes. - -### Deprecations - -* Deprecate `Validator#setup`. This should be done manually now in the - validator's constructor. ([Commit](https://github.com/rails/rails/commit/7d84c3a2f7ede0e8d04540e9c0640de7378e9b3a)) - -### Notable changes - -* Added new API methods `reset_changes` and `changes_applied` to - `ActiveModel::Dirty` that control changes state. - - -Active Support --------------- - -Please refer to the -[Changelog](https://github.com/rails/rails/blob/4-1-stable/activesupport/CHANGELOG.md) -for detailed changes. - - -### Removals - -* Removed `MultiJSON` dependency. As a result, `ActiveSupport::JSON.decode` - no longer accepts an options hash for `MultiJSON`. ([Pull Request](https://github.com/rails/rails/pull/10576) / [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) - -* Removed support for the `encode_json` hook used for encoding custom objects into - JSON. This feature has been extracted into the [activesupport-json_encoder](https://github.com/rails/activesupport-json_encoder) - gem. - ([Related Pull Request](https://github.com/rails/rails/pull/12183) / - [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) - -* Removed deprecated `ActiveSupport::JSON::Variable` with no replacement. - -* Removed deprecated `String#encoding_aware?` core extensions (`core_ext/string/encoding`). - -* Removed deprecated `Module#local_constant_names` in favor of `Module#local_constants`. - -* Removed deprecated `DateTime.local_offset` in favor of `DateTime.civil_from_fromat`. - -* Removed deprecated `Logger` core extensions (`core_ext/logger.rb`). - -* Removed deprecated `Time#time_with_datetime_fallback`, `Time#utc_time` and - `Time#local_time` in favor of `Time#utc` and `Time#local`. - -* Removed deprecated `Hash#diff` with no replacement. - -* Removed deprecated `Date#to_time_in_current_zone` in favor of `Date#in_time_zone`. - -* Removed deprecated `Proc#bind` with no replacement. - -* Removed deprecated `Array#uniq_by` and `Array#uniq_by!`, use native - `Array#uniq` and `Array#uniq!` instead. - -* Removed deprecated `ActiveSupport::BasicObject`, use - `ActiveSupport::ProxyObject` instead. - -* Removed deprecated `BufferedLogger`, use `ActiveSupport::Logger` instead. - -* Removed deprecated `assert_present` and `assert_blank` methods, use `assert - object.blank?` and `assert object.present?` instead. - -### Deprecations - -* Deprecated `Numeric#{ago,until,since,from_now}`, the user is expected to - explicitly convert the value into an AS::Duration, i.e. `5.ago` => `5.seconds.ago` - ([Pull Request](https://github.com/rails/rails/pull/12389)) - -* Deprecated the require path `active_support/core_ext/object/to_json`. Require - `active_support/core_ext/object/json` instead. ([Pull Request](https://github.com/rails/rails/pull/12203)) - -* Deprecated `ActiveSupport::JSON::Encoding::CircularReferenceError`. This feature - has been extracted into the [activesupport-json_encoder](https://github.com/rails/activesupport-json_encoder) - gem. - ([Pull Request](https://github.com/rails/rails/pull/12785) / - [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) - -* Deprecated `ActiveSupport.encode_big_decimal_as_string` option. This feature has - been extracetd into the [activesupport-json_encoder](https://github.com/rails/activesupport-json_encoder) - gem. - ([Pull Request](https://github.com/rails/rails/pull/13060) / - [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) - -### Notable changes - -* `ActiveSupport`'s JSON encoder has been rewritten to take advantage of the - JSON gem rather than doing custom encoding in pure-Ruby. - ([Pull Request](https://github.com/rails/rails/pull/12183) / - [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) - -* Improved compatibility with the JSON gem. - ([Pull Request](https://github.com/rails/rails/pull/12862) / - [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) - -* Added `ActiveSupport::Testing::TimeHelpers#travel` and `#travel_to`. These - methods change current time to the given time or time difference by stubbing - `Time.now` and - `Date.today`. ([Pull Request](https://github.com/rails/rails/pull/12824)) - -* Added `Numeric#in_milliseconds`, like `1.hour.in_milliseconds`, so we can feed - them to JavaScript functions like - `getTime()`. ([Commit](https://github.com/rails/rails/commit/423249504a2b468d7a273cbe6accf4f21cb0e643)) - -* Added `Date#middle_of_day`, `DateTime#middle_of_day` and `Time#middle_of_day` - methods. Also added `midday`, `noon`, `at_midday`, `at_noon` and - `at_middle_of_day` as - aliases. ([Pull Request](https://github.com/rails/rails/pull/10879)) - -* Added `String#remove(pattern)` as a short-hand for the common pattern of - `String#gsub(pattern,'')`. ([Commit](https://github.com/rails/rails/commit/5da23a3f921f0a4a3139495d2779ab0d3bd4cb5f)) - -* Removed 'cow' => 'kine' irregular inflection from default - inflections. ([Commit](https://github.com/rails/rails/commit/c300dca9963bda78b8f358dbcb59cabcdc5e1dc9)) +* The `test_help.rb` file which is required by the default generated test + helper will automatically keep your test database up-to-date with + `db/schema.rb` (or `db/structure.sql`). It raises an error if + reloading the schema does not resolve all pending migrations. Opt out + with `config.active_record.maintain_test_schema = false`. ([Pull + Request](https://github.com/rails/rails/pull/13528)) Action Pack ----------- @@ -330,16 +307,23 @@ for detailed changes. * Removed deprecated constants from Action Controller: - ActionController::AbstractRequest => ActionDispatch::Request - ActionController::Request => ActionDispatch::Request - ActionController::AbstractResponse => ActionDispatch::Response - ActionController::Response => ActionDispatch::Response - ActionController::Routing => ActionDispatch::Routing - ActionController::Integration => ActionDispatch::Integration - ActionController::IntegrationTest => ActionDispatch::IntegrationTest + | Removed | Successor | + |:-----------------------------------|:--------------------------------| + | ActionController::AbstractRequest | ActionDispatch::Request | + | ActionController::Request | ActionDispatch::Request | + | ActionController::AbstractResponse | ActionDispatch::Response | + | ActionController::Response | ActionDispatch::Response | + | ActionController::Routing | ActionDispatch::Routing | + | ActionController::Integration | ActionDispatch::Integration | + | ActionController::IntegrationTest | ActionDispatch::IntegrationTest | ### Notable changes +* `protect_from_forgery` also prevents cross-origin `<script>` tags. + Update your tests to use `xhr :get, :foo, format: :js` instead of + `get :foo, format: :js`. + ([Pull Request](https://github.com/rails/rails/pull/13345)) + * `#url_for` takes a hash with options inside an array. ([Pull Request](https://github.com/rails/rails/pull/9599)) @@ -351,6 +335,17 @@ for detailed changes. * Separated Action View completely from Action Pack. ([Pull Request](https://github.com/rails/rails/pull/11032)) +Action Mailer +------------- + +Please refer to the +[Changelog](https://github.com/rails/rails/blob/4-1-stable/actionmailer/CHANGELOG.md) +for detailed changes. + +### Notable changes + +* Instrument the generation of Action Mailer messages. The time it takes to + generate a message is written to the log. ([Pull Request](https://github.com/rails/rails/pull/12556)) Active Record ------------- @@ -378,7 +373,7 @@ for detailed changes. * Removed deprecated `increment_open_transactions`. * Removed deprecated `PostgreSQLAdapter#outside_transaction?` - methodd. You can use `#transaction_open?` instead. + method. You can use `#transaction_open?` instead. * Removed deprecated `ActiveRecord::Fixtures.find_table_name` in favor of `ActiveRecord::Fixtures.default_fixture_model_name`. @@ -416,6 +411,8 @@ for detailed changes. * Remove implicit join references that were deprecated in 4.0. * Removed `activerecord-deprecated_finders` as a dependency. + Please see [the gem README](https://github.com/rails/activerecord-deprecated_finders#active-record-deprecated-finders) + for more info. * Removed usage of `implicit_readonly`. Please use `readonly` method explicitly to mark records as @@ -425,14 +422,17 @@ for detailed changes. * Deprecated `quoted_locking_column` method, which isn't used anywhere. -* Deprecated the delegation of Array bang methods for associations. - To use them, instead first call `#to_a` on the association to access the - array to be acted - on. ([Pull Request](https://github.com/rails/rails/pull/12129)) - * Deprecated `ConnectionAdapters::SchemaStatements#distinct`, as it is no longer used by internals. ([Pull Request](https://github.com/rails/rails/pull/10556)) +* Deprecated `rake db:test:*` tasks as the test database is now + automatically maintained. See railties release notes. ([Pull + Request](https://github.com/rails/rails/pull/13528)) + +* Deprecate unused `ActiveRecord::Base.symbolized_base_class` + and `ActiveRecord::Base.symbolized_sti_name` without + replacement. [Commit](https://github.com/rails/rails/commit/97e7ca48c139ea5cce2fa9b4be631946252a1ebd) + ### Notable changes * Added `ActiveRecord::Base.to_param` for convenient "pretty" URLs derived from @@ -495,6 +495,130 @@ for detailed changes. object. Helper methods used by multiple fixtures should be defined on modules included in `ActiveRecord::FixtureSet.context_class`. ([Pull Request](https://github.com/rails/rails/pull/13022)) +* Don't create or drop the test database if RAILS_ENV is specified explicitly. + +* `Relation` no longer has mutator methods like `#map!` and `#delete_if`. Convert + to an `Array` by calling `#to_a` before using these methods. ([Pull Request](https://github.com/rails/rails/pull/13314)) + +Active Model +------------ + +Please refer to the +[Changelog](https://github.com/rails/rails/blob/4-1-stable/activemodel/CHANGELOG.md) +for detailed changes. + +### Deprecations + +* Deprecate `Validator#setup`. This should be done manually now in the + validator's constructor. ([Commit](https://github.com/rails/rails/commit/7d84c3a2f7ede0e8d04540e9c0640de7378e9b3a)) + +### Notable changes + +* Added new API methods `reset_changes` and `changes_applied` to + `ActiveModel::Dirty` that control changes state. + + +Active Support +-------------- + +Please refer to the +[Changelog](https://github.com/rails/rails/blob/4-1-stable/activesupport/CHANGELOG.md) +for detailed changes. + + +### Removals + +* Removed `MultiJSON` dependency. As a result, `ActiveSupport::JSON.decode` + no longer accepts an options hash for `MultiJSON`. ([Pull Request](https://github.com/rails/rails/pull/10576) / [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) + +* Removed support for the `encode_json` hook used for encoding custom objects into + JSON. This feature has been extracted into the [activesupport-json_encoder](https://github.com/rails/activesupport-json_encoder) + gem. + ([Related Pull Request](https://github.com/rails/rails/pull/12183) / + [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) + +* Removed deprecated `ActiveSupport::JSON::Variable` with no replacement. + +* Removed deprecated `String#encoding_aware?` core extensions (`core_ext/string/encoding`). + +* Removed deprecated `Module#local_constant_names` in favor of `Module#local_constants`. + +* Removed deprecated `DateTime.local_offset` in favor of `DateTime.civil_from_format`. + +* Removed deprecated `Logger` core extensions (`core_ext/logger.rb`). + +* Removed deprecated `Time#time_with_datetime_fallback`, `Time#utc_time` and + `Time#local_time` in favor of `Time#utc` and `Time#local`. + +* Removed deprecated `Hash#diff` with no replacement. + +* Removed deprecated `Date#to_time_in_current_zone` in favor of `Date#in_time_zone`. + +* Removed deprecated `Proc#bind` with no replacement. + +* Removed deprecated `Array#uniq_by` and `Array#uniq_by!`, use native + `Array#uniq` and `Array#uniq!` instead. + +* Removed deprecated `ActiveSupport::BasicObject`, use + `ActiveSupport::ProxyObject` instead. + +* Removed deprecated `BufferedLogger`, use `ActiveSupport::Logger` instead. + +* Removed deprecated `assert_present` and `assert_blank` methods, use `assert + object.blank?` and `assert object.present?` instead. + +### Deprecations + +* Deprecated `Numeric#{ago,until,since,from_now}`, the user is expected to + explicitly convert the value into an AS::Duration, i.e. `5.ago` => `5.seconds.ago` + ([Pull Request](https://github.com/rails/rails/pull/12389)) + +* Deprecated the require path `active_support/core_ext/object/to_json`. Require + `active_support/core_ext/object/json` instead. ([Pull Request](https://github.com/rails/rails/pull/12203)) + +* Deprecated `ActiveSupport::JSON::Encoding::CircularReferenceError`. This feature + has been extracted into the [activesupport-json_encoder](https://github.com/rails/activesupport-json_encoder) + gem. + ([Pull Request](https://github.com/rails/rails/pull/12785) / + [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) + +* Deprecated `ActiveSupport.encode_big_decimal_as_string` option. This feature has + been extracted into the [activesupport-json_encoder](https://github.com/rails/activesupport-json_encoder) + gem. + ([Pull Request](https://github.com/rails/rails/pull/13060) / + [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) + +### Notable changes + +* `ActiveSupport`'s JSON encoder has been rewritten to take advantage of the + JSON gem rather than doing custom encoding in pure-Ruby. + ([Pull Request](https://github.com/rails/rails/pull/12183) / + [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) + +* Improved compatibility with the JSON gem. + ([Pull Request](https://github.com/rails/rails/pull/12862) / + [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) + +* Added `ActiveSupport::Testing::TimeHelpers#travel` and `#travel_to`. These + methods change current time to the given time or time difference by stubbing + `Time.now` and + `Date.today`. ([Pull Request](https://github.com/rails/rails/pull/12824)) + +* Added `Numeric#in_milliseconds`, like `1.hour.in_milliseconds`, so we can feed + them to JavaScript functions like + `getTime()`. ([Commit](https://github.com/rails/rails/commit/423249504a2b468d7a273cbe6accf4f21cb0e643)) + +* Added `Date#middle_of_day`, `DateTime#middle_of_day` and `Time#middle_of_day` + methods. Also added `midday`, `noon`, `at_midday`, `at_noon` and + `at_middle_of_day` as + aliases. ([Pull Request](https://github.com/rails/rails/pull/10879)) + +* Added `String#remove(pattern)` as a short-hand for the common pattern of + `String#gsub(pattern,'')`. ([Commit](https://github.com/rails/rails/commit/5da23a3f921f0a4a3139495d2779ab0d3bd4cb5f)) + +* Removed 'cow' => 'kine' irregular inflection from default + inflections. ([Commit](https://github.com/rails/rails/commit/c300dca9963bda78b8f358dbcb59cabcdc5e1dc9)) + Credits ------- diff --git a/guides/source/_welcome.html.erb b/guides/source/_welcome.html.erb index ed2025a75d..27c53689c4 100644 --- a/guides/source/_welcome.html.erb +++ b/guides/source/_welcome.html.erb @@ -18,5 +18,5 @@ The guides for Rails 3.2.x are available at <a href="http://guides.rubyonrails.org/v3.2.16/">http://guides.rubyonrails.org/v3.2.16/</a>. </p> <p> - The guides for Rails 2.3.x are available at <a href="http://guides.rubyonrails.org/v2.3.18/">http://guides.rubyonrails.org/v2.3.18/</a>. + 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>. </p> diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index 4252b5ee9a..0234120b45 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -112,6 +112,10 @@ NOTE: The actual URL in this example will be encoded as "/clients?ids%5b%5d=1&id The value of `params[:ids]` will now be `["1", "2", "3"]`. Note that parameter values are always strings; Rails makes no attempt to guess or cast the type. +NOTE: Values such as `[]`, `[nil]` or `[nil, nil, ...]` in `params` are replaced +with `nil` for security reasons by default. See [Security Guide](security.html#unsafe-query-generation) +for more information. + To send a hash you include the key name inside the brackets: ```html @@ -377,6 +381,28 @@ You can also pass a `:domain` key and specify the domain name for the cookie: YourApp::Application.config.session_store :cookie_store, key: '_your_app_session', domain: ".example.com" ``` +You can pass `:serializer` key to specify serializer for serializing session: + +```ruby +YourApp::Application.config.session_store :cookie_store, key: '_your_app_session', serializer: :json_serializer +``` + +Default serializer is `:marshal_serializer`. When Symbol or String is passed it +will look for appropriate class in `ActionDispatch::Session` namespace, so +passing `:my_custom_serializer` would load +`ActionDispatch::Session::MyCustomSerializer`. + +```ruby +YourApp::Application.config.session_store :cookie_store, key: '_your_app_session', serializer: :my_custom_serializer +``` + +It is also possible to pass serializer object with defined `load` and `dump` +public methods: + +```ruby +YourApp::Application.config.session_store :cookie_store, key: '_your_app_session', serializer: MyCustomSerializer +``` + Rails sets up (for the CookieStore) a secret key used for signing the session data. This can be changed in `config/initializers/secret_token.rb` ```ruby @@ -665,14 +691,17 @@ The first is to use a block directly with the *_action methods. The block receiv ```ruby class ApplicationController < ActionController::Base before_action do |controller| - redirect_to new_login_url unless controller.send(:logged_in?) + unless controller.send(:logged_in?) + flash[:error] = "You must be logged in to access this section" + redirect_to new_login_url + end end end ``` Note that the filter in this case uses `send` because the `logged_in?` method is private and the filter is not run in the scope of the controller. This is not the recommended way to implement this particular filter, but in more simple cases it might be useful. -The second way is to use a class (actually, any object that responds to the right methods will do) to handle the filtering. This is useful in cases that are more complex and can not be implemented in a readable and reusable way using the two other methods. As an example, you could rewrite the login filter again to use a class: +The second way is to use a class (actually, any object that responds to the right methods will do) to handle the filtering. This is useful in cases that are more complex and cannot be implemented in a readable and reusable way using the two other methods. As an example, you could rewrite the login filter again to use a class: ```ruby class ApplicationController < ActionController::Base @@ -682,7 +711,7 @@ end class LoginFilter def self.filter(controller) unless controller.send(:logged_in?) - controller.flash[:error] = "You must be logged in" + controller.flash[:error] = "You must be logged in to access this section" controller.redirect_to controller.new_login_url end end @@ -794,7 +823,7 @@ class AdminsController < ApplicationController end ``` -With this in place, you can create namespaced controllers that inherit from `AdminController`. The filter will thus be run for all actions in those controllers, protecting them with HTTP basic authentication. +With this in place, you can create namespaced controllers that inherit from `AdminsController`. The filter will thus be run for all actions in those controllers, protecting them with HTTP basic authentication. ### HTTP Digest Authentication diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index 61fd762304..6dc7fb1606 100644 --- a/guides/source/action_mailer_basics.md +++ b/guides/source/action_mailer_basics.md @@ -138,7 +138,7 @@ When you call the `mail` method now, Action Mailer will detect the two templates Mailers are really just another way to render a view. Instead of rendering a view and sending out the HTTP protocol, they are just sending it out through the -Email protocols instead. Due to this, it makes sense to just have your +email protocols instead. Due to this, it makes sense to just have your controller tell the Mailer to send an email when a user is successfully created. Setting this up is painfully simple. @@ -164,7 +164,7 @@ class UsersController < ApplicationController respond_to do |format| if @user.save - # Tell the UserMailer to send a welcome Email after save + # Tell the UserMailer to send a welcome email after save UserMailer.welcome_email(@user).deliver format.html { redirect_to(@user, notice: 'User was successfully created.') } @@ -611,7 +611,7 @@ files (environment.rb, production.rb, etc...) |`smtp_settings`|Allows detailed configuration for `:smtp` delivery method:<ul><li>`:address` - Allows you to use a remote mail server. Just change it from its default "localhost" setting.</li><li>`:port` - On the off chance that your mail server doesn't run on port 25, you can change it.</li><li>`:domain` - If you need to specify a HELO domain, you can do it here.</li><li>`:user_name` - If your mail server requires authentication, set the username in this setting.</li><li>`:password` - If your mail server requires authentication, set the password in this setting.</li><li>`:authentication` - If your mail server requires authentication, you need to specify the authentication type here. This is a symbol and one of `:plain`, `:login`, `:cram_md5`.</li><li>`:enable_starttls_auto` - Set this to `false` if there is a problem with your server certificate that you cannot resolve.</li></ul>| |`sendmail_settings`|Allows you to override options for the `:sendmail` delivery method.<ul><li>`:location` - The location of the sendmail executable. Defaults to `/usr/sbin/sendmail`.</li><li>`:arguments` - The command line arguments to be passed to sendmail. Defaults to `-i -t`.</li></ul>| |`raise_delivery_errors`|Whether or not errors should be raised if the email fails to be delivered. This only works if the external email server is configured for immediate delivery.| -|`delivery_method`|Defines a delivery method. Possible values are `:smtp` (default), `:sendmail`, `:file` and `:test`.| +|`delivery_method`|Defines a delivery method. Possible values are:<ul><li>`:smtp` (default), can be configured by using `config.action_mailer.smtp_settings`.</li><li>`:sendmail`, can be configured by using `config.action_mailer.sendmail_settings`.</li><li>`:file`: save emails to files; can be configured by using `config.action_mailer.file_settings`.</li><li>`:test`: save emails to `ActionMailer::Base.deliveries` array.</li></ul>See [API docs](http://api.rubyonrails.org/classes/ActionMailer/Base.html) for more info.| |`perform_deliveries`|Determines whether deliveries are actually carried out when the `deliver` method is invoked on the Mail message. By default they are, but this can be turned off to help functional testing.| |`deliveries`|Keeps an array of all the emails sent out through the Action Mailer with delivery_method :test. Most useful for unit and functional testing.| |`default_options`|Allows you to set default values for the `mail` method options (`:from`, `:reply_to`, etc.).| @@ -639,8 +639,8 @@ config.action_mailer.default_options = {from: 'no-reply@example.com'} ### Action Mailer Configuration for Gmail -As Action Mailer now uses the Mail gem, this becomes as simple as adding to your -`config/environments/$RAILS_ENV.rb` file: +As Action Mailer now uses the [Mail gem](https://github.com/mikel/mail), this +becomes as simple as adding to your `config/environments/$RAILS_ENV.rb` file: ```ruby config.action_mailer.delivery_method = :smtp diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md index 769be9840c..6a355a5177 100644 --- a/guides/source/action_view_overview.md +++ b/guides/source/action_view_overview.md @@ -1285,7 +1285,7 @@ Creates a field set for grouping HTML form elements. Creates a file upload field. ```html+erb -<%= form_tag {action: "post"}, {multipart: true} do %> +<%= form_tag({action:"post"}, multipart: true) do %> <label for="file">File to Upload</label> <%= file_field_tag "file" %> <%= submit_tag %> <% end %> @@ -1591,7 +1591,7 @@ Localized Views Action View has the ability render different templates depending on the current locale. -For example, suppose you have a Posts controller 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 `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. 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. diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 863da3be72..667433285f 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -55,7 +55,7 @@ class User < ActiveRecord::Base end ``` -Callbacks can also be registered to only fire on certain lifecycle events: +Callbacks can also be registered to only fire on certain life cycle events: ```ruby class User < ActiveRecord::Base @@ -141,6 +141,55 @@ You have initialized an object! => #<User id: 1> ``` +### `after_touch` + +The `after_touch` callback will be called whenever an Active Record object is touched. + +```ruby +class User < ActiveRecord::Base + after_touch do |user| + puts "You have touched an object" + end +end + +>> u = User.create(name: 'Kuldeep') +=> #<User id: 1, name: "Kuldeep", created_at: "2013-11-25 12:17:49", updated_at: "2013-11-25 12:17:49"> + +>> u.touch +You have touched an object +=> true +``` + +It can be used along with `belongs_to`: + +```ruby +class Employee < ActiveRecord::Base + belongs_to :company, touch: true + after_touch do + puts 'An Employee was touched' + end +end + +class Company < ActiveRecord::Base + has_many :employees + after_touch :log_when_employees_or_company_touched + + private + def log_when_employees_or_company_touched + puts 'Employee/Company was touched' + end +end + +>> @employee = Employee.last +=> #<Employee id: 1, company_id: 1, created_at: "2013-11-25 17:04:22", updated_at: "2013-11-25 17:05:05"> + +# triggers @employee.company.touch +>> @employee.touch +Employee/Company was touched +An Employee was touched +=> true +``` + Running Callbacks ----------------- diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index 4725e2c8a2..d164b08d93 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -1338,11 +1338,6 @@ Client.unscoped { Dynamic Finders --------------- -NOTE: Dynamic finders have been deprecated in Rails 4.0 and will be -removed in Rails 4.1. The best practice is to use Active Record scopes -instead. You can find the deprecation gem at -https://github.com/rails/activerecord-deprecated_finders - For every field (also known as an attribute) you define in your table, Active Record provides a finder method. If you have a field called `first_name` on your `Client` model for example, you get `find_by_first_name` for free from Active Record. If you have a `locked` field on the `Client` model, you also get `find_by_locked` and methods. You can specify an exclamation point (`!`) on the end of the dynamic finders to get them to raise an `ActiveRecord::RecordNotFound` error if they do not return any records, like `Client.find_by_name!("Ryan")` @@ -1352,6 +1347,11 @@ If you want to find both by name and locked, you can chain these finders togethe Find or Build a New Object -------------------------- +NOTE: Some dynamic finders have been deprecated in Rails 4.0 and will be +removed in Rails 4.1. The best practice is to use Active Record scopes +instead. You can find the deprecation gem at +https://github.com/rails/activerecord-deprecated_finders + It's common that you need to find a record or create it if it doesn't exist. You can do that with the `find_or_create_by` and `find_or_create_by!` methods. ### `find_or_create_by` @@ -1455,7 +1455,7 @@ If you'd like to use your own SQL to find records in a table you can use `find_b ```ruby Client.find_by_sql("SELECT * FROM clients INNER JOIN orders ON clients.id = orders.client_id - ORDER clients.created_at desc") + ORDER BY clients.created_at desc") ``` `find_by_sql` provides you with a simple way of making custom calls to the database and retrieving instantiated objects. diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md index efa826e8df..b04c7a90e2 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -616,10 +616,6 @@ The default error message is _"has already been taken"_. This helper passes the record to a separate class for validation. ```ruby -class Person < ActiveRecord::Base - validates_with GoodnessValidator -end - class GoodnessValidator < ActiveModel::Validator def validate(record) if record.first_name == "Evil" @@ -627,6 +623,10 @@ class GoodnessValidator < ActiveModel::Validator end end end + +class Person < ActiveRecord::Base + validates_with GoodnessValidator +end ``` NOTE: Errors added to `record.errors[:base]` relate to the state of the record @@ -644,10 +644,6 @@ Like all other validations, `validates_with` takes the `:if`, `:unless` and validator class as `options`: ```ruby -class Person < ActiveRecord::Base - validates_with GoodnessValidator, fields: [:first_name, :last_name] -end - class GoodnessValidator < ActiveModel::Validator def validate(record) if options[:fields].any?{|field| record.send(field) == "Evil" } @@ -655,6 +651,10 @@ class GoodnessValidator < ActiveModel::Validator end end end + +class Person < ActiveRecord::Base + validates_with GoodnessValidator, fields: [:first_name, :last_name] +end ``` Note that the validator will be initialized *only once* for the whole application diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md index a83aee5d43..59dfefd22f 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -424,7 +424,7 @@ NOTE: Defined in `active_support/core_ext/object/with_options.rb`. ### JSON support -Active Support provides a better implementation of `to_json` than the +json+ gem ordinarily provides for Ruby objects. This is because some classes, like +Hash+, +OrderedHash+, and +Process::Status+ need special handling in order to provide a proper JSON representation. +Active Support provides a better implementation of `to_json` than the `json` gem ordinarily provides for Ruby objects. This is because some classes, like `Hash`, `OrderedHash` and `Process::Status` need special handling in order to provide a proper JSON representation. NOTE: Defined in `active_support/core_ext/object/json.rb`. @@ -2907,6 +2907,16 @@ The method `with_indifferent_access` returns an `ActiveSupport::HashWithIndiffer 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. + +```ruby +{a: 1, b: 2, c: nil}.compact # => {a: 1, b: 2} +``` + +NOTE: Defined in `active_support/core_ext/hash/compact.rb`. + Extensions to `Regexp` ---------------------- diff --git a/guides/source/api_documentation_guidelines.md b/guides/source/api_documentation_guidelines.md index ccb51ce73c..7466b4ff48 100644 --- a/guides/source/api_documentation_guidelines.md +++ b/guides/source/api_documentation_guidelines.md @@ -42,7 +42,21 @@ Spell names correctly: Arel, Test::Unit, RSpec, HTML, MySQL, JavaScript, ERB. Wh Use the article "an" for "SQL", as in "an SQL statement". Also "an SQLite database". -When using pronouns in reference to a hypothetical person, such as "a user with a session cookie", gender neutral pronouns (they/their/them) should be used. Instead of: +Prefer wordings that avoid "you"s and "your"s. For example, instead of + +```markdown +If you need to use `return` statements in your callbacks, it is recommended that you explicitly define them as methods. +``` + +use this style: + +```markdown +If `return` is needed it is recommended to explicitly define a method. +``` + +That said, when using pronouns in reference to a hypothetical person, such as "a +user with a session cookie", gender neutral pronouns (they/their/them) should be +used. Instead of: * he or she... use they. * him or her... use them. @@ -53,7 +67,7 @@ When using pronouns in reference to a hypothetical person, such as "a user with English ------- -Please use American English (<em>color</em>, <em>center</em>, <em>modularize</em>, etc).. See [a list of American and British English spelling differences here](http://en.wikipedia.org/wiki/American_and_British_English_spelling_differences). +Please use American English (<em>color</em>, <em>center</em>, <em>modularize</em>, etc). See [a list of American and British English spelling differences here](http://en.wikipedia.org/wiki/American_and_British_English_spelling_differences). Example Code ------------ @@ -114,6 +128,53 @@ On the other hand, regular comments do not use an arrow: # polymorphic_url(record) # same as comment_url(record) ``` +Booleans +-------- + +In predicates and flags prefer documenting boolean semantics over exact values. + +When "true" or "false" are used as defined in Ruby use regular font. The +singletons `true` and `false` need fixed-width font. Please avoid terms like +"truthy", Ruby defines what is true and false in the language, and thus those +words have a technical meaning and need no substitutes. + +As a rule of thumb, do not document singletons unless absolutely necessary. That +prevents artificial constructs like `!!` or ternaries, allows refactors, and the +code does not need to rely on the exact values returned by methods being called +in the implementation. + +For example: + +```markdown +`config.action_mailer.perform_deliveries` specifies whether mail will actually be delivered and is true by default +``` + +the user does not need to know which is the actual default value of the flag, +and so we only document its boolean semantics. + +An example with a predicate: + +```ruby +# Returns true if the collection is empty. +# +# If the collection has been loaded +# it is equivalent to <tt>collection.size.zero?</tt>. If the +# collection has not been loaded, it is equivalent to +# <tt>collection.exists?</tt>. If the collection has not already been +# loaded and you are going to fetch the records anyway it is better to +# check <tt>collection.length.zero?</tt>. +def empty? + if loaded? + size.zero? + else + @target.blank? && !scope.exists? + end +end +``` + +The API is careful not to commit to any particular value, the predicate has +predicate semantics, that's enough. + Filenames --------- @@ -149,7 +210,10 @@ class Array end ``` -WARNING: Using a pair of `+...+` for fixed-width font only works with **words**; that is: anything matching `\A\w+\z`. For anything else use `<tt>...</tt>`, notably symbols, setters, inline snippets, etc. +WARNING: Using `+...+` for fixed-width font only works with simple content like +ordinary method names, symbols, paths (with forward slashes), etc. Please use +`<tt>...</tt>` for everything else, notably class or module names with a +namespace as in `<tt>ActiveRecord::Base</tt>`. ### Regular Font diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index 39448e92d5..40e0770177 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -26,14 +26,14 @@ been extracted out of the framework into the The asset pipeline is enabled by default. -You can disable the asset pipeline while creating a new application by +You can disable the asset pipeline while creating a new application by passing the `--skip-sprockets` option. ```bash rails new appname --skip-sprockets ``` -Rails 4 automatically adds the `sass-rails`, `coffee-rails` and `uglifier` +Rails 4 automatically adds the `sass-rails`, `coffee-rails` and `uglifier` gems to your Gemfile, which are used by Sprockets for asset compression: ```ruby @@ -42,7 +42,7 @@ gem 'uglifier' gem 'coffee-rails' ``` -Using the `--skip-sprockets` option will prevent Rails 4 from adding +Using the `--skip-sprockets` option will prevent Rails 4 from adding `sass-rails` and `uglifier` to Gemfile, so if you later want to enable the asset pipeline you will have to add those gems to your Gemfile. Also, creating an application with the `--skip-sprockets` option will generate @@ -54,7 +54,7 @@ the comment operator on that line to later enable the asset pipeline: # require "sprockets/railtie" ``` -To set asset compression methods, set the appropriate configuration options +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: @@ -228,7 +228,7 @@ Pipeline assets can be placed inside an application in one of three locations: * `app/assets` is for assets that are owned by the application, such as custom images, JavaScript files or stylesheets. -* `lib/assets` is for your own libraries' code that doesn't really fit into the +* `lib/assets` is for your own libraries' code that doesn't really fit into the scope of the application or those libraries which are shared across applications. * `vendor/assets` is for assets that are owned by outside entities, such as @@ -350,8 +350,8 @@ Alternatively, a request for a file with an MD5 hash such as way. How these hashes are generated is covered in the [In Production](#in-production) section later on in this guide. -Sprockets will also look through the paths specified in `config.assets.paths`, -which includes the standard application paths and any paths added by Rails +Sprockets will also look through the paths specified in `config.assets.paths`, +which includes the standard application paths and any paths added by Rails engines. Images can also be organized into subdirectories if required, and then can be @@ -503,7 +503,7 @@ of these Sprockets directives. Using Sprockets directives all Sass files exist within their own scope, making variables or mixins only available within the document they were defined in. You can do file globbing as well using `@import "*"`, and `@import "**/*"` to add the whole tree equivalent to how -`require_tree` works. Check the [sass-rails +`require_tree` works. Check the [sass-rails documentation](https://github.com/rails/sass-rails#features) for more info and important caveats. @@ -532,7 +532,7 @@ and CSS file. The example used before was a controller called "projects", which generated an `app/assets/javascripts/projects.js.coffee` and an `app/assets/stylesheets/projects.css.scss` file. -In development mode, or if the asset pipeline is disabled, when these files are +In development mode, or if the asset pipeline is disabled, when these files are requested they are processed by the processors provided by the `coffee-script` and `sass` gems and then sent back to the browser as JavaScript and CSS respectively. When asset pipelining is enabled, these files are preprocessed and @@ -577,6 +577,33 @@ would generate this HTML: The `body` param is required by Sprockets. +### Runtime Error Checking + +By default the asset pipeline will check for potential errors in development mode during +runtime. To disable this behavior you can set: + +```ruby +config.assets.raise_runtime_errors = false +``` + +When `raise_runtime_errors` is set to `false` sprockets will not check that dependencies of assets are declared properly. Here is a scenario where you must tell the asset pipeline about a dependency: + +If you have `application.css.erb` that references `logo.png` like this: + +```css +#logo { background: url(<%= asset_data_uri 'logo.png' %>) } +``` + +Then you must declare that `logo.png` is a dependency of `application.css.erb`, so when the image gets re-compiled, the css file does as well. You can do this using the `//= depend_on_asset` declaration: + +```css +//= depend_on_asset "logo.png" +#logo { background: url(<%= asset_data_uri 'logo.png' %>) } +``` + +Without this declaration you may experience strange behavior when pushing to production that is difficult to debug. When you have `raise_runtime_errors` set to `true`, dependencies will be checked at runtime so you can ensure that all dependencies are met. + + ### Turning Debugging Off You can turn off debug mode by updating `config/environments/development.rb` to @@ -611,7 +638,7 @@ Debug mode can also be enabled in Rails helper methods: The `:debug` option is redundant if debug mode is already on. -You can also enable compression in development mode as a sanity check, and +You can also enable compression in development mode as a sanity check, and disable it on-demand as required for debugging. In Production @@ -911,7 +938,7 @@ Customizing the Pipeline ### CSS Compression -There is currently one option for compressing CSS, YUI. The [YUI CSS +One of the options for compressing CSS is YUI. The [YUI CSS compressor](http://yui.github.io/yuicompressor/css.html) provides minification. @@ -921,6 +948,11 @@ gem. ```ruby config.assets.css_compressor = :yui ``` +The other option for compressing CSS if you have the sass-rails gem installed is + +```ruby +config.assets.css_compressor = :sass +``` ### JavaScript Compression diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 59c2594422..5e0010725e 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -137,7 +137,9 @@ numbers. New applications filter out passwords by adding the following `config.f * `config.assets.enabled` a flag that controls whether the asset pipeline is enabled. It is set to true by default. -* `config.assets.compress` a flag that enables the compression of compiled assets. It is explicitly set to true in `config/production.rb`. +*`config.assets.raise_runtime_errors`* Set this flag to `true` to enable additional runtime error checking. Recommended in `config/environments/development.rb` to minimize unexpected behavior when deploying to `production`. + +* `config.assets.compress` a flag that enables the compression of compiled assets. It is explicitly set to true in `config/environments/production.rb`. * `config.assets.css_compressor` defines the CSS compressor to use. It is set by default by `sass-rails`. The unique alternative value at the moment is `:yui`, which uses the `yui-compressor` gem. @@ -244,8 +246,14 @@ config.middleware.delete "Rack::MethodOverride" ### Configuring i18n +All these configuration options are delegated to the `I18n` library. + +* `config.i18n.available_locales` whitelists the available locales for the app. Defaults to all locale keys found in locale files, usually only `:en` on a new application. + * `config.i18n.default_locale` sets the default locale of an application used for i18n. Defaults to `:en`. +* `config.i18n.enforce_available_locales` ensures that all locales passed through i18n must be declared in the `available_locales` list, raising an `I18n::InvalidLocale` exception when setting an unavailable locale. Defaults to `true`. It is recommended not to disable this option unless strongly required, since this works as a security measure against setting any invalid locale from user input. + * `config.i18n.load_path` sets the path Rails uses to look for locale files. Defaults to `config/locales/*.{yml,rb}`. ### Configuring Active Record @@ -282,6 +290,8 @@ config.middleware.delete "Rack::MethodOverride" * `config.active_record.attribute_types_cached_by_default` sets the attribute types that `ActiveRecord::AttributeMethods` will cache by default on reads. The default is `[:datetime, :timestamp, :time, :date]`. +* `config.active_record.maintain_test_schema` is a boolean value which controls whether Active Record should try to keep your test database schema up-to-date with `db/schema.rb` (or `db/structure.sql`) when you run your tests. The default is true. + The MySQL adapter adds one additional configuration option: * `ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans` controls whether Active Record will consider all `tinyint(1)` columns in a MySQL database to be booleans and is true by default. @@ -342,6 +352,10 @@ value. Defaults to `'encrypted cookie'`. * `config.action_dispatch.encrypted_signed_cookie_salt` sets the signed encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. +* `config.action_dispatch.perform_deep_munge` configures whether `deep_munge` + method should be performed on the parameters. See [Security Guide](security.html#unsafe-query-generation) + for more information. It defaults to true. + * `ActionDispatch::Callbacks.before` takes a block of code to run before the request. * `ActionDispatch::Callbacks.to_prepare` takes a block to run after `ActionDispatch::Callbacks.before`, but before the request. Runs for every request in `development` mode, but only once for `production` or environments with `cache_classes` set to `true`. @@ -376,6 +390,8 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. 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`. +* `config.action_view.raise_on_missing_translations` determines whether an error should be raised for missing translations + ### Configuring Action Mailer There are a number of settings available on `config.action_mailer`: @@ -396,17 +412,25 @@ There are a number of settings available on `config.action_mailer`: * `config.action_mailer.raise_delivery_errors` specifies whether to raise an error if email delivery cannot be completed. It defaults to true. -* `config.action_mailer.delivery_method` defines the delivery method. The allowed values are `:smtp` (default), `:sendmail`, and `:test`. +* `config.action_mailer.delivery_method` defines the delivery method and defaults to `:smtp`. See the [configuration section in the Action Mailer guide](http://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-configuration) for more info. * `config.action_mailer.perform_deliveries` specifies whether mail will actually be delivered and is true by default. It can be convenient to set it to false for testing. * `config.action_mailer.default_options` configures Action Mailer defaults. Use to set options like `from` or `reply_to` for every mailer. These default to: ```ruby - :mime_version => "1.0", - :charset => "UTF-8", - :content_type => "text/plain", - :parts_order => [ "text/plain", "text/enriched", "text/html" ] + mime_version: "1.0", + charset: "UTF-8", + content_type: "text/plain", + parts_order: ["text/plain", "text/enriched", "text/html"] + ``` + + Assign a hash to set additional options: + + ```ruby + config.action_mailer.default_options = { + from: "noreply@example.com" + } ``` * `config.action_mailer.observers` registers observers which will be notified when mail is delivered. @@ -431,6 +455,8 @@ There are a few configuration options available in Active Support: * `config.active_support.use_standard_json_time_format` enables or disables serializing dates to ISO 8601 format. Defaults to `true`. +* `config.active_support.time_precision` sets the precision of JSON encoded time values. Defaults to `3`. + * `ActiveSupport::Logger.silencer` is set to `false` to disable the ability to silence logging in a block. The default is `true`. * `ActiveSupport::Cache::Store.logger` specifies the logger to use within cache store operations. @@ -445,14 +471,131 @@ There are a few configuration options available in Active Support: ### Configuring a Database -Just about every Rails application will interact with a database. The database to use is specified in a configuration file called `config/database.yml`. If you open this file in a new Rails application, you'll see a default database configured to use SQLite3. The file contains sections for three different environments in which Rails can run by default: +Just about every Rails application will interact with a database. You can connect to the database by setting an environment variable `ENV['DATABASE_URL']` or by using a configuration file called `config/database.yml`. + +Using the `config/database.yml` file you can specify all the information needed to access your database: + +```yaml +development: + adapter: postgresql + database: blog_development + pool: 5 +``` + +This will connect to the database named `blog_development` using the `postgresql` adapter. This same information can be stored in a URL and provided via an environment variable like this: + +```ruby +> puts ENV['DATABASE_URL'] +postgresql://localhost/blog_development?pool=5 +``` + +The `config/database.yml` file contains sections for three different environments in which Rails can run by default: * The `development` environment is used on your development/local computer as you interact manually with the application. * The `test` environment is used when running automated tests. * The `production` environment is used when you deploy your application for the world to use. +If you wish, you can manually specify a URL inside of your `config/database.yml` + +``` +development: + url: postgresql://localhost/blog_development?pool=5 +``` + +The `config/database.yml` file can contain ERB tags `<%= %>`. Anything in the tags will be evaluated as Ruby code. You can use this to pull out data from an environment variable or to perform calculations to generate the needed connection information. + + TIP: You don't have to update the database configurations manually. If you look at the options of the application generator, you will see that one of the options is named `--database`. This option allows you to choose an adapter from a list of the most used relational databases. You can even run the generator repeatedly: `cd .. && rails new blog --database=mysql`. When you confirm the overwriting of the `config/database.yml` file, your application will be configured for MySQL instead of SQLite. Detailed examples of the common database connections are below. + +### Connection Preference + +Since there are two ways to set your connection, via environment variable it is important to understand how the two can interact. + +If you have an empty `config/database.yml` file but your `ENV['DATABASE_URL']` is present, then Rails will connect to the database via your environment variable: + +``` +$ cat config/database.yml + +$ echo $DATABASE_URL +postgresql://localhost/my_database +``` + +If you have a `config/database.yml` but no `ENV['DATABASE_URL']` then this file will be used to connect to your database: + +``` +$ cat config/database.yml +development: + adapter: postgresql + database: my_database + host: localhost + +$ echo $DATABASE_URL +``` + +If you have both `config/database.yml` and `ENV['DATABASE_URL']` set then Rails will merge the configuration together. To better understand this we must see some examples. + +When duplicate connection information is provided the environment variable will take precedence: + +``` +$ cat config/database.yml +development: + adapter: sqlite3 + database: NOT_my_database + host: localhost + +$ echo $DATABASE_URL +postgresql://localhost/my_database + +$ rails runner 'puts ActiveRecord::Base.connections' +{"development"=>{"adapter"=>"postgresql", "host"=>"localhost", "database"=>"my_database"}} +``` + +Here the adapter, host, and database match the information in `ENV['DATABASE_URL']`. + +If non-duplicate information is provided you will get all unique values, environment variable still takes precedence in cases of any conflicts. + +``` +$ cat config/database.yml +development: + adapter: sqlite3 + pool: 5 + +$ echo $DATABASE_URL +postgresql://localhost/my_database + +$ rails runner 'puts ActiveRecord::Base.connections' +{"development"=>{"adapter"=>"postgresql", "host"=>"localhost", "database"=>"my_database", "pool"=>5}} +``` + +Since pool is not in the `ENV['DATABASE_URL']` provided connection information its information is merged in. Since `adapter` is duplicate, the `ENV['DATABASE_URL']` connection information wins. + +The only way to explicitly not use the connection information in `ENV['DATABASE_URL']` is to specify an explicit URL connectinon using the `"url"` sub key: + +``` +$ cat config/database.yml +development: + url: sqlite3://localhost/NOT_my_database + +$ echo $DATABASE_URL +postgresql://localhost/my_database + +$ rails runner 'puts ActiveRecord::Base.connections' +{"development"=>{"adapter"=>"sqlite3", "host"=>"localhost", "database"=>"NOT_my_database"}} +``` + +Here the connection information in `ENV['DATABASE_URL']` is ignored, note the different adapter and database name. + +Since it is possible to embed ERB in your `config/database.yml` it is best practice to explicitly show you are using the `ENV['DATABASE_URL']` to connect to your database. This is especially useful in production since you should not commit secrets like your database password into your source control (such as Git). + +``` +$ cat config/database.yml +production: + url: <%= ENV['DATABASE_URL'] %> +``` + +Now the behavior is clear, that we are only using the connection information in `ENV['DATABASE_URL']`. + #### Configuring an SQLite3 Database Rails comes with built-in support for [SQLite3](http://www.sqlite.org), which is a lightweight serverless database application. While a busy production environment may overload SQLite, it works well for development and testing. Rails defaults to using an SQLite database when creating a new project, but you can always change it later. diff --git a/guides/source/credits.html.erb b/guides/source/credits.html.erb index 5beae9c29b..7c6858fa2c 100644 --- a/guides/source/credits.html.erb +++ b/guides/source/credits.html.erb @@ -76,5 +76,5 @@ Oscar Del Ben is a software engineer at <a href="http://www.wildfireapp.com/">Wi <% end %> <%= author('Akshay Surve', 'startupjockey', 'akshaysurve.jpg') do %> - Akshay Surve is the Founder at <a href="http://www.deltax.com">DeltaX</a>, hackathon specialist, a midnight code junkie and ocassionally writes prose. You can connect with him on <a href="https://twitter.com/akshaysurve">Twitter</a>, <a href="http://www.linkedin.com/in/akshaysurve">Linkedin</a>, <a href="http://www.akshaysurve.com/">Personal Blog</a> or <a href="http://www.quora.com/Akshay-Surve">Quora</a>. + Akshay Surve is the Founder at <a href="http://www.deltax.com">DeltaX</a>, hackathon specialist, a midnight code junkie and occasionally writes prose. You can connect with him on <a href="https://twitter.com/akshaysurve">Twitter</a>, <a href="http://www.linkedin.com/in/akshaysurve">Linkedin</a>, <a href="http://www.akshaysurve.com/">Personal Blog</a> or <a href="http://www.quora.com/Akshay-Surve">Quora</a>. <% end %> diff --git a/guides/source/development_dependencies_install.md b/guides/source/development_dependencies_install.md index c4e5789a1a..4ee43b6a97 100644 --- a/guides/source/development_dependencies_install.md +++ b/guides/source/development_dependencies_install.md @@ -233,6 +233,8 @@ mysql> GRANT ALL PRIVILEGES ON activerecord_unittest.* to 'rails'@'localhost'; mysql> GRANT ALL PRIVILEGES ON activerecord_unittest2.* to 'rails'@'localhost'; +mysql> GRANT ALL PRIVILEGES ON inexistent_activerecord_unittest.* + to 'rails'@'localhost'; ``` and create the test databases: diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml index ae47744e31..e4653b47fc 100644 --- a/guides/source/documents.yaml +++ b/guides/source/documents.yaml @@ -117,7 +117,7 @@ name: The Rails Initialization Process work_in_progress: true url: initialization.html - description: This guide explains the internals of the Rails initialization process as of Rails 3.1 + description: This guide explains the internals of the Rails initialization process as of Rails 4 - name: Extending Rails documents: diff --git a/guides/source/engines.md b/guides/source/engines.md index 2266b1fd7f..bbd63bb892 100644 --- a/guides/source/engines.md +++ b/guides/source/engines.md @@ -1,7 +1,9 @@ Getting Started with Engines ============================ -In this guide you will learn about engines and how they can be used to provide additional functionality to their host applications through a clean and very easy-to-use interface. +In this guide you will learn about engines and how they can be used to provide +additional functionality to their host applications through a clean and very +easy-to-use interface. After reading this guide, you will know: @@ -16,26 +18,59 @@ After reading this guide, you will know: What are engines? ----------------- -Engines can be considered miniature applications that provide functionality to their host applications. A Rails application is actually just a "supercharged" engine, with the `Rails::Application` class inheriting a lot of its behavior from `Rails::Engine`. - -Therefore, engines and applications can be thought of almost the same thing, just with subtle differences, as you'll see throughout this guide. Engines and applications also share a common structure. - -Engines are also closely related to plugins where the two share a common `lib` directory structure and are both generated using the `rails plugin new` generator. The difference being that an engine is considered a "full plugin" by Rails as indicated by the `--full` option that's passed to the generator command, but this guide will refer to them simply as "engines" throughout. An engine **can** be a plugin, and a plugin **can** be an engine. - -The engine that will be created in this guide will be called "blorgh". The 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 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 and table names are also namespaced. You'll see how to do this later in this guide. - -It's important to keep in mind at all times that the application should **always** take precedence over its engines. An application is the object that has final say in what goes on in the universe (with the universe being the application's environment) where the engine should only be enhancing it, rather than changing it drastically. - -To see demonstrations of other engines, check out [Devise](https://github.com/plataformatec/devise), an engine that provides authentication for its parent applications, or [Forem](https://github.com/radar/forem), an engine that provides forum functionality. There's also [Spree](https://github.com/spree/spree) which provides an e-commerce platform, and [RefineryCMS](https://github.com/refinery/refinerycms), a CMS engine. - -Finally, engines would not have been possible without the work of James Adam, Piotr Sarnacki, the Rails Core Team, and a number of other people. If you ever meet them, don't forget to say thanks! +Engines can be considered miniature applications that provide functionality to +their host applications. A Rails application is actually just a "supercharged" +engine, with the `Rails::Application` class inheriting a lot of its behavior +from `Rails::Engine`. + +Therefore, engines and applications can be thought of almost the same thing, +just with subtle differences, as you'll see throughout this guide. Engines and +applications also share a common structure. + +Engines are also closely related to plugins. The two share a common `lib` +directory structure, and are both generated using the `rails plugin new` +generator. The difference is that an engine is considered a "full plugin" by +Rails (as indicated by the `--full` option that's passed to the generator +command). This guide will refer to them simply as "engines" throughout. An +engine **can** be a plugin, and a plugin **can** be an engine. + +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 +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 +and table names are also namespaced. You'll see how to do this later in this +guide. + +It's important to keep in mind at all times that the application should +**always** take precedence over its engines. An application is the object that +has final say in what goes on in the universe (with the universe being the +application's environment) where the engine should only be enhancing it, rather +than changing it drastically. + +To see demonstrations of other engines, check out +[Devise](https://github.com/plataformatec/devise), an engine that provides +authentication for its parent applications, or +[Forem](https://github.com/radar/forem), an engine that provides forum +functionality. There's also [Spree](https://github.com/spree/spree) which +provides an e-commerce platform, and +[RefineryCMS](https://github.com/refinery/refinerycms), a CMS engine. + +Finally, engines would not have been possible without the work of James Adam, +Piotr Sarnacki, the Rails Core Team, and a number of other people. If you ever +meet them, don't forget to say thanks! Generating an engine -------------------- -To generate an engine, you will need to run the plugin generator and pass it options as appropriate to the need. For the "blorgh" example, you will need to create a "mountable" engine, running this command in a terminal: +To generate an engine, you will need to run the plugin generator and pass it +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 @@ -47,7 +82,8 @@ The full list of options for the plugin generator may be seen by typing: $ rails plugin --help ``` -The `--full` option tells the generator that you want to create an engine, including a skeleton structure by providing the following: +The `--full` option tells the generator that you want to create an engine, +including a skeleton structure that provides the following: * An `app` directory tree * A `config/routes.rb` file: @@ -56,7 +92,9 @@ The `--full` option tells the generator that you want to create an engine, inclu Rails.application.routes.draw do end ``` - * A file at `lib/blorgh/engine.rb` which is identical in function to a standard Rails application's `config/application.rb` file: + + * A file at `lib/blorgh/engine.rb`, which is identical in function to a + * standard Rails application's `config/application.rb` file: ```ruby module Blorgh @@ -65,7 +103,9 @@ The `--full` option tells the generator that you want to create an engine, inclu end ``` -The `--mountable` option tells the generator that you want to create a "mountable" and namespace-isolated engine. This generator will provide the same skeleton structure as would the `--full` option, and will add: +The `--mountable` option tells the generator that you want to create a +"mountable" and namespace-isolated engine. This generator will provide the same +skeleton structure as would the `--full` option, and will add: * Asset manifest files (`application.js` and `application.css`) * A namespaced `ApplicationController` stub @@ -88,23 +128,32 @@ The `--mountable` option tells the generator that you want to create a "mountabl end ``` -Additionally, the `--mountable` option tells the generator to mount the engine inside the dummy testing application located at `test/dummy` by adding the following to the dummy application's routes file at `test/dummy/config/routes.rb`: +Additionally, the `--mountable` option tells the generator to mount the engine +inside the dummy testing application located at `test/dummy` by adding the +following to the dummy application's routes file at +`test/dummy/config/routes.rb`: ```ruby mount Blorgh::Engine, at: "blorgh" ``` -### Inside an engine +### Inside an Engine -#### Critical files +#### Critical Files -At the root of this brand new engine's directory lives a `blorgh.gemspec` file. When you include the engine into an application later on, you will do so with this line in the Rails application's `Gemfile`: +At the root of this brand new engine's directory lives a `blorgh.gemspec` file. +When you include the engine into an application later on, you will do so with +this line in the Rails application's `Gemfile`: ```ruby gem 'blorgh', path: "vendor/engines/blorgh" ``` -Don't forget to run `bundle install` as usual. By specifying it as a gem within the `Gemfile`, Bundler will load it as such, parsing this `blorgh.gemspec` file and requiring a file within the `lib` directory called `lib/blorgh.rb`. This file requires the `blorgh/engine.rb` file (located at `lib/blorgh/engine.rb`) and defines a base module called `Blorgh`. +Don't forget to run `bundle install` as usual. By specifying it as a gem within +the `Gemfile`, Bundler will load it as such, parsing this `blorgh.gemspec` file +and requiring a file within the `lib` directory called `lib/blorgh.rb`. This +file requires the `blorgh/engine.rb` file (located at `lib/blorgh/engine.rb`) +and defines a base module called `Blorgh`. ```ruby require "blorgh/engine" @@ -113,7 +162,10 @@ module Blorgh end ``` -TIP: Some engines choose to use this file to put global configuration options for their engine. It's a relatively good idea, and so if you want to offer configuration options, the file where your engine's `module` is defined is perfect for that. Place the methods inside the module and you'll be good to go. +TIP: Some engines choose to use this file to put global configuration options +for their engine. It's a relatively good idea, so if you want to offer +configuration options, the file where your engine's `module` is defined is +perfect for that. Place the methods inside the module and you'll be good to go. Within `lib/blorgh/engine.rb` is the base class for the engine: @@ -125,43 +177,94 @@ module Blorgh end ``` -By inheriting from the `Rails::Engine` class, this gem notifies Rails that there's an engine at the specified path, and will correctly mount the engine inside the application, performing tasks such as adding the `app` directory of the engine to the load path for models, mailers, controllers and views. - -The `isolate_namespace` method here deserves special notice. This call is responsible for isolating the controllers, models, routes and other things into their own namespace, away from similar components inside the application. Without this, there is a possibility that the engine's components could "leak" into the application, causing unwanted disruption, or that important engine components could be overridden by similarly named things within the application. One of the examples of such conflicts are helpers. Without calling `isolate_namespace`, engine's helpers would be included in an application's controllers. - -NOTE: It is **highly** recommended that the `isolate_namespace` line be left 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 as well. - -Finally, routes will also be isolated within the engine. This is one of the most important parts about namespacing, and is discussed later in the [Routes](#routes) section of this guide. - -#### `app` directory - -Inside the `app` directory are the standard `assets`, `controllers`, `helpers`, `mailers`, `models` and `views` directories that you should be familiar with from an application. The `helpers`, `mailers` and `models` directories are empty and so aren't described in this section. We'll look more into models in a future section, when we're writing the engine. - -Within the `app/assets` directory, there are the `images`, `javascripts` and `stylesheets` directories which, again, you should be familiar with due to their similarity to an application. One difference here however is that each directory contains a sub-directory with the engine name. Because this engine is going to be namespaced, its assets should be too. - -Within the `app/controllers` directory there is a `blorgh` directory and inside that a file called `application_controller.rb`. This file will provide any common functionality for the controllers of the engine. The `blorgh` directory is where the other controllers for the engine will go. By placing them within this namespaced directory, you prevent them from possibly clashing with identically-named controllers within other engines or even within the application. - -NOTE: The `ApplicationController` class inside an engine is named just like a Rails application in order to make it easier for you to convert your applications into engines. - -Lastly, the `app/views` directory contains a `layouts` folder which contains a file at `blorgh/application.html.erb` which allows you to specify a layout for the engine. If this engine is to be used as a stand-alone engine, then you would add any customization to its layout in this file, rather than the application's `app/views/layouts/application.html.erb` file. - -If you don't want to force a layout on to users of the engine, then you can delete this file and reference a different layout in the controllers of your engine. - -#### `bin` directory - -This directory contains one file, `bin/rails`, which enables you to use the `rails` sub-commands and generators just like you would within an application. This means that you will very easily be able to generate new controllers and models for this engine by running commands like this: +By inheriting from the `Rails::Engine` class, this gem notifies Rails that +there's an engine at the specified path, and will correctly mount the engine +inside the application, performing tasks such as adding the `app` directory of +the engine to the load path for models, mailers, controllers and views. + +The `isolate_namespace` method here deserves special notice. This call is +responsible for isolating the controllers, models, routes and other things into +their own namespace, away from similar components inside the application. +Without this, there is a possibility that the engine's components could "leak" +into the application, causing unwanted disruption, or that important engine +components could be overridden by similarly named things within the application. +One of the examples of such conflicts is helpers. Without calling +`isolate_namespace`, the engine's helpers would be included in an application's +controllers. + +NOTE: It is **highly** recommended that the `isolate_namespace` line be left +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 +as well. + +Finally, routes will also be isolated within the engine. This is one of the most +important parts about namespacing, and is discussed later in the +[Routes](#routes) section of this guide. + +#### `app` Directory + +Inside the `app` directory are the standard `assets`, `controllers`, `helpers`, +`mailers`, `models` and `views` directories that you should be familiar with +from an application. The `helpers`, `mailers` and `models` directories are +empty, so they aren't described in this section. We'll look more into models in +a future section, when we're writing the engine. + +Within the `app/assets` directory, there are the `images`, `javascripts` and +`stylesheets` directories which, again, you should be familiar with due to their +similarity to an application. One difference here, however, is that each +directory contains a sub-directory with the engine name. Because this engine is +going to be namespaced, its assets should be too. + +Within the `app/controllers` directory there is a `blorgh` directory that +contains a file called `application_controller.rb`. This file will provide any +common functionality for the controllers of the engine. The `blorgh` directory +is where the other controllers for the engine will go. By placing them within +this namespaced directory, you prevent them from possibly clashing with +identically-named controllers within other engines or even within the +application. + +NOTE: The `ApplicationController` class inside an engine is named just like a +Rails application in order to make it easier for you to convert your +applications into engines. + +Lastly, the `app/views` directory contains a `layouts` folder, which contains a +file at `blorgh/application.html.erb`. This file allows you to specify a layout +for the engine. If this engine is to be used as a stand-alone engine, then you +would add any customization to its layout in this file, rather than the +application's `app/views/layouts/application.html.erb` file. + +If you don't want to force a layout on to users of the engine, then you can +delete this file and reference a different layout in the controllers of your +engine. + +#### `bin` Directory + +This directory contains one file, `bin/rails`, which enables you to use the +`rails` sub-commands and generators just like you would within an application. +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 ``` -Keeping in mind, of course, that anything generated with these commands inside an engine that has `isolate_namespace` inside the `Engine` class will be namespaced. +Keep in mind, of course, that anything generated with these commands inside of +an engine that has `isolate_namespace` in the `Engine` class will be namespaced. -#### `test` directory +#### `test` Directory -The `test` directory is where tests for the engine will go. To test the engine, there is a cut-down version of a Rails application embedded within it at `test/dummy`. This application will mount the engine in the `test/dummy/config/routes.rb` file: +The `test` directory is where tests for the engine will go. To test the engine, +there is a cut-down version of a Rails application embedded within it at +`test/dummy`. This application will mount the engine in the +`test/dummy/config/routes.rb` file: ```ruby Rails.application.routes.draw do @@ -169,18 +272,25 @@ Rails.application.routes.draw do end ``` -This line mounts the engine at the path `/blorgh`, which will make it accessible through the application only at that path. +This line mounts the engine at the path `/blorgh`, which will make it accessible +through the application only at that path. -In the test directory there is the `test/integration` directory, where integration tests for the engine should be placed. Other directories can be created in the `test` directory as well. For example, you may wish to create a `test/models` directory for your models tests. +Inside the test directory there is the `test/integration` directory, where +integration tests for the engine should be placed. Other directories can be +created in the `test` directory as well. For example, you may wish to create a +`test/models` directory for your model tests. Providing engine functionality ------------------------------ -The engine that this guide covers provides posting and commenting functionality and follows a similar thread to the [Getting Started Guide](getting_started.html), with some new twists. +The engine that this guide covers provides posting 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 a Post Resource -The first thing to generate for a blog engine is the `Post` model and related controller. To quickly generate this, you can use the Rails scaffold generator. +The first thing to generate for a blog engine is the `Post` model and related +controller. To quickly generate this, you can use the Rails scaffold generator. ```bash $ rails generate scaffold post title:string text:text @@ -195,7 +305,8 @@ create app/models/blorgh/post.rb invoke test_unit create test/models/blorgh/post_test.rb create test/fixtures/blorgh/posts.yml - route resources :posts +invoke resource_route + route resources :posts invoke scaffold_controller create app/controllers/blorgh/posts_controller.rb invoke erb @@ -220,11 +331,22 @@ 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 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 to the `isolate_namespace` call within the `Engine` class. +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 +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 +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`). +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`). -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 `config/routes.rb` file for the engine into this: +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 +`config/routes.rb` file for the engine into this: ```ruby Blorgh::Engine.routes.draw do @@ -232,12 +354,22 @@ Blorgh::Engine.routes.draw do end ``` -Note here that the routes are drawn upon the `Blorgh::Engine` object rather than the `YourApp::Application` class. This is so that the engine routes are confined to the engine itself and can be mounted at a specific point as shown in the [test directory](#test-directory) section. It also causes the engine's routes to be isolated from those routes that are within the application. The [Routes](#routes) section of -this guide describes it in details. +Note here that the routes are drawn upon the `Blorgh::Engine` object rather than +the `YourApp::Application` class. This is so that the engine routes are confined +to the engine itself and can be mounted at a specific point as shown in the +[test directory](#test-directory) section. It also causes the engine's routes to +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`). +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`). -Everything this generator has created is neatly namespaced. The controller's class is defined within the `Blorgh` module: +Everything this generator has created is neatly namespaced. The controller's +class is defined within the `Blorgh` module: ```ruby module Blorgh @@ -247,7 +379,8 @@ module Blorgh end ``` -NOTE: The `ApplicationController` class being inherited from here is the `Blorgh::ApplicationController`, not an application's `ApplicationController`. +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: @@ -259,38 +392,63 @@ module Blorgh end ``` -This helps prevent conflicts with any other engine or application that may have a post resource as well. +This helps prevent conflicts with any other engine or application that may have +a post resource as well. -Finally, two files that are the assets for this resource are generated, `app/assets/javascripts/blorgh/posts.js` and `app/assets/stylesheets/blorgh/posts.css`. You'll see how to use these a little later. +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 +later. -By default, the scaffold styling is not applied to the engine as the engine's layout file, `app/views/layouts/blorgh/application.html.erb` doesn't load it. To make this apply, insert this line into the `<head>` tag of this layout: +By default, the scaffold styling is not applied to the engine because the +engine's layout file, `app/views/layouts/blorgh/application.html.erb`, doesn't +load it. To make the scaffold styling apply, insert this line into the `<head>` +tag of this layout: ```erb <%= stylesheet_link_tag "scaffold" %> ``` -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 been generated. Click around! You've just generated your first engine's first functions. +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 +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`. +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`. ```ruby >> Blorgh::Post.find(1) => #<Blorgh::Post id: 1 ...> ``` -One final thing is that the `posts` 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 this line is inserted into the `config/routes.rb` file inside the engine: +One final thing is that the `posts` 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 +this line is inserted into the `config/routes.rb` file inside the engine: ```ruby root to: "posts#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 `http://localhost:3000/blorgh` now. +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 +`http://localhost:3000/blorgh` now. -### Generating a comments resource +### Generating a Comments Resource -Now that the engine can create new blog posts, 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 comments and allow people to create new ones. +Now that the engine can create new blog posts, 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 +comments and allow people to create new ones. -Run the model generator and tell it to generate a `Comment` model, with the related table having two columns: a `post_id` integer and `text` text column. +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 +and `text` text column. ```bash $ rails generate model Comment post_id:integer text:text @@ -307,20 +465,26 @@ create test/models/blorgh/comment_test.rb create test/fixtures/blorgh/comments.yml ``` -This generator call will generate just the necessary model files it needs, namespacing the files under a `blorgh` directory and creating a model class called `Blorgh::Comment`. Now run the migration to create our blorgh_comments table: +This generator call will generate just the necessary model files it needs, +namespacing the files under a `blorgh` directory and creating a model class +called `Blorgh::Comment`. Now run the migration to create our blorgh_comments +table: ```bash $ rake db:migrate ``` -To show the comments on a post, edit `app/views/blorgh/posts/show.html.erb` and add this line before the "Edit" link: +To show the comments on a post, edit `app/views/blorgh/posts/show.html.erb` and +add this line before the "Edit" link: ```html+erb <h3>Comments</h3> <%= render @post.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: +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: ```ruby has_many :comments @@ -336,15 +500,22 @@ module Blorgh end ``` -NOTE: Because the `has_many` is defined inside a class that is inside the `Blorgh` module, Rails will know that you want to use the `Blorgh::Comment` model for these objects, so there's no need to specify that using the `:class_name` option here. +NOTE: Because the `has_many` is defined inside a class that is inside the +`Blorgh` module, Rails will know that you want to use the `Blorgh::Comment` +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 post. To add +this, put this line underneath the call to `render @post.comments` in +`app/views/blorgh/posts/show.html.erb`: ```erb <%= render "blorgh/comments/form" %> ``` -Next, the partial that this line will render needs to exist. Create a new directory at `app/views/blorgh/comments` and in it a new file called `_form.html.erb` which has this content to create the required partial: +Next, the partial that this line will render needs to exist. Create a new +directory at `app/views/blorgh/comments` and in it a new file called +`_form.html.erb` which has this content to create the required partial: ```html+erb <h3>New comment</h3> @@ -357,7 +528,10 @@ Next, the partial that this line will render needs to exist. Create a new direct <% end %> ``` -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 inside `config/routes.rb` into these lines: +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 +inside `config/routes.rb` into these lines: ```ruby resources :posts do @@ -367,7 +541,8 @@ end This creates a nested route for the comments, which is what the form requires. -The route now exists, but the controller that this route goes to does not. To create it, run this command: +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 @@ -392,7 +567,10 @@ invoke css create app/assets/stylesheets/blorgh/comments.css ``` -The form will be making a `POST` request to `/posts/:post_id/comments`, which will correspond with the `create` action in `Blorgh::CommentsController`. This action needs to be created and can be done by putting the following lines inside the class definition in `app/controllers/blorgh/comments_controller.rb`: +The form will be making a `POST` request to `/posts/:post_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 @@ -408,119 +586,182 @@ private end ``` -This is the final part required to get the new comment form working. Displaying the comments however, is not quite right yet. If you were to create a comment right now you would see this error: +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" +``` +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" ``` -The engine is unable to find the partial required for rendering the comments. Rails looks first in the application's (`test/dummy`) `app/views` directory and then in the engine's `app/views` directory. When it can't find it, it will throw this error. The engine knows to look for `blorgh/comments/comment` because the model object it is receiving is from the `Blorgh::Comment` class. +The engine is unable to find the partial required for rendering the comments. +Rails looks first in the application's (`test/dummy`) `app/views` directory and +then in the engine's `app/views` directory. When it can't find it, it will throw +this error. The engine knows to look for `blorgh/comments/comment` because the +model object it is receiving is from the `Blorgh::Comment` class. -This partial will be responsible for rendering just the comment text, for now. Create a new file at `app/views/blorgh/comments/_comment.html.erb` and put this line inside it: +This partial will be responsible for rendering just the comment text, for now. +Create a new file at `app/views/blorgh/comments/_comment.html.erb` and put this +line inside it: ```erb <%= comment_counter + 1 %>. <%= comment.text %> ``` -The `comment_counter` local variable is given to us by the `<%= render @post.comments %>` call, as it will define this 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. +The `comment_counter` local variable is given to us by the `<%= render +@post.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. -That completes the comment function of the blogging engine. Now it's time to use it within an application. +That completes the comment function of the blogging engine. Now it's time to use +it within an application. -Hooking into an application +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. +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. -### Mounting the engine +### Mounting the Engine -First, the engine needs to be specified inside the application's `Gemfile`. If there isn't an application handy to test this out in, generate one using the `rails new` command outside of the engine directory like this: +First, the engine needs to be specified inside the application's `Gemfile`. If +there isn't an application handy to test this out in, generate one using the +`rails new` command outside of the engine directory like this: ```bash $ rails new unicorn ``` -Usually, specifying the engine inside the Gemfile would be done by specifying it as a normal, everyday gem. +Usually, specifying the engine inside the Gemfile would be done by specifying it +as a normal, everyday gem. ```ruby gem 'devise' ``` -However, because you are developing the `blorgh` engine on your local machine, you will need to specify the `:path` option in your `Gemfile`: +However, because you are developing the `blorgh` engine on your local machine, +you will need to specify the `:path` option in your `Gemfile`: ```ruby gem 'blorgh', path: "/path/to/blorgh" ``` -As described earlier, by placing the gem in the `Gemfile` it will be loaded when Rails is loaded, as it will first require `lib/blorgh.rb` in the engine and then `lib/blorgh/engine.rb`, which is the file that defines the major pieces of functionality for the engine. +Then run `bundle` to install the gem. + +As described earlier, by placing the gem in the `Gemfile` it will be loaded when +Rails is loaded. It will first require `lib/blorgh.rb` from the engine, then +`lib/blorgh/engine.rb`, which is the file that defines the major pieces of +functionality for the engine. -To make the engine's functionality accessible from within an application, it needs to be mounted in that application's `config/routes.rb` file: +To make the engine's functionality accessible from within an application, it +needs to be mounted in that application's `config/routes.rb` file: ```ruby mount Blorgh::Engine, at: "/blog" ``` -This line will mount the engine at `/blog` in the application. Making it accessible at `http://localhost:3000/blog` when the application runs with `rails server`. +This line will mount the engine at `/blog` in the application. Making it +accessible at `http://localhost:3000/blog` when the application runs with `rails +server`. -NOTE: Other engines, such as Devise, handle this a little differently by making you specify custom helpers such as `devise_for` in the routes. These helpers do exactly the same thing, mounting pieces of the engines's functionality at a pre-defined path which may be customizable. +NOTE: Other engines, such as Devise, handle this a little differently by making +you specify custom helpers (such as `devise_for`) in the routes. These helpers +do exactly the same thing, mounting pieces of the engines's functionality at a +pre-defined path which may be customizable. ### Engine setup -The engine contains migrations for the `blorgh_posts` 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: +The engine contains migrations for the `blorgh_posts` 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 ``` -If you have multiple engines that need migrations copied over, use `railties:install:migrations` instead: +If you have multiple engines that need migrations copied over, use +`railties:install:migrations` instead: ```bash $ rake railties:install:migrations ``` -This command, when run for the first time, will copy over all the migrations from the engine. When run the next time, it will only copy over migrations that haven't been copied over already. The first run for this command will output something such as this: +This command, when run for the first time, will copy over all the migrations +from the engine. When run the next time, it will only copy over migrations that +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_2]_create_blorgh_comments.rb from blorgh ``` -The first timestamp (`[timestamp_1]`) will be the current time and the second timestamp (`[timestamp_2]`) will be the current time plus a second. The reason for this is so that the migrations for the engine are run after any existing migrations in the application. +The first timestamp (`[timestamp_1]`) will be the current time, and the second +timestamp (`[timestamp_2]`) will be the current time plus a second. The reason +for this is so that the migrations for the engine are run after any existing +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 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. +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 +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. -If you would like to run migrations only from one engine, you can do it by specifying `SCOPE`: +If you would like to run migrations only from one engine, you can do it by +specifying `SCOPE`: ```bash rake db:migrate SCOPE=blorgh ``` -This may be useful if you want to revert engine's migrations before removing it. In order to revert all migrations from blorgh engine you can run such code: +This may be useful if you want to revert engine's migrations before removing it. +To revert all migrations from blorgh engine you can run code such as: ```bash rake db:migrate SCOPE=blorgh VERSION=0 ``` -### Using a class provided by the application +### Using a Class Provided by the Application -#### Using a model provided by the application +#### Using a Model Provided by the Application -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 have authors would make a lot of sense. +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 +have authors would make a lot of sense. -A typical application might have a `User` class that would be used to represent authors for a post or a comment. But there could be a case where the application calls this class something different, such as `Person`. For this reason, the engine should not hardcode associations specifically for a `User` class. +A typical application might have a `User` class that would be used to represent +authors for a post or a comment. But there could be a case where the application +calls this class something different, such as `Person`. For this reason, the +engine should not hardcode associations specifically for a `User` class. -To keep it simple in this case, the application will have a class called `User` which will represent the users of the application. It can be generated using this command inside the application: +To keep it simple in this case, the application will have a class called `User` +that represents the users of the application. It can be generated using this +command inside the application: ```bash 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. +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 `author_name` where users can elect to put their name. The engine will then take this name and create a new `User` object from it or find one that already has that name, and then associate the post with it. +Also, to keep it simple, the posts 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 +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 added above the `title` field with this code: +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 +added above the `title` field with this code: ```html+erb <div class="field"> @@ -529,7 +770,8 @@ First, the `author_name` text field needs to be added to the `app/views/blorgh/p </div> ``` -Next, we need to update our `Blorgh::PostController#post_params` method to permit the new form parameter: +Next, we need to update our `Blorgh::PostController#post_params` method to +permit the new form parameter: ```ruby def post_params @@ -537,9 +779,15 @@ def post_params 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` setup for this field so that the setter and getter methods are defined for it. +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 +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 `User` class for the time being. +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 +`User` class for the time being. ```ruby attr_accessor :author_name @@ -553,7 +801,11 @@ private end ``` -By defining that the `author` association's object is represented by 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 `users` table. Because the association is called `author`, there should be an `author_id` column added to the `blorgh_posts` table. +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 +`users` table. Because the association is called `author`, there should be an +`author_id` column added to the `blorgh_posts` table. To generate this new column, run this command within the engine: @@ -561,31 +813,41 @@ To generate this new column, run this command within the engine: $ rails g migration add_author_id_to_blorgh_posts author_id:integer ``` -NOTE: Due to the migration's name and the column specification after it, Rails will automatically know that you want to add a column to a specific table and write that into the migration for you. You don't need to tell it any more than this. +NOTE: Due to the migration's name and the column specification after it, Rails +will automatically know that you want to add a column to a specific table and +write that into the migration for you. You don't need to tell it any more than +this. -This migration will need to be run on the application. To do that, it must first be copied using this command: +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 ``` -Notice here 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. +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 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 +``` +NOTE Migration [timestamp]_create_blorgh_posts.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 ``` -Run this migration using this command: +Run the migration using: ```bash $ 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. +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. -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 post's page. Add this code +above the "Title" output inside `app/views/blorgh/posts/show.html.erb`: ```html+erb <p> @@ -594,13 +856,15 @@ Finally, the author's name should be displayed on the post's page. Add this code </p> ``` -By outputting `@post.author` using the `<%=` tag, the `to_s` method will be called on the object. By default, this will look quite ugly: +By outputting `@post.author` using the `<%=` tag, the `to_s` method will be +called on the object. By default, this will look quite ugly: ``` #<User:0x00000100ccb3b0> ``` -This is undesirable and it would be much better to have the user's name there. To do this, add a `to_s` method to the `User` class within the application: +This is undesirable. It would be much better to have the user's name there. To +do this, add a `to_s` method to the `User` class within the application: ```ruby def to_s @@ -608,50 +872,77 @@ def to_s end ``` -Now instead of the ugly Ruby object output the author's name will be displayed. +Now instead of the ugly Ruby object output, the author's name will be displayed. -#### Using a controller provided by the application +#### Using a Controller Provided by the Application -Because Rails controllers generally share code for things like authentication and accessing session variables, by default they inherit from `ApplicationController`. Rails engines, however are scoped to run independently from the main application, so each engine gets a scoped `ApplicationController`. This namespace prevents code collisions, but often engine controllers should access methods in the main application's `ApplicationController`. An easy way to provide this access is to change the engine's scoped `ApplicationController` to inherit from the main application's `ApplicationController`. For our Blorgh engine this would be done by changing `app/controllers/blorgh/application_controller.rb` to look like: +Because Rails controllers generally share code for things like authentication +and accessing session variables, they inherit from `ApplicationController` by +default. Rails engines, however are scoped to run independently from the main +application, so each engine gets a scoped `ApplicationController`. This +namespace prevents code collisions, but often engine controllers need to access +methods in the main application's `ApplicationController`. An easy way to +provide this access is to change the engine's scoped `ApplicationController` to +inherit from the main application's `ApplicationController`. For our Blorgh +engine this would be done by changing +`app/controllers/blorgh/application_controller.rb` to look like: ```ruby class Blorgh::ApplicationController < ApplicationController end ``` -By default, the engine's controllers inherit from `Blorgh::ApplicationController`. So, after making this change they will have access to the main applications `ApplicationController` as though they were part of the main application. +By default, the engine's controllers inherit from +`Blorgh::ApplicationController`. So, after making this change they will have +access to the main application's `ApplicationController`, as though they were +part of the main application. -This change does require that the engine is run from a Rails application that has an `ApplicationController`. +This change does require that the engine is run from a Rails application that +has an `ApplicationController`. -### Configuring an engine +### Configuring an Engine -This section covers how to make the `User` class configurable, followed by general configuration tips for the engine. +This section covers how to make the `User` class configurable, followed by +general configuration tips for the engine. -#### Setting configuration settings in the application +#### Setting Configuration Settings in the Application -The next step is to make the class that represents a `User` in the application customizable for the engine. This is because, as explained before, that class may not always be `User`. To make this customizable, the engine will have a configuration setting called `author_class` that will be used to specify what the class representing users is inside the application. +The next step is to make the class that represents a `User` in the application +customizable for the engine. This is because that class may not always be +`User`, as previously explained. To make this setting customizable, the engine +will have a configuration setting called `author_class` that will be used to +specify which class represents users inside the application. -To define this configuration setting, you should use a `mattr_accessor` inside the `Blorgh` module for the engine, located at `lib/blorgh.rb` inside the engine. Inside this module, put this line: +To define this configuration setting, you should use a `mattr_accessor` inside +the `Blorgh` module for the engine. Add this line to `lib/blorgh.rb` inside the +engine: ```ruby mattr_accessor :author_class ``` -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`. +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 switching the `Blorgh::Post` model over to this new setting. For the `belongs_to` association inside this model (`app/models/blorgh/post.rb`), it will now become this: +The next step is to switch the `Blorgh::Post` model over to this new setting. +Change the `belongs_to` association inside this model +(`app/models/blorgh/post.rb`) to this: ```ruby belongs_to :author, class_name: Blorgh.author_class ``` -The `set_author` method also located in this class should also use this class: +The `set_author` method in the `Blorgh::Post` model should also use this class: ```ruby self.author = Blorgh.author_class.constantize.find_or_create_by(name: author_name) ``` -To save having to call `constantize` on the `author_class` result all the time, you could instead just override the `author_class` getter method inside the `Blorgh` module in the `lib/blorgh.rb` file to always call `constantize` on the saved value before returning the result: +To save having to call `constantize` on the `author_class` result all the time, +you could instead just override the `author_class` getter method inside the +`Blorgh` module in the `lib/blorgh.rb` file to always call `constantize` on the +saved value before returning the result: ```ruby def self.author_class @@ -665,82 +956,124 @@ This would then turn the above code for `set_author` into this: self.author = Blorgh.author_class.find_or_create_by(name: author_name) ``` -Resulting in something a little shorter, and more implicit in its behavior. The `author_class` method should always return a `Class` object. +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 no longer return a -`String` but a `Class` we must also modify our `belongs_to` definition -in the `Blorgh::Post` model: +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` +model: ```ruby belongs_to :author, class_name: Blorgh.author_class.to_s ``` -To set this configuration setting within the application, an initializer should be used. By using an initializer, the configuration will be set up before the application starts and calls the engine's models which may depend on this configuration setting existing. +To set this configuration setting within the application, an initializer should +be used. By using an initializer, the configuration will be set up before the +application starts and calls the engine's models, which may depend on this +configuration setting existing. -Create a new initializer at `config/initializers/blorgh.rb` inside the application where the `blorgh` engine is installed and put this content in it: +Create a new initializer at `config/initializers/blorgh.rb` inside the +application where the `blorgh` engine is installed and put this content in it: ```ruby Blorgh.author_class = "User" ``` -WARNING: It's very important here to use the `String` version of the class, rather than the class itself. If you were to use the class, Rails would attempt to load that class and then reference the related table, which 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. +WARNING: It's very important here to use the `String` version of the class, +rather than the class itself. If you were to use the class, Rails would attempt +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 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. +Go ahead and try to create a new post. 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 some sort of identifier by which it can be referenced. +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 +some sort of identifier by which it can be referenced. -#### General engine configuration +#### General Engine Configuration -Within an engine, there may come a time where you wish to use things such as initializers, internationalization or other configuration options. The great news is that these things are entirely possible because a Rails engine shares much the same functionality as a Rails application. In fact, a Rails application's functionality is actually a superset of what is provided by engines! +Within an engine, there may come a time where you wish to use things such as +initializers, internationalization or other configuration options. The great +news is that these things are entirely possible, because a Rails engine shares +much the same functionality as a Rails application. In fact, a Rails +application's functionality is actually a superset of what is provided by +engines! If you wish to use an initializer - code that should run before the engine is loaded - the place for it is the `config/initializers` folder. This directory's -functionality is explained in the -[Initializers section](configuring.html#initializers) of the Configuring guide, -and works precisely the same way as the `config/initializers` directory inside -an application. Same goes for if you want to use a standard initializer. +functionality is explained in the [Initializers +section](configuring.html#initializers) of the Configuring guide, and works +precisely the same way as the `config/initializers` directory inside an +application. The same thing goes if you want to use a standard initializer. -For locales, simply place the locale files in the `config/locales` directory, just like you would in an application. +For locales, simply place the locale files in the `config/locales` directory, +just like you would in an application. Testing an engine ----------------- -When an engine is generated there is a smaller dummy application created inside it at `test/dummy`. This application is used as a mounting point for the engine to make testing the engine extremely simple. You may extend this application by generating controllers, models or views from within the directory, and then use those to test your engine. +When an engine is generated, there is a smaller dummy application created inside +it at `test/dummy`. This application is used as a mounting point for the engine, +to make testing the engine extremely simple. You may extend this application by +generating controllers, models or views from within the directory, and then use +those to test your engine. -The `test` directory should be treated like a typical Rails testing environment, allowing for unit, functional and integration tests. +The `test` directory should be treated like a typical Rails testing environment, +allowing for unit, functional and integration tests. -### Functional tests +### Functional Tests -A matter worth taking into consideration when writing functional tests is that the tests are going to be running on an application - the `test/dummy` application - rather than your engine. This is due to the setup of the testing environment; an engine needs an application as a host for testing its main functionality, especially controllers. This means that if you were to make a typical `GET` to a controller in a controller's functional test like this: +A matter worth taking into consideration when writing functional tests is that +the tests are going to be running on an application - the `test/dummy` +application - rather than your engine. This is due to the setup of the testing +environment; an engine needs an application as a host for testing its main +functionality, especially controllers. This means that if you were to make a +typical `GET` to a controller in a controller's functional test like this: ```ruby get :index ``` -It may not function correctly. This is because the application doesn't know how to route these requests to the engine unless you explicitly tell it **how**. To do this, you must pass the `:use_route` option (as a parameter) on these requests also: +It may not function correctly. This is because the application doesn't know how +to route these requests to the engine unless you explicitly tell it **how**. To +do this, you must also pass the `:use_route` option as a parameter on these +requests: ```ruby get :index, use_route: :blorgh ``` -This tells the application that you still want to perform a `GET` request to the `index` action of this controller, just that you want to use the engine's route to get there, rather than the application. +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. Improving engine functionality ------------------------------ -This section explains how to add and/or override engine MVC functionality in the main Rails application. +This section explains how to add and/or override engine MVC functionality in the +main Rails application. ### Overriding Models and Controllers -Engine model and controller classes can be extended by open classing them in the main Rails application (since model and controller classes are just Ruby classes that inherit Rails specific functionality). Open classing an Engine class redefines it for use in the main application. This is usually implemented by using the decorator pattern. +Engine model and controller classes can be extended by open classing them in the +main Rails application (since model and controller classes are just Ruby classes +that inherit Rails specific functionality). Open classing an Engine class +redefines it for use in the main application. This is usually implemented by +using the decorator pattern. -For simple class modifications use `Class#class_eval`, and for complex class modifications, consider using `ActiveSupport::Concern`. +For simple class modifications, use `Class#class_eval`. For complex class +modifications, consider using `ActiveSupport::Concern`. -#### A note on Decorators and loading code +#### A note on Decorators and Loading Code Because these decorators are not referenced by your Rails application itself, -Rails' autoloading system will not kick in and load your decorators. This -means that you need to require them yourself. +Rails' autoloading system will not kick in and load your decorators. This means +that you need to require them yourself. Here is some sample code to do this: @@ -764,7 +1097,7 @@ that isn't referenced by your main application. #### Implementing Decorator Pattern Using Class#class_eval -**Adding** `Post#time_since_created`, +**Adding** `Post#time_since_created`: ```ruby # MyApp/app/decorators/models/blorgh/post_decorator.rb @@ -785,7 +1118,7 @@ end ``` -**Overriding** `Post#summary` +**Overriding** `Post#summary`: ```ruby # MyApp/app/decorators/models/blorgh/post_decorator.rb @@ -810,9 +1143,13 @@ end #### Implementing Decorator Pattern Using ActiveSupport::Concern -Using `Class#class_eval` is great for simple adjustments, but for more complex class modifications, you might want to consider using [`ActiveSupport::Concern`](http://edgeapi.rubyonrails.org/classes/ActiveSupport/Concern.html). ActiveSupport::Concern manages load order of interlinked dependent modules and classes at run time allowing you to significantly modularize your code. +Using `Class#class_eval` is great for simple adjustments, but for more complex +class modifications, you might want to consider using [`ActiveSupport::Concern`] +(http://edgeapi.rubyonrails.org/classes/ActiveSupport/Concern.html). +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** `Post#time_since_created` and **Overriding** `Post#summary`: ```ruby # MyApp/app/models/blorgh/post.rb @@ -845,7 +1182,7 @@ module Blorgh::Concerns::Models::Post extend ActiveSupport::Concern # 'included do' causes the included code to be evaluated in the - # context where it is included (post.rb), rather than be + # context where it is included (post.rb), rather than being # executed in the module's context (blorgh/concerns/models/post). included do attr_accessor :author_name @@ -871,15 +1208,23 @@ module Blorgh::Concerns::Models::Post end ``` -### Overriding views +### Overriding Views -When Rails looks for a view to render, it will first look in the `app/views` directory of the application. If it cannot find the view there, then it will check in the `app/views` directories of all engines which have this directory. +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 index action, it will look the path `app/views/blorgh/posts/index.html.erb`, first within the application. If it cannot find it, it will look inside the engine. +When the application is asked to render the view for `Blorgh::PostsController`'s +index action, it will first look for the path +`app/views/blorgh/posts/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 this view would normally output. +You can override this view in the application by simply creating a new file at +`app/views/blorgh/posts/index.html.erb`. Then you can completely change what +this view would normally output. -Try this now by creating a new file at `app/views/blorgh/posts/index.html.erb` and put this content in it: +Try this now by creating a new file at `app/views/blorgh/posts/index.html.erb` +and put this content in it: ```html+erb <h1>Posts</h1> @@ -894,9 +1239,13 @@ Try this now by creating a new file at `app/views/blorgh/posts/index.html.erb` a ### Routes -Routes inside an engine are, by default, isolated from the application. This is done by the `isolate_namespace` call inside the `Engine` class. This essentially means that the application and its engines can have identically named routes and they will not clash. +Routes inside an engine are isolated from the application by default. This is +done by the `isolate_namespace` call inside the `Engine` class. This essentially +means that the application and its engines can have identically named routes and +they will not clash. -Routes inside an engine are drawn on the `Engine` class within `config/routes.rb`, like this: +Routes inside an engine are drawn on the `Engine` class within +`config/routes.rb`, like this: ```ruby Blorgh::Engine.routes.draw do @@ -904,43 +1253,71 @@ Blorgh::Engine.routes.draw do 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 going to undesired locations if both the application and the engine both have such a helper defined. +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 +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` if it was rendered from the engine: +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` +if it was rendered from the engine: ```erb <%= link_to "Blog posts", posts_path %> ``` -To make this route always use the engine's `posts_path` routing helper method, we must call the method on the routing proxy method that shares the same name as the engine. +To make this route always use the engine's `posts_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 %> ``` -If you wish to reference the application inside the engine in a similar way, use the `main_app` helper: +If you wish to reference the application inside the engine in a similar way, use +the `main_app` helper: ```erb <%= link_to "Home", main_app.root_path %> ``` -If you were to use this inside an engine, it would **always** go to the application's root. If you were to leave off the `main_app` "routing proxy" method call, it could potentially go to the engine's or application's root, depending on where it was called from. +If you were to use this inside an engine, it would **always** go to the +application's root. If you were to leave off the `main_app` "routing proxy" +method call, it could potentially go to the engine's or application's root, +depending on where it was called from. -If a template is rendered from within an engine and it's attempting to use one of the application's routing helper methods, it may result in an undefined method call. If you encounter such an issue, ensure that you're not attempting to call the application's routing methods without the `main_app` prefix from within the engine. +If a template rendered from within an engine attempts to use one of the +application's routing helper methods, it may result in an undefined method call. +If you encounter such an issue, ensure that you're not attempting to call the +application's routing methods without the `main_app` prefix from within the +engine. ### Assets -Assets within an engine work in an identical way to a full application. Because the engine class inherits from `Rails::Engine`, the application will know to look up in the engine's `app/assets` and `lib/assets` directories for potential assets. +Assets within an engine work in an identical way to a full application. Because +the engine class inherits from `Rails::Engine`, the application will know to +look up assets in the engine's 'app/assets' and 'lib/assets' directories. -Much like all the other components of an engine, the assets should also be namespaced. This means if you have an asset called `style.css`, it should be placed at `app/assets/stylesheets/[engine name]/style.css`, rather than `app/assets/stylesheets/style.css`. If this asset wasn't namespaced, then there is a possibility that the host application could have an asset named identically, in which case the application's asset would take precedence and the engine's one would be all but ignored. +Like all of the other components of an engine, the assets should be namespaced. +This means that if you have an asset called `style.css`, it should be placed at +`app/assets/stylesheets/[engine name]/style.css`, rather than +`app/assets/stylesheets/style.css`. If this asset isn't namespaced, there is a +possibility that the host application could have an asset named identically, in +which case the application's asset would take precedence and the engine's one +would be ignored. -Imagine that you did have an asset located at `app/assets/stylesheets/blorgh/style.css` To include this asset inside an application, just use `stylesheet_link_tag` and reference the asset as if it were inside the engine: +Imagine that you did have an asset located at +`app/assets/stylesheets/blorgh/style.css` To include this asset inside an +application, just use `stylesheet_link_tag` and reference the asset as if it +were inside the engine: ```erb <%= stylesheet_link_tag "blorgh/style.css" %> ``` -You can also specify these assets as dependencies of other assets using the Asset Pipeline require statements in processed files: +You can also specify these assets as dependencies of other assets using Asset +Pipeline require statements in processed files: ``` /* @@ -948,16 +1325,21 @@ You can also specify these assets as dependencies of other assets using the Asse */ ``` -INFO. Remember that in order to use languages like Sass or CoffeeScript, you should add the relevant library to your engine's `.gemspec`. +INFO. Remember that in order to use languages like Sass or CoffeeScript, you +should add the relevant library to your engine's `.gemspec`. ### Separate Assets & Precompiling -There are some situations where your engine's assets are not required by the host application. For example, say that you've created -an admin functionality that only exists for your engine. In this case, the host application doesn't need to require `admin.css` -or `admin.js`. Only the gem's admin layout needs these assets. It doesn't make sense for the host app to include `"blorgh/admin.css"` in it's stylesheets. In this situation, you should explicitly define these assets for precompilation. -This tells sprockets to add your engine assets when `rake assets:precompile` is ran. +There are some situations where your engine's assets are not required by the +host application. For example, say that you've created an admin functionality +that only exists for your engine. In this case, the host application doesn't +need to require `admin.css` or `admin.js`. Only the gem's admin layout needs +these assets. It doesn't make sense for the host app to include +`"blorgh/admin.css"` in its stylesheets. In this situation, you should +explicitly define these assets for precompilation. This tells sprockets to add +your engine assets when `rake assets:precompile` is triggered. -You can define assets for precompilation in `engine.rb` +You can define assets for precompilation in `engine.rb`: ```ruby initializer "blorgh.assets.precompile" do |app| @@ -965,15 +1347,15 @@ initializer "blorgh.assets.precompile" do |app| end ``` -For more information, read the [Asset Pipeline guide](asset_pipeline.html) +For more information, read the [Asset Pipeline guide](asset_pipeline.html). -### Other gem dependencies +### Other Gem Dependencies -Gem dependencies inside an engine should be specified inside the -`.gemspec` file at the root of the engine. The reason is that the engine may -be installed as a gem. If dependencies were to be specified inside the `Gemfile`, -these would not be recognized by a traditional gem install and so they would not -be installed, causing the engine to malfunction. +Gem dependencies inside an engine should be specified inside the `.gemspec` file +at the root of the engine. The reason is that the engine may be installed as a +gem. If dependencies were to be specified inside the `Gemfile`, these would not +be recognized by a traditional gem install and so they would not be installed, +causing the engine to malfunction. To specify a dependency that should be installed with the engine during a traditional `gem install`, specify it inside the `Gem::Specification` block @@ -991,11 +1373,12 @@ s.add_development_dependency "moo" ``` Both kinds of dependencies will be installed when `bundle install` is run inside -the application. The development dependencies for the gem will only be used when -the tests for the engine are running. +of the application. The development dependencies for the gem will only be used +when the tests for the engine are running. Note that if you want to immediately require dependencies when the engine is -required, you should require them before the engine's initialization. For example: +required, you should require them before the engine's initialization. For +example: ```ruby require 'other_engine/engine' diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md index 26d9a86be0..455dc7bebe 100644 --- a/guides/source/form_helpers.md +++ b/guides/source/form_helpers.md @@ -342,7 +342,7 @@ If you have created namespaced routes, `form_for` has a nifty shorthand for that form_for [:admin, @article] ``` -will create a form that submits to the articles controller inside the admin namespace (submitting to `admin_article_path(@article)` in the case of an update). If you have several levels of namespacing then the syntax is similar: +will create a form that submits to the `ArticlesController` inside the admin namespace (submitting to `admin_article_path(@article)` in the case of an update). If you have several levels of namespacing then the syntax is similar: ```ruby form_for [:admin, :management, @article] @@ -751,7 +751,7 @@ You might want to render a form with a set of edit fields for each of a person's <%= form_for @person do |person_form| %> <%= person_form.text_field :name %> <% @person.addresses.each do |address| %> - <%= person_form.fields_for address, index: address do |address_form|%> + <%= person_form.fields_for address, index: address.id do |address_form|%> <%= address_form.text_field :city %> <% end %> <% end %> @@ -774,9 +774,16 @@ This will result in a `params` hash that looks like {'person' => {'name' => 'Bob', 'address' => {'23' => {'city' => 'Paris'}, '45' => {'city' => 'London'}}}} ``` -Rails knows that all these inputs should be part of the person hash because you called `fields_for` on the first form builder. By specifying an `:index` option you're telling Rails that instead of naming the inputs `person[address][city]` it should insert that index surrounded by [] between the address and the city. If you pass an Active Record object as we did then Rails will call `to_param` on it, which by default returns the database id. This is often useful as it is then easy to locate which Address record should be modified. You can pass numbers with some other significance, strings or even `nil` (which will result in an array parameter being created). +Rails knows that all these inputs should be part of the person hash because you +called `fields_for` on the first form builder. By specifying an `:index` option +you're telling Rails that instead of naming the inputs `person[address][city]` +it should insert that index surrounded by [] between the address and the city. +This is often useful as it is then easy to locate which Address record +should be modified. You can pass numbers with some other significance, +strings or even `nil` (which will result in an array parameter being created). -To create more intricate nestings, you can specify the first part of the input name (`person[address]` in the previous example) explicitly, for example +To create more intricate nestings, you can specify the first part of the input +name (`person[address]` in the previous example) explicitly: ```erb <%= fields_for 'person[address][primary]', address, index: address do |address_form| %> @@ -859,7 +866,7 @@ end This creates an `addresses_attributes=` method on `Person` that allows you to create, update and (optionally) destroy addresses. -### Building the Form +### Nested Forms The following form allows a user to create a `Person` and its associated addresses. @@ -882,16 +889,18 @@ The following form allows a user to create a `Person` and its associated address ``` -When an association accepts nested attributes `fields_for` renders its block once for every element of the association. In particular, if a person has no addresses it renders nothing. A common pattern is for the controller to build one or more empty children so that at least one set of fields is shown to the user. The example below would result in 3 sets of address fields being rendered on the new person form. +When an association accepts nested attributes `fields_for` renders its block once for every element of the association. In particular, if a person has no addresses it renders nothing. A common pattern is for the controller to build one or more empty children so that at least one set of fields is shown to the user. The example below would result in 2 sets of address fields being rendered on the new person form. ```ruby def new @person = Person.new - 3.times { @person.addresses.build} + 2.times { @person.addresses.build} end ``` -`fields_for` yields a form builder that names parameters in the format expected the accessor generated by `accepts_nested_attributes_for`. For example when creating a user with 2 addresses, the submitted parameters would look like +The `fields_for` yields a form builder. The parameters' name will be what +`accepts_nested_attributes_for` expects. For example when creating a user with +2 addresses, the submitted parameters would look like: ```ruby { @@ -913,7 +922,7 @@ end The keys of the `:addresses_attributes` hash are unimportant, they need merely be different for each address. -If the associated object is already saved, `fields_for` autogenerates a hidden input with the `id` of the saved record. You can disable this by passing `include_id: false` to `fields_for`. You may wish to do this if the autogenerated input is placed in a location where an input tag is not valid HTML or when using an ORM where children do not have an id. +If the associated object is already saved, `fields_for` autogenerates a hidden input with the `id` of the saved record. You can disable this by passing `include_id: false` to `fields_for`. You may wish to do this if the autogenerated input is placed in a location where an input tag is not valid HTML or when using an ORM where children do not have an `id`. ### The Controller @@ -944,7 +953,9 @@ class Person < ActiveRecord::Base end ``` -If the hash of attributes for an object contains the key `_destroy` with a value of '1' or 'true' then the object will be destroyed. This form allows users to remove addresses: +If the hash of attributes for an object contains the key `_destroy` with a value +of `1` or `true` then the object will be destroyed. This form allows users to +remove addresses: ```erb <%= form_for @person do |f| %> @@ -952,7 +963,7 @@ If the hash of attributes for an object contains the key `_destroy` with a value <ul> <%= f.fields_for :addresses do |addresses_form| %> <li> - <%= check_box :_destroy%> + <%= addresses_form.check_box :_destroy%> <%= addresses_form.label :kind %> <%= addresses_form.text_field :kind %> ... diff --git a/guides/source/generators.md b/guides/source/generators.md index e9c8ef0225..4a5377c206 100644 --- a/guides/source/generators.md +++ b/guides/source/generators.md @@ -560,7 +560,7 @@ This method also takes a block: ```ruby vendor "seeds.rb" do - "puts 'in ur app, seeding ur database'" + "puts 'in your app, seeding your database'" end ``` diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index b57441b1c3..dbcedba800 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -86,7 +86,7 @@ current version of Ruby installed: ```bash $ ruby -v -ruby 2.0.0p247 +ruby 2.0.0p353 ``` To install Rails, use the `gem install` command provided by RubyGems: @@ -106,7 +106,7 @@ run the following: $ rails --version ``` -If it says something like "Rails 4.0.0", you are ready to continue. +If it says something like "Rails 4.1.0", you are ready to continue. ### Creating the Blog Application @@ -123,42 +123,40 @@ rights to create files, and type: $ rails new blog ``` -This will create a Rails application called Blog in a directory called blog and +This will create a Rails application called Blog in a `blog` directory and install the gem dependencies that are already mentioned in `Gemfile` using `bundle install`. TIP: You can see all of the command line options that the Rails application builder accepts by running `rails new -h`. -After you create the blog application, switch to its folder to continue work -directly in that application: +After you create the blog application, switch to its folder: ```bash $ cd blog ``` -The `rails new blog` command we ran above created a folder in your working -directory called `blog`. The `blog` directory has a number of auto-generated -files and folders that make up the structure of a Rails application. Most of the -work in this tutorial will happen in the `app/` folder, but here's a basic -rundown on the function of each of the files and folders that Rails created by default: +The `blog` directory has a number of auto-generated files and folders that make +up the structure of a Rails application. Most of the work in this tutorial will +happen in the `app` folder, but here's a basic rundown on the function of each +of the files and folders that Rails created by default: | File/Folder | Purpose | | ----------- | ------- | -|app/|Contains the controllers, models, views, helpers, mailers and assets for your application. You'll focus on this folder for the remainder of this guide.| -|bin/|Contains the rails script that starts your app and can contain other scripts you use to deploy or run your application.| -|config/|Configure your application's runtime rules, routes, database, and more. This is covered in more detail in [Configuring Rails Applications](configuring.html)| +|app|Contains the controllers, models, views, helpers, mailers and assets for your application. You'll focus on this folder for the remainder of this guide.| +|bin|Contains the rails script that starts your app and can contain other scripts you use to deploy or run your application.| +|config/|Configure your application's routes, database, and more. This is covered in more detail in [Configuring Rails Applications](configuring.html).| |config.ru|Rack configuration for Rack based servers used to start the application.| -|db/|Contains your current database schema, as well as the database migrations.| -|Gemfile<br>Gemfile.lock|These files allow you to specify what gem dependencies are needed for your Rails application. These files are used by the Bundler gem. For more information about Bundler, see [the Bundler website](http://gembundler.com) | -|lib/|Extended modules for your application.| -|log/|Application log files.| -|public/|The only folder seen to the world as-is. Contains the static files and compiled assets.| +|db|Contains your current database schema, as well as the database migrations.| +|Gemfile<br>Gemfile.lock|These files allow you to specify what gem dependencies are needed for your Rails application. These files are used by the Bundler gem. For more information about Bundler, see [the Bundler website](http://gembundler.com).| +|lib|Extended modules for your application.| +|log|Application log files.| +|public|The only folder seen by the world as-is. Contains static files and compiled assets.| |Rakefile|This file locates and loads tasks that can be run from the command line. The task definitions are defined throughout the components of Rails. Rather than changing Rakefile, you should add your own tasks by adding files to the lib/tasks directory of your application.| |README.rdoc|This is a brief instruction manual for your application. You should edit this file to tell others what your application does, how to set it up, and so on.| -|test/|Unit tests, fixtures, and other test apparatus. These are covered in [Testing Rails Applications](testing.html)| -|tmp/|Temporary files (like cache, pid and session files)| -|vendor/|A place for all third-party code. In a typical Rails application, this includes Ruby Gems and the Rails source code (if you optionally install it into your project).| +|test|Unit tests, fixtures, and other test apparatus. These are covered in [Testing Rails Applications](testing.html).| +|tmp|Temporary files (like cache, pid, and session files).| +|vendor|A place for all third-party code. In a typical Rails application this includes vendored gems.| Hello, Rails! ------------- @@ -170,7 +168,7 @@ get your Rails application server running. You actually have a functional Rails application already. To see it, you need to start a web server on your development machine. You can do this by running the -following in the root directory of your rails application: +following in the `blog` directory: ```bash $ rails server @@ -179,16 +177,17 @@ $ rails server TIP: Compiling CoffeeScript to JavaScript requires a JavaScript runtime and the absence of a runtime will give you an `execjs` error. Usually Mac OS X and Windows come with a JavaScript runtime installed. Rails adds the `therubyracer` -gem to Gemfile in a commented line for new apps and you can uncomment if you -need it. `therubyrhino` is the recommended runtime for JRuby users and is added -by default to Gemfile in apps generated under JRuby. You can investigate about -all the supported runtimes at [ExecJS](https://github.com/sstephenson/execjs#readme). +gem to the generated `Gemfile` in a commented line for new apps and you can +uncomment if you need it. `therubyrhino` is the recommended runtime for JRuby +users and is added by default to the `Gemfile` in apps generated under JRuby. +You can investigate about all the supported runtimes at +[ExecJS](https://github.com/sstephenson/execjs#readme). -This will fire up WEBrick, a webserver built into Ruby by default. To see your -application in action, open a browser window and navigate to <http://localhost:3000>. -You should see the Rails default information page: +This will fire up WEBrick, a web server distributed with Ruby by default. To see +your application in action, open a browser window and navigate to +<http://localhost:3000>. You should see the Rails default information page: - + TIP: To stop the web server, hit Ctrl+C in the terminal window where it's running. To verify the server has stopped you should see your command prompt @@ -197,7 +196,7 @@ dollar sign `$`. In development mode, Rails does not generally require you to restart the server; changes you make in files will be automatically picked up by the server. -The "Welcome Aboard" page is the _smoke test_ for a new Rails application: it +The "Welcome aboard" page is the _smoke test_ for a new Rails application: it makes sure that you have your software configured correctly enough to serve a page. You can also click on the _About your application's environment_ link to see a summary of your application's environment. @@ -216,8 +215,9 @@ it to a view. A view's purpose is to display this information in a human readable format. An important distinction to make is that it is the _controller_, not the view, where information is collected. The view should just display that information. -By default, view templates are written in a language called ERB (Embedded Ruby) -which is converted by the request cycle in Rails before being sent to the user. +By default, view templates are written in a language called eRuby (Embedded +Ruby) which is processed by the request cycle in Rails before being sent to the +user. To create a new controller, you will need to run the "controller" generator and tell it you want a controller called "welcome" with an action called "index", @@ -231,7 +231,7 @@ Rails will create several files and a route for you. ```bash create app/controllers/welcome_controller.rb - route get "welcome/index" + route get 'welcome/index' invoke erb create app/views/welcome create app/views/welcome/index.html.erb @@ -262,23 +262,25 @@ of code: ### Setting the Application Home Page Now that we have made the controller and view, we need to tell Rails when we -want `Hello, Rails!` to show up. In our case, we want it to show up when we +want "Hello, Rails!" to show up. In our case, we want it to show up when we navigate to the root URL of our site, <http://localhost:3000>. At the moment, -"Welcome Aboard" is occupying that spot. +"Welcome aboard" is occupying that spot. Next, you have to tell Rails where your actual home page is located. Open the file `config/routes.rb` in your editor. ```ruby -Blog::Application.routes.draw do - get "welcome/index" +Rails.application.routes.draw do + get 'welcome/index' # The priority is based upon order of creation: # first created -> highest priority. - # ... + # # You can have the root of your site routed with "root" - # root "welcome#index" + # root 'welcome#index' + # + # ... ``` This is your application's _routing file_ which holds entries in a special DSL @@ -289,17 +291,18 @@ to a specific controller and action. Find the line beginning with `root` and uncomment it. It should look something like the following: ```ruby -root "welcome#index" +root 'welcome#index' ``` -The `root "welcome#index"` tells Rails to map requests to the root of the -application to the welcome controller's index action and `get "welcome/index"` +`root 'welcome#index'` tells Rails to map requests to the root of the +application to the welcome controller's index action and `get 'welcome/index'` tells Rails to map requests to <http://localhost:3000/welcome/index> to the welcome controller's index action. This was created earlier when you ran the controller generator (`rails generate controller welcome index`). -If you navigate to <http://localhost:3000> in your browser, you'll see the -`Hello, Rails!` message you put into `app/views/welcome/index.html.erb`, +Launch the web server again if you stopped it to generate the controller (`rails +server`) and navigate to <http://localhost:3000> in your browser. You'll see the +"Hello, Rails!" message you put into `app/views/welcome/index.html.erb`, indicating that this new route is indeed going to `WelcomeController`'s `index` action and is rendering the view correctly. @@ -325,7 +328,7 @@ Blog::Application.routes.draw do resources :posts - root "welcome#index" + root 'welcome#index' end ``` @@ -388,9 +391,9 @@ It's inside this class that you'll define methods that will become the actions for this controller. These actions will perform CRUD operations on the posts within our system. -NOTE: There are `public`, `private` and `protected` methods in `Ruby` -(for more details you can check on [Programming Ruby](http://www.ruby-doc.org/docs/ProgrammingRuby/)). -But only `public` methods can be actions for controllers. +NOTE: There are `public`, `private` and `protected` methods in Ruby, +but only `public` methods can be actions for controllers. +For more details check out [Programming Ruby](http://www.ruby-doc.org/docs/ProgrammingRuby/). If you refresh <http://localhost:3000/posts/new> now, you'll get a new error: @@ -398,7 +401,7 @@ If you refresh <http://localhost:3000/posts/new> now, you'll get a new error: This error indicates that Rails cannot find the `new` action inside the `PostsController` that you just generated. This is because when controllers are generated in Rails -they are empty by default, unless you tell it you wanted actions during the +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 @@ -757,7 +760,7 @@ the `show` action. That's not very useful though, so let's add the As we have seen in the output of `rake routes`, the route for `show` action is as follows: -```ruby +``` post GET /posts/:id(.:format) posts#show ``` @@ -804,7 +807,7 @@ Visit <http://localhost:3000/posts/new> and give it a try! We still need a way to list all our posts, so let's do that. The route for this as per output of `rake routes` is: -```ruby +``` posts GET /posts(.:format) posts#index ``` @@ -816,7 +819,7 @@ def index end ``` -And then finally a view for this action, located at `app/views/posts/index.html.erb`: +And then finally, add view for this action, located at `app/views/posts/index.html.erb`: ```html+erb <h1>Listing posts</h1> @@ -1406,7 +1409,6 @@ class Post < ActiveRecord::Base has_many :comments validates :title, presence: true, length: { minimum: 5 } - [...] end ``` @@ -1703,8 +1705,8 @@ Deleting Comments ----------------- Another important feature of a blog is being able to delete spam comments. To do -this, we need to implement a link of some sort in the view and a `DELETE` action -in the `CommentsController`. +this, we need to implement a link of some sort in the view and a `destroy` +action in the `CommentsController`. So first, let's add the delete link in the `app/views/comments/_comment.html.erb` partial: @@ -1729,7 +1731,7 @@ So first, let's add the delete link in the Clicking this new "Destroy Comment" link will fire off a `DELETE /posts/:post_id/comments/:id` to our `CommentsController`, which can then use -this to find the comment we want to delete, so let's add a destroy action to our +this to find the comment we want to delete, so let's add a `destroy` action to our controller (`app/controllers/comments_controller.rb`): ```ruby @@ -1771,7 +1773,6 @@ class Post < ActiveRecord::Base has_many :comments, dependent: :destroy validates :title, presence: true, length: { minimum: 5 } - [...] end ``` diff --git a/guides/source/i18n.md b/guides/source/i18n.md index 6f79b3ddd7..8dfb17a681 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -101,7 +101,7 @@ This means, that in the `:en` locale, the key _hello_ will map to the _Hello wor The I18n library will use **English** as a **default locale**, i.e. if you don't set a different locale, `:en` will be used for looking up translations. -NOTE: The i18n library takes a **pragmatic approach** to locale keys (after [some discussion](http://groups.google.com/group/rails-i18n/browse_thread/thread/14dede2c7dbe9470/80eec34395f64f3c?hl=en)), including only the _locale_ ("language") part, like `:en`, `:pl`, not the _region_ part, like `:en-US` or `:en-GB`, which are traditionally used for separating "languages" and "regional setting" or "dialects". Many international applications use only the "language" element of a locale such as `:cs`, `:th` or `:es` (for Czech, Thai and Spanish). However, there are also regional differences within different language groups that may be important. For instance, in the `:en-US` locale you would have $ as a currency symbol, while in `:en-GB`, you would have £. Nothing stops you from separating regional and other settings in this way: you just have to provide full "English - United Kingdom" locale in a `:en-GB` dictionary. Various [Rails I18n plugins](http://rails-i18n.org/wiki) such as [Globalize3](https://github.com/svenfuchs/globalize3) may help you implement it. +NOTE: The i18n library takes a **pragmatic approach** to locale keys (after [some discussion](http://groups.google.com/group/rails-i18n/browse_thread/thread/14dede2c7dbe9470/80eec34395f64f3c?hl=en)), including only the _locale_ ("language") part, like `:en`, `:pl`, not the _region_ part, like `:en-US` or `:en-GB`, which are traditionally used for separating "languages" and "regional setting" or "dialects". Many international applications use only the "language" element of a locale such as `:cs`, `:th` or `:es` (for Czech, Thai and Spanish). However, there are also regional differences within different language groups that may be important. For instance, in the `:en-US` locale you would have $ as a currency symbol, while in `:en-GB`, you would have £. Nothing stops you from separating regional and other settings in this way: you just have to provide full "English - United Kingdom" locale in a `:en-GB` dictionary. Various [Rails I18n plugins](http://rails-i18n.org/wiki) such as [Globalize3](https://github.com/globalize/globalize) may help you implement it. The **translations load path** (`I18n.load_path`) is just a Ruby Array of paths to your translation files that will be loaded automatically and available in your application. You can pick whatever directory and translation file naming scheme makes sense for you. @@ -824,7 +824,7 @@ This way you can provide special translations for various error messages at diff The translated model name, translated attribute name, and value are always available for interpolation. -So, for example, instead of the default error message `"can not be blank"` you could use the attribute name like this : `"Please fill in your %{attribute}"`. +So, for example, instead of the default error message `"cannot be blank"` you could use the attribute name like this : `"Please fill in your %{attribute}"`. * `count`, where available, can be used for pluralization if present: @@ -833,6 +833,7 @@ So, for example, instead of the default error message `"can not be blank"` you c | confirmation | - | :confirmation | - | | acceptance | - | :accepted | - | | presence | - | :blank | - | +| absence | - | :present | - | | length | :within, :in | :too_short | count | | length | :within, :in | :too_long | count | | length | :is | :wrong_length | count | @@ -925,7 +926,7 @@ Customize your I18n Setup ### Using Different Backends -For several reasons the Simple backend shipped with Active Support only does the "simplest thing that could possibly work" _for Ruby on Rails_[^3] ... which means that it is only guaranteed to work for English and, as a side effect, languages that are very similar to English. Also, the simple backend is only capable of reading translations but can not dynamically store them to any format. +For several reasons the Simple backend shipped with Active Support only does the "simplest thing that could possibly work" _for Ruby on Rails_[^3] ... which means that it is only guaranteed to work for English and, as a side effect, languages that are very similar to English. Also, the simple backend is only capable of reading translations but cannot dynamically store them to any format. That does not mean you're stuck with these limitations, though. The Ruby I18n gem makes it very easy to exchange the Simple backend implementation with something else that fits better for your needs. E.g. you could exchange it with Globalize's Static backend: diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md index f4dab57aa5..c72b584ed6 100644 --- a/guides/source/layouts_and_rendering.md +++ b/guides/source/layouts_and_rendering.md @@ -404,7 +404,7 @@ class ProductsController < ApplicationController end ``` -With this declaration, all of the views rendered by the products controller will use `app/views/layouts/inventory.html.erb` as their layout. +With this declaration, all of the views rendered by the `ProductsController` will use `app/views/layouts/inventory.html.erb` as their layout. To assign a specific layout for the entire application, use a `layout` declaration in your `ApplicationController` class: @@ -1008,7 +1008,6 @@ You can also pass local variables into partials, making them even more powerful ```html+erb <h1>New zone</h1> - <%= error_messages_for :zone %> <%= render partial: "form", locals: {zone: @zone} %> ``` @@ -1016,7 +1015,6 @@ You can also pass local variables into partials, making them even more powerful ```html+erb <h1>Editing zone</h1> - <%= error_messages_for :zone %> <%= render partial: "form", locals: {zone: @zone} %> ``` diff --git a/guides/source/migrations.md b/guides/source/migrations.md index 71a177bca7..5d5c2724b1 100644 --- a/guides/source/migrations.md +++ b/guides/source/migrations.md @@ -297,10 +297,10 @@ You can append as many column name/type pairs as you want. You can also specify some options just after the field type between curly braces. You can use the following modifiers: -* `limit` Sets the maximum size of the `string/text/binary/integer` fields -* `precision` Defines the precision for the `decimal` fields -* `scale` Defines the scale for the `decimal` fields -* `polymorphic` Adds a `type` column for `belongs_to` associations +* `limit` Sets the maximum size of the `string/text/binary/integer` fields. +* `precision` Defines the precision for the `decimal` fields, representing the total number of digits in the number. +* `scale` Defines the scale for the `decimal` fields, representing the number of digits after the decimal point. +* `polymorphic` Adds a `type` column for `belongs_to` associations. * `null` Allows or disallows `NULL` values in the column. For instance, running: diff --git a/guides/source/plugins.md b/guides/source/plugins.md index 8587bd48b2..720ca5d117 100644 --- a/guides/source/plugins.md +++ b/guides/source/plugins.md @@ -224,7 +224,6 @@ and migrating the database. First, run: ```bash $ cd test/dummy $ rake db:migrate -$ rake db:test:prepare ``` While you are here, change the Hickwall and Wickwall models so that they know that they are supposed to act diff --git a/guides/source/rails_application_templates.md b/guides/source/rails_application_templates.md index 711d910184..e4222e1283 100644 --- a/guides/source/rails_application_templates.md +++ b/guides/source/rails_application_templates.md @@ -78,7 +78,7 @@ end Adds the given source to the generated application's `Gemfile`. -For example, if you need to source a gem from "http://code.whytheluckystiff.net": +For example, if you need to source a gem from `"http://code.whytheluckystiff.net"`: ```ruby add_source "http://code.whytheluckystiff.net" diff --git a/guides/source/routing.md b/guides/source/routing.md index 019861c3d6..70d4722068 100644 --- a/guides/source/routing.md +++ b/guides/source/routing.md @@ -138,7 +138,7 @@ Sometimes, you have a resource that clients always look up without referencing a get 'profile', to: 'users#show' ``` -Passing a `String` to `match` will expect a `controller#action` format, while passing a `Symbol` will map directly to an action: +Passing a `String` to `get` will expect a `controller#action` format, while passing a `Symbol` will map directly to an action: ```ruby get 'profile', to: :show @@ -239,6 +239,8 @@ In each of these cases, the named routes remain the same as if you did not use ` | PATCH/PUT | /admin/posts/:id | posts#update | post_path(:id) | | DELETE | /admin/posts/:id | posts#destroy | post_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'`._ + ### Nested Resources It's common to have resources that are logically children of other resources. For example, suppose your application includes these models: diff --git a/guides/source/security.md b/guides/source/security.md index c698959a2c..70fb066b64 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -81,7 +81,7 @@ Here are some general guidelines on sessions. * _Do not store large objects in a session_. Instead you should store them in the database and save their id in the session. This will eliminate synchronization headaches and it won't fill up your session storage space (depending on what session storage you chose, see below). This will also be a good idea, if you modify the structure of an object and old versions of it are still in some user's cookies. With server-side session storages you can clear out the sessions, but with client-side storages, this is hard to mitigate. -* _Critical data should not be stored in session_. If the user clears his cookies or closes the browser, they will be lost. And with a client-side session storage, the user can read the data. +* _Critical data should not be stored in session_. If the user clears their cookies or closes the browser, they will be lost. And with a client-side session storage, the user can read the data. ### Session Storage @@ -150,7 +150,7 @@ Another countermeasure is to _save user-specific properties in the session_, ver ### Session Expiry -NOTE: _Sessions that never expire extend the time-frame for attacks such as cross-site reference forgery (CSRF), session hijacking and session fixation._ +NOTE: _Sessions that never expire extend the time-frame for attacks such as cross-site request forgery (CSRF), session hijacking and session fixation._ One possibility is to set the expiry time-stamp of the cookie with the session id. However the client can edit cookies that are stored in the web browser so expiring sessions on the server is safer. Here is an example of how to _expire sessions in a database table_. Call `Session.sweep("20 minutes")` to expire sessions that were used longer than 20 minutes ago. @@ -230,13 +230,15 @@ Or the attacker places the code into the onmouseover event handler of an image: <img src="http://www.harmless.com/img" width="400" height="400" onmouseover="..." /> ``` -There are many other possibilities, including Ajax to attack the victim in the background.
The _solution to this is including a security token in non-GET requests_ which check on the server-side. In Rails 2 or higher, this is a one-liner in the application controller: +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: ```ruby -protect_from_forgery secret: "123456789012345678901234567890..." +protect_from_forgery ``` -This will automatically include a security token, calculated from the current session and the server-side secret, in all forms and Ajax requests generated by Rails. You won't need the secret, if you use CookieStorage as session storage. If the security token doesn't match what was expected, the session will be reset. **Note:** In Rails versions prior to 3.0.4, this raised an `ActionController::InvalidAuthenticityToken` error. +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. 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: @@ -352,7 +354,7 @@ Having one single place in the admin interface or Intranet, where the input has Refer to the Injection section for countermeasures against XSS. It is _recommended to use the SafeErb plugin_ also in an Intranet or administration interface. -**CSRF** Cross-Site Reference Forgery (CSRF) is a gigantic attack method, it allows the attacker to do everything the administrator or Intranet user may do. As you have already seen above how CSRF works, here are a few examples of what attackers can do in the Intranet or admin interface. +**CSRF** Cross-Site Request Forgery (CSRF), also known as Cross-Site Reference Forgery (XSRF), is a gigantic attack method, it allows the attacker to do everything the administrator or Intranet user may do. As you have already seen above how CSRF works, here are a few examples of what attackers can do in the Intranet or admin interface. A real-world example is a [router reconfiguration by CSRF](http://www.h-online.com/security/Symantec-reports-first-active-attack-on-a-DSL-router--/news/102352). The attackers sent a malicious e-mail, with CSRF in it, to Mexican users. The e-mail claimed there was an e-card waiting for them, but it also contained an image tag that resulted in a HTTP-GET request to reconfigure the user's router (which is a popular model in Mexico). The request changed the DNS-settings so that requests to a Mexico-based banking site would be mapped to the attacker's site. Everyone who accessed the banking site through that router saw the attacker's fake web site and had their credentials stolen. @@ -913,6 +915,49 @@ Content-Type: text/html Under certain circumstances this would present the malicious HTML to the victim. However, this only seems to work with Keep-Alive connections (and many browsers are using one-time connections). But you can't rely on this. _In any case this is a serious bug, and you should update your Rails to version 2.0.5 or 2.1.2 to eliminate Header Injection (and thus response splitting) risks._ +Unsafe Query Generation +----------------------- + +Due to the way Active Record interprets parameters in combination with the way +that Rack parses query parameters it was possible to issue unexpected database +queries with `IS NULL` where clauses. As a response to that security issue +([CVE-2012-2660](https://groups.google.com/forum/#!searchin/rubyonrails-security/deep_munge/rubyonrails-security/8SA-M3as7A8/Mr9fi9X4kNgJ), +[CVE-2012-2694](https://groups.google.com/forum/#!searchin/rubyonrails-security/deep_munge/rubyonrails-security/jILZ34tAHF4/7x0hLH-o0-IJ) +and [CVE-2013-0155](https://groups.google.com/forum/#!searchin/rubyonrails-security/CVE-2012-2660/rubyonrails-security/c7jT-EeN9eI/L0u4e87zYGMJ)) +`deep_munge` method was introduced as a solution to keep Rails secure by default. + +Example of vulnerable code that could be used by attacker, if `deep_munge` +wasn't performed is: + +```ruby +unless params[:token].nil? + user = User.find_by_token(params[:token]) + user.reset_password! +end +``` + +When `params[:token]` is one of: `[]`, `[nil]`, `[nil, nil, ...]` or +`['foo', nil]` it will bypass the test for `nil`, but `IS NULL` or +`IN ('foo', NULL)` where clauses still will be added to the SQL query. + +To keep rails secure by default, `deep_munge` replaces some of the values with +`nil`. Below table shows what the parameters look like based on `JSON` sent in +request: + +| JSON | Parameters | +|-----------------------------------|--------------------------| +| `{ "person": null }` | `{ :person => nil }` | +| `{ "person": [] }` | `{ :person => nil }` | +| `{ "person": [null] }` | `{ :person => nil }` | +| `{ "person": [null, null, ...] }` | `{ :person => nil }` | +| `{ "person": ["foo", null] }` | `{ :person => ["foo"] }` | + +It is possible to return to old behaviour and disable `deep_munge` configuring +your application if you are aware of the risk and know how to handle it: + +```ruby +config.action_dispatch.perform_deep_munge = false +``` Default Headers --------------- diff --git a/guides/source/testing.md b/guides/source/testing.md index d00fcd1f03..33cd3e868b 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -211,31 +211,9 @@ This line of code is called an _assertion_. An assertion is a line of code that Every test contains one or more assertions. Only when all the assertions are successful will the test pass. -### Preparing your Application for Testing +### Maintaining the test database schema -Before you can run your tests, you need to ensure that the test database structure is current. For this you can use the following rake commands: - -```bash -$ rake db:migrate -... -$ rake db:test:load -``` - -The `rake db:migrate` above runs any pending migrations on the _development_ environment and updates `db/schema.rb`. The `rake db:test:load` recreates the test database from the current `db/schema.rb`. On subsequent attempts, it is a good idea to first run `db:test:prepare`, as it first checks for pending migrations and warns you appropriately. - -NOTE: `db:test:prepare` will fail with an error if `db/schema.rb` doesn't exist. - -#### Rake Tasks for Preparing your Application for Testing - -| Tasks | Description | -| ------------------------------ | ------------------------------------------------------------------------- | -| `rake db:test:clone` | Recreate the test database from the current environment's database schema | -| `rake db:test:clone_structure` | Recreate the test database from the development structure | -| `rake db:test:load` | Recreate the test database from the current `schema.rb` | -| `rake db:test:prepare` | Check for pending migrations and load the test schema | -| `rake db:test:purge` | Empty the test database. | - -TIP: You can see all these rake tasks and their descriptions by running `rake --tasks --describe` +In order to run your tests, your test database will need to have the current structure. The test helper checks whether your test database has any pending migrations. If so, it will try to load your `db/schema.rb` or `db/structure.sql` into the test database. If migrations are still pending, an error will be raised. ### Running Tests @@ -784,7 +762,7 @@ class UserFlowsTest < ActionDispatch::IntegrationTest u = users(user) sess.https! sess.post "/login", username: u.username, password: u.password - assert_equal '/welcome', path + assert_equal '/welcome', sess.path sess.https!(false) end end diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 596682bb1f..2055452935 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -27,21 +27,80 @@ Upgrading from Rails 4.0 to Rails 4.1 NOTE: This section is a work in progress. +### CSRF protection from remote `<script>` tags + +Or, "whaaat my tests are failing!!!?" + +Cross-site request forgery (CSRF) protection now covers GET requests with +JavaScript responses, too. That prevents a third-party site from referencing +your JavaScript URL and attempting to run it to extract sensitive data. + +This means that your functional and integration tests that use + +```ruby +get :index, format: :js +``` + +will now trigger CSRF protection. Switch to + +```ruby +xhr :get, :index, format: :js +``` + +to explicitly test an XmlHttpRequest. + +If you really mean to load JavaScript from remote `<script>` tags, skip CSRF +protection on that action. + ### Spring If you want to use Spring as your application preloader you need to: -1. add `gem 'spring', group: :development` to your `Gemfile`. -2. install spring using `bundle install`. -3. springify your binstubs with `bundle exec spring binstub --all`. +1. Add `gem 'spring', group: :development` to your `Gemfile`. +2. Install spring using `bundle install`. +3. Springify your binstubs with `bundle exec spring binstub --all`. NOTE: User defined rake tasks will run in the `development` environment by default. If you want them to run in other environments consult the -[Spring README](https://github.com/jonleighton/spring#rake). +[Spring README](https://github.com/rails/spring#rake). + +### `config/secrets.yml` + +If you want to use the new `secrets.yml` convention to store your application's +secrets, you need to: + +1. Create a `secrets.yml` file in your `config` folder with the following content: + + ```yaml + development: + secret_key_base: + + test: + secret_key_base: + + production: + secret_key_base: + ``` + +2. Copy the existing `secret_key_base` from the `secret_token.rb` initializer to + `secrets.yml` under the `production` section. + +3. Remove the `secret_token.rb` initializer. + +4. Use `rake secret` to generate new keys for the `development` and `test` sections. + +5. Restart your server. + +### Changes to test helper + +If your test helper contains a call to +`ActiveRecord::Migration.check_pending!` this can be removed. The check +is now done automatically when you `require 'test_help'`, although +leaving this line in your helper is not harmful in any way. ### Changes in JSON handling -The are a few major changes related to JSON handling in Rails 4.1. +There are a few major changes related to JSON handling in Rails 4.1. #### MultiJSON removal @@ -71,7 +130,7 @@ Rails-specific features. For example: ```ruby class FooBar def as_json(options = nil) - { foo: "bar" } + { foo: 'bar' } end end @@ -89,10 +148,55 @@ part of the rewrite, the following features have been removed from the encoder: 2. Support for the `encode_json` hook 3. Option to encode `BigDecimal` objects as numbers instead of strings -If you application depends on one of these features, you can get them back by +If your application depends on one of these features, you can get them back by adding the [`activesupport-json_encoder`](https://github.com/rails/activesupport-json_encoder) gem to your Gemfile. +### Usage of `return` within inline callback blocks + +Previously, Rails allowed inline callback blocks to use `return` this way: + +```ruby +class ReadOnlyModel < ActiveRecord::Base + before_save { return false } # BAD +end +``` + +This behaviour was never intentionally supported. Due to a change in the internals +of `ActiveSupport::Callbacks`, this is no longer allowed in Rails 4.1. Using a +`return` statement in an inline callback block causes a `LocalJumpError` to +be raised when the callback is executed. + +Inline callback blocks using `return` can be refactored to evaluate to the +returned value: + +```ruby +class ReadOnlyModel < ActiveRecord::Base + before_save { false } # GOOD +end +``` + +Alternatively, if `return` is preferred it is recommended to explicitly define +a method: + +```ruby +class ReadOnlyModel < ActiveRecord::Base + before_save :before_save_callback # GOOD + + private + def before_save_callback + return false + end +end +``` + +This change applies to most places in Rails where callbacks are used, including +Active Record and Active Model callbacks, as well as filters in Action +Controller (e.g. `before_action`). + +See [this pull request](https://github.com/rails/rails/pull/13271) for more +details. + ### Methods defined in Active Record fixtures Rails 4.1 evaluates each fixture's ERB in a separate context, so helper methods @@ -111,6 +215,40 @@ end ActiveRecord::FixtureSet.context_class.send :include, FixtureFileHelpers ``` +### I18n enforcing available locales + +Rails 4.1 now defaults the I18n option `enforce_available_locales` to `true`, +meaning that it will make sure that all locales passed to it must be declared in +the `available_locales` list. + +To disable it (and allow I18n to accept *any* locale option) add the following +configuration to your application: + +```ruby +config.i18n.enforce_available_locales = false +``` + +Note that this option was added as a security measure, to ensure user input could +not be used as locale information unless previously known, so it's recommended not +to disable this option unless you have a strong reason for doing so. + +### Mutator methods called on Relation + +`Relation` no longer has mutator methods like `#map!` and `#delete_if`. Convert +to an `Array` by calling `#to_a` before using these methods. + +It intends to prevent odd bugs and confusion in code that call mutator +methods directly on the `Relation`. + +```ruby +# Instead of this +Author.where(name: 'Hank Moody').compact! + +# Now you have to do this +authors = Author.where(name: 'Hank Moody').to_a +authors.compact! +``` + Upgrading from Rails 3.2 to Rails 4.0 ------------------------------------- @@ -182,7 +320,7 @@ being used, you can update your form to use the `PUT` method instead: <%= form_for [ :update_name, @user ], method: :put do |f| %> ``` -For more on PATCH and why this change was made, see [this post](http://weblog.rubyonrails.org/2012/2/25/edge-rails-patch-is-the-new-primary-http-method-for-updates/) +For more on PATCH and why this change was made, see [this post](http://weblog.rubyonrails.org/2012/2/26/edge-rails-patch-is-the-new-primary-http-method-for-updates/) on the Rails blog. #### A note about media types @@ -365,13 +503,13 @@ get 'こんにちは', controller: 'welcome', action: 'index' ```ruby # Rails 3.x - match "/" => "root#index" + match '/' => 'root#index' # becomes - match "/" => "root#index", via: :get + match '/' => 'root#index', via: :get # or - get "/" => "root#index" + get '/' => 'root#index' ``` * Rails 4.0 has removed `ActionDispatch::BestStandardsSupport` middleware, `<!DOCTYPE html>` already triggers standards mode per http://msdn.microsoft.com/en-us/library/jj676915(v=vs.85).aspx and ChromeFrame header has been moved to `config.action_dispatch.default_headers`. @@ -476,6 +614,10 @@ config.active_record.mass_assignment_sanitizer = :strict Rails 3.2 deprecates `vendor/plugins` and Rails 4.0 will remove them completely. While it's not strictly necessary as part of a Rails 3.2 upgrade, you can start replacing any plugins by extracting them to gems and adding them to your Gemfile. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`. +### Active Record + +Option `:dependent => :restrict` has been removed from `belongs_to`. If you want to prevent deleting the object if there are any associated objects, you can set `:dependent => :destroy` and return `false` after checking for existence of association from any of the associated object's destroy callbacks. + Upgrading from Rails 3.0 to Rails 3.1 ------------------------------------- @@ -563,7 +705,7 @@ You can help test performance with these additions to your test environment: ```ruby # Configure static asset server for tests with Cache-Control for performance config.serve_static_assets = true -config.static_cache_control = "public, max-age=3600" +config.static_cache_control = 'public, max-age=3600' ``` ### config/initializers/wrap_parameters.rb diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md index 301e0e7e6c..3c04204c29 100644 --- a/guides/source/working_with_javascript_in_rails.md +++ b/guides/source/working_with_javascript_in_rails.md @@ -169,7 +169,7 @@ This will generate the following HTML: </form> ``` -Note the `data-remote='true'`. Now, the form will be submitted by Ajax rather +Note the `data-remote="true"`. Now, the form will be submitted by Ajax rather than by the browser's normal submit mechanism. You probably don't want to just sit there with a filled out `<form>`, though. @@ -194,7 +194,17 @@ is very similar to `form_for`. It has a `:remote` option that you can use like this: ```erb -<%= form_tag('/posts', remote: true) %> +<%= form_tag('/posts', remote: true) do %> + ... +<% end %> +``` + +This will generate the following HTML: + +```html +<form accept-charset="UTF-8" action="/posts" data-remote="true" method="post"> + ... +</form> ``` Everything else is the same as `form_for`. See its documentation for full @@ -299,10 +309,10 @@ The `app/views/users/_user.html.erb` partial contains the following: The top portion of the index page displays the users. The bottom portion provides a form to create a new user. -The bottom form will call the create action on the Users controller. Because +The bottom form will call the `create` action on the `UsersController`. Because the form's remote option is set to true, the request will be posted to the -users controller as an Ajax request, looking for JavaScript. In order to -service that request, the create action of your controller would look like +`UsersController` as an Ajax request, looking for JavaScript. In order to +serve that request, the `create` action of your controller would look like this: ```ruby diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 3813ef7909..01c80d3f58 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,22 +1,99 @@ +* Ensure that `bin/rails` is a file before trying to execute it. + + Fixes #13825. + + *bronzle* + +* Use single quotes in generated files. + + *Cristian Mircea Messel*, *Chulki Lee* + +* Only lookup `config.log_level` for stdlib `::Logger` instances. + Assign it as is for third party loggers like `Log4r::Logger`. + + Fixes #13421. + + *Yves Senn* + +* The `Gemfile` of new applications depends on SDoc ~> 0.4.0. + + *Xavier Noria* + +* `test_help.rb` now automatically checks/maintains your test database + schema. (Use `config.active_record.maintain_test_schema = false` to + disable.) + + *Jon Leighton* + +* Configure `secrets.yml` and `database.yml` to read configuration + from the system environment by default for production. + + *José Valim* + +* `config.assets.raise_runtime_errors` is set to true by default + + This option has been introduced in + [sprockets-rails#100][https://github.com/rails/sprockets-rails/pull/100] + and defaults to true in new applications in development. + + *Richard Schneeman* + +* Generates `html` and `text` templates for mailers by default. + + *Kassio Borges* + +* Move `secret_key_base` from `config/initializers/secret_token.rb` + to `config/secrets.yml`. + + `secret_key_base` is now saved in `Rails.application.secrets.secret_key_base` + and it fallbacks to the value of `config.secret_key_base` when it is not + present in `config/secrets.yml`. + + `config/initializers/secret_token.rb` is not generated by default + in new applications. + + *Guillermo Iguaran* + +* Generate a new `secrets.yml` file in the `config` folder for new + applications. By default, this file contains the application's `secret_key_base`, + but it could also be used to store other secrets such as access keys for external + APIs. + + The secrets added to this file will be accessible via `Rails.application.secrets`. + For example, with the following `secrets.yml`: + + development: + secret_key_base: 3b7cd727ee24e8444053437c36cc66c3 + some_api_key: SOMEKEY + + `Rails.application.secrets.some_api_key` will return `SOMEKEY` in the development + environment. + + *Guillermo Iguaran* + +* Add `ENV['DATABASE_URL']` support in `rails dbconsole`. Fixes #13320. + + *Huiming Teo* + * Add `Application#message_verifier` method to return a message verifier. This verifier can be used to generate and verify signed messages in the application. - message = Rails.application.message_verifier('salt').generate('my sensible data') - Rails.application.message_verifier('salt').verify(message) + message = Rails.application.message_verifier(:sensitive_data).generate('my sensible data') + Rails.application.message_verifier(:sensitive_data).verify(message) # => 'my sensible data' It is recommended not to use the same verifier for different things, so you can get different verifiers passing the name argument. - message = Rails.application.message_verifier('cookies').generate('my sensible cookie data') + message = Rails.application.message_verifier(:cookies).generate('my sensible cookie data') See the `ActiveSupport::MessageVerifier` documentation for more information. *Rafael Mendonça França* * The [Spring application - preloader](https://github.com/jonleighton/spring) is now installed + preloader](https://github.com/rails/spring) is now installed by default for new applications. It uses the development group of the Gemfile, so will not be installed in production. diff --git a/railties/MIT-LICENSE b/railties/MIT-LICENSE index 0d7fb865e2..2950f05b11 100644 --- a/railties/MIT-LICENSE +++ b/railties/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2013 David Heinemeier Hansson +Copyright (c) 2004-2014 David Heinemeier Hansson 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.rb b/railties/lib/rails.rb index ea82327365..be7570a5ba 100644 --- a/railties/lib/rails.rb +++ b/railties/lib/rails.rb @@ -25,6 +25,7 @@ module Rails autoload :Info autoload :InfoController + autoload :MailersController autoload :WelcomeController class << self diff --git a/railties/lib/rails/app_rails_loader.rb b/railties/lib/rails/app_rails_loader.rb index 1610751844..56f05b3844 100644 --- a/railties/lib/rails/app_rails_loader.rb +++ b/railties/lib/rails/app_rails_loader.rb @@ -55,7 +55,7 @@ EOS end def self.find_executable - EXECUTABLES.find { |exe| File.exist?(exe) } + EXECUTABLES.find { |exe| File.file?(exe) } end end end diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index e45bfaf6fc..36432e56ba 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -1,4 +1,5 @@ require 'fileutils' +require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/object/blank' require 'active_support/key_generator' require 'active_support/message_verifier' @@ -104,7 +105,7 @@ module Rails delegate :default_url_options, :default_url_options=, to: :routes INITIAL_VARIABLES = [:config, :railties, :routes_reloader, :reloaders, - :routes, :helpers, :app_env_config] # :nodoc: + :routes, :helpers, :app_env_config, :secrets] # :nodoc: def initialize(initial_variable_values = {}, &block) super() @@ -151,8 +152,8 @@ module Rails # number of iterations selected based on consultation with the google security # team. Details at https://github.com/rails/rails/pull/6952#issuecomment-7661220 @caching_key_generator ||= begin - if config.secret_key_base - key_generator = ActiveSupport::KeyGenerator.new(config.secret_key_base, iterations: 1000) + if secrets.secret_key_base + key_generator = ActiveSupport::KeyGenerator.new(secrets.secret_key_base, iterations: 1000) ActiveSupport::CachingKeyGenerator.new(key_generator) else ActiveSupport::LegacyKeyGenerator.new(config.secret_token) @@ -169,18 +170,18 @@ module Rails # # ==== Parameters # - # * +salt+ - the salt that will be used to generate the secret key of the verifier. + # * +verifier_name+ - the name of the message verifier. # # ==== Examples # - # message = Rails.application.message_verifier('salt').generate('my sensible data') - # Rails.application.message_verifier('salt').verify(message) + # message = Rails.application.message_verifier('sensitive_data').generate('my sensible data') + # Rails.application.message_verifier('sensitive_data').verify(message) # # => 'my sensible data' # # See the +ActiveSupport::MessageVerifier+ documentation for more information. - def message_verifier(salt) - @message_verifiers[salt] ||= begin - secret = key_generator.generate_key(salt) + def message_verifier(verifier_name) + @message_verifiers[verifier_name] ||= begin + secret = key_generator.generate_key(verifier_name.to_s) ActiveSupport::MessageVerifier.new(secret) end end @@ -195,7 +196,7 @@ module Rails "action_dispatch.parameter_filter" => config.filter_parameters, "action_dispatch.redirect_filter" => config.filter_redirect, "action_dispatch.secret_token" => config.secret_token, - "action_dispatch.secret_key_base" => config.secret_key_base, + "action_dispatch.secret_key_base" => secrets.secret_key_base, "action_dispatch.show_exceptions" => config.action_dispatch.show_exceptions, "action_dispatch.show_detailed_exceptions" => config.consider_all_requests_local, "action_dispatch.logger" => Rails.logger, @@ -204,7 +205,8 @@ module Rails "action_dispatch.http_auth_salt" => config.action_dispatch.http_auth_salt, "action_dispatch.signed_cookie_salt" => config.action_dispatch.signed_cookie_salt, "action_dispatch.encrypted_cookie_salt" => config.action_dispatch.encrypted_cookie_salt, - "action_dispatch.encrypted_signed_cookie_salt" => config.action_dispatch.encrypted_signed_cookie_salt + "action_dispatch.encrypted_signed_cookie_salt" => config.action_dispatch.encrypted_signed_cookie_salt, + "action_dispatch.session_serializer" => config.session_options[:serializer] }) end end @@ -300,6 +302,27 @@ module Rails @config = configuration end + def secrets #:nodoc: + @secrets ||= begin + secrets = ActiveSupport::OrderedOptions.new + yaml = config.paths["config/secrets"].first + if File.exist?(yaml) + require "erb" + env_secrets = YAML.load(ERB.new(IO.read(yaml)).result)[Rails.env] + secrets.merge!(env_secrets.symbolize_keys) if env_secrets + end + + # Fallback to config.secret_key_base if secrets.secret_key_base isn't set + secrets.secret_key_base ||= config.secret_key_base + + secrets + end + end + + def secrets=(secrets) #:nodoc: + @secrets = secrets + end + def to_app #:nodoc: self end @@ -391,8 +414,8 @@ module Rails end def validate_secret_key_config! #:nodoc: - if config.secret_key_base.blank? && config.secret_token.blank? - raise "You must set config.secret_key_base in your app's config." + if secrets.secret_key_base.blank? && config.secret_token.blank? + raise "Missing `secret_key_base` for '#{Rails.env}' environment, set this value in `config/secrets.yml`" end end end diff --git a/railties/lib/rails/application/bootstrap.rb b/railties/lib/rails/application/bootstrap.rb index a26d41c0cf..33bcab1e57 100644 --- a/railties/lib/rails/application/bootstrap.rb +++ b/railties/lib/rails/application/bootstrap.rb @@ -53,7 +53,11 @@ INFO logger end - Rails.logger.level = ActiveSupport::Logger.const_get(config.log_level.to_s.upcase) + if ::Logger === Rails.logger + Rails.logger.level = ActiveSupport::Logger.const_get(config.log_level.to_s.upcase) + else + Rails.logger.level = config.log_level + end end # Initialize cache early in the stack so railties can make use of it. diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index dd0b9c6d70..e902205a13 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -76,6 +76,7 @@ module Rails @paths ||= begin paths = super paths.add "config/database", with: "config/database.yml" + paths.add "config/secrets", with: "config/secrets.yml" paths.add "config/environment", with: "config/environment.rb" paths.add "lib/templates" paths.add "log", with: "log/#{Rails.env}.log" @@ -87,17 +88,23 @@ module Rails end end - # Loads and returns the configuration of the database. + # Loads and returns the entire raw configuration of database from + # values stored in `config/database.yml`. def database_configuration - yaml = paths["config/database"].first - if File.exist?(yaml) + yaml = Pathname.new(paths["config/database"].first || "") + + config = if yaml.exist? require "erb" - YAML.load ERB.new(IO.read(yaml)).result + YAML.load(ERB.new(yaml.read).result) || {} elsif ENV['DATABASE_URL'] - nil + # Value from ENV['DATABASE_URL'] is set to default database connection + # by Active Record. + {} else raise "Could not load database configuration. No such file - #{yaml}" end + + config rescue Psych::SyntaxError => e raise "YAML syntax error occurred while parsing #{paths["config/database"].first}. " \ "Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \ diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb index 7a1bb1e25c..5b8509b2e9 100644 --- a/railties/lib/rails/application/finisher.rb +++ b/railties/lib/rails/application/finisher.rb @@ -22,6 +22,8 @@ module Rails initializer :add_builtin_route do |app| if Rails.env.development? app.routes.append do + get '/rails/mailers' => "rails/mailers#index" + get '/rails/mailers/*path' => "rails/mailers#preview" get '/rails/info/properties' => "rails/info#properties" get '/rails/info/routes' => "rails/info#routes" get '/rails/info' => "rails/info#index" diff --git a/railties/lib/rails/application_controller.rb b/railties/lib/rails/application_controller.rb new file mode 100644 index 0000000000..9a29ec21cf --- /dev/null +++ b/railties/lib/rails/application_controller.rb @@ -0,0 +1,16 @@ +class Rails::ApplicationController < ActionController::Base # :nodoc: + self.view_paths = File.expand_path('../templates', __FILE__) + layout 'application' + + protected + + def require_local! + unless local_request? + render text: '<p>For security purposes, this information is only available to local requests.</p>', status: :forbidden + end + end + + def local_request? + Rails.application.config.consider_all_requests_local || request.local? + end +end diff --git a/railties/lib/rails/commands/dbconsole.rb b/railties/lib/rails/commands/dbconsole.rb index 3e4cc787c4..f6d8aec30d 100644 --- a/railties/lib/rails/commands/dbconsole.rb +++ b/railties/lib/rails/commands/dbconsole.rb @@ -81,14 +81,11 @@ module Rails def config @config ||= begin - cfg = begin - YAML.load(ERB.new(IO.read("config/database.yml")).result) - rescue SyntaxError, StandardError - require APP_PATH - Rails.application.config.database_configuration + if configurations[environment].blank? + raise ActiveRecord::AdapterNotSpecified, "'#{environment}' database is not configured. Available configuration: #{configurations.inspect}" + else + configurations[environment] end - - cfg[environment] || abort("No database is configured for the environment '#{environment}'") end end @@ -102,6 +99,12 @@ module Rails protected + def configurations + require APP_PATH + ActiveRecord::Base.configurations = Rails.application.config.database_configuration + ActiveRecord::Base.configurations + end + def parse_arguments(arguments) options = {} diff --git a/railties/lib/rails/commands/runner.rb b/railties/lib/rails/commands/runner.rb index dd2ee5639e..3a71f8d3f8 100644 --- a/railties/lib/rails/commands/runner.rb +++ b/railties/lib/rails/commands/runner.rb @@ -9,7 +9,7 @@ if ARGV.first.nil? end ARGV.clone.options do |opts| - opts.banner = "Usage: rails runner [options] ('Some.ruby(code)' or a filename)" + opts.banner = "Usage: rails runner [options] [<'Some.ruby(code)'> | <filename.rb>]" opts.separator "" @@ -22,14 +22,23 @@ ARGV.clone.options do |opts| opts.on("-h", "--help", "Show this help message.") { $stdout.puts opts; exit } + opts.separator "" + opts.separator "Examples: " + + opts.separator " rails runner 'puts Rails.env'" + opts.separator " This runs the code `puts Rails.env` after loading the app" + opts.separator "" + opts.separator " rails runner path/to/filename.rb" + opts.separator " This runs the Ruby file located at `path/to/filename.rb` after loading the app" + if RbConfig::CONFIG['host_os'] !~ /mswin|mingw/ opts.separator "" opts.separator "You can also use runner as a shebang line for your executables:" - opts.separator "-------------------------------------------------------------" - opts.separator "#!/usr/bin/env #{File.expand_path($0)} runner" + opts.separator " -------------------------------------------------------------" + opts.separator " #!/usr/bin/env #{File.expand_path($0)} runner" opts.separator "" - opts.separator "Product.all.each { |p| p.price *= 2 ; p.save! }" - opts.separator "-------------------------------------------------------------" + opts.separator " Product.all.each { |p| p.price *= 2 ; p.save! }" + opts.separator " -------------------------------------------------------------" end opts.order! { |o| code_or_file ||= o } rescue retry diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index 54a0f1c002..5c54cdaa70 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -2,7 +2,6 @@ require 'rails/railtie' require 'rails/engine/railties' require 'active_support/core_ext/module/delegation' require 'pathname' -require 'rbconfig' module Rails # <tt>Rails::Engine</tt> allows you to wrap a specific Rails application or subset of diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb index 366c72ebaa..afdbf5c241 100644 --- a/railties/lib/rails/generators/actions.rb +++ b/railties/lib/rails/generators/actions.rb @@ -9,7 +9,7 @@ module Rails @in_group = nil end - # Adds an entry into Gemfile for the supplied gem. + # Adds an entry into +Gemfile+ for the supplied gem. # # gem "rspec", group: :test # gem "technoweenie-restful-authentication", lib: "restful-authentication", source: "http://gems.github.com/" @@ -61,7 +61,7 @@ module Rails end end - # Add the given source to Gemfile + # Add the given source to +Gemfile+ # # add_source "http://gems.github.com/" def add_source(source, options={}) @@ -72,10 +72,10 @@ module Rails end end - # Adds a line inside the Application class for config/application.rb. + # Adds a line inside the Application class for <tt>config/application.rb</tt>. # - # If options :env is specified, the line is appended to the corresponding - # file in config/environments. + # If options <tt>:env</tt> is specified, the line is appended to the corresponding + # file in <tt>config/environments</tt>. # # environment do # "config.autoload_paths += %W(#{config.root}/extras)" @@ -116,7 +116,7 @@ module Rails end end - # Create a new file in the vendor/ directory. Code can be specified + # Create a new file in the <tt>vendor/</tt> directory. Code can be specified # in a block or a data string can be given. # # vendor("sekrit.rb") do @@ -143,7 +143,7 @@ module Rails create_file("lib/#{filename}", data, verbose: false, &block) end - # Create a new Rakefile with the provided code (either in a block or a string). + # Create a new +Rakefile+ with the provided code (either in a block or a string). # # rakefile("bootstrap.rake") do # project = ask("What is the UNIX name of your project?") @@ -213,7 +213,7 @@ module Rails in_root { run("#{extify(:capify)} .", verbose: false) } end - # Make an entry in Rails routing file config/routes.rb + # Make an entry in Rails routing file <tt>config/routes.rb</tt> # # route "root 'welcome#index'" def route(routing_code) diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 5d4682f6e3..55709b80ae 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -14,6 +14,7 @@ module Rails DATABASES.concat(JDBC_DATABASES) attr_accessor :rails_template + attr_accessor :app_template add_shebang_option! argument :app_path, type: :string @@ -26,6 +27,9 @@ module Rails class_option :template, type: :string, aliases: '-m', desc: "Path to some #{name} template (can be a filesystem path or URL)" + class_option :app_template, type: :string, aliases: '-n', + desc: "Path to some #{name} template (can be a filesystem path or URL)" + class_option :skip_gemfile, type: :boolean, default: false, desc: "Don't create a Gemfile" @@ -122,6 +126,10 @@ module Rails }.curry[@gem_filter] end + def remove_gem(name) + add_gem_entry_filter { |gem| gem.name != name } + end + def builder @builder ||= begin builder_class = get_builder_class @@ -162,6 +170,10 @@ module Rails @target.send :add_gem_entry_filter, *args, &block end + def remove_gem(*args, &block) + @target.send :remove_gem, *args, &block + end + def method_missing(name, *args, &block) @commands << [name, args, block] end @@ -180,7 +192,8 @@ module Rails def apply_rails_template @recorder = TemplateRecorder.new self - apply(rails_template, target: @recorder) if rails_template + apply(rails_template, target: self) if rails_template + apply(app_template, target: @recorder) if app_template rescue Thor::Error, LoadError, Errno::ENOENT => e raise Error, "The template [#{rails_template}] could not be loaded. Error: #{e}" end @@ -210,13 +223,18 @@ module Rails def set_default_accessors! self.destination_root = File.expand_path(app_path, destination_root) - self.rails_template = case options[:template] - when /^https?:\/\// - options[:template] - when String - File.expand_path(options[:template], Dir.pwd) - else - options[:template] + self.rails_template = expand_template options[:template] + self.app_template = expand_template options[:app_template] + end + + def expand_template(name) + case name + when /^https?:\/\// + name + when String + File.expand_path(name, Dir.pwd) + else + name end end @@ -308,7 +326,7 @@ module Rails 'Use SCSS for stylesheets') else gems << GemfileEntry.version('sass-rails', - '~> 4.0.0.rc1', + '~> 4.0.1', 'Use SCSS for stylesheets') end @@ -329,12 +347,12 @@ module Rails def jbuilder_gemfile_entry comment = 'Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder' - GemfileEntry.version('jbuilder', '~> 1.2', comment) + GemfileEntry.version('jbuilder', '~> 2.0', comment) end def sdoc_gemfile_entry comment = 'bundle exec rake doc:rails generates the API under doc/api.' - GemfileEntry.new('sdoc', nil, comment, { group: :doc, require: false }) + GemfileEntry.new('sdoc', '~> 0.4.0', comment, group: :doc) end def coffee_gemfile_entry @@ -371,7 +389,7 @@ module Rails def spring_gemfile_entry return [] unless spring_install? - comment = 'Spring speeds up development by keeping your application running in the background. Read more: https://github.com/jonleighton/spring' + comment = 'Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring' GemfileEntry.new('spring', nil, comment, group: :development) end diff --git a/railties/lib/rails/generators/base.rb b/railties/lib/rails/generators/base.rb index cf33b13fd8..67bab96a22 100644 --- a/railties/lib/rails/generators/base.rb +++ b/railties/lib/rails/generators/base.rb @@ -296,7 +296,7 @@ module Rails end end - # Return the default value for the option name given doing a lookup in + # Returns the default value for the option name given doing a lookup in # Rails::Generators.options. def self.default_value_for_option(name, options) default_for_option(Rails::Generators.options, name, options, options[:default]) diff --git a/railties/lib/rails/generators/erb.rb b/railties/lib/rails/generators/erb.rb index 73e986ee7f..cfd77097d5 100644 --- a/railties/lib/rails/generators/erb.rb +++ b/railties/lib/rails/generators/erb.rb @@ -5,6 +5,10 @@ module Erb # :nodoc: class Base < Rails::Generators::NamedBase #:nodoc: protected + def formats + format + end + def format :html end @@ -13,7 +17,7 @@ module Erb # :nodoc: :erb end - def filename_with_extensions(name) + def filename_with_extensions(name, format) [name, format, handler].compact.join(".") end end diff --git a/railties/lib/rails/generators/erb/controller/controller_generator.rb b/railties/lib/rails/generators/erb/controller/controller_generator.rb index 5f06734ab8..e62aece7c5 100644 --- a/railties/lib/rails/generators/erb/controller/controller_generator.rb +++ b/railties/lib/rails/generators/erb/controller/controller_generator.rb @@ -11,8 +11,10 @@ module Erb # :nodoc: actions.each do |action| @action = action - @path = File.join(base_path, filename_with_extensions(action)) - template filename_with_extensions(:view), @path + Array(formats).each do |format| + @path = File.join(base_path, filename_with_extensions(action, format)) + template filename_with_extensions(:view, format), @path + end end end end diff --git a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb index 7bcac30dde..66b17bd10e 100644 --- a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb +++ b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb @@ -5,8 +5,8 @@ module Erb # :nodoc: class MailerGenerator < ControllerGenerator # :nodoc: protected - def format - :text + def formats + [:text, :html] end end end diff --git a/railties/lib/rails/generators/erb/mailer/templates/view.html.erb b/railties/lib/rails/generators/erb/mailer/templates/view.html.erb new file mode 100644 index 0000000000..8bb7c2b768 --- /dev/null +++ b/railties/lib/rails/generators/erb/mailer/templates/view.html.erb @@ -0,0 +1,5 @@ +<h1><%= class_name %>#<%= @action %></h1> + +<p> + <%%= @greeting %>, find me in app/views/<%= @path %> +</p> diff --git a/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb index bacbc2d280..b219f459ac 100644 --- a/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb +++ b/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb @@ -14,8 +14,10 @@ module Erb # :nodoc: def copy_view_files available_views.each do |view| - filename = filename_with_extensions(view) - template filename, File.join("app/views", controller_file_path, filename) + Array(formats).each do |format| + filename = filename_with_extensions(view, format) + template filename, File.join("app/views", controller_file_path, filename) + end end end diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 87556bd609..d2eca5b2fb 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -78,6 +78,7 @@ module Rails template "routes.rb" template "application.rb" template "environment.rb" + template "secrets.yml" directory "environments" directory "initializers" @@ -225,16 +226,16 @@ module Rails build(:vendor) end - def finish_template - build(:leftovers) - end - def delete_js_folder_skipping_javascript if options[:skip_javascript] remove_dir 'app/assets/javascripts' end end + def finish_template + build(:leftovers) + end + public_task :run_bundle public_task :replay_template public_task :generate_spring_binstubs diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index 68bd62d4b1..6d017e187d 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -6,12 +6,10 @@ source 'https://rubygems.org' # <%= gem.comment %> <% end -%> -<%= gem.commented_out ? '# ' : '' %>gem '<%= gem.name %>'<% if gem.version -%> -, '<%= gem.version %>' -<% elsif gem.options.any? -%> +<%= gem.commented_out ? '# ' : '' %>gem '<%= gem.name %>'<%= %(, '#{gem.version}') if gem.version -%> +<% if gem.options.any? -%> ,<%= gem.padding(max_width) %><%= gem.options.map { |k,v| "#{k}: #{v.inspect}" }.join(', ') %> -<% else %> <% end -%> <% end -%> diff --git a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt index fe71f7122c..75ea52828e 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt +++ b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt @@ -3,14 +3,14 @@ <head> <title><%= camelized %></title> <%- if options[:skip_javascript] -%> - <%%= stylesheet_link_tag "application", media: "all" %> + <%%= stylesheet_link_tag 'application', media: 'all' %> <%- else -%> - <%- if gemfile_entries.any? { |m| m.name == "turbolinks" } -%> - <%%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %> - <%%= javascript_include_tag "application", "data-turbolinks-track" => true %> + <%- if gemfile_entries.any? { |m| m.name == 'turbolinks' } -%> + <%%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> + <%%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> <%- else -%> - <%%= stylesheet_link_tag "application", media: "all" %> - <%%= javascript_include_tag "application" %> + <%%= stylesheet_link_tag 'application', media: 'all' %> + <%%= javascript_include_tag 'application' %> <%- end -%> <%- end -%> <%%= csrf_meta_tags %> diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml index c6dfd50d40..138e3a8664 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml @@ -23,6 +23,7 @@ test: <<: *default database: <%= app_name %>_test +# Do not keep production credentials in the repository, +# instead read the configuration from the environment. production: - <<: *default - database: <%= app_name %>_production + url: <%%= ENV["DATABASE_URL"] %> diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml index fe53cd0ea2..2cdb592eeb 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml @@ -61,6 +61,7 @@ test: <<: *default database: <%= app_name[0,4] %>_tst +# Do not keep production credentials in the repository, +# instead read the configuration from the environment. production: - <<: *default - database: <%= app_name[0,8] %> + url: <%%= ENV["DATABASE_URL"] %> diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml index be1dae332f..cefd30d519 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml @@ -53,6 +53,7 @@ test: <<: *default url: jdbc:db://localhost/<%= app_name %>_test +# Do not keep production credentials in the repository, +# instead read the configuration from the environment. production: - <<: *default - url: jdbc:db://localhost/<%= app_name %>_production + url: <%%= ENV["DATABASE_URL"] %> diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml index 26e2a5976c..d31761349c 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml @@ -26,6 +26,6 @@ test: <<: *default database: <%= app_name %>_test -production: - <<: *default - database: <%= app_name %>_production +# Do not keep production credentials in the repository, +# instead read the configuration from the environment. +production: <%%= ENV["DATABASE_URL"] %> diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml index ccd44cf54b..0d248dc197 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml @@ -6,13 +6,20 @@ default: &default adapter: postgresql encoding: unicode - username: <%= app_name %> - password: development: <<: *default database: <%= app_name %>_development + # The specified database role being used to connect to postgres. + # To create additional roles in postgres see `$ createuser --help`. + # When left blank, postgres will use the default role. This is + # the same name as the operating system user that initialized the database. + #username: <%= app_name %> + + # The password associated with the postgres role (username). + #password: + # Connect on a TCP socket. Omitted by default since the client uses a # domain socket that doesn't need configuration. Windows does not have # domain sockets, so uncomment these lines. @@ -35,6 +42,7 @@ test: <<: *default database: <%= app_name %>_test +# Do not keep production credentials in the repository, +# instead read the configuration from the environment. production: - <<: *default - database: <%= app_name %>_production + url: <%%= ENV["DATABASE_URL"] %> diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml index 28c36eb82f..66eba3bf0d 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml @@ -18,6 +18,7 @@ test: <<: *default database: db/test.sqlite3 +# Do not keep production credentials in the repository, +# instead read the configuration from the environment. production: - <<: *default - database: db/production.sqlite3 + url: <%%= ENV["DATABASE_URL"] %> diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml index 3fc7ce28a1..d618fc28a6 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml @@ -1,4 +1,4 @@ -# MySQL. Versions 4.1 and 5.0 are recommended. +# MySQL. Versions 5.0+ are recommended. # # Install the MYSQL driver # gem install mysql2 @@ -32,6 +32,11 @@ test: <<: *default database: <%= app_name %>_test +# Avoid production credentials in the repository, +# instead read the configuration from the environment. +# +# Example: +# mysql2://myuser:mypass@localhost/somedatabase +# production: - <<: *default - database: <%= app_name %>_production + url: <%%= ENV["DATABASE_URL"] %> diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml index d5b52c969b..d469ec0f99 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml @@ -32,6 +32,7 @@ test: <<: *default database: <%= app_name %>_test +# Do not keep production credentials in the repository, +# instead read the configuration from the environment. production: - <<: *default - database: <%= app_name %>_production + url: <%%= ENV["DATABASE_URL"] %> diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml index eaeb82bddd..93f48656b2 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml @@ -17,16 +17,23 @@ default: &default adapter: postgresql encoding: unicode - # For details on connection pooling, see rails configration guide + # For details on connection pooling, see rails configuration guide # http://guides.rubyonrails.org/configuring.html#database-pooling pool: 5 - username: <%= app_name %> - password: development: <<: *default database: <%= app_name %>_development + # The specified database role being used to connect to postgres. + # To create additional roles in postgres see `$ createuser --help`. + # When left blank, postgres will use the default role. This is + # the same name as the operating system user that initialized the database. + #username: <%= app_name %> + + # The password associated with the postgres role (username). + #password: + # Connect on a TCP socket. Omitted by default since the client uses a # domain socket that doesn't need configuration. Windows does not have # domain sockets, so uncomment these lines. @@ -52,6 +59,11 @@ test: <<: *default database: <%= app_name %>_test +# Do not keep production credentials in the repository, +# instead read the configuration from the environment. +# +# Example: +# postgres://myuser:mypass@localhost/somedatabase +# production: - <<: *default - database: <%= app_name %>_production + url: <%%= ENV["DATABASE_URL"] %> diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml index 1c1a37ca8d..7312ddb6cd 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml @@ -20,6 +20,11 @@ test: <<: *default database: db/test.sqlite3 +# Do not keep production credentials in the repository, +# instead read the configuration from the environment. +# +# Example: +# sqlite3://myuser:mypass@localhost/full/path/to/somedatabase +# production: - <<: *default - database: db/production.sqlite3 + url: <%%= ENV["DATABASE_URL"] %> diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml index 4855f66c0d..aa960e493e 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml @@ -42,6 +42,7 @@ test: <<: *default database: <%= app_name %>_test +# Do not keep production credentials in the repository, +# instead read the configuration from the environment. production: - <<: *default - database: <%= app_name %>_production + url: <%%= ENV["DATABASE_URL"] %> 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 b724d468a2..de12565a73 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 @@ -29,5 +29,13 @@ Rails.application.configure do # This option may cause significant delays in view rendering with a large # number of complex assets. config.assets.debug = 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 -%> + + # Raises error for missing translations + # config.action_view.raise_on_missing_translations = true end diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt index 1dfc9f136b..d2f041aa27 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt @@ -5,7 +5,7 @@ Rails.application.configure do config.cache_classes = true # Eager load code on boot. This eager loads most of Rails and - # your application in memory, allowing both thread web servers + # your application in memory, allowing both threaded web servers # and those relying on copy on write to perform better. # Rake tasks automatically ignore this option for performance. config.eager_load = true @@ -70,7 +70,7 @@ Rails.application.configure do # config.action_mailer.raise_delivery_errors = false # Enable locale fallbacks for I18n (makes lookups for any locale fall back to - # the I18n.default_locale when a translation can not be found). + # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true # Send deprecation notices to registered listeners. diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt index ba0742f97f..053f5b66d7 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt @@ -14,7 +14,7 @@ Rails.application.configure do # Configure static asset server for tests with Cache-Control for performance. config.serve_static_assets = true - config.static_cache_control = "public, max-age=3600" + config.static_cache_control = 'public, max-age=3600' # Show full error reports and disable caching. config.consider_all_requests_local = true @@ -33,4 +33,7 @@ Rails.application.configure do # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr + + # Raises error for missing translations + # config.action_view.raise_on_missing_translations = true end diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt index 2bb9b82c61..923d423287 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt @@ -1,3 +1,3 @@ # Be sure to restart your server when you modify this file. -Rails.application.config.session_store :cookie_store, key: <%= "'_#{app_name}_session'" %> +Rails.application.config.session_store :cookie_store, key: <%= "'_#{app_name}_session'" %>, serializer: :json_serializer diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/secrets.yml index f3cc6098a3..b2669a0f79 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/secrets.yml @@ -7,6 +7,16 @@ # no regular words or you'll be exposed to dictionary attacks. # You can use `rake secret` to generate a secure secret key. -# Make sure your secret_key_base is kept private +# Make sure the secrets in this file are kept private # if you're sharing your code publicly. -Rails.application.config.secret_key_base = '<%= app_secret %>' + +development: + secret_key_base: <%= app_secret %> + +test: + secret_key_base: <%= app_secret %> + +# Do not keep production secrets in the repository, +# instead read values from the environment. +production: + secret_key_base: <%%= ENV["SECRET_KEY_BASE"] %> 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 4fd060341e..6b011e577a 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 @@ -1,11 +1,9 @@ -ENV["RAILS_ENV"] ||= "test" +ENV['RAILS_ENV'] ||= 'test' require File.expand_path('../../config/environment', __FILE__) require 'rails/test_help' class ActiveSupport::TestCase <% unless options[:skip_active_record] -%> - 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 diff --git a/railties/lib/rails/generators/rails/controller/controller_generator.rb b/railties/lib/rails/generators/rails/controller/controller_generator.rb index ef84447df9..33a0d81bf6 100644 --- a/railties/lib/rails/generators/rails/controller/controller_generator.rb +++ b/railties/lib/rails/generators/rails/controller/controller_generator.rb @@ -23,7 +23,7 @@ module Rails # Will generate - # namespace :foo do # namespace :bar do - # get "baz/index" + # get 'baz/index' # end # end def generate_routing_code(action) @@ -36,8 +36,8 @@ module Rails end.join # Create route - # get "baz/index" - route = indent(%{get "#{file_name}/#{action}"\n}, depth * 2) + # get 'baz/index' + route = indent(%{get '#{file_name}/#{action}'\n}, depth * 2) # Create `end` ladder # end diff --git a/railties/lib/rails/generators/rails/plugin/templates/Gemfile b/railties/lib/rails/generators/rails/plugin/templates/Gemfile index d576784415..f0a832f783 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/Gemfile +++ b/railties/lib/rails/generators/rails/plugin/templates/Gemfile @@ -29,15 +29,15 @@ end # <%= gem.comment %> <% end -%> -<%= gem.commented_out ? '# ' : '' %>gem '<%= gem.name %>'<% if gem.version -%> -, '<%= gem.version %>' -<% elsif gem.options.any? -%> +<%= gem.commented_out ? '# ' : '' %>gem '<%= gem.name %>'<%= %(, '#{gem.version}') if gem.version -%> +<% if gem.options.any? -%> ,<%= gem.padding(max_width) %><%= gem.options.map { |k,v| "#{k}: #{v.inspect}" }.join(', ') %> -<% else %> <% end -%> <% end -%> <% end -%> +<% unless defined?(JRUBY_VERSION) -%> # To use debugger # gem 'debugger' +<% end -%> diff --git a/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt b/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt index c8de9f3e0f..c3314d7e68 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt +++ b/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt @@ -3,5 +3,9 @@ ENGINE_ROOT = File.expand_path('../..', __FILE__) ENGINE_PATH = File.expand_path('../../lib/<%= name -%>/engine', __FILE__) +# Set up gems listed in the Gemfile. +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) + require 'rails/all' require 'rails/engine/commands' diff --git a/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb b/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb index a0e5553e44..e4a2bc2b0f 100644 --- a/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb +++ b/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb @@ -9,7 +9,7 @@ module Rails # should give you # # namespace :admin do - # namespace :users + # namespace :users do # resources :products # end # end diff --git a/railties/lib/rails/generators/resource_helpers.rb b/railties/lib/rails/generators/resource_helpers.rb index a01eb57651..7329ee9f48 100644 --- a/railties/lib/rails/generators/resource_helpers.rb +++ b/railties/lib/rails/generators/resource_helpers.rb @@ -15,12 +15,10 @@ module Rails # Set controller variables on initialization. def initialize(*args) #:nodoc: super + controller_name = name if options[:model_name] - controller_name = name self.name = options[:model_name] assign_names!(self.name) - else - controller_name = name end if name == name.pluralize && name.singularize != name.pluralize && !options[:force_plural] diff --git a/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb b/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb index 3334b189bf..85dee1a066 100644 --- a/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb +++ b/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb @@ -4,11 +4,18 @@ module TestUnit # :nodoc: module Generators # :nodoc: class MailerGenerator < Base # :nodoc: argument :actions, type: :array, default: [], banner: "method method" - check_class_collision suffix: "Test" + + def check_class_collision + class_collisions "#{class_name}Test", "#{class_name}Preview" + end def create_test_files template "functional_test.rb", File.join('test/mailers', class_path, "#{file_name}_test.rb") end + + def create_preview_files + template "preview.rb", File.join('test/mailers/previews', class_path, "#{file_name}_preview.rb") + end end end end diff --git a/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb b/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb new file mode 100644 index 0000000000..3bfd5426e8 --- /dev/null +++ b/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb @@ -0,0 +1,13 @@ +<% module_namespacing do -%> +# Preview all emails at http://localhost:3000/rails/mailers/<%= file_path %> +class <%= class_name %>Preview < ActionMailer::Preview +<% actions.each do |action| -%> + + # Preview this email at http://localhost:3000/rails/mailers/<%= file_path %>/<%= action %> + def <%= action %> + <%= class_name %>.<%= action %> + end +<% end -%> + +end +<% end -%> diff --git a/railties/lib/rails/info_controller.rb b/railties/lib/rails/info_controller.rb index 85ea11b4e7..908c4ce65e 100644 --- a/railties/lib/rails/info_controller.rb +++ b/railties/lib/rails/info_controller.rb @@ -1,9 +1,9 @@ +require 'rails/application_controller' require 'action_dispatch/routing/inspector' -class Rails::InfoController < ActionController::Base # :nodoc: - self.view_paths = File.expand_path('../templates', __FILE__) +class Rails::InfoController < Rails::ApplicationController # :nodoc: prepend_view_path ActionDispatch::DebugExceptions::RESCUES_TEMPLATE_PATH - layout -> { request.xhr? ? nil : 'application' } + layout -> { request.xhr? ? false : 'application' } before_filter :require_local! @@ -20,16 +20,4 @@ class Rails::InfoController < ActionController::Base # :nodoc: @routes_inspector = ActionDispatch::Routing::RoutesInspector.new(_routes.routes) @page_title = 'Routes' end - - protected - - def require_local! - unless local_request? - render text: '<p>For security purposes, this information is only available to local requests.</p>', status: :forbidden - end - end - - def local_request? - Rails.application.config.consider_all_requests_local || request.local? - end end diff --git a/railties/lib/rails/mailers_controller.rb b/railties/lib/rails/mailers_controller.rb new file mode 100644 index 0000000000..dd318f52e5 --- /dev/null +++ b/railties/lib/rails/mailers_controller.rb @@ -0,0 +1,73 @@ +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 + + def index + @previews = ActionMailer::Preview.all + @page_title = "Mailer Previews" + end + + def preview + if params[:path] == @preview.preview_name + @page_title = "Mailer Previews for #{@preview.preview_name}" + render action: 'mailer' + else + email = File.basename(params[:path]) + + if @preview.email_exists?(email) + @email = @preview.call(email) + + if params[:part] + part_type = Mime::Type.lookup(params[:part]) + + if part = find_part(part_type) + response.content_type = part_type + render text: part.respond_to?(:decoded) ? part.decoded : part + else + raise AbstractController::ActionNotFound, "Email part '#{part_type}' not found in #{@preview.name}##{email}" + end + else + @part = find_preferred_part(request.format, Mime::HTML, Mime::TEXT) + render action: 'email', layout: false, formats: %w[html] + end + else + raise AbstractController::ActionNotFound, "Email '#{email}' not found in #{@preview.name}" + end + end + end + + protected + def find_preview + candidates = [] + params[:path].to_s.scan(%r{/|$}){ candidates << $` } + preview = candidates.detect{ |candidate| ActionMailer::Preview.exists?(candidate) } + + if preview + @preview = ActionMailer::Preview.find(preview) + else + raise AbstractController::ActionNotFound, "Mailer preview '#{params[:path]}' not found" + end + end + + def find_preferred_part(*formats) + if @email.multipart? + formats.each do |format| + return find_part(format) if @email.parts.any?{ |p| p.mime_type == format } + end + else + @email + end + end + + def find_part(format) + if @email.multipart? + @email.parts.find{ |p| p.mime_type == format } + elsif @email.mime_type == format + @email + end + end +end
\ No newline at end of file diff --git a/railties/lib/rails/paths.rb b/railties/lib/rails/paths.rb index f512aefb23..117bb37487 100644 --- a/railties/lib/rails/paths.rb +++ b/railties/lib/rails/paths.rb @@ -24,7 +24,7 @@ module Rails # # Notice that when you add a path using +add+, the path object created already # contains the path with the same path value given to +add+. In some situations, - # you may not want this behavior, so you can give :with as option. + # you may not want this behavior, so you can give <tt>:with</tt> as option. # # root.add "config/routes", with: "config/routes.rb" # root["config/routes"].inspect # => ["config/routes.rb"] diff --git a/railties/lib/rails/railtie.rb b/railties/lib/rails/railtie.rb index 89ca8cbe11..c63e0c0758 100644 --- a/railties/lib/rails/railtie.rb +++ b/railties/lib/rails/railtie.rb @@ -22,7 +22,7 @@ module Rails # # * creating initializers # * configuring a Rails framework for the application, like setting a generator - # * +adding config.*+ keys to the environment + # * adding <tt>config.*</tt> keys to the environment # * setting up a subscriber with ActiveSupport::Notifications # * adding rake tasks # @@ -63,8 +63,8 @@ module Rails # end # end # - # Finally, you can also pass :before and :after as option to initializer, in case - # you want to couple it with a specific step in the initialization process. + # Finally, you can also pass <tt>:before</tt> and <tt>:after</tt> as option to initializer, + # in case you want to couple it with a specific step in the initialization process. # # == Configuration # diff --git a/railties/lib/rails/templates/layouts/application.html.erb b/railties/lib/rails/templates/layouts/application.html.erb index 50a4755e45..5aecaa712a 100644 --- a/railties/lib/rails/templates/layouts/application.html.erb +++ b/railties/lib/rails/templates/layouts/application.html.erb @@ -29,7 +29,7 @@ </style> </head> <body> -<h2>Your App: <%= link_to 'properties', '/rails/info/properties' %> | <%= link_to 'routes', '/rails/info/routes' %></h2> + <%= yield %> </body> diff --git a/railties/lib/rails/templates/rails/mailers/email.html.erb b/railties/lib/rails/templates/rails/mailers/email.html.erb new file mode 100644 index 0000000000..977feb922b --- /dev/null +++ b/railties/lib/rails/templates/rails/mailers/email.html.erb @@ -0,0 +1,98 @@ +<!DOCTYPE html> +<html><head> +<meta name="viewport" content="width=device-width" /> +<style type="text/css"> + header { + width: 100%; + padding: 10px 0 0 0; + margin: 0; + background: white; + font: 12px "Lucida Grande", sans-serif; + border-bottom: 1px solid #dedede; + overflow: hidden; + } + + dl { + margin: 0 0 10px 0; + padding: 0; + } + + dt { + width: 80px; + padding: 1px; + float: left; + clear: left; + text-align: right; + color: #7f7f7f; + } + + dd { + margin-left: 90px; /* 80px + 10px */ + padding: 1px; + } + + iframe { + border: 0; + width: 100%; + height: 800px; + } +</style> +</head> + +<body> +<header> + <dl> + <% if @email.respond_to?(:smtp_envelope_from) && Array(@email.from) != Array(@email.smtp_envelope_from) %> + <dt>SMTP-From:</dt> + <dd><%= @email.smtp_envelope_from %></dd> + <% end %> + + <% if @email.respond_to?(:smtp_envelope_to) && @email.to != @email.smtp_envelope_to %> + <dt>SMTP-To:</dt> + <dd><%= @email.smtp_envelope_to %></dd> + <% end %> + + <dt>From:</dt> + <dd><%= @email.header['from'] %></dd> + + <% if @email.reply_to %> + <dt>Reply-To:</dt> + <dd><%= @email.header['reply-to'] %></dd> + <% end %> + + <dt>To:</dt> + <dd><%= @email.header['to'] %></dd> + + <% if @email.cc %> + <dt>CC:</dt> + <dd><%= @email.header['cc'] %></dd> + <% end %> + + <dt>Date:</dt> + <dd><%= Time.current.rfc2822 %></dd> + + <dt>Subject:</dt> + <dd><strong><%= @email.subject %></strong></dd> + + <% unless @email.attachments.nil? || @email.attachments.empty? %> + <dt>Attachments:</dt> + <dd> + <%= @email.attachments.map { |a| a.respond_to?(:original_filename) ? a.original_filename : a.filename }.inspect %> + </dd> + <% end %> + + <% if @email.multipart? %> + <dd> + <select onchange="document.getElementsByName('messageBody')[0].src=this.options[this.selectedIndex].value;"> + <option <%= request.format == Mime::HTML ? 'selected' : '' %> value="?part=text%2Fhtml">View as HTML email</option> + <option <%= request.format == Mime::TEXT ? 'selected' : '' %> value="?part=text%2Fplain">View as plain-text email</option> + </select> + </dd> + <% end %> + </dl> +</header> + +<iframe seamless name="messageBody" src="?part=<%= Rack::Utils.escape(@part.mime_type) %>"></iframe> + +</body> +</html>
\ No newline at end of file diff --git a/railties/lib/rails/templates/rails/mailers/index.html.erb b/railties/lib/rails/templates/rails/mailers/index.html.erb new file mode 100644 index 0000000000..c4c9757d57 --- /dev/null +++ b/railties/lib/rails/templates/rails/mailers/index.html.erb @@ -0,0 +1,8 @@ +<% @previews.each do |preview| %> +<h3><%= link_to preview.preview_name.titleize, "/rails/mailers/#{preview.preview_name}" %></h3> +<ul> +<% preview.emails.each do |email| %> +<li><%= link_to email, "/rails/mailers/#{preview.preview_name}/#{email}" %></li> +<% end %> +</ul> +<% end %> diff --git a/railties/lib/rails/templates/rails/mailers/mailer.html.erb b/railties/lib/rails/templates/rails/mailers/mailer.html.erb new file mode 100644 index 0000000000..607c8d1677 --- /dev/null +++ b/railties/lib/rails/templates/rails/mailers/mailer.html.erb @@ -0,0 +1,6 @@ +<h3><%= @preview.preview_name.titleize %></h3> +<ul> +<% @preview.emails.each do |email| %> +<li><%= link_to email, "/rails/mailers/#{@preview.preview_name}/#{email}" %></li> +<% end %> +</ul> diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb index be801befc2..2e6356343d 100644 --- a/railties/lib/rails/test_help.rb +++ b/railties/lib/rails/test_help.rb @@ -11,10 +11,12 @@ require 'rails/generators/test_case' # Config Rails backtrace in tests. require 'rails/backtrace_cleaner' if ENV["BACKTRACE"].nil? - MiniTest.backtrace_filter = Rails.backtrace_cleaner + Minitest.backtrace_filter = Rails.backtrace_cleaner end if defined?(ActiveRecord::Base) + ActiveRecord::Migration.maintain_test_schema! + class ActiveSupport::TestCase include ActiveRecord::TestFixtures self.fixture_path = "#{Rails.root}/test/fixtures/" diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake index 38107e77b2..285e2ce846 100644 --- a/railties/lib/rails/test_unit/testing.rake +++ b/railties/lib/rails/test_unit/testing.rake @@ -1,4 +1,3 @@ -require 'rbconfig' require 'rake/testtask' require 'rails/test_unit/sub_test_task' diff --git a/railties/lib/rails/version.rb b/railties/lib/rails/version.rb index 5a6d8d0983..923cab4e2a 100644 --- a/railties/lib/rails/version.rb +++ b/railties/lib/rails/version.rb @@ -3,7 +3,7 @@ module Rails MAJOR = 4 MINOR = 1 TINY = 0 - PRE = "beta" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/railties/lib/rails/welcome_controller.rb b/railties/lib/rails/welcome_controller.rb index 45b764fa6b..de9cd18b01 100644 --- a/railties/lib/rails/welcome_controller.rb +++ b/railties/lib/rails/welcome_controller.rb @@ -1,6 +1,7 @@ -class Rails::WelcomeController < ActionController::Base # :nodoc: - self.view_paths = File.expand_path('../templates', __FILE__) - layout nil +require 'rails/application_controller' + +class Rails::WelcomeController < Rails::ApplicationController # :nodoc: + layout false def index end diff --git a/railties/test/abstract_unit.rb b/railties/test/abstract_unit.rb index 643cc6b0ee..9ccc286b4e 100644 --- a/railties/test/abstract_unit.rb +++ b/railties/test/abstract_unit.rb @@ -14,6 +14,15 @@ require 'rails/all' module TestApp class Application < Rails::Application config.root = File.dirname(__FILE__) - config.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33' + secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33' end end + +# Skips the current run on Rubinius using Minitest::Assertions#skip +def rubinius_skip(message = '') + skip message if RUBY_ENGINE == 'rbx' +end +# Skips the current run on JRuby using Minitest::Assertions#skip +def jruby_skip(message = '') + skip message if defined?(JRUBY_VERSION) +end diff --git a/railties/test/app_rails_loader_test.rb b/railties/test/app_rails_loader_test.rb index 92cb3233d8..1d3b80253a 100644 --- a/railties/test/app_rails_loader_test.rb +++ b/railties/test/app_rails_loader_test.rb @@ -22,8 +22,14 @@ class AppRailsLoaderTest < ActiveSupport::TestCase exe = "#{script_dir}/rails" test "is not in a Rails application if #{exe} is not found in the current or parent directories" do - File.stubs(:exist?).with('bin/rails').returns(false) - File.stubs(:exist?).with('script/rails').returns(false) + File.stubs(:file?).with('bin/rails').returns(false) + File.stubs(:file?).with('script/rails').returns(false) + + assert !Rails::AppRailsLoader.exec_app_rails + end + + test "is not in a Rails application if #{exe} exists but is a folder" do + FileUtils.mkdir_p(exe) assert !Rails::AppRailsLoader.exec_app_rails end diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 722c940d8e..02d8b2c91d 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -250,7 +250,7 @@ module ApplicationTests test "Use key_generator when secret_key_base is set" do make_basic_app do |app| - app.config.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33' + app.secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33' app.config.session_store :disabled end @@ -270,27 +270,27 @@ module ApplicationTests test "application verifier can be used in the entire application" do make_basic_app do |app| - app.config.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33' + app.secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33' app.config.session_store :disabled end - message = app.message_verifier('salt').generate("some_value") + message = app.message_verifier(:sensitive_value).generate("some_value") - assert_equal 'some_value', Rails.application.message_verifier('salt').verify(message) + assert_equal 'some_value', Rails.application.message_verifier(:sensitive_value).verify(message) - secret = app.key_generator.generate_key('salt') + secret = app.key_generator.generate_key('sensitive_value') verifier = ActiveSupport::MessageVerifier.new(secret) assert_equal 'some_value', verifier.verify(message) end test "application verifier can build different verifiers" do make_basic_app do |app| - app.config.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33' + app.secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33' app.config.session_store :disabled end - default_verifier = app.message_verifier('salt') - text_verifier = app.message_verifier('text') + default_verifier = app.message_verifier(:sensitive_value) + text_verifier = app.message_verifier(:text) message = text_verifier.generate('some_value') @@ -299,10 +299,43 @@ module ApplicationTests default_verifier.verify(message) end - assert_equal default_verifier.object_id, app.message_verifier('salt').object_id + assert_equal default_verifier.object_id, app.message_verifier(:sensitive_value).object_id assert_not_equal default_verifier.object_id, text_verifier.object_id end + test "secrets.secret_key_base is used when config/secrets.yml is present" do + app_file 'config/secrets.yml', <<-YAML + development: + secret_key_base: 3b7cd727ee24e8444053437c36cc66c3 + YAML + + require "#{app_path}/config/environment" + assert_equal '3b7cd727ee24e8444053437c36cc66c3', app.secrets.secret_key_base + end + + test "secret_key_base is copied from config to secrets when not set" do + remove_file "config/secrets.yml" + app_file 'config/initializers/secret_token.rb', <<-RUBY + Rails.application.config.secret_key_base = "3b7cd727ee24e8444053437c36cc66c3" + RUBY + + require "#{app_path}/config/environment" + assert_equal '3b7cd727ee24e8444053437c36cc66c3', app.secrets.secret_key_base + end + + test "custom secrets saved in config/secrets.yml are loaded in app secrets" do + app_file 'config/secrets.yml', <<-YAML + development: + secret_key_base: 3b7cd727ee24e8444053437c36cc66c3 + aws_access_key_id: myamazonaccesskeyid + aws_secret_access_key: myamazonsecretaccesskey + YAML + + require "#{app_path}/config/environment" + assert_equal 'myamazonaccesskeyid', app.secrets.aws_access_key_id + assert_equal 'myamazonsecretaccesskey', app.secrets.aws_secret_access_key + end + test "protect from forgery is the default in a new app" do make_basic_app @@ -494,7 +527,7 @@ module ApplicationTests require "#{app_path}/config/environment" require 'action_view/base' - assert ActionView::Resolver.caching? + assert_equal true, ActionView::Resolver.caching? end test "config.action_view.cache_template_loading without cache_classes default" do @@ -502,7 +535,7 @@ module ApplicationTests require "#{app_path}/config/environment" require 'action_view/base' - assert !ActionView::Resolver.caching? + assert_equal false, ActionView::Resolver.caching? end test "config.action_view.cache_template_loading = false" do @@ -513,7 +546,7 @@ module ApplicationTests require "#{app_path}/config/environment" require 'action_view/base' - assert !ActionView::Resolver.caching? + assert_equal false, ActionView::Resolver.caching? end test "config.action_view.cache_template_loading = true" do @@ -524,7 +557,21 @@ module ApplicationTests require "#{app_path}/config/environment" require 'action_view/base' - assert ActionView::Resolver.caching? + assert_equal true, ActionView::Resolver.caching? + end + + test "config.action_view.cache_template_loading with cache_classes in an environment" do + build_app(initializers: true) + add_to_env_config "development", "config.cache_classes = false" + + # These requires are to emulate an engine loading Action View before the application + require 'action_view' + require 'action_view/railtie' + require 'action_view/base' + + require "#{app_path}/config/environment" + + assert_equal false, ActionView::Resolver.caching? end test "config.action_dispatch.show_exceptions is sent in env" do @@ -707,7 +754,7 @@ module ApplicationTests end end - test "config.log_level with custom logger" do + test "lookup config.log_level with custom logger (stdlib Logger)" do make_basic_app do |app| app.config.logger = Logger.new(STDOUT) app.config.log_level = :info @@ -715,6 +762,19 @@ module ApplicationTests assert_equal Logger::INFO, Rails.logger.level end + test "assign log_level as is with custom logger (third party logger)" do + logger_class = Class.new do + attr_accessor :level + end + logger_instance = logger_class.new + make_basic_app do |app| + app.config.logger = logger_instance + app.config.log_level = :info + end + assert_equal logger_instance, Rails.logger + assert_equal :info, Rails.logger.level + end + test "respond_to? accepts include_private" do make_basic_app diff --git a/railties/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb index 83104acf3c..3601a58f67 100644 --- a/railties/test/application/initializers/frameworks_test.rb +++ b/railties/test/application/initializers/frameworks_test.rb @@ -216,7 +216,7 @@ module ApplicationTests require "#{app_path}/config/environment" orig_database_url = ENV.delete("DATABASE_URL") orig_rails_env, Rails.env = Rails.env, 'development' - database_url_db_name = "db/database_url_db.sqlite3" + database_url_db_name = File.join(app_path, "db/database_url_db.sqlite3") ENV["DATABASE_URL"] = "sqlite3://:@localhost/#{database_url_db_name}" ActiveRecord::Base.establish_connection assert ActiveRecord::Base.connection diff --git a/railties/test/application/initializers/i18n_test.rb b/railties/test/application/initializers/i18n_test.rb index 2a64cd8ba7..bc34897cdf 100644 --- a/railties/test/application/initializers/i18n_test.rb +++ b/railties/test/application/initializers/i18n_test.rb @@ -184,14 +184,48 @@ en: assert_fallbacks ca: [:ca, :"es-ES", :es, :'en-US', :en] end - test "config.i18n.enforce_available_locales is set before config.i18n.default_locale is" do + test "config.i18n.enforce_available_locales is set to true by default and avoids I18n warnings" do add_to_config <<-RUBY config.i18n.default_locale = :it - config.i18n.enforce_available_locales = true RUBY - assert_raises(I18n::InvalidLocale) do - load_app + output = capture(:stderr) { load_app } + assert_no_match %r{deprecated.*enforce_available_locales}, output + assert_equal true, I18n.enforce_available_locales + + assert_raise I18n::InvalidLocale do + I18n.locale = :es + end + end + + test "disable config.i18n.enforce_available_locales" do + add_to_config <<-RUBY + config.i18n.enforce_available_locales = false + config.i18n.default_locale = :fr + RUBY + + output = capture(:stderr) { load_app } + assert_no_match %r{deprecated.*enforce_available_locales}, output + assert_equal false, I18n.enforce_available_locales + + assert_nothing_raised do + I18n.locale = :es + end + end + + test "default config.i18n.enforce_available_locales does not override I18n.enforce_available_locales" do + I18n.enforce_available_locales = false + + add_to_config <<-RUBY + config.i18n.default_locale = :fr + RUBY + + output = capture(:stderr) { load_app } + assert_no_match %r{deprecated.*enforce_available_locales}, output + assert_equal false, I18n.enforce_available_locales + + assert_nothing_raised do + I18n.locale = :es end end end diff --git a/railties/test/application/mailer_previews_test.rb b/railties/test/application/mailer_previews_test.rb new file mode 100644 index 0000000000..c588fd7012 --- /dev/null +++ b/railties/test/application/mailer_previews_test.rb @@ -0,0 +1,428 @@ +require 'isolation/abstract_unit' +require 'rack/test' +module ApplicationTests + class MailerPreviewsTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + include Rack::Test::Methods + + def setup + build_app + boot_rails + end + + def teardown + teardown_app + end + + test "/rails/mailers is accessible in development" do + app("development") + get "/rails/mailers" + assert_equal 200, last_response.status + end + + test "/rails/mailers is not accessible in production" do + app("production") + get "/rails/mailers" + assert_equal 404, last_response.status + end + + test "mailer previews are loaded from the default preview_path" do + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + mail to: "to@example.org" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers" + assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body + assert_match '<li><a href="/rails/mailers/notifier/foo">foo</a></li>', last_response.body + end + + test "mailer previews are loaded from a custom preview_path" do + add_to_config "config.action_mailer.preview_path = '#{app_path}/lib/mailer_previews'" + + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + mail to: "to@example.org" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + app_file 'lib/mailer_previews/notifier_preview.rb', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers" + assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body + assert_match '<li><a href="/rails/mailers/notifier/foo">foo</a></li>', last_response.body + end + + test "mailer previews are reloaded across requests" do + app('development') + + get "/rails/mailers" + assert_no_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body + + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + mail to: "to@example.org" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + get "/rails/mailers" + assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body + + remove_file 'test/mailers/previews/notifier_preview.rb' + sleep(1) + + get "/rails/mailers" + assert_no_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body + end + + test "mailer preview actions are added and removed" do + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + mail to: "to@example.org" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers" + assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body + assert_match '<li><a href="/rails/mailers/notifier/foo">foo</a></li>', last_response.body + assert_no_match '<li><a href="/rails/mailers/notifier/bar">bar</a></li>', last_response.body + + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + mail to: "to@example.org" + end + + def bar + mail to: "to@example.net" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + text_template 'notifier/bar', <<-RUBY + Goodbye, World! + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + + def bar + Notifier.bar + end + end + RUBY + + sleep(1) + + get "/rails/mailers" + assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body + assert_match '<li><a href="/rails/mailers/notifier/foo">foo</a></li>', last_response.body + assert_match '<li><a href="/rails/mailers/notifier/bar">bar</a></li>', last_response.body + + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + mail to: "to@example.org" + end + end + RUBY + + remove_file 'app/views/notifier/bar.text.erb' + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + sleep(1) + + get "/rails/mailers" + assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body + assert_match '<li><a href="/rails/mailers/notifier/foo">foo</a></li>', last_response.body + assert_no_match '<li><a href="/rails/mailers/notifier/bar">bar</a></li>', last_response.body + end + + test "mailer previews are reloaded from a custom preview_path" do + add_to_config "config.action_mailer.preview_path = '#{app_path}/lib/mailer_previews'" + + app('development') + + get "/rails/mailers" + assert_no_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body + + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + mail to: "to@example.org" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + app_file 'lib/mailer_previews/notifier_preview.rb', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + get "/rails/mailers" + assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body + + remove_file 'lib/mailer_previews/notifier_preview.rb' + sleep(1) + + get "/rails/mailers" + assert_no_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body + end + + test "mailer preview not found" do + app('development') + get "/rails/mailers/notifier" + assert last_response.not_found? + assert_match "Mailer preview 'notifier' not found", last_response.body + end + + test "mailer preview email not found" do + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + mail to: "to@example.org" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers/notifier/bar" + assert last_response.not_found? + assert_match "Email 'bar' not found in NotifierPreview", last_response.body + end + + test "mailer preview email part not found" do + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + mail to: "to@example.org" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers/notifier/foo?part=text%2Fhtml" + assert last_response.not_found? + assert_match "Email part 'text/html' not found in NotifierPreview#foo", last_response.body + end + + test "message header uses full display names" do + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "Ruby on Rails <core@rubyonrails.org>" + + def foo + mail to: "Andrew White <andyw@pixeltrix.co.uk>", + cc: "David Heinemeier Hansson <david@heinemeierhansson.com>" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers/notifier/foo" + assert_equal 200, last_response.status + assert_match "Ruby on Rails <core@rubyonrails.org>", last_response.body + assert_match "Andrew White <andyw@pixeltrix.co.uk>", last_response.body + assert_match "David Heinemeier Hansson <david@heinemeierhansson.com>", last_response.body + end + + test "part menu selects correct option" do + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + mail to: "to@example.org" + end + end + RUBY + + html_template 'notifier/foo', <<-RUBY + <p>Hello, World!</p> + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers/notifier/foo.html" + assert_equal 200, last_response.status + assert_match '<option selected value="?part=text%2Fhtml">View as HTML email</option>', last_response.body + + get "/rails/mailers/notifier/foo.txt" + assert_equal 200, last_response.status + assert_match '<option selected value="?part=text%2Fplain">View as plain-text email</option>', last_response.body + end + + private + def build_app + super + app_file "config/routes.rb", "Rails.application.routes.draw do; end" + end + + def mailer(name, contents) + app_file("app/mailers/#{name}.rb", contents) + end + + def mailer_preview(name, contents) + app_file("test/mailers/previews/#{name}_preview.rb", contents) + end + + def html_template(name, contents) + app_file("app/views/#{name}.html.erb", contents) + end + + def text_template(name, contents) + app_file("app/views/#{name}.text.erb", contents) + end + end +end diff --git a/railties/test/application/middleware/session_test.rb b/railties/test/application/middleware/session_test.rb index 14a56176f5..31a64c2f5a 100644 --- a/railties/test/application/middleware/session_test.rb +++ b/railties/test/application/middleware/session_test.rb @@ -318,7 +318,7 @@ module ApplicationTests add_to_config <<-RUBY config.secret_token = "3b7cd727ee24e8444053437c36cc66c4" - config.secret_key_base = nil + secrets.secret_key_base = nil RUBY require "#{app_path}/config/environment" diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb index 4d348c6b4b..35d9c31c1e 100644 --- a/railties/test/application/rake/dbs_test.rb +++ b/railties/test/application/rake/dbs_test.rb @@ -1,4 +1,5 @@ require "isolation/abstract_unit" +require "active_support/core_ext/string/strip" module ApplicationTests module RakeTests @@ -16,11 +17,11 @@ module ApplicationTests end def database_url_db_name - "db/database_url_db.sqlite3" + File.join(app_path, "db/database_url_db.sqlite3") end def set_database_url - ENV['DATABASE_URL'] = "sqlite3://:@localhost/#{database_url_db_name}" + ENV['DATABASE_URL'] = File.join("sqlite3://:@localhost", database_url_db_name) # ensure it's using the DATABASE_URL FileUtils.rm_rf("#{app_path}/config/database.yml") end @@ -60,7 +61,7 @@ module ApplicationTests `rails generate model book title:string; bundle exec rake db:migrate` output = `bundle exec rake db:migrate:status` - assert_match(/database:\s+\S+#{expected[:database]}/, output) + assert_match(%r{database:\s+\S*#{Regexp.escape(expected[:database])}}, output) assert_match(/up\s+\d{14}\s+Create books/, output) end end @@ -126,7 +127,7 @@ module ApplicationTests bundle exec rake db:migrate db:structure:dump` structure_dump = File.read("db/structure.sql") assert_match(/CREATE TABLE \"books\"/, structure_dump) - `bundle exec rake db:drop db:structure:load` + `bundle exec rake environment db:drop db:structure:load` assert_match(/#{expected[:database]}/, ActiveRecord::Base.connection_config[:database]) require "#{app_path}/app/models/book" @@ -153,7 +154,7 @@ module ApplicationTests `rails generate model book title:string; bundle exec rake db:migrate db:structure:dump db:test:load_structure` ActiveRecord::Base.configurations = Rails.application.config.database_configuration - ActiveRecord::Base.establish_connection 'test' + ActiveRecord::Base.establish_connection :test require "#{app_path}/app/models/book" #if structure is not loaded correctly, exception would be raised assert Book.count, 0 @@ -166,6 +167,15 @@ module ApplicationTests require "#{app_path}/config/environment" db_test_load_structure end + + test 'db:test deprecation' do + require "#{app_path}/config/environment" + Dir.chdir(app_path) do + output = `bundle exec rake db:migrate db:test:prepare 2>&1` + assert_equal "WARNING: db:test:prepare is deprecated. The Rails test helper now maintains " \ + "your test schema automatically, see the release notes for details.\n", output + end + end end end end diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index 08316d80e6..317e73245c 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -187,7 +187,7 @@ module ApplicationTests def test_scaffold_tests_pass_by_default output = Dir.chdir(app_path) do `rails generate scaffold user username:string password:string; - bundle exec rake db:migrate db:test:clone test` + bundle exec rake db:migrate test` end assert_match(/7 runs, 13 assertions, 0 failures, 0 errors/, output) @@ -197,7 +197,7 @@ module ApplicationTests def test_scaffold_with_references_columns_tests_pass_by_default output = Dir.chdir(app_path) do `rails generate scaffold LineItems product:references cart:belongs_to; - bundle exec rake db:migrate db:test:clone test` + bundle exec rake db:migrate test` end assert_match(/7 runs, 13 assertions, 0 failures, 0 errors/, output) @@ -208,7 +208,8 @@ module ApplicationTests add_to_config "config.active_record.schema_format = :sql" output = Dir.chdir(app_path) do `rails generate scaffold user username:string; - bundle exec rake db:migrate db:test:clone 2>&1 --trace` + bundle exec rake db:migrate; + bundle exec rake db:test:clone 2>&1 --trace` end assert_match(/Execute db:test:clone_structure/, output) end @@ -217,7 +218,8 @@ module ApplicationTests add_to_config "config.active_record.schema_format = :sql" output = Dir.chdir(app_path) do `rails generate scaffold user username:string; - bundle exec rake db:migrate db:test:prepare 2>&1 --trace` + bundle exec rake db:migrate; + bundle exec rake db:test:prepare 2>&1 --trace` end assert_match(/Execute db:test:load_structure/, output) end diff --git a/railties/test/application/test_test.rb b/railties/test/application/test_test.rb index 6f2f328588..a223180169 100644 --- a/railties/test/application/test_test.rb +++ b/railties/test/application/test_test.rb @@ -67,10 +67,55 @@ module ApplicationTests assert_match %r{/app/test/unit/failing_test\.rb}, output end + test "migrations" do + output = script('generate model user name:string') + version = output.match(/(\d+)_create_users\.rb/)[1] + + app_file 'test/models/user_test.rb', <<-RUBY + require 'test_helper' + + class UserTest < ActiveSupport::TestCase + test "user" do + User.create! name: "Jon" + end + end + RUBY + app_file 'db/schema.rb', '' + + assert_unsuccessful_run "models/user_test.rb", "Migrations are pending" + + app_file 'db/schema.rb', <<-RUBY + ActiveRecord::Schema.define(version: #{version}) do + create_table :users do |t| + t.string :name + end + end + RUBY + + app_file 'config/initializers/disable_maintain_test_schema.rb', <<-RUBY + Rails.application.config.active_record.maintain_test_schema = false + RUBY + + assert_unsuccessful_run "models/user_test.rb", "Could not find table 'users'" + + File.delete "#{app_path}/config/initializers/disable_maintain_test_schema.rb" + + result = assert_successful_test_run('models/user_test.rb') + assert !result.include?("create_table(:users)") + end + private + def assert_unsuccessful_run(name, message) + result = run_test_file(name) + assert_not_equal 0, $?.to_i + assert result.include?(message) + result + end + def assert_successful_test_run(name) result = run_test_file(name) assert_equal 0, $?.to_i, result + result end def run_test_file(name, options = {}) @@ -83,7 +128,7 @@ module ApplicationTests env["RUBYLIB"] = $:.join(':') Dir.chdir(app_path) do - `#{env_string(env)} #{Gem.ruby} #{args.join(' ')}` + `#{env_string(env)} #{Gem.ruby} #{args.join(' ')} 2>&1` end end diff --git a/railties/test/commands/dbconsole_test.rb b/railties/test/commands/dbconsole_test.rb index edb92b3aa2..24db395e6e 100644 --- a/railties/test/commands/dbconsole_test.rb +++ b/railties/test/commands/dbconsole_test.rb @@ -2,29 +2,74 @@ require 'abstract_unit' require 'rails/commands/dbconsole' class Rails::DBConsoleTest < ActiveSupport::TestCase - def teardown - %w[PGUSER PGHOST PGPORT PGPASSWORD].each{|key| ENV.delete(key)} - end - def test_config - Rails::DBConsole.const_set(:APP_PATH, "erb") - - app_config({}) - capture_abort { Rails::DBConsole.new.config } - assert aborted - assert_match(/No database is configured for the environment '\w+'/, output) - app_config(test: "with_init") - assert_equal Rails::DBConsole.new.config, "with_init" - - app_db_file("test:\n without_init") - assert_equal Rails::DBConsole.new.config, "without_init" - - app_db_file("test:\n <%= Rails.something_app_specific %>") - assert_equal Rails::DBConsole.new.config, "with_init" + def setup + Rails::DBConsole.const_set('APP_PATH', 'rails/all') + end - app_db_file("test:\n\ninvalid") - assert_equal Rails::DBConsole.new.config, "with_init" + def teardown + Rails::DBConsole.send(:remove_const, 'APP_PATH') + %w[PGUSER PGHOST PGPORT PGPASSWORD DATABASE_URL].each{|key| ENV.delete(key)} + end + + def test_config_with_db_config_only + config_sample = { + "test"=> { + "adapter"=> "sqlite3", + "host"=> "localhost", + "port"=> "9000", + "database"=> "foo_test", + "user"=> "foo", + "password"=> "bar", + "pool"=> "5", + "timeout"=> "3000" + } + } + app_db_config(config_sample) + assert_equal Rails::DBConsole.new.config, config_sample["test"] + end + + def test_config_with_no_db_config + app_db_config(nil) + assert_raise(ActiveRecord::AdapterNotSpecified) { + Rails::DBConsole.new.config + } + end + + def test_config_with_database_url_only + ENV['DATABASE_URL'] = 'postgresql://foo:bar@localhost:9000/foo_test?pool=5&timeout=3000' + app_db_config(nil) + expected = { + "adapter" => "postgresql", + "host" => "localhost", + "port" => 9000, + "database" => "foo_test", + "username" => "foo", + "password" => "bar", + "pool" => "5", + "timeout" => "3000" + }.sort + assert_equal expected, Rails::DBConsole.new.config.sort + end + + def test_config_choose_database_url_if_exists + host = "database-url-host.com" + ENV['DATABASE_URL'] = "postgresql://foo:bar@#{host}:9000/foo_test?pool=5&timeout=3000" + sample_config = { + "test" => { + "adapter" => "postgresql", + "host" => "not-the-#{host}", + "port" => 9000, + "database" => "foo_test", + "username" => "foo", + "password" => "bar", + "pool" => "5", + "timeout" => "3000" + } + } + app_db_config(sample_config) + assert_equal host, Rails::DBConsole.new.config["host"] end def test_env @@ -177,6 +222,10 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase private + def app_db_config(results) + Rails.application.config.stubs(:database_configuration).returns(results || {}) + end + def dbconsole @dbconsole ||= Rails::DBConsole.new(nil) end @@ -197,11 +246,4 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase end end - def app_db_file(result) - IO.stubs(:read).with("config/database.yml").returns(result) - end - - def app_config(result) - Rails.application.config.stubs(:database_configuration).returns(result.stringify_keys) - end end diff --git a/railties/test/configuration/middleware_stack_proxy_test.rb b/railties/test/configuration/middleware_stack_proxy_test.rb index 2442cb995d..6f3e45f320 100644 --- a/railties/test/configuration/middleware_stack_proxy_test.rb +++ b/railties/test/configuration/middleware_stack_proxy_test.rb @@ -39,7 +39,7 @@ module Rails @stack.swap :foo @stack.delete :foo - mock = MiniTest::Mock.new + mock = Minitest::Mock.new mock.expect :send, nil, [:swap, :foo] mock.expect :send, nil, [:delete, :foo] @@ -50,7 +50,7 @@ module Rails private def assert_playback(msg_name, args) - mock = MiniTest::Mock.new + mock = Minitest::Mock.new mock.expect :send, nil, [msg_name, args] @stack.merge_into(mock) mock.verify diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 7c2040470f..8aa306c8e0 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -58,8 +58,8 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_assets run_generator - assert_file("app/views/layouts/application.html.erb", /stylesheet_link_tag\s+"application", media: "all", "data-turbolinks-track" => true/) - assert_file("app/views/layouts/application.html.erb", /javascript_include_tag\s+"application", "data-turbolinks-track" => true/) + assert_file("app/views/layouts/application.html.erb", /stylesheet_link_tag\s+'application', media: 'all', 'data-turbolinks-track' => true/) + assert_file("app/views/layouts/application.html.erb", /javascript_include_tag\s+'application', 'data-turbolinks-track' => true/) assert_file("app/assets/stylesheets/application.css") assert_file("app/assets/javascripts/application.js") end @@ -163,56 +163,71 @@ class AppGeneratorTest < Rails::Generators::TestCase end end - def test_add_gemfile_entry - template = Tempfile.open 'my_template' - template.puts 'gemfile_entry "tenderlove"' - template.flush + def test_arbitrary_code + output = Tempfile.open('my_template') do |template| + template.puts 'puts "You are using Rails version #{Rails::VERSION::STRING}."' + template.close + run_generator([destination_root, "-m", template.path]) + end + assert_match 'You are using', output + end - run_generator([destination_root, "-m", template.path]) - assert_file "Gemfile", /tenderlove/ - ensure - template.close - template.unlink + def test_add_gemfile_entry + Tempfile.open('my_template') do |template| + template.puts 'gemfile_entry "tenderlove"' + template.flush + template.close + run_generator([destination_root, "-n", template.path]) + assert_file "Gemfile", /tenderlove/ + end end def test_add_skip_entry - template = Tempfile.open 'my_template' - template.puts 'add_gem_entry_filter { |gem| gem.name != "jbuilder" }' - template.flush + Tempfile.open 'my_template' do |template| + template.puts 'add_gem_entry_filter { |gem| gem.name != "jbuilder" }' + template.close - run_generator([destination_root, "-m", template.path]) - assert_file "Gemfile" do |contents| - assert_no_match 'jbuilder', contents + run_generator([destination_root, "-n", template.path]) + assert_file "Gemfile" do |contents| + assert_no_match 'jbuilder', contents + end end - ensure - template.close - template.unlink end - def test_skip_turbolinks_when_it_is_not_on_gemfile - template = Tempfile.open 'my_template' - template.puts 'add_gem_entry_filter { |gem| gem.name != "turbolinks" }' - template.flush + def test_remove_gem + Tempfile.open 'my_template' do |template| + template.puts 'remove_gem "jbuilder"' + template.close - run_generator([destination_root, "-m", template.path]) - assert_file "Gemfile" do |contents| - assert_no_match 'turbolinks', contents + run_generator([destination_root, "-n", template.path]) + assert_file "Gemfile" do |contents| + assert_no_match 'jbuilder', contents + end end + end - assert_file "app/views/layouts/application.html.erb" do |contents| - assert_no_match 'turbolinks', contents - end + def test_skip_turbolinks_when_it_is_not_on_gemfile + Tempfile.open 'my_template' do |template| + template.puts 'add_gem_entry_filter { |gem| gem.name != "turbolinks" }' + template.flush - assert_file "app/views/layouts/application.html.erb" do |contents| - assert_no_match('data-turbolinks-track', contents) - end + run_generator([destination_root, "-n", template.path]) + assert_file "Gemfile" do |contents| + assert_no_match 'turbolinks', contents + end - assert_file "app/assets/javascripts/application.js" do |contents| - assert_no_match 'turbolinks', contents + assert_file "app/views/layouts/application.html.erb" do |contents| + assert_no_match 'turbolinks', contents + end + + assert_file "app/views/layouts/application.html.erb" do |contents| + assert_no_match('data-turbolinks-track', contents) + end + + assert_file "app/assets/javascripts/application.js" do |contents| + assert_no_match 'turbolinks', contents + end end - ensure - template.close - template.unlink end def test_config_another_database @@ -349,8 +364,8 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_no_file "vendor/assets/javascripts" assert_file "app/views/layouts/application.html.erb" do |contents| - assert_match(/stylesheet_link_tag\s+"application", media: "all" %>/, contents) - assert_no_match(/javascript_include_tag\s+"application" \%>/, contents) + assert_match(/stylesheet_link_tag\s+'application', media: 'all' %>/, contents) + assert_no_match(/javascript_include_tag\s+'application' \%>/, contents) end assert_file "Gemfile" do |content| @@ -375,9 +390,9 @@ class AppGeneratorTest < Rails::Generators::TestCase end end - def test_inclusion_of_lazy_loaded_sdoc + def test_inclusion_of_doc run_generator - assert_file 'Gemfile', /gem 'sdoc', \s+group: :doc, require: false/ + assert_file 'Gemfile', /gem 'sdoc',\s+'~> 0.4.0',\s+group: :doc/ end def test_template_from_dir_pwd @@ -418,7 +433,7 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_new_hash_style run_generator [destination_root] assert_file "config/initializers/session_store.rb" do |file| - assert_match(/config.session_store :cookie_store, key: '_.+_session'/, file) + assert_match(/config.session_store :cookie_store, key: '_.+_session', serializer: :json_serializer/, file) end end @@ -439,16 +454,18 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_spring run_generator - assert_file "Gemfile", /gem 'spring'/ + assert_file "Gemfile", /gem 'spring', \s+group: :development/ end def test_spring_binstubs + jruby_skip "spring doesn't run on JRuby" generator.stubs(:bundle_command).with('install') generator.expects(:bundle_command).with('exec spring binstub --all').once quietly { generator.invoke_all } end def test_spring_no_fork + jruby_skip "spring doesn't run on JRuby" Process.stubs(:respond_to?).with(:fork).returns(false) run_generator diff --git a/railties/test/generators/controller_generator_test.rb b/railties/test/generators/controller_generator_test.rb index 2268f04839..4b2f8539d0 100644 --- a/railties/test/generators/controller_generator_test.rb +++ b/railties/test/generators/controller_generator_test.rb @@ -67,7 +67,7 @@ class ControllerGeneratorTest < Rails::Generators::TestCase def test_add_routes run_generator - assert_file "config/routes.rb", /get "account\/foo"/, /get "account\/bar"/ + assert_file "config/routes.rb", /get 'account\/foo'/, /get 'account\/bar'/ end def test_invokes_default_template_engine_even_with_no_action @@ -91,6 +91,6 @@ class ControllerGeneratorTest < Rails::Generators::TestCase def test_namespaced_routes_are_created_in_routes run_generator ["admin/dashboard", "index"] - assert_file "config/routes.rb", /namespace :admin do\n\s+get "dashboard\/index"\n/ + assert_file "config/routes.rb", /namespace :admin do\n\s+get 'dashboard\/index'\n/ end end diff --git a/railties/test/generators/generators_test_helper.rb b/railties/test/generators/generators_test_helper.rb index 32a3d072c9..77ec2f1c0c 100644 --- a/railties/test/generators/generators_test_helper.rb +++ b/railties/test/generators/generators_test_helper.rb @@ -1,10 +1,14 @@ require 'abstract_unit' +require 'active_support/core_ext/module/remove_method' require 'rails/generators' require 'rails/generators/test_case' module Rails - def self.root - @root ||= File.expand_path(File.join(File.dirname(__FILE__), '..', 'fixtures')) + class << self + remove_possible_method :root + def root + @root ||= File.expand_path(File.join(File.dirname(__FILE__), '..', 'fixtures')) + end end end Rails.application.config.root = Rails.root diff --git a/railties/test/generators/mailer_generator_test.rb b/railties/test/generators/mailer_generator_test.rb index 6b2351fc1a..d209801f60 100644 --- a/railties/test/generators/mailer_generator_test.rb +++ b/railties/test/generators/mailer_generator_test.rb @@ -1,7 +1,6 @@ require 'generators/generators_test_helper' require 'rails/generators/mailer/mailer_generator' - class MailerGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper arguments %w(notifier foo bar) @@ -23,8 +22,11 @@ class MailerGeneratorTest < Rails::Generators::TestCase end def test_check_class_collision - content = capture(:stderr){ run_generator ["object"] } - assert_match(/The name 'Object' is either already used in your application or reserved/, content) + Object.send :const_set, :Notifier, Class.new + content = capture(:stderr){ run_generator } + assert_match(/The name 'Notifier' is either already used in your application or reserved/, content) + ensure + Object.send :remove_const, :Notifier end def test_invokes_default_test_framework @@ -34,9 +36,37 @@ class MailerGeneratorTest < Rails::Generators::TestCase assert_match(/test "foo"/, test) assert_match(/test "bar"/, test) end + assert_file "test/mailers/previews/notifier_preview.rb" do |preview| + assert_match(/\# Preview all emails at http:\/\/localhost\:3000\/rails\/mailers\/notifier/, preview) + assert_match(/class NotifierPreview < ActionMailer::Preview/, preview) + assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/notifier\/foo/, preview) + assert_instance_method :foo, preview do |foo| + assert_match(/Notifier.foo/, foo) + end + assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/notifier\/bar/, preview) + assert_instance_method :bar, preview do |bar| + assert_match(/Notifier.bar/, bar) + end + end end - def test_invokes_default_template_engine + def test_check_test_class_collision + Object.send :const_set, :NotifierTest, Class.new + content = capture(:stderr){ run_generator } + assert_match(/The name 'NotifierTest' is either already used in your application or reserved/, content) + ensure + Object.send :remove_const, :NotifierTest + end + + def test_check_preview_class_collision + Object.send :const_set, :NotifierPreview, Class.new + content = capture(:stderr){ run_generator } + assert_match(/The name 'NotifierPreview' is either already used in your application or reserved/, content) + ensure + Object.send :remove_const, :NotifierPreview + end + + def test_invokes_default_text_template_engine run_generator assert_file "app/views/notifier/foo.text.erb" do |view| assert_match(%r(app/views/notifier/foo\.text\.erb), view) @@ -49,6 +79,19 @@ class MailerGeneratorTest < Rails::Generators::TestCase end end + def test_invokes_default_html_template_engine + run_generator + assert_file "app/views/notifier/foo.html.erb" do |view| + assert_match(%r(app/views/notifier/foo\.html\.erb), view) + assert_match(/<%= @greeting %>/, view) + end + + assert_file "app/views/notifier/bar.html.erb" do |view| + assert_match(%r(app/views/notifier/bar\.html\.erb), view) + assert_match(/<%= @greeting %>/, view) + end + end + def test_invokes_default_template_engine_even_with_no_action run_generator ["notifier"] assert_file "app/views/notifier" @@ -65,7 +108,13 @@ class MailerGeneratorTest < Rails::Generators::TestCase assert_match(/class Farm::Animal < ActionMailer::Base/, mailer) assert_match(/en\.farm\.animal\.moos\.subject/, mailer) end + assert_file "test/mailers/previews/farm/animal_preview.rb" do |preview| + assert_match(/\# Preview all emails at http:\/\/localhost\:3000\/rails\/mailers\/farm\/animal/, preview) + assert_match(/class Farm::AnimalPreview < ActionMailer::Preview/, preview) + assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/farm\/animal\/moos/, preview) + end assert_file "app/views/farm/animal/moos.text.erb" + assert_file "app/views/farm/animal/moos.html.erb" end def test_actions_are_turned_into_methods diff --git a/railties/test/generators/namespaced_generators_test.rb b/railties/test/generators/namespaced_generators_test.rb index e17925ff65..d677c21f15 100644 --- a/railties/test/generators/namespaced_generators_test.rb +++ b/railties/test/generators/namespaced_generators_test.rb @@ -63,7 +63,7 @@ class NamespacedControllerGeneratorTest < NamespacedGeneratorTestCase def test_routes_should_not_be_namespaced run_generator - assert_file "config/routes.rb", /get "account\/foo"/, /get "account\/bar"/ + assert_file "config/routes.rb", /get 'account\/foo'/, /get 'account\/bar'/ end def test_invokes_default_template_engine_even_with_no_action diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index 7319a4e9d3..932cd75bcb 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -35,6 +35,12 @@ class PluginGeneratorTest < Rails::Generators::TestCase content = capture(:stderr){ run_generator [File.join(destination_root, "43things")] } assert_equal "Invalid plugin name 43things. Please give a name which does not start with numbers.\n", content + + content = capture(:stderr){ run_generator [File.join(destination_root, "plugin")] } + assert_equal "Invalid plugin name plugin. Please give a name which does not match one of the reserved rails words.\n", content + + content = capture(:stderr){ run_generator [File.join(destination_root, "Digest")] } + assert_equal "Invalid plugin name Digest, constant Digest is already in use. Please choose another plugin name.\n", content end def test_camelcase_plugin_name_underscores_filenames @@ -58,6 +64,17 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "test/integration/navigation_test.rb", /ActionDispatch::IntegrationTest/ end + def test_inclusion_of_debugger + run_generator [destination_root, '--full'] + if defined?(JRUBY_VERSION) + assert_file "Gemfile" do |content| + assert_no_match(/debugger/, content) + end + else + assert_file "Gemfile", /# gem 'debugger'/ + end + end + def test_generating_test_files_in_full_mode_without_unit_test_files run_generator [destination_root, "-T", "--full"] diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 913e2b5e29..6c50911666 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -93,7 +93,8 @@ module TestHelpers # Build an application by invoking the generator and going through the whole stack. def build_app(options = {}) @prev_rails_env = ENV['RAILS_ENV'] - ENV['RAILS_ENV'] = 'development' + ENV['RAILS_ENV'] = "development" + ENV['SECRET_KEY_BASE'] ||= SecureRandom.hex(16) FileUtils.rm_rf(app_path) FileUtils.cp_r(app_template_path, app_path) @@ -117,9 +118,26 @@ module TestHelpers end end + File.open("#{app_path}/config/database.yml", "w") do |f| + f.puts <<-YAML + default: &default + adapter: sqlite3 + pool: 5 + timeout: 5000 + development: + <<: *default + database: db/development.sqlite3 + test: + <<: *default + database: db/test.sqlite3 + production: + <<: *default + database: db/production.sqlite3 + YAML + end + add_to_config <<-RUBY config.eager_load = false - config.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4" config.session_store :cookie_store, key: "_myapp_session" config.active_support.deprecation = :log config.action_controller.allow_forgery_protection = false @@ -139,7 +157,7 @@ module TestHelpers app = Class.new(Rails::Application) app.config.eager_load = false - app.config.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4" + app.secrets.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4" app.config.session_store :cookie_store, key: "_myapp_session" app.config.active_support.deprecation = :log diff --git a/tasks/release.rb b/tasks/release.rb index 66da67bfd0..439a9e0c05 100644 --- a/tasks/release.rb +++ b/tasks/release.rb @@ -1,4 +1,4 @@ -FRAMEWORKS = %w( activesupport activemodel activerecord actionpack actionview actionmailer railties ) +FRAMEWORKS = %w( activesupport activemodel activerecord actionview actionpack actionmailer railties ) root = File.expand_path('../../', __FILE__) version = File.read("#{root}/RAILS_VERSION").strip @@ -120,7 +120,7 @@ namespace :all do end task :tag do - sh "git tag #{tag}" + sh "git tag -m '#{tag} release' #{tag}" sh "git push --tags" end diff --git a/version.rb b/version.rb index 5a6d8d0983..923cab4e2a 100644 --- a/version.rb +++ b/version.rb @@ -3,7 +3,7 @@ module Rails MAJOR = 4 MINOR = 1 TINY = 0 - PRE = "beta" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end |