diff options
author | Aaron Patterson <aaron.patterson@gmail.com> | 2014-02-17 11:21:18 -0800 |
---|---|---|
committer | Aaron Patterson <aaron.patterson@gmail.com> | 2014-02-17 11:21:18 -0800 |
commit | fe42effb11a97cf19777d7b0dba7e1e2dfd3316c (patch) | |
tree | 388f48bc682802cbcae53a0d570d2c8587bbb98b | |
parent | 5ac2879b08b05b7f6eaebc5473e62b4576f84a3f (diff) | |
parent | 3e3ed1ede51f4d2f7f1d30b3754072b1121d5394 (diff) | |
download | rails-fe42effb11a97cf19777d7b0dba7e1e2dfd3316c.tar.gz rails-fe42effb11a97cf19777d7b0dba7e1e2dfd3316c.tar.bz2 rails-fe42effb11a97cf19777d7b0dba7e1e2dfd3316c.zip |
Merge branch 'master' into adequaterecord
* master: (311 commits)
Add a missing changelog entry for #13981 and #14035
Revert "Fixed plugin_generator test"
implements new option :month_format_string for date select helpers [Closes #13618]
add factory methods for empty alias trackers
guarantee a list in the alias tracker so we can remove a conditional
stop exposing table_joins
make most parameters to the AliasTracker required
make a singleton for AssociationScope
pass the association and connection to the scope method
pass the tracker down the stack and construct it in the scope method
clean up add_constraints signature
remove the reflection delegate
remove klass delegator
remove railties changes. fixes #14054
remove chain delegate
remove scope_chain delegate
Add verb to sanitization note
fix path shown in mailer's templates
updated Travis build status image url
fix guide active_support_core_extensions. add Note to String#indent [ci skip]
...
Conflicts:
activerecord/lib/active_record/associations/join_dependency.rb
activerecord/test/cases/associations/association_scope_test.rb
300 files changed, 4974 insertions, 1660 deletions
diff --git a/.travis.yml b/.travis.yml index 3ddaf86fb2..4233b136a8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ rvm: - 1.9.3 - 2.0.0 - 2.1.0 - - rbx + - rbx-2 - jruby env: - "GEM=railties" @@ -17,7 +17,7 @@ env: - "GEM=ar:postgresql" matrix: allow_failures: - - rvm: rbx + - rvm: rbx-2 - rvm: jruby fast_finish: true notifications: @@ -9,7 +9,7 @@ gem 'mocha', '~> 0.14', require: false gem 'rack-cache', '~> 1.2' gem 'bcrypt-ruby', '~> 3.1.2' -gem 'jquery-rails', '~> 2.2.0' +gem 'jquery-rails', '~> 3.1.0' gem 'turbolinks' gem 'coffee-rails', '~> 4.0.0' gem 'arel', github: 'rails/arel' @@ -77,11 +77,6 @@ platforms :jruby do end end -platforms :rbx do - gem 'psych', '~> 2.0' - gem 'rubysl', '~> 2.0' -end - # gems that are necessary for ActiveRecord tests with Oracle database if ENV['ORACLE_ENHANCED'] platforms :ruby do @@ -76,8 +76,7 @@ We encourage you to contribute to Ruby on Rails! Please check out the ## Code Status -* [![Build Status](https://api.travis-ci.org/rails/rails.png)](https://travis-ci.org/rails/rails) -* [![Dependencies](https://gemnasium.com/rails/rails.png?travis)](https://gemnasium.com/rails/rails) +* [![Build Status](https://travis-ci.org/rails/rails.png?branch=master)](https://travis-ci.org/rails/rails) ## License diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index 1867a392eb..5a61746700 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -1,4 +1,26 @@ -* Add mailer previews feature based on 37 Signals mail_view gem +* 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. + + Fixes #13622. + + Example: + + class CSSInlineStyler + def self.previewing_email(message) + # inline CSS styles + end + end + + ActionMailer::Base.register_preview_interceptor CSSInlineStyler + + *Andrew White* + +* Add mailer previews feature based on 37 Signals mail_view gem. *Andrew White* diff --git a/actionmailer/README.rdoc b/actionmailer/README.rdoc index c3dcd3c3e4..e425282fa8 100644 --- a/actionmailer/README.rdoc +++ b/actionmailer/README.rdoc @@ -74,7 +74,7 @@ Or you can just chain the methods together like: == Setting defaults -It is possible to set default values that will be used in every method in your Action Mailer class. To implement this functionality, you just call the public class method <tt>default</tt> which you get for free from <tt>ActionMailer::Base</tt>. This method accepts a Hash as the parameter. You can use any of the headers e-mail messages has, like <tt>:from</tt> as the key. You can also pass in a string as the key, like "Content-Type", but Action Mailer does this out of the box for you, so you won't need to worry about that. Finally, it is also possible to pass in a Proc that will get evaluated when it is needed. +It is possible to set default values that will be used in every method in your Action Mailer class. To implement this functionality, you just call the public class method <tt>default</tt> which you get for free from <tt>ActionMailer::Base</tt>. This method accepts a Hash as the parameter. You can use any of the headers email messages have, like <tt>:from</tt> as the key. You can also pass in a string as the key, like "Content-Type", but Action Mailer does this out of the box for you, so you won't need to worry about that. Finally, it is also possible to pass in a Proc that will get evaluated when it is needed. Note that every value you set with this method will get overwritten if you use the same key in your mailer method. diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index a30e3e65da..eb8cca9ee4 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -51,7 +51,7 @@ 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 <tt>Mail::Message</tt> - # will accept (any valid Email header including optional fields). + # 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 @@ -330,6 +330,21 @@ module ActionMailer # 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 @@ -429,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 <tt>constantize</tt>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 @@ -737,7 +764,7 @@ module ActionMailer m.charset = charset = headers[:charset] # Set configure delivery behavior - wrap_delivery_behavior!(headers.delete(:delivery_method),headers.delete(:delivery_method_options)) + wrap_delivery_behavior!(headers.delete(:delivery_method), headers.delete(:delivery_method_options)) # Assign all headers except parts_order, content_type and body assignable = headers.except(:parts_order, :content_type, :body, :template_name, :template_path) diff --git a/actionmailer/lib/action_mailer/preview.rb b/actionmailer/lib/action_mailer/preview.rb index ecceaf8c70..fde0e86f5b 100644 --- a/actionmailer/lib/action_mailer/preview.rb +++ b/actionmailer/lib/action_mailer/preview.rb @@ -9,7 +9,32 @@ module ActionMailer # # config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews" # - class_attribute :preview_path, instance_writer: false + 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 @@ -23,10 +48,14 @@ module ActionMailer descendants end - # Returns the mail object for the given email name + # 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 - preview.public_send(email) + message = preview.public_send(email) + inform_preview_interceptors(message) + message end # Returns all of the available email previews @@ -56,7 +85,7 @@ module ActionMailer protected def load_previews #:nodoc: - if preview_path? + if preview_path Dir["#{preview_path}/**/*_preview.rb"].each{ |file| require_dependency file } end end @@ -65,8 +94,10 @@ module ActionMailer Base.preview_path end - def preview_path? #:nodoc: - Base.preview_path? + def inform_preview_interceptors(message) #:nodoc: + Base.preview_interceptors.each do |interceptor| + interceptor.previewing_email(message) + end end end end diff --git a/actionmailer/lib/action_mailer/railtie.rb b/actionmailer/lib/action_mailer/railtie.rb index c893ddfef5..8d1e40297b 100644 --- a/actionmailer/lib/action_mailer/railtie.rb +++ b/actionmailer/lib/action_mailer/railtie.rb @@ -46,7 +46,7 @@ module ActionMailer end config.after_initialize do - if ActionMailer::Base.preview_path? + if ActionMailer::Base.preview_path ActiveSupport::Dependencies.autoload_paths << ActionMailer::Base.preview_path end 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/actionmailer/test/delivery_methods_test.rb b/actionmailer/test/delivery_methods_test.rb index 20412c7bb2..609903620b 100644 --- a/actionmailer/test/delivery_methods_test.rb +++ b/actionmailer/test/delivery_methods_test.rb @@ -38,8 +38,10 @@ class DefaultsDeliveryMethodsTest < ActiveSupport::TestCase end test "default sendmail settings" do - settings = {location: '/usr/sbin/sendmail', - arguments: '-i -t'} + settings = { + location: '/usr/sbin/sendmail', + arguments: '-i -t' + } assert_equal settings, ActionMailer::Base.sendmail_settings end end @@ -138,13 +140,15 @@ class MailDeliveryTest < ActiveSupport::TestCase end test "default delivery options can be overridden per mail instance" do - settings = { address: "localhost", - port: 25, - domain: 'localhost.localdomain', - user_name: nil, - password: nil, - authentication: nil, - enable_starttls_auto: true } + settings = { + address: "localhost", + port: 25, + domain: 'localhost.localdomain', + user_name: nil, + password: nil, + authentication: nil, + enable_starttls_auto: true + } assert_equal settings, ActionMailer::Base.smtp_settings overridden_options = {user_name: "overridden", password: "somethingobtuse"} mail_instance = DeliveryMailer.welcome(delivery_method_options: overridden_options) @@ -164,6 +168,13 @@ class MailDeliveryTest < ActiveSupport::TestCase end end + test "undefined delivery methods raises errors" do + DeliveryMailer.delivery_method = nil + assert_raise RuntimeError do + DeliveryMailer.welcome.deliver + end + end + test "does not perform deliveries if requested" do DeliveryMailer.perform_deliveries = false DeliveryMailer.deliveries.clear diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 24dc207656..342f670e78 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,84 @@ +* Add new config option `config.action_dispatch.cookies_serializer` for + specifying a serializer for the signed and encrypted cookie jars. + + The possible values are: + + * `:json` - serialize cookie values with `JSON` + * `:marshal` - serialize cookie values with `Marshal` + * `:hybrid` - transparently migrate existing `Marshal` cookie values to `JSON` + + For new apps `:json` option is added by default and `:marshal` is used + when no option is specified to maintain backwards compatibility. + + *Łukasz Sarnacki*, *Matt Aimonetti*, *Guillermo Iguaran*, *Godfrey Chan*, *Rafael Mendonça França* + +* `FlashHash` now behaves like a `HashWithIndifferentAccess`. + + *Guillermo Iguaran* + +* Set the `:shallow_path` scope option as each scope is generated rather than + waiting until the `shallow` option is set. Also make the behavior of the + `:shallow` resource option consistent with the behavior of the `shallow` method. + + Fixes #12498. + + *Andrew White*, *Aleksi Aalto* + +* Properly require `action_view` in `AbstractController::Rendering` to prevent + uninitialized constant error for `ENCODING_FLAG`. + + *Philipe Fatio* + +* Do not discard query parameters that form a hash with the same root key as + the `wrapper_key` for a request using `wrap_parameters`. + + *Josh Jordan* + +* 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 @@ -26,7 +107,7 @@ *Andrew White* -* Show full route constraints in error message +* 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 diff --git a/actionpack/RUNNING_UNIT_TESTS.rdoc b/actionpack/RUNNING_UNIT_TESTS.rdoc index 08767ae133..ad1448f61b 100644 --- a/actionpack/RUNNING_UNIT_TESTS.rdoc +++ b/actionpack/RUNNING_UNIT_TESTS.rdoc @@ -1,10 +1,10 @@ == Running with Rake The easiest way to run the unit tests is through Rake. The default task runs -the entire test suite for all classes. For more information, checkout the -full array of rake tasks with "rake -T" +the entire test suite for all classes. For more information, check out the +full array of rake tasks with "rake -T". -Rake can be found at http://rake.rubyforge.org +Rake can be found at http://rake.rubyforge.org. == Running by hand diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index 7be61d94c9..f24b03ad16 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' require 'action_view/view_paths' require 'set' 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/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index d5e08b7034..1974bbf529 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -236,6 +236,18 @@ module ActionController #:nodoc: # end # end # + # You can also set an array of variants: + # + # request.variant = [:tablet, :phone] + # + # which will work similarly to formats and MIME types negotiation. If there will be no + # :tablet variant declared, :phone variant will be picked: + # + # respond_to do |format| + # format.html.none + # format.html.phone # this gets rendered + # 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) @@ -488,7 +500,7 @@ module ActionController #:nodoc: response else # `format.html{ |variant| variant.phone }` - variant block syntax variant_collector = VariantCollector.new(@variant) - response.call(variant_collector) #call format block with variants collector + response.call(variant_collector) # call format block with variants collector variant_collector.variant end end @@ -519,15 +531,15 @@ module ActionController #:nodoc: end def variant - key = if @variant.nil? - :none - elsif @variants.has_key?(@variant) - @variant + if @variant.nil? + @variants[:none] || @variants[:any] + elsif (@variants.keys & @variant).any? + @variant.each do |v| + return @variants[v] if @variants.key?(v) + end else - :any + @variants[:any] end - - @variants[key] end end end diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index c9f1d8dcb4..2ca8955741 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -231,7 +231,12 @@ module ActionController # by the metal call stack. def process_action(*args) if _wrapper_enabled? - wrapped_hash = _wrap_parameters request.request_parameters + if request.parameters[_wrapper_key].present? + wrapped_hash = _extract_parameters(request.parameters) + else + wrapped_hash = _wrap_parameters request.request_parameters + end + wrapped_keys = request.request_parameters.keys wrapped_filtered_hash = _wrap_parameters request.filtered_parameters.slice(*wrapped_keys) @@ -259,14 +264,16 @@ module ActionController # Returns the list of parameters which will be selected for wrapped. def _wrap_parameters(parameters) - value = if include_only = _wrapper_options.include + { _wrapper_key => _extract_parameters(parameters) } + end + + def _extract_parameters(parameters) + if include_only = _wrapper_options.include parameters.slice(*include_only) else exclude = _wrapper_options.exclude || [] parameters.except(*(exclude + EXCLUDE_PARAMETERS)) end - - { _wrapper_key => value } end # Checks if we should perform parameters wrapping. 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 920e651b08..3dd2e2a45c 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -74,18 +74,16 @@ module ActionDispatch autoload :MimeNegotiation autoload :Parameters autoload :ParameterFilter - autoload :FilterParameters - autoload :FilterRedirect autoload :Upload autoload :UploadedFile, 'action_dispatch/http/upload' autoload :URL 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' end mattr_accessor :test_app diff --git a/actionpack/lib/action_dispatch/http/filter_redirect.rb b/actionpack/lib/action_dispatch/http/filter_redirect.rb index 900ce1c646..cd603649c3 100644 --- a/actionpack/lib/action_dispatch/http/filter_redirect.rb +++ b/actionpack/lib/action_dispatch/http/filter_redirect.rb @@ -5,7 +5,8 @@ module ActionDispatch FILTERED = '[FILTERED]'.freeze # :nodoc: def filtered_location - if !location_filter.empty? && location_filter_match? + filters = location_filter + if !filters.empty? && location_filter_match?(filters) FILTERED else location @@ -15,15 +16,15 @@ module ActionDispatch private def location_filter - if request.present? + if request request.env['action_dispatch.redirect_filter'] || [] else [] end end - def location_filter_match? - location_filter.any? do |filter| + def location_filter_match?(filters) + filters.any? do |filter| if String === filter location.include?(filter) elsif Regexp === filter diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index c33ba201e1..b803ce8b6f 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -68,10 +68,12 @@ module ActionDispatch # Sets the \variant for template. def variant=(variant) - if variant.is_a? Symbol + if variant.is_a?(Symbol) + @variant = [variant] + elsif variant.is_a?(Array) && variant.any? && variant.all?{ |v| v.is_a?(Symbol) } @variant = variant else - raise ArgumentError, "request.variant must be set to a Symbol, not a #{variant.class}. " \ + raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols, not a #{variant.class}. " \ "For security reasons, never directly set the variant to a user-provided value, " \ "like params[:variant].to_sym. Check user-provided value against a whitelist first, " \ "then set the variant: request.variant = :tablet if params[:variant] == 'tablet'" diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 7b2655b2d8..2c6bcf7b7b 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/module/attribute_accessors' +require 'action_dispatch/http/filter_redirect' require 'monitor' module ActionDispatch # :nodoc: @@ -312,7 +313,7 @@ module ActionDispatch # :nodoc: header.delete CONTENT_TYPE [status, header, []] else - [status, header, self] + [status, header, Rack::BodyProxy.new(self){}] end end end diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index fe110d7938..18e64704f6 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -23,15 +23,15 @@ module ActionDispatch # # This cookie will be deleted when the user's browser is closed. # cookies[:user_name] = "david" # - # # Assign an array of values to a cookie. - # cookies[:lat_lon] = [47.68, -122.37] + # # Cookie values are String based. Other data types need to be serialized. + # cookies[:lat_lon] = JSON.generate([47.68, -122.37]) # # # Sets a cookie that expires in 1 hour. # 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>secrets.secret_key_base</tt> value. - # # It can be read using the signed method <tt>cookies.signed[:name]</tt> + # # The cookie is signed by your app's `secrets.secret_key_base` value. + # # It can be read using the signed method `cookies.signed[:name]` # cookies.signed[:user_id] = current_user.id # # # Sets a "permanent" cookie (which expires in 20 years from now). @@ -42,10 +42,10 @@ module ActionDispatch # # Examples of reading: # - # cookies[:user_name] # => "david" - # cookies.size # => 2 - # cookies[:lat_lon] # => [47.68, -122.37] - # cookies.signed[:login] # => "XJ-122" + # cookies[:user_name] # => "david" + # cookies.size # => 2 + # JSON.parse(cookies[:lat_lon]) # => [47.68, -122.37] + # cookies.signed[:login] # => "XJ-122" # # Example for deleting: # @@ -63,7 +63,7 @@ module ActionDispatch # # The option symbols for setting cookies are: # - # * <tt>:value</tt> - The cookie's value or list of values (as an array). + # * <tt>:value</tt> - The cookie's value. # * <tt>:path</tt> - The path for which this cookie applies. Defaults to the root # of the application. # * <tt>:domain</tt> - The domain for which this cookie applies so you can @@ -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 + COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze # Cookies can typically store 4096 bytes. MAX_COOKIE_SIZE = 4096 @@ -180,7 +181,7 @@ module ActionDispatch def verify_and_upgrade_legacy_signed_message(name, signed_message) @legacy_verifier.verify(signed_message).tap do |value| - self[name] = value + self[name] = { value: value } end rescue ActiveSupport::MessageVerifier::InvalidSignature nil @@ -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?, + serializer: env[COOKIES_SERIALIZER] } end @@ -372,28 +374,89 @@ module ActionDispatch end end + 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 + + # Passing the NullSerializer downstream to the Message{Encryptor,Verifier} + # allows us to handle the (de)serialization step within the cookie jar, + # which gives us the opportunity to detect and migrate legacy cookies. + class NullSerializer + def self.load(value) + value + end + + def self.dump(value) + value + end + end + + module SerializedCookieJars + MARSHAL_SIGNATURE = "\x04\x08".freeze + + protected + def needs_migration?(value) + @options[:serializer] == :hybrid && value.start_with?(MARSHAL_SIGNATURE) + end + + def serialize(name, value) + serializer.dump(value) + end + + def deserialize(name, value) + if value + if needs_migration?(value) + Marshal.load(value).tap do |v| + self[name] = { value: v } + end + else + serializer.load(value) + end + end + end + + def serializer + serializer = @options[:serializer] || :marshal + case serializer + when :marshal + Marshal + when :json, :hybrid + JsonSerializer + else + serializer + end + end + end + class SignedCookieJar #:nodoc: include ChainedCookieJars + include SerializedCookieJars def initialize(parent_jar, key_generator, options = {}) @parent_jar = parent_jar @options = options secret = key_generator.generate_key(@options[:signed_cookie_salt]) - @verifier = ActiveSupport::MessageVerifier.new(secret) + @verifier = ActiveSupport::MessageVerifier.new(secret, serializer: NullSerializer) end def [](name) if signed_message = @parent_jar[name] - verify(signed_message) + deserialize name, verify(signed_message) end end def []=(name, options) if options.is_a?(Hash) options.symbolize_keys! - options[:value] = @verifier.generate(options[:value]) + options[:value] = @verifier.generate(serialize(name, options[:value])) else - options = { :value => @verifier.generate(options) } + options = { :value => @verifier.generate(serialize(name, options)) } end raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE @@ -417,13 +480,14 @@ module ActionDispatch def [](name) if signed_message = @parent_jar[name] - verify(signed_message) || verify_and_upgrade_legacy_signed_message(name, signed_message) + deserialize(name, verify(signed_message)) || verify_and_upgrade_legacy_signed_message(name, signed_message) end end end class EncryptedCookieJar #:nodoc: include ChainedCookieJars + include SerializedCookieJars def initialize(parent_jar, key_generator, options = {}) if ActiveSupport::LegacyKeyGenerator === key_generator @@ -435,12 +499,12 @@ 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: NullSerializer) end def [](name) if encrypted_message = @parent_jar[name] - decrypt_and_verify(encrypted_message) + deserialize name, decrypt_and_verify(encrypted_message) end end @@ -450,7 +514,8 @@ module ActionDispatch else options = { :value => options } end - options[:value] = @encryptor.encrypt_and_sign(options[:value]) + + options[:value] = @encryptor.encrypt_and_sign(serialize(name, options[:value])) raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE @parent_jar[name] = options @@ -473,7 +538,7 @@ module ActionDispatch def [](name) if encrypted_or_signed_message = @parent_jar[name] - decrypt_and_verify(encrypted_or_signed_message) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message) + deserialize(name, decrypt_and_verify(encrypted_or_signed_message)) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message) end end end diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index 89003e7a5e..4821d2a899 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/hash/keys' + module ActionDispatch class Request < Rack::Request # Access the contents of the flash. Use <tt>flash["notice"]</tt> to @@ -50,13 +52,14 @@ module ActionDispatch end def []=(k, v) + k = k.to_s @flash[k] = v @flash.discard(k) v end def [](k) - @flash[k] + @flash[k.to_s] end # Convenience accessor for <tt>flash.now[:alert]=</tt>. @@ -92,8 +95,8 @@ module ActionDispatch end def initialize(flashes = {}, discard = []) #:nodoc: - @discard = Set.new(discard) - @flashes = flashes + @discard = Set.new(stringify_array(discard)) + @flashes = flashes.stringify_keys @now = nil end @@ -106,17 +109,18 @@ module ActionDispatch end def []=(k, v) + k = k.to_s @discard.delete k @flashes[k] = v end def [](k) - @flashes[k] + @flashes[k.to_s] end def update(h) #:nodoc: - @discard.subtract h.keys - @flashes.update h + @discard.subtract stringify_array(h.keys) + @flashes.update h.stringify_keys self end @@ -129,6 +133,7 @@ module ActionDispatch end def delete(key) + key = key.to_s @discard.delete key @flashes.delete key self @@ -155,7 +160,7 @@ module ActionDispatch def replace(h) #:nodoc: @discard.clear - @flashes.replace h + @flashes.replace h.stringify_keys self end @@ -186,6 +191,7 @@ module ActionDispatch # flash.keep # keeps the entire flash # flash.keep(:notice) # keeps only the "notice" entry, the rest of the flash is discarded def keep(k = nil) + k = k.to_s if k @discard.subtract Array(k || keys) k ? self[k] : self end @@ -195,6 +201,7 @@ module ActionDispatch # flash.discard # discard the entire flash at the end of the current action # flash.discard(:warning) # discard only the "warning" entry at the end of the current action def discard(k = nil) + k = k.to_s if k @discard.merge Array(k || keys) k ? self[k] : self end @@ -231,6 +238,12 @@ module ActionDispatch def now_is_loaded? @now end + + def stringify_array(array) + array.map do |item| + item.kind_of?(Symbol) ? item.to_s : item + end + end end def initialize(app) 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/request/utils.rb b/actionpack/lib/action_dispatch/request/utils.rb index a6dca9741c..9d4f1aa3c5 100644 --- a/actionpack/lib/action_dispatch/request/utils.rb +++ b/actionpack/lib/action_dispatch/request/utils.rb @@ -7,18 +7,23 @@ module ActionDispatch 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 f612e91aef..71a0c5e826 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -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 18f37dc732..0b762aa9a4 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -707,6 +707,10 @@ module ActionDispatch options[:path] = args.flatten.join('/') if args.any? options[:constraints] ||= {} + unless shallow? + options[:shallow_path] = options[:path] if args.any? + end + if options[:constraints].is_a?(Hash) defaults = options[:constraints].select do |k, v| URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum)) @@ -1369,7 +1373,7 @@ module ActionDispatch end def shallow - scope(:shallow => true, :shallow_path => @scope[:path]) do + scope(:shallow => true) do yield end end @@ -1410,6 +1414,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) @@ -1440,8 +1445,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 @@ -1489,6 +1494,13 @@ module ActionDispatch return true end + if options.delete(:shallow) + shallow do + send(method, resources.pop, options, &block) + end + return true + end + if resource_scope? nested { send(method, resources.pop, options, &block) } return true @@ -1606,10 +1618,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/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb index d3efca5b6f..c87494aa64 100644 --- a/actionpack/test/controller/filters_test.rb +++ b/actionpack/test/controller/filters_test.rb @@ -225,6 +225,10 @@ class FilterTest < ActionController::TestCase skip_before_filter :clean_up_tmp, if: -> { true } end + class ClassController < ConditionalFilterController + before_filter ConditionalClassFilter + end + class PrependingController < TestController prepend_before_filter :wonderful_life # skip_before_filter :fire_flash @@ -610,6 +614,18 @@ class FilterTest < ActionController::TestCase assert_equal %w( ensure_login ), assigns["ran_filter"] end + def test_skipping_class_filters + test_process(ClassController) + assert_equal true, assigns["ran_class_filter"] + + skipping_class_controller = Class.new(ClassController) do + skip_before_filter ConditionalClassFilter + end + + test_process(skipping_class_controller) + assert_nil assigns['ran_class_filter'] + end + def test_running_collection_condition_filters test_process(ConditionalCollectionFilterController) assert_equal %w( ensure_login ), assigns["ran_filter"] diff --git a/actionpack/test/controller/flash_hash_test.rb b/actionpack/test/controller/flash_hash_test.rb index 5490d9394b..50b36a0567 100644 --- a/actionpack/test/controller/flash_hash_test.rb +++ b/actionpack/test/controller/flash_hash_test.rb @@ -67,6 +67,16 @@ module ActionDispatch assert_equal({'flashes' => {'message' => 'Hello'}, 'discard' => %w[message]}, hash.to_session_value) end + def test_from_session_value_on_json_serializer + decrypted_data = "{ \"session_id\":\"d98bdf6d129618fc2548c354c161cfb5\", \"flash\":{\"discard\":[], \"flashes\":{\"message\":\"hey you\"}} }" + session = ActionDispatch::Cookies::JsonSerializer.load(decrypted_data) + hash = Flash::FlashHash.from_session_value(session['flash']) + + assert_equal({'discard' => %w[message], 'flashes' => { 'message' => 'hey you'}}, hash.to_session_value) + assert_equal "hey you", hash[:message] + assert_equal "hey you", hash["message"] + end + def test_empty? assert @hash.empty? @hash['zomg'] = 'bears' diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb index 9ceab91e42..25a4857eba 100644 --- a/actionpack/test/controller/flash_test.rb +++ b/actionpack/test/controller/flash_test.rb @@ -175,13 +175,13 @@ class FlashTest < ActionController::TestCase assert_equal(:foo_indeed, flash.discard(:foo)) # valid key passed assert_nil flash.discard(:unknown) # non existent key passed - assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.discard().to_hash) # nothing passed - assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.discard(nil).to_hash) # nothing passed + assert_equal({"foo" => :foo_indeed, "bar" => :bar_indeed}, flash.discard().to_hash) # nothing passed + assert_equal({"foo" => :foo_indeed, "bar" => :bar_indeed}, flash.discard(nil).to_hash) # nothing passed assert_equal(:foo_indeed, flash.keep(:foo)) # valid key passed assert_nil flash.keep(:unknown) # non existent key passed - assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.keep().to_hash) # nothing passed - assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.keep(nil).to_hash) # nothing passed + assert_equal({"foo" => :foo_indeed, "bar" => :bar_indeed}, flash.keep().to_hash) # nothing passed + assert_equal({"foo" => :foo_indeed, "bar" => :bar_indeed}, flash.keep(nil).to_hash) # nothing passed end def test_redirect_to_with_alert 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 84e4936f31..499c62cc35 100644 --- a/actionpack/test/controller/mime/respond_to_test.rb +++ b/actionpack/test/controller/mime/respond_to_test.rb @@ -671,6 +671,10 @@ class RespondToControllerTest < ActionController::TestCase end def test_variant_any_any + get :variant_any_any + assert_equal "text/html", @response.content_type + assert_equal "any", @response.body + @request.variant = :phone get :variant_any_any assert_equal "text/html", @response.content_type @@ -740,4 +744,25 @@ class RespondToControllerTest < ActionController::TestCase assert_equal "text/javascript", @response.content_type assert_equal "tablet", @response.body end + + def test_variant_negotiation_inline_syntax + @request.variant = [:tablet, :phone] + get :variant_inline_syntax_without_block + assert_equal "text/html", @response.content_type + assert_equal "phone", @response.body + end + + def test_variant_negotiation_block_syntax + @request.variant = [:tablet, :phone] + get :variant_plus_none_for_format + assert_equal "text/html", @response.content_type + assert_equal "phone", @response.body + end + + def test_variant_negotiation_without_block + @request.variant = [:tablet, :phone] + get :variant_inline_syntax_without_block + assert_equal "text/html", @response.content_type + assert_equal "phone", @response.body + end end 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/params_wrapper_test.rb b/actionpack/test/controller/params_wrapper_test.rb index d87e2b85b0..11ccb6cf3b 100644 --- a/actionpack/test/controller/params_wrapper_test.rb +++ b/actionpack/test/controller/params_wrapper_test.rb @@ -188,6 +188,26 @@ class ParamsWrapperTest < ActionController::TestCase assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'user' => { 'username' => 'sikachu', 'title' => 'Developer' }}) end end + + def test_preserves_query_string_params + with_default_wrapper_options do + @request.env['CONTENT_TYPE'] = 'application/json' + get :parse, { 'user' => { 'username' => 'nixon' } } + assert_parameters( + {'user' => { 'username' => 'nixon' } } + ) + end + end + + def test_empty_parameter_set + with_default_wrapper_options do + @request.env['CONTENT_TYPE'] = 'application/json' + post :parse, {} + assert_parameters( + {'user' => { } } + ) + end + end end class NamespacedParamsWrapperTest < ActionController::TestCase 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/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/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb index d2b4952759..a8035e5bd7 100644 --- a/actionpack/test/controller/url_for_test.rb +++ b/actionpack/test/controller/url_for_test.rb @@ -204,9 +204,6 @@ module AbstractController end def test_relative_url_root_is_respected - # ROUTES TODO: Tests should not have to pass :relative_url_root directly. This - # should probably come from routes. - add_host! assert_equal('https://www.basecamphq.com/subdir/c/a/i', W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => 'https', :script_name => '/subdir') diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index 91ac13e7c6..ba7aaa338d 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -11,6 +11,16 @@ require 'active_support/key_generator' require 'active_support/message_verifier' class CookiesTest < ActionController::TestCase + class CustomSerializer + def self.load(value) + value.to_s + " and loaded" + end + + def self.dump(value) + value.to_s + " was dumped" + end + end + class TestController < ActionController::Base def authenticate cookies["user_name"] = "david" @@ -359,9 +369,72 @@ class CookiesTest < ActionController::TestCase assert_equal 'Jamie', @controller.send(:cookies).permanent[:user_name] end - def test_signed_cookie + def test_signed_cookie_using_default_serializer get :set_signed_cookie - assert_equal 45, @controller.send(:cookies).signed[:user_id] + cookies = @controller.send :cookies + assert_not_equal 45, cookies[:user_id] + assert_equal 45, cookies.signed[:user_id] + end + + def test_signed_cookie_using_marshal_serializer + @request.env["action_dispatch.cookies_serializer"] = :marshal + get :set_signed_cookie + cookies = @controller.send :cookies + assert_not_equal 45, cookies[:user_id] + assert_equal 45, cookies.signed[:user_id] + end + + def test_signed_cookie_using_json_serializer + @request.env["action_dispatch.cookies_serializer"] = :json + get :set_signed_cookie + cookies = @controller.send :cookies + assert_not_equal 45, cookies[:user_id] + assert_equal 45, cookies.signed[:user_id] + end + + def test_signed_cookie_using_custom_serializer + @request.env["action_dispatch.cookies_serializer"] = CustomSerializer + get :set_signed_cookie + assert_not_equal 45, cookies[:user_id] + assert_equal '45 was dumped and loaded', cookies.signed[:user_id] + end + + def test_signed_cookie_using_hybrid_serializer_can_migrate_marshal_dumped_value_to_json + @request.env["action_dispatch.cookies_serializer"] = :hybrid + + key_generator = @request.env["action_dispatch.key_generator"] + signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"] + secret = key_generator.generate_key(signed_cookie_salt) + + marshal_value = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal).generate(45) + @request.headers["Cookie"] = "user_id=#{marshal_value}" + + get :get_signed_cookie + + cookies = @controller.send :cookies + assert_not_equal 45, cookies[:user_id] + assert_equal 45, cookies.signed[:user_id] + + verifier = ActiveSupport::MessageVerifier.new(secret, serializer: JSON) + assert_equal 45, verifier.verify(@response.cookies['user_id']) + end + + def test_signed_cookie_using_hybrid_serializer_can_read_from_json_dumped_value + @request.env["action_dispatch.cookies_serializer"] = :hybrid + + key_generator = @request.env["action_dispatch.key_generator"] + signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"] + secret = key_generator.generate_key(signed_cookie_salt) + json_value = ActiveSupport::MessageVerifier.new(secret, serializer: JSON).generate(45) + @request.headers["Cookie"] = "user_id=#{json_value}" + + get :get_signed_cookie + + cookies = @controller.send :cookies + assert_not_equal 45, cookies[:user_id] + assert_equal 45, cookies.signed[:user_id] + + assert_nil @response.cookies["user_id"] end def test_accessing_nonexistant_signed_cookie_should_not_raise_an_invalid_signature @@ -369,7 +442,18 @@ class CookiesTest < ActionController::TestCase assert_nil @controller.send(:cookies).signed[:non_existant_attribute] end - def test_encrypted_cookie + def test_encrypted_cookie_using_default_serializer + get :set_encrypted_cookie + cookies = @controller.send :cookies + assert_not_equal 'bar', cookies[:foo] + assert_raise TypeError do + cookies.signed[:foo] + end + assert_equal 'bar', cookies.encrypted[:foo] + end + + def test_encrypted_cookie_using_marshal_serializer + @request.env["action_dispatch.cookies_serializer"] = :marshal get :set_encrypted_cookie cookies = @controller.send :cookies assert_not_equal 'bar', cookies[:foo] @@ -379,6 +463,66 @@ class CookiesTest < ActionController::TestCase assert_equal 'bar', cookies.encrypted[:foo] end + def test_encrypted_cookie_using_json_serializer + @request.env["action_dispatch.cookies_serializer"] = :json + get :set_encrypted_cookie + cookies = @controller.send :cookies + assert_not_equal 'bar', cookies[:foo] + assert_raises ::JSON::ParserError do + cookies.signed[:foo] + end + assert_equal 'bar', cookies.encrypted[:foo] + end + + def test_encrypted_cookie_using_custom_serializer + @request.env["action_dispatch.cookies_serializer"] = CustomSerializer + get :set_encrypted_cookie + assert_not_equal 'bar', cookies.encrypted[:foo] + assert_equal 'bar was dumped and loaded', cookies.encrypted[:foo] + end + + def test_encrypted_cookie_using_hybrid_serializer_can_migrate_marshal_dumped_value_to_json + @request.env["action_dispatch.cookies_serializer"] = :hybrid + + key_generator = @request.env["action_dispatch.key_generator"] + encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"] + encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"] + secret = key_generator.generate_key(encrypted_cookie_salt) + sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt) + + marshal_value = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: Marshal).encrypt_and_sign("bar") + @request.headers["Cookie"] = "foo=#{marshal_value}" + + get :get_encrypted_cookie + + cookies = @controller.send :cookies + assert_not_equal "bar", cookies[:foo] + assert_equal "bar", cookies.encrypted[:foo] + + encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON) + assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) + end + + def test_encrypted_cookie_using_hybrid_serializer_can_read_from_json_dumped_value + @request.env["action_dispatch.cookies_serializer"] = :hybrid + + key_generator = @request.env["action_dispatch.key_generator"] + encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"] + encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"] + secret = key_generator.generate_key(encrypted_cookie_salt) + sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt) + json_value = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON).encrypt_and_sign("bar") + @request.headers["Cookie"] = "foo=#{json_value}" + + get :get_encrypted_cookie + + cookies = @controller.send :cookies + assert_not_equal "bar", cookies[:foo] + assert_equal "bar", cookies.encrypted[:foo] + + assert_nil @response.cookies["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] @@ -694,8 +838,6 @@ class CookiesTest < ActionController::TestCase assert_equal "dhh", cookies['user_name'] end - - def test_setting_request_cookies_is_indifferent_access cookies.clear cookies[:user_name] = "andrew" diff --git a/actionpack/test/dispatch/rack_test.rb b/actionpack/test/dispatch/rack_test.rb index 42067854ee..ef1964fd19 100644 --- a/actionpack/test/dispatch/rack_test.rb +++ b/actionpack/test/dispatch/rack_test.rb @@ -119,7 +119,7 @@ class RackRequestTest < BaseRackTest assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", @request.host end - test "cgi environment variables" do + test "CGI environment variables" do assert_equal "Basic", @request.auth_type assert_equal 0, @request.content_length assert_equal nil, @request.content_mime_type 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_test.rb b/actionpack/test/dispatch/request_test.rb index f79fe47897..40e32cb4d3 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -846,8 +846,20 @@ class RequestTest < ActiveSupport::TestCase test "setting variant" do request = stub_request + request.variant = :mobile - assert_equal :mobile, request.variant + assert_equal [:mobile], request.variant + + request.variant = [:phone, :tablet] + assert_equal [:phone, :tablet], request.variant + + assert_raise ArgumentError do + request.variant = [:phone, "tablet"] + end + + assert_raise ArgumentError do + request.variant = "yolo" + end end test "setting variant with non symbol value" do diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index 4501ea095c..959a3bc5cd 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -217,6 +217,24 @@ class ResponseTest < ActiveSupport::TestCase assert_not @response.respond_to?(:method_missing) assert @response.respond_to?(:method_missing, true) end + + test "can be destructured into status, headers and an enumerable body" do + response = ActionDispatch::Response.new(404, { 'Content-Type' => 'text/plain' }, ['Not Found']) + status, headers, body = response + + assert_equal 404, status + assert_equal({ 'Content-Type' => 'text/plain' }, headers) + assert_equal ['Not Found'], body.each.to_a + end + + test "[response].flatten does not recurse infinitely" do + Timeout.timeout(1) do # use a timeout to prevent it stalling indefinitely + status, headers, body = [@response].flatten + assert_equal @response.status, status + assert_equal @response.headers, headers + assert_equal @response.body, body.each.to_a.join + end + end end class ResponseIntegrationTest < ActionDispatch::IntegrationTest diff --git a/actionpack/test/dispatch/routing/inspector_test.rb b/actionpack/test/dispatch/routing/inspector_test.rb index 18a52f13a7..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 diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 795911497e..1fa2cc6cf2 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -1889,6 +1889,65 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal 'notes#destroy', @response.body end + def test_shallow_option_nested_resources_within_scope + draw do + scope '/hello' do + resources :notes, :shallow => true do + resources :trackbacks + end + end + end + + get '/hello/notes/1/trackbacks' + assert_equal 'trackbacks#index', @response.body + assert_equal '/hello/notes/1/trackbacks', note_trackbacks_path(:note_id => 1) + + get '/hello/notes/1/edit' + assert_equal 'notes#edit', @response.body + assert_equal '/hello/notes/1/edit', edit_note_path(:id => '1') + + get '/hello/notes/1/trackbacks/new' + assert_equal 'trackbacks#new', @response.body + assert_equal '/hello/notes/1/trackbacks/new', new_note_trackback_path(:note_id => 1) + + get '/hello/trackbacks/1' + assert_equal 'trackbacks#show', @response.body + assert_equal '/hello/trackbacks/1', trackback_path(:id => '1') + + get '/hello/trackbacks/1/edit' + assert_equal 'trackbacks#edit', @response.body + assert_equal '/hello/trackbacks/1/edit', edit_trackback_path(:id => '1') + + put '/hello/trackbacks/1' + assert_equal 'trackbacks#update', @response.body + + post '/hello/notes/1/trackbacks' + assert_equal 'trackbacks#create', @response.body + + delete '/hello/trackbacks/1' + assert_equal 'trackbacks#destroy', @response.body + + get '/hello/notes' + assert_equal 'notes#index', @response.body + + post '/hello/notes' + assert_equal 'notes#create', @response.body + + get '/hello/notes/new' + assert_equal 'notes#new', @response.body + assert_equal '/hello/notes/new', new_note_path + + get '/hello/notes/1' + assert_equal 'notes#show', @response.body + assert_equal '/hello/notes/1', note_path(:id => 1) + + put '/hello/notes/1' + assert_equal 'notes#update', @response.body + + delete '/hello/notes/1' + assert_equal 'notes#destroy', @response.body + end + def test_custom_resource_routes_are_scoped draw do resources :customers do @@ -2912,6 +2971,68 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest 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 + + def test_shallow_path_inside_namespace_is_not_added_twice + draw do + namespace :admin do + shallow do + resources :posts do + resources :comments + end + end + end + end + + get '/admin/posts/1/comments' + assert_equal 'admin/comments#index', @response.body + assert_equal '/admin/posts/1/comments', admin_post_comments_path('1') + end + private def draw(&block) diff --git a/actionpack/test/dispatch/static_test.rb b/actionpack/test/dispatch/static_test.rb index 5bd1806b21..afdda70748 100644 --- a/actionpack/test/dispatch/static_test.rb +++ b/actionpack/test/dispatch/static_test.rb @@ -136,10 +136,15 @@ module StaticTests def with_static_file(file) path = "#{FIXTURE_LOAD_PATH}/#{public_path}" + file - File.open(path, "wb+") { |f| f.write(file) } + begin + File.open(path, "wb+") { |f| f.write(file) } + rescue Errno::EPROTO + skip "Couldn't create a file #{path}" + end + yield file ensure - File.delete(path) + File.delete(path) if File.exist? path end end diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 19877ca8cb..a0f298a6b1 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,17 +1,38 @@ +* Date select helpers accept a format string for the months selector via the + new option `:month_format_string`. + + When rendered, the format string gets passed keys `:number` (integer), and + `:name` (string), in order to be able to interpolate them as in + + '%{name} (%<number>02d)' + + for example. + + This option is motivated by #13618. + + *Xavier Noria* + +* 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 + Fixes #13074, #13116. *João Britto* -* Use `display:none` instead of `display:inline` for hidden fields +* Use `display:none` instead of `display:inline` for hidden fields. - Fixes #6403 + Fixes #6403. *Gaelian Ditchburn* -* The `video_tag` helper accepts a number as `:size` +* 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 @@ -75,11 +96,11 @@ *Yves Senn* -* Use `set_backtrace` instead of instance variable `@backtrace` in ActionView exceptions +* Use `set_backtrace` instead of instance variable `@backtrace` in ActionView exceptions. *Shimpei Makimoto* -* Fix `simple_format` escapes own output when passing `sanitize: true` +* Fix `simple_format` escapes own output when passing `sanitize: true`. *Paul Seidemann* @@ -97,7 +118,9 @@ *Bogdan Gusiev* -* Ability to pass block to `select` helper +* Ability to pass a block to the `select` helper. + + Example: <%= select(report, "campaign_ids") do %> <% available_campaigns.each do |c| -%> @@ -177,7 +200,7 @@ * Fix default rendered format problem when calling `render` without :content_type option. It should return :html. Fix #11393. - *Gleb Mazovetskiy* *Oleg* *kennyj* + *Gleb Mazovetskiy*, *Oleg*, *kennyj* * Fix `link_to` with block and url hashes. diff --git a/actionview/lib/action_view.rb b/actionview/lib/action_view.rb index 5c729345dc..50712e0830 100644 --- a/actionview/lib/action_view.rb +++ b/actionview/lib/action_view.rb @@ -28,6 +28,8 @@ require 'action_view/version' module ActionView extend ActiveSupport::Autoload + ENCODING_FLAG = '#.*coding[:=]\s*(\S+)[ \t]*' + eager_autoload do autoload :Base autoload :Context @@ -54,7 +56,6 @@ module ActionView autoload_at "action_view/template/resolver" do autoload :Resolver autoload :PathResolver - autoload :FileSystemResolver autoload :OptimizedFileSystemResolver autoload :FallbackFileSystemResolver end @@ -81,8 +82,6 @@ module ActionView autoload :TestCase - ENCODING_FLAG = '#.*coding[:=]\s*(\S+)[ \t]*' - def self.eager_load! super ActionView::Helpers.eager_load! 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/helpers/csrf_helper.rb b/actionview/lib/action_view/helpers/csrf_helper.rb index eeb0ed94b9..5af92c4ff2 100644 --- a/actionview/lib/action_view/helpers/csrf_helper.rb +++ b/actionview/lib/action_view/helpers/csrf_helper.rb @@ -12,8 +12,11 @@ module ActionView # These are used to generate the dynamic forms that implement non-remote links with # <tt>:method</tt>. # - # Note that regular forms generate hidden fields, and that Ajax calls are whitelisted, - # so they do not use these tags. + # You don't need to use these tags for regular forms as they generate their own hidden fields. + # + # For AJAX requests other than GETs, extract the "csrf-token" from the meta-tag and send as the + # "X-CSRF-Token" HTTP header. If you are using jQuery with jquery-rails this happens automatically. + # def csrf_meta_tags if protect_against_forgery? [ diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb index 3d091c4a00..698f0ca31c 100644 --- a/actionview/lib/action_view/helpers/date_helper.rb +++ b/actionview/lib/action_view/helpers/date_helper.rb @@ -169,6 +169,9 @@ module ActionView # "2 - February" instead of "February"). # * <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>:month_format_string</tt> - Set to a format string. The string gets passed keys +:number+ (integer) + # and +:name+ (string). A format string would be something like "%{name} (%<number>02d)" for example. + # See <tt>Kernel.sprintf</tt> for documentation on format sequences. # * <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>Date.today.year - 5</tt>if # you are creating new record. While editing existing record, <tt>:start_year</tt> defaults to @@ -850,24 +853,36 @@ module ActionView I18n.translate(key, :locale => @options[:locale]) end - # Lookup month name for number. - # month_name(1) => "January" + # Looks up month names by number (1-based): # - # If <tt>:use_month_numbers</tt> option is passed - # month_name(1) => 1 + # month_name(1) # => "January" # - # If <tt>:use_two_month_numbers</tt> option is passed - # month_name(1) => '01' + # If the <tt>:use_month_numbers</tt> option is passed: # - # If <tt>:add_month_numbers</tt> option is passed - # month_name(1) => "1 - January" + # month_name(1) # => 1 + # + # If the <tt>:use_two_month_numbers</tt> option is passed: + # + # month_name(1) # => '01' + # + # If the <tt>:add_month_numbers</tt> option is passed: + # + # month_name(1) # => "1 - January" + # + # If the <tt>:month_format_string</tt> option is passed: + # + # month_name(1) # => "January (01)" + # + # depending on the format string. def month_name(number) if @options[:use_month_numbers] number elsif @options[:use_two_digit_numbers] - sprintf "%02d", number + '%02d' % number elsif @options[:add_month_numbers] "#{number} - #{month_names[number]}" + elsif format_string = @options[:month_format_string] + format_string % {number: number, name: month_names[number]} else month_names[number] end 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/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index 56dd7a4390..3ccace1274 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -232,6 +232,11 @@ module ActionView # # <div><input value="New" type="submit" /></div> # # </form>" # + # <%= button_to "New", new_articles_path %> + # # => "<form method="post" action="/articles/new" class="button_to"> + # # <div><input value="New" type="submit" /></div> + # # </form>" + # # <%= button_to [:make_happy, @user] do %> # Make happy <strong><%= @user.name %></strong> # <% end %> diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb index e07d9b6314..76c9890776 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -1,6 +1,7 @@ require 'thread_safe' require 'active_support/core_ext/module/remove_method' require 'active_support/core_ext/module/attribute_accessors' +require 'action_view/template/resolver' module ActionView # = Action View Lookup Context diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb index 99b95fdfb7..7c17220d14 100644 --- a/actionview/lib/action_view/rendering.rb +++ b/actionview/lib/action_view/rendering.rb @@ -94,7 +94,7 @@ module ActionView variant = options[:variant] lookup_context.rendered_format = nil if options[:formats] - lookup_context.variants = [variant] if variant + lookup_context.variants = variant if variant view_renderer.render(view_context, options) end diff --git a/actionview/test/fixtures/customers/_customer.xml.erb b/actionview/test/fixtures/customers/_customer.xml.erb new file mode 100644 index 0000000000..d3f1e0768f --- /dev/null +++ b/actionview/test/fixtures/customers/_customer.xml.erb @@ -0,0 +1 @@ +<greeting><%= greeting %></greeting><name><%= customer.name %></name>
\ No newline at end of file diff --git a/actionview/test/template/date_helper_test.rb b/actionview/test/template/date_helper_test.rb index 5f09aef249..6f77c3c99d 100644 --- a/actionview/test/template/date_helper_test.rb +++ b/actionview/test/template/date_helper_test.rb @@ -326,6 +326,16 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, select_month(8, :add_month_numbers => true) end + def test_select_month_with_format_string + expected = %(<select id="date_month" name="date[month]">\n) + expected << %(<option value="1">January (01)</option>\n<option value="2">February (02)</option>\n<option value="3">March (03)</option>\n<option value="4">April (04)</option>\n<option value="5">May (05)</option>\n<option value="6">June (06)</option>\n<option value="7">July (07)</option>\n<option value="8" selected="selected">August (08)</option>\n<option value="9">September (09)</option>\n<option value="10">October (10)</option>\n<option value="11">November (11)</option>\n<option value="12">December (12)</option>\n) + expected << "</select>\n" + + format_string = '%{name} (%<number>02d)' + assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :month_format_string => format_string) + assert_dom_equal expected, select_month(8, :month_format_string => format_string) + end + def test_select_month_with_numbers_and_names_with_abbv expected = %(<select id="date_month" name="date[month]">\n) expected << %(<option value="1">1 - Jan</option>\n<option value="2">2 - Feb</option>\n<option value="3">3 - Mar</option>\n<option value="4">4 - Apr</option>\n<option value="5">5 - May</option>\n<option value="6">6 - Jun</option>\n<option value="7">7 - Jul</option>\n<option value="8" selected="selected">8 - Aug</option>\n<option value="9">9 - Sep</option>\n<option value="10">10 - Oct</option>\n<option value="11">11 - Nov</option>\n<option value="12">12 - Dec</option>\n) diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb index 055a273cc3..db5d99755c 100644 --- a/actionview/test/template/render_test.rb +++ b/actionview/test/template/render_test.rb @@ -304,6 +304,16 @@ module RenderTestCases assert_equal "Hola: david", @controller_view.render('customer_greeting', :greeting => 'Hola', :customer_greeting => Customer.new("david")) end + def test_render_partial_with_object_uses_render_partial_path + assert_equal "Hello: lifo", + @controller_view.render(:partial => Customer.new("lifo"), :locals => {:greeting => "Hello"}) + end + + def test_render_partial_with_object_and_format_uses_render_partial_path + assert_equal "<greeting>Hello</greeting><name>lifo</name>", + @controller_view.render(:partial => Customer.new("lifo"), :formats => :xml, :locals => {:greeting => "Hello"}) + end + def test_render_partial_using_object assert_equal "Hello: lifo", @controller_view.render(Customer.new("lifo"), :greeting => "Hello") 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/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb index deba33510a..7e978e15d2 100644 --- a/actionview/test/template/url_helper_test.rb +++ b/actionview/test/template/url_helper_test.rb @@ -56,6 +56,13 @@ class UrlHelperTest < ActiveSupport::TestCase assert_dom_equal %{<form method="post" action="http://www.example.com" class="button_to"><div><input type="submit" value="Hello" /></div></form>}, button_to("Hello", "http://www.example.com") end + def test_button_to_with_path + assert_dom_equal( + %{<form method="post" action="/article/Hello" class="button_to"><div><input type="submit" value="Hello" /></div></form>}, + button_to("Hello", article_path("Hello".html_safe)) + ) + end + def test_button_to_with_straight_url_and_request_forgery self.request_forgery = true diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 09fdd84844..500d8dc42f 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,6 +1,33 @@ -* `attribute_changed?` now accepts parameters which check the old and new value of the attribute +* `#to_param` returns `nil` if `#to_key` returns `nil`. Fixes #11399. - `model.name_changed?(from: "Pete", to: "Ringo")` + *Yves Senn* + +* 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 a hash to check if the attribute was + changed `:from` and/or `:to` a given value. + + Example: + + model.name_changed?(from: "Pete", to: "Ringo") *Tejas Dinkar* diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb index 6b0a752566..0a19ef686d 100644 --- a/activemodel/lib/active_model/conversion.rb +++ b/activemodel/lib/active_model/conversion.rb @@ -62,7 +62,7 @@ module ActiveModel # person = Person.create # person.to_param # => "1" def to_param - persisted? ? to_key.join('-') : nil + (persisted? && key = to_key) ? key.join('-') : nil end # Returns a +string+ identifying the path associated with the object. diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index 58d87e6f7f..98ffffeb10 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -136,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 @@ -167,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 010c4bb6f9..9c3bc913e1 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -94,7 +94,7 @@ module ActiveModel # 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? diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index d824a66784..01739d8ae4 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -57,11 +57,15 @@ module ActiveModel include InstanceMethodsOnActivation if options.fetch(:validations, true) - validates_confirmation_of :password, if: :password_confirmation_required? - validates_presence_of :password, on: :create - validates_presence_of :password_confirmation, if: :password_confirmation_required? + # 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 password_confirmation_required? - password_confirmation && password.present? - end end end end diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 6be44b5d63..e9674d5143 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -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/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 b344095807..c84025f083 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -34,13 +34,9 @@ module ActiveModel # * <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/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/test/cases/conversion_test.rb b/activemodel/test/cases/conversion_test.rb index 3bb177591d..c5cfbf909d 100644 --- a/activemodel/test/cases/conversion_test.rb +++ b/activemodel/test/cases/conversion_test.rb @@ -24,6 +24,16 @@ class ConversionTest < ActiveModel::TestCase assert_equal "1", Contact.new(id: 1).to_param end + test "to_param returns nil if to_key is nil" do + klass = Class.new(Contact) do + def persisted? + true + end + end + + assert_nil klass.new.to_param + end + test "to_partial_path default implementation returns a string giving a relative path" do assert_equal "contacts/contact", Contact.new.to_partial_path assert_equal "helicopters/helicopter", Helicopter.new.to_partial_path, diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb index 54427a1513..2853476c91 100644 --- a/activemodel/test/cases/dirty_test.rb +++ b/activemodel/test/cases/dirty_test.rb @@ -41,6 +41,10 @@ class DirtyTest < ActiveModel::TestCase def save changes_applied end + + def reload + reset_changes + end end setup do @@ -83,6 +87,14 @@ class DirtyTest < ActiveModel::TestCase 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? @@ -149,4 +161,19 @@ class DirtyTest < ActiveModel::TestCase @model.size = 1 assert @model.size_changed? end + + test "reload should reset all changes" do + @model.name = 'Dmitry' + @model.name_changed? + @model.save + @model.name = 'Bob' + + assert_equal [nil, 'Dmitry'], @model.previous_changes['name'] + assert_equal 'Dmitry', @model.changed_attributes['name'] + + @model.reload + + assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.previous_changes + assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.changed_attributes + end end diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index bbd186d83d..def28578f8 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -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 diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb index 0314803af6..82fd291064 100644 --- a/activemodel/test/cases/secure_password_test.rb +++ b/activemodel/test/cases/secure_password_test.rb @@ -1,6 +1,5 @@ require 'cases/helper' require 'models/user' -require 'models/oauthed_user' require 'models/visitor' class SecurePasswordTest < ActiveModel::TestCase @@ -9,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 @@ -95,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/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 d289f616b8..f1f9cf1ffd 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,8 +1,264 @@ +* Properly detect if a connection is still active before using it + in multi-threaded environments. + + Fixes #12867. + + *Kevin Casey*, *Matthew Draper*, *William (B.J.) Snow Orvis* + +* When inverting add_index use the index name if present instead of + the columns. + + If there are two indices with matching columns and one of them is + explicitly named then reverting the migration adding the named one + would instead drop the unnamed one. + + The inversion of add_index will now drop the index by its name if + it is present. + + *Hubert Dąbrowski* + +* Add flag to disable schema dump after migration. + + Add a config parameter on Active Record named `dump_schema_after_migration` + which is true by default. Now schema dump does not happen at the + end of migration rake task if `dump_schema_after_migration` is false. + + *Emil Soman* + +* `find_in_batches`, `find_each`, `Result#each` and `Enumerable#index_by` now + return an `Enumerator` that can calculate its size. + + See also #13938. + + *Marc-André Lafortune* + +* Make sure transaction state gets reset after a commit operation on the record. + + If a new transaction was open inside a callback, the record was loosing track + of the transaction level state, and it was leaking that state. + + Fixes #12566. + + *arthurnn* + +* Pass `has_and_belongs_to_many` `:autosave` option to + the underlying `has_many :through` association. + + Fixes #13923. + + *Yves Senn* + +* PostgreSQL implementation of `SchemaStatements#index_name_exists?`. + + The database agnostic implementation does not detect with indexes that are + not supported by the ActiveRecord schema dumper. For example, expressions + indexes would not be detected. + + Fixes #11018. + + *Jonathan Baudanza* + +* Parsing PostgreSQL arrays with empty strings now works correctly. + + Previously, if you tried to parse `{"1","","2","","3"}` the result + would be `["1","2","3"]`, removing the empty strings from the array, + which would be incorrect. Now it will correctly produce `["1","","2","","3"]` + as the result of parsing the above PostgreSQL array. + + Fixes #13907. + + *Maurício Linhares* + +* Associations now raise `ArgumentError` on name conflicts. + + Dangerous association names conflicts include instance or class methods already + defined by `ActiveRecord::Base`. + + Example: + + class Car < ActiveRecord::Base + has_many :errors + end + # Will raise ArgumentError. + + Fixes #13217. + + *Lauro Caetano* + +* Fix regressions on `select_*` methods. + When `select_*` methods receive a `Relation` object, they should be able to + get the arel/binds from it. + Also fix regressions on `select_rows` that was ignoring the binds. + + Fixes #7538, #12017, #13731, #12056. + + *arthurnn* + +* Active Record objects can now be correctly dumped, loaded and dumped again + without issues. + + Previously, if you did `YAML.dump`, `YAML.load` and then `YAML.dump` again + in an Active Record model that used serialization it would fail at the last + dump due to the fields not being correctly serialized before being dumped + to YAML. Now it is possible to dump and load the same object as many times + as needed without any issues. + + Fixes #13861. + + *Maurício Linhares* + +* `find_in_batches` now returns an `Enumerator` when called without a block, so that it + can be chained with other `Enumerable` methods. + + *Marc-André Lafortune* + +* `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 +* Enable partial indexes for `sqlite >= 3.8.0`. See http://www.sqlite.org/partialindex.html @@ -130,7 +386,7 @@ This ensures that `change_table` and `create_table` will use similar objects. - Fixes #13577 and #13503. + Fixes #13577, #13503. *Nishant Modak*, *Prathamesh Sonpatki*, *Rafael Mendonça França* @@ -675,7 +931,7 @@ *Severin Schoepke* -* `ActiveRecord::Store` works together with PG `hstore` columns. +* `ActiveRecord::Store` works together with PostgreSQL `hstore` columns. Fixes #12452. @@ -961,7 +1217,7 @@ *Yves Senn* , *Severin Schoepke* -* Fix multidimensional PG arrays containing non-string items. +* Fix multidimensional PostgreSQL arrays containing non-string items. *Yves Senn* @@ -979,7 +1235,7 @@ *Richard Schneeman* -* Removed redundant override of `xml` column definition for PG, +* Removed redundant override of `xml` column definition for PostgreSQL, in order to use `xml` column type instead of `text`. *Paul Nikitochkin*, *Michael Nikitochkin* diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 714f623af3..142d21ce92 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -130,7 +130,6 @@ module ActiveRecord autoload :JoinDependency, 'active_record/associations/join_dependency' autoload :AssociationScope, 'active_record/associations/association_scope' autoload :AliasTracker, 'active_record/associations/alias_tracker' - autoload :JoinHelper, 'active_record/associations/join_helper' end # Clears out the association cache. @@ -669,11 +668,14 @@ module ActiveRecord # and member posts that use the posts table for STI. In this case, there must be a +type+ # column in the posts table. # + # Note: The <tt>attachable_type=</tt> method is being called when assigning an +attachable+. + # The +class_name+ of the +attachable+ is passed as a String. + # # class Asset < ActiveRecord::Base # belongs_to :attachable, polymorphic: true # - # def attachable_type=(sType) - # super(sType.to_s.classify.constantize.base_class.to_s) + # def attachable_type=(class_name) + # super(class_name.constantize.base_class.to_s) # end # end # @@ -1213,7 +1215,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 @@ -1581,7 +1584,7 @@ module ActiveRecord hm_options[:through] = middle_reflection.name hm_options[:source] = join_model.right_reflection.name - [:before_add, :after_add, :before_remove, :after_remove].each do |k| + [:before_add, :after_add, :before_remove, :after_remove, :autosave].each do |k| hm_options[k] = options[k] if options.key? k end diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 0c23029981..85109aee6c 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -5,16 +5,48 @@ module ActiveRecord # Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and # ActiveRecord::Associations::ThroughAssociationScope class AliasTracker # :nodoc: - attr_reader :aliases, :table_joins, :connection + attr_reader :aliases, :connection + + def self.empty(connection) + new connection, Hash.new(0) + end + + def self.create(connection, table_joins) + if table_joins.empty? + empty connection + else + aliases = Hash.new { |h,k| + h[k] = initial_count_for(connection, k, table_joins) + } + new connection, aliases + end + end + + def self.initial_count_for(connection, name, table_joins) + # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase + quoted_name = connection.quote_table_name(name).downcase + + counts = table_joins.map do |join| + if join.is_a?(Arel::Nodes::StringJoin) + # Table names + table aliases + join.left.downcase.scan( + /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/ + ).size + else + join.left.table_name == name ? 1 : 0 + end + end + + counts.sum + end # table_joins is an array of arel joins which might conflict with the aliases we assign here - def initialize(connection = Base.connection, table_joins = []) - @aliases = Hash.new { |h,k| h[k] = initial_count_for(k) } - @table_joins = table_joins - @connection = connection + def initialize(connection, aliases) + @aliases = aliases + @connection = connection end - def aliased_table_for(table_name, aliased_name = nil) + def aliased_table_for(table_name, aliased_name) table_alias = aliased_name_for(table_name, aliased_name) if table_alias == table_name @@ -24,9 +56,7 @@ module ActiveRecord end end - def aliased_name_for(table_name, aliased_name = nil) - aliased_name ||= table_name - + def aliased_name_for(table_name, aliased_name) if aliases[table_name].zero? # If it's zero, we can have our table_name aliases[table_name] = 1 @@ -48,26 +78,6 @@ module ActiveRecord private - def initial_count_for(name) - return 0 if Arel::Table === table_joins - - # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase - quoted_name = connection.quote_table_name(name).downcase - - counts = table_joins.map do |join| - if join.is_a?(Arel::Nodes::StringJoin) - # Table names + table aliases - join.left.downcase.scan( - /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/ - ).size - else - join.left.table_name == name ? 1 : 0 - end - end - - counts.sum - end - def truncate(name) name.slice(0, connection.table_alias_length - 2) end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 67ea489b22..4e46256862 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -94,7 +94,7 @@ module ActiveRecord # actually gets built. def association_scope if klass - @association_scope ||= AssociationScope.new(self).scope + @association_scope ||= AssociationScope.scope(self, klass.connection) end end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index f455b6934e..bb889a8f3b 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -1,52 +1,77 @@ module ActiveRecord module Associations class AssociationScope #:nodoc: - include JoinHelper + INSTANCE = new - attr_reader :association, :alias_tracker + def self.scope(association, connection) + INSTANCE.scope association, connection + end - delegate :klass, :owner, :reflection, :interpolate, :to => :association - delegate :chain, :scope_chain, :options, :source_options, :active_record, :to => :reflection + def scope(association, connection) + klass = association.klass + reflection = association.reflection + scope = klass.unscoped + owner = association.owner + alias_tracker = AliasTracker.empty connection - def initialize(association) - @association = association - @alias_tracker = AliasTracker.new klass.connection + scope.extending! Array(reflection.options[:extend]) + add_constraints(scope, owner, klass, reflection, alias_tracker) end - def scope - scope = klass.unscoped - scope.extending! Array(options[:extend]) - add_constraints(scope) + def join_type + Arel::Nodes::InnerJoin end private - def column_for(table_name, column_name) + def construct_tables(chain, klass, refl, alias_tracker) + chain.map do |reflection| + alias_tracker.aliased_table_for( + table_name_for(reflection, klass, refl), + table_alias_for(reflection, refl, reflection != refl) + ) + end + end + + def table_alias_for(reflection, refl, join = false) + name = "#{reflection.plural_name}_#{alias_suffix(refl)}" + name << "_join" if join + name + end + + def join(table, constraint) + table.create_join(table, table.create_on(constraint), join_type) + end + + def column_for(table_name, column_name, alias_tracker) columns = alias_tracker.connection.schema_cache.columns_hash(table_name) columns[column_name] end - def bind_value(scope, column, value) + def bind_value(scope, column, value, alias_tracker) substitute = alias_tracker.connection.substitute_at( column, scope.bind_values.length) scope.bind_values += [[column, value]] substitute end - def bind(scope, table_name, column_name, value) - column = column_for table_name, column_name - bind_value scope, column, value + def bind(scope, table_name, column_name, value, tracker) + column = column_for table_name, column_name, tracker + bind_value scope, column, value, tracker end - def add_constraints(scope) - tables = construct_tables + def add_constraints(scope, owner, assoc_klass, refl, tracker) + chain = refl.chain + scope_chain = refl.scope_chain + + tables = construct_tables(chain, assoc_klass, refl, tracker) chain.each_with_index do |reflection, i| table, foreign_table = tables.shift, tables.first if reflection.source_macro == :belongs_to if reflection.options[:polymorphic] - key = reflection.association_primary_key(self.klass) + key = reflection.association_primary_key(assoc_klass) else key = reflection.association_primary_key end @@ -58,12 +83,12 @@ module ActiveRecord end if reflection == chain.last - bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key] + bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key], tracker scope = scope.where(table[key].eq(bind_val)) if reflection.type value = owner.class.base_class.name - bind_val = bind scope, table.table_name, reflection.type.to_s, value + bind_val = bind scope, table.table_name, reflection.type.to_s, value, tracker scope = scope.where(table[reflection.type].eq(bind_val)) end else @@ -71,7 +96,7 @@ module ActiveRecord if reflection.type value = chain[i + 1].klass.base_class.name - bind_val = bind scope, table.table_name, reflection.type.to_s, value + bind_val = bind scope, table.table_name, reflection.type.to_s, value, tracker scope = scope.where(table[reflection.type].eq(bind_val)) end @@ -79,14 +104,14 @@ module ActiveRecord end is_first_chain = i == 0 - klass = is_first_chain ? self.klass : reflection.klass + klass = is_first_chain ? assoc_klass : reflection.klass # Exclude the scope of the association itself, because that # was already merged in the #scope method. scope_chain[i].each do |scope_chain_item| - item = eval_scope(klass, scope_chain_item) + item = eval_scope(klass, scope_chain_item, owner) - if scope_chain_item == self.reflection.scope + if scope_chain_item == refl.scope scope.merge! item.except(:where, :includes, :bind) end @@ -103,22 +128,22 @@ module ActiveRecord scope end - def alias_suffix - reflection.name + def alias_suffix(refl) + refl.name end - def table_name_for(reflection) - if reflection == self.reflection + def table_name_for(reflection, klass, refl) + if reflection == refl # If this is a polymorphic belongs_to, we want to get the klass from the # association because it depends on the polymorphic_type attribute of # the owner klass.table_name else - super + reflection.table_name end end - def eval_scope(klass, scope) + def eval_scope(klass, scope, owner) if scope.is_a?(Relation) scope else diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index 3911d1b520..f085fd1cfd 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -26,6 +26,12 @@ module ActiveRecord::Associations::Builder attr_reader :name, :scope, :options def self.build(model, name, scope, options, &block) + if model.dangerous_attribute_method?(name) + raise ArgumentError, "You tried to define an association named #{name} on the model #{model.name}, but " \ + "this will conflict with a method #{name} already defined by Active Record. " \ + "Please choose a different association name." + end + builder = create_builder model, name, scope, options, &block reflection = builder.build(model) define_accessors model, reflection diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 52531a3520..03ca00fa70 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -24,6 +24,10 @@ module ActiveRecord # If you need to work on all current children, new and existing records, # +load_target+ and the +loaded+ flag are your friends. class CollectionAssociation < Association #:nodoc: + def initialize(owner, reflection) + super + @proxy = CollectionProxy.create(klass, self) + end # Implements the reader method, e.g. foo.items for Foo.has_many :items def reader(force_reload = false) @@ -33,7 +37,7 @@ module ActiveRecord reload end - @proxy ||= CollectionProxy.create(klass, self) + @proxy end # Implements the writer method, e.g. foo.items= for Foo.has_many :items @@ -96,11 +100,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) @@ -526,7 +550,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 @@ -564,10 +588,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 e3fc908444..eba688866c 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -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 @@ -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. @@ -978,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/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 72e0891702..6457182195 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -111,7 +111,7 @@ module ActiveRecord records.each(&:destroy!) update_counter(-records.length) unless inverse_updates_counter_cache? else - if records == :all + if records == :all || !reflection.klass.primary_key scope = self.scope else scope = self.scope.where(reflection.klass.primary_key => records) diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index b5f9ee6cee..b7dc037a65 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -93,8 +93,8 @@ module ActiveRecord # joins # => [] # def initialize(base, associations, joins) - @alias_tracker = AliasTracker.new(base.connection, joins) - @alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1 + @alias_tracker = AliasTracker.create(base.connection, joins) + @alias_tracker.aliased_name_for(base.table_name, base.table_name) # Updates the count for base.table_name to 1 tree = self.class.make_tree associations @join_root = JoinBase.new base, build(tree, base) @join_root.children.each { |child| construct_tables! @join_root, child } @@ -163,16 +163,16 @@ module ActiveRecord def make_outer_joins(parent, child) tables = table_aliases_for(parent, child) - join_type = Arel::OuterJoin - info = make_constraints parent, child, tables, join_type + join_type = Arel::Nodes::OuterJoin + info = make_constraints parent, child, tables, join_type [info] + child.children.flat_map { |c| make_outer_joins(child, c) } end def make_inner_joins(parent, child) tables = child.tables - join_type = Arel::InnerJoin - info = make_constraints parent, child, tables, join_type + join_type = Arel::Nodes::InnerJoin + info = make_constraints parent, child, tables, join_type [info] + child.children.flat_map { |c| make_inner_joins(child, c) } 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 0333816abb..ca36462054 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -28,7 +28,8 @@ module ActiveRecord bind_values = [] tables = tables.reverse - scope_chain_iter = scope_chain.reverse_each + scope_chain_index = 0 + scope_chain = scope_chain.reverse # The chain starts with the target table, but we want to end with it here (makes # more sense in this context), so we reverse @@ -47,13 +48,14 @@ module ActiveRecord constraint = build_constraint(klass, table, key, foreign_table, foreign_key) - scope_chain_items = scope_chain_iter.next.map do |item| + scope_chain_items = scope_chain[scope_chain_index].map do |item| if item.is_a?(Relation) item else ActiveRecord::Relation.create(klass, table).instance_exec(node, &item) end end + scope_chain_index += 1 scope_chain_items.concat [klass.send(:build_default_scope)].compact diff --git a/activerecord/lib/active_record/associations/join_helper.rb b/activerecord/lib/active_record/associations/join_helper.rb deleted file mode 100644 index f345d16841..0000000000 --- a/activerecord/lib/active_record/associations/join_helper.rb +++ /dev/null @@ -1,36 +0,0 @@ -module ActiveRecord - module Associations - # Helper class module which gets mixed into JoinDependency::JoinAssociation and AssociationScope - module JoinHelper #:nodoc: - - def join_type - Arel::InnerJoin - end - - private - - def construct_tables - chain.map do |reflection| - alias_tracker.aliased_table_for( - table_name_for(reflection), - table_alias_for(reflection, reflection != self.reflection) - ) - end - end - - def table_name_for(reflection) - reflection.table_name - end - - def table_alias_for(reflection, join = false) - name = "#{reflection.plural_name}_#{alias_suffix}" - name << "_join" if join - name - end - - def join(table, constraint) - table.create_join(table, table.create_on(constraint), join_type) - end - end - end -end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index e4500af5b2..399aff378a 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -39,7 +39,7 @@ module ActiveRecord end def find_target - if record = scope.first + if record = scope.take set_inverse_instance record end end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 73761520f7..9326c9c117 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 @@ -260,6 +278,11 @@ module ActiveRecord } end + # Placeholder so it can be overriden when needed by serialization + def attributes_for_coder # :nodoc: + attributes + end + # Returns an <tt>#inspect</tt>-like string for the value of the # attribute +attr_name+. String attributes are truncated upto 50 # characters, Date and Time attributes are returned in the 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/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index d484659190..67abbbc2a0 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) @@ -156,6 +164,16 @@ module ActiveRecord super end end + + def attributes_for_coder + attribute_names.each_with_object({}) do |name, attrs| + attrs[name] = if self.class.serialized_attributes.include?(name) + @attributes[name].serialized_value + else + read_attribute(name) + end + end + end end end end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index e9622ca0c1..4f58d06f35 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -301,7 +301,7 @@ module ActiveRecord def association_valid?(reflection, record) return true if record.destroyed? || record.marked_for_destruction? - unless valid = record.valid? + unless valid = record.valid?(self.validation_context) if reflection.options[:autosave] record.errors.each do |attribute, message| attribute = "#{reflection.name}.#{attribute}" 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 cebe741daa..759e162e19 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -393,7 +393,7 @@ module ActiveRecord synchronize do stale = Time.now - @dead_connection_timeout connections.dup.each do |conn| - if conn.in_use? && stale > conn.last_use && !conn.active? + if conn.in_use? && stale > conn.last_use && !conn.active_threadsafe? remove conn end end 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 cc108a3a61..9371d6f992 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -26,6 +26,14 @@ module ActiveRecord # Returns an ActiveRecord::Result instance. def select_all(arel, name = nil, binds = []) + if arel.is_a?(Relation) + relation = arel + arel = relation.arel + if !binds || binds.empty? + binds = relation.bind_values + end + end + select(to_sql(arel, binds), name, binds) end @@ -45,13 +53,16 @@ module ActiveRecord # Returns an array of the values of the first column in a select: # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3] def select_values(arel, name = nil) - select_rows(to_sql(arel, []), name) - .map { |v| v[0] } + binds = [] + if arel.is_a?(Relation) + arel, binds = arel.arel, arel.bind_values + end + select_rows(to_sql(arel, binds), name, binds).map(&:first) end # Returns an array of arrays containing the field values. # Order is the same as that returned by +columns+. - def select_rows(sql, name = nil) + def select_rows(sql, name = nil, binds = []) end undef_method :select_rows 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 88bf15bc18..ad069f5e53 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -120,9 +120,9 @@ module ActiveRecord # The name of the primary key, if one is to be added automatically. # Defaults to +id+. If <tt>:id</tt> is false this option is ignored. # - # Also note that this just sets the primary key in the table. You additionally - # need to configure the primary key in the model via +self.primary_key=+. - # Models do NOT auto-detect the primary key from their table definition. + # Note that Active Record models will automatically detect their + # primary key. This can be avoided by using +self.primary_key=+ on the model + # to define the key explicitly. # # [<tt>:options</tt>] # Any extra options you want appended to the table definition. 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 a8a530e1d5..2ba028e658 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -266,6 +266,12 @@ module ActiveRecord def active? end + # Adapter should redefine this if it needs a threadsafe way to approximate + # if the connection is active + def active_threadsafe? + active? + end + # Disconnects from the database if already connected, and establishes a # new connection with the database. Implementors should call super if they # override the default implementation. 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 7768c24967..23edc8b955 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -298,11 +298,7 @@ module ActiveRecord # Executes the SQL statement in the context of this connection. def execute(sql, name = nil) - if name == :skip_logging - @connection.query(sql) - else - log(sql, name) { @connection.query(sql) } - end + log(sql, name) { @connection.query(sql) } end # MysqlAdapter has to free a result after using it, so we use this method to write @@ -775,7 +771,7 @@ module ActiveRecord end.compact.join(', ') # ...and send them all in one query - execute("SET #{encoding} #{variable_assignments}", :skip_logging) + @connection.query "SET #{encoding} #{variable_assignments}" 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 830b1be021..69c2a361ee 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -217,7 +217,7 @@ module ActiveRecord # Returns an array of arrays containing the field values. # Order is the same as that returned by +columns+. - def select_rows(sql, name = nil) + def select_rows(sql, name = nil, binds = []) execute(sql, name).to_a end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 7dbaa272a3..49f0bfbcde 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -213,9 +213,9 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== - def select_rows(sql, name = nil) + def select_rows(sql, name = nil, binds = []) @connection.query_with_result = true - rows = exec_query(sql, name).rows + rows = exec_query(sql, name, binds).rows @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped rows end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb index 20de8d1982..0b218f2bfd 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb @@ -91,8 +91,9 @@ module ActiveRecord end def add_item_to_array(array, current_item, quoted) - if current_item.length == 0 - elsif !quoted && current_item == 'NULL' + return if !quoted && current_item.length == 0 + + if !quoted && current_item == 'NULL' array.push nil else array.push current_item 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 f349c37724..51ee2829b2 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -46,8 +46,8 @@ module ActiveRecord # Executes a SELECT query and returns an array of rows. Each row is an # array of field values. - def select_rows(sql, name = nil) - select_raw(sql, name).last + def select_rows(sql, name = nil, binds = []) + exec_query(sql, name, binds).rows end # Executes an INSERT query and returns the new record's ID diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 571257f6dd..ae8ede4b42 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -126,6 +126,19 @@ module ActiveRecord SQL end + def index_name_exists?(table_name, index_name, default) + exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 + SELECT COUNT(*) + FROM pg_class t + INNER JOIN pg_index d ON t.oid = d.indrelid + INNER JOIN pg_class i ON d.indexrelid = i.oid + WHERE i.relkind = 'i' + AND i.relname = '#{index_name}' + AND t.relname = '#{table_name}' + AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) ) + SQL + end + # Returns an array of indexes for the given table. def indexes(table_name, name = nil) result = query(<<-SQL, 'SCHEMA') diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index a471383041..36c7462419 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -586,11 +586,16 @@ module ActiveRecord # Is this connection alive and ready for queries? def active? - @connection.connect_poll != PG::PGRES_POLLING_FAILED + @connection.query 'SELECT 1' + true rescue PGError false end + def active_threadsafe? + @connection.connect_poll != PG::PGRES_POLLING_FAILED + end + # Close then reopen the connection. def reconnect! super @@ -942,14 +947,6 @@ module ActiveRecord exec_query(sql, name, binds) end - def select_raw(sql, name = nil) - res = execute(sql, name) - results = result_as_array(res) - fields = res.fields - res.clear - return fields, results - end - # Returns the list of a table's column names, data types, and default values. # # The underlying query is roughly: diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 55533311f5..1e299f12cd 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -347,8 +347,8 @@ module ActiveRecord end alias :create :insert_sql - def select_rows(sql, name = nil) - exec_query(sql, name).rows + def select_rows(sql, name = nil, binds = []) + exec_query(sql, name, binds).rows end def begin_db_transaction #:nodoc: diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index cd8690d500..d9aaf8597f 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -76,6 +76,15 @@ module ActiveRecord mattr_accessor :timestamped_migrations, instance_writer: false self.timestamped_migrations = true + ## + # :singleton-method: + # Specify whether schema dump should happen at the end of the + # db:migrate rake task. This is true by default, which is useful for the + # development environment. This should ideally be false in the production + # environment where dumping schema is rarely needed. + mattr_accessor :dump_schema_after_migration, instance_writer: false + self.dump_schema_after_migration = true + # :nodoc: mattr_accessor :maintain_test_schema, instance_accessor: false @@ -138,12 +147,12 @@ module ActiveRecord # class Post < ActiveRecord::Base # scope :published_and_commented, -> { published.and(self.arel_table[:comments_count].gt(0)) } # end - def arel_table + def arel_table # :nodoc: @arel_table ||= Arel::Table.new(table_name, arel_engine) end # Returns the Arel engine. - def arel_engine + def arel_engine # :nodoc: @arel_engine ||= if Base == self || connection_handler.retrieve_connection_pool(self) self @@ -182,9 +191,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. @@ -255,16 +262,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 @@ -281,7 +284,7 @@ module ActiveRecord # Post.new.encode_with(coder) # coder # => {"attributes" => {"id" => nil, ... }} def encode_with(coder) - coder['attributes'] = attributes + coder['attributes'] = attributes_for_coder end # Returns true if +comparison_object+ is the same exact object, or +comparison_object+ @@ -397,13 +400,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 @@ -443,13 +443,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/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index 5caab09038..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 diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 3deb2d65f8..4aa323fb00 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -62,7 +62,15 @@ module ActiveRecord # Use that class method when you need to know the ordinal value of an enum: # # Conversation.where("status <> ?", Conversation.statuses[:archived]) + # + # Where conditions on an enum attribute must use the ordinal value of an enum. 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| @@ -71,10 +79,12 @@ module ActiveRecord 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 status=(value) self[:status] = statuses[value] end + klass.send(:detect_enum_conflict!, name, "#{name}=") define_method("#{name}=") { |value| if enum_values.has_key?(value) || value.blank? self[name] = enum_values[value] @@ -89,24 +99,31 @@ module ActiveRecord } # 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 :active, -> { where status: 0 } - klass.scope value, -> { klass.where name => i } - # def active?() status == 0 end + klass.send(:detect_enum_conflict!, name, "#{value}?") define_method("#{value}?") { self[name] == i } # 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 @@ -114,10 +131,64 @@ module ActiveRecord private def _enum_methods_module @_enum_methods_module ||= begin - mod = Module.new + 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 diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index da73112e90..08fc91c9df 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -195,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/migration.rb b/activerecord/lib/active_record/migration.rb index b57da73969..b6b02322d7 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -385,8 +385,8 @@ module ActiveRecord attr_accessor :delegate # :nodoc: attr_accessor :disable_ddl_transaction # :nodoc: - def check_pending! - raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration? + def check_pending!(connection = Base.connection) + raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?(connection) end def load_schema_if_pending! @@ -830,17 +830,17 @@ module ActiveRecord SchemaMigration.all.map { |x| x.version.to_i }.sort end - def current_version + def current_version(connection = Base.connection) sm_table = schema_migrations_table_name - if Base.connection.table_exists?(sm_table) + if connection.table_exists?(sm_table) get_all_versions.max || 0 else 0 end end - def needs_migration? - current_version < last_version + def needs_migration?(connection = Base.connection) + current_version(connection) < last_version end def last_version diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index 9139ad953c..c44d8c1665 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -140,7 +140,12 @@ module ActiveRecord def invert_add_index(args) table, columns, options = *args - [:remove_index, [table, (options || {}).merge(column: columns)]] + options ||= {} + + index_name = options[:name] + options_hash = index_name ? { name: index_name } : { column: columns } + + [:remove_index, [table, options_hash]] end def invert_remove_index(args) diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 460fbdb3f8..b1b35ed940 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -389,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 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/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 561387a179..1d5c80bc01 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -34,7 +34,7 @@ db_namespace = namespace :db do ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) do |migration| ENV["SCOPE"].blank? || (ENV["SCOPE"] == migration.scope) end - db_namespace['_dump'].invoke + db_namespace['_dump'].invoke if ActiveRecord::Base.dump_schema_after_migration end task :_dump do @@ -75,7 +75,7 @@ db_namespace = namespace :db do # desc 'Runs the "down" for a given migration VERSION.' task :down => [:environment, :load_config] do version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil - raise 'VERSION is required' unless version + raise 'VERSION is required - To go down one migration, run db:rollback' unless version ActiveRecord::Migrator.run(:down, ActiveRecord::Migrator.migrations_paths, version) db_namespace['_dump'].invoke end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 770b48c550..0959860ec6 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 @@ -496,9 +497,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 @@ -534,7 +536,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 49b01909c6..29fc150b3d 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -52,7 +52,9 @@ module ActiveRecord records.each { |record| yield record } end else - enum_for :find_each, options + enum_for :find_each, options do + options[:start] ? where(table[primary_key].gteq(options[:start])).size : size + end end end @@ -64,6 +66,16 @@ module ActiveRecord # group.each { |person| person.party_all_night! } # end # + # If you do not provide a block to #find_in_batches, it will return an Enumerator + # for chaining with other methods: + # + # Person.find_in_batches.with_index do |group, batch| + # puts "Processing group ##{batch}" + # 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. @@ -88,30 +100,33 @@ module ActiveRecord options.assert_valid_keys(:start, :batch_size) relation = self + start = options[:start] + batch_size = options[:batch_size] || 1000 + + unless block_given? + return to_enum(:find_in_batches, options) do + total = start ? where(table[primary_key].gteq(start)).size : size + (total - 1).div(batch_size) + 1 + end + end if logger && (arel.orders.present? || arel.taken.present?) logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size") end - start = options.delete(:start) - batch_size = options.delete(:batch_size) || 1000 - relation = relation.reorder(batch_order).limit(batch_size) records = start ? relation.where(table[primary_key].gteq(start)).to_a : relation.to_a 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/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 64ac265689..4519a756a7 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: # @@ -231,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 @@ -268,7 +348,15 @@ module ActiveRecord end def construct_relation_for_association_calculations - apply_join_dependency(self, construct_join_dependency(arel.froms.first)) + from = arel.froms.first + if Arel::Table === from + apply_join_dependency(self, construct_join_dependency) + else + # FIXME: as far as I can tell, `from` will always be an Arel::Table. + # There are no tests that test this branch, but presumably it's + # possible for `from` to be a list? + apply_join_dependency(self, construct_join_dependency(from)) + end end def apply_join_dependency(relation, join_dependency) @@ -365,19 +453,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/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 6cf57b679d..7d2b427289 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -120,6 +120,9 @@ module ActiveRecord # Will throw an error, but this will work: # # User.includes(:posts).where('posts.name = ?', 'example').references(:posts) + # + # Note that +includes+ works with association names while +references+ needs + # the actual table name. def includes(*args) check_if_method_has_arguments!(:includes, args) spawn.includes!(*args) @@ -163,24 +166,26 @@ module ActiveRecord self end - # Used to indicate that an association is referenced by an SQL string, and should - # therefore be JOINed in any query rather than loaded separately. + # Use to indicate that the given +table_names+ are referenced by an SQL string, + # and should therefore be JOINed in any query rather than loaded separately. + # This method only works in conjuction with +includes+. + # See #includes for more details. # # User.includes(:posts).where("posts.name = 'foo'") # # => Doesn't JOIN the posts table, resulting in an error. # # User.includes(:posts).where("posts.name = 'foo'").references(:posts) # # => Query now knows the string references posts, so adds a JOIN - def references(*args) - check_if_method_has_arguments!(:references, args) - spawn.references!(*args) + def references(*table_names) + check_if_method_has_arguments!(:references, table_names) + spawn.references!(*table_names) end - def references!(*args) # :nodoc: - args.flatten! - args.map!(&:to_s) + def references!(*table_names) # :nodoc: + table_names.flatten! + table_names.map!(&:to_s) - self.references_values |= args + self.references_values |= table_names self end @@ -234,7 +239,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 @@ -817,11 +824,12 @@ module ActiveRecord end # Returns the Arel object associated with the relation. - def arel + def arel # :nodoc: @arel ||= build_arel end - # Like #arel, but ignores the default scope of the model. + private + def build_arel arel = Arel::SelectManager.new(table.engine, table) @@ -856,8 +864,6 @@ module ActiveRecord arel end - private - def symbol_unscoping(scope) if !VALID_UNSCOPING_VALUES.include?(scope) raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}." @@ -887,8 +893,6 @@ module ActiveRecord 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 == target_value - else - raise "unscope(where: #{target_value.inspect}) failed: unscoping #{rel.class} is unimplemented." end end @@ -1022,7 +1026,10 @@ module ActiveRecord def build_select(arel, selects) if !selects.empty? - arel.project(*selects) + 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 @@ -1080,9 +1087,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/result.rb b/activerecord/lib/active_record/result.rb index 469451e2f4..228b2aa60f 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -54,7 +54,7 @@ module ActiveRecord if block_given? hash_rows.each { |row| yield row } else - hash_rows.to_enum + hash_rows.to_enum { @rows.size } end end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index dacaec26b7..5a71c13d91 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -29,6 +29,7 @@ module ActiveRecord end end alias_method :sanitize_sql, :sanitize_sql_for_conditions + alias_method :sanitize_conditions, :sanitize_sql # Accepts an array, hash, or string of SQL conditions and sanitizes # them into a valid SQL fragment for a SET clause. @@ -122,8 +123,6 @@ module ActiveRecord end end - alias_method :sanitize_conditions, :sanitize_sql - def replace_bind_variables(statement, values) #:nodoc: raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size) bound = values.dup 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/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 c33ffeece0..ec3e8f281b 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -295,7 +295,7 @@ module ActiveRecord def committed! #:nodoc: run_callbacks :commit if destroyed? || persisted? ensure - clear_transaction_record_state + @_start_transaction_state.clear end # Call the +after_rollback+ callbacks. The +force_restore_state+ argument indicates if the record diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index b67e70ec7e..0eb1231c79 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -1,5 +1,7 @@ require "cases/helper" require "models/book" +require "models/post" +require "models/author" module ActiveRecord class AdapterTest < ActiveRecord::TestCase @@ -179,6 +181,27 @@ module ActiveRecord assert result.is_a?(ActiveRecord::Result) end + def test_select_methods_passing_a_association_relation + author = Author.create!(name: 'john') + Post.create!(author: author, title: 'foo', body: 'bar') + query = author.posts.select(:title) + assert_equal({"title" => "foo"}, @connection.select_one(query.arel, nil, query.bind_values)) + assert_equal({"title" => "foo"}, @connection.select_one(query)) + assert @connection.select_all(query).is_a?(ActiveRecord::Result) + assert_equal "foo", @connection.select_value(query) + assert_equal ["foo"], @connection.select_values(query) + end + + def test_select_methods_passing_a_relation + Post.create!(title: 'foo', body: 'bar') + query = Post.where(title: 'foo').select(:title) + assert_equal({"title" => "foo"}, @connection.select_one(query.arel, nil, query.bind_values)) + assert_equal({"title" => "foo"}, @connection.select_one(query)) + assert @connection.select_all(query).is_a?(ActiveRecord::Result) + assert_equal "foo", @connection.select_value(query) + assert_equal ["foo"], @connection.select_values(query) + end + test "type_to_sql returns a String for unmapped types" do assert_equal "special_db_type", @connection.type_to_sql(:special_db_type) end diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index d71e2aa2bb..3090f4478f 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -93,6 +93,18 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert_cycle(:tags, [[['1'], ['2']], [['2'], ['3']]]) end + def test_with_empty_strings + assert_cycle(:tags, [ '1', '2', '', '4', '', '5' ]) + end + + def test_with_multi_dimensional_empty_strings + assert_cycle(:tags, [[['1', '2'], ['', '4'], ['', '5']]]) + end + + def test_with_arbitrary_whitespace + assert_cycle(:tags, [[['1', '2'], [' ', '4'], [' ', '5']]]) + end + def test_multi_dimensional_with_integers assert_cycle(:ratings, [[[1], [7]], [[8], [10]]]) end diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb new file mode 100644 index 0000000000..7202cce390 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +require "cases/helper" +require 'active_record/base' +require 'active_record/connection_adapters/postgresql_adapter' + +class PostgresqlCompositeTest < ActiveRecord::TestCase + class PostgresqlComposite < ActiveRecord::Base + self.table_name = "postgresql_composites" + end + + def teardown + @connection.execute 'DROP TABLE IF EXISTS postgresql_composites' + @connection.execute 'DROP TYPE IF EXISTS full_address' + end + + def setup + @connection = ActiveRecord::Base.connection + @connection.transaction do + @connection.execute <<-SQL + CREATE TYPE full_address AS + ( + city VARCHAR(90), + street VARCHAR(90) + ); + SQL + @connection.create_table('postgresql_composites') do |t| + t.column :address, :full_address + end + end + end + + def test_composite_mapping + @connection.execute "INSERT INTO postgresql_composites VALUES (1, ROW('Paris', 'Champs-Élysées'));" + composite = PostgresqlComposite.first + assert_equal "(Paris,Champs-Élysées)", composite.address + + composite.address = "(Paris,Rue Basse)" + composite.save! + + assert_equal '(Paris,"Rue Basse")', composite.reload.address + end +end diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index 90cca7d3e6..4715fa002d 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -91,40 +91,50 @@ module ActiveRecord assert_operator plan.length, :>, 0 end - # Must have with_manual_interventions set to true for this - # test to run. + # Must have PostgreSQL >= 9.2, or with_manual_interventions set to + # true for this test to run. + # # When prompted, restart the PostgreSQL server with the # "-m fast" option or kill the individual connection assuming # you know the incantation to do that. # To restart PostgreSQL 9.1 on OS X, installed via MacPorts, ... # sudo su postgres -c "pg_ctl restart -D /opt/local/var/db/postgresql91/defaultdb/ -m fast" - if ARTest.config['with_manual_interventions'] - def test_reconnection_after_actual_disconnection_with_verify - original_connection_pid = @connection.query('select pg_backend_pid()') + def test_reconnection_after_actual_disconnection_with_verify + original_connection_pid = @connection.query('select pg_backend_pid()') - # Sanity check. - assert @connection.active? + # Sanity check. + assert @connection.active? + if @connection.send(:postgresql_version) >= 90200 + secondary_connection = ActiveRecord::Base.connection_pool.checkout + secondary_connection.query("select pg_terminate_backend(#{original_connection_pid.first.first})") + ActiveRecord::Base.connection_pool.checkin(secondary_connection) + elsif ARTest.config['with_manual_interventions'] puts 'Kill the connection now (e.g. by restarting the PostgreSQL ' + 'server with the "-m fast" option) and then press enter.' $stdin.gets + else + # We're not capable of terminating the backend ourselves, and + # we're not allowed to seek assistance; bail out without + # actually testing anything. + return + end - @connection.verify! + @connection.verify! - assert @connection.active? + assert @connection.active? - # If we get no exception here, then either we re-connected successfully, or - # we never actually got disconnected. - new_connection_pid = @connection.query('select pg_backend_pid()') + # If we get no exception here, then either we re-connected successfully, or + # we never actually got disconnected. + new_connection_pid = @connection.query('select pg_backend_pid()') - assert_not_equal original_connection_pid, new_connection_pid, - "umm -- looks like you didn't break the connection, because we're still " + - "successfully querying with the same connection pid." + assert_not_equal original_connection_pid, new_connection_pid, + "umm -- looks like you didn't break the connection, because we're still " + + "successfully querying with the same connection pid." - # Repair all fixture connections so other tests won't break. - @fixture_connections.each do |c| - c.verify! - end + # Repair all fixture connections so other tests won't break. + @fixture_connections.each do |c| + c.verify! 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 131080913c..019406dd84 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -6,21 +6,21 @@ module ActiveRecord class PostgreSQLAdapterTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection - @connection.exec_query('drop table if exists ex') - @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') + connection.exec_query('SELECT 1') end end def test_valid_column - column = @connection.columns('ex').find { |col| col.name == 'id' } - assert @connection.valid_type?(column.type) + with_example_table do + column = @connection.columns('ex').find { |col| col.name == 'id' } + assert @connection.valid_type?(column.type) + end end def test_invalid_column @@ -28,7 +28,9 @@ module ActiveRecord end def test_primary_key - assert_equal 'id', @connection.primary_key('ex') + with_example_table do + assert_equal 'id', @connection.primary_key('ex') + end end def test_primary_key_works_tables_containing_capital_letters @@ -36,15 +38,15 @@ module ActiveRecord end def test_non_standard_primary_key - @connection.exec_query('drop table if exists ex') - @connection.exec_query('create table ex(data character varying(255) primary key)') - assert_equal 'data', @connection.primary_key('ex') + with_example_table 'data character varying(255) primary key' do + assert_equal 'data', @connection.primary_key('ex') + end end def test_primary_key_returns_nil_for_no_pk - @connection.exec_query('drop table if exists ex') - @connection.exec_query('create table ex(id integer)') - assert_nil @connection.primary_key('ex') + with_example_table 'id integer' do + assert_nil @connection.primary_key('ex') + end end def test_primary_key_raises_error_if_table_not_found @@ -54,32 +56,40 @@ module ActiveRecord end def test_insert_sql_with_proprietary_returning_clause - id = @connection.insert_sql("insert into ex (number) values(5150)", nil, "number") - assert_equal "5150", id + with_example_table do + id = @connection.insert_sql("insert into ex (number) values(5150)", nil, "number") + assert_equal "5150", id + end end def test_insert_sql_with_quoted_schema_and_table_name - id = @connection.insert_sql('insert into "public"."ex" (number) values(5150)') - expect = @connection.query('select max(id) from ex').first.first - assert_equal expect, id + with_example_table do + id = @connection.insert_sql('insert into "public"."ex" (number) values(5150)') + expect = @connection.query('select max(id) from ex').first.first + assert_equal expect, id + end end def test_insert_sql_with_no_space_after_table_name - id = @connection.insert_sql("insert into ex(number) values(5150)") - expect = @connection.query('select max(id) from ex').first.first - assert_equal expect, id + with_example_table do + id = @connection.insert_sql("insert into ex(number) values(5150)") + expect = @connection.query('select max(id) from ex').first.first + assert_equal expect, id + end 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 + with_example_table do + 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 end def test_insert_sql_with_returning_disabled @@ -135,29 +145,31 @@ module ActiveRecord end def test_pk_and_sequence_for - pk, seq = @connection.pk_and_sequence_for('ex') - assert_equal 'id', pk - assert_equal @connection.default_sequence_name('ex', 'id'), seq + with_example_table do + pk, seq = @connection.pk_and_sequence_for('ex') + assert_equal 'id', pk + assert_equal @connection.default_sequence_name('ex', 'id'), seq + end end def test_pk_and_sequence_for_with_non_standard_primary_key - @connection.exec_query('drop table if exists ex') - @connection.exec_query('create table ex(code serial primary key)') - pk, seq = @connection.pk_and_sequence_for('ex') - assert_equal 'code', pk - assert_equal @connection.default_sequence_name('ex', 'code'), seq + with_example_table 'code serial primary key' do + pk, seq = @connection.pk_and_sequence_for('ex') + assert_equal 'code', pk + assert_equal @connection.default_sequence_name('ex', 'code'), seq + end end def test_pk_and_sequence_for_returns_nil_if_no_seq - @connection.exec_query('drop table if exists ex') - @connection.exec_query('create table ex(id integer primary key)') - assert_nil @connection.pk_and_sequence_for('ex') + with_example_table 'id integer primary key' do + assert_nil @connection.pk_and_sequence_for('ex') + end end def test_pk_and_sequence_for_returns_nil_if_no_pk - @connection.exec_query('drop table if exists ex') - @connection.exec_query('create table ex(id integer)') - assert_nil @connection.pk_and_sequence_for('ex') + with_example_table 'id integer' do + assert_nil @connection.pk_and_sequence_for('ex') + end end def test_pk_and_sequence_for_returns_nil_if_table_not_found @@ -165,23 +177,27 @@ module ActiveRecord end def test_exec_insert_number - insert(@connection, 'number' => 10) + with_example_table do + insert(@connection, 'number' => 10) - result = @connection.exec_query('SELECT number FROM ex WHERE number = 10') + result = @connection.exec_query('SELECT number FROM ex WHERE number = 10') - assert_equal 1, result.rows.length - assert_equal "10", result.rows.last.last + assert_equal 1, result.rows.length + assert_equal "10", result.rows.last.last + end end def test_exec_insert_string - str = 'いただきます!' - insert(@connection, 'number' => 10, 'data' => str) + with_example_table do + str = 'いただきます!' + insert(@connection, 'number' => 10, 'data' => str) - result = @connection.exec_query('SELECT number, data FROM ex WHERE number = 10') + result = @connection.exec_query('SELECT number, data FROM ex WHERE number = 10') - value = result.rows.last.last + value = result.rows.last.last - assert_equal str, value + assert_equal str, value + end end def test_table_alias_length @@ -191,44 +207,50 @@ module ActiveRecord end def test_exec_no_binds - result = @connection.exec_query('SELECT id, data FROM ex') - assert_equal 0, result.rows.length - assert_equal 2, result.columns.length - assert_equal %w{ id data }, result.columns - - string = @connection.quote('foo') - @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})") - result = @connection.exec_query('SELECT id, data FROM ex') - assert_equal 1, result.rows.length - assert_equal 2, result.columns.length - - assert_equal [['1', 'foo']], result.rows + with_example_table do + result = @connection.exec_query('SELECT id, data FROM ex') + assert_equal 0, result.rows.length + assert_equal 2, result.columns.length + assert_equal %w{ id data }, result.columns + + string = @connection.quote('foo') + @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})") + result = @connection.exec_query('SELECT id, data FROM ex') + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length + + assert_equal [['1', 'foo']], result.rows + end end def test_exec_with_binds - string = @connection.quote('foo') - @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})") - result = @connection.exec_query( - 'SELECT id, data FROM ex WHERE id = $1', nil, [[nil, 1]]) + with_example_table do + string = @connection.quote('foo') + @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})") + result = @connection.exec_query( + 'SELECT id, data FROM ex WHERE id = $1', nil, [[nil, 1]]) - assert_equal 1, result.rows.length - assert_equal 2, result.columns.length + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length - assert_equal [['1', 'foo']], result.rows + assert_equal [['1', 'foo']], result.rows + end end def test_exec_typecasts_bind_vals - string = @connection.quote('foo') - @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})") + with_example_table do + string = @connection.quote('foo') + @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})") - column = @connection.columns('ex').find { |col| col.name == 'id' } - result = @connection.exec_query( - 'SELECT id, data FROM ex WHERE id = $1', nil, [[column, '1-fuu']]) + column = @connection.columns('ex').find { |col| col.name == 'id' } + result = @connection.exec_query( + 'SELECT id, data FROM ex WHERE id = $1', nil, [[column, '1-fuu']]) - assert_equal 1, result.rows.length - assert_equal 2, result.columns.length + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length - assert_equal [['1', 'foo']], result.rows + assert_equal [['1', 'foo']], result.rows + end end def test_substitute_at @@ -240,9 +262,11 @@ module ActiveRecord end def test_partial_index - @connection.add_index 'ex', %w{ id number }, :name => 'partial', :where => "number > 100" - index = @connection.indexes('ex').find { |idx| idx.name == 'partial' } - assert_equal "(number > 100)", index.where + with_example_table do + @connection.add_index 'ex', %w{ id number }, :name => 'partial', :where => "number > 100" + index = @connection.indexes('ex').find { |idx| idx.name == 'partial' } + assert_equal "(number > 100)", index.where + end end def test_columns_for_distinct_zero_orders @@ -300,6 +324,14 @@ module ActiveRecord ctx.exec_insert(sql, 'SQL', binds) end + def with_example_table(definition = nil) + definition ||= 'id serial primary key, number integer, data character varying(255)' + @connection.exec_query("create table ex(#{definition})") + yield + ensure + @connection.exec_query('drop table if exists ex') + end + def connection_without_insert_returning ActiveRecord::Base.postgresql_connection(ActiveRecord::Base.configurations['arunit'].merge(:insert_returning => false)) end diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 1e59bca6a7..3f7009c1d1 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -246,6 +246,18 @@ class SchemaTest < ActiveRecord::TestCase assert_nothing_raised { with_schema_search_path nil } end + def test_index_name_exists + with_schema_search_path(SCHEMA_NAME) do + assert @connection.index_name_exists?(TABLE_NAME, INDEX_A_NAME, true) + assert @connection.index_name_exists?(TABLE_NAME, INDEX_B_NAME, true) + assert @connection.index_name_exists?(TABLE_NAME, INDEX_C_NAME, true) + assert @connection.index_name_exists?(TABLE_NAME, INDEX_D_NAME, true) + assert @connection.index_name_exists?(TABLE_NAME, INDEX_E_NAME, true) + assert @connection.index_name_exists?(TABLE_NAME, INDEX_E_NAME, true) + assert_not @connection.index_name_exists?(TABLE_NAME, 'missing_index', true) + end + end + def test_dump_indexes_for_schema_one do_dump_index_tests_for_schema(SCHEMA_NAME, INDEX_A_COLUMN, INDEX_B_COLUMN_S1, INDEX_D_COLUMN, INDEX_E_COLUMN) end diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb index 89210866f0..4d29a20e66 100644 --- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb +++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb @@ -77,7 +77,7 @@ class TimestampTest < ActiveRecord::TestCase end def test_bc_timestamp - date = Date.new(0) - 1.second + date = Date.new(0) - 1.week Developer.create!(:name => "aaron", :updated_at => date) assert_equal date, Developer.find_by_name("aaron").updated_at end diff --git a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb index e78cb88562..b478db749d 100644 --- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb @@ -1,7 +1,7 @@ require "cases/helper" class CopyTableTest < ActiveRecord::TestCase - fixtures :customers, :companies, :comments, :binaries + fixtures :customers def setup @connection = ActiveRecord::Base.connection diff --git a/activerecord/test/cases/associations/association_scope_test.rb b/activerecord/test/cases/associations/association_scope_test.rb index a5ae3cbb20..3e0032ec73 100644 --- a/activerecord/test/cases/associations/association_scope_test.rb +++ b/activerecord/test/cases/associations/association_scope_test.rb @@ -6,8 +6,9 @@ module ActiveRecord module Associations class AssociationScopeTest < ActiveRecord::TestCase test 'does not duplicate conditions' do - association_scope = AssociationScope.new(Author.new.association(:welcome_posts)) - scope = association_scope.scope + scope = AssociationScope.scope(Author.new.association(:welcome_posts), + Author.connection) + wheres = scope.where_values.map(&:right) binds = scope.bind_values.map(&:last) wheres = scope.where_values.map(&:right).reject { |node| Arel::Nodes::BindParam === node diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 3205d0c28b..9340bc0a83 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 @@ -846,4 +853,14 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert post.save assert_equal post.author_id, author2.id end + + test 'dangerous association name raises ArgumentError' do + [:errors, 'errors', :save, 'save'].each do |name| + assert_raises(ArgumentError, "Association #{name} should not be allowed") do + Class.new(ActiveRecord::Base) do + belongs_to name + end + end + end + end end diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index 8aee7ff40e..bac1cb8e2d 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 @@ -775,6 +775,16 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert project.developers.include?(developer) end + def test_destruction_does_not_error_without_primary_key + redbeard = pirates(:redbeard) + george = parrots(:george) + redbeard.parrots << george + assert_equal 2, george.pirates.count + Pirate.includes(:parrots).where(parrot: redbeard.parrot).find(redbeard.id).destroy + assert_equal 1, george.pirates.count + assert_equal [], Pirate.where(id: redbeard.id) + end + test "has and belongs to many associations on new records use null relations" do projects = Developer.new.projects assert_no_queries do diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index e45efb0161..321440cab7 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -22,6 +22,8 @@ require 'models/engine' require 'models/categorization' require 'models/minivan' require 'models/speedometer' +require 'models/pirate' +require 'models/ship' class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCase fixtures :authors, :posts, :comments @@ -216,6 +218,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 +269,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 +281,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 +291,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 +310,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 +324,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 @@ -355,7 +382,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 @@ -364,7 +391,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 @@ -434,15 +461,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 @@ -462,25 +489,25 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [1], posts(:welcome).comments.select { |c| c.id == 1 }.map(&:id) end - def test_select_without_foreign_key + 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 @@ -519,8 +546,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 @@ -573,7 +600,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 @@ -649,14 +676,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 @@ -669,7 +696,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 @@ -681,8 +708,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 @@ -779,8 +806,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 @@ -789,7 +816,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 @@ -799,7 +826,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 @@ -832,7 +859,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 @@ -866,7 +893,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 @@ -897,7 +924,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] @@ -953,10 +980,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 @@ -988,8 +1015,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 @@ -1026,8 +1053,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 @@ -1037,8 +1064,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 @@ -1048,21 +1075,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 @@ -1078,7 +1105,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 @@ -1091,14 +1118,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 @@ -1113,12 +1140,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 @@ -1239,7 +1266,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 @@ -1254,7 +1281,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 @@ -1263,7 +1290,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 @@ -1357,9 +1384,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 @@ -1384,30 +1412,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 @@ -1494,7 +1525,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 @@ -1791,4 +1822,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase topic.approved_replies.create! end end + + test 'dangerous association name raises ArgumentError' do + [:errors, 'errors', :save, 'save'].each do |name| + assert_raises(ArgumentError, "Association #{name} should not be allowed") do + Class.new(ActiveRecord::Base) do + has_many name + end + end + end + end + + test 'has_many_association passes context validation to validate children' do + pirate = FamousPirate.new + pirate.famous_ships << ship = FamousShip.new + assert_equal true, pirate.valid? + assert_equal false, pirate.valid?(:conference) + assert_equal "can't be blank", ship.errors[:name].first + 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..a4650ccdf2 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 } @@ -557,4 +564,14 @@ class HasOneAssociationsTest < ActiveRecord::TestCase end end end + + test 'dangerous association name raises ArgumentError' do + [:errors, 'errors', :save, 'save'].each do |name| + assert_raises(ArgumentError, "Association #{name} should not be allowed") do + Class.new(ActiveRecord::Base) do + has_one name + end + end + end + end 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 fe5de44409..c55dd685a1 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -17,6 +17,8 @@ require 'models/tag' require 'models/tagging' require 'models/treasure' require 'models/eye' +require 'models/electron' +require 'models/molecule' class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase def test_should_not_add_the_same_callbacks_multiple_times_for_has_one @@ -343,6 +345,33 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test end end +class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttributes < ActiveRecord::TestCase + def test_invalid_adding_with_nested_attributes + molecule = Molecule.new + valid_electron = Electron.new(name: 'electron') + invalid_electron = Electron.new + + molecule.electrons = [valid_electron, invalid_electron] + molecule.save + + assert_not invalid_electron.valid? + assert valid_electron.valid? + assert_not molecule.persisted?, 'Molecule should not be persisted when its electrons are invalid' + end + + def test_valid_adding_with_nested_attributes + molecule = Molecule.new + valid_electron = Electron.new(name: 'electron') + + molecule.electrons = [valid_electron] + molecule.save + + assert valid_electron.valid? + assert molecule.persisted? + assert_equal 1, molecule.electrons.count + end +end + class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase fixtures :companies, :people @@ -401,7 +430,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 +484,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 +493,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 +504,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 +517,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 @@ -1183,15 +1212,15 @@ module AutosaveAssociationOnACollectionAssociationTests end def test_should_default_invalid_error_from_i18n - I18n.backend.store_translations(:en, :activerecord => {:errors => { :models => - { @association_name.to_s.singularize.to_sym => { :blank => "cannot be blank" } } + I18n.backend.store_translations(:en, activerecord: {errors: { models: + { @associated_model_name.to_s.to_sym => { blank: "cannot be blank" } } }}) - @pirate.send(@association_name).build(:name => '') + @pirate.send(@association_name).build(name: '') assert !@pirate.valid? assert_equal ["cannot be blank"], @pirate.errors["#{@association_name}.name"] - assert_equal ["#{@association_name.to_s.titleize} name cannot be blank"], @pirate.errors.full_messages + assert_equal ["#{@association_name.to_s.humanize} name cannot be blank"], @pirate.errors.full_messages assert @pirate.errors[@association_name].empty? ensure I18n.backend = I18n::Backend::Simple.new @@ -1307,6 +1336,7 @@ class TestAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase def setup super @association_name = :birds + @associated_model_name = :bird @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?") @child_1 = @pirate.birds.create(:name => 'Posideons Killer') @@ -1321,12 +1351,30 @@ class TestAutosaveAssociationOnAHasAndBelongsToManyAssociation < ActiveRecord::T def setup super + @association_name = :autosaved_parrots + @associated_model_name = :parrot + @habtm = true + + @pirate = Pirate.create(catchphrase: "Don' botharrr talkin' like one, savvy?") + @child_1 = @pirate.parrots.create(name: 'Posideons Killer') + @child_2 = @pirate.parrots.create(name: 'Killer bandita Dionne') + end + + include AutosaveAssociationOnACollectionAssociationTests +end + +class TestAutosaveAssociationOnAHasAndBelongsToManyAssociationWithAcceptsNestedAttributes < ActiveRecord::TestCase + self.use_transactional_fixtures = false unless supports_savepoints? + + def setup + super @association_name = :parrots + @associated_model_name = :parrot @habtm = true - @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?") - @child_1 = @pirate.parrots.create(:name => 'Posideons Killer') - @child_2 = @pirate.parrots.create(:name => 'Killer bandita Dionne') + @pirate = Pirate.create(catchphrase: "Don' botharrr talkin' like one, savvy?") + @child_1 = @pirate.parrots.create(name: 'Posideons Killer') + @child_2 = @pirate.parrots.create(name: 'Killer bandita Dionne') end include AutosaveAssociationOnACollectionAssociationTests diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 983bcd9826..8a0b0b9589 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -321,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 @@ -1380,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 38c2560d69..c12fa03015 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -35,6 +35,14 @@ class EachTest < ActiveRecord::TestCase end end + if Enumerator.method_defined? :size + def test_each_should_return_a_sized_enumerator + assert_equal 11, Post.find_each(:batch_size => 1).size + assert_equal 5, Post.find_each(:batch_size => 2, :start => 7).size + assert_equal 11, Post.find_each(:batch_size => 10_000).size + end + end + def test_each_enumerator_should_execute_one_query_per_batch assert_queries(@total + 1) do Post.find_each(:batch_size => 1).with_index do |post, index| @@ -46,7 +54,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 @@ -151,6 +161,12 @@ class EachTest < ActiveRecord::TestCase assert_equal special_posts_ids, posts.map(&:id) end + def test_find_in_batches_should_not_modify_passed_options + assert_nothing_raised do + Post.find_in_batches({ batch_size: 42, start: 1 }.freeze){} + end + end + def test_find_in_batches_should_use_any_column_as_primary_key nick_order_subscribers = Subscriber.order('nick asc') start_nick = nick_order_subscribers.second.nick @@ -170,4 +186,27 @@ class EachTest < ActiveRecord::TestCase end end end + + def test_find_in_batches_should_return_an_enumerator + enum = nil + assert_queries(0) do + enum = Post.find_in_batches(:batch_size => 1) + end + assert_queries(4) do + enum.first(4) do |batch| + assert_kind_of Array, batch + assert_kind_of Post, batch.first + end + end + end + + if Enumerator.method_defined? :size + def test_find_in_batches_should_return_a_sized_enumerator + assert_equal 11, Post.find_in_batches(:batch_size => 1).size + assert_equal 6, Post.find_in_batches(:batch_size => 2).size + assert_equal 4, Post.find_in_batches(:batch_size => 2, :start => 4).size + assert_equal 4, Post.find_in_batches(:batch_size => 3).size + assert_equal 1, Post.find_in_batches(:batch_size => 10_000).size + end + end end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 2f6913167d..db999f90ab 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -278,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 @@ -286,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 @@ -466,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 @@ -509,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 @@ -557,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 @@ -587,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/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb index dbb2f223cd..c7b64f29c3 100644 --- a/activerecord/test/cases/column_definition_test.rb +++ b/activerecord/test/cases/column_definition_test.rb @@ -82,7 +82,7 @@ module ActiveRecord assert_equal "", not_null_text_column.default end - def test_has_default_should_return_false_for_blog_and_test_data_types + def test_has_default_should_return_false_for_blob_and_text_data_types blob_column = MysqlAdapter::Column.new("title", nil, "blob") assert !blob_column.has_default? @@ -116,7 +116,7 @@ module ActiveRecord assert_equal "", not_null_text_column.default end - def test_has_default_should_return_false_for_blog_and_test_data_types + def test_has_default_should_return_false_for_blob_and_text_data_types blob_column = Mysql2Adapter::Column.new("title", nil, "blob") assert !blob_column.has_default? 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_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 2da51ea015..1cf215017b 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -142,7 +142,7 @@ module ActiveRecord connections = @pool.connections.dup connections.each do |conn| - conn.extend(Module.new { def active?; false; end; }) + conn.extend(Module.new { def active_threadsafe?; false; end; }) end @pool.reap diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index 1f98801e93..1b95708cb3 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -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 @@ -92,4 +163,63 @@ class EnumTest < ActiveRecord::TestCase 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 + 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 9b575557de..b1eded6494 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -254,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! @@ -267,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! @@ -812,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 @@ -863,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 @@ -891,4 +988,12 @@ class FinderTest < ActiveRecord::TestCase ActiveRecord::Base.send(:replace_bind_variables, statement, vars) end end + + 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/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index f3a4887a85..37c6af74da 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -628,7 +628,9 @@ class LoadAllFixturesTest < ActiveRecord::TestCase self.class.fixture_path = FIXTURES_ROOT + "/all" self.class.fixtures :all - assert_equal %w(admin/accounts admin/users developers people tasks), fixture_table_names.sort + if File.symlink? FIXTURES_ROOT + "/all/admin" + assert_equal %w(admin/accounts admin/users developers people tasks), fixture_table_names.sort + end ensure ActiveRecord::FixtureSet.reset_cache end @@ -639,7 +641,9 @@ class LoadAllFixturesWithPathnameTest < ActiveRecord::TestCase self.class.fixture_path = Pathname.new(FIXTURES_ROOT).join('all') self.class.fixtures :all - assert_equal %w(admin/accounts admin/users developers people tasks), fixture_table_names.sort + if File.symlink? FIXTURES_ROOT + "/all/admin" + assert_equal %w(admin/accounts admin/users developers people tasks), fixture_table_names.sort + end ensure ActiveRecord::FixtureSet.reset_cache end diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index d2b5a06b55..e2ff2aa451 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -222,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 diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb index 428145d00b..debacf815c 100644 --- a/activerecord/test/cases/invertible_migration_test.rb +++ b/activerecord/test/cases/invertible_migration_test.rb @@ -106,6 +106,22 @@ module ActiveRecord end end + class RevertNamedIndexMigration1 < SilentMigration + def change + create_table("horses") do |t| + t.column :content, :string + t.column :remind_at, :datetime + end + add_index :horses, :content + end + end + + class RevertNamedIndexMigration2 < SilentMigration + def change + add_index :horses, :content, name: "horses_index_named" + end + end + def teardown %w[horses new_horses].each do |table| if ActiveRecord::Base.connection.table_exists?(table) @@ -255,5 +271,17 @@ module ActiveRecord ActiveRecord::Base.table_name_prefix = ActiveRecord::Base.table_name_suffix = '' end + def test_migrate_revert_add_index_with_name + RevertNamedIndexMigration1.new.migrate(:up) + RevertNamedIndexMigration2.new.migrate(:up) + RevertNamedIndexMigration2.new.migrate(:down) + + connection = ActiveRecord::Base.connection + assert connection.index_exists?(:horses, :content), + "index on content should exist" + assert !connection.index_exists?(:horses, :content, name: "horses_index_named"), + "horses_index_named index should not exist" + end + end end 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/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 35b656ee43..a925cf4c05 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -174,13 +174,13 @@ module ActiveRecord end def test_invert_add_index - remove = @recorder.inverse_of :add_index, [:table, [:one, :two], options: true] - assert_equal [:remove_index, [:table, {column: [:one, :two], options: true}]], remove + remove = @recorder.inverse_of :add_index, [:table, [:one, :two]] + assert_equal [:remove_index, [:table, {column: [:one, :two]}]], remove end def test_invert_add_index_with_name remove = @recorder.inverse_of :add_index, [:table, [:one, :two], name: "new_index"] - assert_equal [:remove_index, [:table, {column: [:one, :two], name: "new_index"}]], remove + assert_equal [:remove_index, [:table, {name: "new_index"}]], remove end def test_invert_add_index_with_no_options diff --git a/activerecord/test/cases/mixin_test.rb b/activerecord/test/cases/mixin_test.rb index ad0d5cce27..7ddb2bfee1 100644 --- a/activerecord/test/cases/mixin_test.rb +++ b/activerecord/test/cases/mixin_test.rb @@ -6,10 +6,14 @@ end class TouchTest < ActiveRecord::TestCase fixtures :mixins - def setup + setup do travel_to Time.now end + teardown do + travel_back + end + def test_update stamped = Mixin.new diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 6f1e518f45..b9f0624f76 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -740,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! @@ -761,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/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 1b915387be..51ddd406ed 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -180,6 +180,11 @@ class PrimaryKeysTest < ActiveRecord::TestCase assert !col1.equal?(col2) end end + + def test_auto_detect_primary_key_from_schema + MixedCaseMonkey.reset_primary_key + assert_equal "monkeyID", MixedCaseMonkey.primary_key + end end class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase @@ -214,4 +219,3 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter) end end end - diff --git a/activerecord/test/cases/reaper_test.rb b/activerecord/test/cases/reaper_test.rb index e53a27d5dd..b62a41c08e 100644 --- a/activerecord/test/cases/reaper_test.rb +++ b/activerecord/test/cases/reaper_test.rb @@ -69,7 +69,7 @@ module ActiveRecord conn = pool.checkout count = pool.connections.length - conn.extend(Module.new { def active?; false; end; }) + conn.extend(Module.new { def active_threadsafe?; false; end; }) while count == pool.connections.length Thread.pass 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/relations_test.rb b/activerecord/test/cases/relations_test.rb index 777b5060b7..f45b578c0f 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? @@ -164,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 @@ -197,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 @@ -775,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 @@ -1505,6 +1536,12 @@ 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) diff --git a/activerecord/test/cases/result_test.rb b/activerecord/test/cases/result_test.rb index b6c583dbf5..2131b32a0c 100644 --- a/activerecord/test/cases/result_test.rb +++ b/activerecord/test/cases/result_test.rb @@ -5,14 +5,16 @@ module ActiveRecord def result Result.new(['col_1', 'col_2'], [ ['row 1 col 1', 'row 1 col 2'], - ['row 2 col 1', 'row 2 col 2'] + ['row 2 col 1', 'row 2 col 2'], + ['row 3 col 1', 'row 3 col 2'], ]) end def test_to_hash_returns_row_hashes assert_equal [ {'col_1' => 'row 1 col 1', 'col_2' => 'row 1 col 2'}, - {'col_1' => 'row 2 col 1', 'col_2' => 'row 2 col 2'} + {'col_1' => 'row 2 col 1', 'col_2' => 'row 2 col 2'}, + {'col_1' => 'row 3 col 1', 'col_2' => 'row 3 col 2'}, ], result.to_hash end @@ -28,5 +30,11 @@ module ActiveRecord assert_kind_of Integer, index end end + + if Enumerator.method_defined? :size + def test_each_without_block_returns_a_sized_enumerator + assert_equal 3, result.each.size + end + end end end diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb index 766b2ff2ef..954eab8022 100644 --- a/activerecord/test/cases/sanitize_test.rb +++ b/activerecord/test/cases/sanitize_test.rb @@ -46,4 +46,9 @@ class SanitizeTest < ActiveRecord::TestCase select_author_sql = Post.send(:sanitize_sql_array, ['id in (:post_ids)', post_ids: david_posts]) assert_match(sub_query_pattern, select_author_sql, 'should sanitize `Relation` as subquery for named bind variables') end + + def test_sanitize_sql_array_handles_empty_statement + select_author_sql = Post.send(:sanitize_sql_array, ['']) + assert_equal('', select_author_sql) + end end diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index 71754cf0a2..170e9a49eb 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -149,6 +149,16 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal expected, received end + def test_unscope_string_where_clauses_involved + dev_relation = Developer.order('salary DESC').where("created_at > ?", 1.year.ago) + expected = dev_relation.collect { |dev| dev.name } + + dev_ordered_relation = DeveloperOrderedBySalary.where(name: 'Jamis').where("created_at > ?", 1.year.ago) + received = dev_ordered_relation.unscope(where: [:name]).collect { |dev| dev.name } + + assert_equal expected, received + end + def test_unscope_with_grouping_attributes expected = Developer.order('salary DESC').collect { |dev| dev.name } received = DeveloperOrderedBySalary.group(:name).unscope(:group).collect { |dev| dev.name } diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index 72c9787b84..f0ad9ebb8a 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -266,6 +266,65 @@ 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 + silence_warnings do + klass.class_eval { scope name, ->{ where(approved: true) } } + end + 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 +403,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 +425,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 6f632b4d8d..978cee9cfb 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -166,4 +166,28 @@ class StoreTest < ActiveRecord::TestCase test "YAML coder initializes the store when a Nil value is given" do assert_equal({}, @john.params) end + + test "attributes_for_coder should return stored fields already serialized" do + attributes = { + "id" => @john.id, + "name"=> @john.name, + "settings" => "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\ncolor: black\n", + "preferences" => "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nremember_login: true\n", + "json_data" => "{\"height\":\"tall\"}", "json_data_empty"=>"{\"is_a_good_guy\":true}", + "params" => "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess {}\n", + "account_id"=> @john.account_id + } + + assert_equal attributes, @john.attributes_for_coder + end + + test "dump, load and dump again a model" do + dumped = YAML.dump(@john) + loaded = YAML.load(dumped) + assert_equal @john, loaded + + second_dump = YAML.dump(loaded) + assert_equal dumped, second_dump + assert_equal @john, YAML.load(second_dump) + end end diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index 7e7d95841b..3d64ecb464 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -16,6 +16,11 @@ class TransactionCallbacksTest < ActiveRecord::TestCase after_commit :do_after_commit, on: :create + attr_accessor :save_on_after_create + after_create do + self.save! if save_on_after_create + end + def history @history ||= [] end @@ -107,6 +112,16 @@ class TransactionCallbacksTest < ActiveRecord::TestCase assert_equal [], reply.history end + def test_only_call_after_commit_on_create_and_doesnt_leaky + r = ReplyWithCallbacks.new(content: 'foo') + r.save_on_after_create = true + r.save! + r.content = 'bar' + r.save! + r.save! + assert_equal [:commit_on_create], r.history + end + def test_only_call_after_commit_on_update_after_transaction_commits_for_existing_record_on_touch add_transaction_execution_blocks @first 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/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/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/electron.rb b/activerecord/test/models/electron.rb index 35af9f679b..6fc270673f 100644 --- a/activerecord/test/models/electron.rb +++ b/activerecord/test/models/electron.rb @@ -1,3 +1,5 @@ class Electron < ActiveRecord::Base belongs_to :molecule + + validates_presence_of :name end diff --git a/activerecord/test/models/mixed_case_monkey.rb b/activerecord/test/models/mixed_case_monkey.rb index 4d37371777..1c35006665 100644 --- a/activerecord/test/models/mixed_case_monkey.rb +++ b/activerecord/test/models/mixed_case_monkey.rb @@ -1,5 +1,3 @@ class MixedCaseMonkey < ActiveRecord::Base - self.primary_key = 'monkeyID' - belongs_to :man end diff --git a/activerecord/test/models/molecule.rb b/activerecord/test/models/molecule.rb index 69325b8d29..26870c8f88 100644 --- a/activerecord/test/models/molecule.rb +++ b/activerecord/test/models/molecule.rb @@ -1,4 +1,6 @@ class Molecule < ActiveRecord::Base belongs_to :liquid has_many :electrons + + accepts_nested_attributes_for :electrons end diff --git a/activerecord/test/models/pirate.rb b/activerecord/test/models/pirate.rb index 170fc2ffe3..8510c596a7 100644 --- a/activerecord/test/models/pirate.rb +++ b/activerecord/test/models/pirate.rb @@ -13,6 +13,7 @@ class Pirate < ActiveRecord::Base :after_add => proc {|p,pa| p.ship_log << "after_adding_proc_parrot_#{pa.id || '<new>'}"}, :before_remove => proc {|p,pa| p.ship_log << "before_removing_proc_parrot_#{pa.id}"}, :after_remove => proc {|p,pa| p.ship_log << "after_removing_proc_parrot_#{pa.id}"} + has_and_belongs_to_many :autosaved_parrots, class_name: "Parrot", autosave: true has_many :treasures, :as => :looter has_many :treasure_estimates, :through => :treasures, :source => :price_estimates @@ -84,3 +85,11 @@ end class DestructivePirate < Pirate has_one :dependent_ship, :class_name => 'Ship', :foreign_key => :pirate_id, :dependent => :destroy end + +class FamousPirate < ActiveRecord::Base + self.table_name = 'pirates' + + has_many :famous_ships + + validates_presence_of :catchphrase, on: :conference +end diff --git a/activerecord/test/models/ship.rb b/activerecord/test/models/ship.rb index 3da031946f..7a369b9d9a 100644 --- a/activerecord/test/models/ship.rb +++ b/activerecord/test/models/ship.rb @@ -17,3 +17,11 @@ class Ship < ActiveRecord::Base false end end + +class FamousShip < ActiveRecord::Base + self.table_name = 'ships' + + belongs_to :famous_pirate + + validates_presence_of :name, on: :conference +end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 9a7d918a25..99a53434f6 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -97,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| diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index adaeb99c87..43bfeff079 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,4 +1,90 @@ -* Added `Hash#compact` and `Hash#compact!` for removing items with nil value from hash. +* Fix the implementation of Multibyte::Unicode.tidy_bytes for JRuby + + The existing implementation caused JRuby to raise the error: + `Encoding::ConverterNotFoundError: code converter not found (UTF-8 to UTF8-MAC)` + + *Justin Coyne* + +* Fix `to_param` behavior when there are nested empty hashes. + + Before: + + params = {c: 3, d: {}}.to_param # => "&c=3" + + After: + + params = {c: 3, d: {}}.to_param # => "c=3&d=" + + Fixes #13892. + + *Hincu Petru* + +* Deprecate custom `BigDecimal` serialization. + + Deprecate the custom `BigDecimal` serialization that is included when requiring + `active_support/all` as a fix for #12467. Let Ruby handle YAML serialization + for `BigDecimal` instead. + + *David Celis* + +* Fix parsing bugs in `XmlMini` + + Symbols or boolean parsing would raise an error for non string values (e.g. + integers). Decimal parsing would fail due to a missing requirement. + + *Birkir A. Barkarson* + +* Maintain the current timezone when calling `wrap_with_time_zone` + + Extend the solution from the fix for #12163 to the general case where `Time` + methods are wrapped with a time zone. + + Fixes #12596. + + *Andrew White* + +* Remove behavior that automatically remove the Date/Time stubs, added by `travel` + and `travel_to` methods, after each test case. + + Now users have to use the `travel_back` or the block version of `travel` and + `travel_to` methods to clean the stubs. + + *Rafael Mendonça França* + +* Add `travel_back` to remove stubs from `travel` and `travel_to`. + + *Rafael Mendonça França* + +* Remove the deprecation about the `#filter` method. + + Filter objects should now rely on method corresponding to the filter type + (e.g. `#before`). + + *Aaron Patterson* + +* Add `ActiveSupport::JSON::Encoding.time_precision` as a way to configure the + precision of encoded time values: + + 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* diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 53154aef27..2b7f5943b5 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -452,7 +452,7 @@ module ActiveSupport # Clear the entire cache. Be careful with this method since it could # affect other processes if shared cache is being used. # - # Options are passed to the underlying cache implementation. + # The options hash is passed to the underlying cache implementation. # # All implementations may not support this method. def clear(options = nil) diff --git a/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb b/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb index 39b8cea807..843c592669 100644 --- a/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb +++ b/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb @@ -1,22 +1,7 @@ require 'bigdecimal' require 'bigdecimal/util' -require 'yaml' class BigDecimal - YAML_MAPPING = { 'Infinity' => '.Inf', '-Infinity' => '-.Inf', 'NaN' => '.NaN' } - - def encode_with(coder) - string = to_s - coder.represent_scalar(nil, YAML_MAPPING[string] || string) - end - - # Backport this method if it doesn't exist - unless method_defined?(:to_d) - def to_d - self - end - end - DEFAULT_STRING_FORMAT = 'F' def to_formatted_s(*args) if args[0].is_a?(Symbol) diff --git a/activesupport/lib/active_support/core_ext/big_decimal/yaml_conversions.rb b/activesupport/lib/active_support/core_ext/big_decimal/yaml_conversions.rb new file mode 100644 index 0000000000..46ba93ead4 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/big_decimal/yaml_conversions.rb @@ -0,0 +1,14 @@ +ActiveSupport::Deprecation.warn 'core_ext/big_decimal/yaml_conversions is deprecated and will be removed in the future.' + +require 'bigdecimal' +require 'yaml' +require 'active_support/core_ext/big_decimal/conversions' + +class BigDecimal + YAML_MAPPING = { 'Infinity' => '.Inf', '-Infinity' => '-.Inf', 'NaN' => '.NaN' } + + def encode_with(coder) + string = to_s + coder.represent_scalar(nil, YAML_MAPPING[string] || string) + end +end diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb index 4501b7ff58..1343beb87a 100644 --- a/activesupport/lib/active_support/core_ext/enumerable.rb +++ b/activesupport/lib/active_support/core_ext/enumerable.rb @@ -35,7 +35,7 @@ module Enumerable if block_given? Hash[map { |elem| [yield(elem), elem] }] else - to_enum :index_by + to_enum(:index_by) { size if respond_to?(:size) } end end diff --git a/activesupport/lib/active_support/core_ext/module/attr_internal.rb b/activesupport/lib/active_support/core_ext/module/attr_internal.rb index db07d549b0..67f0e0335d 100644 --- a/activesupport/lib/active_support/core_ext/module/attr_internal.rb +++ b/activesupport/lib/active_support/core_ext/module/attr_internal.rb @@ -27,7 +27,8 @@ class Module def attr_internal_define(attr_name, type) internal_name = attr_internal_ivar_name(attr_name).sub(/\A@/, '') - class_eval do # class_eval is necessary on 1.9 or else the methods a made private + # class_eval is necessary on 1.9 or else the methods are made private + class_eval do # use native attr_* methods as they are faster on some Ruby implementations send("attr_#{type}", internal_name) end diff --git a/activesupport/lib/active_support/core_ext/module/concerning.rb b/activesupport/lib/active_support/core_ext/module/concerning.rb index b22dc5ff1e..07a392404e 100644 --- a/activesupport/lib/active_support/core_ext/module/concerning.rb +++ b/activesupport/lib/active_support/core_ext/module/concerning.rb @@ -63,7 +63,7 @@ class Module # # == Mix-in noise exiled to its own file: # - # Once our chunk of behavior starts pushing the scroll-to-understand it + # Once our chunk of behavior starts pushing the scroll-to-understand it's # 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. 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/object/to_param.rb b/activesupport/lib/active_support/core_ext/object/to_param.rb index 3b137ce6ae..13be0038c2 100644 --- a/activesupport/lib/active_support/core_ext/object/to_param.rb +++ b/activesupport/lib/active_support/core_ext/object/to_param.rb @@ -51,8 +51,12 @@ class Hash # # This method is also aliased as +to_query+. def to_param(namespace = nil) - collect do |key, value| - value.to_query(namespace ? "#{namespace}[#{key}]" : key) - end.sort! * '&' + if empty? + namespace ? nil.to_query(namespace) : '' + else + collect do |key, value| + value.to_query(namespace ? "#{namespace}[#{key}]" : key) + end.sort! * '&' + end end end diff --git a/activesupport/lib/active_support/core_ext/object/to_query.rb b/activesupport/lib/active_support/core_ext/object/to_query.rb index 5d5fcf00e0..37352fa608 100644 --- a/activesupport/lib/active_support/core_ext/object/to_query.rb +++ b/activesupport/lib/active_support/core_ext/object/to_query.rb @@ -18,7 +18,12 @@ class Array # ['Rails', 'coding'].to_query('hobbies') # => "hobbies%5B%5D=Rails&hobbies%5B%5D=coding" def to_query(key) prefix = "#{key}[]" - collect { |value| value.to_query(prefix) }.join '&' + + if empty? + nil.to_query(prefix) + else + collect { |value| value.to_query(prefix) }.join '&' + end end end diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index 6be19771f5..59675d744e 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -407,7 +407,8 @@ module ActiveSupport #:nodoc: end def load_once_path?(path) - # to_s works around a ruby1.9 issue where #starts_with?(Pathname) will always return false + # to_s works around a ruby1.9 issue where String#starts_with?(Pathname) + # will raise a TypeError: no implicit conversion of Pathname into String autoload_once_paths.any? { |base| path.starts_with? base.to_s } end @@ -665,6 +666,14 @@ module ActiveSupport #:nodoc: constants = normalized.split('::') to_remove = constants.pop + # Remove the file path from the loaded list. + file_path = search_for_file(const.underscore) + if file_path + expanded = File.expand_path(file_path) + expanded.sub!(/\.rb\z/, '') + self.loaded.delete(expanded) + end + if constants.empty? parent = Object else diff --git a/activesupport/lib/active_support/inflections.rb b/activesupport/lib/active_support/inflections.rb index 4ea6abfa12..2ca1124e76 100644 --- a/activesupport/lib/active_support/inflections.rb +++ b/activesupport/lib/active_support/inflections.rb @@ -1,5 +1,11 @@ require 'active_support/inflector/inflections' +#-- +# Defines the standard inflection rules. These are the starting point for +# new projects and are not considered complete. The current set of inflection +# rules is frozen. This means, we do not change them to become more complete. +# This is a safety measure to keep existing applications from breaking. +#++ module ActiveSupport Inflector.inflections(:en) do |inflect| inflect.plural(/$/, 's') diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index cdee4c2ca5..b642d87d76 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -117,7 +117,7 @@ module ActiveSupport result.gsub!(/([a-z\d]*)/i) { |match| "#{inflections.acronyms[match] || match.downcase}" } - result.gsub!(/^\w/) { $&.upcase } if options.fetch(:capitalize, true) + result.gsub!(/^\w/) { |match| match.upcase } if options.fetch(:capitalize, true) result end diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index 2859075e10..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,7 +61,7 @@ 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 # Convert an object into a "JSON-ready" representation composed of @@ -105,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 @@ -161,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 7773611e11..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..." diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb index 84799c2399..ea3cdcd024 100644 --- a/activesupport/lib/active_support/multibyte/unicode.rb +++ b/activesupport/lib/active_support/multibyte/unicode.rb @@ -213,7 +213,7 @@ module ActiveSupport end # Ruby >= 2.1 has String#scrub, which is faster than the workaround used for < 2.1. - if RUBY_VERSION >= '2.1' + if '<3'.respond_to?(:scrub) # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent # resulting in a valid UTF-8 string. # @@ -233,16 +233,16 @@ module ActiveSupport # 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) + reader = Encoding::Converter.new(Encoding::UTF_8, Encoding::UTF_16LE) source = string.dup - out = ''.force_encoding(Encoding::UTF_8_MAC) + out = ''.force_encoding(Encoding::UTF_16LE) 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 << error_bytes.encode(Encoding::UTF_16LE, Encoding::Windows_1252, invalid: :replace, undef: :replace) end reader.finish diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb index 75ead48376..908af176be 100644 --- a/activesupport/lib/active_support/testing/isolation.rb +++ b/activesupport/lib/active_support/testing/isolation.rb @@ -37,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/testing/time_helpers.rb b/activesupport/lib/active_support/testing/time_helpers.rb index 94230e56ba..9e0a3d6345 100644 --- a/activesupport/lib/active_support/testing/time_helpers.rb +++ b/activesupport/lib/active_support/testing/time_helpers.rb @@ -1,10 +1,48 @@ module ActiveSupport module Testing + class SimpleStubs # :nodoc: + Stub = Struct.new(:object, :method_name, :original_method) + + def initialize + @stubs = {} + end + + def stub_object(object, method_name, return_value) + key = [object.object_id, method_name] + + if (stub = @stubs[key]) + unstub_object(stub) + end + + new_name = "__simple_stub__#{method_name}" + + @stubs[key] = Stub.new(object, method_name, new_name) + + object.singleton_class.send :alias_method, new_name, method_name + object.define_singleton_method(method_name) { return_value } + end + + def unstub_all! + @stubs.each_value do |stub| + unstub_object(stub) + end + @stubs = {} + end + + private + + def unstub_object(stub) + singleton_class = stub.object.singleton_class + singleton_class.send :undef_method, stub.method_name + singleton_class.send :alias_method, stub.method_name, stub.original_method + singleton_class.send :undef_method, stub.original_method + end + end + # Containing helpers that helps you test passage of time. module TimeHelpers - # Change current time to the time in the future or in the past by a given time difference by - # stubbing +Time.now+ and +Date.today+. Note that the stubs are automatically removed - # at the end of each test. + # Changes current time to the time in the future or in the past by a given time difference by + # stubbing +Time.now+ and +Date.today+. # # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 # travel 1.day @@ -23,9 +61,8 @@ module ActiveSupport travel_to Time.now + duration, &block end - # Change current time to the given time by stubbing +Time.now+ and +Date.today+ to return the - # time or date passed into this method. Note that the stubs are automatically removed - # at the end of each test. + # Changes current time to the given time by stubbing +Time.now+ and +Date.today+ to return the + # time or date passed into this method. # # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 # travel_to Time.new(2004, 11, 24, 01, 04, 44) @@ -37,19 +74,36 @@ module ActiveSupport # # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 # travel_to Time.new(2004, 11, 24, 01, 04, 44) do - # User.create.created_at # => Wed, 24 Nov 2004 01:04:44 EST -05:00 + # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00 # end # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 def travel_to(date_or_time, &block) - Time.stubs now: date_or_time.to_time - Date.stubs today: date_or_time.to_date + simple_stubs.stub_object(Time, :now, date_or_time.to_time) + simple_stubs.stub_object(Date, :today, date_or_time.to_date) if block_given? block.call - Time.unstub :now - Date.unstub :today + travel_back end end + + # Returns the current time back to its original state, by removing the stubs added by + # `travel` and `travel_to`. + # + # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 + # travel_to Time.new(2004, 11, 24, 01, 04, 44) + # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00 + # travel_back + # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 + def travel_back + simple_stubs.unstub_all! + end + + private + + def simple_stubs + @simple_stubs ||= SimpleStubs.new + end end end end diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 50db7da9d9..c25c97cfa8 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 @@ -367,12 +367,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 @@ -390,7 +390,8 @@ module ActiveSupport def wrap_with_time_zone(time) if time.acts_like?(:time) - self.class.new(nil, time_zone, time) + periods = time_zone.periods_for_local(time) + self.class.new(nil, time_zone, time, periods.include?(period) ? period : nil) elsif time.is_a?(Range) wrap_with_time_zone(time.begin)..wrap_with_time_zone(time.end) else diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index beaac42fa1..eb785d46ce 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -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 diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb index d082a0a499..009ee4db90 100644 --- a/activesupport/lib/active_support/xml_mini.rb +++ b/activesupport/lib/active_support/xml_mini.rb @@ -1,7 +1,9 @@ require 'time' require 'base64' +require 'bigdecimal' require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/string/inflections' +require 'active_support/core_ext/date_time/calculations' module ActiveSupport # = XmlMini @@ -56,13 +58,13 @@ module ActiveSupport # TODO use regexp instead of Date.parse unless defined?(PARSING) PARSING = { - "symbol" => Proc.new { |symbol| symbol.to_sym }, + "symbol" => Proc.new { |symbol| symbol.to_s.to_sym }, "date" => Proc.new { |date| ::Date.parse(date) }, "datetime" => Proc.new { |time| Time.xmlschema(time).utc rescue ::DateTime.parse(time).utc }, "integer" => Proc.new { |integer| integer.to_i }, "float" => Proc.new { |float| float.to_f }, "decimal" => Proc.new { |number| BigDecimal(number) }, - "boolean" => Proc.new { |boolean| %w(1 true).include?(boolean.strip) }, + "boolean" => Proc.new { |boolean| %w(1 true).include?(boolean.to_s.strip) }, "string" => Proc.new { |string| string.to_s }, "yaml" => Proc.new { |yaml| YAML::load(yaml) rescue yaml }, "base64Binary" => Proc.new { |bin| ::Base64.decode64(bin) }, diff --git a/activesupport/test/core_ext/big_decimal/yaml_conversions_test.rb b/activesupport/test/core_ext/big_decimal/yaml_conversions_test.rb new file mode 100644 index 0000000000..e634679d20 --- /dev/null +++ b/activesupport/test/core_ext/big_decimal/yaml_conversions_test.rb @@ -0,0 +1,11 @@ +require 'abstract_unit' + +class BigDecimalYamlConversionsTest < ActiveSupport::TestCase + def test_to_yaml + assert_deprecated { require 'active_support/core_ext/big_decimal/yaml_conversions' } + assert_match("--- 100000.30020320320000000000000000000000000000001\n", BigDecimal.new('100000.30020320320000000000000000000000000000001').to_yaml) + assert_match("--- .Inf\n", BigDecimal.new('Infinity').to_yaml) + assert_match("--- .NaN\n", BigDecimal.new('NaN').to_yaml) + assert_match("--- -.Inf\n", BigDecimal.new('-Infinity').to_yaml) + end +end diff --git a/activesupport/test/core_ext/bigdecimal_test.rb b/activesupport/test/core_ext/bigdecimal_test.rb index b386e55d6c..423a3f2e9d 100644 --- a/activesupport/test/core_ext/bigdecimal_test.rb +++ b/activesupport/test/core_ext/bigdecimal_test.rb @@ -2,18 +2,6 @@ require 'abstract_unit' require 'active_support/core_ext/big_decimal' class BigDecimalTest < ActiveSupport::TestCase - def test_to_yaml - assert_match("--- 100000.30020320320000000000000000000000000000001\n", BigDecimal.new('100000.30020320320000000000000000000000000000001').to_yaml) - assert_match("--- .Inf\n", BigDecimal.new('Infinity').to_yaml) - assert_match("--- .NaN\n", BigDecimal.new('NaN').to_yaml) - assert_match("--- -.Inf\n", BigDecimal.new('-Infinity').to_yaml) - end - - def test_to_d - bd = BigDecimal.new '10' - assert_equal bd, bd.to_d - end - def test_to_s bd = BigDecimal.new '0.01' assert_equal '0.01', bd.to_s diff --git a/activesupport/test/core_ext/enumerable_test.rb b/activesupport/test/core_ext/enumerable_test.rb index 6781e3c20e..6fcf6e8743 100644 --- a/activesupport/test/core_ext/enumerable_test.rb +++ b/activesupport/test/core_ext/enumerable_test.rb @@ -8,7 +8,6 @@ class SummablePayment < Payment end class EnumerableTests < ActiveSupport::TestCase - Enumerator = [].each.class class GenericEnumerable include Enumerable @@ -21,26 +20,6 @@ class EnumerableTests < ActiveSupport::TestCase end end - def test_group_by - names = %w(marcel sam david jeremy) - klass = Struct.new(:name) - objects = (1..50).map do - klass.new names.sample - end - - enum = GenericEnumerable.new(objects) - grouped = enum.group_by { |object| object.name } - - grouped.each do |name, group| - assert group.all? { |person| person.name == name } - end - - assert_equal objects.uniq.map(&:name), grouped.keys - assert({}.merge(grouped), "Could not convert ActiveSupport::OrderedHash into Hash") - assert_equal Enumerator, enum.group_by.class - assert_equal grouped, enum.group_by.each(&:name) - end - def test_sums enum = GenericEnumerable.new([5, 15, 10]) assert_equal 30, enum.sum @@ -94,6 +73,10 @@ class EnumerableTests < ActiveSupport::TestCase assert_equal({ 5 => Payment.new(5), 15 => Payment.new(15), 10 => Payment.new(10) }, payments.index_by { |p| p.price }) assert_equal Enumerator, payments.index_by.class + if Enumerator.method_defined? :size + assert_equal nil, payments.index_by.size + assert_equal 42, (1..42).index_by.size + end assert_equal({ 5 => Payment.new(5), 15 => Payment.new(15), 10 => Payment.new(10) }, payments.index_by.each { |p| p.price }) end diff --git a/activesupport/test/core_ext/object/to_query_test.rb b/activesupport/test/core_ext/object/to_query_test.rb index 92f996f9a4..f887a9e613 100644 --- a/activesupport/test/core_ext/object/to_query_test.rb +++ b/activesupport/test/core_ext/object/to_query_test.rb @@ -46,6 +46,19 @@ class ToQueryTest < ActiveSupport::TestCase :person => {:id => [20, 10]} end + def test_nested_empty_hash + assert_equal '', + {}.to_query + assert_query_equal 'a=1&b%5Bc%5D=3&b%5Bd%5D=', + { a: 1, b: { c: 3, d: {} } } + assert_query_equal 'b%5Bc%5D=false&b%5Be%5D=&b%5Bf%5D=&p=12', + { p: 12, b: { c: false, e: nil, f: '' } } + assert_query_equal 'b%5Bc%5D=3&b%5Bf%5D=&b%5Bk%5D=', + { b: { c: 3, k: {}, f: '' } } + assert_query_equal 'a%5B%5D=&b=3', + {a: [], b: 3} + end + private def assert_query_equal(expected, actual) assert_equal expected.split('&'), actual.to_query.split('&') diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index d4f8ba8cdd..072b970a2d 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -161,6 +161,10 @@ class StringInflectionsTest < ActiveSupport::TestCase end end + def test_humanize_with_html_escape + assert_equal 'Hello', ERB::Util.html_escape("hello").humanize + end + def test_ord assert_equal 97, 'a'.ord assert_equal 97, 'abc'.ord diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index 5494824a40..7fe4d4a6b2 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,16 @@ 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_round_at_dst_boundary + twz = ActiveSupport::TimeWithZone.new(Time.at(1319936400).getutc, ActiveSupport::TimeZone['Madrid']) + assert_equal twz, twz.round + 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 00bec5bd9d..4ca63b3417 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -948,6 +948,18 @@ class DependenciesTest < ActiveSupport::TestCase Object.class_eval { remove_const :A if const_defined?(:A) } end + def test_access_unloaded_constants_for_reload + with_autoloading_fixtures do + assert_kind_of Module, A + assert_kind_of Class, A::B # Necessary to load A::B for the test + ActiveSupport::Dependencies.mark_for_unload(A::B) + ActiveSupport::Dependencies.remove_unloadable_constants! + + A::B # Make sure no circular dependency error + end + end + + def test_autoload_once_paths_should_behave_when_recursively_loading with_loading 'dependencies', 'autoloading_fixtures' do ActiveSupport::Dependencies.autoload_once_paths = [ActiveSupport::Dependencies.autoload_paths.last] 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/test_test.rb b/activesupport/test/test_test.rb index 8a71ef4324..0fa08c0e3a 100644 --- a/activesupport/test/test_test.rb +++ b/activesupport/test/test_test.rb @@ -162,6 +162,10 @@ class TimeHelperTest < ActiveSupport::TestCase Time.stubs now: Time.now end + teardown do + travel_back + end + def test_time_helper_travel expected_time = Time.now + 1.day travel 1.day @@ -201,4 +205,16 @@ class TimeHelperTest < ActiveSupport::TestCase assert_not_equal expected_time, Time.now assert_not_equal Date.new(2004, 11, 24), Date.today end + + def test_time_helper_travel_back + expected_time = Time.new(2004, 11, 24, 01, 04, 44) + + travel_to expected_time + assert_equal expected_time, Time.now + assert_equal Date.new(2004, 11, 24), Date.today + travel_back + + assert_not_equal expected_time, Time.now + assert_not_equal Date.new(2004, 11, 24), Date.today + end end diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb index 1107b48460..cd79efbe8c 100644 --- a/activesupport/test/time_zone_test.rb +++ b/activesupport/test/time_zone_test.rb @@ -97,6 +97,7 @@ class TimeZoneTest < ActiveSupport::TestCase assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].today travel_to(Time.utc(2000, 1, 2, 5)) # midnight Jan 2 EST assert_equal Date.new(2000, 1, 2), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].today + travel_back end def test_tomorrow @@ -108,6 +109,7 @@ class TimeZoneTest < ActiveSupport::TestCase assert_equal Date.new(2000, 1, 2), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].tomorrow travel_to(Time.utc(2000, 1, 2, 5)) # midnight Jan 2 EST assert_equal Date.new(2000, 1, 3), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].tomorrow + travel_back end def test_yesterday @@ -119,6 +121,7 @@ class TimeZoneTest < ActiveSupport::TestCase assert_equal Date.new(1999, 12, 31), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].yesterday travel_to(Time.utc(2000, 1, 2, 5)) # midnight Jan 2 EST assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].yesterday + travel_back end def test_local diff --git a/activesupport/test/xml_mini_test.rb b/activesupport/test/xml_mini_test.rb index d992028323..753effb54e 100644 --- a/activesupport/test/xml_mini_test.rb +++ b/activesupport/test/xml_mini_test.rb @@ -169,4 +169,128 @@ module XmlMiniTest end end end + + class ParsingTest < ActiveSupport::TestCase + def setup + @parsing = ActiveSupport::XmlMini::PARSING + end + + def test_symbol + parser = @parsing['symbol'] + assert_equal :symbol, parser.call('symbol') + assert_equal :symbol, parser.call(:symbol) + assert_equal :'123', parser.call(123) + assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) } + end + + def test_date + parser = @parsing['date'] + assert_equal Date.new(2013,11,12), parser.call("2013-11-12T0211Z") + assert_raises(TypeError) { parser.call(1384190018) } + assert_raises(ArgumentError) { parser.call("not really a date") } + end + + def test_datetime + parser = @parsing['datetime'] + assert_equal Time.new(2013,11,12,02,11,00,0), parser.call("2013-11-12T02:11:00Z") + assert_equal DateTime.new(2013,11,12), parser.call("2013-11-12T0211Z") + assert_equal DateTime.new(2013,11,12,02,11), parser.call("2013-11-12T02:11Z") + assert_equal DateTime.new(2013,11,12,02,11), parser.call("2013-11-12T11:11+9") + assert_raises(ArgumentError) { parser.call("1384190018") } + end + + def test_integer + parser = @parsing['integer'] + assert_equal 123, parser.call(123) + assert_equal 123, parser.call(123.003) + assert_equal 123, parser.call("123") + assert_equal 0, parser.call("") + assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) } + end + + def test_float + parser = @parsing['float'] + assert_equal 123, parser.call("123") + assert_equal 123.003, parser.call("123.003") + assert_equal 123.0, parser.call("123,003") + assert_equal 0.0, parser.call("") + assert_equal 123, parser.call(123) + assert_equal 123.05, parser.call(123.05) + assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) } + end + + def test_decimal + parser = @parsing['decimal'] + assert_equal 123, parser.call("123") + assert_equal 123.003, parser.call("123.003") + assert_equal 123.0, parser.call("123,003") + assert_equal 0.0, parser.call("") + assert_equal 123, parser.call(123) + assert_raises(ArgumentError) { parser.call(123.04) } + assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) } + end + + def test_boolean + parser = @parsing['boolean'] + [1, true, "1"].each do |value| + assert parser.call(value) + end + + [0, false, "0"].each do |value| + assert_not parser.call(value) + end + end + + def test_string + parser = @parsing['string'] + assert_equal "123", parser.call(123) + assert_equal "123", parser.call("123") + assert_equal "[]", parser.call("[]") + assert_equal "[]", parser.call([]) + assert_equal "{}", parser.call({}) + assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) } + end + + def test_yaml + yaml = <<YAML +product: + - sku : BL394D + quantity : 4 + description : Basketball +YAML + expected = { + "product"=> [ + {"sku"=>"BL394D", "quantity"=>4, "description"=>"Basketball"} + ] + } + parser = @parsing['yaml'] + assert_equal(expected, parser.call(yaml)) + assert_equal({1 => 'test'}, parser.call({1 => 'test'})) + assert_equal({"1 => 'test'"=>nil}, parser.call("{1 => 'test'}")) + end + + def test_base64Binary_and_binary + base64 = <<BASE64 +TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlz +IHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2Yg +dGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGlu +dWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRo +ZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4= +BASE64 + expected_base64 = <<EXPECTED +Man is distinguished, not only by his reason, but by this singular passion from +other animals, which is a lust of the mind, that by a perseverance of delight +in the continued and indefatigable generation of knowledge, exceeds the short +vehemence of any carnal pleasure. +EXPECTED + + parser = @parsing['base64Binary'] + assert_equal expected_base64.gsub(/\n/," ").strip, parser.call(base64) + parser.call("NON BASE64 INPUT") + + parser = @parsing['binary'] + assert_equal expected_base64.gsub(/\n/," ").strip, parser.call(base64, 'encoding' => 'base64') + assert_equal "IGNORED INPUT", parser.call("IGNORED INPUT", {}) + end + end end diff --git a/guides/assets/images/getting_started/article_with_comments.png b/guides/assets/images/getting_started/article_with_comments.png Binary files differnew file mode 100644 index 0000000000..1918e9bf28 --- /dev/null +++ b/guides/assets/images/getting_started/article_with_comments.png diff --git a/guides/assets/images/getting_started/challenge.png b/guides/assets/images/getting_started/challenge.png Binary files differindex 4a30e49e6d..cc12162677 100644 --- a/guides/assets/images/getting_started/challenge.png +++ b/guides/assets/images/getting_started/challenge.png diff --git a/guides/assets/images/getting_started/confirm_dialog.png b/guides/assets/images/getting_started/confirm_dialog.png Binary files differindex 1a13eddd91..e57d4b409e 100644 --- a/guides/assets/images/getting_started/confirm_dialog.png +++ b/guides/assets/images/getting_started/confirm_dialog.png diff --git a/guides/assets/images/getting_started/forbidden_attributes_for_new_article.png b/guides/assets/images/getting_started/forbidden_attributes_for_new_article.png Binary files differnew file mode 100644 index 0000000000..e263f7f8b2 --- /dev/null +++ b/guides/assets/images/getting_started/forbidden_attributes_for_new_article.png diff --git a/guides/assets/images/getting_started/forbidden_attributes_for_new_post.png b/guides/assets/images/getting_started/forbidden_attributes_for_new_post.png Binary files differdeleted file mode 100644 index 6c78e52173..0000000000 --- a/guides/assets/images/getting_started/forbidden_attributes_for_new_post.png +++ /dev/null diff --git a/guides/assets/images/getting_started/form_with_errors.png b/guides/assets/images/getting_started/form_with_errors.png Binary files differindex 6910e1647e..04ff8b1e2d 100644 --- a/guides/assets/images/getting_started/form_with_errors.png +++ b/guides/assets/images/getting_started/form_with_errors.png diff --git a/guides/assets/images/getting_started/index_action_with_edit_link.png b/guides/assets/images/getting_started/index_action_with_edit_link.png Binary files differindex bf23cba231..22f994d993 100644 --- a/guides/assets/images/getting_started/index_action_with_edit_link.png +++ b/guides/assets/images/getting_started/index_action_with_edit_link.png diff --git a/guides/assets/images/getting_started/new_article.png b/guides/assets/images/getting_started/new_article.png Binary files differnew file mode 100644 index 0000000000..89fc0b2605 --- /dev/null +++ b/guides/assets/images/getting_started/new_article.png diff --git a/guides/assets/images/getting_started/new_post.png b/guides/assets/images/getting_started/new_post.png Binary files differdeleted file mode 100644 index b20b0192d4..0000000000 --- a/guides/assets/images/getting_started/new_post.png +++ /dev/null diff --git a/guides/assets/images/getting_started/post_with_comments.png b/guides/assets/images/getting_started/post_with_comments.png Binary files differdeleted file mode 100644 index e13095ff8f..0000000000 --- a/guides/assets/images/getting_started/post_with_comments.png +++ /dev/null diff --git a/guides/assets/images/getting_started/routing_error_no_controller.png b/guides/assets/images/getting_started/routing_error_no_controller.png Binary files differindex 35ee4f348f..ae83b6a68c 100644 --- a/guides/assets/images/getting_started/routing_error_no_controller.png +++ b/guides/assets/images/getting_started/routing_error_no_controller.png diff --git a/guides/assets/images/getting_started/show_action_for_posts.png b/guides/assets/images/getting_started/show_action_for_articles.png Binary files differindex 9467df6a07..9467df6a07 100644 --- a/guides/assets/images/getting_started/show_action_for_posts.png +++ b/guides/assets/images/getting_started/show_action_for_articles.png diff --git a/guides/assets/images/getting_started/template_is_missing_articles_new.png b/guides/assets/images/getting_started/template_is_missing_articles_new.png Binary files differnew file mode 100644 index 0000000000..ba630cfc23 --- /dev/null +++ b/guides/assets/images/getting_started/template_is_missing_articles_new.png diff --git a/guides/assets/images/getting_started/template_is_missing_posts_new.png b/guides/assets/images/getting_started/template_is_missing_posts_new.png Binary files differdeleted file mode 100644 index f03db05fb8..0000000000 --- a/guides/assets/images/getting_started/template_is_missing_posts_new.png +++ /dev/null diff --git a/guides/assets/images/getting_started/undefined_method_post_path.png b/guides/assets/images/getting_started/undefined_method_post_path.png Binary files differdeleted file mode 100644 index c29cb2f54f..0000000000 --- a/guides/assets/images/getting_started/undefined_method_post_path.png +++ /dev/null diff --git a/guides/assets/images/getting_started/unknown_action_create_for_articles.png b/guides/assets/images/getting_started/unknown_action_create_for_articles.png Binary files differnew file mode 100644 index 0000000000..ed89c4f3d7 --- /dev/null +++ b/guides/assets/images/getting_started/unknown_action_create_for_articles.png diff --git a/guides/assets/images/getting_started/unknown_action_create_for_posts.png b/guides/assets/images/getting_started/unknown_action_create_for_posts.png Binary files differdeleted file mode 100644 index 8fdd4c574a..0000000000 --- a/guides/assets/images/getting_started/unknown_action_create_for_posts.png +++ /dev/null diff --git a/guides/assets/images/getting_started/unknown_action_new_for_articles.png b/guides/assets/images/getting_started/unknown_action_new_for_articles.png Binary files differnew file mode 100644 index 0000000000..e8f2b9a16a --- /dev/null +++ b/guides/assets/images/getting_started/unknown_action_new_for_articles.png diff --git a/guides/assets/images/getting_started/unknown_action_new_for_posts.png b/guides/assets/images/getting_started/unknown_action_new_for_posts.png Binary files differdeleted file mode 100644 index 7e72feee38..0000000000 --- a/guides/assets/images/getting_started/unknown_action_new_for_posts.png +++ /dev/null diff --git a/guides/code/getting_started/Gemfile b/guides/code/getting_started/Gemfile index a2155c43b9..ecb6e7aa1a 100644 --- a/guides/code/getting_started/Gemfile +++ b/guides/code/getting_started/Gemfile @@ -23,7 +23,7 @@ gem 'jbuilder', '~> 2.0' # bundle exec rake doc:rails generates the API under doc/api. gem 'sdoc', '~> 0.4.0', group: :doc -# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/jonleighton/spring +# 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 diff --git a/guides/code/getting_started/config/environments/production.rb b/guides/code/getting_started/config/environments/production.rb index 93d44723fb..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 diff --git a/guides/rails_guides/helpers.rb b/guides/rails_guides/helpers.rb index 760b196abd..169453400f 100644 --- a/guides/rails_guides/helpers.rb +++ b/guides/rails_guides/helpers.rb @@ -1,3 +1,5 @@ +require 'yaml' + module RailsGuides module Helpers def guide(name, url, options = {}, &block) 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/4_1_release_notes.md b/guides/source/4_1_release_notes.md index 924e5d90db..8fcfc71351 100644 --- a/guides/source/4_1_release_notes.md +++ b/guides/source/4_1_release_notes.md @@ -64,7 +64,7 @@ Spring is running: ``` Have a look at the -[Spring README](https://github.com/jonleighton/spring/blob/master/README.md) to +[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) @@ -175,6 +175,8 @@ conversation.active? # => false conversation.status # => "archived" Conversation.archived # => Relation for all archived Conversations + +Conversation.statuses # => { "active" => 0, "archived" => 1 } ``` See its @@ -241,6 +243,7 @@ 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 -------- @@ -267,7 +270,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)) @@ -278,7 +281,7 @@ for detailed changes. * Exposed `MiddlewareStack#unshift` to environment configuration. ([Pull Request](https://github.com/rails/rails/pull/12479)) -* Add `Application#message_verifier` method to return a message +* Added `Application#message_verifier` method to return a message verifier. ([Pull Request](https://github.com/rails/rails/pull/12995)) * The `test_help.rb` file which is required by the default generated test @@ -288,6 +291,7 @@ for detailed changes. with `config.active_record.maintain_test_schema = false`. ([Pull Request](https://github.com/rails/rails/pull/13528)) + Action Pack ----------- @@ -335,6 +339,16 @@ for detailed changes. * Separated Action View completely from Action Pack. ([Pull Request](https://github.com/rails/rails/pull/11032)) +* Log which keys were affected by deep + munge. ([Pull Request](https://github.com/rails/rails/pull/13813)) + +* New config option `config.action_dispatch.perform_deep_munge` to opt out of + params "deep munging" that was used to address security vulnerability + CVE-2013-0155. ([Pull Request](https://github.com/rails/rails/pull/13188)) + +* New config option `config.action_dispatch.cookies_serializer` for specifying + a serializer for the signed and encrypted cookie jars. (Pull Requests [1](https://github.com/rails/rails/pull/13692), [2](https://github.com/rails/rails/pull/13945) / [More Details](upgrading_ruby_on_rails.html#cookies-serializer)) + Action Mailer ------------- @@ -344,9 +358,13 @@ for detailed changes. ### Notable changes +* Added mailer previews feature based on 37 Signals mail_view + gem. ([Commit](https://github.com/rails/rails/commit/d6dec7fcb6b8fddf8c170182d4fe64ecfc7b2261)) + * 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 ------------- @@ -411,6 +429,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 @@ -420,11 +440,6 @@ 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)) @@ -498,7 +513,32 @@ 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. +* Don't create or drop the test database if RAILS_ENV is specified + explicitly. ([Pull Request](https://github.com/rails/rails/pull/13629)) + +* `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)) + +* `find_in_batches`, `find_each`, `Result#each` and `Enumerable#index_by` now + return an `Enumerator` that can calculate its + size. ([Pull Request](https://github.com/rails/rails/pull/13938)) + +* `scope`, `enum` and Associations now raise on "dangerous" name + conflicts. ([Pull Request](https://github.com/rails/rails/pull/13450), + [Pull Request](https://github.com/rails/rails/pull/13896)) + +* `second` through `fifth` methods act like the `first` + finder. ([Pull Request](https://github.com/rails/rails/pull/13757)) + +* Make `touch` fire the `after_commit` and `after_rollback` + callbacks. ([Pull Request](https://github.com/rails/rails/pull/12031)) + +* Enable partial indexes for `sqlite >= + 3.8.0`. ([Pull Request](https://github.com/rails/rails/pull/13350)) + +* Make `change_column_null` + revertable. ([Commit](https://github.com/rails/rails/commit/724509a9d5322ff502aefa90dd282ba33a281a96)) + Active Model ------------ @@ -517,6 +557,13 @@ for detailed changes. * Added new API methods `reset_changes` and `changes_applied` to `ActiveModel::Dirty` that control changes state. +* Ability to specify multiple contexts when defining a + validation. ([Pull Request](https://github.com/rails/rails/pull/13754)) + +* `attribute_changed?` now accepts a hash to check if the attribute was changed + `:from` and/or `:to` a given + value. ([Pull Request](https://github.com/rails/rails/pull/13131)) + Active Support -------------- @@ -567,6 +614,12 @@ for detailed changes. * Removed deprecated `assert_present` and `assert_blank` methods, use `assert object.blank?` and `assert object.present?` instead. +* Remove deprecated `#filter` method for filter objects, use the corresponding + method instead (e.g. `#before` for a before filter). + +* Removed 'cow' => 'kine' irregular inflection from default + inflections. ([Commit](https://github.com/rails/rails/commit/c300dca9963bda78b8f358dbcb59cabcdc5e1dc9)) + ### Deprecations * Deprecated `Numeric#{ago,until,since,from_now}`, the user is expected to @@ -583,11 +636,14 @@ for detailed changes. [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) + 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)) +* Deprecate custom `BigDecimal` + serialization. ([Pull Request](https://github.com/rails/rails/pull/13911)) + ### Notable changes * `ActiveSupport`'s JSON encoder has been rewritten to take advantage of the @@ -604,6 +660,10 @@ for detailed changes. `Time.now` and `Date.today`. ([Pull Request](https://github.com/rails/rails/pull/12824)) +* Added `ActiveSupport::Testing::TimeHelpers#travel_back`. This method returns + the current time to the original state, by removing the stubs added by `travel` + and `travel_to`. ([Pull Request](https://github.com/rails/rails/pull/13884)) + * 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)) @@ -613,11 +673,30 @@ for detailed changes. `at_middle_of_day` as aliases. ([Pull Request](https://github.com/rails/rails/pull/10879)) +* Added `Date#all_week/month/quarter/year` for generating date + ranges. ([Pull Request](https://github.com/rails/rails/pull/9685)) + +* Added `Time.zone.yesterday` and + `Time.zone.tomorrow`. ([Pull Request](https://github.com/rails/rails/pull/12822)) + * 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)) +* Added `Hash#compact` and `Hash#compact!` for removing items with nil value + from hash. ([Pull Request](https://github.com/rails/rails/pull/13632)) + +* `blank?` and `present?` commit to return + singletons. ([Commit](https://github.com/rails/rails/commit/126dc47665c65cd129967cbd8a5926dddd0aa514)) + +* 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. ([Pull Request](https://github.com/rails/rails/commit/8e21ae37ad9fef6b7393a84f9b5f2e18a831e49a)) + +* Introduce Module#concerning: a natural, low-ceremony way to separate + responsibilities within a + class. ([Commit](https://github.com/rails/rails/commit/1eee0ca6de975b42524105a59e0521d18b38ab81)) + Credits ------- diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index f394daa6aa..222d86afe9 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 @@ -568,6 +572,38 @@ end Note that while for session values you set the key to `nil`, to delete a cookie value you should use `cookies.delete(:key)`. +Rails also provides a signed cookie jar and an encrypted cookie jar for storing +sensitive data. The signed cookie jar appends a cryptographic signature on the +cookie values to protect their integrity. The encrypted cookie jar encrypts the +values in addition to signing them, so that they cannot be read by the end user. +Refer to the [API documentation](http://api.rubyonrails.org/classes/ActionDispatch/Cookies.html) +for more details. + +These special cookie jars use a serializer to serialize the assigned values into +strings and deserializes them into Ruby objects on read. + +You can specify what serializer to use: + +```ruby +Rails.application.config.action_dispatch.cookies_serializer = :json +``` + +The default serializer for new applications is `:json`. For compatibility with +old applications with existing cookies, `:marshal` is used when `serializer` +option is not specified. + +You may also set this option to `:hybrid`, in which case Rails would transparently +deserialize existing (`Marshal`-serialized) cookies on read and re-write them in +the `JSON` format. This is useful for migrating existing applications to the +`:json` serializer. + +It is also possible to pass a custom serializer that responds to `load` and +`dump`: + +```ruby +Rails.application.config.action_dispatch.cookies_serializer = MyCustomSerializer +``` + Rendering XML and JSON data --------------------------- @@ -683,7 +719,7 @@ class ApplicationController < ActionController::Base end class LoginFilter - def self.filter(controller) + def self.before(controller) unless controller.send(:logged_in?) controller.flash[:error] = "You must be logged in to access this section" controller.redirect_to controller.new_login_url @@ -692,7 +728,7 @@ class LoginFilter end ``` -Again, this is not an ideal example for this filter, because it's not run in the scope of the controller but gets the controller passed as an argument. The filter class has a class method `filter` which gets run before or after the action, depending on if it's a before or after filter. Classes used as around filters can also use the same `filter` method, which will get run in the same way. The method must `yield` to execute the action. Alternatively, it can have both a `before` and an `after` method that are run before and after the action. +Again, this is not an ideal example for this filter, because it's not run in the scope of the controller but gets the controller passed as an argument. The filter class must implement a method with the same name as the filter, so for the `before_action` filter the class must implement a `before` method, and so on. The `around` method must `yield` to execute the action. Request Forgery Protection -------------------------- 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/active_record_querying.md b/guides/source/active_record_querying.md index 3783be50c0..d164b08d93 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -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..a483a6dd24 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -575,7 +575,9 @@ This helper validates that the attribute's value is unique right before the object gets saved. It does not create a uniqueness constraint in the database, so it may happen that two different database connections create two records with the same value for a column that you intend to be unique. To avoid that, -you must create a unique index in your database. +you must create a unique index on both columns in your database. See +[the MySQL manual](http://dev.mysql.com/doc/refman/5.6/en/multiple-column-indexes.html) +for more details about multiple column indexes. ```ruby class Account < ActiveRecord::Base @@ -616,10 +618,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 +625,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 +646,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 +653,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 59dfefd22f..2ad09f599b 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -1403,6 +1403,8 @@ The third argument, `indent_empty_lines`, is a flag that says whether empty line The `indent!` method performs indentation in-place. +NOTE: Defined in `active_support/core_ext/string/indent.rb`. + ### Access #### `at(position)` diff --git a/guides/source/api_documentation_guidelines.md b/guides/source/api_documentation_guidelines.md index 311cc23cf0..295c471db9 100644 --- a/guides/source/api_documentation_guidelines.md +++ b/guides/source/api_documentation_guidelines.md @@ -128,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 method has +predicate semantics, that's enough. + Filenames --------- @@ -163,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 bce5d6c55f..fa2e57ff92 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -496,16 +496,11 @@ In this example, `require_self` is used. This puts the CSS contained within the file (if any) at the precise location of the `require_self` call. If `require_self` is called more than once, only the last call is respected. -NOTE. If you want to use multiple Sass files, you should generally use the [Sass -`@import` -rule](http://sass-lang.com/docs/yardoc/file.SASS_REFERENCE.html#import) instead -of these Sprockets directives. Using Sprockets directives all Sass files exist -within their own scope, making variables or mixins only available within the -document they were defined in. You can do file globbing as well using -`@import "*"`, and `@import "**/*"` to add the whole tree equivalent to how -`require_tree` works. Check the [sass-rails -documentation](https://github.com/rails/sass-rails#features) for more info and -important caveats. +NOTE. If you want to use multiple Sass files, you should generally use the [Sass `@import` rule](http://sass-lang.com/docs/yardoc/file.SASS_REFERENCE.html#import) +instead of these Sprockets directives. Using Sprockets directives all Sass files exist within +their own scope, making variables or mixins only available within the document they were defined in. +You can do file globbing as well using `@import "*"`, and `@import "**/*"` to add the whole tree +equivalent to how `require_tree` works. Check the [sass-rails documentation](https://github.com/rails/sass-rails#features) for more info and important caveats. You can have as many manifest files as you need. For example, the `admin.css` and `admin.js` manifest could contain the JS and CSS files that are used for the @@ -938,7 +933,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. @@ -948,6 +943,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 @@ -1018,7 +1018,8 @@ The X-Sendfile header is a directive to the web server to ignore the response from the application, and instead serve a specified file from disk. This option is off by default, but can be enabled if your server supports it. When enabled, this passes responsibility for serving the file to the web server, which is -faster. +faster. Have a look at [send_file](http://api.rubyonrails.org/classes/ActionController/DataStreaming.html#method-i-send_file) +on how to use this feature. Apache and nginx support this option, which can be enabled in `config/environments/production.rb`: @@ -1033,6 +1034,10 @@ option, take care to paste this configuration option only into `production.rb` and any other environments you define with production behavior (not `application.rb`). +TIP: For further details have a look at the docs of your production web server: +- [Apache](https://tn123.org/mod_xsendfile/) +- [Nginx](http://wiki.nginx.org/XSendfile) + Assets Cache Store ------------------ @@ -1145,7 +1150,7 @@ config.assets.digest = true ``` Rails 4 no longer sets default config values for Sprockets in `test.rb`, so -`test.rb` now requies Sprockets configuration. The old defaults in the test +`test.rb` now requires Sprockets configuration. The old defaults in the test environment are: `config.assets.compile = true`, `config.assets.compress = false`, `config.assets.debug = false` and `config.assets.digest = false`. diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index 9867d2dc3f..5ec6ae0f21 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -490,6 +490,19 @@ end With this setup, you can retrieve `@employee.subordinates` and `@employee.manager`. +In your migrations/schema, you will add a references column to the model itself. + +```ruby +class CreateEmployees < ActiveRecord::Migration + def change + create_table :employees do |t| + t.references :manager + t.timestamps + end + end +end +``` + Tips, Tricks, and Warnings -------------------------- diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 412aecadd5..7b72e27b96 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -292,6 +292,12 @@ All these configuration options are delegated to the `I18n` library. * `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. +* `config.active_record.dump_schema_after_migration` is a flag which + controls whether or not schema dump should happen (`db/schema.rb` or + `db/structure.sql`) when you run migrations. This is set to false in + `config/environments/production.rb` which is generated by Rails. The + default value is true if this configuration is not set. + 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. @@ -352,6 +358,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`. @@ -374,7 +384,7 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. * `config.action_view.logger` accepts a logger conforming to the interface of Log4r or the default Ruby Logger class, which is then used to log information from Action View. Set to `nil` to disable logging. -* `config.action_view.erb_trim_mode` gives the trim mode to be used by ERB. It defaults to `'-'`. See the [ERB documentation](http://www.ruby-doc.org/stdlib/libdoc/erb/rdoc/) for more information. +* `config.action_view.erb_trim_mode` gives the trim mode to be used by ERB. It defaults to `'-'`, which turns on trimming of tail spaces and newline when using `<%= -%>` or `<%= =%>`. See the [Erubis documentation](http://www.kuwata-lab.com/erubis/users-guide.06.html#topics-trimspaces) for more information. * `config.action_view.embed_authenticity_token_in_remote_forms` allows you to set the default behavior for `authenticity_token` in forms with `:remote => true`. By default it's set to false, which means that remote forms will not include `authenticity_token`, which is helpful when you're fragment-caching the form. Remote forms get the authenticity from the `meta` tag, so embedding is unnecessary unless you support browsers without JavaScript. In such case you can either pass `:authenticity_token => true` as a form option or set this config setting to `true` @@ -386,6 +396,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`: @@ -406,17 +418,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. @@ -441,6 +461,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. @@ -451,7 +473,6 @@ There are a few configuration options available in Active Support: * `ActiveSupport::Deprecation.silenced` sets whether or not to display deprecation warnings. -* `ActiveSupport::Logger.silencer` is set to `false` to disable the ability to silence logging in a block. The default is `true`. ### Configuring a Database @@ -554,7 +575,7 @@ $ rails runner 'puts ActiveRecord::Base.connections' 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: +The only way to explicitly not use the connection information in `ENV['DATABASE_URL']` is to specify an explicit URL connection using the `"url"` sub key: ``` $ cat config/database.yml @@ -700,7 +721,7 @@ Rails will now prepend "/app1" when generating links. #### Using Passenger -Passenger makes it easiy to run your application in a subdirectory. You can find +Passenger makes it easy to run your application in a subdirectory. You can find the relevant configuration in the [passenger manual](http://www.modrails.com/documentation/Users%20guide%20Apache.html#deploying_rails_to_sub_uri). @@ -918,4 +939,4 @@ ActiveRecord::ConnectionTimeoutError - could not obtain a database connection wi If you get the above error, you might want to increase the size of connection pool by incrementing the `pool` option in `database.yml` -NOTE. If you have enabled `Rails.threadsafe!` mode then there could be a chance that several threads may be accessing multiple connections simultaneously. So depending on your current request load, you could very well have multiple threads contending for a limited amount of connections. +NOTE. As Rails is multi-threaded by default, there could be a chance that several threads may be accessing multiple connections simultaneously. So depending on your current request load, you could very well have multiple threads contending for a limited amount of connections. diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md index 814237ba22..36e3862c6b 100644 --- a/guides/source/contributing_to_ruby_on_rails.md +++ b/guides/source/contributing_to_ruby_on_rails.md @@ -136,7 +136,7 @@ You can invoke `test_jdbcmysql`, `test_jdbcsqlite3` or `test_jdbcpostgresql` als The test suite runs with warnings enabled. Ideally, Ruby on Rails should issue no warnings, but there may be a few, as well as some from third-party libraries. Please ignore (or fix!) them, if any, and submit patches that do not issue new warnings. -As of this writing (December, 2010) they are especially noisy with Ruby 1.9. If you are sure about what you are doing and would like to have a more clear output, there's a way to override the flag: +If you are sure about what you are doing and would like to have a more clear output, there's a way to override the flag: ```bash $ RUBYOPT=-W0 bundle exec rake test @@ -201,7 +201,8 @@ If your comment simply says "+1", then odds are that other reviewers aren't goin Contributing to the Rails Documentation --------------------------------------- -Ruby on Rails has two main sets of documentation: the guides help you in learning about Ruby on Rails, and the API is a reference. +Ruby on Rails has two main sets of documentation: the guides, which help you +learn about Ruby on Rails, and the API, which serves as a reference. You can help improve the Rails guides by making them more coherent, consistent or readable, adding missing information, correcting factual errors, fixing typos, or bringing it up to date with the latest edge Rails. To get involved in the translation of Rails guides, please see [Translating Rails Guides](https://wiki.github.com/rails/docrails/translating-rails-guides). @@ -258,10 +259,10 @@ more if the source code is mounted in `/vagrant` as happens in the recommended workflow with the [rails-dev-box](https://github.com/rails/rails-dev-box). As a compromise, test what your code obviously affects, and if the change is -not in railties run the whole test suite of the affected component. If all is -green that's enough to propose your contribution. We have [Travis CI](https://travis-ci.org/rails/rails) -as a safety net for catching unexpected breakages -elsewhere. +not in railties, run the whole test suite of the affected component. If all +tests are passing, that's enough to propose your contribution. We have +[Travis CI](https://travis-ci.org/rails/rails) as a safety net for catching +unexpected breakages elsewhere. TIP: Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability of Rails will generally not be accepted. 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/getting_started.md b/guides/source/getting_started.md index dbcedba800..53d2a9b55b 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -21,19 +21,22 @@ application from scratch. It does not assume that you have any prior experience with Rails. However, to get the most out of it, you need to have some prerequisites installed: -* The [Ruby](http://www.ruby-lang.org/en/downloads) language version 1.9.3 or newer -* The [RubyGems](http://rubygems.org) packaging system - * To learn more about RubyGems, please read the [RubyGems Guides](http://guides.rubygems.org) -* A working installation of the [SQLite3 Database](http://www.sqlite.org) +* The [Ruby](http://www.ruby-lang.org/en/downloads) language version 1.9.3 or newer. +* The [RubyGems](http://rubygems.org) packaging system, which is installed with Ruby + versions 1.9 and later. To learn more about RubyGems, please read the [RubyGems Guides](http://guides.rubygems.org). +* A working installation of the [SQLite3 Database](http://www.sqlite.org). Rails is a web application framework running on the Ruby programming language. If you have no prior experience with Ruby, you will find a very steep learning -curve diving straight into Rails. There are some good free resources on the -Internet for learning Ruby, including: +curve diving straight into Rails. There are several curated lists of online resources +for learning Ruby: -* [Mr. Neighborly's Humble Little Ruby Book](http://www.humblelittlerubybook.com) -* [Programming Ruby](http://www.ruby-doc.org/docs/ProgrammingRuby/) -* [Why's (Poignant) Guide to Ruby](http://mislav.uniqpath.com/poignant-guide/) +* [Official Ruby Programming Language website](https://www.ruby-lang.org/en/documentation/) +* [reSRC's List of Free Programming Books](http://resrc.io/list/10/list-of-free-programming-books/#ruby) + +Be aware that some resources, while still excellent, cover versions of Ruby as old as +1.6, and commonly 1.8, and will not include some syntax that you will see in day-to-day +development with Rails. What is Rails? -------------- @@ -54,11 +57,13 @@ learned elsewhere, you may have a less happy experience. The Rails philosophy includes two major guiding principles: -* DRY - "Don't Repeat Yourself" - suggests that writing the same code over and - over again is a bad thing. -* Convention Over Configuration - means that Rails makes assumptions about what - you want to do and how you're going to do it, rather than requiring you to - specify every little thing through endless configuration files. +* **Don't Repeat Yourself:** DRY is a principle of software development which + states that "Every piece of knowledge must have a single, unambiguous, authoritative + representation within a system." By not writing the same information over and over + again, our code is more maintainable, more extensible, and less buggy. +* **Convention Over Configuration:** Rails has opinions about the best way to do many + things in a web application, and defaults to this set of conventions, rather than + require that you specify every minutiae through endless configuration files. Creating a New Rails Project ---------------------------- @@ -73,9 +78,9 @@ By following along with this guide, you'll create a Rails project called (very) simple weblog. Before you can start building the application, you need to make sure that you have Rails itself installed. -TIP: The examples below use `#` and `$` to denote superuser and regular -user terminal prompts respectively in a UNIX-like OS. If you are using -Windows, your prompt will look something like `c:\source_code>` +TIP: The examples below use `$` to represent your terminal prompt in a UNIX-like OS, +though it may have been customized to appear differently. If you are using Windows, +your prompt will look something like `c:\source_code>` ### Installing Rails @@ -84,21 +89,35 @@ Open up a command line prompt. On Mac OS X open Terminal.app, on Windows choose dollar sign `$` should be run in the command line. Verify that you have a current version of Ruby installed: +TIP. A number of tools exist to help you quickly install Ruby and Ruby +on Rails on your system. Windows users can use [Rails Installer](http://railsinstaller.org), +while Mac OS X users can use [Rails One Click](http://railsoneclick.com). + ```bash $ ruby -v ruby 2.0.0p353 ``` +If you don't have Ruby installed have a look at +[ruby-lang.org](https://www.ruby-lang.org/en/downloads/) for possible ways to +install Ruby on your platform. + +Many popular UNIX-like OSes ship with an acceptable version of SQLite3. Windows +users and others can find installation instructions at [the SQLite3 website](http://www.sqlite.org). +Verify that it is correctly installed and in your PATH: + +```bash +$ sqlite3 --version +``` + +The program should report its version. + To install Rails, use the `gem install` command provided by RubyGems: ```bash $ gem install rails ``` -TIP. A number of tools exist to help you quickly install Ruby and Ruby -on Rails on your system. Windows users can use [Rails Installer](http://railsinstaller.org), -while Mac OS X users can use [Rails One Click](http://railsoneclick.com). - To verify that you have everything installed correctly, you should be able to run the following: @@ -143,20 +162,20 @@ 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.| +|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.| +|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.| +|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 vendored gems.| +|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! ------------- @@ -315,18 +334,19 @@ Now that you've seen how to create a controller, an action and a view, let's create something with a bit more substance. In the Blog application, you will now create a new _resource_. A resource is the -term used for a collection of similar objects, such as posts, people or animals. +term used for a collection of similar objects, such as articles, people or +animals. You can create, read, update and destroy items for a resource and these operations are referred to as _CRUD_ operations. Rails provides a `resources` method which can be used to declare a standard REST -resource. Here's what `config/routes.rb` should look like after the _post resource_ -is declared. +resource. Here's what `config/routes.rb` should look like after the +_article resource_ is declared. ```ruby Blog::Application.routes.draw do - resources :posts + resources :articles root 'welcome#index' end @@ -335,88 +355,91 @@ end If you run `rake routes`, you'll see that it has defined routes for all the standard RESTful actions. The meaning of the prefix column (and other columns) will be seen later, but for now notice that Rails has inferred the -singular form `post` and makes meaningful use of the distinction. +singular form `article` and makes meaningful use of the distinction. ```bash $ rake routes - Prefix Verb URI Pattern Controller#Action - posts GET /posts(.:format) posts#index - POST /posts(.:format) posts#create - new_post GET /posts/new(.:format) posts#new -edit_post GET /posts/:id/edit(.:format) posts#edit - post GET /posts/:id(.:format) posts#show - PATCH /posts/:id(.:format) posts#update - PUT /posts/:id(.:format) posts#update - DELETE /posts/:id(.:format) posts#destroy - root / welcome#index -``` - -In the next section, you will add the ability to create new posts in your + Prefix Verb URI Pattern Controller#Action + articles GET /articles(.:format) articles#index + POST /articles(.:format) articles#create + new_article GET /articles/new(.:format) articles#new +edit_article GET /articles/:id/edit(.:format) articles#edit + article GET /articles/:id(.:format) articles#show + PATCH /articles/:id(.:format) articles#update + PUT /articles/:id(.:format) articles#update + DELETE /articles/:id(.:format) articles#destroy + root GET / welcome#index +``` + +In the next section, you will add the ability to create new articles in your application and be able to view them. This is the "C" and the "R" from CRUD: creation and reading. The form for doing this will look like this: -![The new post form](images/getting_started/new_post.png) +![The new article form](images/getting_started/new_article.png) It will look a little basic for now, but that's ok. We'll look at improving the styling for it afterwards. ### Laying down the ground work -The first thing that you are going to need to create a new post within the -application is a place to do that. A great place for that would be at `/posts/new`. -With the route already defined, requests can now be made to `/posts/new` in the -application. Navigate to <http://localhost:3000/posts/new> and you'll see a -routing error: +Firstly, you need a place within the application to create a new article. A +great place for that would be at `/articles/new`. With the route already +defined, requests can now be made to `/articles/new` in the application. +Navigate to <http://localhost:3000/articles/new> and you'll see a routing +error: -![Another routing error, uninitialized constant PostsController](images/getting_started/routing_error_no_controller.png) +![Another routing error, uninitialized constant ArticlesController](images/getting_started/routing_error_no_controller.png) This error occurs because the route needs to have a controller defined in order to serve the request. The solution to this particular problem is simple: create -a controller called `PostsController`. You can do this by running this command: +a controller called `ArticlesController`. You can do this by running this +command: ```bash -$ rails g controller posts +$ rails g controller articles ``` -If you open up the newly generated `app/controllers/posts_controller.rb` you'll -see a fairly empty controller: +If you open up the newly generated `app/controllers/articles_controller.rb` +you'll see a fairly empty controller: ```ruby -class PostsController < ApplicationController +class ArticlesController < ApplicationController end ``` -A controller is simply a class that is defined to inherit from `ApplicationController`. +A controller is simply a class that is defined to inherit from +`ApplicationController`. It's inside this class that you'll define methods that will become the actions -for this controller. These actions will perform CRUD operations on the posts +for this controller. These actions will perform CRUD operations on the articles within our system. -NOTE: There are `public`, `private` and `protected` methods in Ruby, +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: +If you refresh <http://localhost:3000/articles/new> now, you'll get a new error: -![Unknown action new for PostsController!](images/getting_started/unknown_action_new_for_posts.png) +![Unknown action new for ArticlesController!](images/getting_started/unknown_action_new_for_articles.png) -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 your wanted actions during the -generation process. +This error indicates that Rails cannot find the `new` action inside the +`ArticlesController` that you just generated. This is because when controllers +are generated in Rails they are empty by default, unless you tell it +your wanted actions during the generation process. To manually define an action inside a controller, all you need to do is to -define a new method inside the controller. Open `app/controllers/posts_controller.rb` -and inside the `PostsController` class, define a `new` method like this: +define a new method inside the controller. +Open `app/controllers/articles_controller.rb` and inside the `ArticlesController` +class, define a `new` method like this: ```ruby def new end ``` -With the `new` method defined in `PostsController`, if you refresh <http://localhost:3000/posts/new> -you'll see another error: +With the `new` method defined in `ArticlesController`, if you refresh +<http://localhost:3000/articles/new> you'll see another error: -![Template is missing for posts/new](images/getting_started/template_is_missing_posts_new.png) +![Template is missing for articles/new](images/getting_started/template_is_missing_articles_new.png) You're getting this error now because Rails expects plain actions like this one to have views associated with them to display their information. With no view @@ -426,16 +449,16 @@ In the above image, the bottom line has been truncated. Let's see what the full thing looks like: <blockquote> -Missing template posts/new, application/new with {locale:[:en], formats:[:html], handlers:[:erb, :builder, :coffee]}. Searched in: * "/path/to/blog/app/views" +Missing template articles/new, application/new with {locale:[:en], formats:[:html], handlers:[:erb, :builder, :coffee]}. Searched in: * "/path/to/blog/app/views" </blockquote> That's quite a lot of text! Let's quickly go through and understand what each part of it does. The first part identifies what template is missing. In this case, it's the -`posts/new` template. Rails will first look for this template. If not found, +`articles/new` template. Rails will first look for this template. If not found, then it will attempt to load a template called `application/new`. It looks for -one here because the `PostsController` inherits from `ApplicationController`. +one here because the `ArticlesController` inherits from `ApplicationController`. The next part of the message contains a hash. The `:locale` key in this hash simply indicates what spoken language template should be retrieved. By default, @@ -451,34 +474,35 @@ Templates within a basic Rails application like this are kept in a single location, but in more complex applications it could be many different paths. The simplest template that would work in this case would be one located at -`app/views/posts/new.html.erb`. The extension of this file name is key: the +`app/views/articles/new.html.erb`. The extension of this file name is key: the first extension is the _format_ of the template, and the second extension is the _handler_ that will be used. Rails is attempting to find a template called -`posts/new` within `app/views` for the application. The format for this template -can only be `html` and the handler must be one of `erb`, `builder` or `coffee`. -Because you want to create a new HTML form, you will be using the `ERB` -language. Therefore the file should be called `posts/new.html.erb` and needs to -be located inside the `app/views` directory of the application. +`articles/new` within `app/views` for the application. The format for this +template can only be `html` and the handler must be one of `erb`, `builder` or +`coffee`. Because you want to create a new HTML form, you will be using the `ERB` +language. Therefore the file should be called `articles/new.html.erb` and needs +to be located inside the `app/views` directory of the application. -Go ahead now and create a new file at `app/views/posts/new.html.erb` and write -this content in it: +Go ahead now and create a new file at `app/views/articles/new.html.erb` and +write this content in it: ```html -<h1>New Post</h1> +<h1>New Article</h1> ``` -When you refresh <http://localhost:3000/posts/new> you'll now see that the page -has a title. The route, controller, action and view are now working -harmoniously! It's time to create the form for a new post. +When you refresh <http://localhost:3000/articles/new> you'll now see that the +page has a title. The route, controller, action and view are now working +harmoniously! It's time to create the form for a new article. ### The first form To create a form within this template, you will use a <em>form builder</em>. The primary form builder for Rails is provided by a helper -method called `form_for`. To use this method, add this code into `app/views/posts/new.html.erb`: +method called `form_for`. To use this method, add this code into +`app/views/articles/new.html.erb`: ```html+erb -<%= form_for :post do |f| %> +<%= form_for :article do |f| %> <p> <%= f.label :title %><br> <%= f.text_field :title %> @@ -499,71 +523,72 @@ If you refresh the page now, you'll see the exact same form as in the example. Building forms in Rails is really just that easy! When you call `form_for`, you pass it an identifying object for this -form. In this case, it's the symbol `:post`. This tells the `form_for` +form. In this case, it's the symbol `:article`. This tells the `form_for` helper what this form is for. Inside the block for this method, the `FormBuilder` object - represented by `f` - is used to build two labels and two -text fields, one each for the title and text of a post. Finally, a call to +text fields, one each for the title and text of an article. Finally, a call to `submit` on the `f` object will create a submit button for the form. There's one problem with this form though. If you inspect the HTML that is generated, by viewing the source of the page, you will see that the `action` -attribute for the form is pointing at `/posts/new`. This is a problem because +attribute for the form is pointing at `/articles/new`. This is a problem because this route goes to the very page that you're on right at the moment, and that -route should only be used to display the form for a new post. +route should only be used to display the form for a new article. The form needs to use a different URL in order to go somewhere else. This can be done quite simply with the `:url` option of `form_for`. Typically in Rails, the action that is used for new form submissions like this is called "create", and so the form should be pointed to that action. -Edit the `form_for` line inside `app/views/posts/new.html.erb` to look like this: +Edit the `form_for` line inside `app/views/articles/new.html.erb` to look like +this: ```html+erb -<%= form_for :post, url: posts_path do |f| %> +<%= form_for :article, url: articles_path do |f| %> ``` -In this example, the `posts_path` helper is passed to the `:url` option. +In this example, the `articles_path` helper is passed to the `:url` option. To see what Rails will do with this, we look back at the output of `rake routes`: ```bash $ rake routes - Prefix Verb URI Pattern Controller#Action - posts GET /posts(.:format) posts#index - POST /posts(.:format) posts#create - new_post GET /posts/new(.:format) posts#new -edit_post GET /posts/:id/edit(.:format) posts#edit - post GET /posts/:id(.:format) posts#show - PATCH /posts/:id(.:format) posts#update - PUT /posts/:id(.:format) posts#update - DELETE /posts/:id(.:format) posts#destroy - root / welcome#index -``` - -The `posts_path` helper tells Rails to point the form -to the URI Pattern associated with the `posts` prefix; and + Prefix Verb URI Pattern Controller#Action + articles GET /articles(.:format) articles#index + POST /articles(.:format) articles#create + new_article GET /articles/new(.:format) articles#new +edit_article GET /articles/:id/edit(.:format) articles#edit + article GET /articles/:id(.:format) articles#show + PATCH /articles/:id(.:format) articles#update + PUT /articles/:id(.:format) articles#update + DELETE /articles/:id(.:format) articles#destroy + root GET / welcome#index +``` + +The `articles_path` helper tells Rails to point the form +to the URI Pattern associated with the `articles` prefix; and the form will (by default) send a `POST` request to that route. This is associated with the -`create` action of the current controller, the `PostsController`. +`create` action of the current controller, the `ArticlesController`. With the form and its associated route defined, you will be able to fill in the form and then click the submit button to begin the process of creating a new -post, so go ahead and do that. When you submit the form, you should see a +article, so go ahead and do that. When you submit the form, you should see a familiar error: -![Unknown action create for PostsController](images/getting_started/unknown_action_create_for_posts.png) +![Unknown action create for ArticlesController](images/getting_started/unknown_action_create_for_articles.png) -You now need to create the `create` action within the `PostsController` for this -to work. +You now need to create the `create` action within the `ArticlesController` for +this to work. -### Creating posts +### Creating articles To make the "Unknown action" go away, you can define a `create` action within -the `PostsController` class in `app/controllers/posts_controller.rb`, underneath -the `new` action: +the `ArticlesController` class in `app/controllers/articles_controller.rb`, +underneath the `new` action: ```ruby -class PostsController < ApplicationController +class ArticlesController < ApplicationController def new end @@ -574,7 +599,7 @@ end If you re-submit the form now, you'll see another familiar error: a template is missing. That's ok, we can ignore that for now. What the `create` action should -be doing is saving our new post to a database. +be doing is saving our new article to the database. When a form is submitted, the fields of the form are sent to Rails as _parameters_. These parameters can then be referenced inside the controller @@ -583,12 +608,12 @@ look like, change the `create` action to this: ```ruby def create - render text: params[:post].inspect + render text: params[:article].inspect end ``` The `render` method here is taking a very simple hash with a key of `text` and -value of `params[:post].inspect`. The `params` method is the object which +value of `params[:article].inspect`. The `params` method is the object which represents the parameters (or fields) coming in from the form. The `params` method returns an `ActiveSupport::HashWithIndifferentAccess` object, which allows you to access the keys of the hash using either strings or symbols. In @@ -598,14 +623,14 @@ If you re-submit the form one more time you'll now no longer get the missing template error. Instead, you'll see something that looks like the following: ```ruby -{"title"=>"First post!", "text"=>"This is my first post."} +{"title"=>"First article!", "text"=>"This is my first article."} ``` -This action is now displaying the parameters for the post that are coming in +This action is now displaying the parameters for the article that are coming in from the form. However, this isn't really all that helpful. Yes, you can see the parameters but nothing in particular is being done with them. -### Creating the Post model +### Creating the Article model Models in Rails use a singular name, and their corresponding database tables use a plural name. Rails provides a generator for creating models, which @@ -613,17 +638,17 @@ most Rails developers tend to use when creating new models. To create the new model, run this command in your terminal: ```bash -$ rails generate model Post title:string text:text +$ rails generate model Article title:string text:text ``` -With that command we told Rails that we want a `Post` model, together +With that command we told Rails that we want a `Article` model, together with a _title_ attribute of type string, and a _text_ attribute -of type text. Those attributes are automatically added to the `posts` -table in the database and mapped to the `Post` model. +of type text. Those attributes are automatically added to the `articles` +table in the database and mapped to the `Article` model. Rails responded by creating a bunch of files. For -now, we're only interested in `app/models/post.rb` and -`db/migrate/20120419084633_create_posts.rb` (your name could be a bit +now, we're only interested in `app/models/article.rb` and +`db/migrate/20140120191729_create_articles.rb` (your name could be a bit different). The latter is responsible for creating the database structure, which is what we'll look at next. @@ -642,13 +667,13 @@ and it's possible to undo a migration after it's been applied to your database. Migration filenames include a timestamp to ensure that they're processed in the order that they were created. -If you look in the `db/migrate/20120419084633_create_posts.rb` file (remember, +If you look in the `db/migrate/20140120191729_create_articles.rb` file (remember, yours will have a slightly different name), here's what you'll find: ```ruby -class CreatePosts < ActiveRecord::Migration +class CreateArticles < ActiveRecord::Migration def change - create_table :posts do |t| + create_table :articles do |t| t.string :title t.text :text @@ -658,12 +683,12 @@ class CreatePosts < ActiveRecord::Migration end ``` -The above migration creates a method named `change` which will be called when you -run this migration. The action defined in this method is also reversible, which -means Rails knows how to reverse the change made by this migration, in case you -want to reverse it later. When you run this migration it will create a -`posts` table with one string column and a text column. It also creates two -timestamp fields to allow Rails to track post creation and update times. +The above migration creates a method named `change` which will be called when +you run this migration. The action defined in this method is also reversible, +which means Rails knows how to reverse the change made by this migration, +in case you want to reverse it later. When you run this migration it will create +an `articles` table with one string column and a text column. It also creates +two timestamp fields to allow Rails to track article creation and update times. TIP: For more information about migrations, refer to [Rails Database Migrations](migrations.html). @@ -674,14 +699,14 @@ At this point, you can use a rake command to run the migration: $ rake db:migrate ``` -Rails will execute this migration command and tell you it created the Posts +Rails will execute this migration command and tell you it created the Articles table. ```bash -== CreatePosts: migrating ==================================================== --- create_table(:posts) +== CreateArticles: migrating ================================================== +-- create_table(:articles) -> 0.0019s -== CreatePosts: migrated (0.0020s) =========================================== +== CreateArticles: migrated (0.0020s) ========================================= ``` NOTE. Because you're working in the development environment by default, this @@ -692,34 +717,35 @@ invoking the command: `rake db:migrate RAILS_ENV=production`. ### Saving data in the controller -Back in `PostsController`, we need to change the `create` action -to use the new `Post` model to save the data in the database. Open `app/controllers/posts_controller.rb` -and change the `create` action to look like this: +Back in `ArticlesController`, we need to change the `create` action +to use the new `Article` model to save the data in the database. +Open `app/controllers/articles_controller.rb` and change the `create` action to +look like this: ```ruby def create - @post = Post.new(params[:post]) + @article = Article.new(params[:article]) - @post.save - redirect_to @post + @article.save + redirect_to @article end ``` Here's what's going on: every Rails model can be initialized with its respective attributes, which are automatically mapped to the respective database columns. In the first line we do just that -(remember that `params[:post]` contains the attributes we're interested in). -Then, `@post.save` is responsible for saving the model in the database. +(remember that `params[:article]` contains the attributes we're interested in). +Then, `@article.save` is responsible for saving the model in the database. Finally, we redirect the user to the `show` action, which we'll define later. -TIP: As we'll see later, `@post.save` returns a boolean indicating -whether the model was saved or not. +TIP: As we'll see later, `@article.save` returns a boolean indicating +whether the article was saved or not. If you now go to -<http://localhost:3000/posts/new> you'll *almost* be able to create a post. Try -it! You should get an error that looks like this: +<http://localhost:3000/articles/new> you'll *almost* be able to create an +article. Try it! You should get an error that looks like this: -![Forbidden attributes for new post](images/getting_started/forbidden_attributes_for_new_post.png) +![Forbidden attributes for new article](images/getting_started/forbidden_attributes_for_new_article.png) Rails has several security features that help you write secure applications, and you're running into one of them now. This one is called @@ -730,28 +756,28 @@ look like this: ```ruby def create - @post = Post.new(post_params) + @article = Article.new(article_params) - @post.save - redirect_to @post + @article.save + redirect_to @article end private - def post_params - params.require(:post).permit(:title, :text) + def article_params + params.require(:article).permit(:title, :text) end ``` See the `permit`? It allows us to accept both `title` and `text` in this action. -TIP: Note that `def post_params` is private. This new approach prevents an +TIP: Note that `def article_params` is private. This new approach prevents an attacker from setting the model's attributes by manipulating the hash passed to the model. For more information, refer to -[this blog post about Strong Parameters](http://weblog.rubyonrails.org/2012/3/21/strong-parameters/). +[this blog article about Strong Parameters](http://weblog.rubyonrails.org/2012/3/21/strong-parameters/). -### Showing Posts +### Showing Articles If you submit the form again now, Rails will complain about not finding the `show` action. That's not very useful though, so let's add the @@ -761,68 +787,70 @@ As we have seen in the output of `rake routes`, the route for `show` action is as follows: ``` -post GET /posts/:id(.:format) posts#show +article GET /articles/:id(.:format) articles#show ``` The special syntax `:id` tells rails that this route expects an `:id` -parameter, which in our case will be the id of the post. +parameter, which in our case will be the id of the article. As we did before, we need to add the `show` action in -`app/controllers/posts_controller.rb` and its respective view. +`app/controllers/articles_controller.rb` and its respective view. ```ruby def show - @post = Post.find(params[:id]) + @article = Article.find(params[:id]) end ``` -A couple of things to note. We use `Post.find` to find the post we're +A couple of things to note. We use `Article.find` to find the article we're interested in, passing in `params[:id]` to get the `:id` parameter from the request. We also use an instance variable (prefixed by `@`) to hold a -reference to the post object. We do this because Rails will pass all instance +reference to the article object. We do this because Rails will pass all instance variables to the view. -Now, create a new file `app/views/posts/show.html.erb` with the following +Now, create a new file `app/views/articles/show.html.erb` with the following content: ```html+erb <p> <strong>Title:</strong> - <%= @post.title %> + <%= @article.title %> </p> <p> <strong>Text:</strong> - <%= @post.text %> + <%= @article.text %> </p> ``` -With this change, you should finally be able to create new posts. -Visit <http://localhost:3000/posts/new> and give it a try! +With this change, you should finally be able to create new articles. +Visit <http://localhost:3000/articles/new> and give it a try! -![Show action for posts](images/getting_started/show_action_for_posts.png) +![Show action for articles](images/getting_started/show_action_for_articles.png) -### Listing all posts +### Listing all articles -We still need a way to list all our posts, so let's do that. +We still need a way to list all our articles, so let's do that. The route for this as per output of `rake routes` is: ``` -posts GET /posts(.:format) posts#index +articles GET /articles(.:format) articles#index ``` -Add the corresponding `index` action for that route inside the `PostsController` in the `app/controllers/posts_controller.rb` file: +Add the corresponding `index` action for that route inside the +`ArticlesController` in the `app/controllers/articles_controller.rb` file: ```ruby def index - @posts = Post.all + @articles = Article.all end ``` -And then finally, add view for this action, located at `app/views/posts/index.html.erb`: +And then finally, add view for this action, located at +`app/views/articles/index.html.erb`: ```html+erb -<h1>Listing posts</h1> +<h1>Listing articles</h1> <table> <tr> @@ -830,70 +858,71 @@ And then finally, add view for this action, located at `app/views/posts/index.ht <th>Text</th> </tr> - <% @posts.each do |post| %> + <% @articles.each do |article| %> <tr> - <td><%= post.title %></td> - <td><%= post.text %></td> + <td><%= article.title %></td> + <td><%= article.text %></td> </tr> <% end %> </table> ``` -Now if you go to `http://localhost:3000/posts` you will see a list of all the -posts that you have created. +Now if you go to `http://localhost:3000/articles` you will see a list of all the +articles that you have created. ### Adding links -You can now create, show, and list posts. Now let's add some links to +You can now create, show, and list articles. Now let's add some links to navigate through pages. Open `app/views/welcome/index.html.erb` and modify it as follows: ```html+erb <h1>Hello, Rails!</h1> -<%= link_to 'My Blog', controller: 'posts' %> +<%= link_to 'My Blog', controller: 'articles' %> ``` The `link_to` method is one of Rails' built-in view helpers. It creates a hyperlink based on text to display and where to go - in this case, to the path -for posts. +for articles. -Let's add links to the other views as well, starting with adding this "New Post" -link to `app/views/posts/index.html.erb`, placing it above the `<table>` tag: +Let's add links to the other views as well, starting with adding this +"New Article" link to `app/views/articles/index.html.erb`, placing it above the +`<table>` tag: ```erb -<%= link_to 'New post', new_post_path %> +<%= link_to 'New article', new_article_path %> ``` -This link will allow you to bring up the form that lets you create a new post. -You should also add a link to this template - `app/views/posts/new.html.erb` - -to go back to the `index` action. Do this by adding this underneath the form in -this template: +This link will allow you to bring up the form that lets you create a new article. + +Also add a link in `app/views/articles/new.html.erb`, underneath the form, to +go back to the `index` action: ```erb -<%= form_for :post do |f| %> +<%= form_for :article do |f| %> ... <% end %> -<%= link_to 'Back', posts_path %> +<%= link_to 'Back', articles_path %> ``` -Finally, add another link to the `app/views/posts/show.html.erb` template to go -back to the `index` action as well, so that people who are viewing a single post -can go back and view the whole list again: +Finally, add another link to the `app/views/articles/show.html.erb` template to +go back to the `index` action as well, so that people who are viewing a single +article can go back and view the whole list again: ```html+erb <p> <strong>Title:</strong> - <%= @post.title %> + <%= @article.title %> </p> <p> <strong>Text:</strong> - <%= @post.text %> + <%= @article.text %> </p> -<%= link_to 'Back', posts_path %> +<%= link_to 'Back', articles_path %> ``` TIP: If you want to link to an action in the same controller, you don't @@ -906,87 +935,88 @@ and restart the web server when a change is made. ### Adding Some Validation -The model file, `app/models/post.rb` is about as simple as it can get: +The model file, `app/models/article.rb` is about as simple as it can get: ```ruby -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base end ``` -There isn't much to this file - but note that the `Post` class inherits from +There isn't much to this file - but note that the `Article` class inherits from `ActiveRecord::Base`. Active Record supplies a great deal of functionality to your Rails models for free, including basic database CRUD (Create, Read, Update, Destroy) operations, data validation, as well as sophisticated search support and the ability to relate multiple models to one another. Rails includes methods to help you validate the data that you send to models. -Open the `app/models/post.rb` file and edit it: +Open the `app/models/article.rb` file and edit it: ```ruby -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base validates :title, presence: true, length: { minimum: 5 } end ``` -These changes will ensure that all posts have a title that is at least five +These changes will ensure that all articles have a title that is at least five characters long. Rails can validate a variety of conditions in a model, including the presence or uniqueness of columns, their format, and the existence of associated objects. Validations are covered in detail in [Active Record Validations](active_record_validations.html) -With the validation now in place, when you call `@post.save` on an invalid -post, it will return `false`. If you open `app/controllers/posts_controller.rb` -again, you'll notice that we don't check the result of calling `@post.save` -inside the `create` action. If `@post.save` fails in this situation, we need to -show the form back to the user. To do this, change the `new` and `create` -actions inside `app/controllers/posts_controller.rb` to these: +With the validation now in place, when you call `@article.save` on an invalid +article, it will return `false`. If you open +`app/controllers/articles_controller.rb` again, you'll notice that we don't +check the result of calling `@article.save` inside the `create` action. +If `@article.save` fails in this situation, we need to show the form back to the +user. To do this, change the `new` and `create` actions inside +`app/controllers/articles_controller.rb` to these: ```ruby def new - @post = Post.new + @article = Article.new end def create - @post = Post.new(post_params) + @article = Article.new(article_params) - if @post.save - redirect_to @post + if @article.save + redirect_to @article else render 'new' end end private - def post_params - params.require(:post).permit(:title, :text) + def article_params + params.require(:article).permit(:title, :text) end ``` -The `new` action is now creating a new instance variable called `@post`, and +The `new` action is now creating a new instance variable called `@article`, and you'll see why that is in just a few moments. Notice that inside the `create` action we use `render` instead of `redirect_to` -when `save` returns `false`. The `render` method is used so that the `@post` +when `save` returns `false`. The `render` method is used so that the `@article` object is passed back to the `new` template when it is rendered. This rendering -is done within the same request as the form submission, whereas the `redirect_to` -will tell the browser to issue another request. +is done within the same request as the form submission, whereas the +`redirect_to` will tell the browser to issue another request. If you reload -<http://localhost:3000/posts/new> and -try to save a post without a title, Rails will send you back to the +<http://localhost:3000/articles/new> and +try to save an article without a title, Rails will send you back to the form, but that's not very useful. You need to tell the user that something went wrong. To do that, you'll modify -`app/views/posts/new.html.erb` to check for error messages: +`app/views/articles/new.html.erb` to check for error messages: ```html+erb -<%= form_for :post, url: posts_path do |f| %> - <% if @post.errors.any? %> +<%= form_for :article, url: articles_path do |f| %> + <% if @article.errors.any? %> <div id="error_explanation"> - <h2><%= pluralize(@post.errors.count, "error") %> prohibited - this post from being saved:</h2> + <h2><%= pluralize(@article.errors.count, "error") %> prohibited + this article from being saved:</h2> <ul> - <% @post.errors.full_messages.each do |msg| %> + <% @article.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> @@ -1007,57 +1037,58 @@ something went wrong. To do that, you'll modify </p> <% end %> -<%= link_to 'Back', posts_path %> +<%= link_to 'Back', articles_path %> ``` A few things are going on. We check if there are any errors with -`@post.errors.any?`, and in that case we show a list of all -errors with `@post.errors.full_messages`. +`@article.errors.any?`, and in that case we show a list of all +errors with `@article.errors.full_messages`. `pluralize` is a rails helper that takes a number and a string as its arguments. If the number is greater than one, the string will be automatically pluralized. -The reason why we added `@post = Post.new` in the `PostsController` is that -otherwise `@post` would be `nil` in our view, and calling -`@post.errors.any?` would throw an error. +The reason why we added `@article = Article.new` in the `ArticlesController` is +that otherwise `@article` would be `nil` in our view, and calling +`@article.errors.any?` would throw an error. TIP: Rails automatically wraps fields that contain an error with a div with class `field_with_errors`. You can define a css rule to make them standout. -Now you'll get a nice error message when saving a post without title when you -attempt to do just that on the new post form [(http://localhost:3000/posts/new)](http://localhost:3000/posts/new). +Now you'll get a nice error message when saving an article without title when +you attempt to do just that on the new article form +[(http://localhost:3000/articles/new)](http://localhost:3000/articles/new). ![Form With Errors](images/getting_started/form_with_errors.png) -### Updating Posts +### Updating Articles We've covered the "CR" part of CRUD. Now let's focus on the "U" part, updating -posts. +articles. -The first step we'll take is adding an `edit` action to the `PostsController`. +The first step we'll take is adding an `edit` action to the `ArticlesController`. ```ruby def edit - @post = Post.find(params[:id]) + @article = Article.find(params[:id]) end ``` The view will contain a form similar to the one we used when creating -new posts. Create a file called `app/views/posts/edit.html.erb` and make +new articles. Create a file called `app/views/articles/edit.html.erb` and make it look as follows: ```html+erb -<h1>Editing post</h1> +<h1>Editing article</h1> -<%= form_for :post, url: post_path(@post), method: :patch do |f| %> - <% if @post.errors.any? %> +<%= form_for :article, url: article_path(@article), method: :patch do |f| %> + <% if @article.errors.any? %> <div id="error_explanation"> - <h2><%= pluralize(@post.errors.count, "error") %> prohibited - this post from being saved:</h2> + <h2><%= pluralize(@article.errors.count, "error") %> prohibited + this article from being saved:</h2> <ul> - <% @post.errors.full_messages.each do |msg| %> + <% @article.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> @@ -1078,7 +1109,7 @@ it look as follows: </p> <% end %> -<%= link_to 'Back', posts_path %> +<%= link_to 'Back', articles_path %> ``` This time we point the form to the `update` action, which is not defined yet @@ -1088,42 +1119,48 @@ The `method: :patch` option tells Rails that we want this form to be submitted via the `PATCH` HTTP method which is the HTTP method you're expected to use to **update** resources according to the REST protocol. -TIP: By default forms built with the _form_for_ helper are sent via `POST`. +The first parameter of the `form_tag` can be an object, say, `@article` which would +cause the helper to fill in the form with the fields of the object. Passing in a +symbol (`:article`) with the same name as the instance variable (`@article`) also +automagically leads to the same behavior. This is what is happening here. More details +can be found in [form_for documentation](http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for). -Next we need to create the `update` action in `app/controllers/posts_controller.rb`: +Next we need to create the `update` action in +`app/controllers/articles_controller.rb`: ```ruby def update - @post = Post.find(params[:id]) + @article = Article.find(params[:id]) - if @post.update(post_params) - redirect_to @post + if @article.update(article_params) + redirect_to @article else render 'edit' end end private - def post_params - params.require(:post).permit(:title, :text) + def article_params + params.require(:article).permit(:title, :text) end ``` The new method, `update`, is used when you want to update a record that already exists, and it accepts a hash containing the attributes that you want to update. As before, if there was an error updating the -post we want to show the form back to the user. +article we want to show the form back to the user. -We reuse the `post_params` method that we defined earlier for the create action. +We reuse the `article_params` method that we defined earlier for the create +action. TIP: You don't need to pass all attributes to `update`. For -example, if you'd call `@post.update(title: 'A new title')` +example, if you'd call `@article.update(title: 'A new title')` Rails would only update the `title` attribute, leaving all other attributes untouched. Finally, we want to show a link to the `edit` action in the list of all the -posts, so let's add that now to `app/views/posts/index.html.erb` to make it -appear next to the "Show" link: +articles, so let's add that now to `app/views/articles/index.html.erb` to make +it appear next to the "Show" link: ```html+erb <table> @@ -1133,26 +1170,26 @@ appear next to the "Show" link: <th colspan="2"></th> </tr> -<% @posts.each do |post| %> +<% @articles.each do |article| %> <tr> - <td><%= post.title %></td> - <td><%= post.text %></td> - <td><%= link_to 'Show', post_path(post) %></td> - <td><%= link_to 'Edit', edit_post_path(post) %></td> + <td><%= article.title %></td> + <td><%= article.text %></td> + <td><%= link_to 'Show', article_path(article) %></td> + <td><%= link_to 'Edit', edit_article_path(article) %></td> </tr> <% end %> </table> ``` -And we'll also add one to the `app/views/posts/show.html.erb` template as well, -so that there's also an "Edit" link on a post's page. Add this at the bottom of -the template: +And we'll also add one to the `app/views/articles/show.html.erb` template as +well, so that there's also an "Edit" link on an article's page. Add this at the +bottom of the template: ```html+erb ... -<%= link_to 'Back', posts_path %> -| <%= link_to 'Edit', edit_post_path(@post) %> +<%= link_to 'Back', articles_path %> +| <%= link_to 'Edit', edit_article_path(@article) %> ``` And here's how our app looks so far: @@ -1169,17 +1206,17 @@ underscore. TIP: You can read more about partials in the [Layouts and Rendering in Rails](layouts_and_rendering.html) guide. -Create a new file `app/views/posts/_form.html.erb` with the following +Create a new file `app/views/articles/_form.html.erb` with the following content: ```html+erb -<%= form_for @post do |f| %> - <% if @post.errors.any? %> +<%= form_for @article do |f| %> + <% if @article.errors.any? %> <div id="error_explanation"> - <h2><%= pluralize(@post.errors.count, "error") %> prohibited - this post from being saved:</h2> + <h2><%= pluralize(@article.errors.count, "error") %> prohibited + this article from being saved:</h2> <ul> - <% @post.errors.full_messages.each do |msg| %> + <% @article.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> @@ -1203,41 +1240,41 @@ content: Everything except for the `form_for` declaration remained the same. The reason we can use this shorter, simpler `form_for` declaration -to stand in for either of the other forms is that `@post` is a *resource* +to stand in for either of the other forms is that `@article` is a *resource* corresponding to a full set of RESTful routes, and Rails is able to infer which URI and method to use. For more information about this use of `form_for`, see [Resource-oriented style](//api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for-label-Resource-oriented+style). -Now, let's update the `app/views/posts/new.html.erb` view to use this new +Now, let's update the `app/views/articles/new.html.erb` view to use this new partial, rewriting it completely: ```html+erb -<h1>New post</h1> +<h1>New article</h1> <%= render 'form' %> -<%= link_to 'Back', posts_path %> +<%= link_to 'Back', articles_path %> ``` -Then do the same for the `app/views/posts/edit.html.erb` view: +Then do the same for the `app/views/articles/edit.html.erb` view: ```html+erb -<h1>Edit post</h1> +<h1>Edit article</h1> <%= render 'form' %> -<%= link_to 'Back', posts_path %> +<%= link_to 'Back', articles_path %> ``` -### Deleting Posts +### Deleting Articles -We're now ready to cover the "D" part of CRUD, deleting posts from the +We're now ready to cover the "D" part of CRUD, deleting articles from the database. Following the REST convention, the route for -deleting posts as per output of `rake routes` is: +deleting articles as per output of `rake routes` is: ```ruby -DELETE /posts/:id(.:format) posts#destroy +DELETE /articles/:id(.:format) articles#destroy ``` The `delete` routing method should be used for routes that destroy @@ -1245,19 +1282,19 @@ resources. If this was left as a typical `get` route, it could be possible for people to craft malicious URLs like this: ```html -<a href='http://example.com/posts/1/destroy'>look at this cat!</a> +<a href='http://example.com/articles/1/destroy'>look at this cat!</a> ``` We use the `delete` method for destroying resources, and this route is mapped to -the `destroy` action inside `app/controllers/posts_controller.rb`, which doesn't -exist yet, but is provided below: +the `destroy` action inside `app/controllers/articles_controller.rb`, which +doesn't exist yet, but is provided below: ```ruby def destroy - @post = Post.find(params[:id]) - @post.destroy + @article = Article.find(params[:id]) + @article.destroy - redirect_to posts_path + redirect_to articles_path end ``` @@ -1266,12 +1303,12 @@ them from the database. Note that we don't need to add a view for this action since we're redirecting to the `index` action. Finally, add a 'Destroy' link to your `index` action template -(`app/views/posts/index.html.erb`) to wrap everything +(`app/views/articles/index.html.erb`) to wrap everything together. ```html+erb -<h1>Listing Posts</h1> -<%= link_to 'New post', new_post_path %> +<h1>Listing Articles</h1> +<%= link_to 'New article', new_article_path %> <table> <tr> <th>Title</th> @@ -1279,13 +1316,13 @@ together. <th colspan="3"></th> </tr> -<% @posts.each do |post| %> +<% @articles.each do |article| %> <tr> - <td><%= post.title %></td> - <td><%= post.text %></td> - <td><%= link_to 'Show', post_path(post) %></td> - <td><%= link_to 'Edit', edit_post_path(post) %></td> - <td><%= link_to 'Destroy', post_path(post), + <td><%= article.title %></td> + <td><%= article.text %></td> + <td><%= link_to 'Show', article_path(article) %></td> + <td><%= link_to 'Edit', edit_article_path(article) %></td> + <td><%= link_to 'Destroy', article_path(article), method: :delete, data: { confirm: 'Are you sure?' } %></td> </tr> <% end %> @@ -1304,7 +1341,7 @@ Without this file, the confirmation dialog box wouldn't appear. ![Confirm Dialog](images/getting_started/confirm_dialog.png) Congratulations, you can now create, show, list, update and destroy -posts. +articles. TIP: In general, Rails encourages the use of resources objects in place of declaring routes manually. @@ -1315,23 +1352,23 @@ Adding a Second Model --------------------- It's time to add a second model to the application. The second model will handle -comments on posts. +comments on articles. ### Generating a Model We're going to see the same generator that we used before when creating -the `Post` model. This time we'll create a `Comment` model to hold -reference of post comments. Run this command in your terminal: +the `Article` model. This time we'll create a `Comment` model to hold +reference of article comments. Run this command in your terminal: ```bash -$ rails generate model Comment commenter:string body:text post:references +$ rails generate model Comment commenter:string body:text article:references ``` This command will generate four files: | File | Purpose | | -------------------------------------------- | ------------------------------------------------------------------------------------------------------ | -| db/migrate/20100207235629_create_comments.rb | Migration to create the comments table in your database (your name will include a different timestamp) | +| db/migrate/20140120201010_create_comments.rb | Migration to create the comments table in your database (your name will include a different timestamp) | | app/models/comment.rb | The Comment model | | test/models/comment_test.rb | Testing harness for the comments model | | test/fixtures/comments.yml | Sample comments for use in testing | @@ -1340,12 +1377,12 @@ First, take a look at `app/models/comment.rb`: ```ruby class Comment < ActiveRecord::Base - belongs_to :post + belongs_to :article end ``` -This is very similar to the `Post` model that you saw earlier. The difference -is the line `belongs_to :post`, which sets up an Active Record _association_. +This is very similar to the `Article` model that you saw earlier. The difference +is the line `belongs_to :article`, which sets up an Active Record _association_. You'll learn a little about associations in the next section of this guide. In addition to the model, Rails has also made a migration to create the @@ -1357,7 +1394,9 @@ class CreateComments < ActiveRecord::Migration create_table :comments do |t| t.string :commenter t.text :body - t.references :post, index: true + + # this line adds an integer column called `article_id`. + t.references :article, index: true t.timestamps end @@ -1386,26 +1425,27 @@ run against the current database, so in this case you will just see: ### Associating Models Active Record associations let you easily declare the relationship between two -models. In the case of comments and posts, you could write out the relationships -this way: +models. In the case of comments and articles, you could write out the +relationships this way: -* Each comment belongs to one post. -* One post can have many comments. +* Each comment belongs to one article. +* One article can have many comments. In fact, this is very close to the syntax that Rails uses to declare this association. You've already seen the line of code inside the `Comment` model -(app/models/comment.rb) that makes each comment belong to a Post: +(app/models/comment.rb) that makes each comment belong to an Article: ```ruby class Comment < ActiveRecord::Base - belongs_to :post + belongs_to :article end ``` -You'll need to edit `app/models/post.rb` to add the other side of the association: +You'll need to edit `app/models/article.rb` to add the other side of the +association: ```ruby -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base has_many :comments validates :title, presence: true, length: { minimum: 5 } @@ -1413,29 +1453,31 @@ end ``` These two declarations enable a good bit of automatic behavior. For example, if -you have an instance variable `@post` containing a post, you can retrieve all -the comments belonging to that post as an array using `@post.comments`. +you have an instance variable `@article` containing an article, you can retrieve +all the comments belonging to that article as an array using +`@article.comments`. TIP: For more information on Active Record associations, see the [Active Record Associations](association_basics.html) guide. ### Adding a Route for Comments -As with the `welcome` controller, we will need to add a route so that Rails knows -where we would like to navigate to see `comments`. Open up the +As with the `welcome` controller, we will need to add a route so that Rails +knows where we would like to navigate to see `comments`. Open up the `config/routes.rb` file again, and edit it as follows: ```ruby -resources :posts do +resources :articles do resources :comments end ``` -This creates `comments` as a _nested resource_ within `posts`. This is another -part of capturing the hierarchical relationship that exists between posts and -comments. +This creates `comments` as a _nested resource_ within `articles`. This is +another part of capturing the hierarchical relationship that exists between +articles and comments. -TIP: For more information on routing, see the [Rails Routing](routing.html) guide. +TIP: For more information on routing, see the [Rails Routing](routing.html) +guide. ### Generating a Controller @@ -1459,27 +1501,27 @@ This creates six files and one empty directory: | app/assets/stylesheets/comment.css.scss | Cascading style sheet for the controller | Like with any blog, our readers will create their comments directly after -reading the post, and once they have added their comment, will be sent back to -the post show page to see their comment now listed. Due to this, our +reading the article, and once they have added their comment, will be sent back +to the article show page to see their comment now listed. Due to this, our `CommentsController` is there to provide a method to create comments and delete spam comments when they arrive. -So first, we'll wire up the Post show template -(`app/views/posts/show.html.erb`) to let us make a new comment: +So first, we'll wire up the Article show template +(`app/views/articles/show.html.erb`) to let us make a new comment: ```html+erb <p> <strong>Title:</strong> - <%= @post.title %> + <%= @article.title %> </p> <p> <strong>Text:</strong> - <%= @post.text %> + <%= @article.text %> </p> <h2>Add a comment:</h2> -<%= form_for([@post, @post.comments.build]) do |f| %> +<%= form_for([@article, @article.comments.build]) do |f| %> <p> <%= f.label :commenter %><br> <%= f.text_field :commenter %> @@ -1493,22 +1535,22 @@ So first, we'll wire up the Post show template </p> <% end %> -<%= link_to 'Back', posts_path %> -| <%= link_to 'Edit', edit_post_path(@post) %> +<%= link_to 'Back', articles_path %> +| <%= link_to 'Edit', edit_article_path(@article) %> ``` -This adds a form on the `Post` show page that creates a new comment by +This adds a form on the `Article` show page that creates a new comment by calling the `CommentsController` `create` action. The `form_for` call here uses -an array, which will build a nested route, such as `/posts/1/comments`. +an array, which will build a nested route, such as `/articles/1/comments`. Let's wire up the `create` in `app/controllers/comments_controller.rb`: ```ruby class CommentsController < ApplicationController def create - @post = Post.find(params[:post_id]) - @comment = @post.comments.create(comment_params) - redirect_to post_path(@post) + @article = Article.find(params[:article_id]) + @comment = @article.comments.create(comment_params) + redirect_to article_path(@article) end private @@ -1518,35 +1560,36 @@ class CommentsController < ApplicationController end ``` -You'll see a bit more complexity here than you did in the controller for posts. -That's a side-effect of the nesting that you've set up. Each request for a -comment has to keep track of the post to which the comment is attached, thus the -initial call to the `find` method of the `Post` model to get the post in question. +You'll see a bit more complexity here than you did in the controller for +articles. That's a side-effect of the nesting that you've set up. Each request +for a comment has to keep track of the article to which the comment is attached, +thus the initial call to the `find` method of the `Article` model to get the +article in question. In addition, the code takes advantage of some of the methods available for an -association. We use the `create` method on `@post.comments` to create and save -the comment. This will automatically link the comment so that it belongs to that -particular post. +association. We use the `create` method on `@article.comments` to create and +save the comment. This will automatically link the comment so that it belongs to +that particular article. -Once we have made the new comment, we send the user back to the original post -using the `post_path(@post)` helper. As we have already seen, this calls the -`show` action of the `PostsController` which in turn renders the `show.html.erb` -template. This is where we want the comment to show, so let's add that to the -`app/views/posts/show.html.erb`. +Once we have made the new comment, we send the user back to the original article +using the `article_path(@article)` helper. As we have already seen, this calls +the `show` action of the `ArticlesController` which in turn renders the +`show.html.erb` template. This is where we want the comment to show, so let's +add that to the `app/views/articles/show.html.erb`. ```html+erb <p> <strong>Title:</strong> - <%= @post.title %> + <%= @article.title %> </p> <p> <strong>Text:</strong> - <%= @post.text %> + <%= @article.text %> </p> <h2>Comments</h2> -<% @post.comments.each do |comment| %> +<% @article.comments.each do |comment| %> <p> <strong>Commenter:</strong> <%= comment.commenter %> @@ -1559,7 +1602,7 @@ template. This is where we want the comment to show, so let's add that to the <% end %> <h2>Add a comment:</h2> -<%= form_for([@post, @post.comments.build]) do |f| %> +<%= form_for([@article, @article.comments.build]) do |f| %> <p> <%= f.label :commenter %><br> <%= f.text_field :commenter %> @@ -1573,26 +1616,26 @@ template. This is where we want the comment to show, so let's add that to the </p> <% end %> -<%= link_to 'Edit Post', edit_post_path(@post) %> | -<%= link_to 'Back to Posts', posts_path %> +<%= link_to 'Edit Article', edit_article_path(@article) %> | +<%= link_to 'Back to Articles', articles_path %> ``` -Now you can add posts and comments to your blog and have them show up in the +Now you can add articles and comments to your blog and have them show up in the right places. -![Post with Comments](images/getting_started/post_with_comments.png) +![Article with Comments](images/getting_started/article_with_comments.png) Refactoring ----------- -Now that we have posts and comments working, take a look at the -`app/views/posts/show.html.erb` template. It is getting long and awkward. We can -use partials to clean it up. +Now that we have articles and comments working, take a look at the +`app/views/articles/show.html.erb` template. It is getting long and awkward. We +can use partials to clean it up. ### Rendering Partial Collections -First, we will make a comment partial to extract showing all the comments for the -post. Create the file `app/views/comments/_comment.html.erb` and put the +First, we will make a comment partial to extract showing all the comments for +the article. Create the file `app/views/comments/_comment.html.erb` and put the following into it: ```html+erb @@ -1607,25 +1650,25 @@ following into it: </p> ``` -Then you can change `app/views/posts/show.html.erb` to look like the +Then you can change `app/views/articles/show.html.erb` to look like the following: ```html+erb <p> <strong>Title:</strong> - <%= @post.title %> + <%= @article.title %> </p> <p> <strong>Text:</strong> - <%= @post.text %> + <%= @article.text %> </p> <h2>Comments</h2> -<%= render @post.comments %> +<%= render @article.comments %> <h2>Add a comment:</h2> -<%= form_for([@post, @post.comments.build]) do |f| %> +<%= form_for([@article, @article.comments.build]) do |f| %> <p> <%= f.label :commenter %><br> <%= f.text_field :commenter %> @@ -1639,13 +1682,13 @@ following: </p> <% end %> -<%= link_to 'Edit Post', edit_post_path(@post) %> | -<%= link_to 'Back to Posts', posts_path %> +<%= link_to 'Edit Article', edit_article_path(@article) %> | +<%= link_to 'Back to Articles', articles_path %> ``` This will now render the partial in `app/views/comments/_comment.html.erb` once -for each comment that is in the `@post.comments` collection. As the `render` -method iterates over the `@post.comments` collection, it assigns each +for each comment that is in the `@article.comments` collection. As the `render` +method iterates over the `@article.comments` collection, it assigns each comment to a local variable named the same as the partial, in this case `comment` which is then available in the partial for us to show. @@ -1655,7 +1698,7 @@ Let us also move that new comment section out to its own partial. Again, you create a file `app/views/comments/_form.html.erb` containing: ```html+erb -<%= form_for([@post, @post.comments.build]) do |f| %> +<%= form_for([@article, @article.comments.build]) do |f| %> <p> <%= f.label :commenter %><br> <%= f.text_field :commenter %> @@ -1670,27 +1713,27 @@ create a file `app/views/comments/_form.html.erb` containing: <% end %> ``` -Then you make the `app/views/posts/show.html.erb` look like the following: +Then you make the `app/views/articles/show.html.erb` look like the following: ```html+erb <p> <strong>Title:</strong> - <%= @post.title %> + <%= @article.title %> </p> <p> <strong>Text:</strong> - <%= @post.text %> + <%= @article.text %> </p> <h2>Comments</h2> -<%= render @post.comments %> +<%= render @article.comments %> <h2>Add a comment:</h2> <%= render "comments/form" %> -<%= link_to 'Edit Post', edit_post_path(@post) %> | -<%= link_to 'Back to Posts', posts_path %> +<%= link_to 'Edit Article', edit_article_path(@article) %> | +<%= link_to 'Back to Articles', articles_path %> ``` The second render just defines the partial template we want to render, @@ -1698,8 +1741,8 @@ The second render just defines the partial template we want to render, string and realize that you want to render the `_form.html.erb` file in the `app/views/comments` directory. -The `@post` object is available to any partials rendered in the view because we -defined it as an instance variable. +The `@article` object is available to any partials rendered in the view because +we defined it as an instance variable. Deleting Comments ----------------- @@ -1723,30 +1766,30 @@ So first, let's add the delete link in the </p> <p> - <%= link_to 'Destroy Comment', [comment.post, comment], + <%= link_to 'Destroy Comment', [comment.article, comment], method: :delete, data: { confirm: 'Are you sure?' } %> </p> ``` 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 -controller (`app/controllers/comments_controller.rb`): +/articles/:article_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 controller (`app/controllers/comments_controller.rb`): ```ruby class CommentsController < ApplicationController def create - @post = Post.find(params[:post_id]) - @comment = @post.comments.create(comment_params) - redirect_to post_path(@post) + @article = Article.find(params[:article_id]) + @comment = @article.comments.create(comment_params) + redirect_to article_path(@article) end def destroy - @post = Post.find(params[:post_id]) - @comment = @post.comments.find(params[:id]) + @article = Article.find(params[:article_id]) + @comment = @article.comments.find(params[:id]) @comment.destroy - redirect_to post_path(@post) + redirect_to article_path(@article) end private @@ -1756,20 +1799,20 @@ class CommentsController < ApplicationController end ``` -The `destroy` action will find the post we are looking at, locate the comment -within the `@post.comments` collection, and then remove it from the -database and send us back to the show action for the post. +The `destroy` action will find the article we are looking at, locate the comment +within the `@article.comments` collection, and then remove it from the +database and send us back to the show action for the article. ### Deleting Associated Objects -If you delete a post then its associated comments will also need to be deleted. -Otherwise they would simply occupy space in the database. Rails allows you to -use the `dependent` option of an association to achieve this. Modify the Post -model, `app/models/post.rb`, as follows: +If you delete an article then its associated comments will also need to be +deleted. Otherwise they would simply occupy space in the database. Rails allows +you to use the `dependent` option of an association to achieve this. Modify the +Article model, `app/models/article.rb`, as follows: ```ruby -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base has_many :comments, dependent: :destroy validates :title, presence: true, length: { minimum: 5 } @@ -1782,33 +1825,34 @@ Security ### Basic Authentication If you were to publish your blog online, anybody would be able to add, edit and -delete posts or delete comments. +delete articles or delete comments. Rails provides a very simple HTTP authentication system that will work nicely in this situation. -In the `PostsController` we need to have a way to block access to the various +In the `ArticlesController` we need to have a way to block access to the various actions if the person is not authenticated, here we can use the Rails `http_basic_authenticate_with` method, allowing access to the requested action if that method allows it. To use the authentication system, we specify it at the top of our -`PostsController`, in this case, we want the user to be authenticated on every -action, except for `index` and `show`, so we write that in `app/controllers/posts_controller.rb`: +`ArticlesController`, in this case, we want the user to be authenticated on +every action, except for `index` and `show`, so we write that in +`app/controllers/articles_controller.rb`: ```ruby -class PostsController < ApplicationController +class ArticlesController < ApplicationController http_basic_authenticate_with name: "dhh", password: "secret", except: [:index, :show] def index - @posts = Post.all + @articles = Article.all end # snipped for brevity ``` -We also only want to allow authenticated users to delete comments, so in the +We also want to allow only authenticated users to delete comments, so in the `CommentsController` (`app/controllers/comments_controller.rb`) we write: ```ruby @@ -1817,21 +1861,22 @@ class CommentsController < ApplicationController http_basic_authenticate_with name: "dhh", password: "secret", only: :destroy def create - @post = Post.find(params[:post_id]) + @article = Article.find(params[:article_id]) ... end # snipped for brevity ``` -Now if you try to create a new post, you will be greeted with a basic HTTP +Now if you try to create a new article, you will be greeted with a basic HTTP Authentication challenge ![Basic HTTP Authentication Challenge](images/getting_started/challenge.png) Other authentication methods are available for Rails applications. Two popular -authentication add-ons for Rails are the [Devise](https://github.com/plataformatec/devise) -rails engine and the [Authlogic](https://github.com/binarylogic/authlogic) gem, +authentication add-ons for Rails are the +[Devise](https://github.com/plataformatec/devise) rails engine and +the [Authlogic](https://github.com/binarylogic/authlogic) gem, along with a number of others. @@ -1887,15 +1932,16 @@ cannot be automatically detected by Rails and corrected. Two very common sources of data that are not UTF-8: -* Your text editor: Most text editors (such as TextMate), default to saving files as - UTF-8. If your text editor does not, this can result in special characters that you - enter in your templates (such as é) to appear as a diamond with a question mark inside - in the browser. This also applies to your i18n translation files. - Most editors that do not already default to UTF-8 (such as some versions of - Dreamweaver) offer a way to change the default to UTF-8. Do so. -* Your database: Rails defaults to converting data from your database into UTF-8 at - the boundary. However, if your database is not using UTF-8 internally, it may not - be able to store all characters that your users enter. For instance, if your database - is using Latin-1 internally, and your user enters a Russian, Hebrew, or Japanese - character, the data will be lost forever once it enters the database. If possible, - use UTF-8 as the internal storage of your database. +* Your text editor: Most text editors (such as TextMate), default to saving + files as UTF-8. If your text editor does not, this can result in special + characters that you enter in your templates (such as é) to appear as a diamond + with a question mark inside in the browser. This also applies to your i18n + translation files. Most editors that do not already default to UTF-8 (such as + some versions of Dreamweaver) offer a way to change the default to UTF-8. Do + so. +* Your database: Rails defaults to converting data from your database into UTF-8 + at the boundary. However, if your database is not using UTF-8 internally, it + may not be able to store all characters that your users enter. For instance, + if your database is using Latin-1 internally, and your user enters a Russian, + Hebrew, or Japanese character, the data will be lost forever once it enters + the database. If possible, use UTF-8 as the internal storage of your database. diff --git a/guides/source/i18n.md b/guides/source/i18n.md index 8dfb17a681..d72717fa3b 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -214,7 +214,7 @@ This approach has almost the same set of advantages as setting the locale from t Getting the locale from `params` and setting it accordingly is not hard; including it in every URL and thus **passing it through the requests** is. To include an explicit option in every URL (e.g. `link_to( books_url(locale: I18n.locale))`) would be tedious and probably impossible, of course. -Rails contains infrastructure for "centralizing dynamic decisions about the URLs" in its [`ApplicationController#default_url_options`](http://api.rubyonrails.org/classes/ActionController/Base.html#M000515, which is useful precisely in this scenario: it enables us to set "defaults" for [`url_for`](http://api.rubyonrails.org/classes/ActionController/Base.html#M000503) and helper methods dependent on it (by implementing/overriding this method). +Rails contains infrastructure for "centralizing dynamic decisions about the URLs" in its [`ApplicationController#default_url_options`](http://api.rubyonrails.org/classes/ActionController/Base.html#M000515), which is useful precisely in this scenario: it enables us to set "defaults" for [`url_for`](http://api.rubyonrails.org/classes/ActionController/Base.html#M000503) and helper methods dependent on it (by implementing/overriding this method). We can include something like this in our `ApplicationController` then: diff --git a/guides/source/initialization.md b/guides/source/initialization.md index 5e2e0ad3e3..ec3cec5c6f 100644 --- a/guides/source/initialization.md +++ b/guides/source/initialization.md @@ -270,7 +270,7 @@ def parse_options(args) options = default_options # Don't evaluate CGI ISINDEX parameters. - # http://hoohoo.ncsa.uiuc.edu/cgi/cl.html + # http://www.meb.uni-bonn.de/docs/cgi/cl.html args.clear if ENV.include?("REQUEST_METHOD") options.merge! opt_parser.parse! args @@ -522,7 +522,7 @@ I18n and Rails configuration are all being defined here. ### Back to `config/environment.rb` The rest of `config/application.rb` defines the configuration for the -`Rails::Application` which will be used once the application is fully +`Rails::Application` which will be used once the application is fully initialized. When `config/application.rb` has finished loading Rails and defined the application namespace, we go back to `config/environment.rb`, where the application is initialized. For example, if the application was called diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md index c72b584ed6..93e25d619e 100644 --- a/guides/source/layouts_and_rendering.md +++ b/guides/source/layouts_and_rendering.md @@ -1119,11 +1119,11 @@ With this change, you can access an instance of the `@products` collection as th You can also pass in arbitrary local variables to any partial you are rendering with the `locals: {}` option: ```erb -<%= render partial: "products", collection: @products, +<%= render partial: "product", collection: @products, as: :item, locals: {title: "Products Page"} %> ``` -Would render a partial `_products.html.erb` once for each instance of `product` in the `@products` instance variable passing the instance to the partial as a local variable called `item` and to each partial, make the local variable `title` available with the value `Products Page`. +In this case, the partial will have access to a local variable `title` with the value "Products Page". TIP: Rails also makes a counter variable available within a partial called by the collection, named after the member of the collection followed by `_counter`. For example, if you're rendering `@products`, within the partial you can refer to `product_counter` to tell you how many times the partial has been rendered. This does not work in conjunction with the `as: :value` option. diff --git a/guides/source/migrations.md b/guides/source/migrations.md index 5d5c2724b1..64c4e1e07e 100644 --- a/guides/source/migrations.md +++ b/guides/source/migrations.md @@ -642,7 +642,7 @@ method for all the migrations that have not yet been run. If there are no such migrations, it exits. It will run these migrations in order based on the date of the migration. -Note that running the `db:migrate` also invokes the `db:schema:dump` task, which +Note that running the `db:migrate` task also invokes the `db:schema:dump` task, which will update your `db/schema.rb` file to match the structure of your database. If you specify a target version, Active Record will run the required migrations diff --git a/guides/source/routing.md b/guides/source/routing.md index 70d4722068..9c495bf09d 100644 --- a/guides/source/routing.md +++ b/guides/source/routing.md @@ -631,7 +631,7 @@ This will define a `user_path` method that will be available in controllers, hel ### HTTP Verb Constraints -In general, you should use the `get`, `post`, `put` and `delete` methods to constrain a route to a particular verb. You can use the `match` method with the `:via` option to match multiple verbs at once: +In general, you should use the `get`, `post`, `put`, `patch` and `delete` methods to constrain a route to a particular verb. You can use the `match` method with the `:via` option to match multiple verbs at once: ```ruby match 'photos', to: 'photos#show', via: [:get, :post] diff --git a/guides/source/security.md b/guides/source/security.md index cffe7c85f1..ece431dae7 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -549,7 +549,7 @@ Injection is very tricky, because the same code or parameter can be malicious in ### Whitelists versus Blacklists -NOTE: _When sanitizing, protecting or verifying something, whitelists over blacklists._ +NOTE: _When sanitizing, protecting or verifying something, prefer whitelists over blacklists._ A blacklist can be a list of bad e-mail addresses, non-public actions or bad HTML tags. This is opposed to a whitelist which lists the good e-mail addresses, public actions, good HTML tags and so on. Although sometimes it is not possible to create a whitelist (in a SPAM filter, for example), _prefer to use whitelist approaches_: @@ -915,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 33cd3e868b..07f3aad1e6 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -339,7 +339,7 @@ NOTE: The execution of each test method stops as soon as any error or an asserti When a test fails you are presented with the corresponding backtrace. By default Rails filters that backtrace and will only print lines relevant to your -application. This eliminates the framwork noise and helps to focus on your +application. This eliminates the framework noise and helps to focus on your code. However there are situations when you want to see the full backtrace. simply set the `BACKTRACE` environment variable to enable this behavior: @@ -401,8 +401,8 @@ Rails adds some custom assertions of its own to the `test/unit` framework: | `assert_no_difference(expressions, message = nil, &block)` | Asserts that the numeric result of evaluating an expression is not changed before and after invoking the passed in block.| | `assert_recognizes(expected_options, path, extras={}, message=nil)` | Asserts that the routing of the given path was handled correctly and that the parsed options (given in the expected_options hash) match path. Basically, it asserts that Rails recognizes the route given by expected_options.| | `assert_generates(expected_path, options, defaults={}, extras = {}, message=nil)` | Asserts that the provided options can be used to generate the provided path. This is the inverse of assert_recognizes. The extras parameter is used to tell the request the names and values of additional request parameters that would be in a query string. The message parameter allows you to specify a custom error message for assertion failures.| -| `assert_response(type, message = nil)` | Asserts that the response comes with a specific status code. You can specify `:success` to indicate 200-299, `:redirect` to indicate 300-399, `:missing` to indicate 404, or `:error` to match the 500-599 range| -| `assert_redirected_to(options = {}, message=nil)` | Assert that the redirection options passed in match those of the redirect called in the latest action. This match can be partial, such that `assert_redirected_to(controller: "weblog")` will also match the redirection of `redirect_to(controller: "weblog", action: "show")` and so on.| +| `assert_response(type, message = nil)` | Asserts that the response comes with a specific status code. You can specify `:success` to indicate 200-299, `:redirect` to indicate 300-399, `:missing` to indicate 404, or `:error` to match the 500-599 range. You can also pass an explicit status number or its symbolic equivalent. For more information, see [full list of status codes](http://rubydoc.info/github/rack/rack/master/Rack/Utils#HTTP_STATUS_CODES-constant) and how their [mapping](http://rubydoc.info/github/rack/rack/master/Rack/Utils#SYMBOL_TO_STATUS_CODE-constant) works.| +| `assert_redirected_to(options = {}, message=nil)` | Assert that the redirection options passed in match those of the redirect called in the latest action. This match can be partial, such that `assert_redirected_to(controller: "weblog")` will also match the redirection of `redirect_to(controller: "weblog", action: "show")` and so on. You can also pass named routes such as `assert_redirected_to root_path` and Active Record objects such as `assert_redirected_to @article`.| | `assert_template(expected = nil, message=nil)` | Asserts that the request was rendered with the appropriate template file.| You'll see the usage of some of these assertions in the next chapter. @@ -518,8 +518,10 @@ You also have access to three instance variables in your functional tests: ### Setting Headers and CGI variables -Headers and cgi variables can be set directly on the `@request` -instance variable: +[HTTP headers](http://tools.ietf.org/search/rfc2616#section-5.3) +and +[CGI variables](http://tools.ietf.org/search/rfc3875#section-4.1) +can be set directly on the `@request` instance variable: ```ruby # setting a HTTP Header @@ -937,7 +939,6 @@ Here's a unit test to test a mailer named `UserMailer` whose action `invite` is require 'test_helper' class UserMailerTest < ActionMailer::TestCase - tests UserMailer test "invite" do # Send the email, then test that it got queued email = UserMailer.create_invite('me@example.com', diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index ab8cabe48d..8aae3bbc1a 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -62,7 +62,7 @@ If you want to use Spring as your application preloader you need to: 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` @@ -98,6 +98,19 @@ If your test helper contains a call to is now done automatically when you `require 'test_help'`, although leaving this line in your helper is not harmful in any way. +### Cookies serializer + +Applications created before Rails 4.1 uses `Marshal` to serialize cookie values into +the signed and encrypted cookie jars. If you want to use the new `JSON`-based format +in your application, you can add an initializer file with the following content: + + ```ruby + Rails.application.config.cookies_serializer :hybrid + ``` + +This would transparently migrate your existing `Marshal`-serialized cookies into the +new `JSON`-based format. + ### Changes in JSON handling There are a few major changes related to JSON handling in Rails 4.1. @@ -148,7 +161,7 @@ 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. @@ -320,7 +333,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 diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index da7a4ce59a..bade9ef543 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,26 @@ +* Do not crash when `config/secrets.yml` is empty. + + *Yves Senn* + +* Set `dump_schema_after_migration` config values in production. + + Set `config.active_record.dump_schema_after_migration` as false + in the generated `config/environments/production.rb` file. + + *Emil Soman* + +* Added Thor-action for creation of migrations. + + Fixes #13588, #12674. + + *Gert Goet* + +* 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* @@ -87,7 +110,7 @@ *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/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 05acd78d98..e37347b576 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -205,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.cookies_serializer" => config.action_dispatch.cookies_serializer }) end end @@ -307,7 +308,8 @@ module Rails yaml = config.paths["config/secrets"].first if File.exist?(yaml) require "erb" - env_secrets = YAML.load(ERB.new(IO.read(yaml)).result)[Rails.env] + all_secrets = YAML.load(ERB.new(IO.read(yaml)).result) || {} + env_secrets = all_secrets[Rails.env] secrets.merge!(env_secrets.symbolize_keys) if env_secrets end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index e902205a13..20e3de32aa 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -109,6 +109,8 @@ module Rails 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. " \ "Error: #{e.message}" + rescue => e + raise e, "Cannot load `Rails.application.database_configuration`:\n#{e.message}", e.backtrace end def log_level diff --git a/railties/lib/rails/generators/actions/create_migration.rb b/railties/lib/rails/generators/actions/create_migration.rb new file mode 100644 index 0000000000..9c3332927f --- /dev/null +++ b/railties/lib/rails/generators/actions/create_migration.rb @@ -0,0 +1,68 @@ +require 'thor/actions/create_file' + +module Rails + module Generators + module Actions + class CreateMigration < Thor::Actions::CreateFile + + def migration_dir + File.dirname(@destination) + end + + def migration_file_name + @base.migration_file_name + end + + def identical? + exists? && File.binread(existing_migration) == render + end + + def revoke! + say_destination = exists? ? relative_existing_migration : relative_destination + say_status :remove, :red, say_destination + return unless exists? + ::FileUtils.rm_rf(existing_migration) unless pretend? + existing_migration + end + + def relative_existing_migration + base.relative_to_original_destination_root(existing_migration) + end + + def existing_migration + @existing_migration ||= begin + @base.class.migration_exists?(migration_dir, migration_file_name) || + File.exist?(@destination) && @destination + end + end + alias :exists? :existing_migration + + protected + + def on_conflict_behavior(&block) + options = base.options.merge(config) + if identical? + say_status :identical, :blue, relative_existing_migration + elsif options[:force] + say_status :remove, :green, relative_existing_migration + say_status :create, :green + unless pretend? + ::FileUtils.rm_rf(existing_migration) + block.call + end + elsif options[:skip] + say_status :skip, :yellow + else + say_status :conflict, :red + raise Error, "Another migration is already named #{migration_file_name}: " + + "#{existing_migration}. Use --force to replace this migration file." + end + end + + def say_status(status, color, message = relative_destination) + base.shell.say_status(status, message, color) if config[:verbose] + end + end + end + end +end diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 1b50569c9e..f1f79d8378 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -14,7 +14,6 @@ module Rails DATABASES.concat(JDBC_DATABASES) attr_accessor :rails_template - attr_accessor :app_template add_shebang_option! argument :app_path, type: :string @@ -27,9 +26,6 @@ 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" @@ -126,10 +122,6 @@ 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 @@ -149,92 +141,21 @@ module Rails FileUtils.cd(destination_root) unless options[:pretend] end - class TemplateRecorder < ::BasicObject # :nodoc: - attr_reader :gems - - def initialize(target) - @target = target - # unfortunately, instance eval has access to these ivars - @app_const = target.send :app_const if target.respond_to?(:app_const, true) - @app_const_base = target.send :app_const_base if target.respond_to?(:app_const_base, true) - @app_name = target.send :app_name if target.respond_to?(:app_name, true) - @commands = [] - @gems = [] - end - - def gemfile_entry(*args) - @target.send :gemfile_entry, *args - end - - def add_gem_entry_filter(*args, &block) - @target.send :add_gem_entry_filter, *args, &block - end - - def remove_gem(*args, &block) - @target.send :remove_gem, *args, &block - end - - def method_missing(name, *args, &block) - @commands << [name, args, block] - end - - def respond_to_missing?(method, priv = false) - super || @target.respond_to?(method, priv) - end - - def replay! - @commands.each do |name, args, block| - @target.send name, *args, &block - end - end - end - def apply_rails_template - @recorder = TemplateRecorder.new self - - apply(rails_template, target: self) if rails_template - apply(app_template, target: @recorder) if app_template + apply rails_template if rails_template rescue Thor::Error, LoadError, Errno::ENOENT => e raise Error, "The template [#{rails_template}] could not be loaded. Error: #{e}" end - def replay_template - @recorder.replay! if @recorder - end - - def apply(path, config={}) - verbose = config.fetch(:verbose, true) - target = config.fetch(:target, self) - is_uri = path =~ /^https?\:\/\// - path = find_in_source_paths(path) unless is_uri - - say_status :apply, path, verbose - shell.padding += 1 if verbose - - if is_uri - contents = open(path, "Accept" => "application/x-thor-template") {|io| io.read } - else - contents = open(path) {|io| io.read } - end - - target.instance_eval(contents, path) - shell.padding -= 1 if verbose - end - def set_default_accessors! self.destination_root = File.expand_path(app_path, destination_root) - self.rails_template = 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 + self.rails_template = case options[:template] + when /^https?:\/\// + options[:template] + when String + File.expand_path(options[:template], Dir.pwd) + else + options[:template] end end @@ -389,7 +310,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 @@ -412,7 +333,8 @@ module Rails require 'bundler' Bundler.with_clean_env do - print `"#{Gem.ruby}" "#{_bundle_command}" #{command}` + output = `"#{Gem.ruby}" "#{_bundle_command}" #{command}` + print output unless options[:quiet] 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 index 8bb7c2b768..b5045671b3 100644 --- a/railties/lib/rails/generators/erb/mailer/templates/view.html.erb +++ b/railties/lib/rails/generators/erb/mailer/templates/view.html.erb @@ -1,5 +1,5 @@ <h1><%= class_name %>#<%= @action %></h1> <p> - <%%= @greeting %>, find me in app/views/<%= @path %> + <%%= @greeting %>, find me in <%= @path %> </p> diff --git a/railties/lib/rails/generators/erb/mailer/templates/view.text.erb b/railties/lib/rails/generators/erb/mailer/templates/view.text.erb index 6d597256a6..342285df19 100644 --- a/railties/lib/rails/generators/erb/mailer/templates/view.text.erb +++ b/railties/lib/rails/generators/erb/mailer/templates/view.text.erb @@ -1,3 +1,3 @@ <%= class_name %>#<%= @action %> -<%%= @greeting %>, find me in app/views/<%= @path %> +<%%= @greeting %>, find me in <%= @path %> diff --git a/railties/lib/rails/generators/migration.rb b/railties/lib/rails/generators/migration.rb index 3566f96f5e..cd388e590a 100644 --- a/railties/lib/rails/generators/migration.rb +++ b/railties/lib/rails/generators/migration.rb @@ -1,4 +1,5 @@ require 'active_support/concern' +require 'rails/generators/actions/create_migration' module Rails module Generators @@ -29,6 +30,19 @@ module Rails end end + def create_migration(destination, data, config = {}, &block) + action Rails::Generators::Actions::CreateMigration.new(self, destination, block || data.to_s, config) + end + + def set_migration_assigns!(destination) + destination = File.expand_path(destination, self.destination_root) + + migration_dir = File.dirname(destination) + @migration_number = self.class.next_migration_number(migration_dir) + @migration_file_name = File.basename(destination, '.rb') + @migration_class_name = @migration_file_name.camelize + end + # Creates a migration template at the given destination. The difference # to the default template method is that the migration version is appended # to the destination file name. @@ -37,26 +51,18 @@ module Rails # available as instance variables in the template to be rendered. # # migration_template "migration.rb", "db/migrate/add_foo_to_bar.rb" - def migration_template(source, destination=nil, config={}) - destination = File.expand_path(destination || source, self.destination_root) + def migration_template(source, destination, config = {}) + source = File.expand_path(find_in_source_paths(source.to_s)) - migration_dir = File.dirname(destination) - @migration_number = self.class.next_migration_number(migration_dir) - @migration_file_name = File.basename(destination).sub(/\.rb$/, '') - @migration_class_name = @migration_file_name.camelize + set_migration_assigns!(destination) + context = instance_eval('binding') - destination = self.class.migration_exists?(migration_dir, @migration_file_name) + dir, base = File.split(destination) + numbered_destination = File.join(dir, ["%migration_number%", base].join('_')) - if !(destination && options[:skip]) && behavior == :invoke - if destination && options.force? - remove_file(destination) - elsif destination - raise Error, "Another migration is already named #{@migration_file_name}: #{destination}. Use --force to remove the old migration file and replace it." - end - destination = File.join(migration_dir, "#{@migration_number}_#{@migration_file_name}.rb") + create_migration numbered_destination, nil, config do + ERB.new(::File.binread(source), nil, '-', '@output_buffer').result(context) end - - template(source, destination, config) 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 d2eca5b2fb..83cb1dc0d5 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -166,7 +166,6 @@ module Rails end public_task :set_default_accessors! - public_task :apply_rails_template public_task :create_root def create_root_files @@ -236,8 +235,7 @@ module Rails build(:leftovers) end - public_task :run_bundle - public_task :replay_template + public_task :apply_rails_template, :run_bundle public_task :generate_spring_binstubs protected 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 cce4743a33..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 @@ -35,4 +35,7 @@ Rails.application.configure do # 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 3baa382bd6..d9cc60d656 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 @@ -81,4 +81,9 @@ Rails.application.configure do # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = ::Logger::Formatter.new + <%- unless options.skip_active_record? -%> + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + <%- end -%> end 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 a90361725b..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 @@ -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/cookies_serializer.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb new file mode 100644 index 0000000000..7a06a89f0f --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb @@ -0,0 +1,3 @@ +# Be sure to restart your server when you modify this file. + +Rails.application.config.action_dispatch.cookies_serializer = :json
\ No newline at end of file diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb index dbe1e37d8e..f6f529b80a 100644 --- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb +++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb @@ -185,7 +185,6 @@ task default: :test end public_task :set_default_accessors! - public_task :apply_rails_template public_task :create_root def create_root_files @@ -242,6 +241,7 @@ task default: :test build(:leftovers) end + public_task :apply_rails_template, :run_bundle def name @name ||= begin @@ -255,9 +255,6 @@ task default: :test end end - public_task :run_bundle - public_task :replay_template - protected def app_templates_dir 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/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/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 02d8b2c91d..b2d0e7e202 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -336,6 +336,14 @@ module ApplicationTests assert_equal 'myamazonsecretaccesskey', app.secrets.aws_secret_access_key end + test "blank config/secrets.yml does not crash the loading process" do + app_file 'config/secrets.yml', <<-YAML + YAML + require "#{app_path}/config/environment" + + assert_nil app.secrets.not_defined + end + test "protect from forgery is the default in a new app" do make_basic_app @@ -781,5 +789,22 @@ module ApplicationTests assert_not Rails.configuration.respond_to?(:method_missing) assert Rails.configuration.respond_to?(:method_missing, true) end + + test "config.active_record.dump_schema_after_migration is false on production" do + build_app + ENV["RAILS_ENV"] = "production" + + require "#{app_path}/config/environment" + + assert_not ActiveRecord::Base.dump_schema_after_migration + end + + test "config.active_record.dump_schema_after_migration is true by default on development" do + ENV["RAILS_ENV"] = "development" + + require "#{app_path}/config/environment" + + assert ActiveRecord::Base.dump_schema_after_migration + end end end diff --git a/railties/test/application/rake/migrations_test.rb b/railties/test/application/rake/migrations_test.rb index 33c753868c..b7fd5d02c5 100644 --- a/railties/test/application/rake/migrations_test.rb +++ b/railties/test/application/rake/migrations_test.rb @@ -153,6 +153,37 @@ module ApplicationTests assert_match(/up\s+\d{3,}\s+Add email to users/, output) end end + + test 'schema generation when dump_schema_after_migration is set' do + add_to_config('config.active_record.dump_schema_after_migration = false') + + Dir.chdir(app_path) do + `rails generate model book title:string; + bundle exec rake db:migrate` + + assert !File.exist?("db/schema.rb") + end + + add_to_config('config.active_record.dump_schema_after_migration = true') + + Dir.chdir(app_path) do + `rails generate model author name:string; + bundle exec rake db:migrate` + + structure_dump = File.read("db/schema.rb") + assert_match(/create_table "authors"/, structure_dump) + end + end + + test 'default schema generation after migration' do + Dir.chdir(app_path) do + `rails generate model book title:string; + bundle exec rake db:migrate` + + structure_dump = File.read("db/schema.rb") + assert_match(/create_table "books"/, structure_dump) + end + end end end end diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index ddecee2ca1..5811379e35 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -163,73 +163,6 @@ class AppGeneratorTest < Rails::Generators::TestCase end end - 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 - - 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 - Tempfile.open 'my_template' do |template| - template.puts 'add_gem_entry_filter { |gem| gem.name != "jbuilder" }' - template.close - - run_generator([destination_root, "-n", template.path]) - assert_file "Gemfile" do |contents| - assert_no_match 'jbuilder', contents - end - end - end - - def test_remove_gem - Tempfile.open 'my_template' do |template| - template.puts 'remove_gem "jbuilder"' - template.close - - run_generator([destination_root, "-n", template.path]) - assert_file "Gemfile" do |contents| - assert_no_match 'jbuilder', contents - end - end - 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 - - run_generator([destination_root, "-n", template.path]) - assert_file "Gemfile" do |contents| - assert_no_match 'turbolinks', contents - end - - assert_file "app/views/layouts/application.html.erb" do |contents| - assert_no_match 'turbolinks', contents - end - - assert_file "app/views/layouts/application.html.erb" do |contents| - assert_no_match('data-turbolinks-track', contents) - end - - assert_file "app/assets/javascripts/application.js" do |contents| - assert_no_match 'turbolinks', contents - end - end - end - def test_config_another_database run_generator([destination_root, "-d", "mysql"]) assert_file "config/database.yml", /mysql/ diff --git a/railties/test/generators/create_migration_test.rb b/railties/test/generators/create_migration_test.rb new file mode 100644 index 0000000000..e16a77479a --- /dev/null +++ b/railties/test/generators/create_migration_test.rb @@ -0,0 +1,134 @@ +require 'generators/generators_test_helper' +require 'rails/generators/rails/migration/migration_generator' + +class CreateMigrationTest < Rails::Generators::TestCase + include GeneratorsTestHelper + + class Migrator < Rails::Generators::MigrationGenerator + include Rails::Generators::Migration + + def self.next_migration_number(dirname) + current_migration_number(dirname) + 1 + end + end + + tests Migrator + + def default_destination_path + "db/migrate/create_articles.rb" + end + + def create_migration(destination_path = default_destination_path, config = {}, generator_options = {}, &block) + migration_name = File.basename(destination_path, '.rb') + generator([migration_name], generator_options) + generator.set_migration_assigns!(destination_path) + + dir, base = File.split(destination_path) + timestamped_destination_path = File.join(dir, ["%migration_number%", base].join('_')) + + @migration = Rails::Generators::Actions::CreateMigration.new(generator, timestamped_destination_path, block || "contents", config) + end + + def migration_exists!(*args) + @existing_migration = create_migration(*args) + invoke! + @generator = nil + end + + def invoke! + capture(:stdout) { @migration.invoke! } + end + + def revoke! + capture(:stdout) { @migration.revoke! } + end + + def test_invoke + create_migration + + assert_match(/create db\/migrate\/1_create_articles.rb\n/, invoke!) + assert_file @migration.destination + end + + def test_invoke_pretended + create_migration(default_destination_path, {}, { pretend: true }) + + assert_no_file @migration.destination + end + + def test_invoke_when_exists + migration_exists! + create_migration + + assert_equal @existing_migration.destination, @migration.existing_migration + end + + def test_invoke_when_exists_identical + migration_exists! + create_migration + + assert_match(/identical db\/migrate\/1_create_articles.rb\n/, invoke!) + assert @migration.identical? + end + + def test_invoke_when_exists_not_identical + migration_exists! + create_migration { "different content" } + + assert_raise(Rails::Generators::Error) { invoke! } + end + + def test_invoke_forced_when_exists_not_identical + dest = "db/migrate/migration.rb" + migration_exists!(dest) + create_migration(dest, force: true) { "different content" } + + stdout = invoke! + assert_match(/remove db\/migrate\/1_migration.rb\n/, stdout) + assert_match(/create db\/migrate\/2_migration.rb\n/, stdout) + assert_file @migration.destination + assert_no_file @existing_migration.destination + end + + def test_invoke_forced_pretended_when_exists_not_identical + migration_exists! + create_migration(default_destination_path, { force: true }, { pretend: true }) do + "different content" + end + + stdout = invoke! + assert_match(/remove db\/migrate\/1_create_articles.rb\n/, stdout) + assert_match(/create db\/migrate\/2_create_articles.rb\n/, stdout) + assert_no_file @migration.destination + end + + def test_invoke_skipped_when_exists_not_identical + migration_exists! + create_migration(default_destination_path, {}, { skip: true }) { "different content" } + + assert_match(/skip db\/migrate\/2_create_articles.rb\n/, invoke!) + assert_no_file @migration.destination + end + + def test_revoke + migration_exists! + create_migration + + assert_match(/remove db\/migrate\/1_create_articles.rb\n/, revoke!) + assert_no_file @existing_migration.destination + end + + def test_revoke_pretended + migration_exists! + create_migration(default_destination_path, {}, { pretend: true }) + + assert_match(/remove db\/migrate\/1_create_articles.rb\n/, revoke!) + assert_file @existing_migration.destination + end + + def test_revoke_when_no_exists + create_migration + + assert_match(/remove db\/migrate\/1_create_articles.rb\n/, revoke!) + end +end diff --git a/railties/test/generators/generator_test.rb b/railties/test/generators/generator_test.rb index 94d2c1bf50..7871399dd7 100644 --- a/railties/test/generators/generator_test.rb +++ b/railties/test/generators/generator_test.rb @@ -1,7 +1,6 @@ require 'active_support/test_case' require 'active_support/testing/autorun' require 'rails/generators/app_base' -require 'rails/generators/rails/app/app_generator' module Rails module Generators diff --git a/railties/test/generators/mailer_generator_test.rb b/railties/test/generators/mailer_generator_test.rb index d209801f60..25649881eb 100644 --- a/railties/test/generators/mailer_generator_test.rb +++ b/railties/test/generators/mailer_generator_test.rb @@ -69,12 +69,12 @@ class MailerGeneratorTest < Rails::Generators::TestCase 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) + assert_match(%r(\sapp/views/notifier/foo\.text\.erb), view) assert_match(/<%= @greeting %>/, view) end assert_file "app/views/notifier/bar.text.erb" do |view| - assert_match(%r(app/views/notifier/bar\.text\.erb), view) + assert_match(%r(\sapp/views/notifier/bar\.text\.erb), view) assert_match(/<%= @greeting %>/, view) end end @@ -82,12 +82,12 @@ class MailerGeneratorTest < Rails::Generators::TestCase 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(%r(\sapp/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(%r(\sapp/views/notifier/bar\.html\.erb), view) assert_match(/<%= @greeting %>/, view) end end |