diff options
94 files changed, 1004 insertions, 1467 deletions
@@ -7,7 +7,7 @@ gemspec # ensure correct loading order gem 'mocha', '~> 0.14', require: false -gem 'rack', github: 'rack/rack' +gem 'rack', github: 'rack/rack', branch: 'master' gem 'rack-cache', '~> 1.2' gem 'jquery-rails', '~> 3.1.0' gem 'turbolinks', github: 'rails/turbolinks', branch: 'master' diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index 6116d1e29f..fd5f4e2831 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -11,6 +11,8 @@ require 'mailers/asset_mailer' class BaseTest < ActiveSupport::TestCase setup do + @original_delivery_method = ActionMailer::Base.delivery_method + ActionMailer::Base.delivery_method = :test @original_asset_host = ActionMailer::Base.asset_host @original_assets_dir = ActionMailer::Base.assets_dir end @@ -19,6 +21,7 @@ class BaseTest < ActiveSupport::TestCase ActionMailer::Base.asset_host = @original_asset_host ActionMailer::Base.assets_dir = @original_assets_dir BaseMailer.deliveries.clear + ActionMailer::Base.delivery_method = @original_delivery_method end test "method call to mail does not raise error" do @@ -468,7 +471,6 @@ class BaseTest < ActiveSupport::TestCase end test "calling deliver on the action should increment the deliveries collection if using the test mailer" do - BaseMailer.delivery_method = :test BaseMailer.welcome.deliver assert_equal(1, BaseMailer.deliveries.length) end diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 44b8fa843d..17617206cb 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,40 @@ +* Add `config.action_dispatch.cookies_digest` option for setting custom + digest. The default remains the same - 'SHA1'. + + *Łukasz Strzałkowski* + +* Move `respond_with` (and the class-level `respond_to`) to + the `responders` gem. + + *José Valim* + +* When your templates change, browser caches bust automatically. + + New default: the template digest is automatically included in your ETags. + When you call `fresh_when @post`, the digest for `posts/show.html.erb` + is mixed in so future changes to the HTML will blow HTTP caches for you. + This makes it easy to HTTP-cache many more of your actions. + + If you render a different template, you can now pass the `:template` + option to include its digest instead: + + fresh_when @post, template: 'widgets/show' + + Pass `template: false` to skip the lookup. To turn this off entirely, set: + + config.action_controller.etag_with_template_digest = false + + *Jeremy Kemper* + +* Remove deprecated `AbstractController::Helpers::ClassMethods::MissingHelperError` + in favor of `AbstractController::Helpers::MissingHelperError`. + + *Yves Senn* + +* Fix `assert_template` not being able to assert that no files were rendered. + + *Guo Xiang Tan* + * Extract source code for the entire exception stack trace for better debugging and diagnosis. diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb index e77e4e01e9..95c67d482b 100644 --- a/actionpack/lib/abstract_controller/helpers.rb +++ b/actionpack/lib/abstract_controller/helpers.rb @@ -27,9 +27,6 @@ module AbstractController end module ClassMethods - MissingHelperError = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('AbstractController::Helpers::ClassMethods::MissingHelperError', - 'AbstractController::Helpers::MissingHelperError') - # When a class is inherited, wrap its helper module in a new module. # This ensures that the parent class's module can be changed # independently of the child class's. diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index 50bc26a80f..7f1aeafe8b 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -17,6 +17,7 @@ module ActionController autoload :ConditionalGet autoload :Cookies autoload :DataStreaming + autoload :EtagWithTemplateDigest autoload :Flash autoload :ForceSSL autoload :Head diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index e6fe6b0b00..7bbf938987 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -213,6 +213,7 @@ module ActionController Rendering, Renderers::All, ConditionalGet, + EtagWithTemplateDigest, RackDelegation, Caching, MimeResponds, diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb index 12d798d0c1..de85e0c1a7 100644 --- a/actionpack/lib/action_controller/caching.rb +++ b/actionpack/lib/action_controller/caching.rb @@ -16,7 +16,7 @@ module ActionController # All the caching stores from ActiveSupport::Cache are available to be used as backends # for Action Controller caching. # - # Configuration examples (MemoryStore is the default): + # Configuration examples (FileStore is the default): # # config.action_controller.cache_store = :memory_store # config.action_controller.cache_store = :file_store, '/path/to/cache/directory' diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index 6e0cd51d8b..a93727df90 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -41,6 +41,11 @@ module ActionController # * <tt>:last_modified</tt>. # * <tt>:public</tt> By default the Cache-Control header is private, set this to # +true+ if you want your application to be cachable by other devices (proxy caches). + # * <tt>:template</tt> By default, the template digest for the current + # controller/action is included in ETags. If the action renders a + # different template, you can include its digest instead. If the action + # doesn't render a template at all, you can pass <tt>template: false</tt> + # to skip any attempt to check for a template digest. # # === Example: # @@ -66,18 +71,24 @@ module ActionController # @article = Article.find(params[:id]) # fresh_when(@article, public: true) # end + # + # When rendering a different template than the default controller/action + # style, you can indicate which digest to include in the ETag: + # + # before_action { fresh_when @article, template: 'widgets/show' } + # def fresh_when(record_or_options, additional_options = {}) if record_or_options.is_a? Hash options = record_or_options - options.assert_valid_keys(:etag, :last_modified, :public) + options.assert_valid_keys(:etag, :last_modified, :public, :template) else record = record_or_options options = { etag: record, last_modified: record.try(:updated_at) }.merge!(additional_options) end - response.etag = combine_etags(options[:etag]) if options[:etag] - response.last_modified = options[:last_modified] if options[:last_modified] - response.cache_control[:public] = true if options[:public] + response.etag = combine_etags(options) if options[:etag] || options[:template] + response.last_modified = options[:last_modified] if options[:last_modified] + response.cache_control[:public] = true if options[:public] head :not_modified if request.fresh?(response) end @@ -93,6 +104,11 @@ module ActionController # * <tt>:last_modified</tt>. # * <tt>:public</tt> By default the Cache-Control header is private, set this to # +true+ if you want your application to be cachable by other devices (proxy caches). + # * <tt>:template</tt> By default, the template digest for the current + # controller/action is included in ETags. If the action renders a + # different template, you can include its digest instead. If the action + # doesn't render a template at all, you can pass <tt>template: false</tt> + # to skip any attempt to check for a template digest. # # === Example: # @@ -133,6 +149,14 @@ module ActionController # end # end # end + # + # When rendering a different template than the default controller/action + # style, you can indicate which digest to include in the ETag: + # + # def show + # super if stale? @article, template: 'widgets/show' + # end + # def stale?(record_or_options, additional_options = {}) fresh_when(record_or_options, additional_options) !request.fresh?(response) @@ -168,8 +192,9 @@ module ActionController end private - def combine_etags(etag) - [ etag, *etaggers.map { |etagger| instance_exec(&etagger) }.compact ] + def combine_etags(options) + etags = etaggers.map { |etagger| instance_exec(options, &etagger) }.compact + etags.unshift options[:etag] end end end diff --git a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb new file mode 100644 index 0000000000..3ca0c6837a --- /dev/null +++ b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb @@ -0,0 +1,50 @@ +module ActionController + # When our views change, they should bubble up into HTTP cache freshness + # and bust browser caches. So the template digest for the current action + # is automatically included in the ETag. + # + # Enabled by default for apps that use Action View. Disable by setting + # + # config.action_controller.etag_with_template_digest = false + # + # Override the template to digest by passing `:template` to `fresh_when` + # and `stale?` calls. For example: + # + # # We're going to render widgets/show, not posts/show + # fresh_when @post, template: 'widgets/show' + # + # # We're not going to render a template, so omit it from the ETag. + # fresh_when @post, template: false + # + module EtagWithTemplateDigest + extend ActiveSupport::Concern + + include ActionController::ConditionalGet + + included do + class_attribute :etag_with_template_digest + self.etag_with_template_digest = true + + ActiveSupport.on_load :action_view, yield: true do |action_view_base| + etag do |options| + determine_template_etag(options) if etag_with_template_digest + end + end + end + + private + def determine_template_etag(options) + if template = pick_template_for_etag(options) + lookup_and_digest_template(template) + end + end + + def pick_template_for_etag(options) + options.fetch(:template) { "#{controller_name}/#{action_name}" } + end + + def lookup_and_digest_template(template) + ActionView::Digestor.digest name: template, finder: lookup_context + end + end +end diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 00e7e980f8..5c869d4ce2 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -5,56 +5,19 @@ module ActionController #:nodoc: module MimeResponds extend ActiveSupport::Concern - included do - class_attribute :responder, :mimes_for_respond_to - self.responder = ActionController::Responder - clear_respond_to - end - module ClassMethods - # Defines mime types that are rendered by default when invoking - # <tt>respond_with</tt>. - # - # respond_to :html, :xml, :json - # - # Specifies that all actions in the controller respond to requests - # for <tt>:html</tt>, <tt>:xml</tt> and <tt>:json</tt>. - # - # To specify on per-action basis, use <tt>:only</tt> and - # <tt>:except</tt> with an array of actions or a single action: - # - # respond_to :html - # respond_to :xml, :json, except: [ :edit ] - # - # This specifies that all actions respond to <tt>:html</tt> - # and all actions except <tt>:edit</tt> respond to <tt>:xml</tt> and - # <tt>:json</tt>. - # - # respond_to :json, only: :create - # - # This specifies that the <tt>:create</tt> action and no other responds - # to <tt>:json</tt>. - def respond_to(*mimes) - options = mimes.extract_options! - - only_actions = Array(options.delete(:only)).map(&:to_s) - except_actions = Array(options.delete(:except)).map(&:to_s) - - new = mimes_for_respond_to.dup - mimes.each do |mime| - mime = mime.to_sym - new[mime] = {} - new[mime][:only] = only_actions unless only_actions.empty? - new[mime][:except] = except_actions unless except_actions.empty? - end - self.mimes_for_respond_to = new.freeze + def respond_to(*) + raise NoMethodError, "The controller-level `respond_to' feature has " \ + "been extracted to the `responders` gem. Add it to your Gemfile to " \ + "continue using this feature. Consult the Rails upgrade guide for " \ + "details." end + end - # Clear all mime types in <tt>respond_to</tt>. - # - def clear_respond_to - self.mimes_for_respond_to = Hash.new.freeze - end + def respond_with(*) + raise NoMethodError, "The `respond_with' feature has been extracted " \ + "to the `responders` gem. Add it to your Gemfile to continue using " \ + "this feature. Consult the Rails upgrade guide for details." end # Without web-service support, an action which collects the data for displaying a list of people @@ -217,7 +180,7 @@ module ActionController #:nodoc: # format.html.phone { redirect_to progress_path } # format.html.none { render "trash" } # end - # + # # Variants also support common `any`/`all` block that formats have. # # It works for both inline: @@ -253,189 +216,13 @@ module ActionController #:nodoc: def respond_to(*mimes, &block) raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given? - if collector = retrieve_collector_from_mimes(mimes, &block) - response = collector.response - response ? response.call : render({}) - end - end - - # For a given controller action, respond_with generates an appropriate - # response based on the mime-type requested by the client. - # - # If the method is called with just a resource, as in this example - - # - # class PeopleController < ApplicationController - # respond_to :html, :xml, :json - # - # def index - # @people = Person.all - # respond_with @people - # end - # end - # - # then the mime-type of the response is typically selected based on the - # request's Accept header and the set of available formats declared - # by previous calls to the controller's class method +respond_to+. Alternatively - # the mime-type can be selected by explicitly setting <tt>request.format</tt> in - # the controller. - # - # If an acceptable format is not identified, the application returns a - # '406 - not acceptable' status. Otherwise, the default response is to render - # a template named after the current action and the selected format, - # e.g. <tt>index.html.erb</tt>. If no template is available, the behavior - # depends on the selected format: - # - # * for an html response - if the request method is +get+, an exception - # is raised but for other requests such as +post+ the response - # depends on whether the resource has any validation errors (i.e. - # assuming that an attempt has been made to save the resource, - # e.g. by a +create+ action) - - # 1. If there are no errors, i.e. the resource - # was saved successfully, the response +redirect+'s to the resource - # i.e. its +show+ action. - # 2. If there are validation errors, the response - # renders a default action, which is <tt>:new</tt> for a - # +post+ request or <tt>:edit</tt> for +patch+ or +put+. - # Thus an example like this - - # - # respond_to :html, :xml - # - # def create - # @user = User.new(params[:user]) - # flash[:notice] = 'User was successfully created.' if @user.save - # respond_with(@user) - # end - # - # is equivalent, in the absence of <tt>create.html.erb</tt>, to - - # - # def create - # @user = User.new(params[:user]) - # respond_to do |format| - # if @user.save - # flash[:notice] = 'User was successfully created.' - # format.html { redirect_to(@user) } - # format.xml { render xml: @user } - # else - # format.html { render action: "new" } - # format.xml { render xml: @user } - # end - # end - # end - # - # * for a JavaScript request - if the template isn't found, an exception is - # raised. - # * for other requests - i.e. data formats such as xml, json, csv etc, if - # the resource passed to +respond_with+ responds to <code>to_<format></code>, - # the method attempts to render the resource in the requested format - # directly, e.g. for an xml request, the response is equivalent to calling - # <code>render xml: resource</code>. - # - # === Nested resources - # - # As outlined above, the +resources+ argument passed to +respond_with+ - # can play two roles. It can be used to generate the redirect url - # for successful html requests (e.g. for +create+ actions when - # no template exists), while for formats other than html and JavaScript - # it is the object that gets rendered, by being converted directly to the - # required format (again assuming no template exists). - # - # For redirecting successful html requests, +respond_with+ also supports - # the use of nested resources, which are supplied in the same way as - # in <code>form_for</code> and <code>polymorphic_url</code>. For example - - # - # def create - # @project = Project.find(params[:project_id]) - # @task = @project.comments.build(params[:task]) - # flash[:notice] = 'Task was successfully created.' if @task.save - # respond_with(@project, @task) - # end - # - # This would cause +respond_with+ to redirect to <code>project_task_url</code> - # instead of <code>task_url</code>. For request formats other than html or - # JavaScript, if multiple resources are passed in this way, it is the last - # one specified that is rendered. - # - # === Customizing response behavior - # - # Like +respond_to+, +respond_with+ may also be called with a block that - # can be used to overwrite any of the default responses, e.g. - - # - # def create - # @user = User.new(params[:user]) - # flash[:notice] = "User was successfully created." if @user.save - # - # respond_with(@user) do |format| - # format.html { render } - # end - # end - # - # The argument passed to the block is an ActionController::MimeResponds::Collector - # object which stores the responses for the formats defined within the - # block. Note that formats with responses defined explicitly in this way - # do not have to first be declared using the class method +respond_to+. - # - # Also, a hash passed to +respond_with+ immediately after the specified - # resource(s) is interpreted as a set of options relevant to all - # formats. Any option accepted by +render+ can be used, e.g. - # respond_with @people, status: 200 - # However, note that these options are ignored after an unsuccessful attempt - # to save a resource, e.g. when automatically rendering <tt>:new</tt> - # after a post request. - # - # Two additional options are relevant specifically to +respond_with+ - - # 1. <tt>:location</tt> - overwrites the default redirect location used after - # a successful html +post+ request. - # 2. <tt>:action</tt> - overwrites the default render action used after an - # unsuccessful html +post+ request. - def respond_with(*resources, &block) - if self.class.mimes_for_respond_to.empty? - raise "In order to use respond_with, first you need to declare the " \ - "formats your controller responds to in the class level." - end - - if collector = retrieve_collector_from_mimes(&block) - options = resources.size == 1 ? {} : resources.extract_options! - options = options.clone - options[:default_response] = collector.response - (options.delete(:responder) || self.class.responder).call(self, resources, options) - end - end - - protected - - # Collect mimes declared in the class method respond_to valid for the - # current action. - def collect_mimes_from_class_level #:nodoc: - action = action_name.to_s - - self.class.mimes_for_respond_to.keys.select do |mime| - config = self.class.mimes_for_respond_to[mime] - - if config[:except] - !config[:except].include?(action) - elsif config[:only] - config[:only].include?(action) - else - true - end - end - end - - # Returns a Collector object containing the appropriate mime-type response - # for the current request, based on the available responses defined by a block. - # In typical usage this is the block passed to +respond_with+ or +respond_to+. - # - # Sends :not_acceptable to the client and returns nil if no suitable format - # is available. - def retrieve_collector_from_mimes(mimes=nil, &block) #:nodoc: - mimes ||= collect_mimes_from_class_level collector = Collector.new(mimes, request.variant) block.call(collector) if block_given? - format = collector.negotiate_format(request) - if format + if format = collector.negotiate_format(request) _process_format(format) - collector + response = collector.response + response ? response.call : render({}) else raise ActionController::UnknownFormat end diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb deleted file mode 100644 index 5096558c67..0000000000 --- a/actionpack/lib/action_controller/metal/responder.rb +++ /dev/null @@ -1,297 +0,0 @@ -require 'active_support/json' - -module ActionController #:nodoc: - # Responsible for exposing a resource to different mime requests, - # usually depending on the HTTP verb. The responder is triggered when - # <code>respond_with</code> is called. The simplest case to study is a GET request: - # - # class PeopleController < ApplicationController - # respond_to :html, :xml, :json - # - # def index - # @people = Person.all - # respond_with(@people) - # end - # end - # - # When a request comes in, for example for an XML response, three steps happen: - # - # 1) the responder searches for a template at people/index.xml; - # - # 2) if the template is not available, it will invoke <code>#to_xml</code> on the given resource; - # - # 3) if the responder does not <code>respond_to :to_xml</code>, call <code>#to_format</code> on it. - # - # === Built-in HTTP verb semantics - # - # The default \Rails responder holds semantics for each HTTP verb. Depending on the - # content type, verb and the resource status, it will behave differently. - # - # Using \Rails default responder, a POST request for creating an object could - # be written as: - # - # def create - # @user = User.new(params[:user]) - # flash[:notice] = 'User was successfully created.' if @user.save - # respond_with(@user) - # end - # - # Which is exactly the same as: - # - # def create - # @user = User.new(params[:user]) - # - # respond_to do |format| - # if @user.save - # flash[:notice] = 'User was successfully created.' - # format.html { redirect_to(@user) } - # format.xml { render xml: @user, status: :created, location: @user } - # else - # format.html { render action: "new" } - # format.xml { render xml: @user.errors, status: :unprocessable_entity } - # end - # end - # end - # - # The same happens for PATCH/PUT and DELETE requests. - # - # === Nested resources - # - # You can supply nested resources as you do in <code>form_for</code> and <code>polymorphic_url</code>. - # Consider the project has many tasks example. The create action for - # TasksController would be like: - # - # def create - # @project = Project.find(params[:project_id]) - # @task = @project.tasks.build(params[:task]) - # flash[:notice] = 'Task was successfully created.' if @task.save - # respond_with(@project, @task) - # end - # - # Giving several resources ensures that the responder will redirect to - # <code>project_task_url</code> instead of <code>task_url</code>. - # - # Namespaced and singleton resources require a symbol to be given, as in - # polymorphic urls. If a project has one manager which has many tasks, it - # should be invoked as: - # - # respond_with(@project, :manager, @task) - # - # Note that if you give an array, it will be treated as a collection, - # so the following is not equivalent: - # - # respond_with [@project, :manager, @task] - # - # === Custom options - # - # <code>respond_with</code> also allows you to pass options that are forwarded - # to the underlying render call. Those options are only applied for success - # scenarios. For instance, you can do the following in the create method above: - # - # def create - # @project = Project.find(params[:project_id]) - # @task = @project.tasks.build(params[:task]) - # flash[:notice] = 'Task was successfully created.' if @task.save - # respond_with(@project, @task, status: 201) - # end - # - # This will return status 201 if the task was saved successfully. If not, - # it will simply ignore the given options and return status 422 and the - # resource errors. You can also override the location to redirect to: - # - # respond_with(@project, location: root_path) - # - # To customize the failure scenario, you can pass a block to - # <code>respond_with</code>: - # - # def create - # @project = Project.find(params[:project_id]) - # @task = @project.tasks.build(params[:task]) - # respond_with(@project, @task, status: 201) do |format| - # if @task.save - # flash[:notice] = 'Task was successfully created.' - # else - # format.html { render "some_special_template" } - # end - # end - # end - # - # Using <code>respond_with</code> with a block follows the same syntax as <code>respond_to</code>. - class Responder - attr_reader :controller, :request, :format, :resource, :resources, :options - - DEFAULT_ACTIONS_FOR_VERBS = { - :post => :new, - :patch => :edit, - :put => :edit - } - - def initialize(controller, resources, options={}) - @controller = controller - @request = @controller.request - @format = @controller.formats.first - @resource = resources.last - @resources = resources - @options = options - @action = options.delete(:action) - @default_response = options.delete(:default_response) - end - - delegate :head, :render, :redirect_to, :to => :controller - delegate :get?, :post?, :patch?, :put?, :delete?, :to => :request - - # Undefine :to_json and :to_yaml since it's defined on Object - undef_method(:to_json) if method_defined?(:to_json) - undef_method(:to_yaml) if method_defined?(:to_yaml) - - # Initializes a new responder and invokes the proper format. If the format is - # not defined, call to_format. - # - def self.call(*args) - new(*args).respond - end - - # Main entry point for responder responsible to dispatch to the proper format. - # - def respond - method = "to_#{format}" - respond_to?(method) ? send(method) : to_format - end - - # HTML format does not render the resource, it always attempt to render a - # template. - # - def to_html - default_render - rescue ActionView::MissingTemplate => e - navigation_behavior(e) - end - - # to_js simply tries to render a template. If no template is found, raises the error. - def to_js - default_render - end - - # All other formats follow the procedure below. First we try to render a - # template, if the template is not available, we verify if the resource - # responds to :to_format and display it. - # - def to_format - if get? || !has_errors? || response_overridden? - default_render - else - display_errors - end - rescue ActionView::MissingTemplate => e - api_behavior(e) - end - - protected - - # This is the common behavior for formats associated with browsing, like :html, :iphone and so forth. - def navigation_behavior(error) - if get? - raise error - elsif has_errors? && default_action - render :action => default_action - else - redirect_to navigation_location - end - end - - # This is the common behavior for formats associated with APIs, such as :xml and :json. - def api_behavior(error) - raise error unless resourceful? - raise MissingRenderer.new(format) unless has_renderer? - - if get? - display resource - elsif post? - display resource, :status => :created, :location => api_location - else - head :no_content - end - end - - # Checks whether the resource responds to the current format or not. - # - def resourceful? - resource.respond_to?("to_#{format}") - end - - # Returns the resource location by retrieving it from the options or - # returning the resources array. - # - def resource_location - options[:location] || resources - end - alias :navigation_location :resource_location - alias :api_location :resource_location - - # If a response block was given, use it, otherwise call render on - # controller. - # - def default_render - if @default_response - @default_response.call(options) - else - controller.default_render(options) - end - end - - # Display is just a shortcut to render a resource with the current format. - # - # display @user, status: :ok - # - # For XML requests it's equivalent to: - # - # render xml: @user, status: :ok - # - # Options sent by the user are also used: - # - # respond_with(@user, status: :created) - # display(@user, status: :ok) - # - # Results in: - # - # render xml: @user, status: :created - # - def display(resource, given_options={}) - controller.render given_options.merge!(options).merge!(format => resource) - end - - def display_errors - controller.render format => resource_errors, :status => :unprocessable_entity - end - - # Check whether the resource has errors. - # - def has_errors? - resource.respond_to?(:errors) && !resource.errors.empty? - end - - # Check whether the necessary Renderer is available - def has_renderer? - Renderers::RENDERERS.include?(format) - end - - # By default, render the <code>:edit</code> action for HTML requests with errors, unless - # the verb was POST. - # - def default_action - @action ||= DEFAULT_ACTIONS_FOR_VERBS[request.request_method_symbol] - end - - def resource_errors - respond_to?("#{format}_resource_errors", true) ? send("#{format}_resource_errors") : resource.errors - end - - def json_resource_errors - {:errors => resource.errors} - end - - def response_overridden? - @default_response.present? - end - end -end diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index eb5d824cbc..152420a54c 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -91,6 +91,13 @@ module ActionController # # assert that no partials were rendered # assert_template partial: false # + # # assert that a file was rendered + # assert_template file: "README.rdoc" + # + # # assert that no file was rendered + # assert_template file: nil + # assert_template file: false + # # In a view test case, you can also assert that specific locals are passed # to partials: # @@ -140,6 +147,8 @@ module ActionController if options[:file] assert_includes @_files.keys, options[:file] + elsif options.key?(:file) + assert @_files.blank?, "expected no files but #{@_files.keys} was rendered" end if expected_partial = options[:partial] diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index a519d6c1fc..8c035c3c6c 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -225,7 +225,7 @@ module ActionDispatch @remote_ip ||= (@env["action_dispatch.remote_ip"] || ip).to_s end - # Returns the unique request id, which is based off either the X-Request-Id header that can + # Returns the unique request id, which is based on either the X-Request-Id header that can # be generated by a firewall, load balancer, or web server or by the RequestId middleware # (which sets the action_dispatch.request_id environment variable). # diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index ac9e5effe2..5b3c0e7316 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -90,6 +90,7 @@ module ActionDispatch SECRET_TOKEN = "action_dispatch.secret_token".freeze SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze + COOKIES_DIGEST = "action_dispatch.cookies_digest".freeze # Cookies can typically store 4096 bytes. MAX_COOKIE_SIZE = 4096 @@ -173,10 +174,14 @@ module ActionDispatch end end + # Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream + # to the Message{Encryptor,Verifier} allows us to handle the + # (de)serialization step within the cookie jar, which gives us the + # opportunity to detect and migrate legacy cookies. module VerifyAndUpgradeLegacySignedMessage def initialize(*args) super - @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: NullSerializer) + @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: ActiveSupport::MessageEncryptor::NullSerializer) end def verify_and_upgrade_legacy_signed_message(name, signed_message) @@ -212,7 +217,8 @@ module ActionDispatch secret_token: env[SECRET_TOKEN], secret_key_base: env[SECRET_KEY_BASE], upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?, - serializer: env[COOKIES_SERIALIZER] + serializer: env[COOKIES_SERIALIZER], + digest: env[COOKIES_DIGEST] } end @@ -393,19 +399,6 @@ module ActionDispatch end end - # Passing the NullSerializer downstream to the Message{Encryptor,Verifier} - # allows us to handle the (de)serialization step within the cookie jar, - # which gives us the opportunity to detect and migrate legacy cookies. - class NullSerializer - def self.load(value) - value - end - - def self.dump(value) - value - end - end - module SerializedCookieJars MARSHAL_SIGNATURE = "\x04\x08".freeze @@ -441,6 +434,10 @@ module ActionDispatch serializer end end + + def digest + @options[:digest] || 'SHA1' + end end class SignedCookieJar #:nodoc: @@ -451,7 +448,7 @@ module ActionDispatch @parent_jar = parent_jar @options = options secret = key_generator.generate_key(@options[:signed_cookie_salt]) - @verifier = ActiveSupport::MessageVerifier.new(secret, serializer: NullSerializer) + @verifier = ActiveSupport::MessageVerifier.new(secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end def [](name) @@ -508,7 +505,7 @@ module ActionDispatch @options = options secret = key_generator.generate_key(@options[:encrypted_cookie_salt]) sign_secret = key_generator.generate_key(@options[:encrypted_signed_cookie_salt]) - @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: NullSerializer) + @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end def [](name) diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb index 5d1740d0d4..25658bac3d 100644 --- a/actionpack/lib/action_dispatch/middleware/request_id.rb +++ b/actionpack/lib/action_dispatch/middleware/request_id.rb @@ -5,7 +5,7 @@ module ActionDispatch # Makes a unique request id available to the action_dispatch.request_id env variable (which is then accessible through # ActionDispatch::Request#uuid) and sends the same id to the client via the X-Request-Id header. # - # The unique request id is either based off the X-Request-Id header in the request, which would typically be generated + # The unique request id is either based on the X-Request-Id header in the request, which would typically be generated # by a firewall, load balancer, or the web server, or, if this header is not available, a random uuid. If the # header is accepted from the outside world, we sanitize it to a max of 255 chars and alphanumeric and dashes only. # diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb index 36b01bf952..c0b53068f7 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb @@ -3,7 +3,7 @@ Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unse <% @traces.each do |name, trace| %> <% if trace.any? %> <%= name %> -<%= trace.map(&:trace).join("\n") %> +<%= trace.map { |t| t[:trace] }.join("\n") %> <% end %> <% end %> diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index cd94f35e8f..e92baa5aa7 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -63,7 +63,7 @@ module ActionDispatch attr_reader :requirements, :conditions, :defaults attr_reader :to, :default_controller, :default_action, :as, :anchor - def self.build(scope, set, path, options) + def self.build(scope, set, path, as, options) options = scope[:options].merge(options) if scope[:options] options.delete :only @@ -74,10 +74,10 @@ module ActionDispatch defaults = (scope[:defaults] || {}).merge options.delete(:defaults) || {} - new scope, set, path, defaults, options + new scope, set, path, defaults, as, options end - def initialize(scope, set, path, defaults, options) + def initialize(scope, set, path, defaults, as, options) @requirements, @conditions = {}, {} @defaults = defaults @set = set @@ -85,7 +85,7 @@ module ActionDispatch @to = options.delete :to @default_controller = options.delete(:controller) || scope[:controller] @default_action = options.delete(:action) || scope[:action] - @as = options.delete :as + @as = as @anchor = options.delete :anchor formatted = options.delete :format @@ -1046,8 +1046,6 @@ module ActionDispatch VALID_ON_OPTIONS = [:new, :collection, :member] RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except, :param, :concerns] CANONICAL_ACTIONS = %w(index create new show update destroy) - RESOURCE_METHOD_SCOPES = [:collection, :member, :new] - RESOURCE_SCOPES = [:resource, :resources] class Resource #:nodoc: attr_reader :controller, :path, :options, :param @@ -1521,7 +1519,7 @@ module ActionDispatch if on = options.delete(:on) send(on) { decomposed_match(path, options) } else - case @scope[:scope_level] + case @scope.scope_level when :resources nested { decomposed_match(path, options) } when :resource @@ -1544,13 +1542,13 @@ module ActionDispatch action = nil end - if !options.fetch(:as, true) # if it's set to nil or false - options.delete(:as) - else - options[:as] = name_for_action(options[:as], action) - end + as = if !options.fetch(:as, true) # if it's set to nil or false + options.delete(:as) + else + name_for_action(options.delete(:as), action) + end - mapping = Mapping.build(@scope, @set, URI.parser.escape(path), options) + mapping = Mapping.build(@scope, @set, URI.parser.escape(path), as, options) app, conditions, requirements, defaults, as, anchor = mapping.to_route @set.add_route(app, conditions, requirements, defaults, as, anchor) end @@ -1564,7 +1562,7 @@ module ActionDispatch raise ArgumentError, "must be called with a path and/or options" end - if @scope[:scope_level] == :resources + if @scope.resources? with_scope_level(:root) do scope(parent_resource.path) do super(options) @@ -1631,15 +1629,15 @@ module ActionDispatch end def resource_scope? #:nodoc: - RESOURCE_SCOPES.include? @scope[:scope_level] + @scope.resource_scope? end def resource_method_scope? #:nodoc: - RESOURCE_METHOD_SCOPES.include? @scope[:scope_level] + @scope.resource_method_scope? end def nested_scope? #:nodoc: - @scope[:scope_level] == :nested + @scope.nested? end def with_exclusive_scope @@ -1655,7 +1653,7 @@ module ActionDispatch end def with_scope_level(kind) - @scope = @scope.new(:scope_level => kind) + @scope = @scope.new_level(kind) yield ensure @scope = @scope.parent @@ -1699,8 +1697,8 @@ module ActionDispatch @scope[:constraints][parent_resource.param] end - def canonical_action?(action, flag) #:nodoc: - flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s) + def canonical_action?(action) #:nodoc: + resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s) end def shallow_scope(path, options = {}) #:nodoc: @@ -1714,7 +1712,7 @@ module ActionDispatch end def path_for_action(action, path) #:nodoc: - if canonical_action?(action, path.blank?) + if path.blank? && canonical_action?(action) @scope[:path].to_s else "#{@scope[:path]}/#{action_path(action, path)}" @@ -1729,15 +1727,17 @@ module ActionDispatch def prefix_name_for_action(as, action) #:nodoc: if as prefix = as - elsif !canonical_action?(action, @scope[:scope_level]) + elsif !canonical_action?(action) prefix = action end - prefix.to_s.tr('-', '_') if prefix + + if prefix && prefix != '/' && !prefix.empty? + Mapper.normalize_name prefix.to_s.tr('-', '_') + end end def name_for_action(as, action) #:nodoc: prefix = prefix_name_for_action(as, action) - prefix = Mapper.normalize_name(prefix) if prefix name_prefix = @scope[:as] if parent_resource @@ -1747,22 +1747,9 @@ module ActionDispatch member_name = parent_resource.member_name end - name = case @scope[:scope_level] - when :nested - [name_prefix, prefix] - when :collection - [prefix, name_prefix, collection_name] - when :new - [prefix, :new, name_prefix, member_name] - when :member - [prefix, name_prefix, member_name] - when :root - [name_prefix, collection_name, prefix] - else - [name_prefix, member_name, prefix] - end + name = @scope.action_name(name_prefix, prefix, collection_name, member_name) - if candidate = name.select(&:present?).join("_").presence + if candidate = name.compact.join("_").presence # If a name was not explicitly given, we check if it is valid # and return nil in case it isn't. Otherwise, we pass the invalid name # forward so the underlying router engine treats it and raises an exception. @@ -1897,11 +1884,48 @@ module ActionDispatch :controller, :action, :path_names, :constraints, :shallow, :blocks, :defaults, :options] - attr_reader :parent + RESOURCE_SCOPES = [:resource, :resources] + RESOURCE_METHOD_SCOPES = [:collection, :member, :new] + + attr_reader :parent, :scope_level - def initialize(hash, parent = {}) + def initialize(hash, parent = {}, scope_level = nil) @hash = hash @parent = parent + @scope_level = scope_level + end + + def nested? + scope_level == :nested + end + + def resources? + scope_level == :resources + end + + def resource_method_scope? + RESOURCE_METHOD_SCOPES.include? scope_level + end + + def action_name(name_prefix, prefix, collection_name, member_name) + case scope_level + when :nested + [name_prefix, prefix] + when :collection + [prefix, name_prefix, collection_name] + when :new + [prefix, :new, name_prefix, member_name] + when :member + [prefix, name_prefix, member_name] + when :root + [name_prefix, collection_name, prefix] + else + [name_prefix, member_name, prefix] + end + end + + def resource_scope? + RESOURCE_SCOPES.include? scope_level end def options @@ -1909,7 +1933,15 @@ module ActionDispatch end def new(hash) - self.class.new hash, self + self.class.new hash, self, scope_level + end + + def new_level(level) + self.class.new(self, self, level) + end + + def fetch(key, &block) + @hash.fetch(key, &block) end def [](key) diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index bd3696cda1..cd8b1ab066 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -142,22 +142,27 @@ module ActionDispatch %w(edit new).each do |action| module_eval <<-EOT, __FILE__, __LINE__ + 1 - def #{action}_polymorphic_url(record_or_hash, options = {}) # def edit_polymorphic_url(record_or_hash, options = {}) - polymorphic_url( # polymorphic_url( - record_or_hash, # record_or_hash, - options.merge(:action => "#{action}")) # options.merge(:action => "edit")) - end # end - # - def #{action}_polymorphic_path(record_or_hash, options = {}) # def edit_polymorphic_path(record_or_hash, options = {}) - polymorphic_url( # polymorphic_url( - record_or_hash, # record_or_hash, - options.merge(:action => "#{action}", :routing_type => :path)) # options.merge(:action => "edit", :routing_type => :path)) - end # end + def #{action}_polymorphic_url(record_or_hash, options = {}) + polymorphic_url_for_action("#{action}", record_or_hash, options) + end + + def #{action}_polymorphic_path(record_or_hash, options = {}) + polymorphic_path_for_action("#{action}", record_or_hash, options) + end EOT end private + def polymorphic_url_for_action(action, record_or_hash, options) + polymorphic_url(record_or_hash, options.merge(:action => action)) + end + + def polymorphic_path_for_action(action, record_or_hash, options) + options = options.merge(:action => action, :routing_type => :path) + polymorphic_path(record_or_hash, options) + end + class HelperMethodBuilder # :nodoc: CACHE = { 'path' => {}, 'url' => {} } diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 4e17d57dad..674fb253f4 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -484,6 +484,9 @@ class ForkingExecutor method = job[1] reporter = job[2] result = Minitest.run_one_method klass, method + if result.error? + translate_exceptions result + end queue.record reporter, result end } @@ -491,6 +494,20 @@ class ForkingExecutor @size.times { @queue << nil } pool.each { |pid| Process.waitpid pid } end + + private + def translate_exceptions(result) + result.failures.map! { |e| + begin + Marshal.dump e + e + rescue TypeError + ex = Exception.new e.message + ex.set_backtrace e.backtrace + Minitest::UnexpectedError.new ex + end + } + end end if ActiveSupport::Testing::Isolation.forking_env? && PROCESS_COUNT > 0 diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb index b6b5a218cc..311302819e 100644 --- a/actionpack/test/controller/action_pack_assertions_test.rb +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -488,6 +488,11 @@ class AssertTemplateTest < ActionController::TestCase assert_raise(ActiveSupport::TestCase::Assertion) do assert_template :file => 'test/hello_world' end + + get :render_file_absolute_path + assert_raise(ActiveSupport::TestCase::Assertion) do + assert_template file: nil + end end def test_with_nil_passes_when_no_template_rendered @@ -612,6 +617,24 @@ class AssertTemplateTest < ActionController::TestCase get :nothing assert_template nil + + get :partial + assert_template partial: 'test/_partial' + + get :nothing + assert_template partial: nil + + get :render_with_layout + assert_template layout: 'layouts/standard' + + get :nothing + assert_template layout: nil + + get :render_file_relative_path + assert_template file: 'README.rdoc' + + get :nothing + assert_template file: nil end end diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb index 0500b7c789..7fd1276e98 100644 --- a/actionpack/test/controller/live_stream_test.rb +++ b/actionpack/test/controller/live_stream_test.rb @@ -162,7 +162,7 @@ module ActionController end def with_stale - render :text => 'stale' if stale?(:etag => "123") + render text: 'stale' if stale?(etag: "123", template: false) end def exception_in_view diff --git a/actionpack/test/controller/mime/respond_with_test.rb b/actionpack/test/controller/mime/respond_with_test.rb deleted file mode 100644 index 115f3b2f41..0000000000 --- a/actionpack/test/controller/mime/respond_with_test.rb +++ /dev/null @@ -1,737 +0,0 @@ -require 'abstract_unit' -require 'controller/fake_models' - -class RespondWithController < ActionController::Base - class CustomerWithJson < Customer - def to_json; super; end - end - - respond_to :html, :json, :touch - respond_to :xml, :except => :using_resource_with_block - respond_to :js, :only => [ :using_resource_with_block, :using_resource, 'using_hash_resource' ] - - def using_resource - respond_with(resource) - end - - def using_hash_resource - respond_with({:result => resource}) - end - - def using_resource_with_block - respond_with(resource) do |format| - format.csv { render :text => "CSV" } - end - end - - def using_resource_with_overwrite_block - respond_with(resource) do |format| - format.html { render :text => "HTML" } - end - end - - def using_resource_with_collection - respond_with([resource, Customer.new("jamis", 9)]) - end - - def using_resource_with_parent - respond_with(Quiz::Store.new("developer?", 11), Customer.new("david", 13)) - end - - def using_resource_with_status_and_location - respond_with(resource, :location => "http://test.host/", :status => :created) - end - - def using_resource_with_json - respond_with(CustomerWithJson.new("david", request.delete? ? nil : 13)) - end - - def using_invalid_resource_with_template - respond_with(resource) - end - - def using_options_with_template - @customer = resource - respond_with(@customer, :status => 123, :location => "http://test.host/") - end - - def using_resource_with_responder - responder = proc { |c, r, o| c.render :text => "Resource name is #{r.first.name}" } - respond_with(resource, :responder => responder) - end - - def using_resource_with_action - respond_with(resource, :action => :foo) do |format| - format.html { raise ActionView::MissingTemplate.new([], "bar", ["foo"], {}, false) } - end - end - - def using_responder_with_respond - responder = Class.new(ActionController::Responder) do - def respond; @controller.render :text => "respond #{format}"; end - end - respond_with(resource, :responder => responder) - end - - def respond_with_additional_params - @params = RespondWithController.params - respond_with({:result => resource}, @params) - end - -protected - def self.params - { - :foo => 'bar' - } - end - - def resource - Customer.new("david", request.delete? ? nil : 13) - end -end - -class InheritedRespondWithController < RespondWithController - clear_respond_to - respond_to :xml, :json - - def index - respond_with(resource) do |format| - format.json { render :text => "JSON" } - end - end -end - -class RenderJsonRespondWithController < RespondWithController - clear_respond_to - respond_to :json - - def index - respond_with(resource) do |format| - format.json { render :json => RenderJsonTestException.new('boom') } - end - end - - def create - resource = ValidatedCustomer.new(params[:name], 1) - respond_with(resource) do |format| - format.json do - if resource.errors.empty? - render :json => { :valid => true } - else - render :json => { :valid => false } - end - end - end - end -end - -class CsvRespondWithController < ActionController::Base - respond_to :csv - - class RespondWithCsv - def to_csv - "c,s,v" - end - end - - def index - respond_with(RespondWithCsv.new) - end -end - -class EmptyRespondWithController < ActionController::Base - def index - respond_with(Customer.new("david", 13)) - end -end - -class RespondWithControllerTest < ActionController::TestCase - def setup - super - @request.host = "www.example.com" - Mime::Type.register_alias('text/html', :iphone) - Mime::Type.register_alias('text/html', :touch) - Mime::Type.register('text/x-mobile', :mobile) - end - - def teardown - super - Mime::Type.unregister(:iphone) - Mime::Type.unregister(:touch) - Mime::Type.unregister(:mobile) - end - - def test_respond_with_shouldnt_modify_original_hash - get :respond_with_additional_params - assert_equal RespondWithController.params, assigns(:params) - end - - def test_using_resource - @request.accept = "application/xml" - get :using_resource - assert_equal "application/xml", @response.content_type - assert_equal "<name>david</name>", @response.body - - @request.accept = "application/json" - assert_raise ActionView::MissingTemplate do - get :using_resource - end - end - - def test_using_resource_with_js_simply_tries_to_render_the_template - @request.accept = "text/javascript" - get :using_resource - assert_equal "text/javascript", @response.content_type - assert_equal "alert(\"Hi\");", @response.body - end - - def test_using_hash_resource_with_js_raises_an_error_if_template_cant_be_found - @request.accept = "text/javascript" - assert_raise ActionView::MissingTemplate do - get :using_hash_resource - end - end - - def test_using_hash_resource - @request.accept = "application/xml" - get :using_hash_resource - assert_equal "application/xml", @response.content_type - assert_equal "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<hash>\n <name>david</name>\n</hash>\n", @response.body - - @request.accept = "application/json" - get :using_hash_resource - assert_equal "application/json", @response.content_type - assert @response.body.include?("result") - assert @response.body.include?('"name":"david"') - assert @response.body.include?('"id":13') - end - - def test_using_hash_resource_with_post - @request.accept = "application/json" - assert_raise ArgumentError, "Nil location provided. Can't build URI." do - post :using_hash_resource - end - end - - def test_using_resource_with_block - @request.accept = "*/*" - get :using_resource_with_block - assert_equal "text/html", @response.content_type - assert_equal 'Hello world!', @response.body - - @request.accept = "text/csv" - get :using_resource_with_block - assert_equal "text/csv", @response.content_type - assert_equal "CSV", @response.body - - @request.accept = "application/xml" - get :using_resource - assert_equal "application/xml", @response.content_type - assert_equal "<name>david</name>", @response.body - end - - def test_using_resource_with_overwrite_block - get :using_resource_with_overwrite_block - assert_equal "text/html", @response.content_type - assert_equal "HTML", @response.body - end - - def test_not_acceptable - @request.accept = "application/xml" - assert_raises(ActionController::UnknownFormat) do - get :using_resource_with_block - end - - @request.accept = "text/javascript" - assert_raises(ActionController::UnknownFormat) do - get :using_resource_with_overwrite_block - end - end - - def test_using_resource_for_post_with_html_redirects_on_success - with_test_route_set do - post :using_resource - assert_equal "text/html", @response.content_type - assert_equal 302, @response.status - assert_equal "http://www.example.com/customers/13", @response.location - assert @response.redirect? - end - end - - def test_using_resource_for_post_with_html_rerender_on_failure - with_test_route_set do - errors = { :name => :invalid } - Customer.any_instance.stubs(:errors).returns(errors) - post :using_resource - assert_equal "text/html", @response.content_type - assert_equal 200, @response.status - assert_equal "New world!\n", @response.body - assert_nil @response.location - end - end - - def test_using_resource_for_post_with_xml_yields_created_on_success - with_test_route_set do - @request.accept = "application/xml" - post :using_resource - assert_equal "application/xml", @response.content_type - assert_equal 201, @response.status - assert_equal "<name>david</name>", @response.body - assert_equal "http://www.example.com/customers/13", @response.location - end - end - - def test_using_resource_for_post_with_xml_yields_unprocessable_entity_on_failure - with_test_route_set do - @request.accept = "application/xml" - errors = { :name => :invalid } - Customer.any_instance.stubs(:errors).returns(errors) - post :using_resource - assert_equal "application/xml", @response.content_type - assert_equal 422, @response.status - assert_equal errors.to_xml, @response.body - assert_nil @response.location - end - end - - def test_using_resource_for_post_with_json_yields_unprocessable_entity_on_failure - with_test_route_set do - @request.accept = "application/json" - errors = { :name => :invalid } - Customer.any_instance.stubs(:errors).returns(errors) - post :using_resource - assert_equal "application/json", @response.content_type - assert_equal 422, @response.status - errors = {:errors => errors} - assert_equal errors.to_json, @response.body - assert_nil @response.location - end - end - - def test_using_resource_for_patch_with_html_redirects_on_success - with_test_route_set do - patch :using_resource - assert_equal "text/html", @response.content_type - assert_equal 302, @response.status - assert_equal "http://www.example.com/customers/13", @response.location - assert @response.redirect? - end - end - - def test_using_resource_for_patch_with_html_rerender_on_failure - with_test_route_set do - errors = { :name => :invalid } - Customer.any_instance.stubs(:errors).returns(errors) - patch :using_resource - assert_equal "text/html", @response.content_type - assert_equal 200, @response.status - assert_equal "Edit world!\n", @response.body - assert_nil @response.location - end - end - - def test_using_resource_for_patch_with_html_rerender_on_failure_even_on_method_override - with_test_route_set do - errors = { :name => :invalid } - Customer.any_instance.stubs(:errors).returns(errors) - @request.env["rack.methodoverride.original_method"] = "POST" - patch :using_resource - assert_equal "text/html", @response.content_type - assert_equal 200, @response.status - assert_equal "Edit world!\n", @response.body - assert_nil @response.location - end - end - - def test_using_resource_for_put_with_html_redirects_on_success - with_test_route_set do - put :using_resource - assert_equal "text/html", @response.content_type - assert_equal 302, @response.status - assert_equal "http://www.example.com/customers/13", @response.location - assert @response.redirect? - end - end - - def test_using_resource_for_put_with_html_rerender_on_failure - with_test_route_set do - errors = { :name => :invalid } - Customer.any_instance.stubs(:errors).returns(errors) - put :using_resource - - assert_equal "text/html", @response.content_type - assert_equal 200, @response.status - assert_equal "Edit world!\n", @response.body - assert_nil @response.location - end - end - - def test_using_resource_for_put_with_html_rerender_on_failure_even_on_method_override - with_test_route_set do - errors = { :name => :invalid } - Customer.any_instance.stubs(:errors).returns(errors) - @request.env["rack.methodoverride.original_method"] = "POST" - put :using_resource - assert_equal "text/html", @response.content_type - assert_equal 200, @response.status - assert_equal "Edit world!\n", @response.body - assert_nil @response.location - end - end - - def test_using_resource_for_put_with_xml_yields_no_content_on_success - @request.accept = "application/xml" - put :using_resource - assert_equal "application/xml", @response.content_type - assert_equal 204, @response.status - assert_equal "", @response.body - end - - def test_using_resource_for_put_with_json_yields_no_content_on_success - @request.accept = "application/json" - put :using_resource_with_json - assert_equal "application/json", @response.content_type - assert_equal 204, @response.status - assert_equal "", @response.body - end - - def test_using_resource_for_put_with_xml_yields_unprocessable_entity_on_failure - @request.accept = "application/xml" - errors = { :name => :invalid } - Customer.any_instance.stubs(:errors).returns(errors) - put :using_resource - assert_equal "application/xml", @response.content_type - assert_equal 422, @response.status - assert_equal errors.to_xml, @response.body - assert_nil @response.location - end - - def test_using_resource_for_put_with_json_yields_unprocessable_entity_on_failure - @request.accept = "application/json" - errors = { :name => :invalid } - Customer.any_instance.stubs(:errors).returns(errors) - put :using_resource - assert_equal "application/json", @response.content_type - assert_equal 422, @response.status - errors = {:errors => errors} - assert_equal errors.to_json, @response.body - assert_nil @response.location - end - - def test_using_resource_for_delete_with_html_redirects_on_success - with_test_route_set do - Customer.any_instance.stubs(:destroyed?).returns(true) - delete :using_resource - assert_equal "text/html", @response.content_type - assert_equal 302, @response.status - assert_equal "http://www.example.com/customers", @response.location - end - end - - def test_using_resource_for_delete_with_xml_yields_no_content_on_success - Customer.any_instance.stubs(:destroyed?).returns(true) - @request.accept = "application/xml" - delete :using_resource - assert_equal "application/xml", @response.content_type - assert_equal 204, @response.status - assert_equal "", @response.body - end - - def test_using_resource_for_delete_with_json_yields_no_content_on_success - Customer.any_instance.stubs(:destroyed?).returns(true) - @request.accept = "application/json" - delete :using_resource_with_json - assert_equal "application/json", @response.content_type - assert_equal 204, @response.status - assert_equal "", @response.body - end - - def test_using_resource_for_delete_with_html_redirects_on_failure - with_test_route_set do - errors = { :name => :invalid } - Customer.any_instance.stubs(:errors).returns(errors) - Customer.any_instance.stubs(:destroyed?).returns(false) - delete :using_resource - assert_equal "text/html", @response.content_type - assert_equal 302, @response.status - assert_equal "http://www.example.com/customers", @response.location - end - end - - def test_using_resource_with_parent_for_get - @request.accept = "application/xml" - get :using_resource_with_parent - assert_equal "application/xml", @response.content_type - assert_equal 200, @response.status - assert_equal "<name>david</name>", @response.body - end - - def test_using_resource_with_parent_for_post - with_test_route_set do - @request.accept = "application/xml" - - post :using_resource_with_parent - assert_equal "application/xml", @response.content_type - assert_equal 201, @response.status - assert_equal "<name>david</name>", @response.body - assert_equal "http://www.example.com/quiz_stores/11/customers/13", @response.location - - errors = { :name => :invalid } - Customer.any_instance.stubs(:errors).returns(errors) - post :using_resource - assert_equal "application/xml", @response.content_type - assert_equal 422, @response.status - assert_equal errors.to_xml, @response.body - assert_nil @response.location - end - end - - def test_using_resource_with_collection - @request.accept = "application/xml" - get :using_resource_with_collection - assert_equal "application/xml", @response.content_type - assert_equal 200, @response.status - assert_match(/<name>david<\/name>/, @response.body) - assert_match(/<name>jamis<\/name>/, @response.body) - end - - def test_using_resource_with_action - @controller.instance_eval do - def render(params={}) - self.response_body = "#{params[:action]} - #{formats}" - end - end - - errors = { :name => :invalid } - Customer.any_instance.stubs(:errors).returns(errors) - - post :using_resource_with_action - assert_equal "foo - #{[:html].to_s}", @controller.response.body - end - - def test_respond_as_responder_entry_point - @request.accept = "text/html" - get :using_responder_with_respond - assert_equal "respond html", @response.body - - @request.accept = "application/xml" - get :using_responder_with_respond - assert_equal "respond xml", @response.body - end - - def test_clear_respond_to - @controller = InheritedRespondWithController.new - @request.accept = "text/html" - assert_raises(ActionController::UnknownFormat) do - get :index - end - end - - def test_first_in_respond_to_has_higher_priority - @controller = InheritedRespondWithController.new - @request.accept = "*/*" - get :index - assert_equal "application/xml", @response.content_type - assert_equal "<name>david</name>", @response.body - end - - def test_block_inside_respond_with_is_rendered - @controller = InheritedRespondWithController.new - @request.accept = "application/json" - get :index - assert_equal "JSON", @response.body - end - - def test_render_json_object_responds_to_str_still_produce_json - @controller = RenderJsonRespondWithController.new - @request.accept = "application/json" - get :index, :format => :json - assert_match(/"message":"boom"/, @response.body) - assert_match(/"error":"RenderJsonTestException"/, @response.body) - end - - def test_api_response_with_valid_resource_respect_override_block - @controller = RenderJsonRespondWithController.new - post :create, :name => "sikachu", :format => :json - assert_equal '{"valid":true}', @response.body - end - - def test_api_response_with_invalid_resource_respect_override_block - @controller = RenderJsonRespondWithController.new - post :create, :name => "david", :format => :json - assert_equal '{"valid":false}', @response.body - end - - def test_no_double_render_is_raised - @request.accept = "text/html" - assert_raise ActionView::MissingTemplate do - get :using_resource - end - end - - def test_using_resource_with_status_and_location - @request.accept = "text/html" - post :using_resource_with_status_and_location - assert @response.redirect? - assert_equal "http://test.host/", @response.location - - @request.accept = "application/xml" - get :using_resource_with_status_and_location - assert_equal 201, @response.status - end - - def test_using_resource_with_status_and_location_with_invalid_resource - errors = { :name => :invalid } - Customer.any_instance.stubs(:errors).returns(errors) - - @request.accept = "text/xml" - - post :using_resource_with_status_and_location - assert_equal errors.to_xml, @response.body - assert_equal 422, @response.status - assert_equal nil, @response.location - - put :using_resource_with_status_and_location - assert_equal errors.to_xml, @response.body - assert_equal 422, @response.status - assert_equal nil, @response.location - end - - def test_using_invalid_resource_with_template - errors = { :name => :invalid } - Customer.any_instance.stubs(:errors).returns(errors) - - @request.accept = "text/xml" - - post :using_invalid_resource_with_template - assert_equal errors.to_xml, @response.body - assert_equal 422, @response.status - assert_equal nil, @response.location - - put :using_invalid_resource_with_template - assert_equal errors.to_xml, @response.body - assert_equal 422, @response.status - assert_equal nil, @response.location - end - - def test_using_options_with_template - @request.accept = "text/xml" - - post :using_options_with_template - assert_equal "<customer-name>david</customer-name>", @response.body - assert_equal 123, @response.status - assert_equal "http://test.host/", @response.location - - put :using_options_with_template - assert_equal "<customer-name>david</customer-name>", @response.body - assert_equal 123, @response.status - assert_equal "http://test.host/", @response.location - end - - def test_using_resource_with_responder - get :using_resource_with_responder - assert_equal "Resource name is david", @response.body - end - - def test_using_resource_with_set_responder - RespondWithController.responder = proc { |c, r, o| c.render :text => "Resource name is #{r.first.name}" } - get :using_resource - assert_equal "Resource name is david", @response.body - ensure - RespondWithController.responder = ActionController::Responder - end - - def test_uses_renderer_if_an_api_behavior - ActionController::Renderers.add :csv do |obj, options| - send_data obj.to_csv, type: Mime::CSV - end - @controller = CsvRespondWithController.new - get :index, format: 'csv' - assert_equal Mime::CSV, @response.content_type - assert_equal "c,s,v", @response.body - ensure - ActionController::Renderers.remove :csv - end - - def test_raises_missing_renderer_if_an_api_behavior_with_no_renderer - @controller = CsvRespondWithController.new - assert_raise ActionController::MissingRenderer do - get :index, format: 'csv' - end - end - - def test_removing_renderers - ActionController::Renderers.add :csv do |obj, options| - send_data obj.to_csv, type: Mime::CSV - end - @controller = CsvRespondWithController.new - @request.accept = "text/csv" - get :index, format: 'csv' - assert_equal Mime::CSV, @response.content_type - - ActionController::Renderers.remove :csv - assert_raise ActionController::MissingRenderer do - get :index, format: 'csv' - end - ensure - ActionController::Renderers.remove :csv - end - - def test_error_is_raised_if_no_respond_to_is_declared_and_respond_with_is_called - @controller = EmptyRespondWithController.new - @request.accept = "*/*" - assert_raise RuntimeError do - get :index - end - end - - private - def with_test_route_set - with_routing do |set| - set.draw do - resources :customers - resources :quiz_stores do - resources :customers - end - get ":controller/:action" - end - yield - end - end -end - -class FlashResponder < ActionController::Responder - def initialize(controller, resources, options={}) - super - end - - def to_html - controller.flash[:notice] = 'Success' - super - end -end - -class FlashResponderController < ActionController::Base - self.responder = FlashResponder - respond_to :html - - def index - respond_with Object.new do |format| - format.html { render :text => 'HTML' } - end - end -end - -class FlashResponderControllerTest < ActionController::TestCase - tests FlashResponderController - - def test_respond_with_block_executed - get :index - assert_equal 'HTML', @response.body - end - - def test_flash_responder_executed - get :index - assert_equal 'Success', flash[:notice] - end -end diff --git a/actionpack/test/controller/mime/responders_test.rb b/actionpack/test/controller/mime/responders_test.rb new file mode 100644 index 0000000000..416e3191e6 --- /dev/null +++ b/actionpack/test/controller/mime/responders_test.rb @@ -0,0 +1,30 @@ +require 'abstract_unit' +require 'controller/fake_models' + +class ResponderTest < ActionController::TestCase + def test_class_level_respond_to + e = assert_raises(NoMethodError) do + Class.new(ActionController::Base) do + respond_to :json + end + end + + assert_includes e.message, '`responders` gem' + end + + def test_respond_with + klass = Class.new(ActionController::Base) do + def index + respond_with Customer.new("david", 13) + end + end + + @controller = klass.new + + e = assert_raises(NoMethodError) do + get :index + end + + assert_includes e.message, '`responders` gem' + end +end diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index 9926130c02..b036b6c08e 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -10,11 +10,17 @@ class TestControllerWithExtraEtags < ActionController::Base etag { nil } def fresh - render text: "stale" if stale?(etag: '123') + render text: "stale" if stale?(etag: '123', template: false) end def array - render text: "stale" if stale?(etag: %w(1 2 3)) + render text: "stale" if stale?(etag: %w(1 2 3), template: false) + end + + def with_template + if stale? template: 'test/hello_world' + render text: 'stale' + end end end @@ -409,6 +415,32 @@ class EtagRenderTest < ActionController::TestCase assert_response :success end + def test_etag_reflects_template_digest + get :with_template + assert_response :ok + assert_not_nil etag = @response.etag + + request.if_none_match = etag + get :with_template + assert_response :not_modified + + # Modify the template digest + path = File.expand_path('../../fixtures/test/hello_world.erb', __FILE__) + old = File.read(path) + + begin + File.write path, 'foo' + ActionView::Digestor.cache.clear + + request.if_none_match = etag + get :with_template + assert_response :ok + assert_not_equal etag, @response.etag + ensure + File.write path, old + end + end + def etag(record) Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(record)).inspect end diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index 0f145666d1..7e7dd94425 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -369,6 +369,35 @@ class CookiesTest < ActionController::TestCase assert_equal 'Jamie', @controller.send(:cookies).permanent[:user_name] end + def test_signed_cookie_using_default_digest + get :set_signed_cookie + cookies = @controller.send :cookies + assert_not_equal 45, cookies[:user_id] + assert_equal 45, cookies.signed[:user_id] + + key_generator = @request.env["action_dispatch.key_generator"] + signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"] + secret = key_generator.generate_key(signed_cookie_salt) + + verifier = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal, digest: 'SHA1') + assert_equal verifier.generate(45), cookies[:user_id] + end + + def test_signed_cookie_using_custom_digest + @request.env["action_dispatch.cookies_digest"] = 'SHA256' + get :set_signed_cookie + cookies = @controller.send :cookies + assert_not_equal 45, cookies[:user_id] + assert_equal 45, cookies.signed[:user_id] + + key_generator = @request.env["action_dispatch.key_generator"] + signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"] + secret = key_generator.generate_key(signed_cookie_salt) + + verifier = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal, digest: 'SHA256') + assert_equal verifier.generate(45), cookies[:user_id] + end + def test_signed_cookie_using_default_serializer get :set_signed_cookie cookies = @controller.send :cookies @@ -481,6 +510,27 @@ class CookiesTest < ActionController::TestCase assert_equal 'bar was dumped and loaded', cookies.encrypted[:foo] end + def test_encrypted_cookie_using_custom_digest + @request.env["action_dispatch.cookies_digest"] = 'SHA256' + get :set_encrypted_cookie + cookies = @controller.send :cookies + assert_not_equal 'bar', cookies[:foo] + assert_equal 'bar', cookies.encrypted[:foo] + + sign_secret = @request.env["action_dispatch.key_generator"].generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"]) + + sha1_verifier = ActiveSupport::MessageVerifier.new(sign_secret, serializer: ActiveSupport::MessageEncryptor::NullSerializer, digest: 'SHA1') + sha256_verifier = ActiveSupport::MessageVerifier.new(sign_secret, serializer: ActiveSupport::MessageEncryptor::NullSerializer, digest: 'SHA256') + + assert_raises(ActiveSupport::MessageVerifier::InvalidSignature) do + sha1_verifier.verify(cookies[:foo]) + end + + assert_nothing_raised do + sha256_verifier.verify(cookies[:foo]) + end + end + def test_encrypted_cookie_using_hybrid_serializer_can_migrate_marshal_dumped_value_to_json @request.env["action_dispatch.cookies_serializer"] = :hybrid diff --git a/actionpack/test/dispatch/mapper_test.rb b/actionpack/test/dispatch/mapper_test.rb index 3e554a9cf6..889f9a4736 100644 --- a/actionpack/test/dispatch/mapper_test.rb +++ b/actionpack/test/dispatch/mapper_test.rb @@ -38,7 +38,7 @@ module ActionDispatch def test_mapping_requirements options = { :controller => 'foo', :action => 'bar', :via => :get } - m = Mapper::Mapping.build({}, FakeSet.new, '/store/:name(*rest)', options) + m = Mapper::Mapping.build({}, FakeSet.new, '/store/:name(*rest)', nil, options) _, _, requirements, _ = m.to_route assert_equal(/.+?/, requirements[:rest]) end diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index 6737609567..fe9ee6f73d 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -640,7 +640,7 @@ end class RequestMethod < BaseRequestTest test "method returns environment's request method when it has not been - overriden by middleware".squish do + overridden by middleware".squish do ActionDispatch::Request::HTTP_METHODS.each do |method| request = stub_request('REQUEST_METHOD' => method) diff --git a/actionpack/test/fixtures/respond_with/edit.html.erb b/actionpack/test/fixtures/respond_with/edit.html.erb deleted file mode 100644 index ae82dfa4fc..0000000000 --- a/actionpack/test/fixtures/respond_with/edit.html.erb +++ /dev/null @@ -1 +0,0 @@ -Edit world! diff --git a/actionpack/test/fixtures/respond_with/new.html.erb b/actionpack/test/fixtures/respond_with/new.html.erb deleted file mode 100644 index 96c8f1b88b..0000000000 --- a/actionpack/test/fixtures/respond_with/new.html.erb +++ /dev/null @@ -1 +0,0 @@ -New world! diff --git a/actionpack/test/fixtures/respond_with/respond_with_additional_params.html.erb b/actionpack/test/fixtures/respond_with/respond_with_additional_params.html.erb deleted file mode 100644 index e69de29bb2..0000000000 --- a/actionpack/test/fixtures/respond_with/respond_with_additional_params.html.erb +++ /dev/null diff --git a/actionpack/test/fixtures/respond_with/using_invalid_resource_with_template.xml.erb b/actionpack/test/fixtures/respond_with/using_invalid_resource_with_template.xml.erb deleted file mode 100644 index bf5869ed22..0000000000 --- a/actionpack/test/fixtures/respond_with/using_invalid_resource_with_template.xml.erb +++ /dev/null @@ -1 +0,0 @@ -<content>I should not be displayed</content>
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_with/using_options_with_template.xml.erb b/actionpack/test/fixtures/respond_with/using_options_with_template.xml.erb deleted file mode 100644 index b313017913..0000000000 --- a/actionpack/test/fixtures/respond_with/using_options_with_template.xml.erb +++ /dev/null @@ -1 +0,0 @@ -<customer-name><%= @customer.name %></customer-name>
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_with/using_resource.js.erb b/actionpack/test/fixtures/respond_with/using_resource.js.erb deleted file mode 100644 index 4417680bce..0000000000 --- a/actionpack/test/fixtures/respond_with/using_resource.js.erb +++ /dev/null @@ -1 +0,0 @@ -alert("Hi");
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_with/using_resource_with_block.html.erb b/actionpack/test/fixtures/respond_with/using_resource_with_block.html.erb deleted file mode 100644 index 6769dd60bd..0000000000 --- a/actionpack/test/fixtures/respond_with/using_resource_with_block.html.erb +++ /dev/null @@ -1 +0,0 @@ -Hello world!
\ No newline at end of file diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 3fc2ab178c..552a902349 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,9 @@ +* Add I18n support for input/textarea placeholder text. + + Placeholder I18n follows the same convention as `label` I18n. + + *Alex Robbin* + * Fix that render layout: 'messages/layout' should also be added to the dependency tracker tree. *DHH* diff --git a/actionview/lib/action_view/helpers/tags/placeholderable.rb b/actionview/lib/action_view/helpers/tags/placeholderable.rb new file mode 100644 index 0000000000..313aa725c9 --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/placeholderable.rb @@ -0,0 +1,32 @@ +module ActionView + module Helpers + module Tags # :nodoc: + module Placeholderable # :nodoc: + def initialize(*) + super + + if tag_value = @options[:placeholder] + object_name = @object_name.gsub(/\[(.*)_attributes\]\[\d+\]/, '.\1') + method_and_value = tag_value.is_a?(TrueClass) ? @method_name : "#{@method_name}.#{tag_value}" + + if object.respond_to?(:to_model) + key = object.class.model_name.i18n_key + i18n_default = ["#{key}.#{method_and_value}".to_sym, ""] + end + + i18n_default ||= "" + placeholder = I18n.t("#{object_name}.#{method_and_value}", :default => i18n_default, :scope => "helpers.placeholder").presence + + placeholder ||= if object && object.class.respond_to?(:human_attribute_name) + object.class.human_attribute_name(method_and_value) + end + + placeholder ||= @method_name.humanize + + @options[:placeholder] = placeholder + end + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/text_area.rb b/actionview/lib/action_view/helpers/tags/text_area.rb index 9ee83ee7c2..69038c1498 100644 --- a/actionview/lib/action_view/helpers/tags/text_area.rb +++ b/actionview/lib/action_view/helpers/tags/text_area.rb @@ -1,7 +1,11 @@ +require 'action_view/helpers/tags/placeholderable' + module ActionView module Helpers module Tags # :nodoc: class TextArea < Base # :nodoc: + include Placeholderable + def render options = @options.stringify_keys add_default_name_and_id(options) diff --git a/actionview/lib/action_view/helpers/tags/text_field.rb b/actionview/lib/action_view/helpers/tags/text_field.rb index e0b80d81c2..5c576a20ca 100644 --- a/actionview/lib/action_view/helpers/tags/text_field.rb +++ b/actionview/lib/action_view/helpers/tags/text_field.rb @@ -1,7 +1,11 @@ +require 'action_view/helpers/tags/placeholderable' + module ActionView module Helpers module Tags # :nodoc: class TextField < Base # :nodoc: + include Placeholderable + def render options = @options.stringify_keys options["size"] = options["maxlength"] unless options.key?("size") diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb index 17ec6a40bf..1d50ea2ff5 100644 --- a/actionview/lib/action_view/helpers/translation_helper.rb +++ b/actionview/lib/action_view/helpers/translation_helper.rb @@ -1,4 +1,5 @@ require 'action_view/helpers/tag_helper' +require 'active_support/core_ext/string/access' require 'i18n/exceptions' module ActionView diff --git a/actionview/lib/action_view/routing_url_for.rb b/actionview/lib/action_view/routing_url_for.rb index 881a123572..75febb8652 100644 --- a/actionview/lib/action_view/routing_url_for.rb +++ b/actionview/lib/action_view/routing_url_for.rb @@ -82,7 +82,9 @@ module ActionView when nil super({:only_path => true}) when Hash - super({ :only_path => options[:host].nil? }.merge!(options.symbolize_keys)) + options = options.symbolize_keys + options[:only_path] = options[:host].nil? unless options.key?(:only_path) + super(options) when :back _back_url when Symbol diff --git a/actionview/test/activerecord/controller_runtime_test.rb b/actionview/test/activerecord/controller_runtime_test.rb index 368bec1c70..b5bfcf12a3 100644 --- a/actionview/test/activerecord/controller_runtime_test.rb +++ b/actionview/test/activerecord/controller_runtime_test.rb @@ -8,8 +8,6 @@ ActionController::Base.send :include, ActiveRecord::Railties::ControllerRuntime class ControllerRuntimeLogSubscriberTest < ActionController::TestCase class LogSubscriberController < ActionController::Base - respond_to :html - def show render :inline => "<%= Project.all %>" end @@ -21,7 +19,7 @@ class ControllerRuntimeLogSubscriberTest < ActionController::TestCase def create ActiveRecord::LogSubscriber.runtime += 100 project = Project.last - respond_with(project, location: url_for(action: :show)) + redirect_to "/" end def redirect diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb index a9f137aec6..d944214961 100644 --- a/actionview/test/template/form_helper_test.rb +++ b/actionview/test/template/form_helper_test.rb @@ -59,6 +59,35 @@ class FormHelperTest < ActionView::TestCase } } + I18n.backend.store_translations 'placeholder', { + activemodel: { + attributes: { + post: { + cost: "Total cost" + }, + :"post/cost" => { + uk: "Pounds" + } + } + }, + helpers: { + placeholder: { + post: { + title: "What is this about?", + written_on: { + spanish: "Escrito en" + }, + comments: { + body: "Write body here" + } + }, + tag: { + value: "Tag" + } + } + } + } + @post = Post.new @comment = Comment.new def @post.errors() @@ -297,6 +326,68 @@ class FormHelperTest < ActionView::TestCase ) end + def test_text_field_placeholder_without_locales + with_locale :placeholder do + assert_dom_equal('<input id="post_body" name="post[body]" placeholder="Body" type="text" value="Back to the hill and over it again!" />', text_field(:post, :body, placeholder: true)) + end + end + + def test_text_field_placeholder_with_locales + with_locale :placeholder do + assert_dom_equal('<input id="post_title" name="post[title]" placeholder="What is this about?" type="text" value="Hello World" />', text_field(:post, :title, placeholder: true)) + end + end + + def test_text_field_placeholder_with_human_attribute_name + with_locale :placeholder do + assert_dom_equal('<input id="post_cost" name="post[cost]" placeholder="Total cost" type="text" />', text_field(:post, :cost, placeholder: true)) + end + end + + def test_text_field_placeholder_with_human_attribute_name_and_value + with_locale :placeholder do + assert_dom_equal('<input id="post_cost" name="post[cost]" placeholder="Pounds" type="text" />', text_field(:post, :cost, placeholder: "uk")) + end + end + + def test_text_field_placeholder_with_locales_and_value + with_locale :placeholder do + assert_dom_equal('<input id="post_written_on" name="post[written_on]" placeholder="Escrito en" type="text" value="2004-06-15" />', text_field(:post, :written_on, placeholder: "spanish")) + end + end + + def test_text_field_placeholder_with_locales_and_nested_attributes + with_locale :placeholder do + form_for(@post, html: { id: 'create-post' }) do |f| + f.fields_for(:comments) do |cf| + concat cf.text_field(:body, placeholder: true) + end + end + + expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do + '<input id="post_comments_attributes_0_body" name="post[comments_attributes][0][body]" placeholder="Write body here" type="text" />' + end + + assert_dom_equal expected, output_buffer + end + end + + def test_text_field_placeholder_with_locales_fallback_and_nested_attributes + with_locale :placeholder do + form_for(@post, html: { id: 'create-post' }) do |f| + f.fields_for(:tags) do |cf| + concat cf.text_field(:value, placeholder: true) + end + end + + expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do + '<input id="post_tags_attributes_0_value" name="post[tags_attributes][0][value]" placeholder="Tag" type="text" value="new tag" />' + end + + assert_dom_equal expected, output_buffer + end + end + def test_text_field assert_dom_equal( '<input id="post_title" name="post[title]" type="text" value="Hello World" />', @@ -665,6 +756,83 @@ class FormHelperTest < ActionView::TestCase ) end + def test_text_area_placeholder_without_locales + with_locale :placeholder do + assert_dom_equal( + %{<textarea id="post_body" name="post[body]" placeholder="Body">\nBack to the hill and over it again!</textarea>}, + text_area(:post, :body, placeholder: true) + ) + end + end + + def test_text_area_placeholder_with_locales + with_locale :placeholder do + assert_dom_equal( + %{<textarea id="post_title" name="post[title]" placeholder="What is this about?">\nHello World</textarea>}, + text_area(:post, :title, placeholder: true) + ) + end + end + + def test_text_area_placeholder_with_human_attribute_name + with_locale :placeholder do + assert_dom_equal( + %{<textarea id="post_cost" name="post[cost]" placeholder="Total cost">\n</textarea>}, + text_area(:post, :cost, placeholder: true) + ) + end + end + + def test_text_area_placeholder_with_human_attribute_name_and_value + with_locale :placeholder do + assert_dom_equal( + %{<textarea id="post_cost" name="post[cost]" placeholder="Pounds">\n</textarea>}, + text_area(:post, :cost, placeholder: "uk") + ) + end + end + + def test_text_area_placeholder_with_locales_and_value + with_locale :placeholder do + assert_dom_equal( + %{<textarea id="post_written_on" name="post[written_on]" placeholder="Escrito en">\n2004-06-15</textarea>}, + text_area(:post, :written_on, placeholder: "spanish") + ) + end + end + + def test_text_area_placeholder_with_locales_and_nested_attributes + with_locale :placeholder do + form_for(@post, html: { id: 'create-post' }) do |f| + f.fields_for(:comments) do |cf| + concat cf.text_area(:body, placeholder: true) + end + end + + expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do + %{<textarea id="post_comments_attributes_0_body" name="post[comments_attributes][0][body]" placeholder="Write body here">\n</textarea>} + end + + assert_dom_equal expected, output_buffer + end + end + + def test_text_area_placeholder_with_locales_fallback_and_nested_attributes + with_locale :placeholder do + form_for(@post, html: { id: 'create-post' }) do |f| + f.fields_for(:tags) do |cf| + concat cf.text_area(:value, placeholder: true) + end + end + + expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do + %{<textarea id="post_tags_attributes_0_value" name="post[tags_attributes][0][value]" placeholder="Tag">\nnew tag</textarea>} + end + + assert_dom_equal expected, output_buffer + end + end + def test_text_area assert_dom_equal( %{<textarea id="post_body" name="post[body]">\nBack to the hill and over it again!</textarea>}, diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index d11243c4c0..ca04f48c1c 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -219,7 +219,7 @@ module ActiveModel rescue TypeError, NoMethodError end - changed_attributes[attr] = value + set_attribute_was(attr, value) end # Handle <tt>reset_*!</tt> for +method_missing+. @@ -233,8 +233,22 @@ module ActiveModel def restore_attribute!(attr) if attribute_changed?(attr) __send__("#{attr}=", changed_attributes[attr]) - changed_attributes.delete(attr) + clear_attribute_changes([attr]) end end + + # This is necessary because `changed_attributes` might be overridden in + # other implemntations (e.g. in `ActiveRecord`) + alias_method :attributes_changed_by_setter, :changed_attributes # :nodoc: + + # Force an attribute to have a particular "before" value + def set_attribute_was(attr, old_value) + attributes_changed_by_setter[attr] = old_value + end + + # Remove changes information for the provided attributes. + def clear_attribute_changes(attributes) + attributes_changed_by_setter.except!(*attributes) + end end end diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index cbed360fff..b09d9e336b 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -6,6 +6,45 @@ *Godfrey Chan* +* `*_was` and `changes` now work correctly for in-place attribute changes as + well. + + *Sean Griffin* + +* Fix regression on after_commit that didnt fire when having nested transactions. + + Fixes #16425 + + *arthurnn* + +* Do not try to write timestamps when a table has no timestamps columns. + + Fixes #8813. + + *Sergey Potapov* + +* `index_exists?` with `:name` option does verify specified columns. + + Example: + + add_index :articles, :title, name: "idx_title" + + # Before: + index_exists? :articles, :title, name: "idx_title" # => `true` + index_exists? :articles, :body, name: "idx_title" # => `true` + + # After: + index_exists? :articles, :title, name: "idx_title" # => `true` + index_exists? :articles, :body, name: "idx_title" # => `false` + + *Yves Senn*, *Matthew Draper* + +* When calling `update_columns` on a record that is not persisted, the error + message now reflects whether that object is a new record or has been + destroyed. + + *Lachlan Sylvester* + * Define `id_was` to get the previous value of the primary key. Currently when we call id_was and we have a custom primary key name diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 1c5a737696..d3b9b8251a 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1209,7 +1209,7 @@ module ActiveRecord # Option examples: # has_many :comments, -> { order "posted_on" } # has_many :comments, -> { includes :author } - # has_many :people, -> { where("deleted = 0").order("name") }, class_name: "Person" + # has_many :people, -> { where(deleted: false).order("name") }, class_name: "Person" # has_many :tracks, -> { order "position" }, dependent: :destroy # has_many :comments, dependent: :nullify # has_many :tags, as: :taggable @@ -1435,7 +1435,7 @@ module ActiveRecord # belongs_to :firm, foreign_key: "client_of" # belongs_to :person, primary_key: "name", foreign_key: "person_name" # belongs_to :author, class_name: "Person", foreign_key: "author_id" - # belongs_to :valid_coupon, ->(o) { where "discounts > #{o.payments_count}" }, + # belongs_to :valid_coupon, ->(o) { where "discounts > ?", o.payments_count }, # class_name: "Coupon", foreign_key: "coupon_id" # belongs_to :attachable, polymorphic: true # belongs_to :project, readonly: true diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 79c3d2b0f5..1413efaf7f 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -103,7 +103,7 @@ module ActiveRecord if has_cached_counter?(reflection) counter = cached_counter_attribute_name(reflection) owner[counter] += difference - owner.changed_attributes.delete(counter) # eww + owner.send(:clear_attribute_changes, counter) # eww end end diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 1fed7f74e7..d57da366bd 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -63,7 +63,7 @@ module ActiveRecord should_reset = (through_scope != through_reflection.klass.unscoped) || (reflection.options[:source_type] && through_reflection.collection?) - # Dont cache the association - we would only be caching a subset + # Don't cache the association - we would only be caching a subset if should_reset owners.each { |owner| owner.association(association_name).reset diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb index 6d38224830..8cc1904575 100644 --- a/activerecord/lib/active_record/attribute.rb +++ b/activerecord/lib/active_record/attribute.rb @@ -30,10 +30,14 @@ module ActiveRecord def value # `defined?` is cheaper than `||=` when we get back falsy values - @value = type_cast(value_before_type_cast) unless defined?(@value) + @value = original_value unless defined?(@value) @value end + def original_value + type_cast(value_before_type_cast) + end + def value_for_database type.type_cast_for_database(value) end @@ -54,7 +58,7 @@ module ActiveRecord self.class.from_database(name, value, type) end - def type_cast + def type_cast(*) raise NotImplementedError end @@ -62,6 +66,13 @@ module ActiveRecord true end + def ==(other) + self.class == other.class && + name == other.name && + value_before_type_cast == other.value_before_type_cast && + type == other.type + end + protected def initialize_dup(other) diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 82f1682b33..1ff28ceccc 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -282,9 +282,9 @@ module ActiveRecord end # Returns an <tt>#inspect</tt>-like string for the value of the - # attribute +attr_name+. String attributes are truncated upto 50 + # attribute +attr_name+. String attributes are truncated up to 50 # characters, Date and Time attributes are returned in the - # <tt>:db</tt> format, Array attributes are truncated upto 10 values. + # <tt>:db</tt> format, Array attributes are truncated up to 10 values. # Other attributes return the value of <tt>#inspect</tt> without # modification. # diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index b58295a106..d3f4e51c33 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -51,14 +51,6 @@ module ActiveRecord super | changed_in_place end - def attribute_changed?(attr_name, options = {}) - result = super - # We can't change "from" something in place. Only setters can define - # "from" and "to" - result ||= changed_in_place?(attr_name) unless options.key?(:from) - result - end - def changes_applied super store_original_raw_attributes @@ -69,12 +61,16 @@ module ActiveRecord original_raw_attributes.clear end + def changed_attributes + super.reverse_merge(attributes_changed_in_place).freeze + end + private def calculate_changes_from_defaults @changed_attributes = nil self.class.column_defaults.each do |attr, orig_value| - changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value) + set_attribute_was(attr, orig_value) if _field_changed?(attr, orig_value) end end @@ -100,9 +96,9 @@ module ActiveRecord def save_changed_attribute(attr, old_value) if attribute_changed?(attr) - changed_attributes.delete(attr) unless _field_changed?(attr, old_value) + clear_attribute_changes(attr) unless _field_changed?(attr, old_value) else - changed_attributes[attr] = old_value if _field_changed?(attr, old_value) + set_attribute_was(attr, old_value) if _field_changed?(attr, old_value) end end @@ -132,6 +128,13 @@ module ActiveRecord @attributes[attr].changed_from?(old_value) end + def attributes_changed_in_place + changed_in_place.each_with_object({}) do |attr_name, h| + orig = @attributes[attr_name].original_value + h[attr_name] = orig + end + end + def changed_in_place self.class.attribute_names.select do |attr_name| changed_in_place?(attr_name) 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 10753defc2..4957e1ac80 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -43,13 +43,14 @@ module ActiveRecord # index_exists?(:suppliers, :company_id, name: "idx_company_id") # def index_exists?(table_name, column_name, options = {}) - column_names = Array(column_name) - index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, :column => column_names) - if options[:unique] - indexes(table_name).any?{ |i| i.unique && i.name == index_name } - else - indexes(table_name).any?{ |i| i.name == index_name } - end + column_names = Array(column_name).map(&:to_s) + index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, column: column_names) + checks = [] + checks << lambda { |i| i.name == index_name } + checks << lambda { |i| i.columns == column_names } + checks << lambda { |i| i.unique } if options[:unique] + + indexes(table_name).any? { |i| checks.all? { |check| check[i] } } end # Returns an array of Column objects for the table specified by +table_name+. diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 4a7f2aaca8..8f06cf3a1f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -111,6 +111,8 @@ module ActiveRecord def commit super connection.release_savepoint(savepoint_name) + parent = connection.transaction_manager.current_transaction + records.each { |r| parent.add_record(r) } end def full_rollback?; false; end @@ -169,16 +171,14 @@ module ActiveRecord transaction = begin_transaction options yield rescue Exception => error - transaction.rollback if transaction + rollback_transaction if transaction raise ensure begin - transaction.commit unless error + commit_transaction unless error rescue Exception transaction.rollback raise - ensure - @stack.pop if transaction end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb index 34ed32ad35..380c50fc14 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb @@ -12,7 +12,7 @@ module ActiveRecord # roundtripping jsonb columns. This causes some false positives for # the comparison here. Therefore, we need to parse and re-dump the # raw value here to ensure the insignificant whitespaces are - # consitent with our encoder's output. + # consistent with our encoder's output. raw_old_value = type_cast_for_database(type_cast_from_database(raw_old_value)) super(raw_old_value, new_value) end diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index f0ee433d0b..5958373e88 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -145,11 +145,11 @@ module ActiveRecord value = read_attribute(attr_name) if attribute_changed?(attr_name) if mapping[old] == value - changed_attributes.delete(attr_name) + clear_attribute_changes([attr_name]) end else if old != value - changed_attributes[attr_name] = mapping.key old + set_attribute_was(attr_name, mapping.key(old)) end end else diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index b5799d6bff..755ff2b2f1 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -287,7 +287,8 @@ module ActiveRecord # This method raises an +ActiveRecord::ActiveRecordError+ when called on new # objects, or when at least one of the attributes is marked as readonly. def update_columns(attributes) - raise ActiveRecordError, "cannot update on a new record object" unless persisted? + raise ActiveRecordError, "cannot update a new record" if new_record? + raise ActiveRecordError, "cannot update a destroyed record" if destroyed? attributes.each_key do |key| verify_readonly_attribute(key.to_s) @@ -465,7 +466,7 @@ module ActiveRecord changes[self.class.locking_column] = increment_lock if locking_enabled? - changed_attributes.except!(*changes.keys) + clear_attribute_changes(changes.keys) primary_key = self.class.primary_key self.class.unscoped.where(primary_key => self[primary_key]).update_all(changes) == 1 else diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 1262b2c291..c8f382ad78 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -952,9 +952,7 @@ WARNING self.bind_values += bind_values attributes = @klass.send(:expand_hash_conditions_for_aggregates, tmp_opts) - attributes.values.grep(ActiveRecord::Relation) do |rel| - self.bind_values += rel.bind_values - end + add_relations_to_bind_values(attributes) PredicateBuilder.build_from_hash(klass, attributes, table) else @@ -1137,5 +1135,19 @@ WARNING raise ArgumentError, "The method .#{method_name}() must contain arguments." end end + + # This function is recursive just for better readablity. + # #where argument doesn't support more than one level nested hash in real world. + def add_relations_to_bind_values(attributes) + if attributes.is_a?(Hash) + attributes.each_value do |value| + if value.is_a?(ActiveRecord::Relation) + self.bind_values += value.bind_values + else + add_relations_to_bind_values(value) + end + end + end + end end end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index ddf3e1804c..936a18d99a 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -47,8 +47,9 @@ module ActiveRecord current_time = current_time_from_proper_timezone all_timestamp_attributes.each do |column| - if respond_to?(column) && respond_to?("#{column}=") && self.send(column).nil? - write_attribute(column.to_s, current_time) + column = column.to_s + if has_attribute?(column) && !attribute_present?(column) + write_attribute(column, current_time) end end end @@ -113,7 +114,7 @@ module ActiveRecord def clear_timestamp_attributes all_timestamp_attributes_in_model.each do |attribute_name| self[attribute_name] = nil - changed_attributes.delete(attribute_name) + clear_attribute_changes([attribute_name]) end end end diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb index e0a783fb45..9456a4a56c 100644 --- a/activerecord/lib/active_record/type/value.rb +++ b/activerecord/lib/active_record/type/value.rb @@ -69,13 +69,20 @@ module ActiveRecord end # Determines whether the mutable value has been modified since it was - # read. Returns +false+ by default. This method should not need to be - # overriden directly. Types which return a mutable value should include + # read. Returns +false+ by default. This method should not be overridden + # directly. Types which return a mutable value should include # +Type::Mutable+, which will define this method. def changed_in_place?(*) false end + def ==(other) + self.class == other.class && + precision == other.precision && + scale == other.scale && + limit == other.limit + end + private def type_cast(value) diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb index b0759dffde..a7b0addc1b 100644 --- a/activerecord/test/cases/adapters/mysql/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql/connection_test.rb @@ -150,7 +150,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase end end - def test_mysql_sql_mode_variable_overides_strict_mode + def test_mysql_sql_mode_variable_overrides_strict_mode run_without_connection do |orig_connection| ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { 'sql_mode' => 'ansi' })) result = ActiveRecord::Base.connection.exec_query 'SELECT @@SESSION.sql_mode' diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 3b35e69e0d..beedb4f3a1 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -76,7 +76,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase end end - def test_mysql_sql_mode_variable_overides_strict_mode + def test_mysql_sql_mode_variable_overrides_strict_mode run_without_connection do |orig_connection| ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { 'sql_mode' => 'ansi' })) result = ActiveRecord::Base.connection.exec_query 'SELECT @@SESSION.sql_mode' 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 cc58a4a1a2..859310575e 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 @@ -254,7 +254,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_build devel = Developer.find(1) - proj = assert_no_queries { devel.projects.build("name" => "Projekt") } + proj = assert_no_queries(ignore_none: false) { devel.projects.build("name" => "Projekt") } assert !devel.projects.loaded? assert_equal devel.projects.last, proj @@ -269,7 +269,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_new_aliased_to_build devel = Developer.find(1) - proj = assert_no_queries { devel.projects.new("name" => "Projekt") } + proj = assert_no_queries(ignore_none: false) { devel.projects.new("name" => "Projekt") } assert !devel.projects.loaded? assert_equal devel.projects.last, proj @@ -503,7 +503,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase developer = project.developers.first - assert_no_queries do + assert_no_queries(ignore_none: false) do assert project.developers.loaded? assert project.developers.include?(developer) end @@ -824,7 +824,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_has_and_belongs_to_many_associations_on_new_records_use_null_relations projects = Developer.new.projects - assert_no_queries do + assert_no_queries(ignore_none: false) do assert_equal [], projects assert_equal [], projects.where(title: 'omg') assert_equal [], projects.pluck(:title) diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 2c07b5cbad..b4917e727a 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -263,7 +263,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase end assert_equal klass.column_names, klass.new.attributes.keys - assert_not klass.new.attributes.key?('id') + assert_not klass.new.has_attribute?('id') end def test_hashes_not_mangled diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb index 91f6aee931..7b325abf1d 100644 --- a/activerecord/test/cases/attribute_test.rb +++ b/activerecord/test/cases/attribute_test.rb @@ -138,5 +138,35 @@ module ActiveRecord test "uninitialized attributes have no value" do assert_nil Attribute.uninitialized(:foo, nil).value end + + test "attributes equal other attributes with the same constructor arguments" do + first = Attribute.from_database(:foo, 1, Type::Integer.new) + second = Attribute.from_database(:foo, 1, Type::Integer.new) + assert_equal first, second + end + + test "attributes do not equal attributes with different names" do + first = Attribute.from_database(:foo, 1, Type::Integer.new) + second = Attribute.from_database(:bar, 1, Type::Integer.new) + assert_not_equal first, second + end + + test "attributes do not equal attributes with different types" do + first = Attribute.from_database(:foo, 1, Type::Integer.new) + second = Attribute.from_database(:foo, 1, Type::Float.new) + assert_not_equal first, second + end + + test "attributes do not equal attributes with different values" do + first = Attribute.from_database(:foo, 1, Type::Integer.new) + second = Attribute.from_database(:foo, 2, Type::Integer.new) + assert_not_equal first, second + end + + test "attributes do not equal attributes of other classes" do + first = Attribute.from_database(:foo, 1, Type::Integer.new) + second = Attribute.from_user(:foo, 1, Type::Integer.new) + assert_not_equal first, second + end end end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 69a7f25213..0c77eedb52 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -661,6 +661,27 @@ class DirtyTest < ActiveRecord::TestCase assert_not model.foo_changed? end + test "in place mutation detection" do + pirate = Pirate.create!(catchphrase: "arrrr") + pirate.catchphrase << " matey!" + + assert pirate.catchphrase_changed? + expected_changes = { + "catchphrase" => ["arrrr", "arrrr matey!"] + } + assert_equal(expected_changes, pirate.changes) + assert_equal("arrrr", pirate.catchphrase_was) + assert pirate.catchphrase_changed?(from: "arrrr") + assert_not pirate.catchphrase_changed?(from: "anything else") + assert pirate.changed_attributes.include?(:catchphrase) + + pirate.save! + pirate.reload + + assert_equal "arrrr matey!", pirate.catchphrase + assert_not pirate.changed? + end + private def with_partial_writes(klass, on = true) old = klass.partial_writes? diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 40e51a0cdc..95d006279d 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -51,7 +51,7 @@ class FinderTest < ActiveRecord::TestCase end def test_symbols_table_ref - Post.first # warm up + Post.where("author_id" => nil) # warm up x = Symbol.all_symbols.count Post.where("title" => {"xxxqqqq" => "bar"}) assert_equal x, Symbol.all_symbols.count diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb index 4e6d7963aa..e6aa901814 100644 --- a/activerecord/test/cases/migration/columns_test.rb +++ b/activerecord/test/cases/migration/columns_test.rb @@ -66,6 +66,9 @@ module ActiveRecord def test_mysql_rename_column_preserves_auto_increment rename_column "test_models", "id", "id_test" assert_equal "auto_increment", connection.columns("test_models").find { |c| c.name == "id_test" }.extra + TestModel.reset_column_information + ensure + rename_column "test_models", "id_test", "id" end end diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb index 93c3bfae7a..ac932378fd 100644 --- a/activerecord/test/cases/migration/index_test.rb +++ b/activerecord/test/cases/migration/index_test.rb @@ -95,6 +95,12 @@ module ActiveRecord assert connection.index_exists?(:testings, [:foo, :bar]) end + def test_index_exists_with_custom_name_checks_columns + connection.add_index :testings, [:foo, :bar], name: "my_index" + assert connection.index_exists?(:testings, [:foo, :bar], name: "my_index") + assert_not connection.index_exists?(:testings, [:foo], name: "my_index") + end + def test_valid_index_options assert_raise ArgumentError do connection.add_index :testings, :foo, unqiue: true diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index a6a36a6fd9..b4804aa9d7 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -61,6 +61,15 @@ module ActiveRecord assert_equal expected.to_sql, actual.to_sql end + def test_belongs_to_nested_where_with_relation + author = authors(:david) + + expected = Author.where(id: author ).joins(:posts) + actual = Author.where(posts: { author_id: Author.where(id: author.id) }).joins(:posts) + + assert_equal expected.to_a, actual.to_a + end + def test_polymorphic_shallow_where treasure = Treasure.new treasure.id = 1 diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 4e71d04bc0..066e6b6468 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -2,6 +2,8 @@ require "cases/helper" require 'support/schema_dumping_helper' class SchemaDumperTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + setup do ActiveRecord::SchemaMigration.create_table end @@ -21,6 +23,8 @@ class SchemaDumperTest < ActiveRecord::TestCase schema_info = ActiveRecord::Base.connection.dump_schema_information assert_match(/20100201010101.*20100301010101/m, schema_info) + ensure + ActiveRecord::SchemaMigration.delete_all end def test_magic_comment diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb index d8a467ec4d..8e512e118a 100644 --- a/activerecord/test/cases/scoping/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -11,6 +11,10 @@ require 'models/reference' class RelationScopingTest < ActiveRecord::TestCase fixtures :authors, :developers, :projects, :comments, :posts, :developers_projects + setup do + developers(:david) + end + def test_reverse_order assert_equal Developer.order("id DESC").to_a.reverse, Developer.order("id DESC").reverse_order end @@ -260,7 +264,7 @@ class NestedRelationScopingTest < ActiveRecord::TestCase end end -class HasManyScopingTest< ActiveRecord::TestCase +class HasManyScopingTest < ActiveRecord::TestCase fixtures :comments, :posts, :people, :references def setup @@ -306,7 +310,7 @@ class HasManyScopingTest< ActiveRecord::TestCase end end -class HasAndBelongsToManyScopingTest< ActiveRecord::TestCase +class HasAndBelongsToManyScopingTest < ActiveRecord::TestCase fixtures :posts, :categories, :categories_posts def setup diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index 23a170388e..4070216733 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -93,7 +93,7 @@ module ActiveRecord # ignored SQL, or better yet, use a different notification for the queries # instead examining the SQL content. oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im] - mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i] + mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /] postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i] sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im] diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 0472246f71..abf6becc17 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -1,4 +1,5 @@ require 'cases/helper' +require 'support/ddl_helper' require 'models/developer' require 'models/owner' require 'models/pet' @@ -424,3 +425,21 @@ class TimestampTest < ActiveRecord::TestCase assert_equal [:created_at, :updated_at], toy.send(:all_timestamp_attributes_in_model) end end + +class TimestampsWithoutTransactionTest < ActiveRecord::TestCase + include DdlHelper + self.use_transactional_fixtures = false + + class TimestampAttributePost < ActiveRecord::Base + attr_accessor :created_at, :updated_at + end + + def test_do_not_write_timestamps_on_save_if_they_are_not_attributes + with_example_table ActiveRecord::Base.connection, "timestamp_attribute_posts", "id integer primary key" do + post = TimestampAttributePost.new(id: 1) + post.save! # should not try to assign and persist created_at, updated_at + assert_nil post.created_at + assert_nil post.updated_at + end + end +end diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index 3d64ecb464..a3f39804b7 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -129,6 +129,19 @@ class TransactionCallbacksTest < ActiveRecord::TestCase assert_equal [:commit_on_update], @first.history end + def test_only_call_after_commit_on_top_level_transactions + @first.after_commit_block{|r| r.history << :after_commit} + assert @first.history.empty? + + @first.transaction do + @first.transaction(requires_new: true) do + @first.touch + end + assert @first.history.empty? + end + assert_equal [:after_commit], @first.history + end + def test_call_after_rollback_after_transaction_rollsback @first.after_commit_block{|r| r.history << :after_commit} @first.after_rollback_block{|r| r.history << :after_rollback} diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index b4849222b8..9cfe041de2 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -80,6 +80,30 @@ class TransactionTest < ActiveRecord::TestCase end end + def test_number_of_transactions_in_commit + num = nil + + Topic.connection.class_eval do + alias :real_commit_db_transaction :commit_db_transaction + define_method(:commit_db_transaction) do + num = transaction_manager.open_transactions + real_commit_db_transaction + end + end + + Topic.transaction do + @first.approved = true + @first.save! + end + + assert_equal 0, num + ensure + Topic.connection.class_eval do + remove_method :commit_db_transaction + alias :commit_db_transaction :real_commit_db_transaction rescue nil + end + end + def test_successful_with_instance_method @first.transaction do @first.approved = true diff --git a/activerecord/test/cases/types_test.rb b/activerecord/test/cases/types_test.rb index 5c54812f30..db4f78d354 100644 --- a/activerecord/test/cases/types_test.rb +++ b/activerecord/test/cases/types_test.rb @@ -149,6 +149,12 @@ module ActiveRecord end end + def test_type_equality + assert_equal Type::Value.new, Type::Value.new + assert_not_equal Type::Value.new, Type::Integer.new + assert_not_equal Type::Value.new(precision: 1), Type::Value.new(precision: 2) + end + if current_adapter?(:SQLite3Adapter) def test_binary_encoding type = SQLite3Binary.new diff --git a/activerecord/test/cases/validations/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb index 3db742c15b..268d7914b5 100644 --- a/activerecord/test/cases/validations/i18n_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_validation_test.rb @@ -6,6 +6,7 @@ class I18nValidationTest < ActiveRecord::TestCase repair_validations(Topic, Reply) def setup + repair_validations(Topic, Reply) Reply.validates_presence_of(:title) @topic = Topic.new @old_load_path, @old_backend = I18n.load_path.dup, I18n.backend diff --git a/activerecord/test/cases/validations_repair_helper.rb b/activerecord/test/cases/validations_repair_helper.rb index c02b3241cd..2bbf0f23b3 100644 --- a/activerecord/test/cases/validations_repair_helper.rb +++ b/activerecord/test/cases/validations_repair_helper.rb @@ -13,7 +13,7 @@ module ActiveRecord end def repair_validations(*model_classes) - yield + yield if block_given? ensure model_classes.each do |k| k.clear_validators! diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 03e8d73ea7..b961b373cb 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,9 @@ +* Fix ActiveSupport::TestCase not to order users' test cases by default. + If this change breaks your tests because your tests are order dependent, you need to explicitly call + ActiveSupport::TestCase.my_tests_are_order_dependent! at the top of your tests. + + *Akira Matsuda* + * Fix DateTime comparison with DateTime::Infinity object. *Rafael Mendonça França* diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index 18ba79a8f9..53022de549 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -303,8 +303,8 @@ module ActiveSupport def safe_constantize(camel_cased_word) constantize(camel_cased_word) rescue NameError => e - raise unless e.message =~ /(uninitialized constant|wrong constant name) #{const_regexp(camel_cased_word)}$/ || - e.name.to_s == camel_cased_word.to_s + raise if e.name && !(camel_cased_word.to_s.split("::").include?(e.name.to_s) || + e.name.to_s == camel_cased_word.to_s) rescue ArgumentError => e raise unless e.message =~ /not missing constant #{const_regexp(camel_cased_word)}\!$/ end diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index b019ad0dec..92ab6fe648 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -40,6 +40,7 @@ module ActiveSupport # Options: # * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by # <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-cbc'. + # * <tt>:digest</tt> - String of digest to use for signing. Default is +SHA1+. # * <tt>:serializer</tt> - Object serializer to use. Default is +Marshal+. def initialize(secret, *signature_key_or_options) options = signature_key_or_options.extract_options! @@ -47,7 +48,7 @@ module ActiveSupport @secret = secret @sign_secret = sign_secret @cipher = options[:cipher] || 'aes-256-cbc' - @verifier = MessageVerifier.new(@sign_secret || @secret, :serializer => NullSerializer) + @verifier = MessageVerifier.new(@sign_secret || @secret, digest: options[:digest] || 'SHA1', serializer: NullSerializer) @serializer = options[:serializer] || Marshal end diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb index a6a878140c..0df599b692 100644 --- a/activesupport/lib/active_support/test_case.rb +++ b/activesupport/lib/active_support/test_case.rb @@ -15,16 +15,11 @@ module ActiveSupport class TestCase < ::Minitest::Test Assertion = Minitest::Assertion - alias_method :method_name, :name - - $tags = {} - def self.for_tag(tag) - yield if $tags[tag] + class << self + alias :my_tests_are_order_dependent! :i_suck_and_my_tests_are_order_dependent! end - # FIXME: we have tests that depend on run order, we should fix that and - # remove this method call. - self.i_suck_and_my_tests_are_order_dependent! + alias_method :method_name, :name include ActiveSupport::Testing::TaggedLogging include ActiveSupport::Testing::SetupAndTeardown diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index a013aadd67..5fc3de651a 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -47,18 +47,22 @@ class DependenciesTest < ActiveSupport::TestCase end def test_tracking_loaded_files - require_dependency 'dependencies/service_one' - require_dependency 'dependencies/service_two' - assert_equal 2, ActiveSupport::Dependencies.loaded.size + with_loading do + require_dependency 'dependencies/service_one' + require_dependency 'dependencies/service_two' + assert_equal 2, ActiveSupport::Dependencies.loaded.size + end ensure Object.send(:remove_const, :ServiceOne) if Object.const_defined?(:ServiceOne) Object.send(:remove_const, :ServiceTwo) if Object.const_defined?(:ServiceTwo) end def test_tracking_identical_loaded_files - require_dependency 'dependencies/service_one' - require_dependency 'dependencies/service_one' - assert_equal 1, ActiveSupport::Dependencies.loaded.size + with_loading do + require_dependency 'dependencies/service_one' + require_dependency 'dependencies/service_one' + assert_equal 1, ActiveSupport::Dependencies.loaded.size + end ensure Object.send(:remove_const, :ServiceOne) if Object.const_defined?(:ServiceOne) end @@ -990,11 +994,4 @@ class DependenciesTest < ActiveSupport::TestCase ensure ActiveSupport::Dependencies.hook! end - -private - def remove_constants(*constants) - constants.each do |constant| - Object.send(:remove_const, constant) if Object.const_defined?(constant) - end - end end diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb index 659fceb852..73f640e8e5 100644 --- a/activesupport/test/multibyte_chars_test.rb +++ b/activesupport/test/multibyte_chars_test.rb @@ -3,19 +3,12 @@ require 'abstract_unit' require 'multibyte_test_helpers' require 'active_support/core_ext/string/multibyte' -class String - def __method_for_multibyte_testing_with_integer_result; 1; end - def __method_for_multibyte_testing; 'result'; end - def __method_for_multibyte_testing!; 'result'; end - def __method_for_multibyte_testing_that_returns_nil!; end -end - class MultibyteCharsTest < ActiveSupport::TestCase include MultibyteTestHelpers def setup @proxy_class = ActiveSupport::Multibyte::Chars - @chars = @proxy_class.new UNICODE_STRING + @chars = @proxy_class.new UNICODE_STRING.dup end def test_wraps_the_original_string @@ -24,6 +17,8 @@ class MultibyteCharsTest < ActiveSupport::TestCase end def test_should_allow_method_calls_to_string + @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing; 'result'; end } + assert_nothing_raised do @chars.__method_for_multibyte_testing end @@ -33,28 +28,36 @@ class MultibyteCharsTest < ActiveSupport::TestCase end def test_forwarded_method_calls_should_return_new_chars_instance + @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing; 'result'; end } + assert_kind_of @proxy_class, @chars.__method_for_multibyte_testing assert_not_equal @chars.object_id, @chars.__method_for_multibyte_testing.object_id end def test_forwarded_bang_method_calls_should_return_the_original_chars_instance_when_result_is_not_nil + @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing!; 'result'; end } + assert_kind_of @proxy_class, @chars.__method_for_multibyte_testing! assert_equal @chars.object_id, @chars.__method_for_multibyte_testing!.object_id end def test_forwarded_bang_method_calls_should_return_nil_when_result_is_nil + @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing_that_returns_nil!; end } + assert_nil @chars.__method_for_multibyte_testing_that_returns_nil! end def test_methods_are_forwarded_to_wrapped_string_for_byte_strings original_encoding = BYTE_STRING.encoding assert_equal BYTE_STRING.length, BYTE_STRING.mb_chars.length - ensure - BYTE_STRING.force_encoding(original_encoding) end def test_forwarded_method_with_non_string_result_should_be_returned_vertabim - assert_equal ''.__method_for_multibyte_testing_with_integer_result, @chars.__method_for_multibyte_testing_with_integer_result + str = '' + str.singleton_class.class_eval { def __method_for_multibyte_testing_with_integer_result; 1; end } + @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing_with_integer_result; 1; end } + + assert_equal str.__method_for_multibyte_testing_with_integer_result, @chars.__method_for_multibyte_testing_with_integer_result end def test_should_concatenate @@ -103,7 +106,6 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase @chars = UNICODE_STRING.dup.mb_chars # Ruby 1.9 only supports basic whitespace @whitespace = "\n\t " - @byte_order_mark = [65279].pack('U') end def test_split_should_return_an_array_of_chars_instances diff --git a/activesupport/test/multibyte_test_helpers.rb b/activesupport/test/multibyte_test_helpers.rb index fdbe2f4350..90af2dadd4 100644 --- a/activesupport/test/multibyte_test_helpers.rb +++ b/activesupport/test/multibyte_test_helpers.rb @@ -1,9 +1,9 @@ # encoding: utf-8 module MultibyteTestHelpers - UNICODE_STRING = 'こにちわ' - ASCII_STRING = 'ohayo' - BYTE_STRING = "\270\236\010\210\245".force_encoding("ASCII-8BIT") + UNICODE_STRING = 'こにちわ'.freeze + ASCII_STRING = 'ohayo'.freeze + BYTE_STRING = "\270\236\010\210\245".force_encoding("ASCII-8BIT").freeze def chars(str) ActiveSupport::Multibyte::Chars.new(str) diff --git a/guides/source/4_2_release_notes.md b/guides/source/4_2_release_notes.md index a39dd9ace0..e8d1dc361a 100644 --- a/guides/source/4_2_release_notes.md +++ b/guides/source/4_2_release_notes.md @@ -94,6 +94,12 @@ Action Pack Please refer to the [Changelog][action-pack] for detailed changes. +### Removals + +* Removed deprecated `AbstractController::Helpers::ClassMethods::MissingHelperError` + in favor of `AbstractController::Helpers::MissingHelperError`. + ([Commit](https://github.com/rails/rails/commit/a1ddde15ae0d612ff2973de9cf768ed701b594e8)) + ### Deprecations * Deprecated support for setting the `:to` option of a router to a symbol or a diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md index 0902e347e2..d0f3a596fe 100644 --- a/guides/source/caching_with_rails.md +++ b/guides/source/caching_with_rails.md @@ -353,7 +353,12 @@ Instead of an options hash, you can also simply pass in a model, Rails will use class ProductsController < ApplicationController def show @product = Product.find(params[:id]) - respond_with(@product) if stale?(@product) + + if stale?(@product) + respond_to do |wants| + # ... normal response processing + end + end end end ``` diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md index 0b05725623..8bc4b10591 100644 --- a/guides/source/contributing_to_ruby_on_rails.md +++ b/guides/source/contributing_to_ruby_on_rails.md @@ -397,7 +397,7 @@ inside, just indent it with 4 spaces: class ArticlesController def index - respond_with Article.limit(10) + render json: Article.limit(10) end end diff --git a/guides/source/generators.md b/guides/source/generators.md index 5e88fa0c70..2349908979 100644 --- a/guides/source/generators.md +++ b/guides/source/generators.md @@ -8,6 +8,7 @@ After reading this guide, you will know: * How to see which generators are available in your application. * How to create a generator using templates. * How Rails searches for generators before invoking them. +* How Rails internally generates Rails code from the templates. * How to customize your scaffold by creating new generators. * How to customize your scaffold by changing generator templates. * How to use fallbacks to avoid overwriting a huge set of generators. @@ -340,6 +341,20 @@ end If you generate another resource, you can see that we get exactly the same result! This is useful if you want to customize your scaffold templates and/or layout by just creating `edit.html.erb`, `index.html.erb` and so on inside `lib/templates/erb/scaffold`. +Many scaffold templates in Rails are written in ERB tags which needs to be escaped, so that the output is a valid ERB code, that can be used correctly in Rails app. + +The following code in one of the generator file, + +```ruby +<%%= stylesheet_include_tag :application %> +``` + +when passed through the generator, would generate the following output. + +```ruby +<%= stylesheet_include_tag :application %> +``` + Adding Generators Fallbacks --------------------------- diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md index f00f7bca1b..8b37b92139 100644 --- a/guides/source/layouts_and_rendering.md +++ b/guides/source/layouts_and_rendering.md @@ -903,7 +903,7 @@ You can also specify multiple videos to play by passing an array of videos to th This will produce: ```erb -<video><source src="trailer.ogg" /><source src="movie.ogg" /></video> +<video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video> ``` #### Linking to Audio Files with the `audio_tag` diff --git a/guides/source/plugins.md b/guides/source/plugins.md index 489bd227cd..f10699fbeb 100644 --- a/guides/source/plugins.md +++ b/guides/source/plugins.md @@ -440,5 +440,5 @@ $ bin/rake rdoc * [Developing a RubyGem using Bundler](https://github.com/radar/guides/blob/master/gem-development.md) * [Using .gemspecs as Intended](http://yehudakatz.com/2010/04/02/using-gemspecs-as-intended/) -* [Gemspec Reference](http://docs.rubygems.org/read/chapter/20) +* [Gemspec Reference](http://guides.rubygems.org/specification-reference/) * [GemPlugins: A Brief Introduction to the Future of Rails Plugins](http://www.intridea.com/blog/2008/6/11/gemplugins-a-brief-introduction-to-the-future-of-rails-plugins) diff --git a/guides/source/security.md b/guides/source/security.md index ebfcc5bdd0..125dd82666 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -118,9 +118,9 @@ It works like this: * A user receives credits, the amount is stored in a session (which is a bad idea anyway, but we'll do this for demonstration purposes). * The user buys something. -* Their new, lower credit will be stored in the session. -* The dark side of the user forces them to take the cookie from the first step (which they copied) and replace the current cookie in the browser. -* The user has their credit back. +* The new adjusted credit value is stored in the session. +* The user takes the cookie from the first step (which they previously copied) and replaces the current cookie in the browser. +* The user has their original credit back. Including a nonce (a random value) in the session solves replay attacks. A nonce is valid only once, and the server has to keep track of all the valid nonces. It gets even more complicated if you have several application servers (mongrels). Storing nonces in a database table would defeat the entire purpose of CookieStore (avoiding accessing the database). @@ -847,7 +847,7 @@ It is recommended to _use RedCloth in combination with a whitelist input filter_ NOTE: _The same security precautions have to be taken for Ajax actions as for "normal" ones. There is at least one exception, however: The output has to be escaped in the controller already, if the action doesn't render a view._ -If you use the [in_place_editor plugin](http://dev.rubyonrails.org/browser/plugins/in_place_editing), or actions that return a string, rather than rendering a view, _you have to escape the return value in the action_. Otherwise, if the return value contains a XSS string, the malicious code will be executed upon return to the browser. Escape any input value using the h() method. +If you use the [in_place_editor plugin](https://rubygems.org/gems/in_place_editing), or actions that return a string, rather than rendering a view, _you have to escape the return value in the action_. Otherwise, if the return value contains a XSS string, the malicious code will be executed upon return to the browser. Escape any input value using the h() method. ### Command Line Injection diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 61639be7c6..6a4660bad0 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -256,7 +256,8 @@ module Rails "action_dispatch.signed_cookie_salt" => config.action_dispatch.signed_cookie_salt, "action_dispatch.encrypted_cookie_salt" => config.action_dispatch.encrypted_cookie_salt, "action_dispatch.encrypted_signed_cookie_salt" => config.action_dispatch.encrypted_signed_cookie_salt, - "action_dispatch.cookies_serializer" => config.action_dispatch.cookies_serializer + "action_dispatch.cookies_serializer" => config.action_dispatch.cookies_serializer, + "action_dispatch.cookies_digest" => config.action_dispatch.cookies_digest }) end end diff --git a/railties/lib/rails/generators/rails/controller/controller_generator.rb b/railties/lib/rails/generators/rails/controller/controller_generator.rb index fbecab1823..a48cc13ed7 100644 --- a/railties/lib/rails/generators/rails/controller/controller_generator.rb +++ b/railties/lib/rails/generators/rails/controller/controller_generator.rb @@ -2,7 +2,7 @@ module Rails module Generators class ControllerGenerator < NamedBase # :nodoc: argument :actions, type: :array, default: [], banner: "action action" - class_option :skip_routes, type: :boolean, desc: "Dont' add routes to config/routes.rb." + class_option :skip_routes, type: :boolean, desc: "Don't add routes to config/routes.rb." check_class_collision suffix: "Controller" diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb index b5765c391e..127e059bf1 100644 --- a/railties/test/generators_test.rb +++ b/railties/test/generators_test.rb @@ -152,6 +152,8 @@ class GeneratorsTest < Rails::Generators::TestCase klass = Rails::Generators.find_by_namespace(:plugin, :remarkable) assert klass assert_equal "test_unit:plugin", klass.namespace + ensure + Rails::Generators.fallbacks.delete(:remarkable) end def test_fallbacks_for_generators_on_find_by_namespace_with_context @@ -159,18 +161,26 @@ class GeneratorsTest < Rails::Generators::TestCase klass = Rails::Generators.find_by_namespace(:remarkable, :rails, :plugin) assert klass assert_equal "test_unit:plugin", klass.namespace + ensure + Rails::Generators.fallbacks.delete(:remarkable) end def test_fallbacks_for_generators_on_invoke Rails::Generators.fallbacks[:shoulda] = :test_unit TestUnit::Generators::ModelGenerator.expects(:start).with(["Account"], {}) Rails::Generators.invoke "shoulda:model", ["Account"] + ensure + Rails::Generators.fallbacks.delete(:shoulda) end def test_nested_fallbacks_for_generators + Rails::Generators.fallbacks[:shoulda] = :test_unit Rails::Generators.fallbacks[:super_shoulda] = :shoulda TestUnit::Generators::ModelGenerator.expects(:start).with(["Account"], {}) Rails::Generators.invoke "super_shoulda:model", ["Account"] + ensure + Rails::Generators.fallbacks.delete(:shoulda) + Rails::Generators.fallbacks.delete(:super_shoulda) end def test_developer_options_are_overwritten_by_user_options |