Merge branch 'master' of github.com:rails/rails
This commit is contained in:
commit
8f15565de8
2
Gemfile
2
Gemfile
@ -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'
|
||||
|
@ -1,3 +1,39 @@
|
||||
* Use the Active Support JSON encoder for cookie jars using the `:json` or
|
||||
`:hybrid` serializer. This allows you to serialize custom Ruby objects into
|
||||
cookies by defining the `#as_json` hook on such objects.
|
||||
|
||||
Fixes #16520.
|
||||
|
||||
*Godfrey Chan*
|
||||
|
||||
* 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`.
|
||||
|
||||
|
@ -17,6 +17,7 @@ module ActionController
|
||||
autoload :ConditionalGet
|
||||
autoload :Cookies
|
||||
autoload :DataStreaming
|
||||
autoload :EtagWithTemplateDigest
|
||||
autoload :Flash
|
||||
autoload :ForceSSL
|
||||
autoload :Head
|
||||
|
@ -213,6 +213,7 @@ def self.without_modules(*modules)
|
||||
Rendering,
|
||||
Renderers::All,
|
||||
ConditionalGet,
|
||||
EtagWithTemplateDigest,
|
||||
RackDelegation,
|
||||
Caching,
|
||||
MimeResponds,
|
||||
|
@ -41,6 +41,11 @@ def etag(&etagger)
|
||||
# * <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 @@ def etag(&etagger)
|
||||
# @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 @@ def fresh_when(record_or_options, additional_options = {})
|
||||
# * <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 @@ def fresh_when(record_or_options, additional_options = {})
|
||||
# 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 @@ def expires_now
|
||||
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
|
||||
|
@ -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
|
@ -5,56 +5,22 @@ module ActionController #:nodoc:
|
||||
module MimeResponds
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
class_attribute :responder, :mimes_for_respond_to
|
||||
self.responder = ActionController::Responder
|
||||
clear_respond_to
|
||||
module ClassMethods
|
||||
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:\n" \
|
||||
" gem 'responders', '~> 2.0'\n" \
|
||||
"Consult the Rails upgrade guide for details."
|
||||
end
|
||||
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
|
||||
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:\n" \
|
||||
" gem 'responders', '~> 2.0'\n" \
|
||||
"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 +183,7 @@ def clear_respond_to
|
||||
# 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 +219,13 @@ def clear_respond_to
|
||||
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
|
||||
|
@ -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
|
@ -3,6 +3,7 @@
|
||||
require 'active_support/core_ext/object/blank'
|
||||
require 'active_support/key_generator'
|
||||
require 'active_support/message_verifier'
|
||||
require 'active_support/json'
|
||||
|
||||
module ActionDispatch
|
||||
class Request < Rack::Request
|
||||
@ -90,6 +91,7 @@ class Cookies
|
||||
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 +175,14 @@ def signed_or_encrypted
|
||||
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 +218,8 @@ def self.options_for_env(env) #:nodoc:
|
||||
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
|
||||
|
||||
@ -385,24 +392,11 @@ def []=(name, options)
|
||||
|
||||
class JsonSerializer
|
||||
def self.load(value)
|
||||
JSON.parse(value, quirks_mode: true)
|
||||
ActiveSupport::JSON.decode(value)
|
||||
end
|
||||
|
||||
def self.dump(value)
|
||||
JSON.generate(value, quirks_mode: true)
|
||||
end
|
||||
end
|
||||
|
||||
# Passing the NullSerializer downstream to the Message{Encryptor,Verifier}
|
||||
# allows us to handle the (de)serialization step within the cookie jar,
|
||||
# which gives us the opportunity to detect and migrate legacy cookies.
|
||||
class NullSerializer
|
||||
def self.load(value)
|
||||
value
|
||||
end
|
||||
|
||||
def self.dump(value)
|
||||
value
|
||||
ActiveSupport::JSON.encode(value)
|
||||
end
|
||||
end
|
||||
|
||||
@ -441,6 +435,10 @@ def serializer
|
||||
serializer
|
||||
end
|
||||
end
|
||||
|
||||
def digest
|
||||
@options[:digest] || 'SHA1'
|
||||
end
|
||||
end
|
||||
|
||||
class SignedCookieJar #:nodoc:
|
||||
@ -451,7 +449,7 @@ def initialize(parent_jar, key_generator, options = {})
|
||||
@parent_jar = parent_jar
|
||||
@options = options
|
||||
secret = key_generator.generate_key(@options[:signed_cookie_salt])
|
||||
@verifier = ActiveSupport::MessageVerifier.new(secret, serializer: NullSerializer)
|
||||
@verifier = ActiveSupport::MessageVerifier.new(secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
|
||||
end
|
||||
|
||||
def [](name)
|
||||
@ -508,7 +506,7 @@ def initialize(parent_jar, key_generator, options = {})
|
||||
@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)
|
||||
|
@ -162,7 +162,7 @@ def thread_locals
|
||||
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
|
||||
|
@ -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
|
32
actionpack/test/controller/mime/responders_test.rb
Normal file
32
actionpack/test/controller/mime/responders_test.rb
Normal file
@ -0,0 +1,32 @@
|
||||
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'
|
||||
assert_includes e.message, '~> 2.0'
|
||||
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'
|
||||
assert_includes e.message, '~> 2.0'
|
||||
end
|
||||
end
|
@ -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 @@ def test_array
|
||||
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
|
||||
|
@ -21,6 +21,16 @@ def self.dump(value)
|
||||
end
|
||||
end
|
||||
|
||||
class JSONWrapper
|
||||
def initialize(obj)
|
||||
@obj = obj
|
||||
end
|
||||
|
||||
def as_json(options = nil)
|
||||
"wrapped: #{@obj.as_json(options)}"
|
||||
end
|
||||
end
|
||||
|
||||
class TestController < ActionController::Base
|
||||
def authenticate
|
||||
cookies["user_name"] = "david"
|
||||
@ -85,6 +95,11 @@ def set_signed_cookie
|
||||
head :ok
|
||||
end
|
||||
|
||||
def set_wrapped_signed_cookie
|
||||
cookies.signed[:user_id] = JSONWrapper.new(45)
|
||||
head :ok
|
||||
end
|
||||
|
||||
def get_signed_cookie
|
||||
cookies.signed[:user_id]
|
||||
head :ok
|
||||
@ -95,6 +110,11 @@ def set_encrypted_cookie
|
||||
head :ok
|
||||
end
|
||||
|
||||
def set_wrapped_encrypted_cookie
|
||||
cookies.encrypted[:foo] = JSONWrapper.new('bar')
|
||||
head :ok
|
||||
end
|
||||
|
||||
def get_encrypted_cookie
|
||||
cookies.encrypted[:foo]
|
||||
head :ok
|
||||
@ -369,6 +389,35 @@ def test_read_permanent_cookie
|
||||
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
|
||||
@ -392,6 +441,14 @@ def test_signed_cookie_using_json_serializer
|
||||
assert_equal 45, cookies.signed[:user_id]
|
||||
end
|
||||
|
||||
def test_wrapped_signed_cookie_using_json_serializer
|
||||
@request.env["action_dispatch.cookies_serializer"] = :json
|
||||
get :set_wrapped_signed_cookie
|
||||
cookies = @controller.send :cookies
|
||||
assert_not_equal 'wrapped: 45', cookies[:user_id]
|
||||
assert_equal 'wrapped: 45', cookies.signed[:user_id]
|
||||
end
|
||||
|
||||
def test_signed_cookie_using_custom_serializer
|
||||
@request.env["action_dispatch.cookies_serializer"] = CustomSerializer
|
||||
get :set_signed_cookie
|
||||
@ -474,6 +531,17 @@ def test_encrypted_cookie_using_json_serializer
|
||||
assert_equal 'bar', cookies.encrypted[:foo]
|
||||
end
|
||||
|
||||
def test_wrapped_encrypted_cookie_using_json_serializer
|
||||
@request.env["action_dispatch.cookies_serializer"] = :json
|
||||
get :set_wrapped_encrypted_cookie
|
||||
cookies = @controller.send :cookies
|
||||
assert_not_equal 'wrapped: bar', cookies[:foo]
|
||||
assert_raises ::JSON::ParserError do
|
||||
cookies.signed[:foo]
|
||||
end
|
||||
assert_equal 'wrapped: bar', cookies.encrypted[:foo]
|
||||
end
|
||||
|
||||
def test_encrypted_cookie_using_custom_serializer
|
||||
@request.env["action_dispatch.cookies_serializer"] = CustomSerializer
|
||||
get :set_encrypted_cookie
|
||||
@ -481,6 +549,27 @@ def test_encrypted_cookie_using_custom_serializer
|
||||
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
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
Edit world!
|
@ -1 +0,0 @@
|
||||
New world!
|
@ -1 +0,0 @@
|
||||
<content>I should not be displayed</content>
|
@ -1 +0,0 @@
|
||||
<customer-name><%= @customer.name %></customer-name>
|
@ -1 +0,0 @@
|
||||
alert("Hi");
|
@ -1 +0,0 @@
|
||||
Hello world!
|
@ -8,8 +8,6 @@
|
||||
|
||||
class ControllerRuntimeLogSubscriberTest < ActionController::TestCase
|
||||
class LogSubscriberController < ActionController::Base
|
||||
respond_to :html
|
||||
|
||||
def show
|
||||
render :inline => "<%= Project.all %>"
|
||||
end
|
||||
@ -21,7 +19,7 @@ def zero
|
||||
def create
|
||||
ActiveRecord::LogSubscriber.runtime += 100
|
||||
project = Project.last
|
||||
respond_with(project, location: url_for(action: :show))
|
||||
redirect_to "/"
|
||||
end
|
||||
|
||||
def redirect
|
||||
|
@ -219,7 +219,7 @@ def attribute_will_change!(attr)
|
||||
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 @@ def reset_attribute!(attr)
|
||||
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
|
||||
|
@ -1,3 +1,16 @@
|
||||
* Fixed an issue where custom accessor methods (such as those generated by
|
||||
`enum`) with the same name as a global method are incorrectly overridden
|
||||
when subclassing.
|
||||
|
||||
Fixes #16288.
|
||||
|
||||
*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
|
||||
|
@ -103,7 +103,7 @@ def update_counter_in_memory(difference, reflection = reflection())
|
||||
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
|
||||
|
||||
|
@ -30,10 +30,14 @@ def initialize(name, value_before_type_cast, type)
|
||||
|
||||
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 @@ def with_value_from_database(value)
|
||||
self.class.from_database(name, value, type)
|
||||
end
|
||||
|
||||
def type_cast
|
||||
def type_cast(*)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
|
@ -57,6 +57,8 @@ def method_body(method_name, const_name)
|
||||
end
|
||||
end
|
||||
|
||||
class GeneratedAttributeMethods < Module; end # :nodoc:
|
||||
|
||||
module ClassMethods
|
||||
def inherited(child_class) #:nodoc:
|
||||
child_class.initialize_generated_modules
|
||||
@ -64,7 +66,7 @@ def inherited(child_class) #:nodoc:
|
||||
end
|
||||
|
||||
def initialize_generated_modules # :nodoc:
|
||||
@generated_attribute_methods = Module.new { extend Mutex_m }
|
||||
@generated_attribute_methods = GeneratedAttributeMethods.new { extend Mutex_m }
|
||||
@attribute_methods_generated = false
|
||||
include @generated_attribute_methods
|
||||
end
|
||||
@ -113,10 +115,11 @@ def instance_method_already_implemented?(method_name)
|
||||
if superclass == Base
|
||||
super
|
||||
else
|
||||
# If B < A and A defines its own attribute method, then we don't want to overwrite that.
|
||||
defined = method_defined_within?(method_name, superclass, superclass.generated_attribute_methods)
|
||||
base_defined = Base.method_defined?(method_name) || Base.private_method_defined?(method_name)
|
||||
defined && !base_defined || super
|
||||
# If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass
|
||||
# defines its own attribute method, then we don't want to overwrite that.
|
||||
defined = method_defined_within?(method_name, superclass, Base) &&
|
||||
! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods)
|
||||
defined || super
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -51,14 +51,6 @@ def changed
|
||||
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 @@ def clear_changes_information
|
||||
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 @@ def raw_write_attribute(attr, value)
|
||||
|
||||
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 @@ def _field_changed?(attr, old_value)
|
||||
@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)
|
||||
|
@ -145,11 +145,11 @@ def save_changed_attribute(attr_name, old)
|
||||
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
|
||||
|
@ -466,7 +466,7 @@ def touch(*names)
|
||||
|
||||
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
|
||||
|
@ -114,7 +114,7 @@ def current_time_from_proper_timezone
|
||||
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
|
||||
|
@ -810,6 +810,24 @@ def title=(val); self.author_name = val; end
|
||||
assert_equal "lol", topic.author_name
|
||||
end
|
||||
|
||||
def test_inherited_custom_accessors_with_reserved_names
|
||||
klass = Class.new(ActiveRecord::Base) do
|
||||
self.table_name = 'computers'
|
||||
self.abstract_class = true
|
||||
def system; "omg"; end
|
||||
def system=(val); self.developer = val; end
|
||||
end
|
||||
|
||||
subklass = Class.new(klass)
|
||||
[klass, subklass].each(&:define_attribute_methods)
|
||||
|
||||
computer = subklass.find(1)
|
||||
assert_equal "omg", computer.system
|
||||
|
||||
computer.developer = 99
|
||||
assert_equal 99, computer.developer
|
||||
end
|
||||
|
||||
def test_on_the_fly_super_invokable_generated_attribute_methods_via_method_missing
|
||||
klass = new_topic_like_ar_class do
|
||||
def title
|
||||
|
@ -661,6 +661,27 @@ def type_cast_for_database(value)
|
||||
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?
|
||||
|
@ -303,8 +303,8 @@ def constantize(camel_cased_word)
|
||||
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
|
||||
|
@ -40,6 +40,7 @@ class InvalidMessage < StandardError; end
|
||||
# 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 @@ def initialize(secret, *signature_key_or_options)
|
||||
@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
|
||||
|
||||
|
@ -336,7 +336,7 @@ def load
|
||||
begin
|
||||
@codepoints, @composition_exclusion, @composition_map, @boundary, @cp1252 = File.open(self.class.filename, 'rb') { |f| Marshal.load f.read }
|
||||
rescue => e
|
||||
raise IOError.new("Couldn't load the Unicode tables for UTF8Handler (#{e.message}), ActiveSupport::Multibyte is unusable")
|
||||
raise IOError.new("Couldn't load the Unicode tables for UTF8Handler (#{e.message}), ActiveSupport::Multibyte is unusable")
|
||||
end
|
||||
|
||||
# Redefine the === method so we can write shorter rules for grapheme cluster breaks
|
||||
@ -368,6 +368,7 @@ def self.filename
|
||||
private
|
||||
|
||||
def apply_mapping(string, mapping) #:nodoc:
|
||||
database.codepoints
|
||||
string.each_codepoint.map do |codepoint|
|
||||
cp = database.codepoints[codepoint]
|
||||
if cp and (ncp = cp.send(mapping)) and ncp > 0
|
||||
@ -385,7 +386,6 @@ def recode_windows1252_chars(string)
|
||||
def database
|
||||
@database ||= UnicodeDatabase.new
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -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
|
||||
```
|
||||
|
@ -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
|
||||
|
||||
|
@ -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`
|
||||
|
@ -256,7 +256,8 @@ def env_config
|
||||
"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
|
||||
|
Loading…
Reference in New Issue
Block a user