diff options
285 files changed, 4563 insertions, 1786 deletions
diff --git a/.travis.yml b/.travis.yml index c8afb46187..730b9a1084 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,9 +5,9 @@ before_install: rvm: - 1.9.3 - 2.0.0 - - 2.1.0-preview2 - - rbx-2.2.1 - - jruby-19mode + - 2.1.0-rc1 + - rbx + - jruby env: - "GEM=railties" - "GEM=ap,am,amo,as,av" @@ -17,8 +17,8 @@ env: - "GEM=ar:postgresql" matrix: allow_failures: - - rvm: rbx-2.2.1 - - rvm: jruby-19mode + - rvm: rbx + - rvm: jruby notifications: email: false irc: @@ -32,3 +32,6 @@ notifications: rooms: - secure: "YA1alef1ESHWGFNVwvmVGCkMe4cUy4j+UcNvMUESraceiAfVyRMAovlQBGs6\n9kBRm7DHYBUXYC2ABQoJbQRLDr/1B5JPf/M8+Qd7BKu8tcDC03U01SMHFLpO\naOs/HLXcDxtnnpL07tGVsm0zhMc5N8tq4/L3SHxK7Vi+TacwQzI=" bundler_args: --path vendor/bundle --without test +services: + - memcached + @@ -36,9 +36,9 @@ group :test do gem 'ruby-prof', '~> 0.11.2' end - platforms :mri_19, :mri_20 do - gem 'debugger' - end + # platforms :mri_19, :mri_20 do + # gem 'debugger' + # end gem 'benchmark-ips' end diff --git a/RAILS_VERSION b/RAILS_VERSION index 14b6456d31..78dae579e8 100644 --- a/RAILS_VERSION +++ b/RAILS_VERSION @@ -1 +1 @@ -4.1.0.beta +4.1.0.beta1 diff --git a/RELEASING_RAILS.rdoc b/RELEASING_RAILS.rdoc index aafbd25486..664505f60d 100644 --- a/RELEASING_RAILS.rdoc +++ b/RELEASING_RAILS.rdoc @@ -110,7 +110,7 @@ what to do in case anything goes wrong: $ rake all:build $ git commit -am'updating RAILS_VERSION' - $ git tag -m'tagging rc release' v3.0.10.rc1 + $ git tag -m 'v3.0.10.rc1 release' v3.0.10.rc1 $ git push $ git push --tags $ for i in $(ls pkg); do gem push $i; done diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index 857cde399a..fc9aefd416 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -1,3 +1,7 @@ +* Add mailer previews feature based on 37 Signals mail_view gem + + *Andrew White* + * Calling `mail()` without arguments serves as getter for the current mail message and keeps previously set headers. diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb index 5b6960c8fc..557eec4728 100644 --- a/actionmailer/lib/action_mailer.rb +++ b/actionmailer/lib/action_mailer.rb @@ -41,6 +41,8 @@ module ActionMailer autoload :Base autoload :DeliveryMethods autoload :MailHelper + autoload :Preview + autoload :Previews, 'action_mailer/preview' autoload :TestCase autoload :TestHelper end diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 5723b2cc1b..3c21db991c 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -308,6 +308,28 @@ module ActionMailer # Note that unless you have a specific reason to do so, you should prefer using before_action # rather than after_action in your ActionMailer classes so that headers are parsed properly. # + # = Previewing emails + # + # You can preview your email templates visually by adding a mailer preview file to the + # <tt>ActionMailer::Base.preview_path</tt>. Since most emails do something interesting + # with database data, you'll need to write some scenarios to load messages with fake data: + # + # class NotifierPreview < ActionMailer::Preview + # def welcome + # Notifier.welcome(User.first) + # end + # end + # + # Methods must return a Mail::Message object which can be generated by calling the mailer + # method without the additional <tt>deliver</tt>. The location of the mailer previews + # directory can be configured using the <tt>preview_path</tt> option which has a default + # of <tt>test/mailers/previews</tt>: + # + # config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews" + # + # An overview of all previews is accessible at <tt>http://localhost:3000/rails/mailers</tt> + # on a running development server instance. + # # = Configuration options # # These options are specified on the class level, like @@ -362,6 +384,7 @@ module ActionMailer # <tt>delivery_method :test</tt>. Most useful for unit and functional testing. class Base < AbstractController::Base include DeliveryMethods + include Previews abstract! diff --git a/actionmailer/lib/action_mailer/preview.rb b/actionmailer/lib/action_mailer/preview.rb new file mode 100644 index 0000000000..43d9ec4bb5 --- /dev/null +++ b/actionmailer/lib/action_mailer/preview.rb @@ -0,0 +1,67 @@ +require 'active_support/descendants_tracker' + +module ActionMailer + module Previews #:nodoc: + extend ActiveSupport::Concern + + included do + # Set the location of mailer previews through app configuration: + # + # config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews" + # + class_attribute :preview_path, instance_writer: false + end + end + + class Preview + extend ActiveSupport::DescendantsTracker + + class << self + # Returns all mailer preview classes + def all + load_previews if descendants.empty? + descendants + end + + # Returns the mail object for the given email name + def call(email) + preview = self.new + preview.public_send(email) + end + + # Returns all of the available email previews + def emails + public_instance_methods(false).map(&:to_s).sort + end + + # Returns true if the email exists + def email_exists?(email) + emails.include?(email) + end + + # Returns true if the preview exists + def exists?(preview) + all.any?{ |p| p.preview_name == preview } + end + + # Find a mailer preview by its underscored class name + def find(preview) + all.find{ |p| p.preview_name == preview } + end + + # Returns the underscored name of the mailer preview without the suffix + def preview_name + name.sub(/Preview$/, '').underscore + end + + protected + def load_previews #:nodoc: + Dir["#{preview_path}/**/*_preview.rb"].each{ |file| require_dependency file } + end + + def preview_path #:nodoc: + Base.preview_path + end + end + end +end diff --git a/actionmailer/lib/action_mailer/railtie.rb b/actionmailer/lib/action_mailer/railtie.rb index 7677ff3a7c..af8009ba97 100644 --- a/actionmailer/lib/action_mailer/railtie.rb +++ b/actionmailer/lib/action_mailer/railtie.rb @@ -40,5 +40,13 @@ module ActionMailer config.compile_methods! if config.respond_to?(:compile_methods!) end end + + initializer "action_mailer.configure_mailer_previews", before: :set_autoload_paths do |app| + if Rails.env.development? + options = app.config.action_mailer + options.preview_path ||= defined?(Rails.root) ? "#{Rails.root}/test/mailers/previews" : nil + app.config.autoload_paths << options.preview_path + end + end end end diff --git a/actionmailer/lib/action_mailer/version.rb b/actionmailer/lib/action_mailer/version.rb index 9d00091972..46eb763c26 100644 --- a/actionmailer/lib/action_mailer/version.rb +++ b/actionmailer/lib/action_mailer/version.rb @@ -1,7 +1,7 @@ module ActionMailer # Returns the version of the currently loaded ActionMailer as a Gem::Version def self.version - Gem::Version.new "4.1.0.beta" + Gem::Version.new "4.1.0.beta1" end module VERSION #:nodoc: diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb index aa18c512c7..cf790c7487 100644 --- a/actionmailer/test/abstract_unit.rb +++ b/actionmailer/test/abstract_unit.rb @@ -20,6 +20,9 @@ ActionMailer::Base.send(:include, ActionView::Layouts) # Show backtraces for deprecated behavior for quicker cleanup. ActiveSupport::Deprecation.debug = true +# Disable available locale checks to avoid warnings running the test suite. +I18n.enforce_available_locales = false + # Bogus template processors ActionView::Template.register_template_handler :haml, lambda { |template| "Look its HAML!".inspect } ActionView::Template.register_template_handler :bak, lambda { |template| "Lame backup".inspect } diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 3d507392b1..3e3df19a84 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,8 +1,39 @@ -* Fix generating a path for engine inside a resources block (#8533) +* `ActionController::Parameters#permit!` permits hashes in array values. + + *Xavier Noria* + +* Converts hashes in arrays of unfiltered params to unpermitted params. + + Fixes #13382 + + *Xavier Noria* + +* New config option to opt out of params "deep munging" that was used to + address security vulnerability CVE-2013-0155. In your app config: + + config.action_dispatch.perform_deep_munge = false + + Take care to understand the security risk involved before disabling this. + [Read more.](https://groups.google.com/forum/#!topic/rubyonrails-security/t1WFuuQyavI) + + *Bernard Potocki* + +* `rake routes` shows routes defined under assets prefix. + + *Ryunosuke SATO* + +* Extend cross-site request forgery (CSRF) protection to GET requests with + JavaScript responses, protecting apps from cross-origin `<script>` tags. + + *Jeremy Kemper* + +* Fix generating a path for engine inside a resources block. + + Fixes #8533. *Piotr Sarnacki* -* Add Mime::Type.register "text/vcard", :vcf to the default list of mime types +* Add `Mime::Type.register "text/vcard", :vcf` to the default list of mime types. *DHH* @@ -26,7 +57,7 @@ The request variant is a specialization of the request format, like `:tablet`, `:phone`, or `:desktop`. - You can set the variant in a before_action: + You can set the variant in a `before_action`: request.variant = :tablet if request.user_agent =~ /iPad/ @@ -45,16 +76,25 @@ app/views/projects/show.html+tablet.erb app/views/projects/show.html+phone.erb + You can also simplify the variants definition using the inline syntax: + + respond_to do |format| + format.js { render "trash" } + format.html.phone { redirect_to progress_path } + format.html.none { render "trash" } + end + *Łukasz Strzałkowski* -* Fix header `Content-Type: #<Mime::NullType:...>` in localized template. +* Fix render of localized templates without an explicit format using wrong + content header and not passing correct formats to template due to the + introduction of the `NullType` for mimes. - When localized template has no format in the template name, - the response now has the default and correct `content-type`. + Templates like `hello.it.erb` were subject to this issue. Fixes #13064. - *Angelo Capilleri* + *Angelo Capilleri*, *Carlos Antonio da Silva* * Try to escape each part of a url correctly when using a redirect route. @@ -85,9 +125,7 @@ * Add `session#fetch` method - fetch behaves similarly to [Hash#fetch](http://www.ruby-doc.org/core-1.9.3/Hash.html#method-i-fetch), - with the exception that the returned value is always saved into the session. - + fetch behaves like [Hash#fetch](http://www.ruby-doc.org/core-1.9.3/Hash.html#method-i-fetch). It returns a value from the hash for the given key. If the key can’t be found, there are several options: diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index 2580a78c7d..7be61d94c9 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -22,7 +22,7 @@ module AbstractController def render(*args, &block) options = _normalize_render(*args, &block) self.response_body = render_to_body(options) - _process_format(rendered_format) + _process_format(rendered_format) if rendered_format self.response_body end @@ -47,7 +47,7 @@ module AbstractController def render_to_body(options = {}) end - # Return Content-Type of rendered content + # Returns Content-Type of rendered content # :api: public def rendered_format Mime::TEXT diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index 424473801d..43407f5b78 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -1,6 +1,6 @@ module ActionController module Head - # Return a response that has no content (merely headers). The options + # Returns a response that has no content (merely headers). The options # argument is interpreted to be a hash of header names and values. # This allows you to easily return a response that consists only of # significant headers: diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb index d3aa8f90c5..b0e164bc57 100644 --- a/actionpack/lib/action_controller/metal/instrumentation.rb +++ b/actionpack/lib/action_controller/metal/instrumentation.rb @@ -67,7 +67,7 @@ module ActionController private - # A hook invoked everytime a before callback is halted. + # A hook invoked every time a before callback is halted. def halted_callback_hook(filter) ActiveSupport::Notifications.instrument("halted_callback.action_controller", :filter => filter) end diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index 0dd788645b..b7d1334dd3 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -48,7 +48,7 @@ module ActionController # the server will receive a +Last-Event-ID+ header with value equal to +id+. # # After setting an option in the constructor of the SSE object, all future - # SSEs sent accross the stream will use those options unless overridden. + # SSEs sent across the stream will use those options unless overridden. # # Example Usage: # diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index dedff9f476..fbc4024c2d 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -184,7 +184,7 @@ module ActionController #:nodoc: # Formats can have different variants. # # The request variant is a specialization of the request format, like <tt>:tablet</tt>, - # <tt>:phone</tt>, or <tt>:desktop<tt>. + # <tt>:phone</tt>, or <tt>:desktop</tt>. # # We often want to render different html/json/xml templates for phones, # tablets, and desktop browsers. Variants make it easy. @@ -209,6 +209,15 @@ module ActionController #:nodoc: # app/views/projects/show.html+tablet.erb # app/views/projects/show.html+phone.erb # + # When you're not sharing any code within the format, you can simplify defining variants + # using the inline syntax: + # + # respond_to do |format| + # format.js { render "trash" } + # format.html.phone { redirect_to progress_path } + # format.html.none { render "trash" } + # end + # # Be sure to check the documentation of +respond_with+ and # <tt>ActionController::MimeResponds.respond_to</tt> for more examples. def respond_to(*mimes, &block) diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index ab14a61b97..2812038938 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -58,7 +58,7 @@ module ActionController # redirect_to post_url(@post), alert: "Watch it, mister!" # redirect_to post_url(@post), status: :found, notice: "Pay attention to the road" # redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id } - # redirect_to { action: 'atom' }, alert: "Something serious happened" + # redirect_to({ action: 'atom' }, alert: "Something serious happened") # # When using <tt>redirect_to :back</tt>, if there is no referrer, ActionController::RedirectBackError will be raised. You may specify some fallback # behavior for this case by rescuing ActionController::RedirectBackError. diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index 66d34f3b67..5c48b4ab98 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -34,8 +34,7 @@ module ActionController def _process_format(format) super - # format is a Mime::NullType instance here then this condition can't be changed to `if format` - self.content_type ||= format.to_s unless format.nil? + self.content_type ||= format.to_s end # Normalize arguments by catching blocks and setting them on :update. diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index bd64b1f812..c88074d4c6 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -5,14 +5,24 @@ module ActionController #:nodoc: class InvalidAuthenticityToken < ActionControllerError #:nodoc: end + class InvalidCrossOriginRequest < ActionControllerError #:nodoc: + end + # Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks # by including a token in the rendered html for your application. This token is # stored as a random string in the session, to which an attacker does not have # access. When a request reaches your application, \Rails verifies the received # token with the token in the session. Only HTML and JavaScript requests are checked, # so this will not protect your XML API (presumably you'll have a different - # authentication scheme there anyway). Also, GET requests are not protected as these - # should be idempotent. + # authentication scheme there anyway). + # + # GET requests are not protected since they don't have side effects like writing + # to the database and don't leak sensitive information. JavaScript requests are + # an exception: a third-party site can use a <script> tag to reference a JavaScript + # URL on your site. When your JavaScript response loads on their site, it executes. + # With carefully crafted JavaScript on their end, sensitive data in your JavaScript + # response may be extracted. To prevent this, only XmlHttpRequest (known as XHR or + # Ajax) requests are allowed to make GET requests for JavaScript responses. # # It's important to remember that XML or JSON requests are also affected and if # you're building an API you'll need something like: @@ -65,17 +75,16 @@ module ActionController #:nodoc: module ClassMethods # Turn on request forgery protection. Bear in mind that only non-GET, HTML/JavaScript requests are checked. # + # class ApplicationController < ActionController::Base + # protect_from_forgery + # end + # # class FooController < ApplicationController # protect_from_forgery except: :index # - # You can disable csrf protection on controller-by-controller basis: - # + # You can disable CSRF protection on controller by skipping the verification before_action: # skip_before_action :verify_authenticity_token # - # It can also be disabled for specific controller actions: - # - # skip_before_action :verify_authenticity_token, except: [:create] - # # Valid Options: # # * <tt>:only/:except</tt> - Passed to the <tt>before_action</tt> call. Set which actions are verified. @@ -89,6 +98,7 @@ module ActionController #:nodoc: self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session) self.request_forgery_protection_token ||= :authenticity_token prepend_before_action :verify_authenticity_token, options + append_after_action :verify_same_origin_request end private @@ -169,18 +179,61 @@ module ActionController #:nodoc: end protected + # The actual before_action that is used to verify the CSRF token. + # Don't override this directly. Provide your own forgery protection + # strategy instead. If you override, you'll disable same-origin + # `<script>` verification. + # + # Lean on the protect_from_forgery declaration to mark which actions are + # due for same-origin request verification. If protect_from_forgery is + # enabled on an action, this before_action flags its after_action to + # verify that JavaScript responses are for XHR requests, ensuring they + # follow the browser's same-origin policy. + def verify_authenticity_token + mark_for_same_origin_verification! + + if !verified_request? + logger.warn "Can't verify CSRF token authenticity" if logger + handle_unverified_request + end + end + def handle_unverified_request forgery_protection_strategy.new(self).handle_unverified_request end - # The actual before_action that is used. Modify this to change how you handle unverified requests. - def verify_authenticity_token - unless verified_request? - logger.warn "Can't verify CSRF token authenticity" if logger - handle_unverified_request + CROSS_ORIGIN_JAVASCRIPT_WARNING = "Security warning: an embedded " \ + "<script> tag on another site requested protected JavaScript. " \ + "If you know what you're doing, go ahead and disable forgery " \ + "protection on this action to permit cross-origin JavaScript embedding." + private_constant :CROSS_ORIGIN_JAVASCRIPT_WARNING + + # If `verify_authenticity_token` was run (indicating that we have + # forgery protection enabled for this request) then also verify that + # we aren't serving an unauthorized cross-origin response. + def verify_same_origin_request + if marked_for_same_origin_verification? && non_xhr_javascript_response? + logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING if logger + raise ActionController::InvalidCrossOriginRequest, CROSS_ORIGIN_JAVASCRIPT_WARNING end end + # GET requests are checked for cross-origin JavaScript after rendering. + def mark_for_same_origin_verification! + @marked_for_same_origin_verification = request.get? + end + + # If the `verify_authenticity_token` before_action ran, verify that + # JavaScript responses are only served to same-origin GET requests. + def marked_for_same_origin_verification? + @marked_for_same_origin_verification ||= false + end + + # Check for cross-origin JavaScript responses. + def non_xhr_javascript_response? + content_type =~ %r(\Atext/javascript) && !request.xhr? + end + # Returns true or false if a request is verified. Checks: # # * is it a GET or HEAD request? Gets should be safe and idempotent diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb index b4ba169e8f..e24b56fa91 100644 --- a/actionpack/lib/action_controller/metal/responder.rb +++ b/actionpack/lib/action_controller/metal/responder.rb @@ -270,7 +270,7 @@ module ActionController #:nodoc: resource.respond_to?(:errors) && !resource.errors.empty? end - # Check whether the neceessary Renderer is available + # Check whether the necessary Renderer is available def has_renderer? Renderers::RENDERERS.include?(format) end diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index b4948d99a8..48a916f2b1 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -3,6 +3,7 @@ require 'active_support/core_ext/array/wrap' require 'active_support/rescuable' require 'action_dispatch/http/upload' require 'stringio' +require 'set' module ActionController # Raised when a required parameter is missing. @@ -125,6 +126,13 @@ module ActionController @permitted = self.class.permit_all_parameters end + # Attribute that keeps track of converted arrays, if any, to avoid double + # looping in the common use case permit + mass-assignment. Defined in a + # method to instantiate it only if needed. + def converted_arrays + @converted_arrays ||= Set.new + end + # Returns +true+ if the parameter is permitted, +false+ otherwise. # # params = ActionController::Parameters.new @@ -149,8 +157,10 @@ module ActionController # Person.new(params) # => #<Person id: nil, name: "Francesco"> def permit! each_pair do |key, value| - convert_hashes_to_parameters(key, value) - self[key].permit! if self[key].respond_to? :permit! + value = convert_hashes_to_parameters(key, value) + Array.wrap(value).each do |_| + _.permit! if _.respond_to? :permit! + end end @permitted = true @@ -284,14 +294,7 @@ module ActionController # params.fetch(:none, 'Francesco') # => "Francesco" # params.fetch(:none) { 'Francesco' } # => "Francesco" def fetch(key, *args) - value = super - # Don't rely on +convert_hashes_to_parameters+ - # so as to not mutate via a +fetch+ - if value.is_a?(Hash) - value = self.class.new(value) - value.permit! if permitted? - end - value + convert_hashes_to_parameters(key, super, false) rescue KeyError raise ActionController::ParameterMissing.new(key) end @@ -329,12 +332,21 @@ module ActionController end private - def convert_hashes_to_parameters(key, value) - if value.is_a?(Parameters) || !value.is_a?(Hash) + def convert_hashes_to_parameters(key, value, assign_if_converted=true) + converted = convert_value_to_parameters(value) + self[key] = converted if assign_if_converted && !converted.equal?(value) + converted + end + + def convert_value_to_parameters(value) + if value.is_a?(Array) && !converted_arrays.member?(value) + converted = value.map { |_| convert_value_to_parameters(_) } + converted_arrays << converted + converted + elsif value.is_a?(Parameters) || !value.is_a?(Hash) value else - # Convert to Parameters on first access - self[key] = self.class.new(value) + self.class.new(value) end end diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index 0833e65d23..a2fc814221 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -3,6 +3,7 @@ require "action_controller" require "action_dispatch/railtie" require "abstract_controller/railties/routes_helpers" require "action_controller/railties/helpers" +require "action_view/railtie" module ActionController class Railtie < Rails::Railtie #:nodoc: diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index 346598b6de..c33ba201e1 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -50,7 +50,7 @@ module ActionDispatch # GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first # def format(view_path = []) - formats.first + formats.first || Mime::NullType.instance end def formats diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index 2a8ff0a5d2..3d2dd2d632 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -28,7 +28,7 @@ module Mime class << self def [](type) return type if type.is_a?(Type) - Type.lookup_by_extension(type) || NullType.instance + Type.lookup_by_extension(type) end def fetch(type) diff --git a/actionpack/lib/action_dispatch/journey/parser.rb b/actionpack/lib/action_dispatch/journey/parser.rb index bb4cbb00e2..430812fafe 100644 --- a/actionpack/lib/action_dispatch/journey/parser.rb +++ b/actionpack/lib/action_dispatch/journey/parser.rb @@ -1,7 +1,7 @@ # # DO NOT MODIFY!!!! # This file is automatically generated by Racc 1.4.9 -# from Racc grammer file "". +# from Racc grammar file "". # require 'racc/parser.rb' diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 3ccd0c9ee8..fe110d7938 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -30,7 +30,7 @@ module ActionDispatch # cookies[:login] = { value: "XJ-122", expires: 1.hour.from_now } # # # Sets a signed cookie, which prevents users from tampering with its value. - # # The cookie is signed by your app's <tt>config.secret_key_base</tt> value. + # # The cookie is signed by your app's <tt>secrets.secret_key_base</tt> value. # # It can be read using the signed method <tt>cookies.signed[:name]</tt> # cookies.signed[:user_id] = current_user.id # @@ -117,10 +117,10 @@ module ActionDispatch # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed # cookie was tampered with by the user (or a 3rd party), nil will be returned. # - # If +config.secret_key_base+ and +config.secret_token+ (deprecated) are both set, + # If +secrets.secret_key_base+ and +config.secret_token+ (deprecated) are both set, # legacy cookies signed with the old key generator will be transparently upgraded. # - # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+. + # This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+. # # Example: # @@ -140,10 +140,10 @@ module ActionDispatch # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read. # If the cookie was tampered with by the user (or a 3rd party), nil will be returned. # - # If +config.secret_key_base+ and +config.secret_token+ (deprecated) are both set, + # If +secrets.secret_key_base+ and +config.secret_token+ (deprecated) are both set, # legacy cookies signed with the old key generator will be transparently upgraded. # - # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+. + # This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+. # # Example: # @@ -409,7 +409,7 @@ module ActionDispatch end # UpgradeLegacySignedCookieJar is used instead of SignedCookieJar if - # config.secret_token and config.secret_key_base are both set. It reads + # config.secret_token and secrets.secret_key_base are both set. It reads # legacy cookies signed with the old dummy key generator and re-saves # them using the new key generator to provide a smooth upgrade path. class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc: @@ -427,7 +427,7 @@ module ActionDispatch def initialize(parent_jar, key_generator, options = {}) if ActiveSupport::LegacyKeyGenerator === key_generator - raise "You didn't set config.secret_key_base, which is required for this cookie jar. " + + raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " + "Read the upgrade documentation to learn more about this new config option." end @@ -465,7 +465,7 @@ module ActionDispatch end # UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore - # instead of EncryptedCookieJar if config.secret_token and config.secret_key_base + # instead of EncryptedCookieJar if config.secret_token and secrets.secret_key_base # are both set. It reads legacy cookies signed with the old dummy key generator and # encrypts and re-saves them using the new key generator to provide a smooth upgrade path. class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc: diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index 11b42ee5be..1ebc189c28 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -31,9 +31,10 @@ module ActionDispatch # # Myapp::Application.config.session_store :cookie_store, key: '_your_app_session' # - # Configure your secret key in config/initializers/secret_token.rb: + # Configure your secret key in config/secrets.yml: # - # Myapp::Application.config.secret_key_base 'secret key' + # development: + # secret_key_base: 'secret key' # # To generate a secret key for an existing application, run `rake secret`. # diff --git a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb index 95461fa693..323873ba4b 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb @@ -89,8 +89,8 @@ } // takes an array of elements with a data-regexp attribute and - // passes their their parent <tr> into the callback function - // if the regexp matchs a given path + // passes their parent <tr> into the callback function + // if the regexp matches a given path function eachElemsForPath(elems, path, func) { each(elems, function(e){ var reg = e.getAttribute("data-regexp"); diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index 2dfaab3587..ddeea24bb3 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -16,6 +16,7 @@ module ActionDispatch config.action_dispatch.signed_cookie_salt = 'signed cookie' config.action_dispatch.encrypted_cookie_salt = 'encrypted cookie' config.action_dispatch.encrypted_signed_cookie_salt = 'signed encrypted cookie' + config.action_dispatch.perform_deep_munge = true config.action_dispatch.default_headers = { 'X-Frame-Options' => 'SAMEORIGIN', @@ -28,6 +29,7 @@ module ActionDispatch initializer "action_dispatch.configure" do |app| ActionDispatch::Http::URL.tld_length = app.config.action_dispatch.tld_length ActionDispatch::Request.ignore_accept_header = app.config.action_dispatch.ignore_accept_header + ActionDispatch::Request::Utils.perform_deep_munge = app.config.action_dispatch.perform_deep_munge ActionDispatch::Response.default_charset = app.config.action_dispatch.default_charset || app.config.encoding ActionDispatch::Response.default_headers = app.config.action_dispatch.default_headers diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb index 6d911a75f1..973627f106 100644 --- a/actionpack/lib/action_dispatch/request/session.rb +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -7,6 +7,9 @@ module ActionDispatch ENV_SESSION_KEY = Rack::Session::Abstract::ENV_SESSION_KEY # :nodoc: ENV_SESSION_OPTIONS_KEY = Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY # :nodoc: + # Singleton object used to determine if an optional param wasn't specified + Unspecified = Object.new + def self.create(store, env, default_options) session_was = find env session = Request::Session.new(store, env) @@ -127,15 +130,12 @@ module ActionDispatch @delegate.delete key.to_s end - def fetch(key, default=nil) - if self.key?(key) - self[key] - elsif default - self[key] = default - elsif block_given? - self[key] = yield(key) + def fetch(key, default=Unspecified, &block) + load_for_read! + if default == Unspecified + @delegate.fetch(key.to_s, &block) else - raise KeyError + @delegate.fetch(key.to_s, default, &block) end end diff --git a/actionpack/lib/action_dispatch/request/utils.rb b/actionpack/lib/action_dispatch/request/utils.rb index 8b43cdada8..a6dca9741c 100644 --- a/actionpack/lib/action_dispatch/request/utils.rb +++ b/actionpack/lib/action_dispatch/request/utils.rb @@ -1,9 +1,15 @@ module ActionDispatch class Request < Rack::Request class Utils # :nodoc: + + mattr_accessor :perform_deep_munge + self.perform_deep_munge = true + class << self # Remove nils from the params hash def deep_munge(hash) + return hash unless perform_deep_munge + hash.each do |k, v| case v when Array diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index 120bc54333..f612e91aef 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -69,7 +69,7 @@ module ActionDispatch end def internal? - controller.to_s =~ %r{\Arails/(info|welcome)} || path =~ %r{\A#{Rails.application.config.assets.prefix}} + controller.to_s =~ %r{\Arails/(info|mailers|welcome)} || path =~ %r{\A#{Rails.application.config.assets.prefix}\z} end def engine? diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index b8abdabca5..04faabef37 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -361,7 +361,7 @@ module ActionDispatch include UrlFor end - # Contains all the mounted helpers accross different + # Contains all the mounted helpers across different # engines and the `main_app` helper for the application. # You can include this in your classes if you want to # access routes for other engines. diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index 496682e8bd..f1f998d932 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -211,7 +211,7 @@ module ActionDispatch def fail_on(exception_class) yield rescue exception_class => e - raise MiniTest::Assertion, e.message + raise Minitest::Assertion, e.message end end end diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 9beb30307b..cc6b763093 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -137,7 +137,7 @@ module ActionDispatch class Session DEFAULT_HOST = "www.example.com" - include MiniTest::Assertions + include Minitest::Assertions include TestProcess, RequestHelpers, Assertions %w( status status_message headers body redirect? ).each do |method| @@ -242,7 +242,7 @@ module ActionDispatch @https = flag end - # Return +true+ if the session is mimicking a secure HTTPS request. + # Returns +true+ if the session is mimicking a secure HTTPS request. # # if session.https? # ... diff --git a/actionpack/lib/action_pack/version.rb b/actionpack/lib/action_pack/version.rb index fd08f392aa..a51f6a434a 100644 --- a/actionpack/lib/action_pack/version.rb +++ b/actionpack/lib/action_pack/version.rb @@ -1,7 +1,7 @@ module ActionPack # Returns the version of the currently loaded ActionPack as a Gem::Version def self.version - Gem::Version.new "4.1.0.beta" + Gem::Version.new "4.1.0.beta1" end module VERSION #:nodoc: diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 0e163a93eb..37e993b4e5 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -43,6 +43,9 @@ Thread.abort_on_exception = true # Show backtraces for deprecated behavior for quicker cleanup. ActiveSupport::Deprecation.debug = true +# Disable available locale checks to avoid warnings running the test suite. +I18n.enforce_available_locales = false + # Register danish language for testing I18n.backend.store_translations 'da', {} I18n.backend.store_translations 'pt-BR', {} @@ -331,7 +334,6 @@ class ThreadsController < ResourcesController; end class MessagesController < ResourcesController; end class CommentsController < ResourcesController; end class ReviewsController < ResourcesController; end -class AuthorsController < ResourcesController; end class LogosController < ResourcesController; end class AccountsController < ResourcesController; end @@ -342,8 +344,6 @@ class PreferencesController < ResourcesController; end module Backoffice class ProductsController < ResourcesController; end - class TagsController < ResourcesController; end - class ManufacturersController < ResourcesController; end class ImagesController < ResourcesController; end module Admin diff --git a/actionpack/test/assertions/response_assertions_test.rb b/actionpack/test/assertions/response_assertions_test.rb index 8eec98e916..5e64cae7e2 100644 --- a/actionpack/test/assertions/response_assertions_test.rb +++ b/actionpack/test/assertions/response_assertions_test.rb @@ -19,7 +19,7 @@ module ActionDispatch @response = FakeResponse.new sym assert_response sym - assert_raises(MiniTest::Assertion) { + assert_raises(Minitest::Assertion) { assert_response :unauthorized } end @@ -29,11 +29,11 @@ module ActionDispatch @response = FakeResponse.new 400 assert_response 400 - assert_raises(MiniTest::Assertion) { + assert_raises(Minitest::Assertion) { assert_response :unauthorized } - assert_raises(MiniTest::Assertion) { + assert_raises(Minitest::Assertion) { assert_response 500 } end @@ -42,11 +42,11 @@ module ActionDispatch @response = FakeResponse.new 401 assert_response :unauthorized - assert_raises(MiniTest::Assertion) { + assert_raises(Minitest::Assertion) { assert_response :ok } - assert_raises(MiniTest::Assertion) { + assert_raises(Minitest::Assertion) { assert_response :success } end diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb index ba4cffcd3e..b6b5a218cc 100644 --- a/actionpack/test/controller/action_pack_assertions_test.rb +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -444,22 +444,18 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase def test_assert_response_uses_exception_message @controller = AssertResponseWithUnexpectedErrorController.new - get :index + e = assert_raise RuntimeError, 'Expected non-success response' do + get :index + end assert_response :success - flunk 'Expected non-success response' - rescue RuntimeError => e - assert e.message.include?('FAIL') + assert_includes 'FAIL', e.message end def test_assert_response_failure_response_with_no_exception @controller = AssertResponseWithUnexpectedErrorController.new get :show - assert_response :success - flunk 'Expected non-success response' - rescue ActiveSupport::TestCase::Assertion - # success - rescue - flunk "assert_response failed to handle failure response with missing, but optional, exception." + assert_response 500 + assert_equal 'Boom', response.body end end diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb index 3b5d7ef446..d3efca5b6f 100644 --- a/actionpack/test/controller/filters_test.rb +++ b/actionpack/test/controller/filters_test.rb @@ -893,17 +893,6 @@ class ControllerWithFilterInstance < PostsController around_filter YieldingFilter.new, :only => :raises_after end -class ControllerWithFilterMethod < PostsController - class YieldingFilter < DefaultFilter - def around(controller) - yield - raise After - end - end - - around_filter YieldingFilter.new.method(:around), :only => :raises_after -end - class ControllerWithProcFilter < PostsController around_filter(:only => :no_raise) do |c,b| c.instance_variable_set(:"@before", true) diff --git a/actionpack/test/controller/http_digest_authentication_test.rb b/actionpack/test/controller/http_digest_authentication_test.rb index 9f1c168209..52a0bc9aa3 100644 --- a/actionpack/test/controller/http_digest_authentication_test.rb +++ b/actionpack/test/controller/http_digest_authentication_test.rb @@ -21,7 +21,7 @@ class HttpDigestAuthenticationTest < ActionController::TestCase def authenticate authenticate_or_request_with_http_digest("SuperSecret") do |username| - # Return the password + # Returns the password USERS[username] end end diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb index b60c5f058d..33a91d72d9 100644 --- a/actionpack/test/controller/parameters/parameters_permit_test.rb +++ b/actionpack/test/controller/parameters/parameters_permit_test.rb @@ -8,9 +8,16 @@ class ParametersPermitTest < ActiveSupport::TestCase end setup do - @params = ActionController::Parameters.new({ person: { - age: "32", name: { first: "David", last: "Heinemeier Hansson" } - }}) + @params = ActionController::Parameters.new( + person: { + age: '32', + name: { + first: 'David', + last: 'Heinemeier Hansson' + }, + addresses: [{city: 'Chicago', state: 'Illinois'}] + } + ) @struct_fields = [] %w(0 1 12).each do |number| @@ -153,6 +160,18 @@ class ParametersPermitTest < ActiveSupport::TestCase assert_equal nil, params[:foo] end + test 'hashes in array values get wrapped' do + params = ActionController::Parameters.new(foo: [{}, {}]) + params[:foo].each do |hash| + assert !hash.permitted? + end + end + + test 'arrays are converted at most once' do + params = ActionController::Parameters.new(foo: [{}]) + assert params[:foo].equal?(params[:foo]) + end + test "fetch doesnt raise ParameterMissing exception if there is a default" do assert_equal "monkey", @params.fetch(:foo, "monkey") assert_equal "monkey", @params.fetch(:foo) { "monkey" } @@ -221,6 +240,7 @@ class ParametersPermitTest < ActiveSupport::TestCase assert @params.permitted? assert @params[:person].permitted? assert @params[:person][:name].permitted? + assert @params[:person][:addresses][0].permitted? end test "permitted takes a default value when Parameters.permit_all_parameters is set" do diff --git a/actionpack/test/controller/render_js_test.rb b/actionpack/test/controller/render_js_test.rb index f070109b27..d550422a2f 100644 --- a/actionpack/test/controller/render_js_test.rb +++ b/actionpack/test/controller/render_js_test.rb @@ -22,7 +22,7 @@ class RenderJSTest < ActionController::TestCase tests TestController def test_render_vanilla_js - get :render_vanilla_js_hello + xhr :get, :render_vanilla_js_hello assert_equal "alert('hello')", @response.body assert_equal "text/javascript", @response.content_type end diff --git a/actionpack/test/controller/render_json_test.rb b/actionpack/test/controller/render_json_test.rb index 7c0a6bd67e..de8d1cbd9b 100644 --- a/actionpack/test/controller/render_json_test.rb +++ b/actionpack/test/controller/render_json_test.rb @@ -100,13 +100,13 @@ class RenderJsonTest < ActionController::TestCase end def test_render_json_with_callback - get :render_json_hello_world_with_callback + xhr :get, :render_json_hello_world_with_callback assert_equal 'alert({"hello":"world"})', @response.body assert_equal 'text/javascript', @response.content_type end def test_render_json_with_custom_content_type - get :render_json_with_custom_content_type + xhr :get, :render_json_with_custom_content_type assert_equal '{"hello":"world"}', @response.body assert_equal 'text/javascript', @response.content_type end diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index 727db79241..1f5fc06410 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -52,18 +52,36 @@ module RequestForgeryProtectionActions render :inline => "<%= form_for(:some_resource, :remote => true, :authenticity_token => 'external_token') {} %>" end + def same_origin_js + render js: 'foo();' + end + + def negotiate_same_origin + respond_to do |format| + format.js { same_origin_js } + end + end + + def cross_origin_js + same_origin_js + end + + def negotiate_cross_origin + negotiate_same_origin + end + def rescue_action(e) raise e end end # sample controllers class RequestForgeryProtectionControllerUsingResetSession < ActionController::Base include RequestForgeryProtectionActions - protect_from_forgery :only => %w(index meta), :with => :reset_session + protect_from_forgery :only => %w(index meta same_origin_js negotiate_same_origin), :with => :reset_session end class RequestForgeryProtectionControllerUsingException < ActionController::Base include RequestForgeryProtectionActions - protect_from_forgery :only => %w(index meta), :with => :exception + protect_from_forgery :only => %w(index meta same_origin_js negotiate_same_origin), :with => :exception end class RequestForgeryProtectionControllerUsingNullSession < ActionController::Base @@ -201,7 +219,7 @@ module RequestForgeryProtectionTests end def test_should_not_allow_post_without_token_irrespective_of_format - assert_blocked { post :index, :format=>'xml' } + assert_blocked { post :index, format: 'xml' } end def test_should_not_allow_patch_without_token @@ -271,6 +289,48 @@ module RequestForgeryProtectionTests end end + def test_should_only_allow_same_origin_js_get_with_xhr_header + assert_cross_origin_blocked { get :same_origin_js } + assert_cross_origin_blocked { get :same_origin_js, format: 'js' } + assert_cross_origin_blocked do + @request.accept = 'text/javascript' + get :negotiate_same_origin + end + + assert_cross_origin_not_blocked { xhr :get, :same_origin_js } + assert_cross_origin_not_blocked { xhr :get, :same_origin_js, format: 'js' } + assert_cross_origin_not_blocked do + @request.accept = 'text/javascript' + xhr :get, :negotiate_same_origin + end + end + + # Allow non-GET requests since GET is all a remote <script> tag can muster. + def test_should_allow_non_get_js_without_xhr_header + assert_cross_origin_not_blocked { post :same_origin_js, custom_authenticity_token: @token } + assert_cross_origin_not_blocked { post :same_origin_js, format: 'js', custom_authenticity_token: @token } + assert_cross_origin_not_blocked do + @request.accept = 'text/javascript' + post :negotiate_same_origin, custom_authenticity_token: @token + end + end + + def test_should_only_allow_cross_origin_js_get_without_xhr_header_if_protection_disabled + assert_cross_origin_not_blocked { get :cross_origin_js } + assert_cross_origin_not_blocked { get :cross_origin_js, format: 'js' } + assert_cross_origin_not_blocked do + @request.accept = 'text/javascript' + get :negotiate_cross_origin + end + + assert_cross_origin_not_blocked { xhr :get, :cross_origin_js } + assert_cross_origin_not_blocked { xhr :get, :cross_origin_js, format: 'js' } + assert_cross_origin_not_blocked do + @request.accept = 'text/javascript' + xhr :get, :negotiate_cross_origin + end + end + def assert_blocked session[:something_like_user_id] = 1 yield @@ -282,6 +342,16 @@ module RequestForgeryProtectionTests assert_nothing_raised { yield } assert_response :success end + + def assert_cross_origin_blocked + assert_raises(ActionController::InvalidCrossOriginRequest) do + yield + end + end + + def assert_cross_origin_not_blocked + assert_not_blocked { yield } + end end # OK let's get our test on @@ -305,13 +375,13 @@ class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController end end -class NullSessionDummyKeyGenerator - def generate_key(secret) - '03312270731a2ed0d11ed091c2338a06' +class RequestForgeryProtectionControllerUsingNullSessionTest < ActionController::TestCase + class NullSessionDummyKeyGenerator + def generate_key(secret) + '03312270731a2ed0d11ed091c2338a06' + end end -end -class RequestForgeryProtectionControllerUsingNullSessionTest < ActionController::TestCase def setup @request.env[ActionDispatch::Cookies::GENERATOR_KEY] = NullSessionDummyKeyGenerator.new end @@ -375,8 +445,8 @@ end class CustomAuthenticityParamControllerTest < ActionController::TestCase def setup - ActionController::Base.request_forgery_protection_token = :custom_token_name super + ActionController::Base.request_forgery_protection_token = :custom_token_name end def teardown diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 2c84e95c6e..df453a0251 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -1833,11 +1833,11 @@ class RackMountIntegrationTests < ActiveSupport::TestCase assert_equal({:controller => 'foo', :action => 'id_default', :id => 1 }, @routes.recognize_path('/id_default')) assert_equal({:controller => 'foo', :action => 'get_or_post'}, @routes.recognize_path('/get_or_post', :method => :get)) assert_equal({:controller => 'foo', :action => 'get_or_post'}, @routes.recognize_path('/get_or_post', :method => :post)) - assert_raise(ActionController::ActionControllerError) { @routes.recognize_path('/get_or_post', :method => :put) } - assert_raise(ActionController::ActionControllerError) { @routes.recognize_path('/get_or_post', :method => :delete) } + assert_raise(ActionController::RoutingError) { @routes.recognize_path('/get_or_post', :method => :put) } + assert_raise(ActionController::RoutingError) { @routes.recognize_path('/get_or_post', :method => :delete) } assert_equal({:controller => 'posts', :action => 'index', :optional => 'bar'}, @routes.recognize_path('/optional/bar')) - assert_raise(ActionController::ActionControllerError) { @routes.recognize_path('/optional') } + assert_raise(ActionController::RoutingError) { @routes.recognize_path('/optional') } assert_equal({:controller => 'posts', :action => 'show', :id => '1', :ws => true}, @routes.recognize_path('/ws/posts/show/1', :method => :get)) assert_equal({:controller => 'posts', :action => 'list', :ws => true}, @routes.recognize_path('/ws/posts/list', :method => :get)) @@ -1916,11 +1916,4 @@ class RackMountIntegrationTests < ActiveSupport::TestCase end extras end - - def assert_raise(e) - result = yield - flunk "Did not raise #{e}, but returned #{result.inspect}" - rescue e - assert true - end end diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb index 8ecc1c7d73..a4c84c29d7 100644 --- a/actionpack/test/controller/send_file_test.rb +++ b/actionpack/test/controller/send_file_test.rb @@ -144,7 +144,7 @@ class SendFileTest < ActionController::TestCase } @controller.headers = {} - assert !@controller.send(:send_file_headers!, options) + assert_raise(ArgumentError) { @controller.send(:send_file_headers!, options) } end def test_send_file_headers_guess_type_from_extension diff --git a/actionpack/test/dispatch/request/query_string_parsing_test.rb b/actionpack/test/dispatch/request/query_string_parsing_test.rb index 0ad0dbc958..d82493140f 100644 --- a/actionpack/test/dispatch/request/query_string_parsing_test.rb +++ b/actionpack/test/dispatch/request/query_string_parsing_test.rb @@ -104,6 +104,21 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest assert_parses({"action" => ['1']}, "action[]=1&action[]") end + test "perform_deep_munge" do + ActionDispatch::Request::Utils.perform_deep_munge = false + begin + assert_parses({"action" => nil}, "action") + assert_parses({"action" => {"foo" => nil}}, "action[foo]") + assert_parses({"action" => {"foo" => {"bar" => nil}}}, "action[foo][bar]") + assert_parses({"action" => {"foo" => {"bar" => [nil]}}}, "action[foo][bar][]") + assert_parses({"action" => {"foo" => [nil]}}, "action[foo][]") + assert_parses({"action" => {"foo" => [{"bar" => nil}]}}, "action[foo][][bar]") + assert_parses({"action" => ['1',nil]}, "action[]=1&action[]") + ensure + ActionDispatch::Request::Utils.perform_deep_munge = true + end + end + test "query string with empty key" do assert_parses( { "action" => "create_customer", "full_name" => "David Heinemeier Hansson" }, diff --git a/actionpack/test/dispatch/request/session_test.rb b/actionpack/test/dispatch/request/session_test.rb index a244d1364c..df55fcc8bc 100644 --- a/actionpack/test/dispatch/request/session_test.rb +++ b/actionpack/test/dispatch/request/session_test.rb @@ -68,13 +68,12 @@ module ActionDispatch assert_equal '1', session.fetch(:one) assert_equal '2', session.fetch(:two, '2') - assert_equal '2', session.fetch(:two) + assert_nil session.fetch(:two, nil) assert_equal 'three', session.fetch(:three) {|el| el.to_s } - assert_equal 'three', session.fetch(:three) assert_raise KeyError do - session.fetch(:four) + session.fetch(:three) end end diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index b308c5749a..f79fe47897 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -606,7 +606,7 @@ class RequestTest < ActiveSupport::TestCase 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest" request.expects(:parameters).at_least_once.returns({}) assert_equal [Mime::JS], request.formats - + request = stub_request 'CONTENT_TYPE' => 'application/xml; charset=UTF-8', 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest" request.expects(:parameters).at_least_once.returns({}) @@ -624,10 +624,10 @@ class RequestTest < ActiveSupport::TestCase test "format is not nil with unknown format" do request = stub_request request.expects(:parameters).at_least_once.returns({ format: :hello }) - assert_equal request.format.nil?, true - assert_equal request.format.html?, false - assert_equal request.format.xml?, false - assert_equal request.format.json?, false + assert request.format.nil? + assert_not request.format.html? + assert_not request.format.xml? + assert_not request.format.json? end test "formats with xhr request" do diff --git a/actionpack/test/dispatch/routing/inspector_test.rb b/actionpack/test/dispatch/routing/inspector_test.rb index c8038bbd7c..18a52f13a7 100644 --- a/actionpack/test/dispatch/routing/inspector_test.rb +++ b/actionpack/test/dispatch/routing/inspector_test.rb @@ -203,6 +203,18 @@ module ActionDispatch assert_no_match(/\/sprockets/, output.first) end + def test_rake_routes_shows_route_defined_in_under_assets_prefix + output = draw do + scope '/sprockets' do + get '/foo' => 'foo#bar' + end + end + assert_equal [ + "Prefix Verb URI Pattern Controller#Action", + " foo GET /sprockets/foo(.:format) foo#bar" + ], output + end + def test_redirect output = draw do get "/foo" => redirect("/foo/bar"), :constraints => { :subdomain => "admin" } diff --git a/actionpack/test/dispatch/url_generation_test.rb b/actionpack/test/dispatch/url_generation_test.rb index f919592d24..fdea27e2d2 100644 --- a/actionpack/test/dispatch/url_generation_test.rb +++ b/actionpack/test/dispatch/url_generation_test.rb @@ -66,7 +66,7 @@ module TestUrlGeneration assert_equal "http://www.example.com:8080/foo", foo_url(host: "www.example.com:8080", protocol: "http://") end - test "port option overides the host" do + test "port option overrides the host" do assert_equal "http://www.example.com:8080/foo", foo_url(host: "www.example.com:8443", protocol: "http://", port: 8080) end diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 0f38195514..b74b36c439 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,11 @@ +* The `video_tag` helper accepts a number as `:size` + + The `:size` option of the `video_tag` helper now can be specified + with a stringified number. The `width` and `height` attributes of + the generated tag will be the same. + + *Kuldeep Aggarwal* + * A Cycle object should accept an array and cycle through it as it would with a set of comma-separated objects. diff --git a/actionview/actionview.gemspec b/actionview/actionview.gemspec index 567e45e14f..e45dd04225 100644 --- a/actionview/actionview.gemspec +++ b/actionview/actionview.gemspec @@ -21,7 +21,7 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', version - s.add_dependency 'builder', '~> 3.1.0' + s.add_dependency 'builder', '~> 3.1' s.add_dependency 'erubis', '~> 2.7.0' s.add_development_dependency 'actionpack', version diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index 163d01c2eb..ea1aadcc43 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -207,14 +207,7 @@ module ActionView options[:alt] = options.fetch(:alt){ image_alt(src) } end - if size = options.delete(:size) - if size =~ %r{\A\d+x\d+\z} - options[:width], options[:height] = size.split('x') - elsif size =~ %r{\A\d+\z} - options[:width] = options[:height] = size - end - end - + options[:width], options[:height] = extract_dimensions(options.delete(:size)) if options[:size] tag("img", options) end @@ -251,9 +244,9 @@ module ActionView # # * <tt>:poster</tt> - Set an image (like a screenshot) to be shown # before the video loads. The path is calculated like the +src+ of +image_tag+. - # * <tt>:size</tt> - Supplied as "{Width}x{Height}", so "30x45" becomes - # width="30" and height="45". <tt>:size</tt> will be ignored if the - # value is not in the correct format. + # * <tt>:size</tt> - Supplied as "{Width}x{Height}" or "{Number}", so "30x45" becomes + # width="30" and height="45", and "50" becomes width="50" and height="50". + # <tt>:size</tt> will be ignored if the value is not in the correct format. # # ==== Examples # @@ -267,6 +260,8 @@ module ActionView # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/assets/screenshot.png" /> # video_tag("/trailers/hd.avi", size: "16x16") # # => <video src="/trailers/hd.avi" width="16" height="16" /> + # video_tag("/trailers/hd.avi", size: "16") + # # => <video height="16" src="/trailers/hd.avi" width="16" /> # video_tag("/trailers/hd.avi", height: '32', width: '32') # # => <video height="32" src="/trailers/hd.avi" width="32" /> # video_tag("trailer.ogg", "trailer.flv") @@ -278,10 +273,7 @@ module ActionView def video_tag(*sources) multiple_sources_tag('video', sources) do |options| options[:poster] = path_to_image(options[:poster]) if options[:poster] - - if size = options.delete(:size) - options[:width], options[:height] = size.split("x") if size =~ %r{^\d+x\d+$} - end + options[:width], options[:height] = extract_dimensions(options.delete(:size)) if options[:size] end end @@ -317,6 +309,14 @@ module ActionView content_tag(type, nil, options) end end + + def extract_dimensions(size) + if size =~ %r{\A\d+x\d+\z} + size.split('x') + elsif size =~ %r{\A\d+\z} + [size, size] + end + end end end end diff --git a/actionview/lib/action_view/renderer/streaming_template_renderer.rb b/actionview/lib/action_view/renderer/streaming_template_renderer.rb index 9cf6eb0c65..3ab2cd36fc 100644 --- a/actionview/lib/action_view/renderer/streaming_template_renderer.rb +++ b/actionview/lib/action_view/renderer/streaming_template_renderer.rb @@ -58,7 +58,7 @@ module ActionView def delayed_render(buffer, template, layout, view, locals) # Wrap the given buffer in the StreamingBuffer and pass it to the - # underlying template handler. Now, everytime something is concatenated + # underlying template handler. Now, every time something is concatenated # to the buffer, it is not appended to an array, but streamed straight # to the client. output = ActionView::StreamingBuffer.new(buffer) diff --git a/actionview/lib/action_view/vendor/html-scanner/html/node.rb b/actionview/lib/action_view/vendor/html-scanner/html/node.rb index 7e7cd4f7b6..27f0f2f6f8 100644 --- a/actionview/lib/action_view/vendor/html-scanner/html/node.rb +++ b/actionview/lib/action_view/vendor/html-scanner/html/node.rb @@ -71,12 +71,12 @@ module HTML #:nodoc: @line, @position = line, pos end - # Return a textual representation of the node. + # Returns a textual representation of the node. def to_s @children.join() end - # Return false (subclasses must override this to provide specific matching + # Returns false (subclasses must override this to provide specific matching # behavior.) +conditions+ may be of any type. def match(conditions) false diff --git a/actionview/lib/action_view/vendor/html-scanner/html/selector.rb b/actionview/lib/action_view/vendor/html-scanner/html/selector.rb index 7f8609c408..dfdd724b9b 100644 --- a/actionview/lib/action_view/vendor/html-scanner/html/selector.rb +++ b/actionview/lib/action_view/vendor/html-scanner/html/selector.rb @@ -488,7 +488,7 @@ module HTML end - # Return the next element after this one. Skips sibling text nodes. + # Returns the next element after this one. Skips sibling text nodes. # # With the +name+ argument, returns the next element with that name, # skipping other sibling elements. diff --git a/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb b/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb index 8ac8d34430..adf4e45930 100644 --- a/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb +++ b/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb @@ -30,7 +30,7 @@ module HTML #:nodoc: @current_line = 1 end - # Return the next token in the sequence, or +nil+ if there are no more tokens in + # Returns the next token in the sequence, or +nil+ if there are no more tokens in # the stream. def next return nil if @scanner.eos? diff --git a/actionview/lib/action_view/version.rb b/actionview/lib/action_view/version.rb index 094dd474df..edb6d8f116 100644 --- a/actionview/lib/action_view/version.rb +++ b/actionview/lib/action_view/version.rb @@ -1,7 +1,7 @@ module ActionView # Returns the version of the currently loaded ActionView as a Gem::Version def self.version - Gem::Version.new "4.1.0.beta" + Gem::Version.new "4.1.0.beta1" end module VERSION #:nodoc: diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb index eef0abb609..9928da4774 100644 --- a/actionview/test/abstract_unit.rb +++ b/actionview/test/abstract_unit.rb @@ -42,6 +42,9 @@ Thread.abort_on_exception = true # Show backtraces for deprecated behavior for quicker cleanup. ActiveSupport::Deprecation.debug = true +# Disable available locale checks to avoid warnings running the test suite. +I18n.enforce_available_locales = false + # Register danish language for testing I18n.backend.store_translations 'da', {} I18n.backend.store_translations 'pt-BR', {} diff --git a/actionview/test/actionpack/abstract/helper_test.rb b/actionview/test/actionpack/abstract/helper_test.rb index 89c4567715..7d346e917d 100644 --- a/actionview/test/actionpack/abstract/helper_test.rb +++ b/actionview/test/actionpack/abstract/helper_test.rb @@ -78,9 +78,9 @@ module AbstractController end def test_declare_missing_helper - AbstractHelpers.helper :missing - flunk "should have raised an exception" - rescue LoadError => e + e = assert_raise AbstractController::Helpers::MissingHelperError do + AbstractHelpers.helper :missing + end assert_equal "helpers/missing_helper.rb", e.path end diff --git a/actionview/test/actionpack/controller/layout_test.rb b/actionview/test/actionpack/controller/layout_test.rb index 5dd23c4b31..b44f57a23e 100644 --- a/actionview/test/actionpack/controller/layout_test.rb +++ b/actionview/test/actionpack/controller/layout_test.rb @@ -215,12 +215,6 @@ class LayoutSetInResponseTest < ActionController::TestCase end end -class RenderWithTemplateOptionController < LayoutTest - def hello - render :template => 'alt/hello' - end -end - class SetsNonExistentLayoutFile < LayoutTest layout "nofile" end diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb index 8c99504050..45b8049b83 100644 --- a/actionview/test/actionpack/controller/render_test.rb +++ b/actionview/test/actionpack/controller/render_test.rb @@ -44,8 +44,6 @@ module Quiz end end - class Store < Question; end - # Controller class QuestionsController < ApplicationController def new @@ -693,9 +691,9 @@ class RenderTest < ActionController::TestCase end def test_line_offset - get :render_line_offset - flunk "the action should have raised an exception" - rescue StandardError => exc + exc = assert_raises ActionView::Template::Error do + get :render_line_offset + end line = exc.backtrace.first assert(line =~ %r{:(\d+):}) assert_equal "1", $1, @@ -971,7 +969,7 @@ class RenderTest < ActionController::TestCase end def test_should_implicitly_render_js_template_without_layout - get :render_implicit_js_template_without_layout, :format => :js + xhr :get, :render_implicit_js_template_without_layout, :format => :js assert_no_match %r{<html>}, @response.body end diff --git a/actionview/test/fixtures/developer.rb b/actionview/test/fixtures/developer.rb index 4941463015..8b3f0a8039 100644 --- a/actionview/test/fixtures/developer.rb +++ b/actionview/test/fixtures/developer.rb @@ -4,7 +4,3 @@ class Developer < ActiveRecord::Base has_many :topics, :through => :replies accepts_nested_attributes_for :projects end - -class DeVeLoPeR < ActiveRecord::Base - self.table_name = "developers" -end diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb index 214a13654e..3ca71d3376 100644 --- a/actionview/test/template/asset_tag_helper_test.rb +++ b/actionview/test/template/asset_tag_helper_test.rb @@ -2,14 +2,6 @@ require 'zlib' require 'abstract_unit' require 'active_support/ordered_options' -class FakeController - attr_accessor :request - - def config - @config ||= ActiveSupport::InheritableOptions.new(ActionController::Base.config) - end -end - class AssetTagHelperTest < ActionView::TestCase tests ActionView::Helpers::AssetTagHelper @@ -245,7 +237,7 @@ class AssetTagHelperTest < ActionView::TestCase %(video_tag("gold.m4v", :size => "160x120")) => %(<video height="120" src="/videos/gold.m4v" width="160"></video>), %(video_tag("gold.m4v", "size" => "320x240")) => %(<video height="240" src="/videos/gold.m4v" width="320"></video>), %(video_tag("trailer.ogg", :poster => "screenshot.png")) => %(<video poster="/images/screenshot.png" src="/videos/trailer.ogg"></video>), - %(video_tag("error.avi", "size" => "100")) => %(<video src="/videos/error.avi"></video>), + %(video_tag("error.avi", "size" => "100")) => %(<video height="100" src="/videos/error.avi" width="100"></video>), %(video_tag("error.avi", "size" => "100 x 100")) => %(<video src="/videos/error.avi"></video>), %(video_tag("error.avi", "size" => "x")) => %(<video src="/videos/error.avi"></video>), %(video_tag("http://media.rubyonrails.org/video/rails_blog_2.mov")) => %(<video src="http://media.rubyonrails.org/video/rails_blog_2.mov"></video>), diff --git a/actionview/test/template/form_collections_helper_test.rb b/actionview/test/template/form_collections_helper_test.rb index 68c83f2059..7a62b9d907 100644 --- a/actionview/test/template/form_collections_helper_test.rb +++ b/actionview/test/template/form_collections_helper_test.rb @@ -60,7 +60,7 @@ class FormCollectionsHelperTest < ActionView::TestCase assert_no_select 'input[type=radio][value=other][disabled=disabled]' end - test 'collection radio accepts single disable item' do + test 'collection radio accepts single disabled item' do collection = [[1, true], [0, false]] with_collection_radio_buttons :user, :active, collection, :last, :first, :disabled => true @@ -300,7 +300,7 @@ class FormCollectionsHelperTest < ActionView::TestCase assert_no_select 'input[type=checkbox][value=2][disabled=disabled]' end - test 'collection check boxes accepts single disable item' do + test 'collection check boxes accepts single disabled item' do collection = (1..3).map{|i| [i, "Category #{i}"] } with_collection_check_boxes :user, :category_ids, collection, :first, :last, :disabled => 1 diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index 11e755649c..36e565f692 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -20,5 +20,5 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', version - s.add_dependency 'builder', '~> 3.1.0' + s.add_dependency 'builder', '~> 3.1' end diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 8d6d405e96..391442afa7 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -14,7 +14,7 @@ module ActiveModel class MissingAttributeError < NoMethodError end - # == Active \Model Attribute Methods + # == Active Model Attribute Methods # # <tt>ActiveModel::AttributeMethods</tt> provides a way to add prefixes and # suffixes to your methods as well as handling the creation of diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index 377aa6ee27..b27a39b787 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -30,7 +30,7 @@ module ActiveModel # end # # Then in your class, you can use the +before_create+, +after_create+ and - # +around_create+ methods, just as you would in an Active Record module. + # +around_create+ methods, just as you would in an Active Record model. # # before_create :action_before_create # diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb index 21e4eb3c86..1856c852a2 100644 --- a/activemodel/lib/active_model/conversion.rb +++ b/activemodel/lib/active_model/conversion.rb @@ -41,7 +41,7 @@ module ActiveModel end # Returns an Enumerable of all key attributes if any is set, regardless if - # the object is persisted or not. If there no key attributes, returns +nil+. + # the object is persisted or not. Returns +nil+ if there are no key attributes. # # class Person < ActiveRecord::Base # end diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index c5f1b3f11a..8ccb25c613 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -149,12 +149,12 @@ module ActiveModel end # Handle <tt>*_changed?</tt> for +method_missing+. - def attribute_changed?(attr) + def attribute_changed?(attr) # :nodoc: changed_attributes.include?(attr) end # Handle <tt>*_was</tt> for +method_missing+. - def attribute_was(attr) + def attribute_was(attr) # :nodoc: attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr) end diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index 690856aee1..bddacc8c45 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -61,7 +61,7 @@ module ActiveModel # end # # Note that the validator is initialized only once for the whole application - # lifecycle, and not on each validation run. + # life cycle, and not on each validation run. # # The easiest way to add custom validators for validating individual attributes # is with the convenient <tt>ActiveModel::EachValidator</tt>. @@ -83,7 +83,7 @@ module ActiveModel # end # # It can be useful to access the class that is using that validator when there are prerequisites such - # as an +attr_accessor+ being present. This class is accessable via +options[:class]+ in the constructor. + # as an +attr_accessor+ being present. This class is accessible via +options[:class]+ in the constructor. # To setup your validator override the constructor. # # class MyValidator < ActiveModel::Validator @@ -109,7 +109,7 @@ module ActiveModel deprecated_setup(options) end - # Return the kind for this validator. + # Returns the kind for this validator. # # PresenceValidator.new.kind # => :presence # UniquenessValidator.new.kind # => :uniqueness diff --git a/activemodel/lib/active_model/version.rb b/activemodel/lib/active_model/version.rb index 86340bba37..58ba3ab9b2 100644 --- a/activemodel/lib/active_model/version.rb +++ b/activemodel/lib/active_model/version.rb @@ -1,7 +1,7 @@ module ActiveModel # Returns the version of the currently loaded ActiveModel as a Gem::Version def self.version - Gem::Version.new "4.1.0.beta" + Gem::Version.new "4.1.0.beta1" end module VERSION #:nodoc: diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb index 7a63674757..522a7cebb4 100644 --- a/activemodel/test/cases/helper.rb +++ b/activemodel/test/cases/helper.rb @@ -7,4 +7,7 @@ require 'active_support/core_ext/string/access' # Show backtraces for deprecated behavior for quicker cleanup. ActiveSupport::Deprecation.debug = true +# Disable available locale checks to avoid warnings running the test suite. +I18n.enforce_available_locales = false + require 'active_support/testing/autorun' diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb index 41d0b2263e..0314803af6 100644 --- a/activemodel/test/cases/secure_password_test.rb +++ b/activemodel/test/cases/secure_password_test.rb @@ -2,7 +2,6 @@ require 'cases/helper' require 'models/user' require 'models/oauthed_user' require 'models/visitor' -require 'models/administrator' class SecurePasswordTest < ActiveModel::TestCase setup do diff --git a/activemodel/test/models/administrator.rb b/activemodel/test/models/administrator.rb deleted file mode 100644 index 2f3aff290c..0000000000 --- a/activemodel/test/models/administrator.rb +++ /dev/null @@ -1,11 +0,0 @@ -class Administrator - extend ActiveModel::Callbacks - include ActiveModel::Validations - include ActiveModel::SecurePassword - - define_model_callbacks :create - - attr_accessor :name, :password_digest - - has_secure_password -end diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index af1d192daa..6b17d6b265 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,16 +1,208 @@ +* Deprecated use of string argument as a configuration lookup in `ActiveRecord::Base.establish_connection`. Instead, a symbol must be given. + + *José Valim* + +* Fixed `update_column`, `update_columns`, and `update_all` to correctly serialize + values for `array`, `hstore` and `json` column types in PostgreSQL. + + Fixes #12261. + + *Tadas Tamosauskas*, *Carlos Antonio da Silva* + +* Do not consider PostgreSQL array columns as number or text columns. + + The code uses these checks in several places to know what to do with a + particular column, for instance AR attribute query methods has a branch + like this: + + if column.number? + !value.zero? + end + + This should never be true for array columns, since it would be the same + as running [].zero?, which results in a NoMethodError exception. + + Fixing this by ensuring that array columns in PostgreSQL never return + true for number?/text? checks. + + *Carlos Antonio da Silva* + +* When connecting to a non-existant database, the error: + `ActiveRecord::NoDatabaseError` will now be raised. When being used with Rails + the error message will include information on how to create a database: + `rake db:create`. Supported adapters: postgresql, mysql, mysql2, sqlite3 + + *Richard Schneeman* + +* Do not raise `'can not touch on a new record object'` exception on destroying already destroyed + `belongs_to` association with `touch: true` option + + Fixes: #13445 + + Example: + + # Given Comment has belongs_to :post, touch: true + comment.post.destroy + comment.destroy # no longer raises an error + + *Paul Nikitochkin* + +* Fix a bug when assigning an array containing string numbers to a + PostgreSQL integer array column. + + Fixes #13444. + + Example: + + # Given Book#ratings is of type :integer, array: true + Book.new(ratings: [1, 2]) # worked before + Book.new(ratings: ['1', '2']) # now works as well + + *Damien Mathieu* + +* Improve the default select when `from` is used. + + Previously, if you did something like Topic.from(:temp_topics), it + would generate SQL like: + + SELECT topics.* FROM temp_topics; + + Which is will cause an error since there's not a topics table to select + from. + + Now the default if you use from is just `*`: + + SELECT * FROM temp_topics; + + *Cody Cutrer* + +* Fix `PostgreSQL` insert to properly extract table name from multiline string SQL. + + Previously, executing an insert SQL in `PostgreSQL` with a command like this: + + insert into articles( + number) + values( + 5152 + ) + + would not work because the adapter was unable to extract the correct `articles` + table name. + + *Kuldeep Aggarwal* + +* `Relation` no longer has mutator methods like `#map!` and `#delete_if`. Convert + to an `Array` by calling `#to_a` before using these methods. + + It intends to prevent odd bugs and confusion in code that call mutator + methods directly on the `Relation`. + + Example: + + # Instead of this + Author.where(name: 'Hank Moody').compact! + + # Now you have to do this + authors = Author.where(name: 'Hank Moody').to_a + authors.compact! + + *Lauro Caetano* + +* Better support for `where()` conditions that use a `belongs_to` + association name. + + Using the name of an association in `where` previously worked only + if the value was a single `ActiveRecord::Base` object. e.g. + + Post.where(author: Author.first) + + Any other values, including `nil`, would cause invalid SQL to be + generated. This change supports arguments in the `where` query + conditions where the key is a `belongs_to` association name and the + value is `nil`, an `Array` of `ActiveRecord::Base` objects, or an + `ActiveRecord::Relation` object. + + class Post < ActiveRecord::Base + belongs_to :author + end + + `nil` value finds records where the association is not set: + + Post.where(author: nil) + # SELECT "posts".* FROM "posts" WHERE "posts"."author_id" IS NULL + + `Array` values find records where the association foreign key + matches the ids of the passed ActiveRecord models, resulting + in the same query as `Post.where(author_id: [1,2])`: + + authors_array = [Author.find(1), Author.find(2)] + Post.where(author: authors_array) + # SELECT "posts".* FROM "posts" WHERE "posts"."author_id" IN (1, 2) + + `ActiveRecord::Relation` values find records using the same + query as `Post.where(author_id: Author.where(last_name: "Emde"))` + + Post.where(author: Author.where(last_name: "Emde")) + # SELECT "posts".* FROM "posts" + # WHERE "posts"."author_id" IN ( + # SELECT "authors"."id" FROM "authors" + # WHERE "authors"."last_name" = 'Emde') + + Polymorphic `belongs_to` associations will continue to be handled + appropriately, with the polymorphic `association_type` field added + to the query to match the base class of the value. This feature + previously only worked when the value was a single `ActveRecord::Base`. + + class Post < ActiveRecord::Base + belongs_to :author, polymorphic: true + end + + Post.where(author: Author.where(last_name: "Emde")) + # Generates a query similar to: + Post.where(author_id: Author.where(last_name: "Emde"), author_type: "Author") + + *Martin Emde* + +* Respect temporary option when dropping tables with MySQL. + + Normal DROP TABLE also works, but commits the transaction. + + drop_table :temporary_table, temporary: true + + *Cody Cutrer* + +* Add option to create tables from a query. + + create_table(:long_query, temporary: true, + as: "SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id") + + Generates: + + CREATE TEMPORARY TABLE long_query AS + SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id + + *Cody Cutrer* + +* `db:test:clone` and `db:test:prepare` must load Rails environment. + + `db:test:clone` and `db:test:prepare` use `ActiveRecord::Base`. configurations, + so we need to load the Rails environment, otherwise the config wont be in place. + + *arthurnn* + * Use the right column to type cast grouped calculations with custom expressions. - Fixes: #13230 + Fixes #13230. Example: - # Before - Account.group(:firm_name).sum('0.01 * credit_limit') - # => { '37signals' => '0.5' } + # Before + Account.group(:firm_name).sum('0.01 * credit_limit') + # => { '37signals' => '0.5' } - # After - Account.group(:firm_name).sum('0.01 * credit_limit') - # => { '37signals' => 0.5 } + # After + Account.group(:firm_name).sum('0.01 * credit_limit') + # => { '37signals' => 0.5 } *Paul Nikitochkin* @@ -449,12 +641,6 @@ *Paul Nikitochkin* -* Deprecate the delegation of Array bang methods for associations. - To use them, instead first call `#to_a` on the association to access the - array to be acted on. - - *Ben Woosley* - * `CollectionAssociation#first`/`#last` (e.g. `has_many`) use a `LIMIT`ed query to fetch results rather than loading the entire collection. diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index ce7d41cf54..67ea489b22 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -104,11 +104,12 @@ module ActiveRecord # Set the inverse association, if possible def set_inverse_instance(record) - if record && invertible_for?(record) + if invertible_for?(record) inverse = record.association(inverse_reflection_for(record).name) inverse.target = owner inverse.inversed = true end + record end # Returns the class of the target. belongs_to polymorphic overrides this to look at the diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index e1fa5225b5..8272a5584c 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -8,13 +8,16 @@ module ActiveRecord end def replace(record) - raise_on_type_mismatch!(record) if record - - update_counters(record) - replace_keys(record) - set_inverse_instance(record) - - @updated = true if record + if record + raise_on_type_mismatch!(record) + update_counters(record) + replace_keys(record) + set_inverse_instance(record) + @updated = true + else + decrement_counters + remove_keys + end self.target = record end @@ -34,35 +37,41 @@ module ActiveRecord !loaded? && foreign_key_present? && klass end - def update_counters(record) + def with_cache_name counter_cache_name = reflection.counter_cache_column + return unless counter_cache_name && owner.persisted? + yield counter_cache_name + end - if counter_cache_name && owner.persisted? && different_target?(record) - if record - record.class.increment_counter(counter_cache_name, record.id) - end + def update_counters(record) + with_cache_name do |name| + return unless different_target? record + record.class.increment_counter(name, record.id) + decrement_counter name + end + end - if foreign_key_present? - klass.decrement_counter(counter_cache_name, target_id) - end + def decrement_counters + with_cache_name { |name| decrement_counter name } + end + + def decrement_counter counter_cache_name + if foreign_key_present? + klass.decrement_counter(counter_cache_name, target_id) end end # Checks whether record is different to the current target, without loading it def different_target?(record) - if record.nil? - owner[reflection.foreign_key] - else - record.id != owner[reflection.foreign_key] - end + record.id != owner[reflection.foreign_key] end def replace_keys(record) - if record - owner[reflection.foreign_key] = record[reflection.association_primary_key(record.class)] - else - owner[reflection.foreign_key] = nil - end + owner[reflection.foreign_key] = record[reflection.association_primary_key(record.class)] + end + + def remove_keys + owner[reflection.foreign_key] = nil end def foreign_key_present? diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index eae5eed3a1..b710cf6bdb 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -11,7 +11,12 @@ module ActiveRecord def replace_keys(record) super - owner[reflection.foreign_type] = record && record.class.base_class.name + owner[reflection.foreign_type] = record.class.base_class.name + end + + def remove_keys + super + owner[reflection.foreign_type] = nil end def different_target?(record) diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index 37ba1c73b1..3911d1b520 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/module/attribute_accessors' + # This is the parent Association class which defines the variables # used by all associations. # @@ -13,65 +15,67 @@ module ActiveRecord::Associations::Builder class Association #:nodoc: class << self attr_accessor :extensions + # TODO: This class accessor is needed to make activerecord-deprecated_finders work. + # We can move it to a constant in 5.0. + attr_accessor :valid_options end self.extensions = [] - VALID_OPTIONS = [:class_name, :class, :foreign_key, :validate] + self.valid_options = [:class_name, :class, :foreign_key, :validate] + + attr_reader :name, :scope, :options def self.build(model, name, scope, options, &block) - extension = define_extensions model, name, &block - reflection = create_reflection model, name, scope, options, extension + builder = create_builder model, name, scope, options, &block + reflection = builder.build(model) define_accessors model, reflection define_callbacks model, reflection + builder.define_extensions model reflection end - def self.create_reflection(model, name, scope, options, extension = nil) + def self.create_builder(model, name, scope, options, &block) raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol) + new(model, name, scope, options, &block) + end + + def initialize(model, name, scope, options) + # TODO: Move this to create_builder as soon we drop support to activerecord-deprecated_finders. if scope.is_a?(Hash) options = scope scope = nil end - validate_options(options) + # TODO: Remove this model argument as soon we drop support to activerecord-deprecated_finders. + @name = name + @scope = scope + @options = options - scope = build_scope(scope, extension) - - ActiveRecord::Reflection.create(macro, name, scope, options, model) - end - - def self.build_scope(scope, extension) - new_scope = scope + validate_options if scope && scope.arity == 0 - new_scope = proc { instance_exec(&scope) } - end - - if extension - new_scope = wrap_scope new_scope, extension + @scope = proc { instance_exec(&scope) } end - - new_scope end - def self.wrap_scope(scope, extension) - scope + def build(model) + ActiveRecord::Reflection.create(macro, name, scope, options, model) end - def self.macro + def macro raise NotImplementedError end - def self.valid_options(options) - VALID_OPTIONS + Association.extensions.flat_map(&:valid_options) + def valid_options + Association.valid_options + Association.extensions.flat_map(&:valid_options) end - def self.validate_options(options) - options.assert_valid_keys(valid_options(options)) + def validate_options + options.assert_valid_keys(valid_options) end - def self.define_extensions(model, name) + def define_extensions(model) end def self.define_callbacks(model, reflection) @@ -114,6 +118,8 @@ module ActiveRecord::Associations::Builder raise NotImplementedError end + private + def self.add_before_destroy_callbacks(model, reflection) unless valid_dependent_options.include? reflection.options[:dependent] raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{reflection.options[:dependent]}" diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index e8e36e7cd0..5ccaa55a32 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -1,10 +1,10 @@ module ActiveRecord::Associations::Builder class BelongsTo < SingularAssociation #:nodoc: - def self.macro + def macro :belongs_to end - def self.valid_options(options) + def valid_options super + [:foreign_type, :polymorphic, :touch, :counter_cache] end @@ -23,6 +23,8 @@ module ActiveRecord::Associations::Builder add_counter_cache_methods mixin end + private + def self.add_counter_cache_methods(mixin) return if mixin.method_defined? :belongs_to_counter_cache_after_create @@ -110,7 +112,7 @@ module ActiveRecord::Associations::Builder end record = o.send name - unless record.nil? || record.new_record? + if record && record.persisted? if touch != true record.touch touch else diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb index 2ff67f904d..bc15a49996 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -7,11 +7,22 @@ module ActiveRecord::Associations::Builder CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove] - def self.valid_options(options) + def valid_options super + [:table_name, :before_add, :after_add, :before_remove, :after_remove, :extend] end + attr_reader :block_extension + + def initialize(model, name, scope, options) + super + @mod = nil + if block_given? + @mod = Module.new(&Proc.new) + @scope = wrap_scope @scope, @mod + end + end + def self.define_callbacks(model, reflection) super name = reflection.name @@ -21,11 +32,10 @@ module ActiveRecord::Associations::Builder } end - def self.define_extensions(model, name) - if block_given? + def define_extensions(model) + if @mod extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension" - extension = Module.new(&Proc.new) - model.parent.const_set(extension_module_name, extension) + model.parent.const_set(extension_module_name, @mod) end end @@ -68,7 +78,9 @@ module ActiveRecord::Associations::Builder CODE end - def self.wrap_scope(scope, mod) + private + + def wrap_scope(scope, mod) if scope proc { |owner| instance_exec(owner, &scope).extending(mod) } else diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index 1c9c04b044..e472277374 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -20,7 +20,7 @@ module ActiveRecord::Associations::Builder def self.build(lhs_class, name, options) if options[:join_table] - KnownTable.new options[:join_table] + KnownTable.new options[:join_table].to_s else class_name = options.fetch(:class_name) { name.to_s.camelize.singularize @@ -84,11 +84,11 @@ module ActiveRecord::Associations::Builder middle_name = [lhs_model.name.downcase.pluralize, association_name].join('_').gsub(/::/, '_').to_sym middle_options = middle_options join_model - - HasMany.create_reflection(lhs_model, - middle_name, - nil, - middle_options) + hm_builder = HasMany.create_builder(lhs_model, + middle_name, + nil, + middle_options) + hm_builder.build lhs_model end private diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index 227184cd19..7909b93622 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -1,10 +1,10 @@ module ActiveRecord::Associations::Builder class HasMany < CollectionAssociation #:nodoc: - def self.macro + def macro :has_many end - def self.valid_options(options) + def valid_options super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache] end diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index 064a3c8b51..f359efd496 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -1,10 +1,10 @@ module ActiveRecord::Associations::Builder class HasOne < SingularAssociation #:nodoc: - def self.macro + def macro :has_one end - def self.valid_options(options) + def valid_options valid = super + [:order, :as] valid += [:through, :source, :source_type] if options[:through] valid @@ -14,6 +14,8 @@ module ActiveRecord::Associations::Builder [:destroy, :delete, :nullify, :restrict_with_error, :restrict_with_exception] end + private + def self.add_before_destroy_callbacks(model, reflection) super unless reflection.options[:through] end diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index a6e8300642..e655c389a6 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -2,7 +2,7 @@ module ActiveRecord::Associations::Builder class SingularAssociation < Association #:nodoc: - def self.valid_options(options) + def valid_options super + [:remote, :dependent, :primary_key, :inverse_of] end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 62f23f54f9..e9ed5d9979 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -4,7 +4,7 @@ module ActiveRecord # # CollectionAssociation is an abstract class that provides common stuff to # ease the implementation of association proxies that represent - # collections. See the class hierarchy in AssociationProxy. + # collections. See the class hierarchy in Association. # # CollectionAssociation: # HasManyAssociation => has_many @@ -193,7 +193,11 @@ module ActiveRecord # Count all records using SQL. Construct options and pass them with # scope to the target class's +count+. - def count(column_name = nil) + def count(column_name = nil, count_options = {}) + # TODO: Remove count_options argument as soon we remove support to + # activerecord-deprecated_finders. + column_name, count_options = nil, column_name if column_name.is_a?(Hash) + relation = scope if association_scope.distinct_value # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 0b37ecf5b7..0dabe256e3 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -669,8 +669,10 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] - def count(column_name = nil) - @association.count(column_name) + def count(column_name = nil, options = {}) + # TODO: Remove options argument as soon we remove support to + # activerecord-deprecated_finders. + @association.count(column_name, options) end # Returns the size of the collection. If the collection hasn't been loaded, diff --git a/activerecord/lib/active_record/associations/preloader/singular_association.rb b/activerecord/lib/active_record/associations/preloader/singular_association.rb index 2b5cfda8ce..f60647a81e 100644 --- a/activerecord/lib/active_record/associations/preloader/singular_association.rb +++ b/activerecord/lib/active_record/associations/preloader/singular_association.rb @@ -11,7 +11,7 @@ module ActiveRecord association = owner.association(reflection.name) association.target = record - association.set_inverse_instance(record) + association.set_inverse_instance(record) if record end end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index 02dc464536..e4500af5b2 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -39,7 +39,9 @@ module ActiveRecord end def find_target - scope.first.tap { |record| set_inverse_instance(record) } + if record = scope.first + set_inverse_instance record + end end def replace(record) diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 217fc52dd5..73761520f7 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -330,7 +330,7 @@ module ActiveRecord end # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, - # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). It raises + # "2004-12-12" in a date column is cast to a date object, like Date.new(2004, 12, 12)). It raises # <tt>ActiveModel::MissingAttributeError</tt> if the identified attribute is missing. # # Alias for the <tt>read_attribute</tt> method. diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index c152a246b5..d01e9aea59 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -102,7 +102,7 @@ module ActiveRecord end # Returns the value of the attribute identified by <tt>attr_name</tt> after - # it has been typecast (for example, "2004-12-12" in a data column is cast + # it has been typecast (for example, "2004-12-12" in a date column is cast # to a date object, like Date.new(2004, 12, 12)). def read_attribute(attr_name) # If it's cached, just return it diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index cfdcae7f63..cebe741daa 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -86,7 +86,7 @@ module ActiveRecord end end - # Return the number of threads currently waiting on this + # Returns the number of threads currently waiting on this # queue. def num_waiting synchronize do diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 1a323b6a9c..e196df079b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -346,7 +346,7 @@ module ActiveRecord protected - # Return a subquery for the given key using the join information. + # Returns a subquery for the given key using the join information. def subquery_for(key, select) subselect = select.clone subselect.projections = [key] diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 552a22d28a..75501852ed 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -43,7 +43,9 @@ module ActiveRecord # SQLite does not understand dates, so this method will convert a Date # to a String. def type_cast(value, column) - return value.id if value.respond_to?(:quoted_id) + if value.respond_to?(:quoted_id) && value.respond_to?(:id) + return value.id + end case value when String, ActiveSupport::Multibyte::Chars diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb index 7c330a2f25..a51691bfa8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -34,9 +34,10 @@ module ActiveRecord def visit_TableDefinition(o) create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE " - create_sql << "#{quote_table_name(o.name)} (" - create_sql << o.columns.map { |c| accept c }.join(', ') - create_sql << ") #{o.options}" + create_sql << "#{quote_table_name(o.name)} " + create_sql << "(#{o.columns.map { |c| accept c }.join(', ')}) " unless o.as + create_sql << "#{o.options}" + create_sql << " AS #{@conn.to_sql(o.as)}" if o.as create_sql end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 063b19871a..c39bf15e83 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -49,14 +49,15 @@ module ActiveRecord # An array of ColumnDefinition objects, representing the column changes # that have been defined. attr_accessor :indexes - attr_reader :name, :temporary, :options + attr_reader :name, :temporary, :options, :as - def initialize(types, name, temporary, options) + def initialize(types, name, temporary, options, as = nil) @columns_hash = {} @indexes = {} @native = types @temporary = temporary @options = options + @as = as @name = name end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 51a6929dab..00383bad3b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -131,6 +131,9 @@ module ActiveRecord # [<tt>:force</tt>] # Set to true to drop the table before creating it. # Defaults to false. + # [<tt>:as</tt>] + # SQL to use to generate the table. When this option is used, the block is + # ignored, as are the <tt>:id</tt> and <tt>:primary_key</tt> options. # # ====== Add a backend specific option to the generated SQL (MySQL) # @@ -169,19 +172,31 @@ module ActiveRecord # supplier_id int # ) # + # ====== Create a temporary table based on a query + # + # create_table(:long_query, temporary: true, + # as: "SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id") + # + # generates: + # + # CREATE TEMPORARY TABLE long_query AS + # SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id + # # See also TableDefinition#column for details on how to create columns. def create_table(table_name, options = {}) - td = create_table_definition table_name, options[:temporary], options[:options] + td = create_table_definition table_name, options[:temporary], options[:options], options[:as] - unless options[:id] == false - pk = options.fetch(:primary_key) { - Base.get_primary_key table_name.to_s.singularize - } + if !options[:as] + unless options[:id] == false + pk = options.fetch(:primary_key) { + Base.get_primary_key table_name.to_s.singularize + } - td.primary_key pk, options.fetch(:id, :primary_key), options - end + td.primary_key pk, options.fetch(:id, :primary_key), options + end - yield td if block_given? + yield td if block_given? + end if options[:force] && table_exists?(table_name) drop_table(table_name, options) @@ -826,8 +841,8 @@ module ActiveRecord end private - def create_table_definition(name, temporary, options) - TableDefinition.new native_database_types, name, temporary, options + def create_table_definition(name, temporary, options, as = nil) + TableDefinition.new native_database_types, name, temporary, options, as end def create_alter_table(name) 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 1b9f865666..7768c24967 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -303,12 +303,6 @@ module ActiveRecord else log(sql, name) { @connection.query(sql) } end - rescue ActiveRecord::StatementInvalid => exception - if exception.message.split(":").first =~ /Packets out of order/ - raise ActiveRecord::StatementInvalid.new("'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings.", exception.original_exception) - else - raise - end end # MysqlAdapter has to free a result after using it, so we use this method to write @@ -492,6 +486,10 @@ module ActiveRecord rename_table_indexes(table_name, new_name) end + def drop_table(table_name, options = {}) + execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE #{quote_table_name(table_name)}" + end + def rename_index(table_name, old_name, new_name) if (version[0] == 5 && version[1] >= 7) || version[0] >= 6 execute "ALTER TABLE #{quote_table_name(table_name)} RENAME INDEX #{quote_table_name(old_name)} TO #{quote_table_name(new_name)}" diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 64fc9e95d8..7e16156408 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -16,38 +16,24 @@ module ActiveRecord ## # Builds a ConnectionSpecification from user input class Resolver # :nodoc: - attr_reader :config, :klass, :configurations + attr_reader :configurations - def initialize(config, configurations) - @config = config + def initialize(configurations) @configurations = configurations end - def spec - case config - when nil - raise AdapterNotSpecified unless defined?(Rails.env) - resolve_string_connection Rails.env - when Symbol, String - resolve_string_connection config.to_s - when Hash - resolve_hash_connection config - end - end - - private - def resolve_string_connection(spec) # :nodoc: - hash = configurations.fetch(spec) do |k| - connection_url_to_hash(k) + def resolve(config) + if config + resolve_connection config + elsif defined?(Rails.env) + resolve_env_connection Rails.env.to_sym + else + raise AdapterNotSpecified end - - raise(AdapterNotSpecified, "#{spec} database is not configured") unless hash - - resolve_hash_connection hash end - def resolve_hash_connection(spec) # :nodoc: - spec = spec.symbolize_keys + def spec(config) + spec = resolve(config).symbolize_keys raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter) @@ -61,20 +47,64 @@ module ActiveRecord end adapter_method = "#{spec[:adapter]}_connection" - ConnectionSpecification.new(spec, adapter_method) end - def connection_url_to_hash(url) # :nodoc: - config = URI.parse url + private + + def resolve_connection(spec) #:nodoc: + case spec + when Symbol, String + resolve_env_connection spec + when Hash + resolve_hash_connection spec + end + end + + def resolve_env_connection(spec) # :nodoc: + # Rails has historically accepted a string to mean either + # an environment key or a url spec, so we have deprecated + # this ambiguous behaviour and in the future this function + # can be removed in favor of resolve_string_connection and + # resolve_symbol_connection. + if config = configurations[spec.to_s] + if spec.is_a?(String) + ActiveSupport::Deprecation.warn "Passing a string to ActiveRecord::Base.establish_connection " \ + "for a configuration lookup is deprecated, please pass a symbol (#{spec.to_sym.inspect}) instead" + end + resolve_connection(config) + elsif spec.is_a?(String) + resolve_string_connection(spec) + else + raise(AdapterNotSpecified, "#{spec} database is not configured") + end + end + + def resolve_hash_connection(spec) # :nodoc: + spec + end + + def resolve_string_connection(spec) # :nodoc: + config = URI.parse spec adapter = config.scheme adapter = "postgresql" if adapter == "postgres" - spec = { :adapter => adapter, - :username => config.user, - :password => config.password, - :port => config.port, - :database => config.path.sub(%r{^/},""), - :host => config.host } + + database = if adapter == 'sqlite3' + if '/:memory:' == config.path + ':memory:' + else + config.path + end + else + config.path.sub(%r{^/},"") + end + + spec = { "adapter" => adapter, + "username" => config.user, + "password" => config.password, + "port" => config.port, + "database" => database, + "host" => config.host } spec.reject!{ |_,value| value.blank? } @@ -83,8 +113,7 @@ module ActiveRecord spec.map { |key,value| spec[key] = uri_parser.unescape(value) if value.is_a?(String) } if config.query - options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys - + options = Hash[config.query.split("&").map{ |pair| pair.split("=") }] spec.merge!(options) end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index e790f731ea..6d8e994654 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -18,6 +18,12 @@ module ActiveRecord client = Mysql2::Client.new(config) options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0] ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config) + rescue Mysql2::Error => error + if error.message.include?("Unknown database") + raise ActiveRecord::NoDatabaseError.new(error.message) + else + raise error + end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index c4dcba0501..7dbaa272a3 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -34,6 +34,12 @@ module ActiveRecord default_flags |= Mysql::CLIENT_FOUND_ROWS if Mysql.const_defined?(:CLIENT_FOUND_ROWS) options = [host, username, password, database, port, socket, default_flags] ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config) + rescue Mysql::Error => error + if error.message.include?("Unknown database") + raise ActiveRecord::NoDatabaseError.new(error.message) + else + raise error + end end end @@ -419,14 +425,19 @@ module ActiveRecord if result types = {} + fields = [] result.fetch_fields.each { |field| + field_name = field.name + fields << field_name + if field.decimals > 0 - types[field.name] = Fields::Decimal.new + types[field_name] = Fields::Decimal.new else - types[field.name] = Fields.find_type field + types[field_name] = Fields.find_type field end } - result_set = ActiveRecord::Result.new(types.keys, result.to_a, types) + + result_set = ActiveRecord::Result.new(fields, result.to_a, types) result.free else result_set = ActiveRecord::Result.new([], []) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb index bf34f2bdae..35ce881302 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb @@ -144,7 +144,7 @@ module ActiveRecord def quote_and_escape(value) case value - when "NULL" + when "NULL", Numeric value else "\"#{value.gsub(/"/,"\\\"")}\"" diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index d23a24589c..11a5eba464 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -46,7 +46,7 @@ module ActiveRecord # PostgreSQL-specific extensions to column definitions in a table. class PostgreSQLColumn < Column #:nodoc: attr_accessor :array - # Instantiates a new PostgreSQL column definition in a table. + def initialize(name, default, oid_type, sql_type = nil, null = true) @oid_type = oid_type default_value = self.class.extract_value_from_default(default) @@ -62,6 +62,14 @@ module ActiveRecord @default_function = default if has_default_function?(default_value, default) end + def number? + !array && super + end + + def text? + !array && super + end + # :stopdoc: class << self include ConnectionAdapters::PostgreSQLColumn::Cast @@ -865,6 +873,12 @@ module ActiveRecord PostgreSQLColumn.money_precision = (postgresql_version >= 80300) ? 19 : 10 configure_connection + rescue ::PG::Error => error + if error.message.include?("does not exist") + raise ActiveRecord::NoDatabaseError.new(error.message) + else + raise error + end end # Configures the encoding, verbosity, schema search path, and time zone of the connection. @@ -969,12 +983,12 @@ module ActiveRecord end def extract_table_ref_from_insert_sql(sql) - sql[/into\s+([^\(]*).*values\s*\(/i] + sql[/into\s+([^\(]*).*values\s*\(/im] $1.strip if $1 end - def create_table_definition(name, temporary, options) - TableDefinition.new native_database_types, name, temporary, options + def create_table_definition(name, temporary, options, as = nil) + TableDefinition.new native_database_types, name, temporary, options, as end def update_table_definition(table_name, base) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 2cf1015f2c..92bb70ba53 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -31,6 +31,12 @@ module ActiveRecord db.busy_timeout(ConnectionAdapters::SQLite3Adapter.type_cast_config_to_integer(config[:timeout])) if config[:timeout] ConnectionAdapters::SQLite3Adapter.new(db, logger, config) + rescue Errno::ENOENT => error + if error.message.include?("No such file or directory") + raise ActiveRecord::NoDatabaseError.new(error.message) + else + raise error + end end end @@ -586,7 +592,11 @@ module ActiveRecord def translate_exception(exception, message) case exception.message - when /column(s)? .* (is|are) not unique/ + # SQLite 3.8.2 returns a newly formatted error message: + # UNIQUE constraint failed: *table_name*.*column_name* + # Older versions of SQLite return: + # column *column_name* is not unique + when /column(s)? .* (is|are) not unique/, /UNIQUE constraint failed: .*/ RecordNotUnique.new(message, exception) else super diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index a1943dfcb0..c4afadbd9b 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -32,11 +32,18 @@ module ActiveRecord # "postgres://myuser:mypass@localhost/somedatabase" # ) # + # In case <tt>ActiveRecord::Base.configurations</tt> is set (Rails + # automatically loads the contents of config/database.yml into it), + # a symbol can also be given as argument, representing a key in the + # configuration hash: + # + # ActiveRecord::Base.establish_connection(:production) + # # The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError # may be returned on an error. def establish_connection(spec = ENV["DATABASE_URL"]) - resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new spec, configurations - spec = resolver.spec + resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new configurations + spec = resolver.spec(spec) unless respond_to?(spec.adapter_method) raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter" diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 7e3bef9431..dcbdf75627 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -82,10 +82,10 @@ module ActiveRecord # Increment a numeric field by one, via a direct SQL update. # - # This method is used primarily for maintaining counter_cache columns used to - # store aggregate values. For example, a DiscussionBoard may cache posts_count - # and comments_count to avoid running an SQL query to calculate the number of - # posts and comments there are each time it is displayed. + # This method is used primarily for maintaining counter_cache columns that are + # used to store aggregate values. For example, a DiscussionBoard may cache + # posts_count and comments_count to avoid running an SQL query to calculate the + # number of posts and comments there are, each time it is displayed. # # ==== Parameters # diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index e650ebcf64..5caab09038 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -84,13 +84,18 @@ module ActiveRecord "#{finder}(#{attributes_hash})" end + # The parameters in the signature may have reserved Ruby words, in order + # to prevent errors, we start each param name with `_`. + # # Extended in activerecord-deprecated_finders def signature - attribute_names.join(', ') + attribute_names.map { |name| "_#{name}" }.join(', ') end + # Given that the parameters starts with `_`, the finder needs to use the + # same parameter name. def attributes_hash - "{" + attribute_names.map { |name| ":#{name} => #{name}" }.join(',') + "}" + "{" + attribute_names.map { |name| ":#{name} => _#{name}" }.join(',') + "}" end def finder diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 837989aaa7..2f8439892b 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -55,12 +55,12 @@ module ActiveRecord def enum(definitions) klass = self definitions.each do |name, values| - # DIRECTION = { } + # STATUS = { } enum_values = _enum_methods_module.const_set name.to_s.upcase, ActiveSupport::HashWithIndifferentAccess.new name = name.to_sym _enum_methods_module.module_eval do - # def direction=(value) self[:direction] = DIRECTION[value] end + # def status=(value) self[:status] = STATUS[value] end define_method("#{name}=") { |value| unless enum_values.has_key?(value) raise ArgumentError, "'#{value}' is not a valid #{name}" @@ -68,20 +68,20 @@ module ActiveRecord self[name] = enum_values[value] } - # def direction() DIRECTION.key self[:direction] end + # def status() STATUS.key self[:status] end define_method(name) { enum_values.key self[name] } pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index pairs.each do |value, i| enum_values[value] = i - # scope :incoming, -> { where direction: 0 } + # scope :active, -> { where status: 0 } klass.scope value, -> { klass.where name => i } - # def incoming?() direction == 0 end + # def active?() status == 0 end define_method("#{value}?") { self[name] == i } - # def incoming! update! direction: :incoming end + # def active!() update! status: :active end define_method("#{value}!") { update! name => value } end end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 7e38719811..7f6228131f 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -94,6 +94,18 @@ module ActiveRecord class PreparedStatementInvalid < ActiveRecordError end + # Raised when a given database does not exist + class NoDatabaseError < ActiveRecordError + def initialize(message) + super extend_message(message) + end + + # can be over written to add additional error information. + def extend_message(message) + message + end + end + # Raised on attempt to save stale record. Record is stale when it's being saved in another query after # instantiation, for example, when two users edit the same wiki page and one starts editing and saves # the page before the other. @@ -188,7 +200,7 @@ module ActiveRecord end end - # Raised when a primary key is needed, but there is not one specified in the schema or model. + # Raised when a primary key is needed, but not specified in the schema or model. class UnknownPrimaryKey < ActiveRecordError attr_reader :model diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index d241788a9b..a7a54483bc 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -604,7 +604,7 @@ module ActiveRecord } end - # Return a hash of rows to be inserted. The key is the table, the value is + # Returns a hash of rows to be inserted. The key is the table, the value is # a list of rows to insert to that table. def table_rows now = config.default_timezone == :utc ? Time.now.utc : Time.now diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 27576b1e61..31e2518540 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -98,8 +98,10 @@ module ActiveRecord super() else define_method :to_param do - if (default = super()) && (result = send(method_name).to_s).present? - "#{default}-#{result.squish.truncate(20, separator: /\s/, omission: nil).parameterize}" + if (default = super()) && + (result = send(method_name).to_s).present? && + (param = result.squish.truncate(20, separator: /\s/, omission: nil).parameterize).present? + "#{default}-#{param}" else default end diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index 080b20134d..5b255c3fe5 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -50,8 +50,10 @@ module ActiveRecord 0 end - def calculate(_operation, _column_name) - if _operation == :count + def calculate(operation, _column_name, _options = {}) + # TODO: Remove _options argument as soon we remove support to + # activerecord-deprecated_finders. + if operation == :count 0 else nil diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index eef08aea88..2c796f97e6 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -43,11 +43,24 @@ module ActiveRecord namespace :db do task :load_config do ActiveRecord::Tasks::DatabaseTasks.db_dir = Rails.application.config.paths["db"].first - ActiveRecord::Tasks::DatabaseTasks.database_configuration = Rails.application.config.database_configuration ActiveRecord::Tasks::DatabaseTasks.migrations_paths = Rails.application.paths['db/migrate'].to_a ActiveRecord::Tasks::DatabaseTasks.fixtures_path = File.join Rails.root, 'test', 'fixtures' ActiveRecord::Tasks::DatabaseTasks.root = Rails.root + configuration = if ENV["DATABASE_URL"] + { Rails.env => ENV["DATABASE_URL"] } + else + Rails.application.config.database_configuration || {} + end + + resolver = ActiveRecord::ConnectionAdapters::ConnectionSpecification::Resolver.new(configuration) + + configuration.each do |key, value| + configuration[key] = resolver.resolve(value) if value + end + + ActiveRecord::Tasks::DatabaseTasks.database_configuration = configuration + if defined?(ENGINE_PATH) && engine = Rails::Engine.find(ENGINE_PATH) if engine.paths['db/migrate'].existent ActiveRecord::Tasks::DatabaseTasks.migrations_paths += engine.paths['db/migrate'].to_a @@ -122,6 +135,14 @@ module ActiveRecord # and then establishes the connection. initializer "active_record.initialize_database" do |app| ActiveSupport.on_load(:active_record) do + + class ActiveRecord::NoDatabaseError + def extend_message(message) + message << "Run `$ bin/rake db:create db:migrate` to create your database" + message + end + end + self.configurations = app.config.database_configuration || {} establish_connection end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 52b3d3e5e6..90dfc177e5 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -14,11 +14,7 @@ db_namespace = namespace :db do desc 'Create the database from DATABASE_URL or config/database.yml for the current Rails.env (use db:create:all to create all databases in the config)' task :create => [:load_config] do - if ENV['DATABASE_URL'] - ActiveRecord::Tasks::DatabaseTasks.create_database_url - else - ActiveRecord::Tasks::DatabaseTasks.create_current - end + ActiveRecord::Tasks::DatabaseTasks.create_current end namespace :drop do @@ -29,11 +25,7 @@ db_namespace = namespace :db do desc 'Drops the database using DATABASE_URL or the current Rails.env (use db:drop:all to drop all databases)' task :drop => [:load_config] do - if ENV['DATABASE_URL'] - ActiveRecord::Tasks::DatabaseTasks.drop_database_url - else - ActiveRecord::Tasks::DatabaseTasks.drop_current - end + ActiveRecord::Tasks::DatabaseTasks.drop_current end desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." @@ -343,7 +335,7 @@ db_namespace = namespace :db do end # desc "Recreate the test database from a fresh schema" - task :clone do + task :clone => :environment do case ActiveRecord::Base.schema_format when :ruby db_namespace["test:clone_schema"].invoke @@ -364,7 +356,7 @@ db_namespace = namespace :db do end # desc 'Check for pending migrations and load the test schema' - task :prepare => :load_config do + task :prepare => [:environment, :load_config] do unless ActiveRecord::Base.configurations.blank? db_namespace['test:load'].invoke end @@ -401,4 +393,3 @@ namespace :railties do end task 'test:prepare' => ['db:test:prepare', 'db:test:load', 'db:abort_if_pending_migrations'] - diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index cf24d10a8d..45ffb99868 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -19,16 +19,21 @@ module ActiveRecord # # Person.group(:city).count # # => { 'Rome' => 5, 'Paris' => 3 } - def count(column_name = nil) - calculate(:count, column_name) + def count(column_name = nil, options = {}) + # TODO: Remove options argument as soon we remove support to + # activerecord-deprecated_finders. + column_name, options = nil, column_name if column_name.is_a?(Hash) + calculate(:count, column_name, options) end # Calculates the average value on a given column. Returns +nil+ if there's # no row. See +calculate+ for examples with options. # # Person.average(:age) # => 35.8 - def average(column_name) - calculate(:average, column_name) + def average(column_name, options = {}) + # TODO: Remove options argument as soon we remove support to + # activerecord-deprecated_finders. + calculate(:average, column_name, options) end # Calculates the minimum value on a given column. The value is returned @@ -36,8 +41,10 @@ module ActiveRecord # +calculate+ for examples with options. # # Person.minimum(:age) # => 7 - def minimum(column_name) - calculate(:minimum, column_name) + def minimum(column_name, options = {}) + # TODO: Remove options argument as soon we remove support to + # activerecord-deprecated_finders. + calculate(:minimum, column_name, options) end # Calculates the maximum value on a given column. The value is returned @@ -45,8 +52,10 @@ module ActiveRecord # +calculate+ for examples with options. # # Person.maximum(:age) # => 93 - def maximum(column_name) - calculate(:maximum, column_name) + def maximum(column_name, options = {}) + # TODO: Remove options argument as soon we remove support to + # activerecord-deprecated_finders. + calculate(:maximum, column_name, options) end # Calculates the sum of values on a given column. The value is returned @@ -89,15 +98,17 @@ module ActiveRecord # Person.group(:last_name).having("min(age) > 17").minimum(:age) # # Person.sum("2 * age") - def calculate(operation, column_name) + def calculate(operation, column_name, options = {}) + # TODO: Remove options argument as soon we remove support to + # activerecord-deprecated_finders. if column_name.is_a?(Symbol) && attribute_alias?(column_name) column_name = attribute_alias(column_name) end if has_include?(column_name) - construct_relation_for_association_calculations.calculate(operation, column_name) + construct_relation_for_association_calculations.calculate(operation, column_name, options) else - perform_calculation(operation, column_name) + perform_calculation(operation, column_name, options) end end @@ -180,7 +191,9 @@ module ActiveRecord eager_loading? || (includes_values.present? && (column_name || references_eager_loaded_tables?)) end - def perform_calculation(operation, column_name) + def perform_calculation(operation, column_name, options = {}) + # TODO: Remove options argument as soon we remove support to + # activerecord-deprecated_finders. operation = operation.to_s.downcase # If #count is used with #distinct / #uniq it is considered distinct. (eg. relation.distinct.count) diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 1e15bddcdf..21beed332f 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -1,3 +1,4 @@ +require 'set' require 'active_support/concern' require 'active_support/deprecation' @@ -36,7 +37,14 @@ module ActiveRecord # may vary depending on the klass of a relation, so we create a subclass of Relation # for each different klass, and the delegations are compiled into that subclass only. - delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :to => :to_a + BLACKLISTED_ARRAY_METHODS = [ + :compact!, :flatten!, :reject!, :reverse!, :rotate!, :map!, + :shuffle!, :slice!, :sort!, :sort_by!, :delete_if, + :keep_if, :pop, :shift, :delete_at, :compact + ].to_set # :nodoc: + + delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, to: :to_a + delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, :connection, :columns_hash, :to => :klass @@ -64,7 +72,7 @@ module ActiveRecord RUBY else define_method method do |*args, &block| - scoping { @klass.send(method, *args, &block) } + scoping { @klass.public_send(method, *args, &block) } end end end @@ -83,13 +91,10 @@ module ActiveRecord def method_missing(method, *args, &block) if @klass.respond_to?(method) self.class.delegate_to_scoped_klass(method) - scoping { @klass.send(method, *args, &block) } - elsif array_delegable?(method) - self.class.delegate method, :to => :to_a - to_a.send(method, *args, &block) + scoping { @klass.public_send(method, *args, &block) } elsif arel.respond_to?(method) self.class.delegate method, :to => :arel - arel.send(method, *args, &block) + arel.public_send(method, *args, &block) else super end @@ -109,30 +114,24 @@ module ActiveRecord end def respond_to?(method, include_private = false) - super || array_delegable?(method) || - @klass.respond_to?(method, include_private) || + super || @klass.respond_to?(method, include_private) || + array_delegable?(method) || arel.respond_to?(method, include_private) end protected def array_delegable?(method) - defined = Array.method_defined?(method) - if defined && method.to_s.ends_with?('!') - ActiveSupport::Deprecation.warn( - "Association will no longer delegate #{method} to #to_a as of Rails 4.2. You instead must first call #to_a on the association to expose the array to be acted on." - ) - end - defined + Array.method_defined?(method) && BLACKLISTED_ARRAY_METHODS.exclude?(method) end def method_missing(method, *args, &block) if @klass.respond_to?(method) - scoping { @klass.send(method, *args, &block) } + scoping { @klass.public_send(method, *args, &block) } elsif array_delegable?(method) - to_a.send(method, *args, &block) + to_a.public_send(method, *args, &block) elsif arel.respond_to?(method) - arel.send(method, *args, &block) + arel.public_send(method, *args, &block) else super end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index c60cd27a83..1252af7635 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -55,9 +55,9 @@ module ActiveRecord # # For polymorphic relationships, find the foreign key and type: # PriceEstimate.where(estimate_of: treasure) - if klass && value.is_a?(Base) && reflection = klass.reflect_on_association(column.to_sym) - if reflection.polymorphic? - queries << build(table[reflection.foreign_type], value.class.base_class) + if klass && reflection = klass.reflect_on_association(column.to_sym) + if reflection.polymorphic? && base_class = polymorphic_base_class_from_value(value) + queries << build(table[reflection.foreign_type], base_class) end column = reflection.foreign_key @@ -67,6 +67,18 @@ module ActiveRecord queries end + def self.polymorphic_base_class_from_value(value) + case value + when Relation + value.klass.base_class + when Array + val = value.compact.first + val.class.base_class if val.is_a?(Base) + when Base + value.class.base_class + end + end + def self.references(attributes) attributes.map do |key, value| if value.is_a?(Hash) diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index cacc787eba..3d0709266a 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -427,7 +427,7 @@ module ActiveRecord # === string # # A single string, without additional arguments, is passed to the query - # constructor as a SQL fragment, and used in the where clause of the query. + # constructor as an SQL fragment, and used in the where clause of the query. # # Client.where("orders_count = '2'") # # SELECT * from clients where orders_count = '2'; @@ -631,12 +631,11 @@ module ActiveRecord self end - # Returns a chainable relation with zero records, specifically an - # instance of the <tt>ActiveRecord::NullRelation</tt> class. + # Returns a chainable relation with zero records. # - # The returned <tt>ActiveRecord::NullRelation</tt> inherits from Relation and implements the - # Null Object pattern. It is an object with defined null behavior and always returns an empty - # array of records without querying the database. + # The returned relation implements the Null Object pattern. It is an + # object with defined null behavior and always returns an empty array of + # records without querying the database. # # Any subsequent condition chained to the returned relation will continue # generating an empty relation and will not fire any query to the database. @@ -656,7 +655,7 @@ module ActiveRecord # when 'Reviewer' # Post.published # when 'Bad User' - # Post.none # => returning [] instead breaks the previous code + # Post.none # It can't be chained if [] is returned. # end # end # @@ -983,8 +982,10 @@ module ActiveRecord end def build_select(arel, selects) - unless selects.empty? + if !selects.empty? arel.project(*selects) + elsif from_value + arel.project(Arel.star) else arel.project(@klass.arel_table[Arel.star]) end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index cab8fd745a..dacaec26b7 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -100,8 +100,9 @@ module ActiveRecord # { status: nil, group_id: 1 } # # => "status = NULL , group_id = 1" def sanitize_sql_hash_for_assignment(attrs, table) + c = connection attrs.map do |attr, value| - "#{connection.quote_table_name_for_assignment(table, attr)} = #{quote_bound_value(value)}" + "#{c.quote_table_name_for_assignment(table, attr)} = #{quote_bound_value(value, c, columns_hash[attr.to_s])}" end.join(', ') end @@ -152,8 +153,10 @@ module ActiveRecord end end - def quote_bound_value(value, c = connection) #:nodoc: - if value.respond_to?(:map) && !value.acts_like?(:string) + def quote_bound_value(value, c = connection, column = nil) #:nodoc: + if column + c.quote(value, column) + elsif value.respond_to?(:map) && !value.acts_like?(:string) if value.respond_to?(:empty?) && value.empty? c.quote(nil) else diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index be7d496d15..06592eece2 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -56,11 +56,7 @@ module ActiveRecord if options.has_key?(:config) @current_config = options[:config] else - @current_config ||= if ENV['DATABASE_URL'] - database_url_config - else - ActiveRecord::Base.configurations[options[:env]] - end + @current_config ||= ActiveRecord::Base.configurations[options[:env]] end end @@ -85,10 +81,6 @@ module ActiveRecord ActiveRecord::Base.establish_connection environment end - def create_database_url - create database_url_config - end - def drop(*arguments) configuration = arguments.first class_for_adapter(configuration['adapter']).new(*arguments).drop @@ -107,10 +99,6 @@ module ActiveRecord } end - def drop_database_url - drop database_url_config - end - def charset_current(environment = env) charset ActiveRecord::Base.configurations[environment] end @@ -165,11 +153,6 @@ module ActiveRecord private - def database_url_config - @database_url_config ||= - ConnectionAdapters::ConnectionSpecification::Resolver.new(ENV["DATABASE_URL"], {}).spec.config.stringify_keys - end - def class_for_adapter(adapter) key = @tasks.keys.detect { |pattern| adapter[pattern] } unless key diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb index de5fd05468..863c3ebe4d 100644 --- a/activerecord/lib/active_record/version.rb +++ b/activerecord/lib/active_record/version.rb @@ -1,7 +1,7 @@ module ActiveRecord # Returns the version of the currently loaded ActiveRecord as a Gem::Version def self.version - Gem::Version.new "4.1.0.beta" + Gem::Version.new "4.1.0.beta1" end module VERSION #:nodoc: diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 34be68a97f..b67e70ec7e 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -92,7 +92,7 @@ module ActiveRecord ) end ensure - ActiveRecord::Base.establish_connection 'arunit' + ActiveRecord::Base.establish_connection :arunit end end end @@ -191,7 +191,7 @@ module ActiveRecord end def setup - Klass.establish_connection 'arunit' + Klass.establish_connection :arunit @connection = Klass.connection end diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb index a2c933d96a..a8b5cfb0cd 100644 --- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb @@ -16,6 +16,20 @@ module ActiveRecord eosql end + def test_bad_connection_mysql + assert_raise ActiveRecord::NoDatabaseError do + connection = ActiveRecord::Base.mysql_connection(adapter: "mysql", database: "should_not_exist-cinco-dog-db") + connection.exec_query('drop table if exists ex') + end + end + + def test_bad_connection_mysql2 + assert_raise ActiveRecord::NoDatabaseError do + connection = ActiveRecord::Base.mysql2_connection(adapter: "mysql2", database: "should_not_exist-cinco-dog-db") + connection.exec_query('drop table if exists ex') + end + end + def test_valid_column column = @conn.columns('ex').find { |col| col.name == 'id' } assert @conn.valid_type?(column.type) diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb index 5db60ff8a0..43c9116b5a 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb @@ -65,6 +65,15 @@ module ActiveRecord assert_nil index_c.using assert_equal :fulltext, index_c.type end + + def test_drop_temporary_table + @connection.transaction do + @connection.create_table(:temp_table, temporary: true) + # if it doesn't properly say DROP TEMPORARY TABLE, the transaction commit + # will complain that no transaction is active + @connection.drop_table(:temp_table, temporary: true) + end + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index 9536cceb1d..d71e2aa2bb 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -10,12 +10,12 @@ class PostgresqlArrayTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection - @connection.transaction do - @connection.create_table('pg_arrays') do |t| - t.string 'tags', array: true - t.integer 'ratings', array: true - end + @connection.transaction do + @connection.create_table('pg_arrays') do |t| + t.string 'tags', array: true + t.integer 'ratings', array: true end + end @column = PgArray.columns.find { |c| c.name == 'tags' } end @@ -26,6 +26,12 @@ class PostgresqlArrayTest < ActiveRecord::TestCase def test_column assert_equal :string, @column.type assert @column.array + assert_not @column.text? + + ratings_column = PgArray.columns_hash['ratings'] + assert_equal :integer, ratings_column.type + assert ratings_column.array + assert_not ratings_column.number? end def test_change_column_with_array @@ -50,8 +56,6 @@ class PostgresqlArrayTest < ActiveRecord::TestCase end def test_type_cast_array - assert @column - data = '{1,2,3}' oid_type = @column.instance_variable_get('@oid_type').subtype # we are getting the instance variable in this test, but in the @@ -66,6 +70,12 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert_equal([nil], @column.type_cast('{NULL}')) end + def test_type_cast_integers + x = PgArray.new(ratings: ['1', '2']) + assert x.save! + assert_equal(['1', '2'], x.ratings) + end + def test_rewrite @connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')" x = PgArray.first @@ -118,6 +128,16 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]", record.attribute_for_inspect(:ratings)) end + def test_update_all + pg_array = PgArray.create! tags: ["one", "two", "three"] + + PgArray.update_all tags: ["four", "five"] + assert_equal ["four", "five"], pg_array.reload.tags + + PgArray.update_all tags: [] + assert_equal [], pg_array.reload.tags + end + private def assert_cycle field, array # test creation diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index 01de202d11..04a458fbce 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -3,9 +3,6 @@ require "cases/helper" class PostgresqlArray < ActiveRecord::Base end -class PostgresqlRange < ActiveRecord::Base -end - class PostgresqlTsvector < ActiveRecord::Base end @@ -46,104 +43,6 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase @connection.execute("INSERT INTO postgresql_arrays (id, commission_by_quarter, nicknames) VALUES (1, '{35000,21000,18000,17000}', '{foo,bar,baz}')") @first_array = PostgresqlArray.find(1) - @connection.execute <<_SQL if @connection.supports_ranges? - INSERT INTO postgresql_ranges ( - date_range, - num_range, - ts_range, - tstz_range, - int4_range, - int8_range - ) VALUES ( - '[''2012-01-02'', ''2012-01-04'']', - '[0.1, 0.2]', - '[''2010-01-01 14:30'', ''2011-01-01 14:30'']', - '[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']', - '[1, 10]', - '[10, 100]' - ) -_SQL - - @connection.execute <<_SQL if @connection.supports_ranges? - INSERT INTO postgresql_ranges ( - date_range, - num_range, - ts_range, - tstz_range, - int4_range, - int8_range - ) VALUES ( - '(''2012-01-02'', ''2012-01-04'')', - '[0.1, 0.2)', - '[''2010-01-01 14:30'', ''2011-01-01 14:30'')', - '[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'')', - '(1, 10)', - '(10, 100)' - ) -_SQL - - @connection.execute <<_SQL if @connection.supports_ranges? - INSERT INTO postgresql_ranges ( - date_range, - num_range, - ts_range, - tstz_range, - int4_range, - int8_range - ) VALUES ( - '(''2012-01-02'',]', - '[0.1,]', - '[''2010-01-01 14:30'',]', - '[''2010-01-01 14:30:00+05'',]', - '(1,]', - '(10,]' - ) -_SQL - - @connection.execute <<_SQL if @connection.supports_ranges? - INSERT INTO postgresql_ranges ( - date_range, - num_range, - ts_range, - tstz_range, - int4_range, - int8_range - ) VALUES ( - '[,]', - '[,]', - '[,]', - '[,]', - '[,]', - '[,]' - ) -_SQL - - @connection.execute <<_SQL if @connection.supports_ranges? - INSERT INTO postgresql_ranges ( - date_range, - num_range, - ts_range, - tstz_range, - int4_range, - int8_range - ) VALUES ( - '(''2012-01-02'', ''2012-01-02'')', - '(0.1, 0.1)', - '(''2010-01-01 14:30'', ''2010-01-01 14:30'')', - '(''2010-01-01 14:30:00+05'', ''2010-01-01 06:30:00-03'')', - '(1, 1)', - '(10, 10)' - ) -_SQL - - if @connection.supports_ranges? - @first_range = PostgresqlRange.find(1) - @second_range = PostgresqlRange.find(2) - @third_range = PostgresqlRange.find(3) - @fourth_range = PostgresqlRange.find(4) - @empty_range = PostgresqlRange.find(5) - end - @connection.execute("INSERT INTO postgresql_tsvectors (id, text_vector) VALUES (1, ' ''text'' ''vector'' ')") @first_tsvector = PostgresqlTsvector.find(1) @@ -230,187 +129,6 @@ _SQL assert_equal "'text' 'vector'", @first_tsvector.text_vector end - if ActiveRecord::Base.connection.supports_ranges? - def test_data_type_of_range_types - assert_equal :daterange, @first_range.column_for_attribute(:date_range).type - assert_equal :numrange, @first_range.column_for_attribute(:num_range).type - assert_equal :tsrange, @first_range.column_for_attribute(:ts_range).type - assert_equal :tstzrange, @first_range.column_for_attribute(:tstz_range).type - assert_equal :int4range, @first_range.column_for_attribute(:int4_range).type - assert_equal :int8range, @first_range.column_for_attribute(:int8_range).type - end - - def test_int4range_values - assert_equal 1...11, @first_range.int4_range - assert_equal 2...10, @second_range.int4_range - assert_equal 2...Float::INFINITY, @third_range.int4_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int4_range) - assert_nil @empty_range.int4_range - end - - def test_int8range_values - assert_equal 10...101, @first_range.int8_range - assert_equal 11...100, @second_range.int8_range - assert_equal 11...Float::INFINITY, @third_range.int8_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int8_range) - assert_nil @empty_range.int8_range - end - - def test_daterange_values - assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 5), @first_range.date_range - assert_equal Date.new(2012, 1, 3)...Date.new(2012, 1, 4), @second_range.date_range - assert_equal Date.new(2012, 1, 3)...Float::INFINITY, @third_range.date_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.date_range) - assert_nil @empty_range.date_range - end - - def test_numrange_values - assert_equal BigDecimal.new('0.1')..BigDecimal.new('0.2'), @first_range.num_range - assert_equal BigDecimal.new('0.1')...BigDecimal.new('0.2'), @second_range.num_range - assert_equal BigDecimal.new('0.1')...BigDecimal.new('Infinity'), @third_range.num_range - assert_equal BigDecimal.new('-Infinity')...BigDecimal.new('Infinity'), @fourth_range.num_range - assert_nil @empty_range.num_range - end - - def test_tsrange_values - tz = ::ActiveRecord::Base.default_timezone - assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)..Time.send(tz, 2011, 1, 1, 14, 30, 0), @first_range.ts_range - assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 1, 1, 14, 30, 0), @second_range.ts_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.ts_range) - assert_nil @empty_range.ts_range - end - - def test_tstzrange_values - assert_equal Time.parse('2010-01-01 09:30:00 UTC')..Time.parse('2011-01-01 17:30:00 UTC'), @first_range.tstz_range - assert_equal Time.parse('2010-01-01 09:30:00 UTC')...Time.parse('2011-01-01 17:30:00 UTC'), @second_range.tstz_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.tstz_range) - assert_nil @empty_range.tstz_range - end - - def test_create_tstzrange - tstzrange = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2011-02-02 14:30:00 CDT') - range = PostgresqlRange.new(:tstz_range => tstzrange) - assert range.save - assert range.reload - assert_equal range.tstz_range, tstzrange - assert_equal range.tstz_range, Time.parse('2010-01-01 13:30:00 UTC')...Time.parse('2011-02-02 19:30:00 UTC') - end - - def test_update_tstzrange - new_tstzrange = Time.parse('2010-01-01 14:30:00 CDT')...Time.parse('2011-02-02 14:30:00 CET') - @first_range.tstz_range = new_tstzrange - assert @first_range.save - assert @first_range.reload - assert_equal new_tstzrange, @first_range.tstz_range - @first_range.tstz_range = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2010-01-01 13:30:00 +0000') - assert @first_range.save - assert @first_range.reload - assert_nil @first_range.tstz_range - end - - def test_create_tsrange - tz = ::ActiveRecord::Base.default_timezone - tsrange = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0) - range = PostgresqlRange.new(:ts_range => tsrange) - assert range.save - assert range.reload - assert_equal range.ts_range, tsrange - end - - def test_update_tsrange - tz = ::ActiveRecord::Base.default_timezone - new_tsrange = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0) - @first_range.ts_range = new_tsrange - assert @first_range.save - assert @first_range.reload - assert_equal new_tsrange, @first_range.ts_range - @first_range.ts_range = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0) - assert @first_range.save - assert @first_range.reload - assert_nil @first_range.ts_range - end - - def test_create_numrange - numrange = BigDecimal.new('0.5')...BigDecimal.new('1') - range = PostgresqlRange.new(:num_range => numrange) - assert range.save - assert range.reload - assert_equal range.num_range, numrange - end - - def test_update_numrange - new_numrange = BigDecimal.new('0.5')...BigDecimal.new('1') - @first_range.num_range = new_numrange - assert @first_range.save - assert @first_range.reload - assert_equal new_numrange, @first_range.num_range - @first_range.num_range = BigDecimal.new('0.5')...BigDecimal.new('0.5') - assert @first_range.save - assert @first_range.reload - assert_nil @first_range.num_range - end - - def test_create_daterange - daterange = Range.new(Date.new(2012, 1, 1), Date.new(2013, 1, 1), true) - range = PostgresqlRange.new(:date_range => daterange) - assert range.save - assert range.reload - assert_equal range.date_range, daterange - end - - def test_update_daterange - new_daterange = Date.new(2012, 2, 3)...Date.new(2012, 2, 10) - @first_range.date_range = new_daterange - assert @first_range.save - assert @first_range.reload - assert_equal new_daterange, @first_range.date_range - @first_range.date_range = Date.new(2012, 2, 3)...Date.new(2012, 2, 3) - assert @first_range.save - assert @first_range.reload - assert_nil @first_range.date_range - end - - def test_create_int4range - int4range = Range.new(3, 50, true) - range = PostgresqlRange.new(:int4_range => int4range) - assert range.save - assert range.reload - assert_equal range.int4_range, int4range - end - - def test_update_int4range - new_int4range = 6...10 - @first_range.int4_range = new_int4range - assert @first_range.save - assert @first_range.reload - assert_equal new_int4range, @first_range.int4_range - @first_range.int4_range = 3...3 - assert @first_range.save - assert @first_range.reload - assert_nil @first_range.int4_range - end - - def test_create_int8range - int8range = Range.new(30, 50, true) - range = PostgresqlRange.new(:int8_range => int8range) - assert range.save - assert range.reload - assert_equal range.int8_range, int8range - end - - def test_update_int8range - new_int8range = 60000...10000000 - @first_range.int8_range = new_int8range - assert @first_range.save - assert @first_range.reload - assert_equal new_int8range, @first_range.int8_range - @first_range.int8_range = 39999...39999 - assert @first_range.save - assert @first_range.reload - assert_nil @first_range.int8_range - end - end - def test_money_values assert_equal 567.89, @first_money.wealth assert_equal(-567.89, @second_money.wealth) diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index 2845413575..6df5d8f533 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -206,6 +206,16 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase def test_multiline assert_cycle("a\nb" => "c\nd") end + + def test_update_all + hstore = Hstore.create! tags: { "one" => "two" } + + Hstore.update_all tags: { "three" => "four" } + assert_equal({ "three" => "four" }, hstore.reload.tags) + + Hstore.update_all tags: { } + assert_equal({ }, hstore.reload.tags) + end end private diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index c33c7ef968..01e7334aad 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -121,4 +121,14 @@ class PostgresqlJSONTest < ActiveRecord::TestCase x = JsonDataType.first assert_equal "640×1136", x.resolution end + + def test_update_all + json = JsonDataType.create! payload: { "one" => "two" } + + JsonDataType.update_all payload: { "three" => "four" } + assert_equal({ "three" => "four" }, json.reload.payload) + + JsonDataType.update_all payload: { } + assert_equal({ }, json.reload.payload) + end end diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index 8b017760b1..778175e2e8 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -10,6 +10,13 @@ module ActiveRecord @connection.exec_query('create table ex(id serial primary key, number integer, data character varying(255))') end + def test_bad_connection + assert_raise ActiveRecord::NoDatabaseError do + connection = ActiveRecord::Base.postgresql_connection(database: "should_not_exist-cinco-dog-db", adapter: "postgresql") + connection.exec_query('drop table if exists ex') + end + end + def test_valid_column column = @connection.columns('ex').find { |col| col.name == 'id' } assert @connection.valid_type?(column.type) @@ -62,6 +69,18 @@ module ActiveRecord assert_equal expect, id end + def test_multiline_insert_sql + id = @connection.insert_sql(<<-SQL) + insert into ex( + number) + values( + 5152 + ) + SQL + expect = @connection.query('select max(id) from ex').first.first + assert_equal expect, id + end + def test_insert_sql_with_returning_disabled connection = connection_without_insert_returning id = connection.insert_sql("insert into postgresql_partitioned_table_parent (number) VALUES (1)") diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb new file mode 100644 index 0000000000..a56b8ac791 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -0,0 +1,245 @@ +require "cases/helper" +require 'active_record/base' +require 'active_record/connection_adapters/postgresql_adapter' + +if ActiveRecord::Base.connection.supports_ranges? + class PostgresqlRange < ActiveRecord::Base + self.table_name = "postgresql_ranges" + end + + class PostgresqlRangeTest < ActiveRecord::TestCase + def teardown + @connection.execute 'DROP TABLE IF EXISTS postgresql_ranges' + end + + def setup + @connection = ActiveRecord::Base.connection + begin + @connection.transaction do + @connection.create_table('postgresql_ranges') do |t| + t.daterange :date_range + t.numrange :num_range + t.tsrange :ts_range + t.tstzrange :tstz_range + t.int4range :int4_range + t.int8range :int8_range + end + end + rescue ActiveRecord::StatementInvalid + return skip "do not test on PG without range" + end + + insert_range(id: 101, + date_range: "[''2012-01-02'', ''2012-01-04'']", + num_range: "[0.1, 0.2]", + ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'']", + tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']", + int4_range: "[1, 10]", + int8_range: "[10, 100]") + + insert_range(id: 102, + date_range: "(''2012-01-02'', ''2012-01-04'')", + num_range: "[0.1, 0.2)", + ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'')", + tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'')", + int4_range: "(1, 10)", + int8_range: "(10, 100)") + + insert_range(id: 103, + date_range: "(''2012-01-02'',]", + num_range: "[0.1,]", + ts_range: "[''2010-01-01 14:30'',]", + tstz_range: "[''2010-01-01 14:30:00+05'',]", + int4_range: "(1,]", + int8_range: "(10,]") + + insert_range(id: 104, + date_range: "[,]", + num_range: "[,]", + ts_range: "[,]", + tstz_range: "[,]", + int4_range: "[,]", + int8_range: "[,]") + + insert_range(id: 105, + date_range: "(''2012-01-02'', ''2012-01-02'')", + num_range: "(0.1, 0.1)", + ts_range: "(''2010-01-01 14:30'', ''2010-01-01 14:30'')", + tstz_range: "(''2010-01-01 14:30:00+05'', ''2010-01-01 06:30:00-03'')", + int4_range: "(1, 1)", + int8_range: "(10, 10)") + + @new_range = PostgresqlRange.new + @first_range = PostgresqlRange.find(101) + @second_range = PostgresqlRange.find(102) + @third_range = PostgresqlRange.find(103) + @fourth_range = PostgresqlRange.find(104) + @empty_range = PostgresqlRange.find(105) + end + + def test_data_type_of_range_types + assert_equal :daterange, @first_range.column_for_attribute(:date_range).type + assert_equal :numrange, @first_range.column_for_attribute(:num_range).type + assert_equal :tsrange, @first_range.column_for_attribute(:ts_range).type + assert_equal :tstzrange, @first_range.column_for_attribute(:tstz_range).type + assert_equal :int4range, @first_range.column_for_attribute(:int4_range).type + assert_equal :int8range, @first_range.column_for_attribute(:int8_range).type + end + + def test_int4range_values + assert_equal 1...11, @first_range.int4_range + assert_equal 2...10, @second_range.int4_range + assert_equal 2...Float::INFINITY, @third_range.int4_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int4_range) + assert_nil @empty_range.int4_range + end + + def test_int8range_values + assert_equal 10...101, @first_range.int8_range + assert_equal 11...100, @second_range.int8_range + assert_equal 11...Float::INFINITY, @third_range.int8_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int8_range) + assert_nil @empty_range.int8_range + end + + def test_daterange_values + assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 5), @first_range.date_range + assert_equal Date.new(2012, 1, 3)...Date.new(2012, 1, 4), @second_range.date_range + assert_equal Date.new(2012, 1, 3)...Float::INFINITY, @third_range.date_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.date_range) + assert_nil @empty_range.date_range + end + + def test_numrange_values + assert_equal BigDecimal.new('0.1')..BigDecimal.new('0.2'), @first_range.num_range + assert_equal BigDecimal.new('0.1')...BigDecimal.new('0.2'), @second_range.num_range + assert_equal BigDecimal.new('0.1')...BigDecimal.new('Infinity'), @third_range.num_range + assert_equal BigDecimal.new('-Infinity')...BigDecimal.new('Infinity'), @fourth_range.num_range + assert_nil @empty_range.num_range + end + + def test_tsrange_values + tz = ::ActiveRecord::Base.default_timezone + assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)..Time.send(tz, 2011, 1, 1, 14, 30, 0), @first_range.ts_range + assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 1, 1, 14, 30, 0), @second_range.ts_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.ts_range) + assert_nil @empty_range.ts_range + end + + def test_tstzrange_values + assert_equal Time.parse('2010-01-01 09:30:00 UTC')..Time.parse('2011-01-01 17:30:00 UTC'), @first_range.tstz_range + assert_equal Time.parse('2010-01-01 09:30:00 UTC')...Time.parse('2011-01-01 17:30:00 UTC'), @second_range.tstz_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.tstz_range) + assert_nil @empty_range.tstz_range + end + + def test_create_tstzrange + tstzrange = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2011-02-02 14:30:00 CDT') + round_trip(@new_range, :tstz_range, tstzrange) + assert_equal @new_range.tstz_range, tstzrange + assert_equal @new_range.tstz_range, Time.parse('2010-01-01 13:30:00 UTC')...Time.parse('2011-02-02 19:30:00 UTC') + end + + def test_update_tstzrange + assert_equal_round_trip(@first_range, :tstz_range, + Time.parse('2010-01-01 14:30:00 CDT')...Time.parse('2011-02-02 14:30:00 CET')) + assert_nil_round_trip(@first_range, :tstz_range, + Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2010-01-01 13:30:00 +0000')) + end + + def test_create_tsrange + tz = ::ActiveRecord::Base.default_timezone + assert_equal_round_trip(@new_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0)) + end + + def test_update_tsrange + tz = ::ActiveRecord::Base.default_timezone + assert_equal_round_trip(@first_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0)) + assert_nil_round_trip(@first_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0)) + end + + def test_create_numrange + assert_equal_round_trip(@new_range, :num_range, + BigDecimal.new('0.5')...BigDecimal.new('1')) + end + + def test_update_numrange + assert_equal_round_trip(@first_range, :num_range, + BigDecimal.new('0.5')...BigDecimal.new('1')) + assert_nil_round_trip(@first_range, :num_range, + BigDecimal.new('0.5')...BigDecimal.new('0.5')) + end + + def test_create_daterange + assert_equal_round_trip(@new_range, :date_range, + Range.new(Date.new(2012, 1, 1), Date.new(2013, 1, 1), true)) + end + + def test_update_daterange + assert_equal_round_trip(@first_range, :date_range, + Date.new(2012, 2, 3)...Date.new(2012, 2, 10)) + assert_nil_round_trip(@first_range, :date_range, + Date.new(2012, 2, 3)...Date.new(2012, 2, 3)) + end + + def test_create_int4range + assert_equal_round_trip(@new_range, :int4_range, Range.new(3, 50, true)) + end + + def test_update_int4range + assert_equal_round_trip(@first_range, :int4_range, 6...10) + assert_nil_round_trip(@first_range, :int4_range, 3...3) + end + + def test_create_int8range + assert_equal_round_trip(@new_range, :int8_range, Range.new(30, 50, true)) + end + + def test_update_int8range + assert_equal_round_trip(@first_range, :int8_range, 60000...10000000) + assert_nil_round_trip(@first_range, :int8_range, 39999...39999) + end + + private + def assert_equal_round_trip(range, attribute, value) + round_trip(range, attribute, value) + assert_equal value, range.public_send(attribute) + end + + def assert_nil_round_trip(range, attribute, value) + round_trip(range, attribute, value) + assert_nil range.public_send(attribute) + end + + def round_trip(range, attribute, value) + range.public_send "#{attribute}=", value + assert range.save + assert range.reload + end + + def insert_range(values) + @connection.execute <<-SQL + INSERT INTO postgresql_ranges ( + id, + date_range, + num_range, + ts_range, + tstz_range, + int4_range, + int8_range + ) VALUES ( + #{values[:id]}, + '#{values[:date_range]}', + '#{values[:num_range]}', + '#{values[:ts_range]}', + '#{values[:tstz_range]}', + '#{values[:int4_range]}', + '#{values[:int8_range]}' + ) + SQL + end + end +end diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index a7b2764fc1..ba89487838 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -95,6 +95,13 @@ module ActiveRecord end }.new assert_equal 10, @conn.type_cast(quoted_id_obj, nil) + + quoted_id_obj = Class.new { + def quoted_id + "'zomg'" + end + } + assert_raise(TypeError) { @conn.type_cast(quoted_id_obj, nil) } end end end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index ce7c869eec..0598ff25f8 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -1,6 +1,7 @@ # encoding: utf-8 require "cases/helper" require 'models/owner' +require 'tempfile' module ActiveRecord module ConnectionAdapters @@ -25,6 +26,34 @@ module ActiveRecord ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber) end + def test_bad_connection + assert_raise ActiveRecord::NoDatabaseError do + connection = ActiveRecord::Base.sqlite3_connection(adapter: "sqlite3", database: "/tmp/should/_not/_exist/-cinco-dog.db") + connection.exec_query('drop table if exists ex') + end + end + + def test_connect_with_url + original_connection = ActiveRecord::Base.remove_connection + tf = Tempfile.open 'whatever' + url = "sqlite3://#{tf.path}" + ActiveRecord::Base.establish_connection(url) + assert ActiveRecord::Base.connection + ensure + tf.close + tf.unlink + ActiveRecord::Base.establish_connection(original_connection) + end + + def test_connect_memory_with_url + original_connection = ActiveRecord::Base.remove_connection + url = "sqlite3:///:memory:" + ActiveRecord::Base.establish_connection(url) + assert ActiveRecord::Base.connection + ensure + ActiveRecord::Base.establish_connection(original_connection) + end + def test_valid_column column = @conn.columns('items').find { |col| col.name == 'id' } assert @conn.valid_type?(column.type) diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 7c913bc78b..3205d0c28b 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -356,6 +356,14 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_queries(2) { line_item.destroy } end + def test_belongs_to_with_touch_option_on_destroy_with_destroyed_parent + line_item = LineItem.create! + invoice = Invoice.create!(line_items: [line_item]) + invoice.destroy + + assert_queries(1) { line_item.destroy } + end + def test_belongs_to_with_touch_option_on_touch_and_reassigned_parent line_item = LineItem.create! Invoice.create!(line_items: [line_item]) @@ -578,6 +586,19 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_nil essay.writer_id end + def test_polymorphic_assignment_with_nil + essay = Essay.new + assert_nil essay.writer_id + assert_nil essay.writer_type + + essay.writer_id = 1 + essay.writer_type = 'Author' + + essay.writer = nil + assert_nil essay.writer_id + assert_nil essay.writer_type + end + def test_belongs_to_proxy_should_not_respond_to_private_methods assert_raise(NoMethodError) { companies(:first_firm).private_method } assert_raise(NoMethodError) { companies(:second_client).firm.private_method } diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb index f8f2832ab1..4c1fdfdd9a 100644 --- a/activerecord/test/cases/associations/extension_test.rb +++ b/activerecord/test/cases/associations/extension_test.rb @@ -75,6 +75,7 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase private def extend!(model) - ActiveRecord::Associations::Builder::HasMany.define_extensions(model, :association_name) { } + builder = ActiveRecord::Associations::Builder::HasMany.new(model, :association_name, nil, {}) { } + builder.define_extensions(model) end end diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index be928ec8ee..8aee7ff40e 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -570,6 +570,13 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert !developer.special_projects.include?(other_project) end + def test_symbol_join_table + developer = Developer.first + sp = developer.sym_special_projects.create("name" => "omg") + developer.reload + assert_includes developer.sym_special_projects, sp + end + def test_update_attributes_after_push_without_duplicate_join_table_rows developer = Developer.new("name" => "Kano") project = SpecialProject.create("name" => "Special Project") diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index bfb80afa61..c6291e8aa4 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -318,9 +318,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_belongs_to_sanity c = Client.new - assert_nil c.firm - - flunk "belongs_to failed if check" if c.firm + assert_nil c.firm, "belongs_to failed sanity check on new object" end def test_find_ids @@ -1781,12 +1779,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [original_child], car.reload.failed_bulbs end - + test 'updates counter cache when default scope is given' do topic = DefaultRejectedTopic.create approved: true assert_difference "topic.reload.replies_count", 1 do topic.approved_replies.create! end - end + end end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index cde188f6c3..cb8e564da1 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -121,6 +121,10 @@ class BasicsTest < ActiveRecord::TestCase assert_equal 1, Topic.limit(1).to_a.length end + def test_limit_should_take_value_from_latest_limit + assert_equal 1, Topic.limit(2).limit(1).to_a.length + end + def test_invalid_limit assert_raises(ArgumentError) do Topic.limit("asdfadf").to_a diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb index c8dfc3244b..ba8440a16b 100644 --- a/activerecord/test/cases/connection_specification/resolver_test.rb +++ b/activerecord/test/cases/connection_specification/resolver_test.rb @@ -4,58 +4,74 @@ module ActiveRecord module ConnectionAdapters class ConnectionSpecification class ResolverTest < ActiveRecord::TestCase - def resolve(spec) - Resolver.new(spec, {}).spec.config + def resolve(spec, config={}) + Resolver.new(config).resolve(spec) + end + + def spec(spec, config={}) + Resolver.new(config).spec(spec) end def test_url_invalid_adapter - assert_raises(LoadError) do - resolve 'ridiculous://foo?encoding=utf8' + error = assert_raises(LoadError) do + spec 'ridiculous://foo?encoding=utf8' end + + assert_match "Could not load 'active_record/connection_adapters/ridiculous_adapter'", error.message end # The abstract adapter is used simply to bypass the bit of code that # checks that the adapter file can be required in. + def test_url_from_environment + spec = resolve :production, 'production' => 'abstract://foo?encoding=utf8' + assert_equal({ + "adapter" => "abstract", + "host" => "foo", + "encoding" => "utf8" }, spec) + end + def test_url_host_no_db spec = resolve 'abstract://foo?encoding=utf8' assert_equal({ - adapter: "abstract", - host: "foo", - encoding: "utf8" }, spec) + "adapter" => "abstract", + "host" => "foo", + "encoding" => "utf8" }, spec) end def test_url_host_db spec = resolve 'abstract://foo/bar?encoding=utf8' assert_equal({ - adapter: "abstract", - database: "bar", - host: "foo", - encoding: "utf8" }, spec) + "adapter" => "abstract", + "database" => "bar", + "host" => "foo", + "encoding" => "utf8" }, spec) end def test_url_port spec = resolve 'abstract://foo:123?encoding=utf8' assert_equal({ - adapter: "abstract", - port: 123, - host: "foo", - encoding: "utf8" }, spec) + "adapter" => "abstract", + "port" => 123, + "host" => "foo", + "encoding" => "utf8" }, spec) end def test_encoded_password password = 'am@z1ng_p@ssw0rd#!' encoded_password = URI.encode_www_form_component(password) spec = resolve "abstract://foo:#{encoded_password}@localhost/bar" - assert_equal password, spec[:password] + assert_equal password, spec["password"] end - def test_descriptive_error_message_when_adapter_is_missing - error = assert_raise(LoadError) do - resolve(adapter: 'non-existing') - end + def test_url_host_db_for_sqlite3 + spec = resolve 'sqlite3://foo:bar@dburl:9000/foo_test' + assert_equal('/foo_test', spec["database"]) + end - assert_match "Could not load 'active_record/connection_adapters/non-existing_adapter'", error.message + def test_url_host_memory_db_for_sqlite3 + spec = resolve 'sqlite3://foo:bar@dburl:9000/:memory:' + assert_equal(':memory:', spec["database"]) end end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 5e8f1c851f..5125d5df2a 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -12,6 +12,7 @@ require 'models/developer' require 'models/customer' require 'models/toy' require 'models/matey' +require 'models/dog' class FinderTest < ActiveRecord::TestCase fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers, :categories, :categorizations @@ -641,6 +642,13 @@ class FinderTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordNotFound) { Topic.find_by_title!("The First Topic!") } end + def test_find_by_on_attribute_that_is_a_reserved_word + dog_alias = 'Dog' + dog = Dog.create(alias: dog_alias) + + assert_equal dog, Dog.find_by_alias(dog_alias) + end + def test_find_by_one_attribute_that_is_an_alias assert_equal topics(:first), Topic.find_by_heading("The First Topic") assert_nil Topic.find_by_heading("The First Topic!") @@ -847,7 +855,7 @@ class FinderTest < ActiveRecord::TestCase def test_with_limiting_with_custom_select posts = Post.references(:authors).merge( - :includes => :author, :select => ' posts.*, authors.id as "author_id"', + :includes => :author, :select => 'posts.*, authors.id as "author_id"', :limit => 3, :order => 'posts.id' ).to_a assert_equal 3, posts.size diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 34e8f1be0f..3758224b0c 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -20,6 +20,9 @@ Thread.abort_on_exception = true # Show backtraces for deprecated behavior for quicker cleanup. ActiveSupport::Deprecation.debug = true +# Disable available locale checks to avoid warnings running the test suite. +I18n.enforce_available_locales = false + # Connect to the database ARTest.connect @@ -149,23 +152,6 @@ end load_schema -class << Time - unless method_defined? :now_before_time_travel - alias_method :now_before_time_travel, :now - end - - def now - (@now ||= nil) || now_before_time_travel - end - - def travel_to(time, &block) - @now = time - block.call - ensure - @now = nil - end -end - class SQLSubscriber attr_reader :logged attr_reader :payloads @@ -183,7 +169,6 @@ class SQLSubscriber def finish(name, id, payload); end end - module InTimeZone private diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb index 07ffcef875..2e71b1a40d 100644 --- a/activerecord/test/cases/integration_test.rb +++ b/activerecord/test/cases/integration_test.rb @@ -1,3 +1,5 @@ +# encoding: utf-8 + require 'cases/helper' require 'models/company' require 'models/developer' @@ -46,6 +48,12 @@ class IntegrationTest < ActiveRecord::TestCase assert_equal '4-ab-ab-ab-ab-ab-ab', firm.to_param end + def test_to_param_class_method_multibyte_character + firm = Firm.find(4) + firm.name = "戦場ヶ原 ひたぎ" + assert_equal '4', firm.to_param + end + def test_to_param_class_method_uses_default_if_blank firm = Firm.find(4) firm.name = nil diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb index 8065541bfe..c1d7cd5874 100644 --- a/activerecord/test/cases/migration/change_table_test.rb +++ b/activerecord/test/cases/migration/change_table_test.rb @@ -5,7 +5,7 @@ module ActiveRecord class Migration class TableTest < ActiveRecord::TestCase def setup - @connection = MiniTest::Mock.new + @connection = Minitest::Mock.new end def teardown diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 7c3988859f..ebcc778b8b 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -443,6 +443,32 @@ class MigrationTest < ActiveRecord::TestCase Person.connection.drop_table :binary_testings rescue nil end + def test_create_table_with_query + Person.connection.drop_table :table_from_query_testings rescue nil + Person.connection.create_table(:person, force: true) + + Person.connection.create_table :table_from_query_testings, as: "SELECT id FROM person" + + columns = Person.connection.columns(:table_from_query_testings) + assert_equal 1, columns.length + assert_equal "id", columns.first.name + + Person.connection.drop_table :table_from_query_testings rescue nil + end + + def test_create_table_with_query_from_relation + Person.connection.drop_table :table_from_query_testings rescue nil + Person.connection.create_table(:person, force: true) + + Person.connection.create_table :table_from_query_testings, as: Person.select(:id) + + columns = Person.connection.columns(:table_from_query_testings) + assert_equal 1, columns.length + assert_equal "id", columns.first.name + + Person.connection.drop_table :table_from_query_testings rescue nil + end + if current_adapter? :OracleAdapter def test_create_table_with_custom_sequence_name # table name is 29 chars, the standard sequence name will @@ -743,7 +769,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps" @existing_migrations = Dir[@migrations_path + "/*.rb"] - Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do + travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") @@ -768,7 +794,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy_with_timestamps" sources[:omg] = MIGRATIONS_ROOT + "/to_copy_with_timestamps2" - Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do + travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do copied = ActiveRecord::Migration.copy(@migrations_path, sources) assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") @@ -788,7 +814,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps" @existing_migrations = Dir[@migrations_path + "/*.rb"] - Time.travel_to(Time.utc(2010, 2, 20, 10, 10, 10)) do + travel_to(Time.utc(2010, 2, 20, 10, 10, 10)) do ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) assert File.exist?(@migrations_path + "/20100301010102_people_have_hobbies.bukkits.rb") assert File.exist?(@migrations_path + "/20100301010103_people_have_descriptions.bukkits.rb") @@ -863,7 +889,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase @migrations_path = MIGRATIONS_ROOT + "/non_existing" @existing_migrations = [] - Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do + travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") @@ -878,7 +904,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase @migrations_path = MIGRATIONS_ROOT + "/empty" @existing_migrations = [] - Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do + travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb index 2e386a172a..3831de6ae3 100644 --- a/activerecord/test/cases/multiple_db_test.rb +++ b/activerecord/test/cases/multiple_db_test.rb @@ -101,7 +101,7 @@ class MultipleDbTest < ActiveRecord::TestCase College.first.courses.first end ensure - ActiveRecord::Base.establish_connection 'arunit' + ActiveRecord::Base.establish_connection :arunit end end end diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index 13677797cf..9b2bfed039 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -6,95 +6,63 @@ module ActiveRecord class DelegationTest < ActiveRecord::TestCase fixtures :posts - def assert_responds(target, method) - assert target.respond_to?(method) - assert_nothing_raised do - method_arity = target.to_a.method(method).arity + def call_method(target, method) + method_arity = target.to_a.method(method).arity - if method_arity.zero? - target.send(method) - elsif method_arity < 0 - if method == :shuffle! - target.send(method) - else - target.send(method, 1) - end + if method_arity.zero? + target.public_send(method) + elsif method_arity < 0 + if method == :shuffle! + target.public_send(method) else - raise NotImplementedError + target.public_send(method, 1) end + elsif method_arity == 1 + target.public_send(method, 1) + else + raise NotImplementedError end end end - class DelegationAssociationTest < DelegationTest - def target - Post.first.comments - end + module DelegationWhitelistBlacklistTests + ARRAY_DELEGATES = [ + :+, :-, :|, :&, :[], + :all?, :collect, :detect, :each, :each_cons, :each_with_index, + :exclude?, :find_all, :flat_map, :group_by, :include?, :length, + :map, :none?, :one?, :partition, :reject, :reverse, + :sample, :second, :sort, :sort_by, :third, + :to_ary, :to_set, :to_xml, :to_yaml + ] - [:map, :collect].each do |method| - test "##{method} is delegated" do - assert_responds(target, method) - assert_equal(target.pluck(:body), target.send(method) {|post| post.body }) - end - - test "##{method}! is not delegated" do - assert_deprecated do - assert_responds(target, "#{method}!") - end + ARRAY_DELEGATES.each do |method| + define_method "test_delegates_#{method}_to_Array" do + assert_respond_to target, method end end - [:compact!, :flatten!, :reject!, :reverse!, :rotate!, - :shuffle!, :slice!, :sort!, :sort_by!].each do |method| - test "##{method} delegation is deprecated" do - assert_deprecated do - assert_responds(target, method) - end - end - end - - [:select!, :uniq!].each do |method| - test "##{method} is implemented" do - assert_responds(target, method) + ActiveRecord::Delegation::BLACKLISTED_ARRAY_METHODS.each do |method| + define_method "test_#{method}_is_not_delegated_to_Array" do + assert_raises(NoMethodError) { call_method(target, method) } end end end - class DelegationRelationTest < DelegationTest - fixtures :comments + class DelegationAssociationTest < DelegationTest + include DelegationWhitelistBlacklistTests def target - Comment.where(body: 'Normal type') + Post.first.comments end + end - [:map, :collect].each do |method| - test "##{method} is delegated" do - assert_responds(target, method) - assert_equal(target.pluck(:body), target.send(method) {|post| post.body }) - end - - test "##{method}! is not delegated" do - assert_deprecated do - assert_responds(target, "#{method}!") - end - end - end + class DelegationRelationTest < DelegationTest + include DelegationWhitelistBlacklistTests - [:compact!, :flatten!, :reject!, :reverse!, :rotate!, - :shuffle!, :slice!, :sort!, :sort_by!].each do |method| - test "##{method} delegation is deprecated" do - assert_deprecated do - assert_responds(target, method) - end - end - end + fixtures :comments - [:select!, :uniq!].each do |method| - test "##{method} triggers an immutable error" do - assert_raises ActiveRecord::ImmutableRelation do - assert_responds(target, method) - end - end + def target + Comment.all end end end diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index 56e4605ccc..937f226b1d 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -35,6 +35,21 @@ module ActiveRecord assert_equal Post.where(author_id: 1).to_sql, Post.where(author: author).to_sql end + def test_belongs_to_nil_where + assert_equal Post.where(author_id: nil).to_sql, Post.where(author: nil).to_sql + end + + def test_belongs_to_array_value_where + assert_equal Post.where(author_id: [1,2]).to_sql, Post.where(author: [1,2]).to_sql + end + + def test_belongs_to_nested_relation_where + expected = Post.where(author_id: Author.where(id: [1,2])).to_sql + actual = Post.where(author: Author.where(id: [1,2])).to_sql + + assert_equal expected, actual + end + def test_belongs_to_nested_where parent = Comment.new parent.id = 1 @@ -55,6 +70,25 @@ module ActiveRecord assert_equal expected.to_sql, actual.to_sql end + def test_polymorphic_nested_array_where + treasure = Treasure.new + treasure.id = 1 + hidden = HiddenTreasure.new + hidden.id = 2 + + expected = PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: [treasure, hidden]) + actual = PriceEstimate.where(estimate_of: [treasure, hidden]) + + assert_equal expected.to_sql, actual.to_sql + end + + def test_polymorphic_nested_relation_where + expected = PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: Treasure.where(id: [1,2])) + actual = PriceEstimate.where(estimate_of: Treasure.where(id: [1,2])) + + assert_equal expected.to_sql, actual.to_sql + end + def test_polymorphic_sti_shallow_where treasure = HiddenTreasure.new treasure.id = 1 diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index baa3acf3fb..031da8e6d6 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -151,6 +151,11 @@ class RelationTest < ActiveRecord::TestCase assert_equal relation.to_a, Comment.select('a.*').from(relation, :a).to_a end + def test_finding_with_subquery_without_select + relation = Topic.where(:approved => true) + assert_equal relation.to_a, Topic.from(relation).to_a + end + def test_finding_with_conditions assert_equal ["David"], Author.where(:name => 'David').map(&:name) assert_equal ['Mary'], Author.where(["name = ?", 'Mary']).map(&:name) @@ -295,7 +300,7 @@ class RelationTest < ActiveRecord::TestCase def test_null_relation_calculations_methods assert_no_queries do assert_equal 0, Developer.none.count - assert_equal 0, Developer.none.calculate(:count, nil) + assert_equal 0, Developer.none.calculate(:count, nil, {}) assert_equal nil, Developer.none.calculate(:average, 'salary') end end @@ -1506,26 +1511,55 @@ class RelationTest < ActiveRecord::TestCase assert !Post.all.respond_to?(:by_lifo) end - class OMGTopic < ActiveRecord::Base - self.table_name = 'topics' + test "merge collapses wheres from the LHS only" do + left = Post.where(title: "omg").where(comments_count: 1) + right = Post.where(title: "wtf").where(title: "bbq") - def self.__omg__ - "omgtopic" - end + expected = [left.where_values[1]] + right.where_values + merged = left.merge(right) + + assert_equal expected, merged.where_values + assert !merged.to_sql.include?("omg") + assert merged.to_sql.include?("wtf") + assert merged.to_sql.include?("bbq") end - test "delegations do not clash across classes" do - begin - class ::Array - def __omg__ - "array" - end - end + def test_merging_removes_rhs_bind_parameters + left = Post.where(id: Arel::Nodes::BindParam.new('?')) + column = Post.columns_hash['id'] + left.bind_values += [[column, 20]] + right = Post.where(id: 10) - assert_equal "array", Topic.all.__omg__ - assert_equal "omgtopic", OMGTopic.all.__omg__ - ensure - Array.send(:remove_method, :__omg__) - end + merged = left.merge(right) + assert_equal [], merged.bind_values + end + + def test_merging_keeps_lhs_bind_parameters + column = Post.columns_hash['id'] + binds = [[column, 20]] + + right = Post.where(id: Arel::Nodes::BindParam.new('?')) + right.bind_values += binds + left = Post.where(id: 10) + + merged = left.merge(right) + assert_equal binds, merged.bind_values + end + + def test_merging_reorders_bind_params + post = Post.first + id_column = Post.columns_hash['id'] + title_column = Post.columns_hash['title'] + + bv = Post.connection.substitute_at id_column, 0 + + right = Post.where(id: bv) + right.bind_values += [[id_column, post.id]] + + left = Post.where(title: bv) + left.bind_values += [[title_column, post.title]] + + merged = left.merge(right) + assert_equal post, merged.first end end diff --git a/activerecord/test/cases/transaction_isolation_test.rb b/activerecord/test/cases/transaction_isolation_test.rb index 84c16fb109..f89c26532d 100644 --- a/activerecord/test/cases/transaction_isolation_test.rb +++ b/activerecord/test/cases/transaction_isolation_test.rb @@ -28,8 +28,8 @@ if ActiveRecord::Base.connection.supports_transaction_isolation? end setup do - Tag.establish_connection 'arunit' - Tag2.establish_connection 'arunit' + Tag.establish_connection :arunit + Tag2.establish_connection :arunit Tag.destroy_all end diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index 5f55696c1d..de618902aa 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -56,13 +56,11 @@ class ValidationsTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordInvalid) { WrongReply.create! } assert_raise(ActiveRecord::RecordInvalid) { WrongReply.new.save! } - begin - r = WrongReply.new + r = WrongReply.new + invalid = assert_raise ActiveRecord::RecordInvalid do r.save! - flunk - rescue ActiveRecord::RecordInvalid => invalid - assert_equal r, invalid.record end + assert_equal r, invalid.record end def test_exception_on_create_bang_many diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index a26de55758..2e2d8a0d37 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -36,6 +36,10 @@ class Developer < ActiveRecord::Base end has_and_belongs_to_many :special_projects, :join_table => 'developers_projects', :association_foreign_key => 'project_id' + has_and_belongs_to_many :sym_special_projects, + :join_table => :developers_projects, + :association_foreign_key => 'project_id', + :class_name => 'SpecialProject' has_many :audit_logs has_many :contracts diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index 6b7012a172..a86a188bcf 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -1,6 +1,6 @@ ActiveRecord::Schema.define do - %w(postgresql_ranges postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings postgresql_uuids postgresql_ltrees + %w(postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings postgresql_uuids postgresql_ltrees postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent postgresql_json_data_type).each do |table_name| execute "DROP TABLE IF EXISTS #{quote_table_name table_name}" end @@ -74,18 +74,6 @@ _SQL ); _SQL - execute <<_SQL if supports_ranges? - CREATE TABLE postgresql_ranges ( - id SERIAL PRIMARY KEY, - date_range daterange, - num_range numrange, - ts_range tsrange, - tstz_range tstzrange, - int4_range int4range, - int8_range int8range - ); -_SQL - execute <<_SQL CREATE TABLE postgresql_tsvectors ( id SERIAL PRIMARY KEY, @@ -221,4 +209,3 @@ _SQL t.text :text, limit: 100_000 end end - diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 92e2e4d409..ac546fc296 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -245,6 +245,7 @@ ActiveRecord::Schema.define do t.integer :trainer_id t.integer :breeder_id t.integer :dog_lover_id + t.string :alias end create_table :edges, force: true, id: false do |t| diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb index 196b3a9493..d11fd9cfc1 100644 --- a/activerecord/test/support/connection.rb +++ b/activerecord/test/support/connection.rb @@ -15,7 +15,7 @@ module ARTest puts "Using #{connection_name}" ActiveRecord::Base.logger = ActiveSupport::Logger.new("debug.log", 0, 100 * 1024 * 1024) ActiveRecord::Base.configurations = connection_config - ActiveRecord::Base.establish_connection 'arunit' - ARUnit2Model.establish_connection 'arunit2' + ActiveRecord::Base.establish_connection :arunit + ARUnit2Model.establish_connection :arunit2 end end diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index d23b598144..3379460fa2 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,11 +1,77 @@ -* Fix file descriptor being leaked on each call to `Kernel.silence_stream` +* Fixed Float related error in NumberHelper with large precisions. - *Mario Visic* + before: + ActiveSupport::NumberHelper.number_to_rounded '3.14159', precision: 50 + #=> "3.14158999999999988261834005243144929409027099609375" + after: + ActiveSupport::NumberHelper.number_to_rounded '3.14159', precision: 50 + #=> "3.14159000000000000000000000000000000000000000000000" -* Ensure `config.i18n.enforce_available_locales` is set before any other - configuration option. + *Kenta Murata*, *Akira Matsuda* - *Yves Senn* +* Default the new `I18n.enforce_available_locales` config to `true`, meaning + `I18n` will make sure that all locales passed to it must be declared in the + `available_locales` list. + + To disable it add the following configuration to your application: + + config.i18n.enforce_available_locales = false + + This also ensures I18n configuration is properly initialized taking the new + option into account, to avoid their deprecations while booting up the app. + + *Carlos Antonio da Silva*, *Yves Senn* + +* Introduce Module#concerning: a natural, low-ceremony way to separate + responsibilities within a class. + + Imported from https://github.com/37signals/concerning#readme + + class Todo < ActiveRecord::Base + concerning :EventTracking do + included do + has_many :events + end + + def latest_event + ... + end + + private + def some_internal_method + ... + end + end + + concerning :Trashable do + def trashed? + ... + end + + def latest_event + super some_option: true + end + end + end + + is equivalent to defining these modules inline, extending them into + concerns, then mixing them in to the class. + + Inline concerns tame "junk drawer" classes that intersperse many unrelated + class-level declarations, public instance methods, and private + implementation. Coalesce related bits and give them definition. + These are a stepping stone toward future growth & refactoring. + + When to move on from an inline concern: + * Encapsulating state? Extract collaborator object. + * Encompassing more public behavior or implementation? Move to separate file. + * Sharing behavior among classes? Move to separate file. + + *Jeremy Kemper* + +* Fix file descriptor being leaked on each call to `Kernel.silence_stream`. + + *Mario Visic* * Added `Date#all_week/month/quarter/year` for generating date ranges. @@ -292,7 +358,7 @@ *Arun Agrawal* -* Remove deprecated `DateTime.local_offset` in favor of `DateTime.civil_from_fromat`. +* Remove deprecated `DateTime.local_offset` in favor of `DateTime.civil_from_format`. *Arun Agrawal* diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index 0427022dc6..f3625e8b79 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -20,7 +20,7 @@ Gem::Specification.new do |s| s.rdoc_options.concat ['--encoding', 'UTF-8'] - s.add_dependency 'i18n', '~> 0.6', '>= 0.6.4' + s.add_dependency 'i18n', '~> 0.6', '>= 0.6.9' s.add_dependency 'json', '~> 1.7', '>= 1.7.7' s.add_dependency 'tzinfo', '~> 1.1' s.add_dependency 'minitest', '~> 5.1' diff --git a/activesupport/lib/active_support/backtrace_cleaner.rb b/activesupport/lib/active_support/backtrace_cleaner.rb index c88ae3e661..d58578b7bc 100644 --- a/activesupport/lib/active_support/backtrace_cleaner.rb +++ b/activesupport/lib/active_support/backtrace_cleaner.rb @@ -22,7 +22,7 @@ module ActiveSupport # <tt>BacktraceCleaner#remove_silencers!</tt>, which will restore the # backtrace to a pristine state. If you need to reconfigure an existing # BacktraceCleaner so that it does not filter or modify the paths of any lines - # of the backtrace, you can call <tt>BacktraceCleaner#remove_filters!<tt> + # of the backtrace, you can call <tt>BacktraceCleaner#remove_filters!</tt> # These two methods will give you a completely untouched backtrace. # # Inspired by the Quiet Backtrace gem by Thoughtbot. diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index d584d50da8..53154aef27 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -401,7 +401,7 @@ module ActiveSupport end end - # Return +true+ if the cache contains an entry for the given key. + # Returns +true+ if the cache contains an entry for the given key. # # Options are passed to the underlying cache implementation. def exist?(name, options = nil) diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index 5cd6065077..8ed60aebac 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -22,15 +22,15 @@ module ActiveSupport extend Strategy::LocalCache end - # Deletes all items from the cache. In this case it deletes all the entries in the specified - # file store directory except for .gitkeep. Be careful which directory is specified in your + # Deletes all items from the cache. In this case it deletes all the entries in the specified + # file store directory except for .gitkeep. Be careful which directory is specified in your # config file when using +FileStore+ because everything in that directory will be deleted. def clear(options = nil) root_dirs = Dir.entries(cache_path).reject {|f| (EXCLUDED_DIRS + [".gitkeep"]).include?(f)} FileUtils.rm_r(root_dirs.collect{|f| File.join(cache_path, f)}) end - # Premptively iterates through all stored keys and removes the ones which have expired. + # Preemptively iterates through all stored keys and removes the ones which have expired. def cleanup(options = nil) options = merged_options(options) search_dir(cache_path) do |fname| @@ -43,33 +43,13 @@ module ActiveSupport # Increments an already existing integer value that is stored in the cache. # If the key is not found nothing is done. def increment(name, amount = 1, options = nil) - file_name = key_file_path(namespaced_key(name, options)) - lock_file(file_name) do - options = merged_options(options) - if num = read(name, options) - num = num.to_i + amount - write(name, num, options) - num - else - nil - end - end + modify_value(name, amount, options) end # Decrements an already existing integer value that is stored in the cache. # If the key is not found nothing is done. def decrement(name, amount = 1, options = nil) - file_name = key_file_path(namespaced_key(name, options)) - lock_file(file_name) do - options = merged_options(options) - if num = read(name, options) - num = num.to_i - amount - write(name, num, options) - num - else - nil - end - end + modify_value(name, -amount, options) end def delete_matched(matcher, options = nil) @@ -184,6 +164,22 @@ module ActiveSupport end end end + + # Modifies the amount of an already existing integer value that is stored in the cache. + # If the key is not found nothing is done. + def modify_value(name, amount, options) + file_name = key_file_path(namespaced_key(name, options)) + + lock_file(file_name) do + options = merged_options(options) + + if num = read(name, options) + num = num.to_i + amount + write(name, num, options) + num + end + end + end end end end diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb index 34ac91334a..8a0523d0e2 100644 --- a/activesupport/lib/active_support/cache/memory_store.rb +++ b/activesupport/lib/active_support/cache/memory_store.rb @@ -36,7 +36,7 @@ module ActiveSupport end end - # Premptively iterates through all stored keys and removes the ones which have expired. + # Preemptively iterates through all stored keys and removes the ones which have expired. def cleanup(options = nil) options = merged_options(options) instrument(:cleanup, :size => @data.size) do diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index c3aac31323..e14ece7f35 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -7,14 +7,14 @@ require 'active_support/core_ext/kernel/singleton_class' require 'thread' module ActiveSupport - # Callbacks are code hooks that are run at key points in an object's lifecycle. + # Callbacks are code hooks that are run at key points in an object's life cycle. # The typical use case is to have a base class define a set of callbacks # relevant to the other functionality it supplies, so that subclasses can # install callbacks that enhance or modify the base functionality without # needing to override or redefine methods of the base class. # # Mixing in this module allows you to define the events in the object's - # lifecycle that will support callbacks (via +ClassMethods.define_callbacks+), + # life cycle that will support callbacks (via +ClassMethods.define_callbacks+), # set the instance methods, procs, or callback objects to be called (via # +ClassMethods.set_callback+), and run the installed callbacks at the # appropriate times (via +run_callbacks+). @@ -89,7 +89,7 @@ module ActiveSupport private - # A hook invoked everytime a before callback is halted. + # A hook invoked every time a before callback is halted. # This can be overridden in AS::Callback implementors in order # to provide better debugging/logging. def halted_callback_hook(filter) @@ -577,7 +577,7 @@ module ActiveSupport # The callback can be specified as a symbol naming an instance method; as a # proc, lambda, or block; as a string to be instance evaluated; or as an # object that responds to a certain method determined by the <tt>:scope</tt> - # argument to +define_callback+. + # argument to +define_callbacks+. # # If a proc, lambda, or block is given, its body is evaluated in the context # of the current object. It can also optionally accept the current object as @@ -648,7 +648,7 @@ module ActiveSupport self.set_callbacks name, callbacks.dup.clear end - # Define sets of events in the object lifecycle that support callbacks. + # Define sets of events in the object life cycle that support callbacks. # # define_callbacks :validate # define_callbacks :initialize, :save, :destroy diff --git a/activesupport/lib/active_support/configurable.rb b/activesupport/lib/active_support/configurable.rb index e0d39d509f..3dd44e32d8 100644 --- a/activesupport/lib/active_support/configurable.rb +++ b/activesupport/lib/active_support/configurable.rb @@ -107,7 +107,7 @@ module ActiveSupport options = names.extract_options! names.each do |name| - raise NameError.new('invalid config attribute name') unless name =~ /^[_A-Za-z]\w*$/ + raise NameError.new('invalid config attribute name') unless name =~ /\A[_A-Za-z]\w*\z/ reader, reader_line = "def #{name}; config.#{name}; end", __LINE__ writer, writer_line = "def #{name}=(value); config.#{name} = value; end", __LINE__ diff --git a/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb b/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb index ff870f5fd1..c2219beb5a 100644 --- a/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb +++ b/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb @@ -4,20 +4,21 @@ require 'active_support/core_ext/module/remove_method' class Class def superclass_delegating_accessor(name, options = {}) # Create private _name and _name= methods that can still be used if the public - # methods are overridden. This allows - _superclass_delegating_accessor("_#{name}") + # methods are overridden. + _superclass_delegating_accessor("_#{name}", options) - # Generate the public methods name, name=, and name? + # Generate the public methods name, name=, and name?. # These methods dispatch to the private _name, and _name= methods, making them - # overridable + # overridable. singleton_class.send(:define_method, name) { send("_#{name}") } singleton_class.send(:define_method, "#{name}?") { !!send("_#{name}") } singleton_class.send(:define_method, "#{name}=") { |value| send("_#{name}=", value) } - # If an instance_reader is needed, generate methods for name and name= on the - # class itself, so instances will be able to see them - define_method(name) { send("_#{name}") } if options[:instance_reader] != false - define_method("#{name}?") { !!send("#{name}") } if options[:instance_reader] != false + # If an instance_reader is needed, generate public instance methods name and name?. + if options[:instance_reader] != false + define_method(name) { send("_#{name}") } + define_method("#{name}?") { !!send("#{name}") } + end end private diff --git a/activesupport/lib/active_support/core_ext/hash/except.rb b/activesupport/lib/active_support/core_ext/hash/except.rb index d90e996ad4..682d089881 100644 --- a/activesupport/lib/active_support/core_ext/hash/except.rb +++ b/activesupport/lib/active_support/core_ext/hash/except.rb @@ -1,5 +1,5 @@ class Hash - # Return a hash that includes everything but the given keys. This is useful for + # Returns a hash that includes everything but the given keys. This is useful for # limiting a set of parameters to everything but a few known toggles: # # @person.update(params[:person].except(:admin)) diff --git a/activesupport/lib/active_support/core_ext/hash/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb index 93e716585b..3d41aa8572 100644 --- a/activesupport/lib/active_support/core_ext/hash/keys.rb +++ b/activesupport/lib/active_support/core_ext/hash/keys.rb @@ -1,5 +1,5 @@ class Hash - # Return a new hash with all keys converted using the block operation. + # Returns a new hash with all keys converted using the block operation. # # hash = { name: 'Rob', age: '28' } # @@ -22,7 +22,7 @@ class Hash self end - # Return a new hash with all keys converted to strings. + # Returns a new hash with all keys converted to strings. # # hash = { name: 'Rob', age: '28' } # @@ -38,7 +38,7 @@ class Hash transform_keys!{ |key| key.to_s } end - # Return a new hash with all keys converted to symbols, as long as + # Returns a new hash with all keys converted to symbols, as long as # they respond to +to_sym+. # # hash = { 'name' => 'Rob', 'age' => '28' } @@ -73,7 +73,7 @@ class Hash end end - # Return a new hash with all keys converted by the block operation. + # Returns a new hash with all keys converted by the block operation. # This includes the keys from the root hash and from all # nested hashes. # @@ -100,7 +100,7 @@ class Hash self end - # Return a new hash with all keys converted to strings. + # Returns a new hash with all keys converted to strings. # This includes the keys from the root hash and from all # nested hashes. # @@ -119,7 +119,7 @@ class Hash deep_transform_keys!{ |key| key.to_s } end - # Return a new hash with all keys converted to symbols, as long as + # Returns a new hash with all keys converted to symbols, as long as # they respond to +to_sym+. This includes the keys from the root hash # and from all nested hashes. # diff --git a/activesupport/lib/active_support/core_ext/module.rb b/activesupport/lib/active_support/core_ext/module.rb index f2d4887df6..b4efff8b24 100644 --- a/activesupport/lib/active_support/core_ext/module.rb +++ b/activesupport/lib/active_support/core_ext/module.rb @@ -4,6 +4,7 @@ require 'active_support/core_ext/module/anonymous' require 'active_support/core_ext/module/reachable' require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/module/attr_internal' +require 'active_support/core_ext/module/concerning' require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/module/deprecation' require 'active_support/core_ext/module/remove_method' diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb index f70a839074..d317df5079 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -49,7 +49,7 @@ class Module # include HairColors # end # - # Person.hair_colors # => [:brown, :black, :blonde, :red] + # + # Person.hair_colors # => [:brown, :black, :blonde, :red] def mattr_reader(*syms) options = syms.extract_options! syms.each do |sym| @@ -181,7 +181,7 @@ class Module # Or pass <tt>instance_accessor: false</tt>, to opt out both instance methods. # # module HairColors - # mattr_accessor :hair_colors, instance_acessor: false + # mattr_accessor :hair_colors, instance_accessor: false # end # # class Person diff --git a/activesupport/lib/active_support/core_ext/module/concerning.rb b/activesupport/lib/active_support/core_ext/module/concerning.rb new file mode 100644 index 0000000000..b22dc5ff1e --- /dev/null +++ b/activesupport/lib/active_support/core_ext/module/concerning.rb @@ -0,0 +1,135 @@ +require 'active_support/concern' + +class Module + # = Bite-sized separation of concerns + # + # We often find ourselves with a medium-sized chunk of behavior that we'd + # like to extract, but only mix in to a single class. + # + # Extracting a plain old Ruby object to encapsulate it and collaborate or + # delegate to the original object is often a good choice, but when there's + # no additional state to encapsulate or we're making DSL-style declarations + # about the parent class, introducing new collaborators can obfuscate rather + # than simplify. + # + # The typical route is to just dump everything in a monolithic class, perhaps + # with a comment, as a least-bad alternative. Using modules in separate files + # means tedious sifting to get a big-picture view. + # + # = Dissatisfying ways to separate small concerns + # + # == Using comments: + # + # class Todo + # # Other todo implementation + # # ... + # + # ## Event tracking + # has_many :events + # + # before_create :track_creation + # after_destroy :track_deletion + # + # private + # def track_creation + # # ... + # end + # end + # + # == With an inline module: + # + # Noisy syntax. + # + # class Todo + # # Other todo implementation + # # ... + # + # module EventTracking + # extend ActiveSupport::Concern + # + # included do + # has_many :events + # before_create :track_creation + # after_destroy :track_deletion + # end + # + # private + # def track_creation + # # ... + # end + # end + # include EventTracking + # end + # + # == Mix-in noise exiled to its own file: + # + # Once our chunk of behavior starts pushing the scroll-to-understand it + # boundary, we give in and move it to a separate file. At this size, the + # overhead feels in good proportion to the size of our extraction, despite + # diluting our at-a-glance sense of how things really work. + # + # class Todo + # # Other todo implementation + # # ... + # + # include TodoEventTracking + # end + # + # = Introducing Module#concerning + # + # By quieting the mix-in noise, we arrive at a natural, low-ceremony way to + # separate bite-sized concerns. + # + # class Todo + # # Other todo implementation + # # ... + # + # concerning :EventTracking do + # included do + # has_many :events + # before_create :track_creation + # after_destroy :track_deletion + # end + # + # private + # def track_creation + # # ... + # end + # end + # end + # + # Todo.ancestors + # # => Todo, Todo::EventTracking, Object + # + # This small step has some wonderful ripple effects. We can + # * grok the behavior of our class in one glance, + # * clean up monolithic junk-drawer classes by separating their concerns, and + # * stop leaning on protected/private for crude "this is internal stuff" modularity. + module Concerning + # Define a new concern and mix it in. + def concerning(topic, &block) + include concern(topic, &block) + end + + # A low-cruft shortcut to define a concern. + # + # concern :EventTracking do + # ... + # end + # + # is equivalent to + # + # module EventTracking + # extend ActiveSupport::Concern + # + # ... + # end + def concern(topic, &module_definition) + const_set topic, Module.new { + extend ::ActiveSupport::Concern + module_eval(&module_definition) + } + end + end + include Concerning +end diff --git a/activesupport/lib/active_support/core_ext/string/output_safety.rb b/activesupport/lib/active_support/core_ext/string/output_safety.rb index 1b2098fc84..1b20507c0b 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -183,15 +183,14 @@ module ActiveSupport #:nodoc: end def %(args) - args = Array(args).map do |arg| - if !html_safe? || arg.html_safe? - arg - else - ERB::Util.h(arg) - end + case args + when Hash + escaped_args = Hash[args.map { |k,arg| [k, html_escape_interpolated_argument(arg)] }] + else + escaped_args = Array(args).map { |arg| html_escape_interpolated_argument(arg) } end - self.class.new(super(args)) + self.class.new(super(escaped_args)) end def html_safe? @@ -224,6 +223,12 @@ module ActiveSupport #:nodoc: EOT end end + + private + + def html_escape_interpolated_argument(arg) + (!html_safe? || arg.html_safe?) ? arg : ERB::Util.h(arg) + end end end diff --git a/activesupport/lib/active_support/core_ext/thread.rb b/activesupport/lib/active_support/core_ext/thread.rb index d5a420301a..ac1ffa4128 100644 --- a/activesupport/lib/active_support/core_ext/thread.rb +++ b/activesupport/lib/active_support/core_ext/thread.rb @@ -33,7 +33,7 @@ class Thread _locals[key.to_sym] = value end - # Returns an an array of the names of the thread-local variables (as Symbols). + # Returns an array of the names of the thread-local variables (as Symbols). # # thr = Thread.new do # Thread.current.thread_variable_set(:cat, 'meow') diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb index dcdea70443..23cd6716e3 100644 --- a/activesupport/lib/active_support/i18n_railtie.rb +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -8,6 +8,8 @@ module I18n config.i18n.railties_load_path = [] config.i18n.load_path = [] config.i18n.fallbacks = ActiveSupport::OrderedOptions.new + # Enforce I18n to check the available locales when setting a locale. + config.i18n.enforce_available_locales = true # Set the i18n configuration after initialization since a lot of # configuration is still usually done in application initializers. @@ -31,10 +33,11 @@ module I18n fallbacks = app.config.i18n.delete(:fallbacks) - if app.config.i18n.has_key?(:enforce_available_locales) - # this option needs to be set before `default_locale=` to work properly. - I18n.enforce_available_locales = app.config.i18n.delete(:enforce_available_locales) - end + # Avoid issues with setting the default_locale by disabling available locales + # check while configuring. + enforce_available_locales = app.config.i18n.delete(:enforce_available_locales) + enforce_available_locales = I18n.enforce_available_locales unless I18n.enforce_available_locales.nil? + I18n.enforce_available_locales = false app.config.i18n.each do |setting, value| case setting @@ -49,6 +52,9 @@ module I18n init_fallbacks(fallbacks) if fallbacks && validate_fallbacks(fallbacks) + # Restore available locales check so it will take place from now on. + I18n.enforce_available_locales = enforce_available_locales + reloader = ActiveSupport::FileUpdateChecker.new(I18n.load_path.dup){ I18n.reload! } app.reloaders << reloader ActionDispatch::Reloader.to_prepare { reloader.execute_if_updated } diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index 060dcb6995..2859075e10 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -63,33 +63,28 @@ module ActiveSupport private_constant :ESCAPED_CHARS, :ESCAPE_REGEX_WITH_HTML_ENTITIES, :ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, :EscapedString - # Recursively turn the given object into a "jsonified" Ruby data structure - # that the JSON gem understands - i.e. we want only Hash, Array, String, - # Numeric, true, false and nil in the final tree. Calls #as_json on it if - # it's not from one of these base types. - # - # This allows developers to implement #as_json withouth having to worry - # about what base types of objects they are allowed to return and having - # to remember calling #as_json recursively. - # - # By default, the options hash is not passed to the children data structures - # to avoid undesiarable result. Develoers must opt-in by implementing - # custom #as_json methods (e.g. Hash#as_json and Array#as_json). + # Convert an object into a "JSON-ready" representation composed of + # primitives like Hash, Array, String, Numeric, and true/false/nil. + # Recursively calls #as_json to the object to recursively build a + # fully JSON-ready object. + # + # This allows developers to implement #as_json without having to + # worry about what base types of objects they are allowed to return + # or having to remember to call #as_json recursively. + # + # Note: the +options+ hash passed to +object.to_json+ is only passed + # to +object.as_json+, not any of this method's recursive +#as_json+ + # calls. def jsonify(value) - if value.is_a?(Hash) - Hash[value.map { |k, v| [jsonify(k), jsonify(v)] }] - elsif value.is_a?(Array) - value.map { |v| jsonify(v) } - elsif value.is_a?(String) + case value + when String EscapedString.new(value) - elsif value.is_a?(Numeric) + when Numeric, NilClass, TrueClass, FalseClass value - elsif value == true - true - elsif value == false - false - elsif value == nil - nil + when Hash + Hash[value.map { |k, v| [jsonify(k), jsonify(v)] }] + when Array + value.map { |v| jsonify(v) } else jsonify value.as_json end diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb index a35d5980fe..8e6e1dcfeb 100644 --- a/activesupport/lib/active_support/message_verifier.rb +++ b/activesupport/lib/active_support/message_verifier.rb @@ -39,8 +39,9 @@ module ActiveSupport if data.present? && digest.present? && secure_compare(digest, generate_digest(data)) begin @serializer.load(::Base64.strict_decode64(data)) - rescue ArgumentError - raise InvalidSignature + rescue ArgumentError => argument_error + raise InvalidSignature if argument_error.message =~ %r{invalid base64} + raise end else raise InvalidSignature diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb index c6658dba96..b169e3af01 100644 --- a/activesupport/lib/active_support/number_helper.rb +++ b/activesupport/lib/active_support/number_helper.rb @@ -1,4 +1,3 @@ - module ActiveSupport module NumberHelper extend ActiveSupport::Autoload @@ -343,6 +342,5 @@ module ActiveSupport def number_to_human(number, options = {}) NumberToHumanConverter.convert(number, options) end - end end diff --git a/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb index d1335f6910..78d2c9ae6e 100644 --- a/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb @@ -1,6 +1,6 @@ module ActiveSupport module NumberHelper - class NumberToHumanSizeConverter < NumberConverter + class NumberToHumanSizeConverter < NumberConverter #:nodoc: STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb] self.namespace = :human diff --git a/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb b/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb index 4c33c30772..af2ee56d91 100644 --- a/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb @@ -1,6 +1,6 @@ module ActiveSupport module NumberHelper - class NumberToPhoneConverter < NumberConverter + class NumberToPhoneConverter < NumberConverter #:nodoc: def convert str = country_code(opts[:country_code]) str << convert_to_phone_number(number.to_s.strip) diff --git a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb index 273667fdae..c42354fc83 100644 --- a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb @@ -5,28 +5,50 @@ module ActiveSupport self.validate_float = true def convert - @number = Float(number) - precision = options.delete :precision significant = options.delete :significant + case number + when Float, String + @number = BigDecimal(number.to_s) + when Rational + if significant + @number = BigDecimal(number, digit_count(number.to_i) + precision) + else + @number = BigDecimal(number, precision) + end + else + @number = number.to_d + end + if significant && precision > 0 digits, rounded_number = digits_and_rounded_number(precision) precision -= digits precision = 0 if precision < 0 # don't let it be negative else - rounded_number = BigDecimal.new(number.to_s).round(precision).to_f + rounded_number = number.round(precision) + rounded_number = rounded_number.to_i if precision == 0 rounded_number = rounded_number.abs if rounded_number.zero? # prevent showing negative zeros end - delimited_number = NumberToDelimitedConverter.convert("%01.#{precision}f" % rounded_number, options) + formatted_string = + case rounded_number + when BigDecimal + s = rounded_number.to_s('F') + '0'*precision + a, b = s.split('.', 2) + a + '.' + b[0, precision] + else + "%01.#{precision}f" % rounded_number + end + + delimited_number = NumberToDelimitedConverter.convert(formatted_string, options) format_number(delimited_number) end private def digits_and_rounded_number(precision) - if number.zero? + if zero? [1, 0] else digits = digit_count(number) @@ -38,11 +60,11 @@ module ActiveSupport end def calculate_rounded_number(multiplier) - (BigDecimal.new(number.to_s) / BigDecimal.new(multiplier.to_f.to_s)).round.to_f * multiplier + (number / BigDecimal.new(multiplier.to_f.to_s)).round * multiplier end def digit_count(number) - (Math.log10(number.abs) + 1).floor + (Math.log10(absolute_number(number)) + 1).floor end def strip_insignificant_zeros @@ -57,6 +79,14 @@ module ActiveSupport number end end + + def absolute_number(number) + number.respond_to?(:abs) ? number.abs : number.to_d.abs + end + + def zero? + number.respond_to?(:zero?) ? number.zero? : number.to_d.zero? + end end end end diff --git a/activesupport/lib/active_support/per_thread_registry.rb b/activesupport/lib/active_support/per_thread_registry.rb index a5e7389d16..ca2e4d5625 100644 --- a/activesupport/lib/active_support/per_thread_registry.rb +++ b/activesupport/lib/active_support/per_thread_registry.rb @@ -32,12 +32,15 @@ module ActiveSupport # # If the class has an initializer, it must accept no arguments. module PerThreadRegistry + def self.extended(object) + object.instance_variable_set '@per_thread_registry_key', object.name.freeze + end + def instance - Thread.current[name] ||= new + Thread.current[@per_thread_registry_key] ||= new end protected - def method_missing(name, *args, &block) # :nodoc: # Caches the method definition as a singleton method of the receiver. define_singleton_method(name) do |*a, &b| diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index a22e61d286..3862ab5c42 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -359,14 +359,14 @@ module ActiveSupport class << self alias_method :create, :new - # Return a TimeZone instance with the given name, or +nil+ if no + # Returns a TimeZone instance with the given name, or +nil+ if no # such TimeZone instance exists. (This exists to support the use of # this class with the +composed_of+ macro.) def new(name) self[name] end - # Return an array of all TimeZone objects. There are multiple + # Returns an array of all TimeZone objects. There are multiple # TimeZone objects per time zone, in many cases, to make it easier # for users to find their own time zone. def all diff --git a/activesupport/lib/active_support/version.rb b/activesupport/lib/active_support/version.rb index 8762330a6e..b3f0e7198d 100644 --- a/activesupport/lib/active_support/version.rb +++ b/activesupport/lib/active_support/version.rb @@ -1,7 +1,7 @@ module ActiveSupport # Returns the version of the currently loaded ActiveSupport as a Gem::Version def self.version - Gem::Version.new "4.1.0.beta" + Gem::Version.new "4.1.0.beta1" end module VERSION #:nodoc: diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb index 65de48a7f6..1dfa3833f0 100644 --- a/activesupport/test/abstract_unit.rb +++ b/activesupport/test/abstract_unit.rb @@ -15,7 +15,6 @@ silence_warnings do end require 'active_support/testing/autorun' -require 'empty_bool' ENV['NO_RELOAD'] = '1' require 'active_support' @@ -25,6 +24,9 @@ Thread.abort_on_exception = true # Show backtraces for deprecated behavior for quicker cleanup. ActiveSupport::Deprecation.debug = true +# Disable available locale checks to avoid warnings running the test suite. +I18n.enforce_available_locales = false + # Skips the current run on Rubinius using Minitest::Assertions#skip def rubinius_skip(message = '') skip message if RUBY_ENGINE == 'rbx' diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb index f8e2ce22fa..32c2dfdfc0 100644 --- a/activesupport/test/callbacks_test.rb +++ b/activesupport/test/callbacks_test.rb @@ -1,27 +1,6 @@ require 'abstract_unit' module CallbacksTest - class Phone - include ActiveSupport::Callbacks - define_callbacks :save - - set_callback :save, :before, :before_save1 - set_callback :save, :after, :after_save1 - - def before_save1; self.history << :before; end - def after_save1; self.history << :after; end - - def save - run_callbacks :save do - raise 'boom' - end - end - - def history - @history ||= [] - end - end - class Record include ActiveSupport::Callbacks diff --git a/activesupport/test/configurable_test.rb b/activesupport/test/configurable_test.rb index d00273a028..ef847fc557 100644 --- a/activesupport/test/configurable_test.rb +++ b/activesupport/test/configurable_test.rb @@ -95,6 +95,20 @@ class ConfigurableActiveSupport < ActiveSupport::TestCase config_accessor "invalid attribute name" end end + + assert_raises NameError do + Class.new do + include ActiveSupport::Configurable + config_accessor "invalid\nattribute" + end + end + + assert_raises NameError do + Class.new do + include ActiveSupport::Configurable + config_accessor "invalid\n" + end + end end def assert_method_defined(object, method) diff --git a/activesupport/test/core_ext/blank_test.rb b/activesupport/test/core_ext/blank_test.rb index a68c074777..3e3176f993 100644 --- a/activesupport/test/core_ext/blank_test.rb +++ b/activesupport/test/core_ext/blank_test.rb @@ -4,6 +4,14 @@ require 'abstract_unit' require 'active_support/core_ext/object/blank' class BlankTest < ActiveSupport::TestCase + class EmptyTrue + def empty?() true; end + end + + class EmptyFalse + def empty?() false; end + end + BLANK = [ EmptyTrue.new, nil, false, '', ' ', " \n\t \r ", ' ', [], {} ] NOT = [ EmptyFalse.new, Object.new, true, 0, 1, 'a', [nil], { nil => 0 } ] diff --git a/activesupport/test/core_ext/class/delegating_attributes_test.rb b/activesupport/test/core_ext/class/delegating_attributes_test.rb index 148f82946c..0e0742d147 100644 --- a/activesupport/test/core_ext/class/delegating_attributes_test.rb +++ b/activesupport/test/core_ext/class/delegating_attributes_test.rb @@ -39,10 +39,13 @@ class DelegatingAttributesTest < ActiveSupport::TestCase end def test_simple_accessor_declaration_with_instance_reader_false + _instance_methods = single_class.public_instance_methods single_class.superclass_delegating_accessor :no_instance_reader, :instance_reader => false assert_respond_to single_class, :no_instance_reader assert_respond_to single_class, :no_instance_reader= - assert !single_class.public_instance_methods.map(&:to_s).include?("no_instance_reader") + assert !_instance_methods.include?(:no_instance_reader) + assert !_instance_methods.include?(:no_instance_reader?) + assert !_instance_methods.include?(:_no_instance_reader) end def test_working_with_simple_attributes diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb index 8eae8c832c..28ba33331e 100644 --- a/activesupport/test/core_ext/duration_test.rb +++ b/activesupport/test/core_ext/duration_test.rb @@ -53,12 +53,10 @@ class DurationTest < ActiveSupport::TestCase end def test_argument_error - 1.second.ago('') - flunk("no exception was raised") - rescue ArgumentError => e + e = assert_raise ArgumentError do + 1.second.ago('') + end assert_equal 'expected a time or date, got ""', e.message, "ensure ArgumentError is not being raised by dependencies.rb" - rescue Exception => e - flunk("ArgumentError should be raised, but we got #{e.class} instead") end def test_fractional_weeks diff --git a/activesupport/test/core_ext/module/concerning_test.rb b/activesupport/test/core_ext/module/concerning_test.rb new file mode 100644 index 0000000000..c6863b24a4 --- /dev/null +++ b/activesupport/test/core_ext/module/concerning_test.rb @@ -0,0 +1,35 @@ +require 'abstract_unit' +require 'active_support/core_ext/module/concerning' + +class ConcerningTest < ActiveSupport::TestCase + def test_concern_shortcut_creates_a_module_but_doesnt_include_it + mod = Module.new { concern(:Foo) { } } + assert_kind_of Module, mod::Foo + assert mod::Foo.respond_to?(:included) + assert !mod.ancestors.include?(mod::Foo), mod.ancestors.inspect + end + + def test_concern_creates_a_module_extended_with_active_support_concern + klass = Class.new do + concern :Foo do + included { @foo = 1 } + def should_be_public; end + end + end + + # Declares a concern but doesn't include it + assert_kind_of Module, klass::Foo + assert !klass.ancestors.include?(klass::Foo), klass.ancestors.inspect + + # Public method visibility by default + assert klass::Foo.public_instance_methods.map(&:to_s).include?('should_be_public') + + # Calls included hook + assert_equal 1, Class.new { include klass::Foo }.instance_variable_get('@foo') + end + + def test_concerning_declares_a_concern_and_includes_it_immediately + klass = Class.new { concerning(:Foo) { } } + assert klass.ancestors.include?(klass::Foo), klass.ancestors.inspect + end +end diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb index 283b13ff8b..5b99fae411 100644 --- a/activesupport/test/core_ext/module_test.rb +++ b/activesupport/test/core_ext/module_test.rb @@ -12,12 +12,6 @@ class Ab Constant3 = "Goodbye World" end -module Xy - class Bc - include One - end -end - module Yz module Zy class Cd diff --git a/activesupport/test/core_ext/name_error_test.rb b/activesupport/test/core_ext/name_error_test.rb index 03ce09f22a..7525f80cf0 100644 --- a/activesupport/test/core_ext/name_error_test.rb +++ b/activesupport/test/core_ext/name_error_test.rb @@ -3,18 +3,18 @@ require 'active_support/core_ext/name_error' class NameErrorTest < ActiveSupport::TestCase def test_name_error_should_set_missing_name - SomeNameThatNobodyWillUse____Really ? 1 : 0 - flunk "?!?!" - rescue NameError => exc + exc = assert_raise NameError do + SomeNameThatNobodyWillUse____Really ? 1 : 0 + end assert_equal "NameErrorTest::SomeNameThatNobodyWillUse____Really", exc.missing_name assert exc.missing_name?(:SomeNameThatNobodyWillUse____Really) assert exc.missing_name?("NameErrorTest::SomeNameThatNobodyWillUse____Really") end def test_missing_method_should_ignore_missing_name - some_method_that_does_not_exist - flunk "?!?!" - rescue NameError => exc + exc = assert_raise NameError do + some_method_that_does_not_exist + end assert !exc.missing_name?(:Foo) assert_nil exc.missing_name end diff --git a/activesupport/test/core_ext/object_and_class_ext_test.rb b/activesupport/test/core_ext/object_and_class_ext_test.rb index 8d748791e3..0f454fdd95 100644 --- a/activesupport/test/core_ext/object_and_class_ext_test.rb +++ b/activesupport/test/core_ext/object_and_class_ext_test.rb @@ -3,31 +3,6 @@ require 'active_support/time' require 'active_support/core_ext/object' require 'active_support/core_ext/class/subclasses' -class ClassA; end -class ClassB < ClassA; end -class ClassC < ClassB; end -class ClassD < ClassA; end - -class ClassI; end -class ClassJ < ClassI; end - -class ClassK -end -module Nested - class << self - def on_const_missing(&callback) - @on_const_missing = callback - end - private - def const_missing(mod_id) - @on_const_missing[mod_id] if @on_const_missing - super - end - end - class ClassL < ClassK - end -end - class ObjectTests < ActiveSupport::TestCase class DuckTime def acts_like_time? diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index e56bab6d4c..00bec5bd9d 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -73,12 +73,11 @@ class DependenciesTest < ActiveSupport::TestCase $raises_exception_load_count = 0 5.times do |count| - begin + e = assert_raise Exception, 'should have loaded dependencies/raises_exception which raises an exception' do require_dependency filename - flunk 'should have loaded dependencies/raises_exception which raises an exception' - rescue Exception => e - assert_equal 'Loading me failed, so do not add to loaded or history.', e.message end + + assert_equal 'Loading me failed, so do not add to loaded or history.', e.message assert_equal count + 1, $raises_exception_load_count assert !ActiveSupport::Dependencies.loaded.include?(filename) @@ -366,26 +365,19 @@ class DependenciesTest < ActiveSupport::TestCase def test_non_existing_const_raises_name_error_with_fully_qualified_name with_autoloading_fixtures do - begin - A::DoesNotExist.nil? - flunk "No raise!!" - rescue NameError => e - assert_equal "uninitialized constant A::DoesNotExist", e.message - end - begin - A::B::DoesNotExist.nil? - flunk "No raise!!" - rescue NameError => e - assert_equal "uninitialized constant A::B::DoesNotExist", e.message - end + e = assert_raise(NameError) { A::DoesNotExist.nil? } + assert_equal "uninitialized constant A::DoesNotExist", e.message + + e = assert_raise(NameError) { A::B::DoesNotExist.nil? } + assert_equal "uninitialized constant A::B::DoesNotExist", e.message end end def test_smart_name_error_strings - Object.module_eval "ImaginaryObject" - flunk "No raise!!" - rescue NameError => e - assert e.message.include?("uninitialized constant ImaginaryObject") + e = assert_raise NameError do + Object.module_eval "ImaginaryObject" + end + assert_includes "uninitialized constant ImaginaryObject", e.message end def test_loadable_constants_for_path_should_handle_empty_autoloads @@ -543,8 +535,8 @@ class DependenciesTest < ActiveSupport::TestCase require_dependency 'e' mod = Module.new - msg = 'E cannot be autoloaded from an anonymous class or module' - assert_raise(NameError, msg) { mod::E } + e = assert_raise(NameError) { mod::E } + assert_equal 'E cannot be autoloaded from an anonymous class or module', e.message end end @@ -553,12 +545,10 @@ class DependenciesTest < ActiveSupport::TestCase c = ServiceOne ActiveSupport::Dependencies.clear assert ! defined?(ServiceOne) - begin + e = assert_raise ArgumentError do ActiveSupport::Dependencies.load_missing_constant(c, :FakeMissing) - flunk "Expected exception" - rescue ArgumentError => e - assert_match %r{ServiceOne has been removed from the module tree}i, e.message end + assert_match %r{ServiceOne has been removed from the module tree}i, e.message end end @@ -897,12 +887,10 @@ class DependenciesTest < ActiveSupport::TestCase with_autoloading_fixtures do Object.send(:remove_const, :RaisesNameError) if defined?(::RaisesNameError) 2.times do - begin + e = assert_raise NameError do ::RaisesNameError::FooBarBaz.object_id - flunk 'should have raised NameError when autoloaded file referenced FooBarBaz' - rescue NameError => e - assert_equal 'uninitialized constant RaisesNameError::FooBarBaz', e.message end + assert_equal 'uninitialized constant RaisesNameError::FooBarBaz', e.message assert !defined?(::RaisesNameError), "::RaisesNameError is defined but it should have failed!" end diff --git a/activesupport/test/deprecation_test.rb b/activesupport/test/deprecation_test.rb index 9674851b9d..ee1c69502e 100644 --- a/activesupport/test/deprecation_test.rb +++ b/activesupport/test/deprecation_test.rb @@ -104,14 +104,11 @@ class DeprecationTest < ActiveSupport::TestCase message = 'Revise this deprecated stuff now!' callstack = %w(foo bar baz) - begin + e = assert_raise ActiveSupport::DeprecationException do ActiveSupport::Deprecation.behavior.first.call(message, callstack) - rescue ActiveSupport::DeprecationException => e - assert_equal message, e.message - assert_equal callstack, e.backtrace - else - flunk 'the :raise deprecation behaviour should raise the expected exception' end + assert_equal message, e.message + assert_equal callstack, e.backtrace end def test_default_stderr_behavior @@ -174,7 +171,7 @@ class DeprecationTest < ActiveSupport::TestCase ActiveSupport::Deprecation.warn 'abc' ActiveSupport::Deprecation.warn 'def' end - rescue MiniTest::Assertion + rescue Minitest::Assertion flunk 'assert_deprecated should match any warning in block, not just the last one' end diff --git a/activesupport/test/empty_bool.rb b/activesupport/test/empty_bool.rb deleted file mode 100644 index 005b3523ef..0000000000 --- a/activesupport/test/empty_bool.rb +++ /dev/null @@ -1,7 +0,0 @@ -class EmptyTrue - def empty?() true; end -end - -class EmptyFalse - def empty?() false; end -end diff --git a/activesupport/test/fixtures/custom.rb b/activesupport/test/fixtures/custom.rb deleted file mode 100644 index 0eefce0c25..0000000000 --- a/activesupport/test/fixtures/custom.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Custom -end
\ No newline at end of file diff --git a/activesupport/test/message_verifier_test.rb b/activesupport/test/message_verifier_test.rb index f208814468..a5748d28ba 100644 --- a/activesupport/test/message_verifier_test.rb +++ b/activesupport/test/message_verifier_test.rb @@ -55,6 +55,20 @@ class MessageVerifierTest < ActiveSupport::TestCase ActiveSupport.use_standard_json_time_format = prev end + def test_raise_error_when_argument_class_is_not_loaded + # To generate the valid message below: + # + # AutoloadClass = Struct.new(:foo) + # valid_message = @verifier.generate(foo: AutoloadClass.new('foo')) + # + valid_message = "BAh7BjoIZm9vbzonTWVzc2FnZVZlcmlmaWVyVGVzdDo6QXV0b2xvYWRDbGFzcwY6CUBmb29JIghmb28GOgZFVA==--f3ef39a5241c365083770566dc7a9eb5d6ace914" + exception = assert_raise(ArgumentError, NameError) do + @verifier.verify(valid_message) + end + assert_includes ["uninitialized constant MessageVerifierTest::AutoloadClass", + "undefined class/module MessageVerifierTest::AutoloadClass"], exception.message + end + def assert_not_verified(message) assert_raise(ActiveSupport::MessageVerifier::InvalidSignature) do @verifier.verify(message) diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb index 2bf73291a2..659fceb852 100644 --- a/activesupport/test/multibyte_chars_test.rb +++ b/activesupport/test/multibyte_chars_test.rb @@ -221,9 +221,9 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase end def test_include_raises_when_nil_is_passed - @chars.include?(nil) - flunk "Expected chars.include?(nil) to raise TypeError or NoMethodError" - rescue Exception + assert_raises TypeError, NoMethodError, "Expected chars.include?(nil) to raise TypeError or NoMethodError" do + @chars.include?(nil) + end end def test_index_should_return_character_offset diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb index 111db59b2b..6d8d835de7 100644 --- a/activesupport/test/number_helper_test.rb +++ b/activesupport/test/number_helper_test.rb @@ -123,6 +123,12 @@ module ActiveSupport assert_equal("10.00", number_helper.number_to_rounded(9.995, :precision => 2)) assert_equal("11.00", number_helper.number_to_rounded(10.995, :precision => 2)) assert_equal("0.00", number_helper.number_to_rounded(-0.001, :precision => 2)) + + assert_equal("111.23460000000000000000", number_helper.number_to_rounded(111.2346, :precision => 20)) + assert_equal("111.23460000000000000000", number_helper.number_to_rounded(Rational(1112346, 10000), :precision => 20)) + assert_equal("111.23460000000000000000", number_helper.number_to_rounded('111.2346', :precision => 20)) + assert_equal("111.23460000000000000000", number_helper.number_to_rounded(BigDecimal(111.2346, Float::DIG), :precision => 20)) + assert_equal("111.2346" + "0"*96, number_helper.number_to_rounded('111.2346', :precision => 100)) end end @@ -155,6 +161,14 @@ module ActiveSupport assert_equal "10.0", number_helper.number_to_rounded(9.995, :precision => 3, :significant => true) assert_equal "9.99", number_helper.number_to_rounded(9.994, :precision => 3, :significant => true) assert_equal "11.0", number_helper.number_to_rounded(10.995, :precision => 3, :significant => true) + + assert_equal "9775.0000000000000000", number_helper.number_to_rounded(9775, :precision => 20, :significant => true ) + assert_equal "9775.0000000000000000", number_helper.number_to_rounded(9775.0, :precision => 20, :significant => true ) + assert_equal "9775.0000000000000000", number_helper.number_to_rounded(Rational(9775, 1), :precision => 20, :significant => true ) + assert_equal "97.750000000000000000", number_helper.number_to_rounded(Rational(9775, 100), :precision => 20, :significant => true ) + assert_equal "9775.0000000000000000", number_helper.number_to_rounded(BigDecimal(9775), :precision => 20, :significant => true ) + assert_equal "9775.0000000000000000", number_helper.number_to_rounded("9775", :precision => 20, :significant => true ) + assert_equal "9775." + "0"*96, number_helper.number_to_rounded("9775", :precision => 100, :significant => true ) end end diff --git a/activesupport/test/safe_buffer_test.rb b/activesupport/test/safe_buffer_test.rb index 047b89be2a..efa9d5e61f 100644 --- a/activesupport/test/safe_buffer_test.rb +++ b/activesupport/test/safe_buffer_test.rb @@ -140,4 +140,29 @@ class SafeBufferTest < ActiveSupport::TestCase # should still be unsafe assert !y.html_safe?, "should not be safe" end + + test 'Should work with interpolation (array argument)' do + x = 'foo %s bar'.html_safe % ['qux'] + assert_equal 'foo qux bar', x + end + + test 'Should work with interpolation (hash argument)' do + x = 'foo %{x} bar'.html_safe % { x: 'qux' } + assert_equal 'foo qux bar', x + end + + test 'Should escape unsafe interpolated args' do + x = 'foo %{x} bar'.html_safe % { x: '<br/>' } + assert_equal 'foo <br/> bar', x + end + + test 'Should not escape safe interpolated args' do + x = 'foo %{x} bar'.html_safe % { x: '<br/>'.html_safe } + assert_equal 'foo <br/> bar', x + end + + test 'Should interpolate to a safe string' do + x = 'foo %{x} bar'.html_safe % { x: 'qux' } + assert x.html_safe?, 'should be safe' + end end diff --git a/activesupport/test/test_test.rb b/activesupport/test/test_test.rb index 5ed2da7e8b..8a71ef4324 100644 --- a/activesupport/test/test_test.rb +++ b/activesupport/test/test_test.rb @@ -21,10 +21,10 @@ class AssertDifferenceTest < ActiveSupport::TestCase assert_equal true, assert_not(nil) assert_equal true, assert_not(false) - e = assert_raises(MiniTest::Assertion) { assert_not true } + e = assert_raises(Minitest::Assertion) { assert_not true } assert_equal 'Expected true to be nil or false', e.message - e = assert_raises(MiniTest::Assertion) { assert_not true, 'custom' } + e = assert_raises(Minitest::Assertion) { assert_not true, 'custom' } assert_equal 'custom', e.message end @@ -73,7 +73,7 @@ class AssertDifferenceTest < ActiveSupport::TestCase end def test_array_of_expressions_identify_failure - assert_raises(MiniTest::Assertion) do + assert_raises(Minitest::Assertion) do assert_difference ['@object.num', '1 + 1'] do @object.increment end @@ -81,7 +81,7 @@ class AssertDifferenceTest < ActiveSupport::TestCase end def test_array_of_expressions_identify_failure_when_message_provided - assert_raises(MiniTest::Assertion) do + assert_raises(Minitest::Assertion) do assert_difference ['@object.num', '1 + 1'], 1, 'something went wrong' do @object.increment end diff --git a/activesupport/test/testing/constant_lookup_test.rb b/activesupport/test/testing/constant_lookup_test.rb index aca2951450..71a9561189 100644 --- a/activesupport/test/testing/constant_lookup_test.rb +++ b/activesupport/test/testing/constant_lookup_test.rb @@ -6,7 +6,6 @@ class Bar < Foo def index; end def self.index; end end -class Baz < Bar; end module FooBar; end class ConstantLookupTest < ActiveSupport::TestCase diff --git a/guides/bug_report_templates/action_controller_gem.rb b/guides/bug_report_templates/action_controller_gem.rb index 89ac28671a..e7a1e9bd87 100644 --- a/guides/bug_report_templates/action_controller_gem.rb +++ b/guides/bug_report_templates/action_controller_gem.rb @@ -29,7 +29,7 @@ end require 'minitest/autorun' require 'rack/test' -class BugTest < MiniTest::Unit::TestCase +class BugTest < Minitest::Unit::TestCase include Rack::Test::Methods def test_returns_success diff --git a/guides/bug_report_templates/active_record_gem.rb b/guides/bug_report_templates/active_record_gem.rb index a8868a1877..37cd700fbf 100644 --- a/guides/bug_report_templates/active_record_gem.rb +++ b/guides/bug_report_templates/active_record_gem.rb @@ -25,7 +25,7 @@ class Comment < ActiveRecord::Base belongs_to :post end -class BugTest < MiniTest::Unit::TestCase +class BugTest < Minitest::Unit::TestCase def test_association_stuff post = Post.create! post.comments << Comment.create! diff --git a/guides/code/getting_started/Gemfile b/guides/code/getting_started/Gemfile index eef6180804..d573488bdb 100644 --- a/guides/code/getting_started/Gemfile +++ b/guides/code/getting_started/Gemfile @@ -28,7 +28,7 @@ group :doc do end # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder -gem 'jbuilder', '~> 1.2' +gem 'jbuilder', '~> 2.0' # To use ActiveModel has_secure_password # gem 'bcrypt-ruby', '~> 3.1.2' diff --git a/guides/code/getting_started/Gemfile.lock b/guides/code/getting_started/Gemfile.lock index 2d5c50ef5c..1c30ac5d75 100644 --- a/guides/code/getting_started/Gemfile.lock +++ b/guides/code/getting_started/Gemfile.lock @@ -40,7 +40,7 @@ GEM multi_json (~> 1.0) hike (1.2.3) i18n (0.6.4) - jbuilder (1.4.2) + jbuilder (2.0.0) activesupport (>= 3.0.0) multi_json (>= 1.2.0) jquery-rails (3.0.2) @@ -110,7 +110,7 @@ PLATFORMS DEPENDENCIES coffee-rails - jbuilder (~> 1.2) + jbuilder (~> 2.0) jquery-rails rails (= 4.0.0) sass-rails diff --git a/guides/source/3_1_release_notes.md b/guides/source/3_1_release_notes.md index 5c99892e39..485f8c756b 100644 --- a/guides/source/3_1_release_notes.md +++ b/guides/source/3_1_release_notes.md @@ -286,7 +286,7 @@ Action Pack end ``` - You can restrict it to some actions by using `:only` or `:except`. Please read the docs at [`ActionController::Streaming`](http://api.rubyonrails.org/classes/ActionController/Streaming.html) for more information. + You can restrict it to some actions by using `:only` or `:except`. Please read the docs at [`ActionController::Streaming`](http://api.rubyonrails.org/v3.1.0/classes/ActionController/Streaming.html) for more information. * The redirect route method now also accepts a hash of options which will only change the parts of the url in question, or an object which responds to call, allowing for redirects to be reused. diff --git a/guides/source/3_2_release_notes.md b/guides/source/3_2_release_notes.md index a9484cf97a..ce811a583b 100644 --- a/guides/source/3_2_release_notes.md +++ b/guides/source/3_2_release_notes.md @@ -238,7 +238,7 @@ Action Pack end ``` - In the example above, Posts controller will no longer automatically look up for a posts layout. If you need this functionality you could either remove `layout "application"` from `ApplicationController` or explicitly set it to `nil` in `PostsController`. + In the example above, `PostsController` will no longer automatically look up for a posts layout. If you need this functionality you could either remove `layout "application"` from `ApplicationController` or explicitly set it to `nil` in `PostsController`. * Deprecated `ActionController::UnknownAction` in favor of `AbstractController::ActionNotFound`. diff --git a/guides/source/4_0_release_notes.md b/guides/source/4_0_release_notes.md index c0eb77c1e7..19c690233c 100644 --- a/guides/source/4_0_release_notes.md +++ b/guides/source/4_0_release_notes.md @@ -15,7 +15,7 @@ These release notes cover only the major changes. To know about various bug fixe Upgrading to Rails 4.0 ---------------------- -If you're upgrading an existing application, it's a great idea to have good test coverage before going in. You should also first upgrade to Rails 3.2 in case you haven't and make sure your application still runs as expected before attempting an update to Rails 4.0. A list of things to watch out for when upgrading is available in the [Upgrading to Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-3-2-to-rails-4-0) guide. +If you're upgrading an existing application, it's a great idea to have good test coverage before going in. You should also first upgrade to Rails 3.2 in case you haven't and make sure your application still runs as expected before attempting an update to Rails 4.0. A list of things to watch out for when upgrading is available in the [Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-3-2-to-rails-4-0) guide. Creating a Rails 4.0 application @@ -90,7 +90,7 @@ Major Features * **match do not catch all** ([commit](https://github.com/rails/rails/commit/90d2802b71a6e89aedfe40564a37bd35f777e541)) - In the routing DSL, match requires the HTTP verb or verbs to be specified. * **html entities escaped by default** ([commit](https://github.com/rails/rails/commit/5f189f41258b83d49012ec5a0678d827327e7543)) - Strings rendered in erb are escaped unless wrapped with `raw` or `html_safe` is called. * **New security headers** ([commit](https://github.com/rails/rails/commit/6794e92b204572d75a07bd6413bdae6ae22d5a82)) - Rails sends the following headers with every HTTP request: `X-Frame-Options` (prevents clickjacking by forbidding the browser from embedding the page in a frame), `X-XSS-Protection` (asks the browser to halt script injection) and `X-Content-Type-Options` (prevents the browser from opening a jpeg as an exe). - + Extraction of features to gems --------------------------- @@ -268,7 +268,7 @@ Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/a * `scoped_by_...` can be rewritten using `where(...)`. * `find_or_initialize_by_...` can be rewritten using `find_or_initialize_by(...)`. * `find_or_create_by_...` can be rewritten using `find_or_create_by(...)`. - * `find_or_create_by_...!` can be rewritten using `find_or_create_by!(...)`. + * `find_or_create_by_...!` can be rewritten using `find_or_create_by!(...)`. Credits ------- diff --git a/guides/source/4_1_release_notes.md b/guides/source/4_1_release_notes.md index 5c50ed83ea..5f1ecb1cae 100644 --- a/guides/source/4_1_release_notes.md +++ b/guides/source/4_1_release_notes.md @@ -3,9 +3,10 @@ Ruby on Rails 4.1 Release Notes Highlights in Rails 4.1: -* Variants -* Spring -* Action View extracted from Action Pack +* Spring application preloader +* `config/secrets.yml` +* Action Pack variants +* Action Mailer previews These release notes cover only the major changes. To know about various bug fixes and changes, please refer to the change logs or check out the @@ -22,22 +23,83 @@ coverage before going in. You should also first upgrade to Rails 4.0 in case you haven't and make sure your application still runs as expected before attempting an update to Rails 4.1. A list of things to watch out for when upgrading is available in the -[Upgrading to Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-4-0-to-rails-4-1) +[Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-4-0-to-rails-4-1) guide. Major Features -------------- -### Variants +### Spring Application Preloader -We often want to render different html/json/xml templates for phones, -tablets, and desktop browsers. Variants makes it easy. +Spring is a Rails application preloader. It speeds up development by keeping +your application running in the background so you don't need to boot it every +time you run a test, rake task or migration. + +New Rails 4.1 applications will ship with "springified" binstubs. This means +that `bin/rails` and `bin/rake` will automatically take advantage of preloaded +spring environments. + +**Running rake tasks:** + +``` +bin/rake test:models +``` + +**Running a Rails command:** + +``` +bin/rails console +``` + +**Spring introspection:** + +``` +$ bin/spring status +Spring is running: + + 1182 spring server | my_app | started 29 mins ago + 3656 spring app | my_app | started 23 secs ago | test mode + 3746 spring app | my_app | started 10 secs ago | development mode +``` + +Have a look at the +[Spring README](https://github.com/jonleighton/spring/blob/master/README.md) to +see all available features. + +See the [Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#spring) +guide on how to migrate existing applications to use this feature. + +### `config/secrets.yml` + +Rails 4.1 generates a new `secrets.yml` file in the `config` folder. By default, +this file contains the application's `secret_key_base`, but it could also be +used to store other secrets such as access keys for external APIs. + +The secrets added to this file are accessible via `Rails.application.secrets`. +For example, with the following `config/secrets.yml`: + +```yaml +development: + secret_key_base: 3b7cd727ee24e8444053437c36cc66c3 + some_api_key: SOMEKEY +``` + +`Rails.application.secrets.some_api_key` returns `SOMEKEY` in the development +environment. + +See the [Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#config-secrets-yml) +guide on how to migrate existing applications to use this feature. + +### Action Pack Variants + +We often want to render different HTML/JSON/XML templates for phones, +tablets, and desktop browsers. Variants make it easy. The request variant is a specialization of the request format, like `:tablet`, `:phone`, or `:desktop`. -You can set the variant in a before_action: +You can set the variant in a `before_action`: ```ruby request.variant = :tablet if request.user_agent =~ /iPad/ @@ -62,46 +124,41 @@ app/views/projects/show.html+tablet.erb app/views/projects/show.html+phone.erb ``` -### Spring - -New Rails 4.1 applications will ship with "springified" binstubs. This means -that `bin/rails` and `bin/rake` will automatically take advantage preloaded -spring environments. - -**running rake tasks:** +You can also simplify the variants definition using the inline syntax: -``` -bin/rake routes +```ruby +respond_to do |format| + format.js { render "trash" } + format.html.phone { redirect_to progress_path } + format.html.none { render "trash" } +end ``` -**running tests:** +### Action Mailer Previews -``` -bin/rake test -bin/rake test test/models -bin/rake test test/models/user_test.rb -``` +Action Mailer previews provide a way to visually see how emails look by visiting +a special URL that renders them. -**running a console:** +You implement a preview class whose methods return the mail object you'd like +to check: -``` -bin/rails console +```ruby +class NotifierPreview < ActionMailer::Preview + def welcome + Notifier.welcome(User.first) + end +end ``` -**spring introspection:** +The preview is available in http://localhost:3000/rails/mailers/notifier/welcome, +and a list of them in http://localhost:3000/rails/mailers. -``` -$ bundle exec spring status -Spring is running: +By default, these preview classes live in `test/mailers/previews`. +This can be configured using the `preview_path` option. - 1182 spring server | my_app | started 29 mins ago - 3656 spring app | my_app | started 23 secs ago | test mode - 3746 spring app | my_app | started 10 secs ago | development mode -``` - -Have a look at the -[Spring README](https://github.com/jonleighton/spring/blob/master/README.md) to -see a all available features. +See its +[documentation](http://api.rubyonrails.org/v4.1.0/classes/ActionMailer/Base.html) +for a detailed write up. ### Active Record enums @@ -113,31 +170,76 @@ class Conversation < ActiveRecord::Base enum status: [ :active, :archived ] end -conversation.archive! +conversation.archived! conversation.active? # => false conversation.status # => "archived" Conversation.archived # => Relation for all archived Conversations ``` -See -[active_record/enum.rb](https://github.com/rails/rails/blob/4-1-stable/activerecord/lib/active_record/enum.rb#L2-L42) +See its +[documentation](http://api.rubyonrails.org/v4.1.0/classes/ActiveRecord/Enum.html) for a detailed write up. -### Application message verifier. +### Message Verifiers + +Message verifiers can be used to generate and verify signed messages. This can +be useful to safely transport sensitive data like remember-me tokens and +friends. -Create a message verifier that can be used to generate and verify signed -messages in the application. +The method `Rails.application.message_verifier` returns a new message verifier +that signs messages with a key derived from secret_key_base and the given +message verifier name: ```ruby -message = Rails.application.message_verifier('salt').generate('my sensible data') -Rails.application.message_verifier('salt').verify(message) -# => 'my sensible data' +signed_token = Rails.application.message_verifier(:remember_me).generate(token) +Rails.application.message_verifier(:remember_me).verify(signed_token) # => token + +Rails.application.message_verifier(:remember_me).verify(tampered_token) +# raises ActiveSupport::MessageVerifier::InvalidSignature ``` -Documentation -------------- +### Module#concerning + +A natural, low-ceremony way to separate responsibilities within a class: +```ruby +class Todo < ActiveRecord::Base + concerning :EventTracking do + included do + has_many :events + end + + def latest_event + ... + end + + private + def some_internal_method + ... + end + end +end +``` + +This example is equivalent to defining a `EventTracking` module inline, +extending it with `ActiveSupport::Concern`, then mixing it in to the +`Todo` class. + +See its +[documentation](http://api.rubyonrails.org/v4.1.0/classes/Module/Concerning.html) +for a detailed write up and the intended use cases. + +### CSRF protection from remote `<script>` tags + +Cross-site request forgery (CSRF) protection now covers GET requests with +JavaScript responses, too. That prevents a third-party site from referencing +your JavaScript URL and attempting to run it to extract sensitive data. + +This means any of your tests that hit `.js` URLs will now fail CSRF protection +unless they use `xhr`. Upgrade your tests to be explicit about expecting +XmlHttpRequests. Instead of `post :create, format: :js`, switch to the explicit +`xhr :post, :create, format: :js`. Railties -------- @@ -179,138 +281,6 @@ for detailed changes. * Add `Application#message_verifier` method to return a message verifier. ([Pull Request](https://github.com/rails/rails/pull/12995)) -Action Mailer -------------- - -Please refer to the -[Changelog](https://github.com/rails/rails/blob/4-1-stable/actionmailer/CHANGELOG.md) -for detailed changes. - -### Notable changes - -* Instrument the generation of Action Mailer messages. The time it takes to - generate a message is written to the log. ([Pull Request](https://github.com/rails/rails/pull/12556)) - - -Active Model ------------- - -Please refer to the -[Changelog](https://github.com/rails/rails/blob/4-1-stable/activemodel/CHANGELOG.md) -for detailed changes. - -### Deprecations - -* Deprecate `Validator#setup`. This should be done manually now in the - validator's constructor. ([Commit](https://github.com/rails/rails/commit/7d84c3a2f7ede0e8d04540e9c0640de7378e9b3a)) - -### Notable changes - -* Added new API methods `reset_changes` and `changes_applied` to - `ActiveModel::Dirty` that control changes state. - - -Active Support --------------- - -Please refer to the -[Changelog](https://github.com/rails/rails/blob/4-1-stable/activesupport/CHANGELOG.md) -for detailed changes. - - -### Removals - -* Removed `MultiJSON` dependency. As a result, `ActiveSupport::JSON.decode` - no longer accepts an options hash for `MultiJSON`. ([Pull Request](https://github.com/rails/rails/pull/10576) / [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) - -* Removed support for the `encode_json` hook used for encoding custom objects into - JSON. This feature has been extracted into the [activesupport-json_encoder](https://github.com/rails/activesupport-json_encoder) - gem. - ([Related Pull Request](https://github.com/rails/rails/pull/12183) / - [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) - -* Removed deprecated `ActiveSupport::JSON::Variable` with no replacement. - -* Removed deprecated `String#encoding_aware?` core extensions (`core_ext/string/encoding`). - -* Removed deprecated `Module#local_constant_names` in favor of `Module#local_constants`. - -* Removed deprecated `DateTime.local_offset` in favor of `DateTime.civil_from_fromat`. - -* Removed deprecated `Logger` core extensions (`core_ext/logger.rb`). - -* Removed deprecated `Time#time_with_datetime_fallback`, `Time#utc_time` and - `Time#local_time` in favor of `Time#utc` and `Time#local`. - -* Removed deprecated `Hash#diff` with no replacement. - -* Removed deprecated `Date#to_time_in_current_zone` in favor of `Date#in_time_zone`. - -* Removed deprecated `Proc#bind` with no replacement. - -* Removed deprecated `Array#uniq_by` and `Array#uniq_by!`, use native - `Array#uniq` and `Array#uniq!` instead. - -* Removed deprecated `ActiveSupport::BasicObject`, use - `ActiveSupport::ProxyObject` instead. - -* Removed deprecated `BufferedLogger`, use `ActiveSupport::Logger` instead. - -* Removed deprecated `assert_present` and `assert_blank` methods, use `assert - object.blank?` and `assert object.present?` instead. - -### Deprecations - -* Deprecated `Numeric#{ago,until,since,from_now}`, the user is expected to - explicitly convert the value into an AS::Duration, i.e. `5.ago` => `5.seconds.ago` - ([Pull Request](https://github.com/rails/rails/pull/12389)) - -* Deprecated the require path `active_support/core_ext/object/to_json`. Require - `active_support/core_ext/object/json` instead. ([Pull Request](https://github.com/rails/rails/pull/12203)) - -* Deprecated `ActiveSupport::JSON::Encoding::CircularReferenceError`. This feature - has been extracted into the [activesupport-json_encoder](https://github.com/rails/activesupport-json_encoder) - gem. - ([Pull Request](https://github.com/rails/rails/pull/12785) / - [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) - -* Deprecated `ActiveSupport.encode_big_decimal_as_string` option. This feature has - been extracetd into the [activesupport-json_encoder](https://github.com/rails/activesupport-json_encoder) - gem. - ([Pull Request](https://github.com/rails/rails/pull/13060) / - [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) - -### Notable changes - -* `ActiveSupport`'s JSON encoder has been rewritten to take advantage of the - JSON gem rather than doing custom encoding in pure-Ruby. - ([Pull Request](https://github.com/rails/rails/pull/12183) / - [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) - -* Improved compatibility with the JSON gem. - ([Pull Request](https://github.com/rails/rails/pull/12862) / - [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) - -* Added `ActiveSupport::Testing::TimeHelpers#travel` and `#travel_to`. These - methods change current time to the given time or time difference by stubbing - `Time.now` and - `Date.today`. ([Pull Request](https://github.com/rails/rails/pull/12824)) - -* Added `Numeric#in_milliseconds`, like `1.hour.in_milliseconds`, so we can feed - them to JavaScript functions like - `getTime()`. ([Commit](https://github.com/rails/rails/commit/423249504a2b468d7a273cbe6accf4f21cb0e643)) - -* Added `Date#middle_of_day`, `DateTime#middle_of_day` and `Time#middle_of_day` - methods. Also added `midday`, `noon`, `at_midday`, `at_noon` and - `at_middle_of_day` as - aliases. ([Pull Request](https://github.com/rails/rails/pull/10879)) - -* Added `String#remove(pattern)` as a short-hand for the common pattern of - `String#gsub(pattern,'')`. ([Commit](https://github.com/rails/rails/commit/5da23a3f921f0a4a3139495d2779ab0d3bd4cb5f)) - -* Removed 'cow' => 'kine' irregular inflection from default - inflections. ([Commit](https://github.com/rails/rails/commit/c300dca9963bda78b8f358dbcb59cabcdc5e1dc9)) - Action Pack ----------- @@ -330,16 +300,23 @@ for detailed changes. * Removed deprecated constants from Action Controller: - ActionController::AbstractRequest => ActionDispatch::Request - ActionController::Request => ActionDispatch::Request - ActionController::AbstractResponse => ActionDispatch::Response - ActionController::Response => ActionDispatch::Response - ActionController::Routing => ActionDispatch::Routing - ActionController::Integration => ActionDispatch::Integration - ActionController::IntegrationTest => ActionDispatch::IntegrationTest + | Removed | Successor | + |:-----------------------------------|:--------------------------------| + | ActionController::AbstractRequest | ActionDispatch::Request | + | ActionController::Request | ActionDispatch::Request | + | ActionController::AbstractResponse | ActionDispatch::Response | + | ActionController::Response | ActionDispatch::Response | + | ActionController::Routing | ActionDispatch::Routing | + | ActionController::Integration | ActionDispatch::Integration | + | ActionController::IntegrationTest | ActionDispatch::IntegrationTest | ### Notable changes +* `protect_from_forgery` also prevents cross-origin `<script>` tags. + Update your tests to use `xhr :get, :foo, format: :js` instead of + `get :foo, format: :js`. + ([Pull Request](https://github.com/rails/rails/pull/13345)) + * `#url_for` takes a hash with options inside an array. ([Pull Request](https://github.com/rails/rails/pull/9599)) @@ -351,6 +328,17 @@ for detailed changes. * Separated Action View completely from Action Pack. ([Pull Request](https://github.com/rails/rails/pull/11032)) +Action Mailer +------------- + +Please refer to the +[Changelog](https://github.com/rails/rails/blob/4-1-stable/actionmailer/CHANGELOG.md) +for detailed changes. + +### Notable changes + +* Instrument the generation of Action Mailer messages. The time it takes to + generate a message is written to the log. ([Pull Request](https://github.com/rails/rails/pull/12556)) Active Record ------------- @@ -495,6 +483,125 @@ 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)) +Active Model +------------ + +Please refer to the +[Changelog](https://github.com/rails/rails/blob/4-1-stable/activemodel/CHANGELOG.md) +for detailed changes. + +### Deprecations + +* Deprecate `Validator#setup`. This should be done manually now in the + validator's constructor. ([Commit](https://github.com/rails/rails/commit/7d84c3a2f7ede0e8d04540e9c0640de7378e9b3a)) + +### Notable changes + +* Added new API methods `reset_changes` and `changes_applied` to + `ActiveModel::Dirty` that control changes state. + + +Active Support +-------------- + +Please refer to the +[Changelog](https://github.com/rails/rails/blob/4-1-stable/activesupport/CHANGELOG.md) +for detailed changes. + + +### Removals + +* Removed `MultiJSON` dependency. As a result, `ActiveSupport::JSON.decode` + no longer accepts an options hash for `MultiJSON`. ([Pull Request](https://github.com/rails/rails/pull/10576) / [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) + +* Removed support for the `encode_json` hook used for encoding custom objects into + JSON. This feature has been extracted into the [activesupport-json_encoder](https://github.com/rails/activesupport-json_encoder) + gem. + ([Related Pull Request](https://github.com/rails/rails/pull/12183) / + [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) + +* Removed deprecated `ActiveSupport::JSON::Variable` with no replacement. + +* Removed deprecated `String#encoding_aware?` core extensions (`core_ext/string/encoding`). + +* Removed deprecated `Module#local_constant_names` in favor of `Module#local_constants`. + +* Removed deprecated `DateTime.local_offset` in favor of `DateTime.civil_from_format`. + +* Removed deprecated `Logger` core extensions (`core_ext/logger.rb`). + +* Removed deprecated `Time#time_with_datetime_fallback`, `Time#utc_time` and + `Time#local_time` in favor of `Time#utc` and `Time#local`. + +* Removed deprecated `Hash#diff` with no replacement. + +* Removed deprecated `Date#to_time_in_current_zone` in favor of `Date#in_time_zone`. + +* Removed deprecated `Proc#bind` with no replacement. + +* Removed deprecated `Array#uniq_by` and `Array#uniq_by!`, use native + `Array#uniq` and `Array#uniq!` instead. + +* Removed deprecated `ActiveSupport::BasicObject`, use + `ActiveSupport::ProxyObject` instead. + +* Removed deprecated `BufferedLogger`, use `ActiveSupport::Logger` instead. + +* Removed deprecated `assert_present` and `assert_blank` methods, use `assert + object.blank?` and `assert object.present?` instead. + +### Deprecations + +* Deprecated `Numeric#{ago,until,since,from_now}`, the user is expected to + explicitly convert the value into an AS::Duration, i.e. `5.ago` => `5.seconds.ago` + ([Pull Request](https://github.com/rails/rails/pull/12389)) + +* Deprecated the require path `active_support/core_ext/object/to_json`. Require + `active_support/core_ext/object/json` instead. ([Pull Request](https://github.com/rails/rails/pull/12203)) + +* Deprecated `ActiveSupport::JSON::Encoding::CircularReferenceError`. This feature + has been extracted into the [activesupport-json_encoder](https://github.com/rails/activesupport-json_encoder) + gem. + ([Pull Request](https://github.com/rails/rails/pull/12785) / + [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) + +* Deprecated `ActiveSupport.encode_big_decimal_as_string` option. This feature has + been extracetd into the [activesupport-json_encoder](https://github.com/rails/activesupport-json_encoder) + gem. + ([Pull Request](https://github.com/rails/rails/pull/13060) / + [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) + +### Notable changes + +* `ActiveSupport`'s JSON encoder has been rewritten to take advantage of the + JSON gem rather than doing custom encoding in pure-Ruby. + ([Pull Request](https://github.com/rails/rails/pull/12183) / + [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) + +* Improved compatibility with the JSON gem. + ([Pull Request](https://github.com/rails/rails/pull/12862) / + [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) + +* Added `ActiveSupport::Testing::TimeHelpers#travel` and `#travel_to`. These + methods change current time to the given time or time difference by stubbing + `Time.now` and + `Date.today`. ([Pull Request](https://github.com/rails/rails/pull/12824)) + +* Added `Numeric#in_milliseconds`, like `1.hour.in_milliseconds`, so we can feed + them to JavaScript functions like + `getTime()`. ([Commit](https://github.com/rails/rails/commit/423249504a2b468d7a273cbe6accf4f21cb0e643)) + +* Added `Date#middle_of_day`, `DateTime#middle_of_day` and `Time#middle_of_day` + methods. Also added `midday`, `noon`, `at_midday`, `at_noon` and + `at_middle_of_day` as + aliases. ([Pull Request](https://github.com/rails/rails/pull/10879)) + +* Added `String#remove(pattern)` as a short-hand for the common pattern of + `String#gsub(pattern,'')`. ([Commit](https://github.com/rails/rails/commit/5da23a3f921f0a4a3139495d2779ab0d3bd4cb5f)) + +* Removed 'cow' => 'kine' irregular inflection from default + inflections. ([Commit](https://github.com/rails/rails/commit/c300dca9963bda78b8f358dbcb59cabcdc5e1dc9)) + Credits ------- diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index 4252b5ee9a..8ea3859475 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -794,7 +794,7 @@ class AdminsController < ApplicationController end ``` -With this in place, you can create namespaced controllers that inherit from `AdminController`. The filter will thus be run for all actions in those controllers, protecting them with HTTP basic authentication. +With this in place, you can create namespaced controllers that inherit from `AdminsController`. The filter will thus be run for all actions in those controllers, protecting them with HTTP basic authentication. ### HTTP Digest Authentication diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md index 769be9840c..6a355a5177 100644 --- a/guides/source/action_view_overview.md +++ b/guides/source/action_view_overview.md @@ -1285,7 +1285,7 @@ Creates a field set for grouping HTML form elements. Creates a file upload field. ```html+erb -<%= form_tag {action: "post"}, {multipart: true} do %> +<%= form_tag({action:"post"}, multipart: true) do %> <label for="file">File to Upload</label> <%= file_field_tag "file" %> <%= submit_tag %> <% end %> @@ -1591,7 +1591,7 @@ Localized Views Action View has the ability render different templates depending on the current locale. -For example, suppose you have a Posts controller with a show action. By default, calling this action will render `app/views/posts/show.html.erb`. But if you set `I18n.locale = :de`, then `app/views/posts/show.de.html.erb` will be rendered instead. If the localized template isn't present, the undecorated version will be used. This means you're not required to provide localized views for all cases, but they will be preferred and used if available. +For example, suppose you have a `PostsController` with a show action. By default, calling this action will render `app/views/posts/show.html.erb`. But if you set `I18n.locale = :de`, then `app/views/posts/show.de.html.erb` will be rendered instead. If the localized template isn't present, the undecorated version will be used. This means you're not required to provide localized views for all cases, but they will be preferred and used if available. You can use the same technique to localize the rescue files in your public directory. For example, setting `I18n.locale = :de` and creating `public/500.de.html` and `public/404.de.html` would allow you to have localized rescue pages. diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 863da3be72..667433285f 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -55,7 +55,7 @@ class User < ActiveRecord::Base end ``` -Callbacks can also be registered to only fire on certain lifecycle events: +Callbacks can also be registered to only fire on certain life cycle events: ```ruby class User < ActiveRecord::Base @@ -141,6 +141,55 @@ You have initialized an object! => #<User id: 1> ``` +### `after_touch` + +The `after_touch` callback will be called whenever an Active Record object is touched. + +```ruby +class User < ActiveRecord::Base + after_touch do |user| + puts "You have touched an object" + end +end + +>> u = User.create(name: 'Kuldeep') +=> #<User id: 1, name: "Kuldeep", created_at: "2013-11-25 12:17:49", updated_at: "2013-11-25 12:17:49"> + +>> u.touch +You have touched an object +=> true +``` + +It can be used along with `belongs_to`: + +```ruby +class Employee < ActiveRecord::Base + belongs_to :company, touch: true + after_touch do + puts 'An Employee was touched' + end +end + +class Company < ActiveRecord::Base + has_many :employees + after_touch :log_when_employees_or_company_touched + + private + def log_when_employees_or_company_touched + puts 'Employee/Company was touched' + end +end + +>> @employee = Employee.last +=> #<Employee id: 1, company_id: 1, created_at: "2013-11-25 17:04:22", updated_at: "2013-11-25 17:05:05"> + +# triggers @employee.company.touch +>> @employee.touch +Employee/Company was touched +An Employee was touched +=> true +``` + Running Callbacks ----------------- diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md index a83aee5d43..1a43bd206e 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -424,7 +424,7 @@ NOTE: Defined in `active_support/core_ext/object/with_options.rb`. ### JSON support -Active Support provides a better implementation of `to_json` than the +json+ gem ordinarily provides for Ruby objects. This is because some classes, like +Hash+, +OrderedHash+, and +Process::Status+ need special handling in order to provide a proper JSON representation. +Active Support provides a better implementation of `to_json` than the `json` gem ordinarily provides for Ruby objects. This is because some classes, like `Hash`, `OrderedHash` and `Process::Status` need special handling in order to provide a proper JSON representation. NOTE: Defined in `active_support/core_ext/object/json.rb`. diff --git a/guides/source/api_documentation_guidelines.md b/guides/source/api_documentation_guidelines.md index ccb51ce73c..311cc23cf0 100644 --- a/guides/source/api_documentation_guidelines.md +++ b/guides/source/api_documentation_guidelines.md @@ -42,7 +42,21 @@ Spell names correctly: Arel, Test::Unit, RSpec, HTML, MySQL, JavaScript, ERB. Wh Use the article "an" for "SQL", as in "an SQL statement". Also "an SQLite database". -When using pronouns in reference to a hypothetical person, such as "a user with a session cookie", gender neutral pronouns (they/their/them) should be used. Instead of: +Prefer wordings that avoid "you"s and "your"s. For example, instead of + +```markdown +If you need to use `return` statements in your callbacks, it is recommended that you explicitly define them as methods. +``` + +use this style: + +```markdown +If `return` is needed it is recommended to explicitly define a method. +``` + +That said, when using pronouns in reference to a hypothetical person, such as "a +user with a session cookie", gender neutral pronouns (they/their/them) should be +used. Instead of: * he or she... use they. * him or her... use them. @@ -53,7 +67,7 @@ When using pronouns in reference to a hypothetical person, such as "a user with English ------- -Please use American English (<em>color</em>, <em>center</em>, <em>modularize</em>, etc).. See [a list of American and British English spelling differences here](http://en.wikipedia.org/wiki/American_and_British_English_spelling_differences). +Please use American English (<em>color</em>, <em>center</em>, <em>modularize</em>, etc). See [a list of American and British English spelling differences here](http://en.wikipedia.org/wiki/American_and_British_English_spelling_differences). Example Code ------------ diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index 39448e92d5..77189e1442 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -577,6 +577,33 @@ would generate this HTML: The `body` param is required by Sprockets. +### Runtime Error Checking + +By default the asset pipeline will check for potential errors in development mode during +runtime. To disable this behavior you can set: + +```ruby +config.assets.raise_runtime_errors = false +``` + +When `raise_runtime_errors` is set to `false` sprockets will not check that dependencies of assets are declared properly. Here is a scenario where you must tell the asset pipeline about a dependency: + +If you have `application.css.erb` that references `logo.png` like this: + +```css +#logo { background: url(<%= asset_data_uri 'logo.png' %>) } +``` + +Then you must declare that `logo.png` is a dependency of `application.css.erb`, so when the image gets re-compiled, the css file does as well. You can do this using the `//= depend_on_asset` declaration: + +```css +//= depend_on_asset "logo.png" +#logo { background: url(<%= asset_data_uri 'logo.png' %>) } +``` + +Without this declaration you may experience strange behavior when pushing to production that is difficult to debug. When you have `raise_runtime_errors` set to `true`, dependencies will be checked at runtime so you can ensure that all dependencies are met. + + ### Turning Debugging Off You can turn off debug mode by updating `config/environments/development.rb` to diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 59c2594422..c30b806907 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -137,7 +137,9 @@ numbers. New applications filter out passwords by adding the following `config.f * `config.assets.enabled` a flag that controls whether the asset pipeline is enabled. It is set to true by default. -* `config.assets.compress` a flag that enables the compression of compiled assets. It is explicitly set to true in `config/production.rb`. +*`config.assets.raise_runtime_errors`* Set this flag to `true` to enable additional runtime error checking. Recommended in `config/environments/development.rb` to minimize unexpected behavior when deploying to `production`. + +* `config.assets.compress` a flag that enables the compression of compiled assets. It is explicitly set to true in `config/environments/production.rb`. * `config.assets.css_compressor` defines the CSS compressor to use. It is set by default by `sass-rails`. The unique alternative value at the moment is `:yui`, which uses the `yui-compressor` gem. @@ -244,8 +246,14 @@ config.middleware.delete "Rack::MethodOverride" ### Configuring i18n +All these configuration options are delegated to the `I18n` library. + +* `config.i18n.available_locales` whitelists the available locales for the app. Defaults to all locale keys found in locale files, usually only `:en` on a new application. + * `config.i18n.default_locale` sets the default locale of an application used for i18n. Defaults to `:en`. +* `config.i18n.enforce_available_locales` ensures that all locales passed through i18n must be declared in the `available_locales` list, raising an `I18n::InvalidLocale` exception when setting an unavailable locale. Defaults to `true`. It is recommended not to disable this option unless strongly required, since this works as a security measure against setting any invalid locale from user input. + * `config.i18n.load_path` sets the path Rails uses to look for locale files. Defaults to `config/locales/*.{yml,rb}`. ### Configuring Active Record diff --git a/guides/source/credits.html.erb b/guides/source/credits.html.erb index 5beae9c29b..7c6858fa2c 100644 --- a/guides/source/credits.html.erb +++ b/guides/source/credits.html.erb @@ -76,5 +76,5 @@ Oscar Del Ben is a software engineer at <a href="http://www.wildfireapp.com/">Wi <% end %> <%= author('Akshay Surve', 'startupjockey', 'akshaysurve.jpg') do %> - Akshay Surve is the Founder at <a href="http://www.deltax.com">DeltaX</a>, hackathon specialist, a midnight code junkie and ocassionally writes prose. You can connect with him on <a href="https://twitter.com/akshaysurve">Twitter</a>, <a href="http://www.linkedin.com/in/akshaysurve">Linkedin</a>, <a href="http://www.akshaysurve.com/">Personal Blog</a> or <a href="http://www.quora.com/Akshay-Surve">Quora</a>. + Akshay Surve is the Founder at <a href="http://www.deltax.com">DeltaX</a>, hackathon specialist, a midnight code junkie and occasionally writes prose. You can connect with him on <a href="https://twitter.com/akshaysurve">Twitter</a>, <a href="http://www.linkedin.com/in/akshaysurve">Linkedin</a>, <a href="http://www.akshaysurve.com/">Personal Blog</a> or <a href="http://www.quora.com/Akshay-Surve">Quora</a>. <% end %> diff --git a/guides/source/engines.md b/guides/source/engines.md index 2266b1fd7f..87b0a1ac16 100644 --- a/guides/source/engines.md +++ b/guides/source/engines.md @@ -1,7 +1,9 @@ Getting Started with Engines ============================ -In this guide you will learn about engines and how they can be used to provide additional functionality to their host applications through a clean and very easy-to-use interface. +In this guide you will learn about engines and how they can be used to provide +additional functionality to their host applications through a clean and very +easy-to-use interface. After reading this guide, you will know: @@ -16,26 +18,59 @@ After reading this guide, you will know: What are engines? ----------------- -Engines can be considered miniature applications that provide functionality to their host applications. A Rails application is actually just a "supercharged" engine, with the `Rails::Application` class inheriting a lot of its behavior from `Rails::Engine`. - -Therefore, engines and applications can be thought of almost the same thing, just with subtle differences, as you'll see throughout this guide. Engines and applications also share a common structure. - -Engines are also closely related to plugins where the two share a common `lib` directory structure and are both generated using the `rails plugin new` generator. The difference being that an engine is considered a "full plugin" by Rails as indicated by the `--full` option that's passed to the generator command, but this guide will refer to them simply as "engines" throughout. An engine **can** be a plugin, and a plugin **can** be an engine. - -The engine that will be created in this guide will be called "blorgh". The engine will provide blogging functionality to its host applications, allowing for new posts and comments to be created. At the beginning of this guide, you will be working solely within the engine itself, but in later sections you'll see how to hook it into an application. - -Engines can also be isolated from their host applications. This means that an application is able to have a path provided by a routing helper such as `posts_path` and use an engine also that provides a path also called `posts_path`, and the two would not clash. Along with this, controllers, models and table names are also namespaced. You'll see how to do this later in this guide. - -It's important to keep in mind at all times that the application should **always** take precedence over its engines. An application is the object that has final say in what goes on in the universe (with the universe being the application's environment) where the engine should only be enhancing it, rather than changing it drastically. - -To see demonstrations of other engines, check out [Devise](https://github.com/plataformatec/devise), an engine that provides authentication for its parent applications, or [Forem](https://github.com/radar/forem), an engine that provides forum functionality. There's also [Spree](https://github.com/spree/spree) which provides an e-commerce platform, and [RefineryCMS](https://github.com/refinery/refinerycms), a CMS engine. - -Finally, engines would not have been possible without the work of James Adam, Piotr Sarnacki, the Rails Core Team, and a number of other people. If you ever meet them, don't forget to say thanks! +Engines can be considered miniature applications that provide functionality to +their host applications. A Rails application is actually just a "supercharged" +engine, with the `Rails::Application` class inheriting a lot of its behavior +from `Rails::Engine`. + +Therefore, engines and applications can be thought of almost the same thing, +just with subtle differences, as you'll see throughout this guide. Engines and +applications also share a common structure. + +Engines are also closely related to plugins. The two share a common `lib` +directory structure, and are both generated using the `rails plugin new` +generator. The difference is that an engine is considered a "full plugin" by +Rails (as indicated by the `--full` option that's passed to the generator +command). This guide will refer to them simply as "engines" throughout. An +engine **can** be a plugin, and a plugin **can** be an engine. + +The engine that will be created in this guide will be called "blorgh". The +engine will provide blogging functionality to its host applications, allowing +for new posts and comments to be created. At the beginning of this guide, you +will be working solely within the engine itself, but in later sections you'll +see how to hook it into an application. + +Engines can also be isolated from their host applications. This means that an +application is able to have a path provided by a routing helper such as +`posts_path` and use an engine also that provides a path also called +`posts_path`, and the two would not clash. Along with this, controllers, models +and table names are also namespaced. You'll see how to do this later in this +guide. + +It's important to keep in mind at all times that the application should +**always** take precedence over its engines. An application is the object that +has final say in what goes on in the universe (with the universe being the +application's environment) where the engine should only be enhancing it, rather +than changing it drastically. + +To see demonstrations of other engines, check out +[Devise](https://github.com/plataformatec/devise), an engine that provides +authentication for its parent applications, or +[Forem](https://github.com/radar/forem), an engine that provides forum +functionality. There's also [Spree](https://github.com/spree/spree) which +provides an e-commerce platform, and +[RefineryCMS](https://github.com/refinery/refinerycms), a CMS engine. + +Finally, engines would not have been possible without the work of James Adam, +Piotr Sarnacki, the Rails Core Team, and a number of other people. If you ever +meet them, don't forget to say thanks! Generating an engine -------------------- -To generate an engine, you will need to run the plugin generator and pass it options as appropriate to the need. For the "blorgh" example, you will need to create a "mountable" engine, running this command in a terminal: +To generate an engine, you will need to run the plugin generator and pass it +options as appropriate to the need. For the "blorgh" example, you will need to +create a "mountable" engine, running this command in a terminal: ```bash $ rails plugin new blorgh --mountable @@ -47,7 +82,8 @@ The full list of options for the plugin generator may be seen by typing: $ rails plugin --help ``` -The `--full` option tells the generator that you want to create an engine, including a skeleton structure by providing the following: +The `--full` option tells the generator that you want to create an engine, +including a skeleton structure that provides the following: * An `app` directory tree * A `config/routes.rb` file: @@ -56,7 +92,9 @@ The `--full` option tells the generator that you want to create an engine, inclu Rails.application.routes.draw do end ``` - * A file at `lib/blorgh/engine.rb` which is identical in function to a standard Rails application's `config/application.rb` file: + + * A file at `lib/blorgh/engine.rb`, which is identical in function to a + * standard Rails application's `config/application.rb` file: ```ruby module Blorgh @@ -65,7 +103,9 @@ The `--full` option tells the generator that you want to create an engine, inclu end ``` -The `--mountable` option tells the generator that you want to create a "mountable" and namespace-isolated engine. This generator will provide the same skeleton structure as would the `--full` option, and will add: +The `--mountable` option tells the generator that you want to create a +"mountable" and namespace-isolated engine. This generator will provide the same +skeleton structure as would the `--full` option, and will add: * Asset manifest files (`application.js` and `application.css`) * A namespaced `ApplicationController` stub @@ -88,23 +128,32 @@ The `--mountable` option tells the generator that you want to create a "mountabl end ``` -Additionally, the `--mountable` option tells the generator to mount the engine inside the dummy testing application located at `test/dummy` by adding the following to the dummy application's routes file at `test/dummy/config/routes.rb`: +Additionally, the `--mountable` option tells the generator to mount the engine +inside the dummy testing application located at `test/dummy` by adding the +following to the dummy application's routes file at +`test/dummy/config/routes.rb`: ```ruby mount Blorgh::Engine, at: "blorgh" ``` -### Inside an engine +### Inside an Engine -#### Critical files +#### Critical Files -At the root of this brand new engine's directory lives a `blorgh.gemspec` file. When you include the engine into an application later on, you will do so with this line in the Rails application's `Gemfile`: +At the root of this brand new engine's directory lives a `blorgh.gemspec` file. +When you include the engine into an application later on, you will do so with +this line in the Rails application's `Gemfile`: ```ruby gem 'blorgh', path: "vendor/engines/blorgh" ``` -Don't forget to run `bundle install` as usual. By specifying it as a gem within the `Gemfile`, Bundler will load it as such, parsing this `blorgh.gemspec` file and requiring a file within the `lib` directory called `lib/blorgh.rb`. This file requires the `blorgh/engine.rb` file (located at `lib/blorgh/engine.rb`) and defines a base module called `Blorgh`. +Don't forget to run `bundle install` as usual. By specifying it as a gem within +the `Gemfile`, Bundler will load it as such, parsing this `blorgh.gemspec` file +and requiring a file within the `lib` directory called `lib/blorgh.rb`. This +file requires the `blorgh/engine.rb` file (located at `lib/blorgh/engine.rb`) +and defines a base module called `Blorgh`. ```ruby require "blorgh/engine" @@ -113,7 +162,10 @@ module Blorgh end ``` -TIP: Some engines choose to use this file to put global configuration options for their engine. It's a relatively good idea, and so if you want to offer configuration options, the file where your engine's `module` is defined is perfect for that. Place the methods inside the module and you'll be good to go. +TIP: Some engines choose to use this file to put global configuration options +for their engine. It's a relatively good idea, so if you want to offer +configuration options, the file where your engine's `module` is defined is +perfect for that. Place the methods inside the module and you'll be good to go. Within `lib/blorgh/engine.rb` is the base class for the engine: @@ -125,43 +177,94 @@ module Blorgh end ``` -By inheriting from the `Rails::Engine` class, this gem notifies Rails that there's an engine at the specified path, and will correctly mount the engine inside the application, performing tasks such as adding the `app` directory of the engine to the load path for models, mailers, controllers and views. - -The `isolate_namespace` method here deserves special notice. This call is responsible for isolating the controllers, models, routes and other things into their own namespace, away from similar components inside the application. Without this, there is a possibility that the engine's components could "leak" into the application, causing unwanted disruption, or that important engine components could be overridden by similarly named things within the application. One of the examples of such conflicts are helpers. Without calling `isolate_namespace`, engine's helpers would be included in an application's controllers. - -NOTE: It is **highly** recommended that the `isolate_namespace` line be left within the `Engine` class definition. Without it, classes generated in an engine **may** conflict with an application. - -What this isolation of the namespace means is that a model generated by a call to `rails g model` such as `rails g model post` won't be called `Post`, but instead be namespaced and called `Blorgh::Post`. In addition, the table for the model is namespaced, becoming `blorgh_posts`, rather than simply `posts`. Similar to the model namespacing, a controller called `PostsController` becomes `Blorgh::PostsController` and the views for that controller will not be at `app/views/posts`, but `app/views/blorgh/posts` instead. Mailers are namespaced as well. - -Finally, routes will also be isolated within the engine. This is one of the most important parts about namespacing, and is discussed later in the [Routes](#routes) section of this guide. - -#### `app` directory - -Inside the `app` directory are the standard `assets`, `controllers`, `helpers`, `mailers`, `models` and `views` directories that you should be familiar with from an application. The `helpers`, `mailers` and `models` directories are empty and so aren't described in this section. We'll look more into models in a future section, when we're writing the engine. - -Within the `app/assets` directory, there are the `images`, `javascripts` and `stylesheets` directories which, again, you should be familiar with due to their similarity to an application. One difference here however is that each directory contains a sub-directory with the engine name. Because this engine is going to be namespaced, its assets should be too. - -Within the `app/controllers` directory there is a `blorgh` directory and inside that a file called `application_controller.rb`. This file will provide any common functionality for the controllers of the engine. The `blorgh` directory is where the other controllers for the engine will go. By placing them within this namespaced directory, you prevent them from possibly clashing with identically-named controllers within other engines or even within the application. - -NOTE: The `ApplicationController` class inside an engine is named just like a Rails application in order to make it easier for you to convert your applications into engines. - -Lastly, the `app/views` directory contains a `layouts` folder which contains a file at `blorgh/application.html.erb` which allows you to specify a layout for the engine. If this engine is to be used as a stand-alone engine, then you would add any customization to its layout in this file, rather than the application's `app/views/layouts/application.html.erb` file. - -If you don't want to force a layout on to users of the engine, then you can delete this file and reference a different layout in the controllers of your engine. - -#### `bin` directory - -This directory contains one file, `bin/rails`, which enables you to use the `rails` sub-commands and generators just like you would within an application. This means that you will very easily be able to generate new controllers and models for this engine by running commands like this: +By inheriting from the `Rails::Engine` class, this gem notifies Rails that +there's an engine at the specified path, and will correctly mount the engine +inside the application, performing tasks such as adding the `app` directory of +the engine to the load path for models, mailers, controllers and views. + +The `isolate_namespace` method here deserves special notice. This call is +responsible for isolating the controllers, models, routes and other things into +their own namespace, away from similar components inside the application. +Without this, there is a possibility that the engine's components could "leak" +into the application, causing unwanted disruption, or that important engine +components could be overridden by similarly named things within the application. +One of the examples of such conflicts is helpers. Without calling +`isolate_namespace`, the engine's helpers would be included in an application's +controllers. + +NOTE: It is **highly** recommended that the `isolate_namespace` line be left +within the `Engine` class definition. Without it, classes generated in an engine +**may** conflict with an application. + +What this isolation of the namespace means is that a model generated by a call +to `rails g model`, such as `rails g model post`, won't be called `Post`, but +instead be namespaced and called `Blorgh::Post`. In addition, the table for the +model is namespaced, becoming `blorgh_posts`, rather than simply `posts`. +Similar to the model namespacing, a controller called `PostsController` becomes +`Blorgh::PostsController` and the views for that controller will not be at +`app/views/posts`, but `app/views/blorgh/posts` instead. Mailers are namespaced +as well. + +Finally, routes will also be isolated within the engine. This is one of the most +important parts about namespacing, and is discussed later in the +[Routes](#routes) section of this guide. + +#### `app` Directory + +Inside the `app` directory are the standard `assets`, `controllers`, `helpers`, +`mailers`, `models` and `views` directories that you should be familiar with +from an application. The `helpers`, `mailers` and `models` directories are +empty, so they aren't described in this section. We'll look more into models in +a future section, when we're writing the engine. + +Within the `app/assets` directory, there are the `images`, `javascripts` and +`stylesheets` directories which, again, you should be familiar with due to their +similarity to an application. One difference here, however, is that each +directory contains a sub-directory with the engine name. Because this engine is +going to be namespaced, its assets should be too. + +Within the `app/controllers` directory there is a `blorgh` directory that +contains a file called `application_controller.rb`. This file will provide any +common functionality for the controllers of the engine. The `blorgh` directory +is where the other controllers for the engine will go. By placing them within +this namespaced directory, you prevent them from possibly clashing with +identically-named controllers within other engines or even within the +application. + +NOTE: The `ApplicationController` class inside an engine is named just like a +Rails application in order to make it easier for you to convert your +applications into engines. + +Lastly, the `app/views` directory contains a `layouts` folder, which contains a +file at `blorgh/application.html.erb`. This file allows you to specify a layout +for the engine. If this engine is to be used as a stand-alone engine, then you +would add any customization to its layout in this file, rather than the +application's `app/views/layouts/application.html.erb` file. + +If you don't want to force a layout on to users of the engine, then you can +delete this file and reference a different layout in the controllers of your +engine. + +#### `bin` Directory + +This directory contains one file, `bin/rails`, which enables you to use the +`rails` sub-commands and generators just like you would within an application. +This means that you will be able to generate new controllers and models for this +engine very easily by running commands like this: ```bash rails g model ``` -Keeping in mind, of course, that anything generated with these commands inside an engine that has `isolate_namespace` inside the `Engine` class will be namespaced. +Keep in mind, of course, that anything generated with these commands inside of +an engine that has `isolate_namespace` in the `Engine` class will be namespaced. -#### `test` directory +#### `test` Directory -The `test` directory is where tests for the engine will go. To test the engine, there is a cut-down version of a Rails application embedded within it at `test/dummy`. This application will mount the engine in the `test/dummy/config/routes.rb` file: +The `test` directory is where tests for the engine will go. To test the engine, +there is a cut-down version of a Rails application embedded within it at +`test/dummy`. This application will mount the engine in the +`test/dummy/config/routes.rb` file: ```ruby Rails.application.routes.draw do @@ -169,18 +272,25 @@ Rails.application.routes.draw do end ``` -This line mounts the engine at the path `/blorgh`, which will make it accessible through the application only at that path. +This line mounts the engine at the path `/blorgh`, which will make it accessible +through the application only at that path. -In the test directory there is the `test/integration` directory, where integration tests for the engine should be placed. Other directories can be created in the `test` directory as well. For example, you may wish to create a `test/models` directory for your models tests. +Inside the test directory there is the `test/integration` directory, where +integration tests for the engine should be placed. Other directories can be +created in the `test` directory as well. For example, you may wish to create a +`test/models` directory for your model tests. Providing engine functionality ------------------------------ -The engine that this guide covers provides posting and commenting functionality and follows a similar thread to the [Getting Started Guide](getting_started.html), with some new twists. +The engine that this guide covers provides posting and commenting functionality +and follows a similar thread to the [Getting Started +Guide](getting_started.html), with some new twists. -### Generating a post resource +### Generating a Post Resource -The first thing to generate for a blog engine is the `Post` model and related controller. To quickly generate this, you can use the Rails scaffold generator. +The first thing to generate for a blog engine is the `Post` model and related +controller. To quickly generate this, you can use the Rails scaffold generator. ```bash $ rails generate scaffold post title:string text:text @@ -195,7 +305,8 @@ create app/models/blorgh/post.rb invoke test_unit create test/models/blorgh/post_test.rb create test/fixtures/blorgh/posts.yml - route resources :posts +invoke resource_route + route resources :posts invoke scaffold_controller create app/controllers/blorgh/posts_controller.rb invoke erb @@ -220,11 +331,22 @@ invoke css create app/assets/stylesheets/scaffold.css ``` -The first thing that the scaffold generator does is invoke the `active_record` generator, which generates a migration and a model for the resource. Note here, however, that the migration is called `create_blorgh_posts` rather than the usual `create_posts`. This is due to the `isolate_namespace` method called in the `Blorgh::Engine` class's definition. The model here is also namespaced, being placed at `app/models/blorgh/post.rb` rather than `app/models/post.rb` due to the `isolate_namespace` call within the `Engine` class. +The first thing that the scaffold generator does is invoke the `active_record` +generator, which generates a migration and a model for the resource. Note here, +however, that the migration is called `create_blorgh_posts` rather than the +usual `create_posts`. This is due to the `isolate_namespace` method called in +the `Blorgh::Engine` class's definition. The model here is also namespaced, +being placed at `app/models/blorgh/post.rb` rather than `app/models/post.rb` due +to the `isolate_namespace` call within the `Engine` class. -Next, the `test_unit` generator is invoked for this model, generating a model test at `test/models/blorgh/post_test.rb` (rather than `test/models/post_test.rb`) and a fixture at `test/fixtures/blorgh/posts.yml` (rather than `test/fixtures/posts.yml`). +Next, the `test_unit` generator is invoked for this model, generating a model +test at `test/models/blorgh/post_test.rb` (rather than +`test/models/post_test.rb`) and a fixture at `test/fixtures/blorgh/posts.yml` +(rather than `test/fixtures/posts.yml`). -After that, a line for the resource is inserted into the `config/routes.rb` file for the engine. This line is simply `resources :posts`, turning the `config/routes.rb` file for the engine into this: +After that, a line for the resource is inserted into the `config/routes.rb` file +for the engine. This line is simply `resources :posts`, turning the +`config/routes.rb` file for the engine into this: ```ruby Blorgh::Engine.routes.draw do @@ -232,12 +354,22 @@ Blorgh::Engine.routes.draw do end ``` -Note here that the routes are drawn upon the `Blorgh::Engine` object rather than the `YourApp::Application` class. This is so that the engine routes are confined to the engine itself and can be mounted at a specific point as shown in the [test directory](#test-directory) section. It also causes the engine's routes to be isolated from those routes that are within the application. The [Routes](#routes) section of -this guide describes it in details. +Note here that the routes are drawn upon the `Blorgh::Engine` object rather than +the `YourApp::Application` class. This is so that the engine routes are confined +to the engine itself and can be mounted at a specific point as shown in the +[test directory](#test-directory) section. It also causes the engine's routes to +be isolated from those routes that are within the application. The +[Routes](#routes) section of this guide describes it in detail. -Next, the `scaffold_controller` generator is invoked, generating a controller called `Blorgh::PostsController` (at `app/controllers/blorgh/posts_controller.rb`) and its related views at `app/views/blorgh/posts`. This generator also generates a test for the controller (`test/controllers/blorgh/posts_controller_test.rb`) and a helper (`app/helpers/blorgh/posts_controller.rb`). +Next, the `scaffold_controller` generator is invoked, generating a controller +called `Blorgh::PostsController` (at +`app/controllers/blorgh/posts_controller.rb`) and its related views at +`app/views/blorgh/posts`. This generator also generates a test for the +controller (`test/controllers/blorgh/posts_controller_test.rb`) and a helper +(`app/helpers/blorgh/posts_controller.rb`). -Everything this generator has created is neatly namespaced. The controller's class is defined within the `Blorgh` module: +Everything this generator has created is neatly namespaced. The controller's +class is defined within the `Blorgh` module: ```ruby module Blorgh @@ -247,7 +379,8 @@ module Blorgh end ``` -NOTE: The `ApplicationController` class being inherited from here is the `Blorgh::ApplicationController`, not an application's `ApplicationController`. +NOTE: The `ApplicationController` class being inherited from here is the +`Blorgh::ApplicationController`, not an application's `ApplicationController`. The helper inside `app/helpers/blorgh/posts_helper.rb` is also namespaced: @@ -259,38 +392,63 @@ module Blorgh end ``` -This helps prevent conflicts with any other engine or application that may have a post resource as well. +This helps prevent conflicts with any other engine or application that may have +a post resource as well. -Finally, two files that are the assets for this resource are generated, `app/assets/javascripts/blorgh/posts.js` and `app/assets/stylesheets/blorgh/posts.css`. You'll see how to use these a little later. +Finally, the assets for this resource are generated in two files: +`app/assets/javascripts/blorgh/posts.js` and +`app/assets/stylesheets/blorgh/posts.css`. You'll see how to use these a little +later. -By default, the scaffold styling is not applied to the engine as the engine's layout file, `app/views/layouts/blorgh/application.html.erb` doesn't load it. To make this apply, insert this line into the `<head>` tag of this layout: +By default, the scaffold styling is not applied to the engine because the +engine's layout file, `app/views/layouts/blorgh/application.html.erb`, doesn't +load it. To make the scaffold styling apply, insert this line into the `<head>` +tag of this layout: ```erb <%= stylesheet_link_tag "scaffold" %> ``` -You can see what the engine has so far by running `rake db:migrate` at the root of our engine to run the migration generated by the scaffold generator, and then running `rails server` in `test/dummy`. When you open `http://localhost:3000/blorgh/posts` you will see the default scaffold that has been generated. Click around! You've just generated your first engine's first functions. +You can see what the engine has so far by running `rake db:migrate` at the root +of our engine to run the migration generated by the scaffold generator, and then +running `rails server` in `test/dummy`. When you open +`http://localhost:3000/blorgh/posts` you will see the default scaffold that has +been generated. Click around! You've just generated your first engine's first +functions. -If you'd rather play around in the console, `rails console` will also work just like a Rails application. Remember: the `Post` model is namespaced, so to reference it you must call it as `Blorgh::Post`. +If you'd rather play around in the console, `rails console` will also work just +like a Rails application. Remember: the `Post` model is namespaced, so to +reference it you must call it as `Blorgh::Post`. ```ruby >> Blorgh::Post.find(1) => #<Blorgh::Post id: 1 ...> ``` -One final thing is that the `posts` resource for this engine should be the root of the engine. Whenever someone goes to the root path where the engine is mounted, they should be shown a list of posts. This can be made to happen if this line is inserted into the `config/routes.rb` file inside the engine: +One final thing is that the `posts` resource for this engine should be the root +of the engine. Whenever someone goes to the root path where the engine is +mounted, they should be shown a list of posts. This can be made to happen if +this line is inserted into the `config/routes.rb` file inside the engine: ```ruby root to: "posts#index" ``` -Now people will only need to go to the root of the engine to see all the posts, rather than visiting `/posts`. This means that instead of `http://localhost:3000/blorgh/posts`, you only need to go to `http://localhost:3000/blorgh` now. +Now people will only need to go to the root of the engine to see all the posts, +rather than visiting `/posts`. This means that instead of +`http://localhost:3000/blorgh/posts`, you only need to go to +`http://localhost:3000/blorgh` now. -### Generating a comments resource +### Generating a Comments Resource -Now that the engine can create new blog posts, it only makes sense to add commenting functionality as well. To do this, you'll need to generate a comment model, a comment controller and then modify the posts scaffold to display comments and allow people to create new ones. +Now that the engine can create new blog posts, it only makes sense to add +commenting functionality as well. To do this, you'll need to generate a comment +model, a comment controller and then modify the posts scaffold to display +comments and allow people to create new ones. -Run the model generator and tell it to generate a `Comment` model, with the related table having two columns: a `post_id` integer and `text` text column. +From the application root, run the model generator. Tell it to generate a +`Comment` model, with the related table having two columns: a `post_id` integer +and `text` text column. ```bash $ rails generate model Comment post_id:integer text:text @@ -307,20 +465,26 @@ create test/models/blorgh/comment_test.rb create test/fixtures/blorgh/comments.yml ``` -This generator call will generate just the necessary model files it needs, namespacing the files under a `blorgh` directory and creating a model class called `Blorgh::Comment`. Now run the migration to create our blorgh_comments table: +This generator call will generate just the necessary model files it needs, +namespacing the files under a `blorgh` directory and creating a model class +called `Blorgh::Comment`. Now run the migration to create our blorgh_comments +table: ```bash $ rake db:migrate ``` -To show the comments on a post, edit `app/views/blorgh/posts/show.html.erb` and add this line before the "Edit" link: +To show the comments on a post, edit `app/views/blorgh/posts/show.html.erb` and +add this line before the "Edit" link: ```html+erb <h3>Comments</h3> <%= render @post.comments %> ``` -This line will require there to be a `has_many` association for comments defined on the `Blorgh::Post` model, which there isn't right now. To define one, open `app/models/blorgh/post.rb` and add this line into the model: +This line will require there to be a `has_many` association for comments defined +on the `Blorgh::Post` model, which there isn't right now. To define one, open +`app/models/blorgh/post.rb` and add this line into the model: ```ruby has_many :comments @@ -336,15 +500,22 @@ module Blorgh end ``` -NOTE: Because the `has_many` is defined inside a class that is inside the `Blorgh` module, Rails will know that you want to use the `Blorgh::Comment` model for these objects, so there's no need to specify that using the `:class_name` option here. +NOTE: Because the `has_many` is defined inside a class that is inside the +`Blorgh` module, Rails will know that you want to use the `Blorgh::Comment` +model for these objects, so there's no need to specify that using the +`:class_name` option here. -Next, there needs to be a form so that comments can be created on a post. To add this, put this line underneath the call to `render @post.comments` in `app/views/blorgh/posts/show.html.erb`: +Next, there needs to be a form so that comments can be created on a post. To add +this, put this line underneath the call to `render @post.comments` in +`app/views/blorgh/posts/show.html.erb`: ```erb <%= render "blorgh/comments/form" %> ``` -Next, the partial that this line will render needs to exist. Create a new directory at `app/views/blorgh/comments` and in it a new file called `_form.html.erb` which has this content to create the required partial: +Next, the partial that this line will render needs to exist. Create a new +directory at `app/views/blorgh/comments` and in it a new file called +`_form.html.erb` which has this content to create the required partial: ```html+erb <h3>New comment</h3> @@ -357,7 +528,10 @@ Next, the partial that this line will render needs to exist. Create a new direct <% end %> ``` -When this form is submitted, it is going to attempt to perform a `POST` request to a route of `/posts/:post_id/comments` within the engine. This route doesn't exist at the moment, but can be created by changing the `resources :posts` line inside `config/routes.rb` into these lines: +When this form is submitted, it is going to attempt to perform a `POST` request +to a route of `/posts/:post_id/comments` within the engine. This route doesn't +exist at the moment, but can be created by changing the `resources :posts` line +inside `config/routes.rb` into these lines: ```ruby resources :posts do @@ -367,7 +541,8 @@ end This creates a nested route for the comments, which is what the form requires. -The route now exists, but the controller that this route goes to does not. To create it, run this command: +The route now exists, but the controller that this route goes to does not. To +create it, run this command from the application root: ```bash $ rails g controller comments @@ -392,7 +567,10 @@ invoke css create app/assets/stylesheets/blorgh/comments.css ``` -The form will be making a `POST` request to `/posts/:post_id/comments`, which will correspond with the `create` action in `Blorgh::CommentsController`. This action needs to be created and can be done by putting the following lines inside the class definition in `app/controllers/blorgh/comments_controller.rb`: +The form will be making a `POST` request to `/posts/:post_id/comments`, which +will correspond with the `create` action in `Blorgh::CommentsController`. This +action needs to be created, which can be done by putting the following lines +inside the class definition in `app/controllers/blorgh/comments_controller.rb`: ```ruby def create @@ -408,119 +586,182 @@ private end ``` -This is the final part required to get the new comment form working. Displaying the comments however, is not quite right yet. If you were to create a comment right now you would see this error: +This is the final step required to get the new comment form working. Displaying +the comments, however, is not quite right yet. If you were to create a comment +right now, you would see this error: -``` -Missing partial blorgh/comments/comment with {:handlers=>[:erb, :builder], :formats=>[:html], :locale=>[:en, :en]}. Searched in: - * "/Users/ryan/Sites/side_projects/blorgh/test/dummy/app/views" - * "/Users/ryan/Sites/side_projects/blorgh/app/views" +``` +Missing partial blorgh/comments/comment with {:handlers=>[:erb, :builder], +:formats=>[:html], :locale=>[:en, :en]}. Searched in: * +"/Users/ryan/Sites/side_projects/blorgh/test/dummy/app/views" * +"/Users/ryan/Sites/side_projects/blorgh/app/views" ``` -The engine is unable to find the partial required for rendering the comments. Rails looks first in the application's (`test/dummy`) `app/views` directory and then in the engine's `app/views` directory. When it can't find it, it will throw this error. The engine knows to look for `blorgh/comments/comment` because the model object it is receiving is from the `Blorgh::Comment` class. +The engine is unable to find the partial required for rendering the comments. +Rails looks first in the application's (`test/dummy`) `app/views` directory and +then in the engine's `app/views` directory. When it can't find it, it will throw +this error. The engine knows to look for `blorgh/comments/comment` because the +model object it is receiving is from the `Blorgh::Comment` class. -This partial will be responsible for rendering just the comment text, for now. Create a new file at `app/views/blorgh/comments/_comment.html.erb` and put this line inside it: +This partial will be responsible for rendering just the comment text, for now. +Create a new file at `app/views/blorgh/comments/_comment.html.erb` and put this +line inside it: ```erb <%= comment_counter + 1 %>. <%= comment.text %> ``` -The `comment_counter` local variable is given to us by the `<%= render @post.comments %>` call, as it will define this automatically and increment the counter as it iterates through each comment. It's used in this example to display a small number next to each comment when it's created. +The `comment_counter` local variable is given to us by the `<%= render +@post.comments %>` call, which will define it automatically and increment the +counter as it iterates through each comment. It's used in this example to +display a small number next to each comment when it's created. -That completes the comment function of the blogging engine. Now it's time to use it within an application. +That completes the comment function of the blogging engine. Now it's time to use +it within an application. -Hooking into an application +Hooking Into an Application --------------------------- -Using an engine within an application is very easy. This section covers how to mount the engine into an application and the initial setup required, as well as linking the engine to a `User` class provided by the application to provide ownership for posts and comments within the engine. +Using an engine within an application is very easy. This section covers how to +mount the engine into an application and the initial setup required, as well as +linking the engine to a `User` class provided by the application to provide +ownership for posts and comments within the engine. -### Mounting the engine +### Mounting the Engine -First, the engine needs to be specified inside the application's `Gemfile`. If there isn't an application handy to test this out in, generate one using the `rails new` command outside of the engine directory like this: +First, the engine needs to be specified inside the application's `Gemfile`. If +there isn't an application handy to test this out in, generate one using the +`rails new` command outside of the engine directory like this: ```bash $ rails new unicorn ``` -Usually, specifying the engine inside the Gemfile would be done by specifying it as a normal, everyday gem. +Usually, specifying the engine inside the Gemfile would be done by specifying it +as a normal, everyday gem. ```ruby gem 'devise' ``` -However, because you are developing the `blorgh` engine on your local machine, you will need to specify the `:path` option in your `Gemfile`: +However, because you are developing the `blorgh` engine on your local machine, +you will need to specify the `:path` option in your `Gemfile`: ```ruby gem 'blorgh', path: "/path/to/blorgh" ``` -As described earlier, by placing the gem in the `Gemfile` it will be loaded when Rails is loaded, as it will first require `lib/blorgh.rb` in the engine and then `lib/blorgh/engine.rb`, which is the file that defines the major pieces of functionality for the engine. +Then run `bundle` to install the gem. -To make the engine's functionality accessible from within an application, it needs to be mounted in that application's `config/routes.rb` file: +As described earlier, by placing the gem in the `Gemfile` it will be loaded when +Rails is loaded. It will first require `lib/blorgh.rb` from the engine, then +`lib/blorgh/engine.rb`, which is the file that defines the major pieces of +functionality for the engine. + +To make the engine's functionality accessible from within an application, it +needs to be mounted in that application's `config/routes.rb` file: ```ruby mount Blorgh::Engine, at: "/blog" ``` -This line will mount the engine at `/blog` in the application. Making it accessible at `http://localhost:3000/blog` when the application runs with `rails server`. +This line will mount the engine at `/blog` in the application. Making it +accessible at `http://localhost:3000/blog` when the application runs with `rails +server`. -NOTE: Other engines, such as Devise, handle this a little differently by making you specify custom helpers such as `devise_for` in the routes. These helpers do exactly the same thing, mounting pieces of the engines's functionality at a pre-defined path which may be customizable. +NOTE: Other engines, such as Devise, handle this a little differently by making +you specify custom helpers (such as `devise_for`) in the routes. These helpers +do exactly the same thing, mounting pieces of the engines's functionality at a +pre-defined path which may be customizable. ### Engine setup -The engine contains migrations for the `blorgh_posts` and `blorgh_comments` table which need to be created in the application's database so that the engine's models can query them correctly. To copy these migrations into the application use this command: +The engine contains migrations for the `blorgh_posts` and `blorgh_comments` +table which need to be created in the application's database so that the +engine's models can query them correctly. To copy these migrations into the +application use this command: ```bash $ rake blorgh:install:migrations ``` -If you have multiple engines that need migrations copied over, use `railties:install:migrations` instead: +If you have multiple engines that need migrations copied over, use +`railties:install:migrations` instead: ```bash $ rake railties:install:migrations ``` -This command, when run for the first time, will copy over all the migrations from the engine. When run the next time, it will only copy over migrations that haven't been copied over already. The first run for this command will output something such as this: +This command, when run for the first time, will copy over all the migrations +from the engine. When run the next time, it will only copy over migrations that +haven't been copied over already. The first run for this command will output +something such as this: ```bash Copied migration [timestamp_1]_create_blorgh_posts.rb from blorgh Copied migration [timestamp_2]_create_blorgh_comments.rb from blorgh ``` -The first timestamp (`[timestamp_1]`) will be the current time and the second timestamp (`[timestamp_2]`) will be the current time plus a second. The reason for this is so that the migrations for the engine are run after any existing migrations in the application. +The first timestamp (`[timestamp_1]`) will be the current time, and the second +timestamp (`[timestamp_2]`) will be the current time plus a second. The reason +for this is so that the migrations for the engine are run after any existing +migrations in the application. -To run these migrations within the context of the application, simply run `rake db:migrate`. When accessing the engine through `http://localhost:3000/blog`, the posts will be empty. This is because the table created inside the application is different from the one created within the engine. Go ahead, play around with the newly mounted engine. You'll find that it's the same as when it was only an engine. +To run these migrations within the context of the application, simply run `rake +db:migrate`. When accessing the engine through `http://localhost:3000/blog`, the +posts will be empty. This is because the table created inside the application is +different from the one created within the engine. Go ahead, play around with the +newly mounted engine. You'll find that it's the same as when it was only an +engine. -If you would like to run migrations only from one engine, you can do it by specifying `SCOPE`: +If you would like to run migrations only from one engine, you can do it by +specifying `SCOPE`: ```bash rake db:migrate SCOPE=blorgh ``` -This may be useful if you want to revert engine's migrations before removing it. In order to revert all migrations from blorgh engine you can run such code: +This may be useful if you want to revert engine's migrations before removing it. +To revert all migrations from blorgh engine you can run code such as: ```bash rake db:migrate SCOPE=blorgh VERSION=0 ``` -### Using a class provided by the application +### Using a Class Provided by the Application -#### Using a model provided by the application +#### Using a Model Provided by the Application -When an engine is created, it may want to use specific classes from an application to provide links between the pieces of the engine and the pieces of the application. In the case of the `blorgh` engine, making posts and comments have authors would make a lot of sense. +When an engine is created, it may want to use specific classes from an +application to provide links between the pieces of the engine and the pieces of +the application. In the case of the `blorgh` engine, making posts and comments +have authors would make a lot of sense. -A typical application might have a `User` class that would be used to represent authors for a post or a comment. But there could be a case where the application calls this class something different, such as `Person`. For this reason, the engine should not hardcode associations specifically for a `User` class. +A typical application might have a `User` class that would be used to represent +authors for a post or a comment. But there could be a case where the application +calls this class something different, such as `Person`. For this reason, the +engine should not hardcode associations specifically for a `User` class. -To keep it simple in this case, the application will have a class called `User` which will represent the users of the application. It can be generated using this command inside the application: +To keep it simple in this case, the application will have a class called `User` +that represents the users of the application. It can be generated using this +command inside the application: ```bash rails g model user name:string ``` -The `rake db:migrate` command needs to be run here to ensure that our application has the `users` table for future use. +The `rake db:migrate` command needs to be run here to ensure that our +application has the `users` table for future use. -Also, to keep it simple, the posts form will have a new text field called `author_name` where users can elect to put their name. The engine will then take this name and create a new `User` object from it or find one that already has that name, and then associate the post with it. +Also, to keep it simple, the posts form will have a new text field called +`author_name`, where users can elect to put their name. The engine will then +take this name and either create a new `User` object from it, or find one that +already has that name. The engine will then associate the post with the found or +created `User` object. -First, the `author_name` text field needs to be added to the `app/views/blorgh/posts/_form.html.erb` partial inside the engine. This can be added above the `title` field with this code: +First, the `author_name` text field needs to be added to the +`app/views/blorgh/posts/_form.html.erb` partial inside the engine. This can be +added above the `title` field with this code: ```html+erb <div class="field"> @@ -529,7 +770,8 @@ First, the `author_name` text field needs to be added to the `app/views/blorgh/p </div> ``` -Next, we need to update our `Blorgh::PostController#post_params` method to permit the new form parameter: +Next, we need to update our `Blorgh::PostController#post_params` method to +permit the new form parameter: ```ruby def post_params @@ -537,9 +779,15 @@ def post_params end ``` -The `Blorgh::Post` model should then have some code to convert the `author_name` field into an actual `User` object and associate it as that post's `author` before the post is saved. It will also need to have an `attr_accessor` setup for this field so that the setter and getter methods are defined for it. +The `Blorgh::Post` model should then have some code to convert the `author_name` +field into an actual `User` object and associate it as that post's `author` +before the post is saved. It will also need to have an `attr_accessor` set up +for this field, so that the setter and getter methods are defined for it. -To do all this, you'll need to add the `attr_accessor` for `author_name`, the association for the author and the `before_save` call into `app/models/blorgh/post.rb`. The `author` association will be hard-coded to the `User` class for the time being. +To do all this, you'll need to add the `attr_accessor` for `author_name`, the +association for the author and the `before_save` call into +`app/models/blorgh/post.rb`. The `author` association will be hard-coded to the +`User` class for the time being. ```ruby attr_accessor :author_name @@ -553,7 +801,11 @@ private end ``` -By defining that the `author` association's object is represented by the `User` class a link is established between the engine and the application. There needs to be a way of associating the records in the `blorgh_posts` table with the records in the `users` table. Because the association is called `author`, there should be an `author_id` column added to the `blorgh_posts` table. +By representing the `author` association's object with the `User` class, a link +is established between the engine and the application. There needs to be a way +of associating the records in the `blorgh_posts` table with the records in the +`users` table. Because the association is called `author`, there should be an +`author_id` column added to the `blorgh_posts` table. To generate this new column, run this command within the engine: @@ -561,31 +813,41 @@ To generate this new column, run this command within the engine: $ rails g migration add_author_id_to_blorgh_posts author_id:integer ``` -NOTE: Due to the migration's name and the column specification after it, Rails will automatically know that you want to add a column to a specific table and write that into the migration for you. You don't need to tell it any more than this. +NOTE: Due to the migration's name and the column specification after it, Rails +will automatically know that you want to add a column to a specific table and +write that into the migration for you. You don't need to tell it any more than +this. -This migration will need to be run on the application. To do that, it must first be copied using this command: +This migration will need to be run on the application. To do that, it must first +be copied using this command: ```bash $ rake blorgh:install:migrations ``` -Notice here that only _one_ migration was copied over here. This is because the first two migrations were copied over the first time this command was run. +Notice that only _one_ migration was copied over here. This is because the first +two migrations were copied over the first time this command was run. -``` -NOTE Migration [timestamp]_create_blorgh_posts.rb from blorgh has been skipped. Migration with the same name already exists. -NOTE Migration [timestamp]_create_blorgh_comments.rb from blorgh has been skipped. Migration with the same name already exists. -Copied migration [timestamp]_add_author_id_to_blorgh_posts.rb from blorgh +``` +NOTE Migration [timestamp]_create_blorgh_posts.rb from blorgh has been +skipped. Migration with the same name already exists. NOTE Migration +[timestamp]_create_blorgh_comments.rb from blorgh has been skipped. Migration +with the same name already exists. Copied migration +[timestamp]_add_author_id_to_blorgh_posts.rb from blorgh ``` -Run this migration using this command: +Run the migration using: ```bash $ rake db:migrate ``` -Now with all the pieces in place, an action will take place that will associate an author - represented by a record in the `users` table - with a post, represented by the `blorgh_posts` table from the engine. +Now with all the pieces in place, an action will take place that will associate +an author - represented by a record in the `users` table - with a post, +represented by the `blorgh_posts` table from the engine. -Finally, the author's name should be displayed on the post's page. Add this code above the "Title" output inside `app/views/blorgh/posts/show.html.erb`: +Finally, the author's name should be displayed on the post's page. Add this code +above the "Title" output inside `app/views/blorgh/posts/show.html.erb`: ```html+erb <p> @@ -594,13 +856,15 @@ Finally, the author's name should be displayed on the post's page. Add this code </p> ``` -By outputting `@post.author` using the `<%=` tag, the `to_s` method will be called on the object. By default, this will look quite ugly: +By outputting `@post.author` using the `<%=` tag, the `to_s` method will be +called on the object. By default, this will look quite ugly: ``` #<User:0x00000100ccb3b0> ``` -This is undesirable and it would be much better to have the user's name there. To do this, add a `to_s` method to the `User` class within the application: +This is undesirable. It would be much better to have the user's name there. To +do this, add a `to_s` method to the `User` class within the application: ```ruby def to_s @@ -608,50 +872,77 @@ def to_s end ``` -Now instead of the ugly Ruby object output the author's name will be displayed. +Now instead of the ugly Ruby object output, the author's name will be displayed. -#### Using a controller provided by the application +#### Using a Controller Provided by the Application -Because Rails controllers generally share code for things like authentication and accessing session variables, by default they inherit from `ApplicationController`. Rails engines, however are scoped to run independently from the main application, so each engine gets a scoped `ApplicationController`. This namespace prevents code collisions, but often engine controllers should access methods in the main application's `ApplicationController`. An easy way to provide this access is to change the engine's scoped `ApplicationController` to inherit from the main application's `ApplicationController`. For our Blorgh engine this would be done by changing `app/controllers/blorgh/application_controller.rb` to look like: +Because Rails controllers generally share code for things like authentication +and accessing session variables, they inherit from `ApplicationController` by +default. Rails engines, however are scoped to run independently from the main +application, so each engine gets a scoped `ApplicationController`. This +namespace prevents code collisions, but often engine controllers need to access +methods in the main application's `ApplicationController`. An easy way to +provide this access is to change the engine's scoped `ApplicationController` to +inherit from the main application's `ApplicationController`. For our Blorgh +engine this would be done by changing +`app/controllers/blorgh/application_controller.rb` to look like: ```ruby class Blorgh::ApplicationController < ApplicationController end ``` -By default, the engine's controllers inherit from `Blorgh::ApplicationController`. So, after making this change they will have access to the main applications `ApplicationController` as though they were part of the main application. +By default, the engine's controllers inherit from +`Blorgh::ApplicationController`. So, after making this change they will have +access to the main application's `ApplicationController`, as though they were +part of the main application. -This change does require that the engine is run from a Rails application that has an `ApplicationController`. +This change does require that the engine is run from a Rails application that +has an `ApplicationController`. -### Configuring an engine +### Configuring an Engine -This section covers how to make the `User` class configurable, followed by general configuration tips for the engine. +This section covers how to make the `User` class configurable, followed by +general configuration tips for the engine. -#### Setting configuration settings in the application +#### Setting Configuration Settings in the Application -The next step is to make the class that represents a `User` in the application customizable for the engine. This is because, as explained before, that class may not always be `User`. To make this customizable, the engine will have a configuration setting called `author_class` that will be used to specify what the class representing users is inside the application. +The next step is to make the class that represents a `User` in the application +customizable for the engine. This is because that class may not always be +`User`, as previously explained. To make this setting customizable, the engine +will have a configuration setting called `author_class` that will be used to +specify which class represents users inside the application. -To define this configuration setting, you should use a `mattr_accessor` inside the `Blorgh` module for the engine, located at `lib/blorgh.rb` inside the engine. Inside this module, put this line: +To define this configuration setting, you should use a `mattr_accessor` inside +the `Blorgh` module for the engine. Add this line to `lib/blorgh.rb` inside the +engine: ```ruby mattr_accessor :author_class ``` -This method works like its brothers `attr_accessor` and `cattr_accessor`, but provides a setter and getter method on the module with the specified name. To use it, it must be referenced using `Blorgh.author_class`. +This method works like its brothers, `attr_accessor` and `cattr_accessor`, but +provides a setter and getter method on the module with the specified name. To +use it, it must be referenced using `Blorgh.author_class`. -The next step is switching the `Blorgh::Post` model over to this new setting. For the `belongs_to` association inside this model (`app/models/blorgh/post.rb`), it will now become this: +The next step is to switch the `Blorgh::Post` model over to this new setting. +Change the `belongs_to` association inside this model +(`app/models/blorgh/post.rb`) to this: ```ruby belongs_to :author, class_name: Blorgh.author_class ``` -The `set_author` method also located in this class should also use this class: +The `set_author` method in the `Blorgh::Post` model should also use this class: ```ruby self.author = Blorgh.author_class.constantize.find_or_create_by(name: author_name) ``` -To save having to call `constantize` on the `author_class` result all the time, you could instead just override the `author_class` getter method inside the `Blorgh` module in the `lib/blorgh.rb` file to always call `constantize` on the saved value before returning the result: +To save having to call `constantize` on the `author_class` result all the time, +you could instead just override the `author_class` getter method inside the +`Blorgh` module in the `lib/blorgh.rb` file to always call `constantize` on the +saved value before returning the result: ```ruby def self.author_class @@ -665,82 +956,123 @@ This would then turn the above code for `set_author` into this: self.author = Blorgh.author_class.find_or_create_by(name: author_name) ``` -Resulting in something a little shorter, and more implicit in its behavior. The `author_class` method should always return a `Class` object. +Resulting in something a little shorter, and more implicit in its behavior. The +`author_class` method should always return a `Class` object. -Since we changed the `author_class` method to no longer return a -`String` but a `Class` we must also modify our `belongs_to` definition -in the `Blorgh::Post` model: +Since we changed the `author_class` method to return a `String` instead of a +`Class`, we must also modify our `belongs_to` definition in the `Blorgh::Post` +model: ```ruby belongs_to :author, class_name: Blorgh.author_class.to_s ``` -To set this configuration setting within the application, an initializer should be used. By using an initializer, the configuration will be set up before the application starts and calls the engine's models which may depend on this configuration setting existing. +To set this configuration setting within the application, an initializer should +be used. By using an initializer, the configuration will be set up before the +application starts and calls the engine's models, which may depend on this +configuration setting existing. -Create a new initializer at `config/initializers/blorgh.rb` inside the application where the `blorgh` engine is installed and put this content in it: +Create a new initializer at `config/initializers/blorgh.rb` inside the +application where the `blorgh` engine is installed and put this content in it: ```ruby Blorgh.author_class = "User" ``` -WARNING: It's very important here to use the `String` version of the class, rather than the class itself. If you were to use the class, Rails would attempt to load that class and then reference the related table, which could lead to problems if the table wasn't already existing. Therefore, a `String` should be used and then converted to a class using `constantize` in the engine later on. +WARNING: It's very important here to use the `String` version of the class, +rather than the class itself. If you were to use the class, Rails would attempt +to load that class and then reference the related table. This could lead to +problems if the table wasn't already existing. Therefore, a `String` should be +used and then converted to a class using `constantize` in the engine later on. -Go ahead and try to create a new post. You will see that it works exactly in the same way as before, except this time the engine is using the configuration setting in `config/initializers/blorgh.rb` to learn what the class is. +Go ahead and try to create a new post. You will see that it works exactly in the +same way as before, except this time the engine is using the configuration +setting in `config/initializers/blorgh.rb` to learn what the class is. -There are now no strict dependencies on what the class is, only what the API for the class must be. The engine simply requires this class to define a `find_or_create_by` method which returns an object of that class to be associated with a post when it's created. This object, of course, should have some sort of identifier by which it can be referenced. +There are now no strict dependencies on what the class is, only what the API for +the class must be. The engine simply requires this class to define a +`find_or_create_by` method which returns an object of that class, to be +associated with a post when it's created. This object, of course, should have +some sort of identifier by which it can be referenced. -#### General engine configuration +#### General Engine Configuration -Within an engine, there may come a time where you wish to use things such as initializers, internationalization or other configuration options. The great news is that these things are entirely possible because a Rails engine shares much the same functionality as a Rails application. In fact, a Rails application's functionality is actually a superset of what is provided by engines! +Within an engine, there may come a time where you wish to use things such as +initializers, internationalization or other configuration options. The great +news is that these things are entirely possible, because a Rails engine shares +much the same functionality as a Rails application. In fact, a Rails +application's functionality is actually a superset of what is provided by +engines! If you wish to use an initializer - code that should run before the engine is loaded - the place for it is the `config/initializers` folder. This directory's -functionality is explained in the -[Initializers section](configuring.html#initializers) of the Configuring guide, -and works precisely the same way as the `config/initializers` directory inside -an application. Same goes for if you want to use a standard initializer. +functionality is explained in the [Initializers +section](configuring.html#initializers) of the Configuring guide, and works +precisely the same way as the `config/initializers` directory inside an +application. The same thing goes if you want to use a standard initializer. -For locales, simply place the locale files in the `config/locales` directory, just like you would in an application. +For locales, simply place the locale files in the `config/locales` directory, +just like you would in an application. -Testing an engine ------------------ +Testing an engine ----------------- -When an engine is generated there is a smaller dummy application created inside it at `test/dummy`. This application is used as a mounting point for the engine to make testing the engine extremely simple. You may extend this application by generating controllers, models or views from within the directory, and then use those to test your engine. +When an engine is generated, there is a smaller dummy application created inside +it at `test/dummy`. This application is used as a mounting point for the engine, +to make testing the engine extremely simple. You may extend this application by +generating controllers, models or views from within the directory, and then use +those to test your engine. -The `test` directory should be treated like a typical Rails testing environment, allowing for unit, functional and integration tests. +The `test` directory should be treated like a typical Rails testing environment, +allowing for unit, functional and integration tests. -### Functional tests +### Functional Tests -A matter worth taking into consideration when writing functional tests is that the tests are going to be running on an application - the `test/dummy` application - rather than your engine. This is due to the setup of the testing environment; an engine needs an application as a host for testing its main functionality, especially controllers. This means that if you were to make a typical `GET` to a controller in a controller's functional test like this: +A matter worth taking into consideration when writing functional tests is that +the tests are going to be running on an application - the `test/dummy` +application - rather than your engine. This is due to the setup of the testing +environment; an engine needs an application as a host for testing its main +functionality, especially controllers. This means that if you were to make a +typical `GET` to a controller in a controller's functional test like this: ```ruby get :index ``` -It may not function correctly. This is because the application doesn't know how to route these requests to the engine unless you explicitly tell it **how**. To do this, you must pass the `:use_route` option (as a parameter) on these requests also: +It may not function correctly. This is because the application doesn't know how +to route these requests to the engine unless you explicitly tell it **how**. To +do this, you must also pass the `:use_route` option as a parameter on these +requests: ```ruby get :index, use_route: :blorgh ``` -This tells the application that you still want to perform a `GET` request to the `index` action of this controller, just that you want to use the engine's route to get there, rather than the application. +This tells the application that you still want to perform a `GET` request to the +`index` action of this controller, but you want to use the engine's route to get +there, rather than the application's one. Improving engine functionality ------------------------------ -This section explains how to add and/or override engine MVC functionality in the main Rails application. +This section explains how to add and/or override engine MVC functionality in the +main Rails application. ### Overriding Models and Controllers -Engine model and controller classes can be extended by open classing them in the main Rails application (since model and controller classes are just Ruby classes that inherit Rails specific functionality). Open classing an Engine class redefines it for use in the main application. This is usually implemented by using the decorator pattern. +Engine model and controller classes can be extended by open classing them in the +main Rails application (since model and controller classes are just Ruby classes +that inherit Rails specific functionality). Open classing an Engine class +redefines it for use in the main application. This is usually implemented by +using the decorator pattern. -For simple class modifications use `Class#class_eval`, and for complex class modifications, consider using `ActiveSupport::Concern`. +For simple class modifications, use `Class#class_eval`. For complex class +modifications, consider using `ActiveSupport::Concern`. -#### A note on Decorators and loading code +#### A note on Decorators and Loading Code Because these decorators are not referenced by your Rails application itself, -Rails' autoloading system will not kick in and load your decorators. This -means that you need to require them yourself. +Rails' autoloading system will not kick in and load your decorators. This means +that you need to require them yourself. Here is some sample code to do this: @@ -764,7 +1096,7 @@ that isn't referenced by your main application. #### Implementing Decorator Pattern Using Class#class_eval -**Adding** `Post#time_since_created`, +**Adding** `Post#time_since_created`: ```ruby # MyApp/app/decorators/models/blorgh/post_decorator.rb @@ -785,7 +1117,7 @@ end ``` -**Overriding** `Post#summary` +**Overriding** `Post#summary`: ```ruby # MyApp/app/decorators/models/blorgh/post_decorator.rb @@ -810,9 +1142,13 @@ end #### Implementing Decorator Pattern Using ActiveSupport::Concern -Using `Class#class_eval` is great for simple adjustments, but for more complex class modifications, you might want to consider using [`ActiveSupport::Concern`](http://edgeapi.rubyonrails.org/classes/ActiveSupport/Concern.html). ActiveSupport::Concern manages load order of interlinked dependent modules and classes at run time allowing you to significantly modularize your code. +Using `Class#class_eval` is great for simple adjustments, but for more complex +class modifications, you might want to consider using [`ActiveSupport::Concern`] +(http://edgeapi.rubyonrails.org/classes/ActiveSupport/Concern.html). +ActiveSupport::Concern manages load order of interlinked dependent modules and +classes at run time allowing you to significantly modularize your code. -**Adding** `Post#time_since_created` and **Overriding** `Post#summary` +**Adding** `Post#time_since_created` and **Overriding** `Post#summary`: ```ruby # MyApp/app/models/blorgh/post.rb @@ -845,7 +1181,7 @@ module Blorgh::Concerns::Models::Post extend ActiveSupport::Concern # 'included do' causes the included code to be evaluated in the - # context where it is included (post.rb), rather than be + # context where it is included (post.rb), rather than being # executed in the module's context (blorgh/concerns/models/post). included do attr_accessor :author_name @@ -871,15 +1207,23 @@ module Blorgh::Concerns::Models::Post end ``` -### Overriding views +### Overriding Views -When Rails looks for a view to render, it will first look in the `app/views` directory of the application. If it cannot find the view there, then it will check in the `app/views` directories of all engines which have this directory. +When Rails looks for a view to render, it will first look in the `app/views` +directory of the application. If it cannot find the view there, it will check in +the `app/views` directories of all engines that have this directory. -When the application is asked to render the view for `Blorgh::PostsController`'s index action, it will look the path `app/views/blorgh/posts/index.html.erb`, first within the application. If it cannot find it, it will look inside the engine. +When the application is asked to render the view for `Blorgh::PostsController`'s +index action, it will first look for the path +`app/views/blorgh/posts/index.html.erb` within the application. If it cannot +find it, it will look inside the engine. -You can override this view in the application by simply creating a new file at `app/views/blorgh/posts/index.html.erb`. Then you can completely change what this view would normally output. +You can override this view in the application by simply creating a new file at +`app/views/blorgh/posts/index.html.erb`. Then you can completely change what +this view would normally output. -Try this now by creating a new file at `app/views/blorgh/posts/index.html.erb` and put this content in it: +Try this now by creating a new file at `app/views/blorgh/posts/index.html.erb` +and put this content in it: ```html+erb <h1>Posts</h1> @@ -894,9 +1238,13 @@ Try this now by creating a new file at `app/views/blorgh/posts/index.html.erb` a ### Routes -Routes inside an engine are, by default, isolated from the application. This is done by the `isolate_namespace` call inside the `Engine` class. This essentially means that the application and its engines can have identically named routes and they will not clash. +Routes inside an engine are isolated from the application by default. This is +done by the `isolate_namespace` call inside the `Engine` class. This essentially +means that the application and its engines can have identically named routes and +they will not clash. -Routes inside an engine are drawn on the `Engine` class within `config/routes.rb`, like this: +Routes inside an engine are drawn on the `Engine` class within +`config/routes.rb`, like this: ```ruby Blorgh::Engine.routes.draw do @@ -904,43 +1252,71 @@ Blorgh::Engine.routes.draw do end ``` -By having isolated routes such as this, if you wish to link to an area of an engine from within an application, you will need to use the engine's routing proxy method. Calls to normal routing methods such as `posts_path` may end up going to undesired locations if both the application and the engine both have such a helper defined. +By having isolated routes such as this, if you wish to link to an area of an +engine from within an application, you will need to use the engine's routing +proxy method. Calls to normal routing methods such as `posts_path` may end up +going to undesired locations if both the application and the engine have such a +helper defined. -For instance, the following example would go to the application's `posts_path` if that template was rendered from the application, or the engine's `posts_path` if it was rendered from the engine: +For instance, the following example would go to the application's `posts_path` +if that template was rendered from the application, or the engine's `posts_path` +if it was rendered from the engine: ```erb <%= link_to "Blog posts", posts_path %> ``` -To make this route always use the engine's `posts_path` routing helper method, we must call the method on the routing proxy method that shares the same name as the engine. +To make this route always use the engine's `posts_path` routing helper method, +we must call the method on the routing proxy method that shares the same name as +the engine. ```erb <%= link_to "Blog posts", blorgh.posts_path %> ``` -If you wish to reference the application inside the engine in a similar way, use the `main_app` helper: +If you wish to reference the application inside the engine in a similar way, use +the `main_app` helper: ```erb <%= link_to "Home", main_app.root_path %> ``` -If you were to use this inside an engine, it would **always** go to the application's root. If you were to leave off the `main_app` "routing proxy" method call, it could potentially go to the engine's or application's root, depending on where it was called from. +If you were to use this inside an engine, it would **always** go to the +application's root. If you were to leave off the `main_app` "routing proxy" +method call, it could potentially go to the engine's or application's root, +depending on where it was called from. -If a template is rendered from within an engine and it's attempting to use one of the application's routing helper methods, it may result in an undefined method call. If you encounter such an issue, ensure that you're not attempting to call the application's routing methods without the `main_app` prefix from within the engine. +If a template rendered from within an engine attempts to use one of the +application's routing helper methods, it may result in an undefined method call. +If you encounter such an issue, ensure that you're not attempting to call the +application's routing methods without the `main_app` prefix from within the +engine. ### Assets -Assets within an engine work in an identical way to a full application. Because the engine class inherits from `Rails::Engine`, the application will know to look up in the engine's `app/assets` and `lib/assets` directories for potential assets. +Assets within an engine work in an identical way to a full application. Because +the engine class inherits from `Rails::Engine`, the application will know to +look up assets in the engine's 'app/assets' and 'lib/assets' directories. -Much like all the other components of an engine, the assets should also be namespaced. This means if you have an asset called `style.css`, it should be placed at `app/assets/stylesheets/[engine name]/style.css`, rather than `app/assets/stylesheets/style.css`. If this asset wasn't namespaced, then there is a possibility that the host application could have an asset named identically, in which case the application's asset would take precedence and the engine's one would be all but ignored. +Like all of the other components of an engine, the assets should be namespaced. +This means that if you have an asset called `style.css`, it should be placed at +`app/assets/stylesheets/[engine name]/style.css`, rather than +`app/assets/stylesheets/style.css`. If this asset isn't namespaced, there is a +possibility that the host application could have an asset named identically, in +which case the application's asset would take precedence and the engine's one +would be ignored. -Imagine that you did have an asset located at `app/assets/stylesheets/blorgh/style.css` To include this asset inside an application, just use `stylesheet_link_tag` and reference the asset as if it were inside the engine: +Imagine that you did have an asset located at +`app/assets/stylesheets/blorgh/style.css` To include this asset inside an +application, just use `stylesheet_link_tag` and reference the asset as if it +were inside the engine: ```erb <%= stylesheet_link_tag "blorgh/style.css" %> ``` -You can also specify these assets as dependencies of other assets using the Asset Pipeline require statements in processed files: +You can also specify these assets as dependencies of other assets using Asset +Pipeline require statements in processed files: ``` /* @@ -948,16 +1324,21 @@ You can also specify these assets as dependencies of other assets using the Asse */ ``` -INFO. Remember that in order to use languages like Sass or CoffeeScript, you should add the relevant library to your engine's `.gemspec`. +INFO. Remember that in order to use languages like Sass or CoffeeScript, you +should add the relevant library to your engine's `.gemspec`. ### Separate Assets & Precompiling -There are some situations where your engine's assets are not required by the host application. For example, say that you've created -an admin functionality that only exists for your engine. In this case, the host application doesn't need to require `admin.css` -or `admin.js`. Only the gem's admin layout needs these assets. It doesn't make sense for the host app to include `"blorgh/admin.css"` in it's stylesheets. In this situation, you should explicitly define these assets for precompilation. -This tells sprockets to add your engine assets when `rake assets:precompile` is ran. +There are some situations where your engine's assets are not required by the +host application. For example, say that you've created an admin functionality +that only exists for your engine. In this case, the host application doesn't +need to require `admin.css` or `admin.js`. Only the gem's admin layout needs +these assets. It doesn't make sense for the host app to include +`"blorgh/admin.css"` in its stylesheets. In this situation, you should +explicitly define these assets for precompilation. This tells sprockets to add +your engine assets when `rake assets:precompile` is triggered. -You can define assets for precompilation in `engine.rb` +You can define assets for precompilation in `engine.rb`: ```ruby initializer "blorgh.assets.precompile" do |app| @@ -965,15 +1346,15 @@ initializer "blorgh.assets.precompile" do |app| end ``` -For more information, read the [Asset Pipeline guide](asset_pipeline.html) +For more information, read the [Asset Pipeline guide](asset_pipeline.html). -### Other gem dependencies +### Other Gem Dependencies -Gem dependencies inside an engine should be specified inside the -`.gemspec` file at the root of the engine. The reason is that the engine may -be installed as a gem. If dependencies were to be specified inside the `Gemfile`, -these would not be recognized by a traditional gem install and so they would not -be installed, causing the engine to malfunction. +Gem dependencies inside an engine should be specified inside the `.gemspec` file +at the root of the engine. The reason is that the engine may be installed as a +gem. If dependencies were to be specified inside the `Gemfile`, these would not +be recognized by a traditional gem install and so they would not be installed, +causing the engine to malfunction. To specify a dependency that should be installed with the engine during a traditional `gem install`, specify it inside the `Gem::Specification` block @@ -991,11 +1372,12 @@ s.add_development_dependency "moo" ``` Both kinds of dependencies will be installed when `bundle install` is run inside -the application. The development dependencies for the gem will only be used when -the tests for the engine are running. +of the application. The development dependencies for the gem will only be used +when the tests for the engine are running. Note that if you want to immediately require dependencies when the engine is -required, you should require them before the engine's initialization. For example: +required, you should require them before the engine's initialization. For +example: ```ruby require 'other_engine/engine' diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md index e8279b9c0c..ec4a255398 100644 --- a/guides/source/form_helpers.md +++ b/guides/source/form_helpers.md @@ -342,7 +342,7 @@ If you have created namespaced routes, `form_for` has a nifty shorthand for that form_for [:admin, @article] ``` -will create a form that submits to the articles controller inside the admin namespace (submitting to `admin_article_path(@article)` in the case of an update). If you have several levels of namespacing then the syntax is similar: +will create a form that submits to the `ArticlesController` inside the admin namespace (submitting to `admin_article_path(@article)` in the case of an update). If you have several levels of namespacing then the syntax is similar: ```ruby form_for [:admin, :management, @article] diff --git a/guides/source/generators.md b/guides/source/generators.md index e9c8ef0225..4a5377c206 100644 --- a/guides/source/generators.md +++ b/guides/source/generators.md @@ -560,7 +560,7 @@ This method also takes a block: ```ruby vendor "seeds.rb" do - "puts 'in ur app, seeding ur database'" + "puts 'in your app, seeding your database'" end ``` diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index b57441b1c3..afb3bb22bf 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -86,7 +86,7 @@ current version of Ruby installed: ```bash $ ruby -v -ruby 2.0.0p247 +ruby 2.0.0p353 ``` To install Rails, use the `gem install` command provided by RubyGems: @@ -398,7 +398,7 @@ If you refresh <http://localhost:3000/posts/new> now, you'll get a new error: This error indicates that Rails cannot find the `new` action inside the `PostsController` that you just generated. This is because when controllers are generated in Rails -they are empty by default, unless you tell it you wanted actions during the +they are empty by default, unless you tell it your wanted actions during the generation process. To manually define an action inside a controller, all you need to do is to @@ -1703,8 +1703,8 @@ Deleting Comments ----------------- Another important feature of a blog is being able to delete spam comments. To do -this, we need to implement a link of some sort in the view and a `DELETE` action -in the `CommentsController`. +this, we need to implement a link of some sort in the view and a `destroy` +action in the `CommentsController`. So first, let's add the delete link in the `app/views/comments/_comment.html.erb` partial: @@ -1729,7 +1729,7 @@ So first, let's add the delete link in the Clicking this new "Destroy Comment" link will fire off a `DELETE /posts/:post_id/comments/:id` to our `CommentsController`, which can then use -this to find the comment we want to delete, so let's add a destroy action to our +this to find the comment we want to delete, so let's add a `destroy` action to our controller (`app/controllers/comments_controller.rb`): ```ruby diff --git a/guides/source/i18n.md b/guides/source/i18n.md index 6f79b3ddd7..156ec7435c 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -101,7 +101,7 @@ This means, that in the `:en` locale, the key _hello_ will map to the _Hello wor The I18n library will use **English** as a **default locale**, i.e. if you don't set a different locale, `:en` will be used for looking up translations. -NOTE: The i18n library takes a **pragmatic approach** to locale keys (after [some discussion](http://groups.google.com/group/rails-i18n/browse_thread/thread/14dede2c7dbe9470/80eec34395f64f3c?hl=en)), including only the _locale_ ("language") part, like `:en`, `:pl`, not the _region_ part, like `:en-US` or `:en-GB`, which are traditionally used for separating "languages" and "regional setting" or "dialects". Many international applications use only the "language" element of a locale such as `:cs`, `:th` or `:es` (for Czech, Thai and Spanish). However, there are also regional differences within different language groups that may be important. For instance, in the `:en-US` locale you would have $ as a currency symbol, while in `:en-GB`, you would have £. Nothing stops you from separating regional and other settings in this way: you just have to provide full "English - United Kingdom" locale in a `:en-GB` dictionary. Various [Rails I18n plugins](http://rails-i18n.org/wiki) such as [Globalize3](https://github.com/svenfuchs/globalize3) may help you implement it. +NOTE: The i18n library takes a **pragmatic approach** to locale keys (after [some discussion](http://groups.google.com/group/rails-i18n/browse_thread/thread/14dede2c7dbe9470/80eec34395f64f3c?hl=en)), including only the _locale_ ("language") part, like `:en`, `:pl`, not the _region_ part, like `:en-US` or `:en-GB`, which are traditionally used for separating "languages" and "regional setting" or "dialects". Many international applications use only the "language" element of a locale such as `:cs`, `:th` or `:es` (for Czech, Thai and Spanish). However, there are also regional differences within different language groups that may be important. For instance, in the `:en-US` locale you would have $ as a currency symbol, while in `:en-GB`, you would have £. Nothing stops you from separating regional and other settings in this way: you just have to provide full "English - United Kingdom" locale in a `:en-GB` dictionary. Various [Rails I18n plugins](http://rails-i18n.org/wiki) such as [Globalize3](https://github.com/globalize/globalize) may help you implement it. The **translations load path** (`I18n.load_path`) is just a Ruby Array of paths to your translation files that will be loaded automatically and available in your application. You can pick whatever directory and translation file naming scheme makes sense for you. @@ -833,6 +833,7 @@ So, for example, instead of the default error message `"can not be blank"` you c | confirmation | - | :confirmation | - | | acceptance | - | :accepted | - | | presence | - | :blank | - | +| absence | - | :present | - | | length | :within, :in | :too_short | count | | length | :within, :in | :too_long | count | | length | :is | :wrong_length | count | diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md index f4dab57aa5..c2d9772063 100644 --- a/guides/source/layouts_and_rendering.md +++ b/guides/source/layouts_and_rendering.md @@ -404,7 +404,7 @@ class ProductsController < ApplicationController end ``` -With this declaration, all of the views rendered by the products controller will use `app/views/layouts/inventory.html.erb` as their layout. +With this declaration, all of the views rendered by the `ProductsController` will use `app/views/layouts/inventory.html.erb` as their layout. To assign a specific layout for the entire application, use a `layout` declaration in your `ApplicationController` class: diff --git a/guides/source/migrations.md b/guides/source/migrations.md index 71a177bca7..5d5c2724b1 100644 --- a/guides/source/migrations.md +++ b/guides/source/migrations.md @@ -297,10 +297,10 @@ You can append as many column name/type pairs as you want. You can also specify some options just after the field type between curly braces. You can use the following modifiers: -* `limit` Sets the maximum size of the `string/text/binary/integer` fields -* `precision` Defines the precision for the `decimal` fields -* `scale` Defines the scale for the `decimal` fields -* `polymorphic` Adds a `type` column for `belongs_to` associations +* `limit` Sets the maximum size of the `string/text/binary/integer` fields. +* `precision` Defines the precision for the `decimal` fields, representing the total number of digits in the number. +* `scale` Defines the scale for the `decimal` fields, representing the number of digits after the decimal point. +* `polymorphic` Adds a `type` column for `belongs_to` associations. * `null` Allows or disallows `NULL` values in the column. For instance, running: diff --git a/guides/source/rails_application_templates.md b/guides/source/rails_application_templates.md index 711d910184..e4222e1283 100644 --- a/guides/source/rails_application_templates.md +++ b/guides/source/rails_application_templates.md @@ -78,7 +78,7 @@ end Adds the given source to the generated application's `Gemfile`. -For example, if you need to source a gem from "http://code.whytheluckystiff.net": +For example, if you need to source a gem from `"http://code.whytheluckystiff.net"`: ```ruby add_source "http://code.whytheluckystiff.net" diff --git a/guides/source/security.md b/guides/source/security.md index c698959a2c..21cc3deb8a 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -230,13 +230,15 @@ Or the attacker places the code into the onmouseover event handler of an image: <img src="http://www.harmless.com/img" width="400" height="400" onmouseover="..." /> ``` -There are many other possibilities, including Ajax to attack the victim in the background.
The _solution to this is including a security token in non-GET requests_ which check on the server-side. In Rails 2 or higher, this is a one-liner in the application controller: +There are many other possibilities, like using a `<script>` tag to make a cross-site request to a URL with a JSONP or JavaScript response. The response is executable code that the attacker can find a way to run, possibly extracting sensitive data. To protect against this data leakage, we disallow cross-site `<script>` tags. Only Ajax requests may have JavaScript responses since XmlHttpRequest is subject to the browser Same-Origin policy - meaning only your site can initiate the request. + +To protect against all other forged requests, we introduce a _required security token_ that our site knows but other sites don't know. We include the security token in requests and verify it on the server. This is a one-liner in your application controller: ```ruby -protect_from_forgery secret: "123456789012345678901234567890..." +protect_from_forgery ``` -This will automatically include a security token, calculated from the current session and the server-side secret, in all forms and Ajax requests generated by Rails. You won't need the secret, if you use CookieStorage as session storage. If the security token doesn't match what was expected, the session will be reset. **Note:** In Rails versions prior to 3.0.4, this raised an `ActionController::InvalidAuthenticityToken` error. +This will automatically include a security token in all forms and Ajax requests generated by Rails. If the security token doesn't match what was expected, the session will be reset. It is common to use persistent cookies to store user information, with `cookies.permanent` for example. In this case, the cookies will not be cleared and the out of the box CSRF protection will not be effective. If you are using a different cookie store than the session for this information, you must handle what to do with it yourself: diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index de06ab291f..2f0f3573fb 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -27,6 +27,31 @@ Upgrading from Rails 4.0 to Rails 4.1 NOTE: This section is a work in progress. +### CSRF protection from remote `<script>` tags + +Or, "whaaat my tests are failing!!!?" + +Cross-site request forgery (CSRF) protection now covers GET requests with +JavaScript responses, too. That prevents a third-party site from referencing +your JavaScript URL and attempting to run it to extract sensitive data. + +This means that your functional and integration tests that use + +```ruby +get :index, format: :js +``` + +will now trigger CSRF protection. Switch to + +```ruby +xhr :get, :index, format: :js +``` + +to explicitly test an XmlHttpRequest. + +If you really mean to load JavaScript from remote `<script>` tags, skip CSRF +protection on that action. + ### Spring If you want to use Spring as your application preloader you need to: @@ -39,9 +64,36 @@ 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). +### `config/secrets.yml` + +If you want to use the new `secrets.yml` convention to store your application's +secrets, you need to: + +1. Create a `secrets.yml` file in your `config` folder with the following content: + + ```yaml + development: + secret_key_base: + + test: + secret_key_base: + + production: + secret_key_base: + ``` + +2. Copy the existing `secret_key_base` from the `secret_token.rb` initializer to + `secrets.yml` under the `production` section. + +3. Remove the `secret_token.rb` initializer. + +4. Use `rake secret` to generate new keys for the `development` and `test` sections. + +5. Restart your server. + ### Changes in JSON handling -The are a few major changes related to JSON handling in Rails 4.1. +There are a few major changes related to JSON handling in Rails 4.1. #### MultiJSON removal @@ -93,6 +145,51 @@ If you application depends on one of these features, you can get them back by adding the [`activesupport-json_encoder`](https://github.com/rails/activesupport-json_encoder) gem to your Gemfile. +### Usage of `return` within inline callback blocks + +Previously, Rails allowed inline callback blocks to use `return` this way: + +```ruby +class ReadOnlyModel < ActiveRecord::Base + before_save { return false } # BAD +end +``` + +This behaviour was never intentionally supported. Due to a change in the internals +of `ActiveSupport::Callbacks`, this is no longer allowed in Rails 4.1. Using a +`return` statement in an inline callback block causes a `LocalJumpError` to +be raised when the callback is executed. + +Inline callback blocks using `return` can be refactored to evaluate to the +returned value: + +```ruby +class ReadOnlyModel < ActiveRecord::Base + before_save { false } # GOOD +end +``` + +Alternatively, if `return` is preferred it is recommended to explicitly define +a method: + +```ruby +class ReadOnlyModel < ActiveRecord::Base + before_save :before_save_callback # GOOD + + private + def before_save_callback + return false + end +end +``` + +This change applies to most places in Rails where callbacks are used, including +Active Record and Active Model callbacks, as well as filters in Action +Controller (e.g. `before_action`). + +See [this pull request](https://github.com/rails/rails/pull/13271) for more +details. + ### Methods defined in Active Record fixtures Rails 4.1 evaluates each fixture's ERB in a separate context, so helper methods @@ -111,6 +208,40 @@ end ActiveRecord::FixtureSet.context_class.send :include, FixtureFileHelpers ``` +### I18n enforcing available locales + +Rails 4.1 now defaults the I18n option `enforce_available_locales` to `true`, +meaning that it will make sure that all locales passed to it must be declared in +the `available_locales` list. + +To disable it (and allow I18n to accept *any* locale option) add the following +configuration to your application: + +```ruby +config.i18n.enforce_available_locales = false +``` + +Note that this option was added as a security measure, to ensure user input could +not be used as locale information unless previously known, so it's recommended not +to disable this option unless you have a strong reason for doing so. + +### Mutator methods called on Relation + +`Relation` no longer has mutator methods like `#map!` and `#delete_if`. Convert +to an `Array` by calling `#to_a` before using these methods. + +It intends to prevent odd bugs and confusion in code that call mutator +methods directly on the `Relation`. + +```ruby +# Instead of this +Author.where(name: 'Hank Moody').compact! + +# Now you have to do this +authors = Author.where(name: 'Hank Moody').to_a +authors.compact! +``` + Upgrading from Rails 3.2 to Rails 4.0 ------------------------------------- diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md index 301e0e7e6c..3c04204c29 100644 --- a/guides/source/working_with_javascript_in_rails.md +++ b/guides/source/working_with_javascript_in_rails.md @@ -169,7 +169,7 @@ This will generate the following HTML: </form> ``` -Note the `data-remote='true'`. Now, the form will be submitted by Ajax rather +Note the `data-remote="true"`. Now, the form will be submitted by Ajax rather than by the browser's normal submit mechanism. You probably don't want to just sit there with a filled out `<form>`, though. @@ -194,7 +194,17 @@ is very similar to `form_for`. It has a `:remote` option that you can use like this: ```erb -<%= form_tag('/posts', remote: true) %> +<%= form_tag('/posts', remote: true) do %> + ... +<% end %> +``` + +This will generate the following HTML: + +```html +<form accept-charset="UTF-8" action="/posts" data-remote="true" method="post"> + ... +</form> ``` Everything else is the same as `form_for`. See its documentation for full @@ -299,10 +309,10 @@ The `app/views/users/_user.html.erb` partial contains the following: The top portion of the index page displays the users. The bottom portion provides a form to create a new user. -The bottom form will call the create action on the Users controller. Because +The bottom form will call the `create` action on the `UsersController`. Because the form's remote option is set to true, the request will be posted to the -users controller as an Ajax request, looking for JavaScript. In order to -service that request, the create action of your controller would look like +`UsersController` as an Ajax request, looking for JavaScript. In order to +serve that request, the `create` action of your controller would look like this: ```ruby diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 3813ef7909..a83c2b2355 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,15 +1,65 @@ +* Configure `secrets.yml` and `database.yml` to read configuration + from the system environment by default for production. + + *José Valim* + +* `config.assets.raise_runtime_errors` is set to true by default + + This option has been introduced in + [sprockets-rails#100][https://github.com/rails/sprockets-rails/pull/100] + and defaults to true in new applications in development. + + *Richard Schneeman* + +* Generates `html` and `text` templates for mailers by default. + + *Kassio Borges* + +* Move `secret_key_base` from `config/initializers/secret_token.rb` + to `config/secrets.yml`. + + `secret_key_base` is now saved in `Rails.application.secrets.secret_key_base` + and it fallbacks to the value of `config.secret_key_base` when it is not + present in `config/secrets.yml`. + + `config/initializers/secret_token.rb` is not generated by default + in new applications. + + *Guillermo Iguaran* + +* Generate a new `secrets.yml` file in the `config` folder for new + applications. By default, this file contains the application's `secret_key_base`, + but it could also be used to store other secrets such as access keys for external + APIs. + + The secrets added to this file will be accessible via `Rails.application.secrets`. + For example, with the following `secrets.yml`: + + development: + secret_key_base: 3b7cd727ee24e8444053437c36cc66c3 + some_api_key: SOMEKEY + + `Rails.application.secrets.some_api_key` will return `SOMEKEY` in the development + environment. + + *Guillermo Iguaran* + +* Add `ENV['DATABASE_URL']` support in `rails dbconsole`. Fixes #13320. + + *Huiming Teo* + * Add `Application#message_verifier` method to return a message verifier. This verifier can be used to generate and verify signed messages in the application. - message = Rails.application.message_verifier('salt').generate('my sensible data') - Rails.application.message_verifier('salt').verify(message) + message = Rails.application.message_verifier(:sensitive_data).generate('my sensible data') + Rails.application.message_verifier(:sensitive_data).verify(message) # => 'my sensible data' It is recommended not to use the same verifier for different things, so you can get different verifiers passing the name argument. - message = Rails.application.message_verifier('cookies').generate('my sensible cookie data') + message = Rails.application.message_verifier(:cookies).generate('my sensible cookie data') See the `ActiveSupport::MessageVerifier` documentation for more information. diff --git a/railties/lib/rails.rb b/railties/lib/rails.rb index ea82327365..be7570a5ba 100644 --- a/railties/lib/rails.rb +++ b/railties/lib/rails.rb @@ -25,6 +25,7 @@ module Rails autoload :Info autoload :InfoController + autoload :MailersController autoload :WelcomeController class << self diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index e45bfaf6fc..05acd78d98 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -1,4 +1,5 @@ require 'fileutils' +require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/object/blank' require 'active_support/key_generator' require 'active_support/message_verifier' @@ -104,7 +105,7 @@ module Rails delegate :default_url_options, :default_url_options=, to: :routes INITIAL_VARIABLES = [:config, :railties, :routes_reloader, :reloaders, - :routes, :helpers, :app_env_config] # :nodoc: + :routes, :helpers, :app_env_config, :secrets] # :nodoc: def initialize(initial_variable_values = {}, &block) super() @@ -151,8 +152,8 @@ module Rails # number of iterations selected based on consultation with the google security # team. Details at https://github.com/rails/rails/pull/6952#issuecomment-7661220 @caching_key_generator ||= begin - if config.secret_key_base - key_generator = ActiveSupport::KeyGenerator.new(config.secret_key_base, iterations: 1000) + if secrets.secret_key_base + key_generator = ActiveSupport::KeyGenerator.new(secrets.secret_key_base, iterations: 1000) ActiveSupport::CachingKeyGenerator.new(key_generator) else ActiveSupport::LegacyKeyGenerator.new(config.secret_token) @@ -169,18 +170,18 @@ module Rails # # ==== Parameters # - # * +salt+ - the salt that will be used to generate the secret key of the verifier. + # * +verifier_name+ - the name of the message verifier. # # ==== Examples # - # message = Rails.application.message_verifier('salt').generate('my sensible data') - # Rails.application.message_verifier('salt').verify(message) + # message = Rails.application.message_verifier('sensitive_data').generate('my sensible data') + # Rails.application.message_verifier('sensitive_data').verify(message) # # => 'my sensible data' # # See the +ActiveSupport::MessageVerifier+ documentation for more information. - def message_verifier(salt) - @message_verifiers[salt] ||= begin - secret = key_generator.generate_key(salt) + def message_verifier(verifier_name) + @message_verifiers[verifier_name] ||= begin + secret = key_generator.generate_key(verifier_name.to_s) ActiveSupport::MessageVerifier.new(secret) end end @@ -195,7 +196,7 @@ module Rails "action_dispatch.parameter_filter" => config.filter_parameters, "action_dispatch.redirect_filter" => config.filter_redirect, "action_dispatch.secret_token" => config.secret_token, - "action_dispatch.secret_key_base" => config.secret_key_base, + "action_dispatch.secret_key_base" => secrets.secret_key_base, "action_dispatch.show_exceptions" => config.action_dispatch.show_exceptions, "action_dispatch.show_detailed_exceptions" => config.consider_all_requests_local, "action_dispatch.logger" => Rails.logger, @@ -300,6 +301,27 @@ module Rails @config = configuration end + def secrets #:nodoc: + @secrets ||= begin + secrets = ActiveSupport::OrderedOptions.new + yaml = config.paths["config/secrets"].first + if File.exist?(yaml) + require "erb" + env_secrets = YAML.load(ERB.new(IO.read(yaml)).result)[Rails.env] + secrets.merge!(env_secrets.symbolize_keys) if env_secrets + end + + # Fallback to config.secret_key_base if secrets.secret_key_base isn't set + secrets.secret_key_base ||= config.secret_key_base + + secrets + end + end + + def secrets=(secrets) #:nodoc: + @secrets = secrets + end + def to_app #:nodoc: self end @@ -391,8 +413,8 @@ module Rails end def validate_secret_key_config! #:nodoc: - if config.secret_key_base.blank? && config.secret_token.blank? - raise "You must set config.secret_key_base in your app's config." + if secrets.secret_key_base.blank? && config.secret_token.blank? + raise "Missing `secret_key_base` for '#{Rails.env}' environment, set this value in `config/secrets.yml`" end end end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index dd0b9c6d70..9975bb8596 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -76,6 +76,7 @@ module Rails @paths ||= begin paths = super paths.add "config/database", with: "config/database.yml" + paths.add "config/secrets", with: "config/secrets.yml" paths.add "config/environment", with: "config/environment.rb" paths.add "lib/templates" paths.add "log", with: "log/#{Rails.env}.log" diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb index 7a1bb1e25c..5b8509b2e9 100644 --- a/railties/lib/rails/application/finisher.rb +++ b/railties/lib/rails/application/finisher.rb @@ -22,6 +22,8 @@ module Rails initializer :add_builtin_route do |app| if Rails.env.development? app.routes.append do + get '/rails/mailers' => "rails/mailers#index" + get '/rails/mailers/*path' => "rails/mailers#preview" get '/rails/info/properties' => "rails/info#properties" get '/rails/info/routes' => "rails/info#routes" get '/rails/info' => "rails/info#index" diff --git a/railties/lib/rails/application_controller.rb b/railties/lib/rails/application_controller.rb new file mode 100644 index 0000000000..9a29ec21cf --- /dev/null +++ b/railties/lib/rails/application_controller.rb @@ -0,0 +1,16 @@ +class Rails::ApplicationController < ActionController::Base # :nodoc: + self.view_paths = File.expand_path('../templates', __FILE__) + layout 'application' + + protected + + def require_local! + unless local_request? + render text: '<p>For security purposes, this information is only available to local requests.</p>', status: :forbidden + end + end + + def local_request? + Rails.application.config.consider_all_requests_local || request.local? + end +end diff --git a/railties/lib/rails/commands/dbconsole.rb b/railties/lib/rails/commands/dbconsole.rb index 3e4cc787c4..c265ed8f36 100644 --- a/railties/lib/rails/commands/dbconsole.rb +++ b/railties/lib/rails/commands/dbconsole.rb @@ -81,14 +81,10 @@ module Rails def config @config ||= begin - cfg = begin - YAML.load(ERB.new(IO.read("config/database.yml")).result) - rescue SyntaxError, StandardError - require APP_PATH - Rails.application.config.database_configuration - end - - cfg[environment] || abort("No database is configured for the environment '#{environment}'") + require APP_PATH + ActiveRecord::ConnectionAdapters::ConnectionSpecification::Resolver.new( + Rails.application.config.database_configuration || {} + ).resolve(ENV["DATABASE_URL"]) end end diff --git a/railties/lib/rails/commands/runner.rb b/railties/lib/rails/commands/runner.rb index dd2ee5639e..3a71f8d3f8 100644 --- a/railties/lib/rails/commands/runner.rb +++ b/railties/lib/rails/commands/runner.rb @@ -9,7 +9,7 @@ if ARGV.first.nil? end ARGV.clone.options do |opts| - opts.banner = "Usage: rails runner [options] ('Some.ruby(code)' or a filename)" + opts.banner = "Usage: rails runner [options] [<'Some.ruby(code)'> | <filename.rb>]" opts.separator "" @@ -22,14 +22,23 @@ ARGV.clone.options do |opts| opts.on("-h", "--help", "Show this help message.") { $stdout.puts opts; exit } + opts.separator "" + opts.separator "Examples: " + + opts.separator " rails runner 'puts Rails.env'" + opts.separator " This runs the code `puts Rails.env` after loading the app" + opts.separator "" + opts.separator " rails runner path/to/filename.rb" + opts.separator " This runs the Ruby file located at `path/to/filename.rb` after loading the app" + if RbConfig::CONFIG['host_os'] !~ /mswin|mingw/ opts.separator "" opts.separator "You can also use runner as a shebang line for your executables:" - opts.separator "-------------------------------------------------------------" - opts.separator "#!/usr/bin/env #{File.expand_path($0)} runner" + opts.separator " -------------------------------------------------------------" + opts.separator " #!/usr/bin/env #{File.expand_path($0)} runner" opts.separator "" - opts.separator "Product.all.each { |p| p.price *= 2 ; p.save! }" - opts.separator "-------------------------------------------------------------" + opts.separator " Product.all.each { |p| p.price *= 2 ; p.save! }" + opts.separator " -------------------------------------------------------------" end opts.order! { |o| code_or_file ||= o } rescue retry diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 5d4682f6e3..3305a57b62 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -308,7 +308,7 @@ module Rails 'Use SCSS for stylesheets') else gems << GemfileEntry.version('sass-rails', - '~> 4.0.0.rc1', + '~> 4.0.1', 'Use SCSS for stylesheets') end @@ -329,7 +329,7 @@ module Rails def jbuilder_gemfile_entry comment = 'Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder' - GemfileEntry.version('jbuilder', '~> 1.2', comment) + GemfileEntry.version('jbuilder', '~> 2.0', comment) end def sdoc_gemfile_entry diff --git a/railties/lib/rails/generators/base.rb b/railties/lib/rails/generators/base.rb index cf33b13fd8..67bab96a22 100644 --- a/railties/lib/rails/generators/base.rb +++ b/railties/lib/rails/generators/base.rb @@ -296,7 +296,7 @@ module Rails end end - # Return the default value for the option name given doing a lookup in + # Returns the default value for the option name given doing a lookup in # Rails::Generators.options. def self.default_value_for_option(name, options) default_for_option(Rails::Generators.options, name, options, options[:default]) diff --git a/railties/lib/rails/generators/erb.rb b/railties/lib/rails/generators/erb.rb index 73e986ee7f..cfd77097d5 100644 --- a/railties/lib/rails/generators/erb.rb +++ b/railties/lib/rails/generators/erb.rb @@ -5,6 +5,10 @@ module Erb # :nodoc: class Base < Rails::Generators::NamedBase #:nodoc: protected + def formats + format + end + def format :html end @@ -13,7 +17,7 @@ module Erb # :nodoc: :erb end - def filename_with_extensions(name) + def filename_with_extensions(name, format) [name, format, handler].compact.join(".") end end diff --git a/railties/lib/rails/generators/erb/controller/controller_generator.rb b/railties/lib/rails/generators/erb/controller/controller_generator.rb index 5f06734ab8..e62aece7c5 100644 --- a/railties/lib/rails/generators/erb/controller/controller_generator.rb +++ b/railties/lib/rails/generators/erb/controller/controller_generator.rb @@ -11,8 +11,10 @@ module Erb # :nodoc: actions.each do |action| @action = action - @path = File.join(base_path, filename_with_extensions(action)) - template filename_with_extensions(:view), @path + Array(formats).each do |format| + @path = File.join(base_path, filename_with_extensions(action, format)) + template filename_with_extensions(:view, format), @path + end end end end diff --git a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb index 7bcac30dde..66b17bd10e 100644 --- a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb +++ b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb @@ -5,8 +5,8 @@ module Erb # :nodoc: class MailerGenerator < ControllerGenerator # :nodoc: protected - def format - :text + def formats + [:text, :html] end end end diff --git a/railties/lib/rails/generators/erb/mailer/templates/view.html.erb b/railties/lib/rails/generators/erb/mailer/templates/view.html.erb new file mode 100644 index 0000000000..8bb7c2b768 --- /dev/null +++ b/railties/lib/rails/generators/erb/mailer/templates/view.html.erb @@ -0,0 +1,5 @@ +<h1><%= class_name %>#<%= @action %></h1> + +<p> + <%%= @greeting %>, find me in app/views/<%= @path %> +</p> diff --git a/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb index bacbc2d280..b219f459ac 100644 --- a/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb +++ b/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb @@ -14,8 +14,10 @@ module Erb # :nodoc: def copy_view_files available_views.each do |view| - filename = filename_with_extensions(view) - template filename, File.join("app/views", controller_file_path, filename) + Array(formats).each do |format| + filename = filename_with_extensions(view, format) + template filename, File.join("app/views", controller_file_path, filename) + end end end diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 87556bd609..e12ee3c713 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -78,6 +78,7 @@ module Rails template "routes.rb" template "application.rb" template "environment.rb" + template "secrets.yml" directory "environments" directory "initializers" diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml index c6dfd50d40..334d2ba51c 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml @@ -23,6 +23,6 @@ test: <<: *default database: <%= app_name %>_test -production: - <<: *default - database: <%= app_name %>_production +# Do not keep production credentials in the repository, +# instead read the configuration from the environment. +production: <%%= ENV["RAILS_DATABASE_URL"] %> diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml index fe53cd0ea2..0a2dc1b19e 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml @@ -61,6 +61,6 @@ test: <<: *default database: <%= app_name[0,4] %>_tst -production: - <<: *default - database: <%= app_name[0,8] %> +# Do not keep production credentials in the repository, +# instead read the configuration from the environment. +production: <%%= ENV["RAILS_DATABASE_URL"] %> diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml index be1dae332f..cda6bcab06 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml @@ -53,6 +53,6 @@ test: <<: *default url: jdbc:db://localhost/<%= app_name %>_test -production: - <<: *default - url: jdbc:db://localhost/<%= app_name %>_production +# Do not keep production credentials in the repository, +# instead read the configuration from the environment. +production: <%%= ENV["RAILS_DATABASE_URL"] %> diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml index 26e2a5976c..b316b3ade1 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml @@ -26,6 +26,6 @@ test: <<: *default database: <%= app_name %>_test -production: - <<: *default - database: <%= app_name %>_production +# Do not keep production credentials in the repository, +# instead read the configuration from the environment. +production: <%%= ENV["RAILS_DATABASE_URL"] %> diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml index ccd44cf54b..d76632f0ac 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml @@ -6,13 +6,20 @@ default: &default adapter: postgresql encoding: unicode - username: <%= app_name %> - password: development: <<: *default database: <%= app_name %>_development + # The specified database role being used to connect to postgres. + # To create additional roles in postgres see `$ createuser --help`. + # When left blank, postgres will use the default role. This is + # the same name as the operating system user that initialized the database. + #username: <%= app_name %> + + # The password associated with the postgres role (username). + #password: + # Connect on a TCP socket. Omitted by default since the client uses a # domain socket that doesn't need configuration. Windows does not have # domain sockets, so uncomment these lines. @@ -35,6 +42,6 @@ test: <<: *default database: <%= app_name %>_test -production: - <<: *default - database: <%= app_name %>_production +# Do not keep production credentials in the repository, +# instead read the configuration from the environment. +production: <%%= ENV["RAILS_DATABASE_URL"] %> diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml index 28c36eb82f..4aaa97b104 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml @@ -18,6 +18,6 @@ test: <<: *default database: db/test.sqlite3 -production: - <<: *default - database: db/production.sqlite3 +# Do not keep production credentials in the repository, +# instead read the configuration from the environment. +production: <%%= ENV["RAILS_DATABASE_URL"] %> diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml index 3fc7ce28a1..28a5418b49 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml @@ -32,6 +32,10 @@ test: <<: *default database: <%= app_name %>_test -production: - <<: *default - database: <%= app_name %>_production +# Avoid production credentials in the repository, +# instead read the configuration from the environment. +# +# Example: +# mysql2://myuser:mypass@localhost/somedatabase +# +production: <%%= ENV["RAILS_DATABASE_URL"] %> diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml index d5b52c969b..31abbe8ba6 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml @@ -32,6 +32,6 @@ test: <<: *default database: <%= app_name %>_test -production: - <<: *default - database: <%= app_name %>_production +# Do not keep production credentials in the repository, +# instead read the configuration from the environment. +production: <%%= ENV["RAILS_DATABASE_URL"] %> diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml index eaeb82bddd..7a7a8c08e0 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml @@ -17,16 +17,23 @@ default: &default adapter: postgresql encoding: unicode - # For details on connection pooling, see rails configration guide + # For details on connection pooling, see rails configuration guide # http://guides.rubyonrails.org/configuring.html#database-pooling pool: 5 - username: <%= app_name %> - password: development: <<: *default database: <%= app_name %>_development + # The specified database role being used to connect to postgres. + # To create additional roles in postgres see `$ createuser --help`. + # When left blank, postgres will use the default role. This is + # the same name as the operating system user that initialized the database. + #username: <%= app_name %> + + # The password associated with the postgres role (username). + #password: + # Connect on a TCP socket. Omitted by default since the client uses a # domain socket that doesn't need configuration. Windows does not have # domain sockets, so uncomment these lines. @@ -52,6 +59,10 @@ test: <<: *default database: <%= app_name %>_test -production: - <<: *default - database: <%= app_name %>_production +# Do not keep production credentials in the repository, +# instead read the configuration from the environment. +# +# Example: +# postgres://myuser:mypass@localhost/somedatabase +# +production: <%%= ENV["RAILS_DATABASE_URL"] %> diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml index 1c1a37ca8d..d8b0b61e14 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml @@ -20,6 +20,10 @@ test: <<: *default database: db/test.sqlite3 -production: - <<: *default - database: db/production.sqlite3 +# Do not keep production credentials in the repository, +# instead read the configuration from the environment. +# +# Example: +# sqlite3://myuser:mypass@localhost/somedatabase +# +production: <%%= ENV["RAILS_DATABASE_URL"] %> diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml index 4855f66c0d..c270497879 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml @@ -42,6 +42,6 @@ test: <<: *default database: <%= app_name %>_test -production: - <<: *default - database: <%= app_name %>_production +# Do not keep production credentials in the repository, +# instead read the configuration from the environment. +production: <%%= ENV["RAILS_DATABASE_URL"] %> diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt index b724d468a2..cce4743a33 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt @@ -29,5 +29,10 @@ Rails.application.configure do # This option may cause significant delays in view rendering with a large # number of complex assets. config.assets.debug = true + + # Adds additional error checking when serving assets at runtime. + # Checks for improperly declared sprockets dependencies. + # Raises helpful error messages. + config.assets.raise_runtime_errors = true <%- end -%> end diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/secrets.yml index f3cc6098a3..6e2c45e119 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/secrets.yml @@ -7,6 +7,16 @@ # no regular words or you'll be exposed to dictionary attacks. # You can use `rake secret` to generate a secure secret key. -# Make sure your secret_key_base is kept private +# Make sure the secrets in this file are kept private # if you're sharing your code publicly. -Rails.application.config.secret_key_base = '<%= app_secret %>' + +development: + secret_key_base: <%= app_secret %> + +test: + secret_key_base: <%= app_secret %> + +# Do not keep production secrets in the repository, +# instead read values from the environment. +production: + secret_key_base: <%%= ENV["RAILS_SECRET_KEY_BASE"] %> diff --git a/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb b/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb index 3334b189bf..85dee1a066 100644 --- a/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb +++ b/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb @@ -4,11 +4,18 @@ module TestUnit # :nodoc: module Generators # :nodoc: class MailerGenerator < Base # :nodoc: argument :actions, type: :array, default: [], banner: "method method" - check_class_collision suffix: "Test" + + def check_class_collision + class_collisions "#{class_name}Test", "#{class_name}Preview" + end def create_test_files template "functional_test.rb", File.join('test/mailers', class_path, "#{file_name}_test.rb") end + + def create_preview_files + template "preview.rb", File.join('test/mailers/previews', class_path, "#{file_name}_preview.rb") + end end end end diff --git a/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb b/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb new file mode 100644 index 0000000000..3bfd5426e8 --- /dev/null +++ b/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb @@ -0,0 +1,13 @@ +<% module_namespacing do -%> +# Preview all emails at http://localhost:3000/rails/mailers/<%= file_path %> +class <%= class_name %>Preview < ActionMailer::Preview +<% actions.each do |action| -%> + + # Preview this email at http://localhost:3000/rails/mailers/<%= file_path %>/<%= action %> + def <%= action %> + <%= class_name %>.<%= action %> + end +<% end -%> + +end +<% end -%> diff --git a/railties/lib/rails/info_controller.rb b/railties/lib/rails/info_controller.rb index 85ea11b4e7..908c4ce65e 100644 --- a/railties/lib/rails/info_controller.rb +++ b/railties/lib/rails/info_controller.rb @@ -1,9 +1,9 @@ +require 'rails/application_controller' require 'action_dispatch/routing/inspector' -class Rails::InfoController < ActionController::Base # :nodoc: - self.view_paths = File.expand_path('../templates', __FILE__) +class Rails::InfoController < Rails::ApplicationController # :nodoc: prepend_view_path ActionDispatch::DebugExceptions::RESCUES_TEMPLATE_PATH - layout -> { request.xhr? ? nil : 'application' } + layout -> { request.xhr? ? false : 'application' } before_filter :require_local! @@ -20,16 +20,4 @@ class Rails::InfoController < ActionController::Base # :nodoc: @routes_inspector = ActionDispatch::Routing::RoutesInspector.new(_routes.routes) @page_title = 'Routes' end - - protected - - def require_local! - unless local_request? - render text: '<p>For security purposes, this information is only available to local requests.</p>', status: :forbidden - end - end - - def local_request? - Rails.application.config.consider_all_requests_local || request.local? - end end diff --git a/railties/lib/rails/mailers_controller.rb b/railties/lib/rails/mailers_controller.rb new file mode 100644 index 0000000000..dd318f52e5 --- /dev/null +++ b/railties/lib/rails/mailers_controller.rb @@ -0,0 +1,73 @@ +require 'rails/application_controller' + +class Rails::MailersController < Rails::ApplicationController # :nodoc: + prepend_view_path ActionDispatch::DebugExceptions::RESCUES_TEMPLATE_PATH + + before_filter :require_local! + before_filter :find_preview, only: :preview + + def index + @previews = ActionMailer::Preview.all + @page_title = "Mailer Previews" + end + + def preview + if params[:path] == @preview.preview_name + @page_title = "Mailer Previews for #{@preview.preview_name}" + render action: 'mailer' + else + email = File.basename(params[:path]) + + if @preview.email_exists?(email) + @email = @preview.call(email) + + if params[:part] + part_type = Mime::Type.lookup(params[:part]) + + if part = find_part(part_type) + response.content_type = part_type + render text: part.respond_to?(:decoded) ? part.decoded : part + else + raise AbstractController::ActionNotFound, "Email part '#{part_type}' not found in #{@preview.name}##{email}" + end + else + @part = find_preferred_part(request.format, Mime::HTML, Mime::TEXT) + render action: 'email', layout: false, formats: %w[html] + end + else + raise AbstractController::ActionNotFound, "Email '#{email}' not found in #{@preview.name}" + end + end + end + + protected + def find_preview + candidates = [] + params[:path].to_s.scan(%r{/|$}){ candidates << $` } + preview = candidates.detect{ |candidate| ActionMailer::Preview.exists?(candidate) } + + if preview + @preview = ActionMailer::Preview.find(preview) + else + raise AbstractController::ActionNotFound, "Mailer preview '#{params[:path]}' not found" + end + end + + def find_preferred_part(*formats) + if @email.multipart? + formats.each do |format| + return find_part(format) if @email.parts.any?{ |p| p.mime_type == format } + end + else + @email + end + end + + def find_part(format) + if @email.multipart? + @email.parts.find{ |p| p.mime_type == format } + elsif @email.mime_type == format + @email + end + end +end
\ No newline at end of file diff --git a/railties/lib/rails/templates/layouts/application.html.erb b/railties/lib/rails/templates/layouts/application.html.erb index 50a4755e45..5aecaa712a 100644 --- a/railties/lib/rails/templates/layouts/application.html.erb +++ b/railties/lib/rails/templates/layouts/application.html.erb @@ -29,7 +29,7 @@ </style> </head> <body> -<h2>Your App: <%= link_to 'properties', '/rails/info/properties' %> | <%= link_to 'routes', '/rails/info/routes' %></h2> + <%= yield %> </body> diff --git a/railties/lib/rails/templates/rails/mailers/email.html.erb b/railties/lib/rails/templates/rails/mailers/email.html.erb new file mode 100644 index 0000000000..977feb922b --- /dev/null +++ b/railties/lib/rails/templates/rails/mailers/email.html.erb @@ -0,0 +1,98 @@ +<!DOCTYPE html> +<html><head> +<meta name="viewport" content="width=device-width" /> +<style type="text/css"> + header { + width: 100%; + padding: 10px 0 0 0; + margin: 0; + background: white; + font: 12px "Lucida Grande", sans-serif; + border-bottom: 1px solid #dedede; + overflow: hidden; + } + + dl { + margin: 0 0 10px 0; + padding: 0; + } + + dt { + width: 80px; + padding: 1px; + float: left; + clear: left; + text-align: right; + color: #7f7f7f; + } + + dd { + margin-left: 90px; /* 80px + 10px */ + padding: 1px; + } + + iframe { + border: 0; + width: 100%; + height: 800px; + } +</style> +</head> + +<body> +<header> + <dl> + <% if @email.respond_to?(:smtp_envelope_from) && Array(@email.from) != Array(@email.smtp_envelope_from) %> + <dt>SMTP-From:</dt> + <dd><%= @email.smtp_envelope_from %></dd> + <% end %> + + <% if @email.respond_to?(:smtp_envelope_to) && @email.to != @email.smtp_envelope_to %> + <dt>SMTP-To:</dt> + <dd><%= @email.smtp_envelope_to %></dd> + <% end %> + + <dt>From:</dt> + <dd><%= @email.header['from'] %></dd> + + <% if @email.reply_to %> + <dt>Reply-To:</dt> + <dd><%= @email.header['reply-to'] %></dd> + <% end %> + + <dt>To:</dt> + <dd><%= @email.header['to'] %></dd> + + <% if @email.cc %> + <dt>CC:</dt> + <dd><%= @email.header['cc'] %></dd> + <% end %> + + <dt>Date:</dt> + <dd><%= Time.current.rfc2822 %></dd> + + <dt>Subject:</dt> + <dd><strong><%= @email.subject %></strong></dd> + + <% unless @email.attachments.nil? || @email.attachments.empty? %> + <dt>Attachments:</dt> + <dd> + <%= @email.attachments.map { |a| a.respond_to?(:original_filename) ? a.original_filename : a.filename }.inspect %> + </dd> + <% end %> + + <% if @email.multipart? %> + <dd> + <select onchange="document.getElementsByName('messageBody')[0].src=this.options[this.selectedIndex].value;"> + <option <%= request.format == Mime::HTML ? 'selected' : '' %> value="?part=text%2Fhtml">View as HTML email</option> + <option <%= request.format == Mime::TEXT ? 'selected' : '' %> value="?part=text%2Fplain">View as plain-text email</option> + </select> + </dd> + <% end %> + </dl> +</header> + +<iframe seamless name="messageBody" src="?part=<%= Rack::Utils.escape(@part.mime_type) %>"></iframe> + +</body> +</html>
\ No newline at end of file diff --git a/railties/lib/rails/templates/rails/mailers/index.html.erb b/railties/lib/rails/templates/rails/mailers/index.html.erb new file mode 100644 index 0000000000..c4c9757d57 --- /dev/null +++ b/railties/lib/rails/templates/rails/mailers/index.html.erb @@ -0,0 +1,8 @@ +<% @previews.each do |preview| %> +<h3><%= link_to preview.preview_name.titleize, "/rails/mailers/#{preview.preview_name}" %></h3> +<ul> +<% preview.emails.each do |email| %> +<li><%= link_to email, "/rails/mailers/#{preview.preview_name}/#{email}" %></li> +<% end %> +</ul> +<% end %> diff --git a/railties/lib/rails/templates/rails/mailers/mailer.html.erb b/railties/lib/rails/templates/rails/mailers/mailer.html.erb new file mode 100644 index 0000000000..607c8d1677 --- /dev/null +++ b/railties/lib/rails/templates/rails/mailers/mailer.html.erb @@ -0,0 +1,6 @@ +<h3><%= @preview.preview_name.titleize %></h3> +<ul> +<% @preview.emails.each do |email| %> +<li><%= link_to email, "/rails/mailers/#{@preview.preview_name}/#{email}" %></li> +<% end %> +</ul> diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb index be801befc2..7765c9ae86 100644 --- a/railties/lib/rails/test_help.rb +++ b/railties/lib/rails/test_help.rb @@ -11,7 +11,7 @@ require 'rails/generators/test_case' # Config Rails backtrace in tests. require 'rails/backtrace_cleaner' if ENV["BACKTRACE"].nil? - MiniTest.backtrace_filter = Rails.backtrace_cleaner + Minitest.backtrace_filter = Rails.backtrace_cleaner end if defined?(ActiveRecord::Base) diff --git a/railties/lib/rails/version.rb b/railties/lib/rails/version.rb index 5a6d8d0983..923cab4e2a 100644 --- a/railties/lib/rails/version.rb +++ b/railties/lib/rails/version.rb @@ -3,7 +3,7 @@ module Rails MAJOR = 4 MINOR = 1 TINY = 0 - PRE = "beta" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/railties/lib/rails/welcome_controller.rb b/railties/lib/rails/welcome_controller.rb index 45b764fa6b..de9cd18b01 100644 --- a/railties/lib/rails/welcome_controller.rb +++ b/railties/lib/rails/welcome_controller.rb @@ -1,6 +1,7 @@ -class Rails::WelcomeController < ActionController::Base # :nodoc: - self.view_paths = File.expand_path('../templates', __FILE__) - layout nil +require 'rails/application_controller' + +class Rails::WelcomeController < Rails::ApplicationController # :nodoc: + layout false def index end diff --git a/railties/test/abstract_unit.rb b/railties/test/abstract_unit.rb index 643cc6b0ee..ade08d3f5a 100644 --- a/railties/test/abstract_unit.rb +++ b/railties/test/abstract_unit.rb @@ -14,6 +14,6 @@ require 'rails/all' module TestApp class Application < Rails::Application config.root = File.dirname(__FILE__) - config.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33' + secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33' end end diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index b3fbceb0dc..7a8fed6732 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -250,7 +250,7 @@ module ApplicationTests test "Use key_generator when secret_key_base is set" do make_basic_app do |app| - app.config.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33' + app.secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33' app.config.session_store :disabled end @@ -270,27 +270,27 @@ module ApplicationTests test "application verifier can be used in the entire application" do make_basic_app do |app| - app.config.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33' + app.secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33' app.config.session_store :disabled end - message = app.message_verifier('salt').generate("some_value") + message = app.message_verifier(:sensitive_value).generate("some_value") - assert_equal 'some_value', Rails.application.message_verifier('salt').verify(message) + assert_equal 'some_value', Rails.application.message_verifier(:sensitive_value).verify(message) - secret = app.key_generator.generate_key('salt') + secret = app.key_generator.generate_key('sensitive_value') verifier = ActiveSupport::MessageVerifier.new(secret) assert_equal 'some_value', verifier.verify(message) end test "application verifier can build different verifiers" do make_basic_app do |app| - app.config.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33' + app.secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33' app.config.session_store :disabled end - default_verifier = app.message_verifier('salt') - text_verifier = app.message_verifier('text') + default_verifier = app.message_verifier(:sensitive_value) + text_verifier = app.message_verifier(:text) message = text_verifier.generate('some_value') @@ -299,10 +299,43 @@ module ApplicationTests default_verifier.verify(message) end - assert_equal default_verifier.object_id, app.message_verifier('salt').object_id + assert_equal default_verifier.object_id, app.message_verifier(:sensitive_value).object_id assert_not_equal default_verifier.object_id, text_verifier.object_id end + test "secrets.secret_key_base is used when config/tokens.yml is present" do + app_file 'config/secrets.yml', <<-YAML + development: + secret_key_base: 3b7cd727ee24e8444053437c36cc66c3 + YAML + + require "#{app_path}/config/environment" + assert_equal '3b7cd727ee24e8444053437c36cc66c3', app.secrets.secret_key_base + end + + test "secret_key_base is copied from config to secrets when not set" do + remove_file "config/secrets.yml" + app_file 'config/initializers/secret_token.rb', <<-RUBY + Rails.application.config.secret_key_base = "3b7cd727ee24e8444053437c36cc66c3" + RUBY + + require "#{app_path}/config/environment" + assert_equal '3b7cd727ee24e8444053437c36cc66c3', app.secrets.secret_key_base + end + + test "custom secrets saved in config/tokens.yml are loaded in app secrets" do + app_file 'config/secrets.yml', <<-YAML + development: + secret_key_base: 3b7cd727ee24e8444053437c36cc66c3 + aws_access_key_id: myamazonaccesskeyid + aws_secret_access_key: myamazonsecretaccesskey + YAML + + require "#{app_path}/config/environment" + assert_equal 'myamazonaccesskeyid', app.secrets.aws_access_key_id + assert_equal 'myamazonsecretaccesskey', app.secrets.aws_secret_access_key + end + test "protect from forgery is the default in a new app" do make_basic_app diff --git a/railties/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb index 83104acf3c..3601a58f67 100644 --- a/railties/test/application/initializers/frameworks_test.rb +++ b/railties/test/application/initializers/frameworks_test.rb @@ -216,7 +216,7 @@ module ApplicationTests require "#{app_path}/config/environment" orig_database_url = ENV.delete("DATABASE_URL") orig_rails_env, Rails.env = Rails.env, 'development' - database_url_db_name = "db/database_url_db.sqlite3" + database_url_db_name = File.join(app_path, "db/database_url_db.sqlite3") ENV["DATABASE_URL"] = "sqlite3://:@localhost/#{database_url_db_name}" ActiveRecord::Base.establish_connection assert ActiveRecord::Base.connection diff --git a/railties/test/application/initializers/i18n_test.rb b/railties/test/application/initializers/i18n_test.rb index 2a64cd8ba7..bc34897cdf 100644 --- a/railties/test/application/initializers/i18n_test.rb +++ b/railties/test/application/initializers/i18n_test.rb @@ -184,14 +184,48 @@ en: assert_fallbacks ca: [:ca, :"es-ES", :es, :'en-US', :en] end - test "config.i18n.enforce_available_locales is set before config.i18n.default_locale is" do + test "config.i18n.enforce_available_locales is set to true by default and avoids I18n warnings" do add_to_config <<-RUBY config.i18n.default_locale = :it - config.i18n.enforce_available_locales = true RUBY - assert_raises(I18n::InvalidLocale) do - load_app + output = capture(:stderr) { load_app } + assert_no_match %r{deprecated.*enforce_available_locales}, output + assert_equal true, I18n.enforce_available_locales + + assert_raise I18n::InvalidLocale do + I18n.locale = :es + end + end + + test "disable config.i18n.enforce_available_locales" do + add_to_config <<-RUBY + config.i18n.enforce_available_locales = false + config.i18n.default_locale = :fr + RUBY + + output = capture(:stderr) { load_app } + assert_no_match %r{deprecated.*enforce_available_locales}, output + assert_equal false, I18n.enforce_available_locales + + assert_nothing_raised do + I18n.locale = :es + end + end + + test "default config.i18n.enforce_available_locales does not override I18n.enforce_available_locales" do + I18n.enforce_available_locales = false + + add_to_config <<-RUBY + config.i18n.default_locale = :fr + RUBY + + output = capture(:stderr) { load_app } + assert_no_match %r{deprecated.*enforce_available_locales}, output + assert_equal false, I18n.enforce_available_locales + + assert_nothing_raised do + I18n.locale = :es end end end diff --git a/railties/test/application/mailer_previews_test.rb b/railties/test/application/mailer_previews_test.rb new file mode 100644 index 0000000000..14abb33cc6 --- /dev/null +++ b/railties/test/application/mailer_previews_test.rb @@ -0,0 +1,388 @@ +require 'isolation/abstract_unit' +require 'rack/test' +module ApplicationTests + class MailerPreviewsTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + include Rack::Test::Methods + + def setup + build_app + boot_rails + end + + def teardown + teardown_app + end + + test "/rails/mailers is accessible in development" do + app("development") + get "/rails/mailers" + assert_equal 200, last_response.status + end + + test "/rails/mailers is not accessible in production" do + app("production") + get "/rails/mailers" + assert_equal 404, last_response.status + end + + test "mailer previews are loaded from the default preview_path" do + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + mail to: "to@example.org" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers" + assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body + assert_match '<li><a href="/rails/mailers/notifier/foo">foo</a></li>', last_response.body + end + + test "mailer previews are loaded from a custom preview_path" do + add_to_config "config.action_mailer.preview_path = '#{app_path}/lib/mailer_previews'" + + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + mail to: "to@example.org" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + app_file 'lib/mailer_previews/notifier_preview.rb', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers" + assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body + assert_match '<li><a href="/rails/mailers/notifier/foo">foo</a></li>', last_response.body + end + + test "mailer previews are reloaded across requests" do + app('development') + + get "/rails/mailers" + assert_no_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body + + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + mail to: "to@example.org" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + get "/rails/mailers" + assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body + + remove_file 'test/mailers/previews/notifier_preview.rb' + sleep(1) + + get "/rails/mailers" + assert_no_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body + end + + test "mailer preview actions are added and removed" do + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + mail to: "to@example.org" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers" + assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body + assert_match '<li><a href="/rails/mailers/notifier/foo">foo</a></li>', last_response.body + assert_no_match '<li><a href="/rails/mailers/notifier/bar">bar</a></li>', last_response.body + + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + mail to: "to@example.org" + end + + def bar + mail to: "to@example.net" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + text_template 'notifier/bar', <<-RUBY + Goodbye, World! + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + + def bar + Notifier.bar + end + end + RUBY + + sleep(1) + + get "/rails/mailers" + assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body + assert_match '<li><a href="/rails/mailers/notifier/foo">foo</a></li>', last_response.body + assert_match '<li><a href="/rails/mailers/notifier/bar">bar</a></li>', last_response.body + + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + mail to: "to@example.org" + end + end + RUBY + + remove_file 'app/views/notifier/bar.text.erb' + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + sleep(1) + + get "/rails/mailers" + assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body + assert_match '<li><a href="/rails/mailers/notifier/foo">foo</a></li>', last_response.body + assert_no_match '<li><a href="/rails/mailers/notifier/bar">bar</a></li>', last_response.body + end + + test "mailer preview not found" do + app('development') + get "/rails/mailers/notifier" + assert last_response.not_found? + assert_match "Mailer preview 'notifier' not found", last_response.body + end + + test "mailer preview email not found" do + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + mail to: "to@example.org" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers/notifier/bar" + assert last_response.not_found? + assert_match "Email 'bar' not found in NotifierPreview", last_response.body + end + + test "mailer preview email part not found" do + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + mail to: "to@example.org" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers/notifier/foo?part=text%2Fhtml" + assert last_response.not_found? + assert_match "Email part 'text/html' not found in NotifierPreview#foo", last_response.body + end + + test "message header uses full display names" do + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "Ruby on Rails <core@rubyonrails.org>" + + def foo + mail to: "Andrew White <andyw@pixeltrix.co.uk>", + cc: "David Heinemeier Hansson <david@heinemeierhansson.com>" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers/notifier/foo" + assert_equal 200, last_response.status + assert_match "Ruby on Rails <core@rubyonrails.org>", last_response.body + assert_match "Andrew White <andyw@pixeltrix.co.uk>", last_response.body + assert_match "David Heinemeier Hansson <david@heinemeierhansson.com>", last_response.body + end + + test "part menu selects correct option" do + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + mail to: "to@example.org" + end + end + RUBY + + html_template 'notifier/foo', <<-RUBY + <p>Hello, World!</p> + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers/notifier/foo.html" + assert_equal 200, last_response.status + assert_match '<option selected value="?part=text%2Fhtml">View as HTML email</option>', last_response.body + + get "/rails/mailers/notifier/foo.txt" + assert_equal 200, last_response.status + assert_match '<option selected value="?part=text%2Fplain">View as plain-text email</option>', last_response.body + end + + private + def build_app + super + app_file 'config/routes.rb', "Rails.application.routes.draw do; end" + end + + def mailer(name, contents) + app_file("app/mailers/#{name}.rb", contents) + end + + def mailer_preview(name, contents) + app_file("test/mailers/previews/#{name}_preview.rb", contents) + end + + def html_template(name, contents) + app_file("app/views/#{name}.html.erb", contents) + end + + def text_template(name, contents) + app_file("app/views/#{name}.text.erb", contents) + end + end +end diff --git a/railties/test/application/middleware/session_test.rb b/railties/test/application/middleware/session_test.rb index 14a56176f5..31a64c2f5a 100644 --- a/railties/test/application/middleware/session_test.rb +++ b/railties/test/application/middleware/session_test.rb @@ -318,7 +318,7 @@ module ApplicationTests add_to_config <<-RUBY config.secret_token = "3b7cd727ee24e8444053437c36cc66c4" - config.secret_key_base = nil + secrets.secret_key_base = nil RUBY require "#{app_path}/config/environment" diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb index 4d348c6b4b..9a36d1ca01 100644 --- a/railties/test/application/rake/dbs_test.rb +++ b/railties/test/application/rake/dbs_test.rb @@ -16,11 +16,11 @@ module ApplicationTests end def database_url_db_name - "db/database_url_db.sqlite3" + File.join(app_path, "db/database_url_db.sqlite3") end def set_database_url - ENV['DATABASE_URL'] = "sqlite3://:@localhost/#{database_url_db_name}" + ENV['DATABASE_URL'] = File.join("sqlite3://:@localhost", database_url_db_name) # ensure it's using the DATABASE_URL FileUtils.rm_rf("#{app_path}/config/database.yml") end @@ -60,7 +60,7 @@ module ApplicationTests `rails generate model book title:string; bundle exec rake db:migrate` output = `bundle exec rake db:migrate:status` - assert_match(/database:\s+\S+#{expected[:database]}/, output) + assert_match(%r{database:\s+\S*#{Regexp.escape(expected[:database])}}, output) assert_match(/up\s+\d{14}\s+Create books/, output) end end @@ -153,7 +153,7 @@ module ApplicationTests `rails generate model book title:string; bundle exec rake db:migrate db:structure:dump db:test:load_structure` ActiveRecord::Base.configurations = Rails.application.config.database_configuration - ActiveRecord::Base.establish_connection 'test' + ActiveRecord::Base.establish_connection :test require "#{app_path}/app/models/book" #if structure is not loaded correctly, exception would be raised assert Book.count, 0 diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index 08316d80e6..33d3c4a19e 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -208,7 +208,8 @@ module ApplicationTests add_to_config "config.active_record.schema_format = :sql" output = Dir.chdir(app_path) do `rails generate scaffold user username:string; - bundle exec rake db:migrate db:test:clone 2>&1 --trace` + bundle exec rake db:migrate; + bundle exec rake db:test:clone 2>&1 --trace` end assert_match(/Execute db:test:clone_structure/, output) end @@ -217,7 +218,8 @@ module ApplicationTests add_to_config "config.active_record.schema_format = :sql" output = Dir.chdir(app_path) do `rails generate scaffold user username:string; - bundle exec rake db:migrate db:test:prepare 2>&1 --trace` + bundle exec rake db:migrate; + bundle exec rake db:test:prepare 2>&1 --trace` end assert_match(/Execute db:test:load_structure/, output) end diff --git a/railties/test/commands/dbconsole_test.rb b/railties/test/commands/dbconsole_test.rb index edb92b3aa2..7ad83a8b5d 100644 --- a/railties/test/commands/dbconsole_test.rb +++ b/railties/test/commands/dbconsole_test.rb @@ -2,29 +2,74 @@ require 'abstract_unit' require 'rails/commands/dbconsole' class Rails::DBConsoleTest < ActiveSupport::TestCase - def teardown - %w[PGUSER PGHOST PGPORT PGPASSWORD].each{|key| ENV.delete(key)} - end - def test_config - Rails::DBConsole.const_set(:APP_PATH, "erb") - - app_config({}) - capture_abort { Rails::DBConsole.new.config } - assert aborted - assert_match(/No database is configured for the environment '\w+'/, output) - app_config(test: "with_init") - assert_equal Rails::DBConsole.new.config, "with_init" - - app_db_file("test:\n without_init") - assert_equal Rails::DBConsole.new.config, "without_init" - - app_db_file("test:\n <%= Rails.something_app_specific %>") - assert_equal Rails::DBConsole.new.config, "with_init" + def setup + Rails::DBConsole.const_set('APP_PATH', 'rails/all') + end - app_db_file("test:\n\ninvalid") - assert_equal Rails::DBConsole.new.config, "with_init" + def teardown + Rails::DBConsole.send(:remove_const, 'APP_PATH') + %w[PGUSER PGHOST PGPORT PGPASSWORD DATABASE_URL].each{|key| ENV.delete(key)} + end + + def test_config_with_db_config_only + config_sample = { + "test"=> { + "adapter"=> "sqlite3", + "host"=> "localhost", + "port"=> "9000", + "database"=> "foo_test", + "user"=> "foo", + "password"=> "bar", + "pool"=> "5", + "timeout"=> "3000" + } + } + app_db_config(config_sample) + assert_equal Rails::DBConsole.new.config, config_sample["test"] + end + + def test_config_with_no_db_config + app_db_config(nil) + assert_raise(ActiveRecord::AdapterNotSpecified) { + Rails::DBConsole.new.config + } + end + + def test_config_with_database_url_only + ENV['DATABASE_URL'] = 'postgresql://foo:bar@localhost:9000/foo_test?pool=5&timeout=3000' + app_db_config(nil) + expected = { + "adapter" => "postgresql", + "host" => "localhost", + "port" => 9000, + "database" => "foo_test", + "username" => "foo", + "password" => "bar", + "pool" => "5", + "timeout" => "3000" + }.sort + assert_equal expected, Rails::DBConsole.new.config.sort + end + + def test_config_choose_database_url_if_exists + host = "database-url-host.com" + ENV['DATABASE_URL'] = "postgresql://foo:bar@#{host}:9000/foo_test?pool=5&timeout=3000" + sample_config = { + "test" => { + "adapter" => "postgresql", + "host" => "not-the-#{host}", + "port" => 9000, + "database" => "foo_test", + "username" => "foo", + "password" => "bar", + "pool" => "5", + "timeout" => "3000" + } + } + app_db_config(sample_config) + assert_equal host, Rails::DBConsole.new.config["host"] end def test_env @@ -177,6 +222,10 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase private + def app_db_config(results) + Rails.application.config.stubs(:database_configuration).returns(results) + end + def dbconsole @dbconsole ||= Rails::DBConsole.new(nil) end @@ -197,11 +246,4 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase end end - def app_db_file(result) - IO.stubs(:read).with("config/database.yml").returns(result) - end - - def app_config(result) - Rails.application.config.stubs(:database_configuration).returns(result.stringify_keys) - end end diff --git a/railties/test/configuration/middleware_stack_proxy_test.rb b/railties/test/configuration/middleware_stack_proxy_test.rb index 2442cb995d..6f3e45f320 100644 --- a/railties/test/configuration/middleware_stack_proxy_test.rb +++ b/railties/test/configuration/middleware_stack_proxy_test.rb @@ -39,7 +39,7 @@ module Rails @stack.swap :foo @stack.delete :foo - mock = MiniTest::Mock.new + mock = Minitest::Mock.new mock.expect :send, nil, [:swap, :foo] mock.expect :send, nil, [:delete, :foo] @@ -50,7 +50,7 @@ module Rails private def assert_playback(msg_name, args) - mock = MiniTest::Mock.new + mock = Minitest::Mock.new mock.expect :send, nil, [msg_name, args] @stack.merge_into(mock) mock.verify diff --git a/railties/test/generators/generators_test_helper.rb b/railties/test/generators/generators_test_helper.rb index 32a3d072c9..77ec2f1c0c 100644 --- a/railties/test/generators/generators_test_helper.rb +++ b/railties/test/generators/generators_test_helper.rb @@ -1,10 +1,14 @@ require 'abstract_unit' +require 'active_support/core_ext/module/remove_method' require 'rails/generators' require 'rails/generators/test_case' module Rails - def self.root - @root ||= File.expand_path(File.join(File.dirname(__FILE__), '..', 'fixtures')) + class << self + remove_possible_method :root + def root + @root ||= File.expand_path(File.join(File.dirname(__FILE__), '..', 'fixtures')) + end end end Rails.application.config.root = Rails.root diff --git a/railties/test/generators/mailer_generator_test.rb b/railties/test/generators/mailer_generator_test.rb index 6b2351fc1a..d209801f60 100644 --- a/railties/test/generators/mailer_generator_test.rb +++ b/railties/test/generators/mailer_generator_test.rb @@ -1,7 +1,6 @@ require 'generators/generators_test_helper' require 'rails/generators/mailer/mailer_generator' - class MailerGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper arguments %w(notifier foo bar) @@ -23,8 +22,11 @@ class MailerGeneratorTest < Rails::Generators::TestCase end def test_check_class_collision - content = capture(:stderr){ run_generator ["object"] } - assert_match(/The name 'Object' is either already used in your application or reserved/, content) + Object.send :const_set, :Notifier, Class.new + content = capture(:stderr){ run_generator } + assert_match(/The name 'Notifier' is either already used in your application or reserved/, content) + ensure + Object.send :remove_const, :Notifier end def test_invokes_default_test_framework @@ -34,9 +36,37 @@ class MailerGeneratorTest < Rails::Generators::TestCase assert_match(/test "foo"/, test) assert_match(/test "bar"/, test) end + assert_file "test/mailers/previews/notifier_preview.rb" do |preview| + assert_match(/\# Preview all emails at http:\/\/localhost\:3000\/rails\/mailers\/notifier/, preview) + assert_match(/class NotifierPreview < ActionMailer::Preview/, preview) + assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/notifier\/foo/, preview) + assert_instance_method :foo, preview do |foo| + assert_match(/Notifier.foo/, foo) + end + assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/notifier\/bar/, preview) + assert_instance_method :bar, preview do |bar| + assert_match(/Notifier.bar/, bar) + end + end end - def test_invokes_default_template_engine + def test_check_test_class_collision + Object.send :const_set, :NotifierTest, Class.new + content = capture(:stderr){ run_generator } + assert_match(/The name 'NotifierTest' is either already used in your application or reserved/, content) + ensure + Object.send :remove_const, :NotifierTest + end + + def test_check_preview_class_collision + Object.send :const_set, :NotifierPreview, Class.new + content = capture(:stderr){ run_generator } + assert_match(/The name 'NotifierPreview' is either already used in your application or reserved/, content) + ensure + Object.send :remove_const, :NotifierPreview + end + + def test_invokes_default_text_template_engine run_generator assert_file "app/views/notifier/foo.text.erb" do |view| assert_match(%r(app/views/notifier/foo\.text\.erb), view) @@ -49,6 +79,19 @@ class MailerGeneratorTest < Rails::Generators::TestCase end end + def test_invokes_default_html_template_engine + run_generator + assert_file "app/views/notifier/foo.html.erb" do |view| + assert_match(%r(app/views/notifier/foo\.html\.erb), view) + assert_match(/<%= @greeting %>/, view) + end + + assert_file "app/views/notifier/bar.html.erb" do |view| + assert_match(%r(app/views/notifier/bar\.html\.erb), view) + assert_match(/<%= @greeting %>/, view) + end + end + def test_invokes_default_template_engine_even_with_no_action run_generator ["notifier"] assert_file "app/views/notifier" @@ -65,7 +108,13 @@ class MailerGeneratorTest < Rails::Generators::TestCase assert_match(/class Farm::Animal < ActionMailer::Base/, mailer) assert_match(/en\.farm\.animal\.moos\.subject/, mailer) end + assert_file "test/mailers/previews/farm/animal_preview.rb" do |preview| + assert_match(/\# Preview all emails at http:\/\/localhost\:3000\/rails\/mailers\/farm\/animal/, preview) + assert_match(/class Farm::AnimalPreview < ActionMailer::Preview/, preview) + assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/farm\/animal\/moos/, preview) + end assert_file "app/views/farm/animal/moos.text.erb" + assert_file "app/views/farm/animal/moos.html.erb" end def test_actions_are_turned_into_methods diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 913e2b5e29..a1f1973563 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -93,7 +93,8 @@ module TestHelpers # Build an application by invoking the generator and going through the whole stack. def build_app(options = {}) @prev_rails_env = ENV['RAILS_ENV'] - ENV['RAILS_ENV'] = 'development' + ENV['RAILS_ENV'] = 'development' + ENV['RAILS_SECRET_KEY_BASE'] ||= SecureRandom.hex(16) FileUtils.rm_rf(app_path) FileUtils.cp_r(app_template_path, app_path) @@ -117,9 +118,26 @@ module TestHelpers end end + File.open("#{app_path}/config/database.yml", "w") do |f| + f.puts <<-YAML + default: &default + adapter: sqlite3 + pool: 5 + timeout: 5000 + development: + <<: *default + database: db/development.sqlite3 + test: + <<: *default + database: db/test.sqlite3 + production: + <<: *default + database: db/production.sqlite3 + YAML + end + add_to_config <<-RUBY config.eager_load = false - config.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4" config.session_store :cookie_store, key: "_myapp_session" config.active_support.deprecation = :log config.action_controller.allow_forgery_protection = false @@ -139,7 +157,7 @@ module TestHelpers app = Class.new(Rails::Application) app.config.eager_load = false - app.config.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4" + app.secrets.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4" app.config.session_store :cookie_store, key: "_myapp_session" app.config.active_support.deprecation = :log diff --git a/tasks/release.rb b/tasks/release.rb index 66da67bfd0..439a9e0c05 100644 --- a/tasks/release.rb +++ b/tasks/release.rb @@ -1,4 +1,4 @@ -FRAMEWORKS = %w( activesupport activemodel activerecord actionpack actionview actionmailer railties ) +FRAMEWORKS = %w( activesupport activemodel activerecord actionview actionpack actionmailer railties ) root = File.expand_path('../../', __FILE__) version = File.read("#{root}/RAILS_VERSION").strip @@ -120,7 +120,7 @@ namespace :all do end task :tag do - sh "git tag #{tag}" + sh "git tag -m '#{tag} release' #{tag}" sh "git push --tags" end diff --git a/version.rb b/version.rb index 5a6d8d0983..923cab4e2a 100644 --- a/version.rb +++ b/version.rb @@ -3,7 +3,7 @@ module Rails MAJOR = 4 MINOR = 1 TINY = 0 - PRE = "beta" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end |