mirror of
https://github.com/github/rails.git
synced 2026-04-26 03:00:59 -04:00
Merge branch 'master' of git://github.com/rails/rails into rails
This commit is contained in:
@@ -23,8 +23,7 @@ module ActionMailer #:nodoc:
|
||||
# bcc ["bcc@example.com", "Order Watcher <watcher@example.com>"]
|
||||
# from "system@example.com"
|
||||
# subject "New account information"
|
||||
#
|
||||
# @account = recipient
|
||||
# body :account => recipient
|
||||
# end
|
||||
# end
|
||||
#
|
||||
@@ -36,7 +35,7 @@ module ActionMailer #:nodoc:
|
||||
# * <tt>cc</tt> - Takes one or more email addresses. These addresses will receive a carbon copy of your email. Sets the <tt>Cc:</tt> header.
|
||||
# * <tt>bcc</tt> - Takes one or more email addresses. These addresses will receive a blind carbon copy of your email. Sets the <tt>Bcc:</tt> header.
|
||||
# * <tt>reply_to</tt> - Takes one or more email addresses. These addresses will be listed as the default recipients when replying to your email. Sets the <tt>Reply-To:</tt> header.
|
||||
# * <tt>sent_on</tt> - The date on which the message was sent. If not set, the header wil be set by the delivery agent.
|
||||
# * <tt>sent_on</tt> - The date on which the message was sent. If not set, the header will be set by the delivery agent.
|
||||
# * <tt>content_type</tt> - Specify the content type of the message. Defaults to <tt>text/plain</tt>.
|
||||
# * <tt>headers</tt> - Specify additional headers to be set for the message, e.g. <tt>headers 'X-Mail-Count' => 107370</tt>.
|
||||
#
|
||||
@@ -144,12 +143,13 @@ module ActionMailer #:nodoc:
|
||||
# subject "New account information"
|
||||
# from "system@example.com"
|
||||
# content_type "multipart/alternative"
|
||||
# body :account => recipient
|
||||
#
|
||||
# part :content_type => "text/html",
|
||||
# :body => render_message("signup-as-html", :account => recipient)
|
||||
# :data => render_message("signup-as-html")
|
||||
#
|
||||
# part "text/plain" do |p|
|
||||
# p.body = render_message("signup-as-plain", :account => recipient)
|
||||
# p.body = render_message("signup-as-plain")
|
||||
# p.content_transfer_encoding = "base64"
|
||||
# end
|
||||
# end
|
||||
@@ -492,11 +492,13 @@ module ActionMailer #:nodoc:
|
||||
# body, headers, etc.) can be set on it.
|
||||
def part(params)
|
||||
params = {:content_type => params} if String === params
|
||||
|
||||
if custom_headers = params.delete(:headers)
|
||||
ActiveSupport::Deprecation.warn('Passing custom headers with :headers => {} is deprecated. ' <<
|
||||
'Please just pass in custom headers directly.', caller[0,10])
|
||||
params.merge!(custom_headers)
|
||||
end
|
||||
|
||||
part = Mail::Part.new(params)
|
||||
yield part if block_given?
|
||||
@parts << part
|
||||
@@ -514,6 +516,20 @@ module ActionMailer #:nodoc:
|
||||
part(params, &block)
|
||||
end
|
||||
|
||||
# Allow you to set assigns for your template:
|
||||
#
|
||||
# body :greetings => "Hi"
|
||||
#
|
||||
# Will make @greetings available in the template to be rendered.
|
||||
def body(object=nil)
|
||||
returning(super) do # Run deprecation hooks
|
||||
if object.is_a?(Hash)
|
||||
@assigns_set = true
|
||||
object.each { |k, v| instance_variable_set(:"@#{k}", v) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Instantiate a new mailer object. If +method_name+ is not +nil+, the mailer
|
||||
# will be initialized according to the named method. If not, the mailer will
|
||||
# remain uninitialized (useful when you only need to invoke the "receive"
|
||||
@@ -549,6 +565,23 @@ module ActionMailer #:nodoc:
|
||||
|
||||
private
|
||||
|
||||
# Render a message but does not set it as mail body. Useful for rendering
|
||||
# data for part and attachments.
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
# render_message "special_message"
|
||||
# render_message :template => "special_message"
|
||||
# render_message :inline => "<%= 'Hi!' %>"
|
||||
def render_message(object)
|
||||
case object
|
||||
when String
|
||||
render_to_body(:template => object)
|
||||
else
|
||||
render_to_body(object)
|
||||
end
|
||||
end
|
||||
|
||||
# Set up the default values for the various instance variables of this
|
||||
# mailer. Subclasses may override this method to provide different
|
||||
# defaults.
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
module ActionMailer
|
||||
# TODO Remove this module all together in a next release. Ensure that super
|
||||
# hooks in ActionMailer::Base are removed as well.
|
||||
# hooks and @assigns_set in ActionMailer::Base are removed as well.
|
||||
module DeprecatedBody
|
||||
def self.included(base)
|
||||
base.class_eval do
|
||||
# Define the body of the message. This is either a Hash (in which case it
|
||||
# specifies the variables to pass to the template when it is rendered),
|
||||
# or a string, in which case it specifies the actual text of the message.
|
||||
adv_attr_accessor :body
|
||||
end
|
||||
end
|
||||
extend ActionMailer::AdvAttrAccessor
|
||||
|
||||
# Define the body of the message. This is either a Hash (in which case it
|
||||
# specifies the variables to pass to the template when it is rendered),
|
||||
# or a string, in which case it specifies the actual text of the message.
|
||||
adv_attr_accessor :body
|
||||
|
||||
def initialize_defaults(method_name)
|
||||
@body ||= {}
|
||||
@@ -18,32 +16,28 @@ module ActionMailer
|
||||
def attachment(params, &block)
|
||||
if params[:body]
|
||||
ActiveSupport::Deprecation.warn('attachment :body => "string" is deprecated. To set the body of an attachment ' <<
|
||||
'please use :data instead, like attachment :data => "string".', caller[0,10])
|
||||
'please use :data instead, like attachment :data => "string"', caller[0,10])
|
||||
params[:data] = params.delete(:body)
|
||||
end
|
||||
end
|
||||
|
||||
def create_parts
|
||||
if String === @body
|
||||
ActiveSupport::Deprecation.warn('body is deprecated. To set the body with a text ' <<
|
||||
'call render(:text => "body").', caller[0,10])
|
||||
if String === @body && !defined?(@assigns_set)
|
||||
ActiveSupport::Deprecation.warn('body(String) is deprecated. To set the body with a text ' <<
|
||||
'call render(:text => "body")', caller[0,10])
|
||||
self.response_body = @body
|
||||
elsif @body.is_a?(Hash) && !@body.empty?
|
||||
ActiveSupport::Deprecation.warn('body is deprecated. To set assigns simply ' <<
|
||||
'use instance variables', caller[0,10])
|
||||
@body.each { |k, v| instance_variable_set(:"@#{k}", v) }
|
||||
elsif self.response_body
|
||||
@body = self.response_body
|
||||
end
|
||||
end
|
||||
|
||||
def render(*args)
|
||||
options = args.last.is_a?(Hash) ? args.last : {}
|
||||
if options[:body]
|
||||
ActiveSupport::Deprecation.warn(':body is deprecated. To set assigns simply ' <<
|
||||
'use instance variables', caller[0,1])
|
||||
ActiveSupport::Deprecation.warn(':body in render deprecated. Please call body ' <<
|
||||
'with a hash instead', caller[0,1])
|
||||
|
||||
options.delete(:body).each do |k, v|
|
||||
instance_variable_set(:"@#{k}", v)
|
||||
end
|
||||
body options.delete(:body)
|
||||
end
|
||||
|
||||
super
|
||||
|
||||
@@ -120,11 +120,11 @@ class TestMailer < ActionMailer::Base
|
||||
content_type "multipart/alternative"
|
||||
|
||||
part "text/plain" do |p|
|
||||
p.body = "blah"
|
||||
p.body = render_message(:text => "blah")
|
||||
end
|
||||
|
||||
part "text/html" do |p|
|
||||
p.body = "<b>blah</b>"
|
||||
p.body = render_message(:inline => "<%= content_tag(:b, 'blah') %>")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -301,7 +301,6 @@ class TestMailer < ActionMailer::Base
|
||||
render :text => "testing"
|
||||
end
|
||||
|
||||
# This tests body calls accepeting a hash, which is deprecated.
|
||||
def body_ivar(recipient)
|
||||
recipients recipient
|
||||
subject "Body as a local variable"
|
||||
@@ -1075,7 +1074,7 @@ EOF
|
||||
|
||||
def test_return_path_with_create
|
||||
mail = TestMailer.create_return_path
|
||||
assert_equal "another@somewhere.test", mail.return_path
|
||||
assert_equal ["another@somewhere.test"], mail.return_path
|
||||
end
|
||||
|
||||
def test_return_path_with_deliver
|
||||
@@ -1086,8 +1085,7 @@ EOF
|
||||
end
|
||||
|
||||
def test_body_is_stored_as_an_ivar
|
||||
mail = nil
|
||||
ActiveSupport::Deprecation.silence { mail = TestMailer.create_body_ivar(@recipient) }
|
||||
mail = TestMailer.create_body_ivar(@recipient)
|
||||
assert_equal "body: foo\nbar: baz", mail.body.to_s
|
||||
end
|
||||
|
||||
|
||||
@@ -62,15 +62,19 @@ module AbstractController
|
||||
# Array[String]:: A list of all methods that should be considered
|
||||
# actions.
|
||||
def action_methods
|
||||
@action_methods ||=
|
||||
@action_methods ||= begin
|
||||
# All public instance methods of this class, including ancestors
|
||||
public_instance_methods(true).map { |m| m.to_s }.to_set -
|
||||
# Except for public instance methods of Base and its ancestors
|
||||
internal_methods.map { |m| m.to_s } +
|
||||
# Be sure to include shadowed public instance methods of this class
|
||||
public_instance_methods(false).map { |m| m.to_s } -
|
||||
# And always exclude explicitly hidden actions
|
||||
hidden_actions
|
||||
methods = public_instance_methods(true).map { |m| m.to_s }.to_set -
|
||||
# Except for public instance methods of Base and its ancestors
|
||||
internal_methods.map { |m| m.to_s } +
|
||||
# Be sure to include shadowed public instance methods of this class
|
||||
public_instance_methods(false).map { |m| m.to_s } -
|
||||
# And always exclude explicitly hidden actions
|
||||
hidden_actions
|
||||
|
||||
# Clear out AS callback method pollution
|
||||
methods.reject { |method| method =~ /_one_time_conditions/ }
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the full controller name, underscored, without the ending Controller.
|
||||
@@ -116,68 +120,72 @@ module AbstractController
|
||||
self.class.controller_path
|
||||
end
|
||||
|
||||
private
|
||||
# Returns true if the name can be considered an action. This can
|
||||
# be overridden in subclasses to modify the semantics of what
|
||||
# can be considered an action.
|
||||
#
|
||||
# ==== Parameters
|
||||
# name<String>:: The name of an action to be tested
|
||||
#
|
||||
# ==== Returns
|
||||
# TrueClass, FalseClass
|
||||
def action_method?(name)
|
||||
self.class.action_methods.include?(name)
|
||||
def action_methods
|
||||
self.class.action_methods
|
||||
end
|
||||
|
||||
# Call the action. Override this in a subclass to modify the
|
||||
# behavior around processing an action. This, and not #process,
|
||||
# is the intended way to override action dispatching.
|
||||
def process_action(method_name, *args)
|
||||
send_action(method_name, *args)
|
||||
end
|
||||
|
||||
# Actually call the method associated with the action. Override
|
||||
# this method if you wish to change how action methods are called,
|
||||
# not to add additional behavior around it. For example, you would
|
||||
# override #send_action if you want to inject arguments into the
|
||||
# method.
|
||||
alias send_action send
|
||||
|
||||
# If the action name was not found, but a method called "action_missing"
|
||||
# was found, #method_for_action will return "_handle_action_missing".
|
||||
# This method calls #action_missing with the current action name.
|
||||
def _handle_action_missing
|
||||
action_missing(@_action_name)
|
||||
end
|
||||
|
||||
# Takes an action name and returns the name of the method that will
|
||||
# handle the action. In normal cases, this method returns the same
|
||||
# name as it receives. By default, if #method_for_action receives
|
||||
# a name that is not an action, it will look for an #action_missing
|
||||
# method and return "_handle_action_missing" if one is found.
|
||||
#
|
||||
# Subclasses may override this method to add additional conditions
|
||||
# that should be considered an action. For instance, an HTTP controller
|
||||
# with a template matching the action name is considered to exist.
|
||||
#
|
||||
# If you override this method to handle additional cases, you may
|
||||
# also provide a method (like _handle_method_missing) to handle
|
||||
# the case.
|
||||
#
|
||||
# If none of these conditions are true, and method_for_action
|
||||
# returns nil, an ActionNotFound exception will be raised.
|
||||
#
|
||||
# ==== Parameters
|
||||
# action_name<String>:: An action name to find a method name for
|
||||
#
|
||||
# ==== Returns
|
||||
# String:: The name of the method that handles the action
|
||||
# nil:: No method name could be found. Raise ActionNotFound.
|
||||
def method_for_action(action_name)
|
||||
if action_method?(action_name) then action_name
|
||||
elsif respond_to?(:action_missing, true) then "_handle_action_missing"
|
||||
private
|
||||
# Returns true if the name can be considered an action. This can
|
||||
# be overridden in subclasses to modify the semantics of what
|
||||
# can be considered an action.
|
||||
#
|
||||
# ==== Parameters
|
||||
# name<String>:: The name of an action to be tested
|
||||
#
|
||||
# ==== Returns
|
||||
# TrueClass, FalseClass
|
||||
def action_method?(name)
|
||||
self.class.action_methods.include?(name)
|
||||
end
|
||||
|
||||
# Call the action. Override this in a subclass to modify the
|
||||
# behavior around processing an action. This, and not #process,
|
||||
# is the intended way to override action dispatching.
|
||||
def process_action(method_name, *args)
|
||||
send_action(method_name, *args)
|
||||
end
|
||||
|
||||
# Actually call the method associated with the action. Override
|
||||
# this method if you wish to change how action methods are called,
|
||||
# not to add additional behavior around it. For example, you would
|
||||
# override #send_action if you want to inject arguments into the
|
||||
# method.
|
||||
alias send_action send
|
||||
|
||||
# If the action name was not found, but a method called "action_missing"
|
||||
# was found, #method_for_action will return "_handle_action_missing".
|
||||
# This method calls #action_missing with the current action name.
|
||||
def _handle_action_missing
|
||||
action_missing(@_action_name)
|
||||
end
|
||||
|
||||
# Takes an action name and returns the name of the method that will
|
||||
# handle the action. In normal cases, this method returns the same
|
||||
# name as it receives. By default, if #method_for_action receives
|
||||
# a name that is not an action, it will look for an #action_missing
|
||||
# method and return "_handle_action_missing" if one is found.
|
||||
#
|
||||
# Subclasses may override this method to add additional conditions
|
||||
# that should be considered an action. For instance, an HTTP controller
|
||||
# with a template matching the action name is considered to exist.
|
||||
#
|
||||
# If you override this method to handle additional cases, you may
|
||||
# also provide a method (like _handle_method_missing) to handle
|
||||
# the case.
|
||||
#
|
||||
# If none of these conditions are true, and method_for_action
|
||||
# returns nil, an ActionNotFound exception will be raised.
|
||||
#
|
||||
# ==== Parameters
|
||||
# action_name<String>:: An action name to find a method name for
|
||||
#
|
||||
# ==== Returns
|
||||
# String:: The name of the method that handles the action
|
||||
# nil:: No method name could be found. Raise ActionNotFound.
|
||||
def method_for_action(action_name)
|
||||
if action_method?(action_name) then action_name
|
||||
elsif respond_to?(:action_missing, true) then "_handle_action_missing"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -42,7 +42,7 @@ module AbstractController
|
||||
# Delegates render_to_body and sticks the result in self.response_body.
|
||||
def render(*args)
|
||||
if response_body
|
||||
raise AbstractController::DoubleRenderError, "OMG"
|
||||
raise AbstractController::DoubleRenderError, "Can only render or redirect once per action"
|
||||
end
|
||||
|
||||
self.response_body = render_to_body(*args)
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
module ActionController
|
||||
class Dispatcher
|
||||
cattr_accessor :prepare_each_request
|
||||
self.prepare_each_request = false
|
||||
|
||||
class << self
|
||||
def before_dispatch(*args, &block)
|
||||
ActiveSupport::Deprecation.warn "ActionController::Dispatcher.before_dispatch is deprecated. " <<
|
||||
@@ -18,7 +15,7 @@ module ActionController
|
||||
|
||||
def to_prepare(*args, &block)
|
||||
ActiveSupport::Deprecation.warn "ActionController::Dispatcher.to_prepare is deprecated. " <<
|
||||
"Please use ActionDispatch::Callbacks.to_prepare instead.", caller
|
||||
"Please use config.to_prepare instead", caller
|
||||
ActionDispatch::Callbacks.after(*args, &block)
|
||||
end
|
||||
|
||||
|
||||
@@ -1,48 +1,4 @@
|
||||
module ActionController #:nodoc:
|
||||
# Cookies are read and written through ActionController#cookies.
|
||||
#
|
||||
# The cookies being read are the ones received along with the request, the cookies
|
||||
# being written will be sent out with the response. Reading a cookie does not get
|
||||
# the cookie object itself back, just the value it holds.
|
||||
#
|
||||
# Examples for writing:
|
||||
#
|
||||
# # Sets a simple session cookie.
|
||||
# cookies[:user_name] = "david"
|
||||
#
|
||||
# # Sets a cookie that expires in 1 hour.
|
||||
# cookies[:login] = { :value => "XJ-122", :expires => 1.hour.from_now }
|
||||
#
|
||||
# Examples for reading:
|
||||
#
|
||||
# cookies[:user_name] # => "david"
|
||||
# cookies.size # => 2
|
||||
#
|
||||
# Example for deleting:
|
||||
#
|
||||
# cookies.delete :user_name
|
||||
#
|
||||
# Please note that if you specify a :domain when setting a cookie, you must also specify the domain when deleting the cookie:
|
||||
#
|
||||
# cookies[:key] = {
|
||||
# :value => 'a yummy cookie',
|
||||
# :expires => 1.year.from_now,
|
||||
# :domain => 'domain.com'
|
||||
# }
|
||||
#
|
||||
# cookies.delete(:key, :domain => 'domain.com')
|
||||
#
|
||||
# The option symbols for setting cookies are:
|
||||
#
|
||||
# * <tt>:value</tt> - The cookie's value or list of values (as an array).
|
||||
# * <tt>:path</tt> - The path for which this cookie applies. Defaults to the root
|
||||
# of the application.
|
||||
# * <tt>:domain</tt> - The domain for which this cookie applies.
|
||||
# * <tt>:expires</tt> - The time at which this cookie expires, as a Time object.
|
||||
# * <tt>:secure</tt> - Whether this cookie is a only transmitted to HTTPS servers.
|
||||
# Default is +false+.
|
||||
# * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or
|
||||
# only HTTP. Defaults to +false+.
|
||||
module Cookies
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
@@ -52,146 +8,10 @@ module ActionController #:nodoc:
|
||||
helper_method :cookies
|
||||
cattr_accessor :cookie_verifier_secret
|
||||
end
|
||||
|
||||
protected
|
||||
# Returns the cookie container, which operates as described above.
|
||||
|
||||
private
|
||||
def cookies
|
||||
@cookies ||= CookieJar.build(request, response)
|
||||
request.cookie_jar
|
||||
end
|
||||
end
|
||||
|
||||
class CookieJar < Hash #:nodoc:
|
||||
def self.build(request, response)
|
||||
new.tap do |hash|
|
||||
hash.update(request.cookies)
|
||||
hash.response = response
|
||||
end
|
||||
end
|
||||
|
||||
attr_accessor :response
|
||||
|
||||
# Returns the value of the cookie by +name+, or +nil+ if no such cookie exists.
|
||||
def [](name)
|
||||
super(name.to_s)
|
||||
end
|
||||
|
||||
# Sets the cookie named +name+. The second argument may be the very cookie
|
||||
# value, or a hash of options as documented above.
|
||||
def []=(key, options)
|
||||
if options.is_a?(Hash)
|
||||
options.symbolize_keys!
|
||||
value = options[:value]
|
||||
else
|
||||
value = options
|
||||
options = { :value => value }
|
||||
end
|
||||
|
||||
super(key.to_s, value)
|
||||
|
||||
options[:path] ||= "/"
|
||||
response.set_cookie(key, options)
|
||||
end
|
||||
|
||||
# Removes the cookie on the client machine by setting the value to an empty string
|
||||
# and setting its expiration date into the past. Like <tt>[]=</tt>, you can pass in
|
||||
# an options hash to delete cookies with extra data such as a <tt>:path</tt>.
|
||||
def delete(key, options = {})
|
||||
options.symbolize_keys!
|
||||
options[:path] ||= "/"
|
||||
value = super(key.to_s)
|
||||
response.delete_cookie(key, options)
|
||||
value
|
||||
end
|
||||
|
||||
# Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example:
|
||||
#
|
||||
# cookies.permanent[:prefers_open_id] = true
|
||||
# # => Set-Cookie: prefers_open_id=true; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
|
||||
#
|
||||
# This jar is only meant for writing. You'll read permanent cookies through the regular accessor.
|
||||
#
|
||||
# This jar allows chaining with the signed jar as well, so you can set permanent, signed cookies. Examples:
|
||||
#
|
||||
# cookies.permanent.signed[:remember_me] = current_user.id
|
||||
# # => Set-Cookie: discount=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
|
||||
def permanent
|
||||
@permanent ||= PermanentCookieJar.new(self)
|
||||
end
|
||||
|
||||
# Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from
|
||||
# the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed
|
||||
# cookie was tampered with by the user (or a 3rd party), an ActiveSupport::MessageVerifier::InvalidSignature exception will
|
||||
# be raised.
|
||||
#
|
||||
# This jar requires that you set a suitable secret for the verification on ActionController::Base.cookie_verifier_secret.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# cookies.signed[:discount] = 45
|
||||
# # => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/
|
||||
#
|
||||
# cookies.signed[:discount] # => 45
|
||||
def signed
|
||||
@signed ||= SignedCookieJar.new(self)
|
||||
end
|
||||
end
|
||||
|
||||
class PermanentCookieJar < CookieJar #:nodoc:
|
||||
def initialize(parent_jar)
|
||||
@parent_jar = parent_jar
|
||||
end
|
||||
|
||||
def []=(key, options)
|
||||
if options.is_a?(Hash)
|
||||
options.symbolize_keys!
|
||||
else
|
||||
options = { :value => options }
|
||||
end
|
||||
|
||||
options[:expires] = 20.years.from_now
|
||||
@parent_jar[key] = options
|
||||
end
|
||||
|
||||
def signed
|
||||
@signed ||= SignedCookieJar.new(self)
|
||||
end
|
||||
|
||||
def controller
|
||||
@parent_jar.controller
|
||||
end
|
||||
|
||||
def method_missing(method, *arguments, &block)
|
||||
@parent_jar.send(method, *arguments, &block)
|
||||
end
|
||||
end
|
||||
|
||||
class SignedCookieJar < CookieJar #:nodoc:
|
||||
def initialize(parent_jar)
|
||||
unless ActionController::Base.cookie_verifier_secret
|
||||
raise "You must set ActionController::Base.cookie_verifier_secret to use signed cookies"
|
||||
end
|
||||
|
||||
@parent_jar = parent_jar
|
||||
@verifier = ActiveSupport::MessageVerifier.new(ActionController::Base.cookie_verifier_secret)
|
||||
end
|
||||
|
||||
def [](name)
|
||||
@verifier.verify(@parent_jar[name])
|
||||
end
|
||||
|
||||
def []=(key, options)
|
||||
if options.is_a?(Hash)
|
||||
options.symbolize_keys!
|
||||
options[:value] = @verifier.generate(options[:value])
|
||||
else
|
||||
options = { :value => @verifier.generate(options) }
|
||||
end
|
||||
|
||||
@parent_jar[key] = options
|
||||
end
|
||||
|
||||
def method_missing(method, *arguments, &block)
|
||||
@parent_jar.send(method, *arguments, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -20,11 +20,7 @@ module ActionController
|
||||
result = super
|
||||
payload[:controller] = self.class.name
|
||||
payload[:action] = self.action_name
|
||||
payload[:formats] = request.formats.map(&:to_s)
|
||||
payload[:remote_ip] = request.remote_ip
|
||||
payload[:method] = request.method
|
||||
payload[:status] = response.status
|
||||
payload[:request_uri] = request.request_uri rescue "unknown"
|
||||
append_info_to_payload(payload)
|
||||
result
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module ActionController #:nodoc:
|
||||
# Responder is responsible to expose a resource for different mime requests,
|
||||
# Responder is responsible for exposing a resource to different mime requests,
|
||||
# usually depending on the HTTP verb. The responder is triggered when
|
||||
# respond_with is called. The simplest case to study is a GET request:
|
||||
# <code>respond_with</code> is called. The simplest case to study is a GET request:
|
||||
#
|
||||
# class PeopleController < ApplicationController
|
||||
# respond_to :html, :xml, :json
|
||||
@@ -12,17 +12,17 @@ module ActionController #:nodoc:
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# When a request comes, for example with format :xml, three steps happen:
|
||||
# When a request comes in, for example for an XML response, three steps happen:
|
||||
#
|
||||
# 1) responder searches for a template at people/index.xml;
|
||||
# 1) the responder searches for a template at people/index.xml;
|
||||
#
|
||||
# 2) if the template is not available, it will invoke :to_xml in the given resource;
|
||||
# 2) if the template is not available, it will invoke <code>#to_xml</code> on the given resource;
|
||||
#
|
||||
# 3) if the responder does not respond_to :to_xml, call :to_format on it.
|
||||
# 3) if the responder does not <code>respond_to :to_xml</code>, call <code>#to_format</code> on it.
|
||||
#
|
||||
# === Builtin HTTP verb semantics
|
||||
#
|
||||
# Rails default responder holds semantics for each HTTP verb. Depending on the
|
||||
# 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
|
||||
@@ -55,7 +55,7 @@ module ActionController #:nodoc:
|
||||
#
|
||||
# === Nested resources
|
||||
#
|
||||
# You can given nested resource as you do in form_for and polymorphic_url.
|
||||
# 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:
|
||||
#
|
||||
@@ -67,15 +67,15 @@ module ActionController #:nodoc:
|
||||
# end
|
||||
#
|
||||
# Giving an array of resources, you ensure that the responder will redirect to
|
||||
# project_task_url instead of task_url.
|
||||
# <code>project_task_url</code> instead of <code>task_url</code>.
|
||||
#
|
||||
# Namespaced and singleton resources requires a symbol to be given, as in
|
||||
# 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)
|
||||
#
|
||||
# Check polymorphic_url documentation for more examples.
|
||||
# Check <code>polymorphic_url</code> documentation for more examples.
|
||||
#
|
||||
class Responder
|
||||
attr_reader :controller, :request, :format, :resource, :resources, :options
|
||||
@@ -126,7 +126,7 @@ module ActionController #:nodoc:
|
||||
navigation_behavior(e)
|
||||
end
|
||||
|
||||
# All others formats follow the procedure below. First we try to render a
|
||||
# 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.
|
||||
#
|
||||
@@ -183,11 +183,11 @@ module ActionController #:nodoc:
|
||||
@default_response.call
|
||||
end
|
||||
|
||||
# display is just a shortcut to render a resource with the current format.
|
||||
# Display is just a shortcut to render a resource with the current format.
|
||||
#
|
||||
# display @user, :status => :ok
|
||||
#
|
||||
# For xml request is equivalent to:
|
||||
# For XML requests it's equivalent to:
|
||||
#
|
||||
# render :xml => @user, :status => :ok
|
||||
#
|
||||
@@ -204,14 +204,14 @@ module ActionController #:nodoc:
|
||||
controller.render given_options.merge!(options).merge!(format => resource)
|
||||
end
|
||||
|
||||
# Check if the resource has errors or not.
|
||||
# Check whether the resource has errors.
|
||||
#
|
||||
def has_errors?
|
||||
resource.respond_to?(:errors) && !resource.errors.empty?
|
||||
end
|
||||
|
||||
# By default, render the :edit action for html requests with failure, unless
|
||||
# the verb is post.
|
||||
# By default, render the <code>:edit</code> action for HTML requests with failure, unless
|
||||
# the verb is POST.
|
||||
#
|
||||
def default_action
|
||||
@action ||= ACTIONS_FOR_VERBS[request.method]
|
||||
|
||||
@@ -10,6 +10,9 @@ module ActionController
|
||||
@_response = response
|
||||
@_response.request = request
|
||||
ret = process(request.parameters[:action])
|
||||
if cookies = @_request.env['action_dispatch.cookies']
|
||||
cookies.write(@_response)
|
||||
end
|
||||
@_response.body ||= self.response_body
|
||||
@_response.prepare!
|
||||
set_test_assigns
|
||||
|
||||
@@ -23,7 +23,6 @@ module ActionController
|
||||
initializer "action_controller.initialize_routing" do |app|
|
||||
app.route_configuration_files << app.config.routes_configuration_file
|
||||
app.route_configuration_files << app.config.builtin_routes_configuration_file
|
||||
app.reload_routes!
|
||||
end
|
||||
|
||||
initializer "action_controller.initialize_framework_caches" do
|
||||
@@ -40,54 +39,5 @@ module ActionController
|
||||
ActionController::Base.view_paths = view_path if ActionController::Base.view_paths.blank?
|
||||
end
|
||||
|
||||
class MetalMiddlewareBuilder
|
||||
def initialize(metals)
|
||||
@metals = metals
|
||||
end
|
||||
|
||||
def new(app)
|
||||
ActionDispatch::Cascade.new(@metals, app)
|
||||
end
|
||||
|
||||
def name
|
||||
ActionDispatch::Cascade.name
|
||||
end
|
||||
alias_method :to_s, :name
|
||||
end
|
||||
|
||||
initializer "action_controller.initialize_metal" do |app|
|
||||
metal_root = "#{Rails.root}/app/metal"
|
||||
load_list = app.config.metals || Dir["#{metal_root}/**/*.rb"]
|
||||
|
||||
metals = load_list.map { |metal|
|
||||
metal = File.basename(metal.gsub("#{metal_root}/", ''), '.rb')
|
||||
require_dependency metal
|
||||
metal.camelize.constantize
|
||||
}.compact
|
||||
|
||||
middleware = MetalMiddlewareBuilder.new(metals)
|
||||
app.config.middleware.insert_before(:"ActionDispatch::ParamsParser", middleware)
|
||||
end
|
||||
|
||||
# Prepare dispatcher callbacks and run 'prepare' callbacks
|
||||
initializer "action_controller.prepare_dispatcher" do |app|
|
||||
# TODO: This used to say unless defined?(Dispatcher). Find out why and fix.
|
||||
# Notice that at this point, ActionDispatch::Callbacks were already loaded.
|
||||
require 'rails/dispatcher'
|
||||
ActionController::Dispatcher.prepare_each_request = true unless app.config.cache_classes
|
||||
|
||||
unless app.config.cache_classes
|
||||
# Setup dev mode route reloading
|
||||
routes_last_modified = app.routes_changed_at
|
||||
reload_routes = lambda do
|
||||
unless app.routes_changed_at == routes_last_modified
|
||||
routes_last_modified = app.routes_changed_at
|
||||
app.reload_routes!
|
||||
end
|
||||
end
|
||||
ActionDispatch::Callbacks.before { |callbacks| reload_routes.call }
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,18 +3,13 @@ module ActionController
|
||||
class Subscriber < Rails::Subscriber
|
||||
def process_action(event)
|
||||
payload = event.payload
|
||||
|
||||
info "\nProcessed #{payload[:controller]}##{payload[:action]} " \
|
||||
"to #{payload[:formats].join(', ')} (for #{payload[:remote_ip]} at #{event.time.to_s(:db)}) " \
|
||||
"[#{payload[:method].to_s.upcase}]"
|
||||
|
||||
info " Parameters: #{payload[:params].inspect}" unless payload[:params].blank?
|
||||
|
||||
additions = ActionController::Base.log_process_action(payload)
|
||||
|
||||
message = "Completed in %.0fms" % event.duration
|
||||
message << " (#{additions.join(" | ")})" unless additions.blank?
|
||||
message << " | #{payload[:status]} [#{payload[:request_uri]}]\n\n"
|
||||
message << " by #{payload[:controller]}##{payload[:action]} [#{payload[:status]}]"
|
||||
|
||||
info(message)
|
||||
end
|
||||
|
||||
@@ -43,8 +43,10 @@ module ActionDispatch
|
||||
autoload_under 'middleware' do
|
||||
autoload :Callbacks
|
||||
autoload :Cascade
|
||||
autoload :Cookies
|
||||
autoload :Flash
|
||||
autoload :Head
|
||||
autoload :Notifications
|
||||
autoload :ParamsParser
|
||||
autoload :Rescue
|
||||
autoload :ShowExceptions
|
||||
@@ -55,7 +57,15 @@ module ActionDispatch
|
||||
autoload :Routing
|
||||
|
||||
module Http
|
||||
autoload :Headers, 'action_dispatch/http/headers'
|
||||
extend ActiveSupport::Autoload
|
||||
|
||||
autoload :Cache
|
||||
autoload :Headers
|
||||
autoload :MimeNegotiation
|
||||
autoload :Parameters
|
||||
autoload :Upload
|
||||
autoload :UploadedFile, 'action_dispatch/http/upload'
|
||||
autoload :URL
|
||||
end
|
||||
|
||||
module Session
|
||||
|
||||
123
actionpack/lib/action_dispatch/http/cache.rb
Normal file
123
actionpack/lib/action_dispatch/http/cache.rb
Normal file
@@ -0,0 +1,123 @@
|
||||
module ActionDispatch
|
||||
module Http
|
||||
module Cache
|
||||
module Request
|
||||
def if_modified_since
|
||||
if since = env['HTTP_IF_MODIFIED_SINCE']
|
||||
Time.rfc2822(since) rescue nil
|
||||
end
|
||||
end
|
||||
|
||||
def if_none_match
|
||||
env['HTTP_IF_NONE_MATCH']
|
||||
end
|
||||
|
||||
def not_modified?(modified_at)
|
||||
if_modified_since && modified_at && if_modified_since >= modified_at
|
||||
end
|
||||
|
||||
def etag_matches?(etag)
|
||||
if_none_match && if_none_match == etag
|
||||
end
|
||||
|
||||
# Check response freshness (Last-Modified and ETag) against request
|
||||
# If-Modified-Since and If-None-Match conditions. If both headers are
|
||||
# supplied, both must match, or the request is not considered fresh.
|
||||
def fresh?(response)
|
||||
last_modified = if_modified_since
|
||||
etag = if_none_match
|
||||
|
||||
return false unless last_modified || etag
|
||||
|
||||
success = true
|
||||
success &&= not_modified?(response.last_modified) if last_modified
|
||||
success &&= etag_matches?(response.etag) if etag
|
||||
success
|
||||
end
|
||||
end
|
||||
|
||||
module Response
|
||||
def cache_control
|
||||
@cache_control ||= {}
|
||||
end
|
||||
|
||||
def last_modified
|
||||
if last = headers['Last-Modified']
|
||||
Time.httpdate(last)
|
||||
end
|
||||
end
|
||||
|
||||
def last_modified?
|
||||
headers.include?('Last-Modified')
|
||||
end
|
||||
|
||||
def last_modified=(utc_time)
|
||||
headers['Last-Modified'] = utc_time.httpdate
|
||||
end
|
||||
|
||||
def etag
|
||||
@etag
|
||||
end
|
||||
|
||||
def etag?
|
||||
@etag
|
||||
end
|
||||
|
||||
def etag=(etag)
|
||||
key = ActiveSupport::Cache.expand_cache_key(etag)
|
||||
@etag = %("#{Digest::MD5.hexdigest(key)}")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def handle_conditional_get!
|
||||
if etag? || last_modified? || !@cache_control.empty?
|
||||
set_conditional_cache_control!
|
||||
elsif nonempty_ok_response?
|
||||
self.etag = @body
|
||||
|
||||
if request && request.etag_matches?(etag)
|
||||
self.status = 304
|
||||
self.body = []
|
||||
end
|
||||
|
||||
set_conditional_cache_control!
|
||||
else
|
||||
headers["Cache-Control"] = "no-cache"
|
||||
end
|
||||
end
|
||||
|
||||
def nonempty_ok_response?
|
||||
@status == 200 && string_body?
|
||||
end
|
||||
|
||||
def string_body?
|
||||
!@blank && @body.respond_to?(:all?) && @body.all? { |part| part.is_a?(String) }
|
||||
end
|
||||
|
||||
DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate"
|
||||
|
||||
def set_conditional_cache_control!
|
||||
control = @cache_control
|
||||
|
||||
if control.empty?
|
||||
headers["Cache-Control"] = DEFAULT_CACHE_CONTROL
|
||||
elsif @cache_control[:no_cache]
|
||||
headers["Cache-Control"] = "no-cache"
|
||||
else
|
||||
extras = control[:extras]
|
||||
max_age = control[:max_age]
|
||||
|
||||
options = []
|
||||
options << "max-age=#{max_age.to_i}" if max_age
|
||||
options << (control[:public] ? "public" : "private")
|
||||
options << "must-revalidate" if control[:must_revalidate]
|
||||
options.concat(extras) if extras
|
||||
|
||||
headers["Cache-Control"] = options.join(", ")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
101
actionpack/lib/action_dispatch/http/mime_negotiation.rb
Normal file
101
actionpack/lib/action_dispatch/http/mime_negotiation.rb
Normal file
@@ -0,0 +1,101 @@
|
||||
module ActionDispatch
|
||||
module Http
|
||||
module MimeNegotiation
|
||||
# The MIME type of the HTTP request, such as Mime::XML.
|
||||
#
|
||||
# For backward compatibility, the post \format is extracted from the
|
||||
# X-Post-Data-Format HTTP header if present.
|
||||
def content_type
|
||||
@env["action_dispatch.request.content_type"] ||= begin
|
||||
if @env['CONTENT_TYPE'] =~ /^([^,\;]*)/
|
||||
Mime::Type.lookup($1.strip.downcase)
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the accepted MIME type for the request.
|
||||
def accepts
|
||||
@env["action_dispatch.request.accepts"] ||= begin
|
||||
header = @env['HTTP_ACCEPT'].to_s.strip
|
||||
|
||||
if header.empty?
|
||||
[content_type]
|
||||
else
|
||||
Mime::Type.parse(header)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the Mime type for the \format used in the request.
|
||||
#
|
||||
# GET /posts/5.xml | request.format => Mime::XML
|
||||
# GET /posts/5.xhtml | request.format => Mime::HTML
|
||||
# GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first depending on the value of <tt>ActionController::Base.use_accept_header</tt>
|
||||
#
|
||||
def format(view_path = [])
|
||||
formats.first
|
||||
end
|
||||
|
||||
def formats
|
||||
accept = @env['HTTP_ACCEPT']
|
||||
|
||||
@env["action_dispatch.request.formats"] ||=
|
||||
if parameters[:format]
|
||||
Array(Mime[parameters[:format]])
|
||||
elsif xhr? || (accept && !accept.include?(?,))
|
||||
accepts
|
||||
else
|
||||
[Mime::HTML]
|
||||
end
|
||||
end
|
||||
|
||||
# Sets the \format by string extension, which can be used to force custom formats
|
||||
# that are not controlled by the extension.
|
||||
#
|
||||
# class ApplicationController < ActionController::Base
|
||||
# before_filter :adjust_format_for_iphone
|
||||
#
|
||||
# private
|
||||
# def adjust_format_for_iphone
|
||||
# request.format = :iphone if request.env["HTTP_USER_AGENT"][/iPhone/]
|
||||
# end
|
||||
# end
|
||||
def format=(extension)
|
||||
parameters[:format] = extension.to_s
|
||||
@env["action_dispatch.request.formats"] = [Mime::Type.lookup_by_extension(parameters[:format])]
|
||||
end
|
||||
|
||||
# Returns a symbolized version of the <tt>:format</tt> parameter of the request.
|
||||
# If no \format is given it returns <tt>:js</tt>for Ajax requests and <tt>:html</tt>
|
||||
# otherwise.
|
||||
def template_format
|
||||
parameter_format = parameters[:format]
|
||||
|
||||
if parameter_format
|
||||
parameter_format
|
||||
elsif xhr?
|
||||
:js
|
||||
else
|
||||
:html
|
||||
end
|
||||
end
|
||||
|
||||
# Receives an array of mimes and return the first user sent mime that
|
||||
# matches the order array.
|
||||
#
|
||||
def negotiate_mime(order)
|
||||
formats.each do |priority|
|
||||
if priority == Mime::ALL
|
||||
return order.first
|
||||
elsif order.include?(priority)
|
||||
return priority
|
||||
end
|
||||
end
|
||||
|
||||
order.include?(Mime::ALL) ? formats.first : nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
50
actionpack/lib/action_dispatch/http/parameters.rb
Normal file
50
actionpack/lib/action_dispatch/http/parameters.rb
Normal file
@@ -0,0 +1,50 @@
|
||||
require 'active_support/core_ext/hash/keys'
|
||||
|
||||
module ActionDispatch
|
||||
module Http
|
||||
module Parameters
|
||||
# Returns both GET and POST \parameters in a single hash.
|
||||
def parameters
|
||||
@env["action_dispatch.request.parameters"] ||= request_parameters.merge(query_parameters).update(path_parameters).with_indifferent_access
|
||||
end
|
||||
alias :params :parameters
|
||||
|
||||
def path_parameters=(parameters) #:nodoc:
|
||||
@env.delete("action_dispatch.request.symbolized_path_parameters")
|
||||
@env.delete("action_dispatch.request.parameters")
|
||||
@env["action_dispatch.request.path_parameters"] = parameters
|
||||
end
|
||||
|
||||
# The same as <tt>path_parameters</tt> with explicitly symbolized keys.
|
||||
def symbolized_path_parameters
|
||||
@env["action_dispatch.request.symbolized_path_parameters"] ||= path_parameters.symbolize_keys
|
||||
end
|
||||
|
||||
# Returns a hash with the \parameters used to form the \path of the request.
|
||||
# Returned hash keys are strings:
|
||||
#
|
||||
# {'action' => 'my_action', 'controller' => 'my_controller'}
|
||||
#
|
||||
# See <tt>symbolized_path_parameters</tt> for symbolized keys.
|
||||
def path_parameters
|
||||
@env["action_dispatch.request.path_parameters"] ||= {}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Convert nested Hashs to HashWithIndifferentAccess
|
||||
def normalize_parameters(value)
|
||||
case value
|
||||
when Hash
|
||||
h = {}
|
||||
value.each { |k, v| h[k] = normalize_parameters(v) }
|
||||
h.with_indifferent_access
|
||||
when Array
|
||||
value.map { |e| normalize_parameters(e) }
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2,14 +2,17 @@ require 'tempfile'
|
||||
require 'stringio'
|
||||
require 'strscan'
|
||||
|
||||
require 'active_support/memoizable'
|
||||
require 'active_support/core_ext/array/wrap'
|
||||
require 'active_support/core_ext/hash/indifferent_access'
|
||||
require 'active_support/core_ext/string/access'
|
||||
require 'action_dispatch/http/headers'
|
||||
|
||||
module ActionDispatch
|
||||
class Request < Rack::Request
|
||||
include ActionDispatch::Http::Cache::Request
|
||||
include ActionDispatch::Http::MimeNegotiation
|
||||
include ActionDispatch::Http::Parameters
|
||||
include ActionDispatch::Http::Upload
|
||||
include ActionDispatch::Http::URL
|
||||
|
||||
%w[ AUTH_TYPE GATEWAY_INTERFACE
|
||||
PATH_TRANSLATED REMOTE_HOST
|
||||
@@ -19,9 +22,11 @@ module ActionDispatch
|
||||
HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING
|
||||
HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM
|
||||
HTTP_NEGOTIATE HTTP_PRAGMA ].each do |env|
|
||||
define_method(env.sub(/^HTTP_/n, '').downcase) do
|
||||
@env[env]
|
||||
end
|
||||
class_eval <<-METHOD, __FILE__, __LINE__ + 1
|
||||
def #{env.sub(/^HTTP_/n, '').downcase}
|
||||
@env["#{env}"]
|
||||
end
|
||||
METHOD
|
||||
end
|
||||
|
||||
def key?(key)
|
||||
@@ -81,25 +86,6 @@ module ActionDispatch
|
||||
Http::Headers.new(@env)
|
||||
end
|
||||
|
||||
# Returns the content length of the request as an integer.
|
||||
def content_length
|
||||
super.to_i
|
||||
end
|
||||
|
||||
# The MIME type of the HTTP request, such as Mime::XML.
|
||||
#
|
||||
# For backward compatibility, the post \format is extracted from the
|
||||
# X-Post-Data-Format HTTP header if present.
|
||||
def content_type
|
||||
@env["action_dispatch.request.content_type"] ||= begin
|
||||
if @env['CONTENT_TYPE'] =~ /^([^,\;]*)/
|
||||
Mime::Type.lookup($1.strip.downcase)
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def forgery_whitelisted?
|
||||
method == :get || xhr? || content_type.nil? || !content_type.verify_request?
|
||||
end
|
||||
@@ -108,104 +94,9 @@ module ActionDispatch
|
||||
content_type.to_s
|
||||
end
|
||||
|
||||
# Returns the accepted MIME type for the request.
|
||||
def accepts
|
||||
@env["action_dispatch.request.accepts"] ||= begin
|
||||
header = @env['HTTP_ACCEPT'].to_s.strip
|
||||
|
||||
if header.empty?
|
||||
[content_type]
|
||||
else
|
||||
Mime::Type.parse(header)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def if_modified_since
|
||||
if since = env['HTTP_IF_MODIFIED_SINCE']
|
||||
Time.rfc2822(since) rescue nil
|
||||
end
|
||||
end
|
||||
|
||||
def if_none_match
|
||||
env['HTTP_IF_NONE_MATCH']
|
||||
end
|
||||
|
||||
def not_modified?(modified_at)
|
||||
if_modified_since && modified_at && if_modified_since >= modified_at
|
||||
end
|
||||
|
||||
def etag_matches?(etag)
|
||||
if_none_match && if_none_match == etag
|
||||
end
|
||||
|
||||
# Check response freshness (Last-Modified and ETag) against request
|
||||
# If-Modified-Since and If-None-Match conditions. If both headers are
|
||||
# supplied, both must match, or the request is not considered fresh.
|
||||
def fresh?(response)
|
||||
last_modified = if_modified_since
|
||||
etag = if_none_match
|
||||
|
||||
return false unless last_modified || etag
|
||||
|
||||
success = true
|
||||
success &&= not_modified?(response.last_modified) if last_modified
|
||||
success &&= etag_matches?(response.etag) if etag
|
||||
success
|
||||
end
|
||||
|
||||
# Returns the Mime type for the \format used in the request.
|
||||
#
|
||||
# GET /posts/5.xml | request.format => Mime::XML
|
||||
# GET /posts/5.xhtml | request.format => Mime::HTML
|
||||
# GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first depending on the value of <tt>ActionController::Base.use_accept_header</tt>
|
||||
#
|
||||
def format(view_path = [])
|
||||
formats.first
|
||||
end
|
||||
|
||||
def formats
|
||||
accept = @env['HTTP_ACCEPT']
|
||||
|
||||
@env["action_dispatch.request.formats"] ||=
|
||||
if parameters[:format]
|
||||
Array.wrap(Mime[parameters[:format]])
|
||||
elsif xhr? || (accept && !accept.include?(?,))
|
||||
accepts
|
||||
else
|
||||
[Mime::HTML]
|
||||
end
|
||||
end
|
||||
|
||||
# Sets the \format by string extension, which can be used to force custom formats
|
||||
# that are not controlled by the extension.
|
||||
#
|
||||
# class ApplicationController < ActionController::Base
|
||||
# before_filter :adjust_format_for_iphone
|
||||
#
|
||||
# private
|
||||
# def adjust_format_for_iphone
|
||||
# request.format = :iphone if request.env["HTTP_USER_AGENT"][/iPhone/]
|
||||
# end
|
||||
# end
|
||||
def format=(extension)
|
||||
parameters[:format] = extension.to_s
|
||||
@env["action_dispatch.request.formats"] = [Mime::Type.lookup_by_extension(parameters[:format])]
|
||||
end
|
||||
|
||||
# Returns a symbolized version of the <tt>:format</tt> parameter of the request.
|
||||
# If no \format is given it returns <tt>:js</tt>for Ajax requests and <tt>:html</tt>
|
||||
# otherwise.
|
||||
def template_format
|
||||
parameter_format = parameters[:format]
|
||||
|
||||
if parameter_format
|
||||
parameter_format
|
||||
elsif xhr?
|
||||
:js
|
||||
else
|
||||
:html
|
||||
end
|
||||
# Returns the content length of the request as an integer.
|
||||
def content_length
|
||||
super.to_i
|
||||
end
|
||||
|
||||
# Returns true if the request's "X-Requested-With" header contains
|
||||
@@ -238,7 +129,7 @@ module ActionDispatch
|
||||
if @env.include? 'HTTP_CLIENT_IP'
|
||||
if ActionController::Base.ip_spoofing_check && remote_ips && !remote_ips.include?(@env['HTTP_CLIENT_IP'])
|
||||
# We don't know which came from the proxy, and which from the user
|
||||
raise ActionController::ActionControllerError.new(<<EOM)
|
||||
raise ActionController::ActionControllerError.new <<EOM
|
||||
IP spoofing attack?!
|
||||
HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect}
|
||||
HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}
|
||||
@@ -264,124 +155,6 @@ EOM
|
||||
(@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil
|
||||
end
|
||||
|
||||
# Returns the complete URL used for this request.
|
||||
def url
|
||||
protocol + host_with_port + request_uri
|
||||
end
|
||||
|
||||
# Returns 'https://' if this is an SSL request and 'http://' otherwise.
|
||||
def protocol
|
||||
ssl? ? 'https://' : 'http://'
|
||||
end
|
||||
|
||||
# Is this an SSL request?
|
||||
def ssl?
|
||||
@env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https'
|
||||
end
|
||||
|
||||
# Returns the \host for this request, such as "example.com".
|
||||
def raw_host_with_port
|
||||
if forwarded = env["HTTP_X_FORWARDED_HOST"]
|
||||
forwarded.split(/,\s?/).last
|
||||
else
|
||||
env['HTTP_HOST'] || "#{env['SERVER_NAME'] || env['SERVER_ADDR']}:#{env['SERVER_PORT']}"
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the host for this request, such as example.com.
|
||||
def host
|
||||
raw_host_with_port.sub(/:\d+$/, '')
|
||||
end
|
||||
|
||||
# Returns a \host:\port string for this request, such as "example.com" or
|
||||
# "example.com:8080".
|
||||
def host_with_port
|
||||
"#{host}#{port_string}"
|
||||
end
|
||||
|
||||
# Returns the port number of this request as an integer.
|
||||
def port
|
||||
if raw_host_with_port =~ /:(\d+)$/
|
||||
$1.to_i
|
||||
else
|
||||
standard_port
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the standard \port number for this request's protocol.
|
||||
def standard_port
|
||||
case protocol
|
||||
when 'https://' then 443
|
||||
else 80
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a \port suffix like ":8080" if the \port number of this request
|
||||
# is not the default HTTP \port 80 or HTTPS \port 443.
|
||||
def port_string
|
||||
port == standard_port ? '' : ":#{port}"
|
||||
end
|
||||
|
||||
def server_port
|
||||
@env['SERVER_PORT'].to_i
|
||||
end
|
||||
|
||||
# Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify
|
||||
# a different <tt>tld_length</tt>, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk".
|
||||
def domain(tld_length = 1)
|
||||
return nil unless named_host?(host)
|
||||
|
||||
host.split('.').last(1 + tld_length).join('.')
|
||||
end
|
||||
|
||||
# Returns all the \subdomains as an array, so <tt>["dev", "www"]</tt> would be
|
||||
# returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>,
|
||||
# such as 2 to catch <tt>["www"]</tt> instead of <tt>["www", "rubyonrails"]</tt>
|
||||
# in "www.rubyonrails.co.uk".
|
||||
def subdomains(tld_length = 1)
|
||||
return [] unless named_host?(host)
|
||||
parts = host.split('.')
|
||||
parts[0..-(tld_length+2)]
|
||||
end
|
||||
|
||||
# Returns the query string, accounting for server idiosyncrasies.
|
||||
def query_string
|
||||
@env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].to_s.split('?', 2)[1] || '')
|
||||
end
|
||||
|
||||
# Returns the request URI, accounting for server idiosyncrasies.
|
||||
# WEBrick includes the full URL. IIS leaves REQUEST_URI blank.
|
||||
def request_uri
|
||||
if uri = @env['REQUEST_URI']
|
||||
# Remove domain, which webrick puts into the request_uri.
|
||||
(%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri
|
||||
else
|
||||
# Construct IIS missing REQUEST_URI from SCRIPT_NAME and PATH_INFO.
|
||||
uri = @env['PATH_INFO'].to_s
|
||||
|
||||
if script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$})
|
||||
uri = uri.sub(/#{script_filename}\//, '')
|
||||
end
|
||||
|
||||
env_qs = @env['QUERY_STRING'].to_s
|
||||
uri += "?#{env_qs}" unless env_qs.empty?
|
||||
|
||||
if uri.blank?
|
||||
@env.delete('REQUEST_URI')
|
||||
else
|
||||
@env['REQUEST_URI'] = uri
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the interpreted \path to requested resource after all the installation
|
||||
# directory of this application was taken into account.
|
||||
def path
|
||||
path = request_uri.to_s[/\A[^\?]*/]
|
||||
path.sub!(/\A#{ActionController::Base.relative_url_root}/, '')
|
||||
path
|
||||
end
|
||||
|
||||
# Read the request \body. This is useful for web services that need to
|
||||
# work with raw requests directly.
|
||||
def raw_post
|
||||
@@ -392,33 +165,6 @@ EOM
|
||||
@env['RAW_POST_DATA']
|
||||
end
|
||||
|
||||
# Returns both GET and POST \parameters in a single hash.
|
||||
def parameters
|
||||
@env["action_dispatch.request.parameters"] ||= request_parameters.merge(query_parameters).update(path_parameters).with_indifferent_access
|
||||
end
|
||||
alias_method :params, :parameters
|
||||
|
||||
def path_parameters=(parameters) #:nodoc:
|
||||
@env.delete("action_dispatch.request.symbolized_path_parameters")
|
||||
@env.delete("action_dispatch.request.parameters")
|
||||
@env["action_dispatch.request.path_parameters"] = parameters
|
||||
end
|
||||
|
||||
# The same as <tt>path_parameters</tt> with explicitly symbolized keys.
|
||||
def symbolized_path_parameters
|
||||
@env["action_dispatch.request.symbolized_path_parameters"] ||= path_parameters.symbolize_keys
|
||||
end
|
||||
|
||||
# Returns a hash with the \parameters used to form the \path of the request.
|
||||
# Returned hash keys are strings:
|
||||
#
|
||||
# {'action' => 'my_action', 'controller' => 'my_controller'}
|
||||
#
|
||||
# See <tt>symbolized_path_parameters</tt> for symbolized keys.
|
||||
def path_parameters
|
||||
@env["action_dispatch.request.path_parameters"] ||= {}
|
||||
end
|
||||
|
||||
# The request body is an IO input stream. If the RAW_POST_DATA environment
|
||||
# variable is already set, wrap it in a StringIO.
|
||||
def body
|
||||
@@ -434,18 +180,6 @@ EOM
|
||||
FORM_DATA_MEDIA_TYPES.include?(content_type.to_s)
|
||||
end
|
||||
|
||||
# Override Rack's GET method to support indifferent access
|
||||
def GET
|
||||
@env["action_dispatch.request.query_parameters"] ||= normalize_parameters(super)
|
||||
end
|
||||
alias_method :query_parameters, :GET
|
||||
|
||||
# Override Rack's POST method to support indifferent access
|
||||
def POST
|
||||
@env["action_dispatch.request.request_parameters"] ||= normalize_parameters(super)
|
||||
end
|
||||
alias_method :request_parameters, :POST
|
||||
|
||||
def body_stream #:nodoc:
|
||||
@env['rack.input']
|
||||
end
|
||||
@@ -463,6 +197,19 @@ EOM
|
||||
@env['rack.session.options'] = options
|
||||
end
|
||||
|
||||
# Override Rack's GET method to support indifferent access
|
||||
def GET
|
||||
@env["action_dispatch.request.query_parameters"] ||= normalize_parameters(super)
|
||||
end
|
||||
alias :query_parameters :GET
|
||||
|
||||
# Override Rack's POST method to support indifferent access
|
||||
def POST
|
||||
@env["action_dispatch.request.request_parameters"] ||= normalize_parameters(super)
|
||||
end
|
||||
alias :request_parameters :POST
|
||||
|
||||
|
||||
# Returns the authorization header regardless of whether it was specified directly or through one of the
|
||||
# proxy alternatives.
|
||||
def authorization
|
||||
@@ -471,77 +218,5 @@ EOM
|
||||
@env['X_HTTP_AUTHORIZATION'] ||
|
||||
@env['REDIRECT_X_HTTP_AUTHORIZATION']
|
||||
end
|
||||
|
||||
# Receives an array of mimes and return the first user sent mime that
|
||||
# matches the order array.
|
||||
#
|
||||
def negotiate_mime(order)
|
||||
formats.each do |priority|
|
||||
if priority == Mime::ALL
|
||||
return order.first
|
||||
elsif order.include?(priority)
|
||||
return priority
|
||||
end
|
||||
end
|
||||
|
||||
order.include?(Mime::ALL) ? formats.first : nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def named_host?(host)
|
||||
!(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
|
||||
end
|
||||
|
||||
module UploadedFile
|
||||
def self.extended(object)
|
||||
object.class_eval do
|
||||
attr_accessor :original_path, :content_type
|
||||
alias_method :local_path, :path if method_defined?(:path)
|
||||
end
|
||||
end
|
||||
|
||||
# Take the basename of the upload's original filename.
|
||||
# This handles the full Windows paths given by Internet Explorer
|
||||
# (and perhaps other broken user agents) without affecting
|
||||
# those which give the lone filename.
|
||||
# The Windows regexp is adapted from Perl's File::Basename.
|
||||
def original_filename
|
||||
unless defined? @original_filename
|
||||
@original_filename =
|
||||
unless original_path.blank?
|
||||
if original_path =~ /^(?:.*[:\\\/])?(.*)/m
|
||||
$1
|
||||
else
|
||||
File.basename original_path
|
||||
end
|
||||
end
|
||||
end
|
||||
@original_filename
|
||||
end
|
||||
end
|
||||
|
||||
# Convert nested Hashs to HashWithIndifferentAccess and replace
|
||||
# file upload hashs with UploadedFile objects
|
||||
def normalize_parameters(value)
|
||||
case value
|
||||
when Hash
|
||||
if value.has_key?(:tempfile)
|
||||
upload = value[:tempfile]
|
||||
upload.extend(UploadedFile)
|
||||
upload.original_path = value[:filename]
|
||||
upload.content_type = value[:type]
|
||||
upload
|
||||
else
|
||||
h = {}
|
||||
value.each { |k, v| h[k] = normalize_parameters(v) }
|
||||
h.with_indifferent_access
|
||||
end
|
||||
when Array
|
||||
value.map { |e| normalize_parameters(e) }
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -32,6 +32,8 @@ module ActionDispatch # :nodoc:
|
||||
# end
|
||||
# end
|
||||
class Response < Rack::Response
|
||||
include ActionDispatch::Http::Cache::Response
|
||||
|
||||
attr_accessor :request, :blank
|
||||
|
||||
attr_writer :header, :sending_file
|
||||
@@ -55,10 +57,6 @@ module ActionDispatch # :nodoc:
|
||||
yield self if block_given?
|
||||
end
|
||||
|
||||
def cache_control
|
||||
@cache_control ||= {}
|
||||
end
|
||||
|
||||
def status=(status)
|
||||
@status = Rack::Utils.status_code(status)
|
||||
end
|
||||
@@ -114,33 +112,6 @@ module ActionDispatch # :nodoc:
|
||||
# information.
|
||||
attr_accessor :charset, :content_type
|
||||
|
||||
def last_modified
|
||||
if last = headers['Last-Modified']
|
||||
Time.httpdate(last)
|
||||
end
|
||||
end
|
||||
|
||||
def last_modified?
|
||||
headers.include?('Last-Modified')
|
||||
end
|
||||
|
||||
def last_modified=(utc_time)
|
||||
headers['Last-Modified'] = utc_time.httpdate
|
||||
end
|
||||
|
||||
def etag
|
||||
@etag
|
||||
end
|
||||
|
||||
def etag?
|
||||
@etag
|
||||
end
|
||||
|
||||
def etag=(etag)
|
||||
key = ActiveSupport::Cache.expand_cache_key(etag)
|
||||
@etag = %("#{Digest::MD5.hexdigest(key)}")
|
||||
end
|
||||
|
||||
CONTENT_TYPE = "Content-Type"
|
||||
|
||||
cattr_accessor(:default_charset) { "utf-8" }
|
||||
@@ -148,7 +119,7 @@ module ActionDispatch # :nodoc:
|
||||
def to_a
|
||||
assign_default_content_type_and_charset!
|
||||
handle_conditional_get!
|
||||
self["Set-Cookie"] = @cookie.join("\n")
|
||||
self["Set-Cookie"] = @cookie.join("\n") unless @cookie.blank?
|
||||
self["ETag"] = @etag if @etag
|
||||
super
|
||||
end
|
||||
@@ -222,31 +193,6 @@ module ActionDispatch # :nodoc:
|
||||
end
|
||||
|
||||
private
|
||||
def handle_conditional_get!
|
||||
if etag? || last_modified? || !@cache_control.empty?
|
||||
set_conditional_cache_control!
|
||||
elsif nonempty_ok_response?
|
||||
self.etag = @body
|
||||
|
||||
if request && request.etag_matches?(etag)
|
||||
self.status = 304
|
||||
self.body = []
|
||||
end
|
||||
|
||||
set_conditional_cache_control!
|
||||
else
|
||||
headers["Cache-Control"] = "no-cache"
|
||||
end
|
||||
end
|
||||
|
||||
def nonempty_ok_response?
|
||||
@status == 200 && string_body?
|
||||
end
|
||||
|
||||
def string_body?
|
||||
!@blank && @body.respond_to?(:all?) && @body.all? { |part| part.is_a?(String) }
|
||||
end
|
||||
|
||||
def assign_default_content_type_and_charset!
|
||||
return if headers[CONTENT_TYPE].present?
|
||||
|
||||
@@ -259,27 +205,5 @@ module ActionDispatch # :nodoc:
|
||||
headers[CONTENT_TYPE] = type
|
||||
end
|
||||
|
||||
DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate"
|
||||
|
||||
def set_conditional_cache_control!
|
||||
control = @cache_control
|
||||
|
||||
if control.empty?
|
||||
headers["Cache-Control"] = DEFAULT_CACHE_CONTROL
|
||||
elsif @cache_control[:no_cache]
|
||||
headers["Cache-Control"] = "no-cache"
|
||||
else
|
||||
extras = control[:extras]
|
||||
max_age = control[:max_age]
|
||||
|
||||
options = []
|
||||
options << "max-age=#{max_age.to_i}" if max_age
|
||||
options << (control[:public] ? "public" : "private")
|
||||
options << "must-revalidate" if control[:must_revalidate]
|
||||
options.concat(extras) if extras
|
||||
|
||||
headers["Cache-Control"] = options.join(", ")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
48
actionpack/lib/action_dispatch/http/upload.rb
Normal file
48
actionpack/lib/action_dispatch/http/upload.rb
Normal file
@@ -0,0 +1,48 @@
|
||||
module ActionDispatch
|
||||
module Http
|
||||
module UploadedFile
|
||||
def self.extended(object)
|
||||
object.class_eval do
|
||||
attr_accessor :original_path, :content_type
|
||||
alias_method :local_path, :path if method_defined?(:path)
|
||||
end
|
||||
end
|
||||
|
||||
# Take the basename of the upload's original filename.
|
||||
# This handles the full Windows paths given by Internet Explorer
|
||||
# (and perhaps other broken user agents) without affecting
|
||||
# those which give the lone filename.
|
||||
# The Windows regexp is adapted from Perl's File::Basename.
|
||||
def original_filename
|
||||
unless defined? @original_filename
|
||||
@original_filename =
|
||||
unless original_path.blank?
|
||||
if original_path =~ /^(?:.*[:\\\/])?(.*)/m
|
||||
$1
|
||||
else
|
||||
File.basename original_path
|
||||
end
|
||||
end
|
||||
end
|
||||
@original_filename
|
||||
end
|
||||
end
|
||||
|
||||
module Upload
|
||||
# Convert nested Hashs to HashWithIndifferentAccess and replace
|
||||
# file upload hashs with UploadedFile objects
|
||||
def normalize_parameters(value)
|
||||
if Hash === value && value.has_key?(:tempfile)
|
||||
upload = value[:tempfile]
|
||||
upload.extend(UploadedFile)
|
||||
upload.original_path = value[:filename]
|
||||
upload.content_type = value[:type]
|
||||
upload
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
private :normalize_parameters
|
||||
end
|
||||
end
|
||||
end
|
||||
129
actionpack/lib/action_dispatch/http/url.rb
Normal file
129
actionpack/lib/action_dispatch/http/url.rb
Normal file
@@ -0,0 +1,129 @@
|
||||
module ActionDispatch
|
||||
module Http
|
||||
module URL
|
||||
# Returns the complete URL used for this request.
|
||||
def url
|
||||
protocol + host_with_port + request_uri
|
||||
end
|
||||
|
||||
# Returns 'https://' if this is an SSL request and 'http://' otherwise.
|
||||
def protocol
|
||||
ssl? ? 'https://' : 'http://'
|
||||
end
|
||||
|
||||
# Is this an SSL request?
|
||||
def ssl?
|
||||
@env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https'
|
||||
end
|
||||
|
||||
# Returns the \host for this request, such as "example.com".
|
||||
def raw_host_with_port
|
||||
if forwarded = env["HTTP_X_FORWARDED_HOST"]
|
||||
forwarded.split(/,\s?/).last
|
||||
else
|
||||
env['HTTP_HOST'] || "#{env['SERVER_NAME'] || env['SERVER_ADDR']}:#{env['SERVER_PORT']}"
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the host for this request, such as example.com.
|
||||
def host
|
||||
raw_host_with_port.sub(/:\d+$/, '')
|
||||
end
|
||||
|
||||
# Returns a \host:\port string for this request, such as "example.com" or
|
||||
# "example.com:8080".
|
||||
def host_with_port
|
||||
"#{host}#{port_string}"
|
||||
end
|
||||
|
||||
# Returns the port number of this request as an integer.
|
||||
def port
|
||||
if raw_host_with_port =~ /:(\d+)$/
|
||||
$1.to_i
|
||||
else
|
||||
standard_port
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the standard \port number for this request's protocol.
|
||||
def standard_port
|
||||
case protocol
|
||||
when 'https://' then 443
|
||||
else 80
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a \port suffix like ":8080" if the \port number of this request
|
||||
# is not the default HTTP \port 80 or HTTPS \port 443.
|
||||
def port_string
|
||||
port == standard_port ? '' : ":#{port}"
|
||||
end
|
||||
|
||||
def server_port
|
||||
@env['SERVER_PORT'].to_i
|
||||
end
|
||||
|
||||
# Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify
|
||||
# a different <tt>tld_length</tt>, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk".
|
||||
def domain(tld_length = 1)
|
||||
return nil unless named_host?(host)
|
||||
|
||||
host.split('.').last(1 + tld_length).join('.')
|
||||
end
|
||||
|
||||
# Returns all the \subdomains as an array, so <tt>["dev", "www"]</tt> would be
|
||||
# returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>,
|
||||
# such as 2 to catch <tt>["www"]</tt> instead of <tt>["www", "rubyonrails"]</tt>
|
||||
# in "www.rubyonrails.co.uk".
|
||||
def subdomains(tld_length = 1)
|
||||
return [] unless named_host?(host)
|
||||
parts = host.split('.')
|
||||
parts[0..-(tld_length+2)]
|
||||
end
|
||||
|
||||
# Returns the query string, accounting for server idiosyncrasies.
|
||||
def query_string
|
||||
@env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].to_s.split('?', 2)[1] || '')
|
||||
end
|
||||
|
||||
# Returns the request URI, accounting for server idiosyncrasies.
|
||||
# WEBrick includes the full URL. IIS leaves REQUEST_URI blank.
|
||||
def request_uri
|
||||
if uri = @env['REQUEST_URI']
|
||||
# Remove domain, which webrick puts into the request_uri.
|
||||
(%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri
|
||||
else
|
||||
# Construct IIS missing REQUEST_URI from SCRIPT_NAME and PATH_INFO.
|
||||
uri = @env['PATH_INFO'].to_s
|
||||
|
||||
if script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$})
|
||||
uri = uri.sub(/#{script_filename}\//, '')
|
||||
end
|
||||
|
||||
env_qs = @env['QUERY_STRING'].to_s
|
||||
uri += "?#{env_qs}" unless env_qs.empty?
|
||||
|
||||
if uri.blank?
|
||||
@env.delete('REQUEST_URI')
|
||||
else
|
||||
@env['REQUEST_URI'] = uri
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the interpreted \path to requested resource after all the installation
|
||||
# directory of this application was taken into account.
|
||||
def path
|
||||
path = request_uri.to_s[/\A[^\?]*/]
|
||||
path.sub!(/\A#{ActionController::Base.relative_url_root}/, '')
|
||||
path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def named_host?(host)
|
||||
!(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -45,8 +45,6 @@ module ActionDispatch
|
||||
run_callbacks(:prepare) if @prepare_each_request
|
||||
@app.call(env)
|
||||
end
|
||||
ensure
|
||||
ActiveSupport::Notifications.instrument "action_dispatch.callback"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
216
actionpack/lib/action_dispatch/middleware/cookies.rb
Normal file
216
actionpack/lib/action_dispatch/middleware/cookies.rb
Normal file
@@ -0,0 +1,216 @@
|
||||
module ActionDispatch
|
||||
class Request
|
||||
def cookie_jar
|
||||
env['action_dispatch.cookies'] ||= Cookies::CookieJar.build(self)
|
||||
end
|
||||
end
|
||||
|
||||
# Cookies are read and written through ActionController#cookies.
|
||||
#
|
||||
# The cookies being read are the ones received along with the request, the cookies
|
||||
# being written will be sent out with the response. Reading a cookie does not get
|
||||
# the cookie object itself back, just the value it holds.
|
||||
#
|
||||
# Examples for writing:
|
||||
#
|
||||
# # Sets a simple session cookie.
|
||||
# cookies[:user_name] = "david"
|
||||
#
|
||||
# # Sets a cookie that expires in 1 hour.
|
||||
# cookies[:login] = { :value => "XJ-122", :expires => 1.hour.from_now }
|
||||
#
|
||||
# Examples for reading:
|
||||
#
|
||||
# cookies[:user_name] # => "david"
|
||||
# cookies.size # => 2
|
||||
#
|
||||
# Example for deleting:
|
||||
#
|
||||
# cookies.delete :user_name
|
||||
#
|
||||
# Please note that if you specify a :domain when setting a cookie, you must also specify the domain when deleting the cookie:
|
||||
#
|
||||
# cookies[:key] = {
|
||||
# :value => 'a yummy cookie',
|
||||
# :expires => 1.year.from_now,
|
||||
# :domain => 'domain.com'
|
||||
# }
|
||||
#
|
||||
# cookies.delete(:key, :domain => 'domain.com')
|
||||
#
|
||||
# The option symbols for setting cookies are:
|
||||
#
|
||||
# * <tt>:value</tt> - The cookie's value or list of values (as an array).
|
||||
# * <tt>:path</tt> - The path for which this cookie applies. Defaults to the root
|
||||
# of the application.
|
||||
# * <tt>:domain</tt> - The domain for which this cookie applies.
|
||||
# * <tt>:expires</tt> - The time at which this cookie expires, as a Time object.
|
||||
# * <tt>:secure</tt> - Whether this cookie is a only transmitted to HTTPS servers.
|
||||
# Default is +false+.
|
||||
# * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or
|
||||
# only HTTP. Defaults to +false+.
|
||||
class Cookies
|
||||
class CookieJar < Hash #:nodoc:
|
||||
def self.build(request)
|
||||
new.tap do |hash|
|
||||
hash.update(request.cookies)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize
|
||||
@set_cookies = {}
|
||||
@delete_cookies = {}
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
# Returns the value of the cookie by +name+, or +nil+ if no such cookie exists.
|
||||
def [](name)
|
||||
super(name.to_s)
|
||||
end
|
||||
|
||||
# Sets the cookie named +name+. The second argument may be the very cookie
|
||||
# value, or a hash of options as documented above.
|
||||
def []=(key, options)
|
||||
if options.is_a?(Hash)
|
||||
options.symbolize_keys!
|
||||
value = options[:value]
|
||||
else
|
||||
value = options
|
||||
options = { :value => value }
|
||||
end
|
||||
|
||||
value = super(key.to_s, value)
|
||||
|
||||
options[:path] ||= "/"
|
||||
@set_cookies[key] = options
|
||||
value
|
||||
end
|
||||
|
||||
# Removes the cookie on the client machine by setting the value to an empty string
|
||||
# and setting its expiration date into the past. Like <tt>[]=</tt>, you can pass in
|
||||
# an options hash to delete cookies with extra data such as a <tt>:path</tt>.
|
||||
def delete(key, options = {})
|
||||
options.symbolize_keys!
|
||||
options[:path] ||= "/"
|
||||
value = super(key.to_s)
|
||||
@delete_cookies[key] = options
|
||||
value
|
||||
end
|
||||
|
||||
# Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example:
|
||||
#
|
||||
# cookies.permanent[:prefers_open_id] = true
|
||||
# # => Set-Cookie: prefers_open_id=true; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
|
||||
#
|
||||
# This jar is only meant for writing. You'll read permanent cookies through the regular accessor.
|
||||
#
|
||||
# This jar allows chaining with the signed jar as well, so you can set permanent, signed cookies. Examples:
|
||||
#
|
||||
# cookies.permanent.signed[:remember_me] = current_user.id
|
||||
# # => Set-Cookie: discount=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
|
||||
def permanent
|
||||
@permanent ||= PermanentCookieJar.new(self)
|
||||
end
|
||||
|
||||
# Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from
|
||||
# the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed
|
||||
# cookie was tampered with by the user (or a 3rd party), an ActiveSupport::MessageVerifier::InvalidSignature exception will
|
||||
# be raised.
|
||||
#
|
||||
# This jar requires that you set a suitable secret for the verification on ActionController::Base.cookie_verifier_secret.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# cookies.signed[:discount] = 45
|
||||
# # => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/
|
||||
#
|
||||
# cookies.signed[:discount] # => 45
|
||||
def signed
|
||||
@signed ||= SignedCookieJar.new(self)
|
||||
end
|
||||
|
||||
def write(response)
|
||||
@set_cookies.each { |k, v| response.set_cookie(k, v) }
|
||||
@delete_cookies.each { |k, v| response.delete_cookie(k, v) }
|
||||
end
|
||||
end
|
||||
|
||||
class PermanentCookieJar < CookieJar #:nodoc:
|
||||
def initialize(parent_jar)
|
||||
@parent_jar = parent_jar
|
||||
end
|
||||
|
||||
def []=(key, options)
|
||||
if options.is_a?(Hash)
|
||||
options.symbolize_keys!
|
||||
else
|
||||
options = { :value => options }
|
||||
end
|
||||
|
||||
options[:expires] = 20.years.from_now
|
||||
@parent_jar[key] = options
|
||||
end
|
||||
|
||||
def signed
|
||||
@signed ||= SignedCookieJar.new(self)
|
||||
end
|
||||
|
||||
def controller
|
||||
@parent_jar.controller
|
||||
end
|
||||
|
||||
def method_missing(method, *arguments, &block)
|
||||
@parent_jar.send(method, *arguments, &block)
|
||||
end
|
||||
end
|
||||
|
||||
class SignedCookieJar < CookieJar #:nodoc:
|
||||
def initialize(parent_jar)
|
||||
unless ActionController::Base.cookie_verifier_secret
|
||||
raise "You must set ActionController::Base.cookie_verifier_secret to use signed cookies"
|
||||
end
|
||||
|
||||
@parent_jar = parent_jar
|
||||
@verifier = ActiveSupport::MessageVerifier.new(ActionController::Base.cookie_verifier_secret)
|
||||
end
|
||||
|
||||
def [](name)
|
||||
if value = @parent_jar[name]
|
||||
@verifier.verify(value)
|
||||
end
|
||||
end
|
||||
|
||||
def []=(key, options)
|
||||
if options.is_a?(Hash)
|
||||
options.symbolize_keys!
|
||||
options[:value] = @verifier.generate(options[:value])
|
||||
else
|
||||
options = { :value => @verifier.generate(options) }
|
||||
end
|
||||
|
||||
@parent_jar[key] = options
|
||||
end
|
||||
|
||||
def method_missing(method, *arguments, &block)
|
||||
@parent_jar.send(method, *arguments, &block)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
status, headers, body = @app.call(env)
|
||||
|
||||
if cookie_jar = env['action_dispatch.cookies']
|
||||
response = Rack::Response.new(body, status, headers)
|
||||
cookie_jar.write(response)
|
||||
response.to_a
|
||||
else
|
||||
[status, headers, body]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
24
actionpack/lib/action_dispatch/middleware/notifications.rb
Normal file
24
actionpack/lib/action_dispatch/middleware/notifications.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
module ActionDispatch
|
||||
# Provide notifications in the middleware stack. Notice that for the before_dispatch
|
||||
# and after_dispatch notifications, we just send the original env, so we don't pile
|
||||
# up large env hashes in the queue. However, in exception cases, the whole env hash
|
||||
# is actually useful, so we send it all.
|
||||
class Notifications
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(stack_env)
|
||||
env = stack_env.dup
|
||||
ActiveSupport::Notifications.instrument("action_dispatch.before_dispatch", :env => env)
|
||||
|
||||
ActiveSupport::Notifications.instrument!("action_dispatch.after_dispatch", :env => env) do
|
||||
@app.call(stack_env)
|
||||
end
|
||||
rescue Exception => exception
|
||||
ActiveSupport::Notifications.instrument('action_dispatch.exception',
|
||||
:env => stack_env, :exception => exception)
|
||||
raise exception
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -20,7 +20,7 @@ module ActionDispatch
|
||||
# * :exception - The exception raised;
|
||||
#
|
||||
class ShowExceptions
|
||||
LOCALHOST = '127.0.0.1'.freeze
|
||||
LOCALHOST = ['127.0.0.1', '::1'].freeze
|
||||
|
||||
RESCUES_TEMPLATE_PATH = File.join(File.dirname(__FILE__), 'templates')
|
||||
|
||||
@@ -61,11 +61,8 @@ module ActionDispatch
|
||||
def call(env)
|
||||
@app.call(env)
|
||||
rescue Exception => exception
|
||||
ActiveSupport::Notifications.instrument 'action_dispatch.show_exception',
|
||||
:env => env, :exception => exception do
|
||||
raise exception if env['action_dispatch.show_exceptions'] == false
|
||||
render_exception(env, exception)
|
||||
end
|
||||
raise exception if env['action_dispatch.show_exceptions'] == false
|
||||
render_exception(env, exception)
|
||||
end
|
||||
|
||||
private
|
||||
@@ -88,7 +85,10 @@ module ActionDispatch
|
||||
def rescue_action_locally(request, exception)
|
||||
template = ActionView::Base.new([RESCUES_TEMPLATE_PATH],
|
||||
:request => request,
|
||||
:exception => exception
|
||||
:exception => exception,
|
||||
:application_trace => application_trace(exception),
|
||||
:framework_trace => framework_trace(exception),
|
||||
:full_trace => full_trace(exception)
|
||||
)
|
||||
file = "rescues/#{@@rescue_templates[exception.class.name]}.erb"
|
||||
body = template.render(:file => file, :layout => 'rescues/layout.erb')
|
||||
@@ -118,7 +118,7 @@ module ActionDispatch
|
||||
|
||||
# True if the request came from localhost, 127.0.0.1.
|
||||
def local_request?(request)
|
||||
request.remote_addr == LOCALHOST && request.remote_ip == LOCALHOST
|
||||
LOCALHOST.any?{ |local_ip| request.remote_addr == local_ip && request.remote_ip == local_ip }
|
||||
end
|
||||
|
||||
def status_code(exception)
|
||||
@@ -148,9 +148,21 @@ module ActionDispatch
|
||||
end
|
||||
end
|
||||
|
||||
def clean_backtrace(exception)
|
||||
def application_trace(exception)
|
||||
clean_backtrace(exception, :silent)
|
||||
end
|
||||
|
||||
def framework_trace(exception)
|
||||
clean_backtrace(exception, :noise)
|
||||
end
|
||||
|
||||
def full_trace(exception)
|
||||
clean_backtrace(exception, :all)
|
||||
end
|
||||
|
||||
def clean_backtrace(exception, *args)
|
||||
defined?(Rails) && Rails.respond_to?(:backtrace_cleaner) ?
|
||||
Rails.backtrace_cleaner.clean(exception.backtrace) :
|
||||
Rails.backtrace_cleaner.clean(exception.backtrace, *args) :
|
||||
exception.backtrace
|
||||
end
|
||||
|
||||
|
||||
@@ -11,13 +11,20 @@
|
||||
clean_params.delete("controller")
|
||||
|
||||
request_dump = clean_params.empty? ? 'None' : clean_params.inspect.gsub(',', ",\n")
|
||||
|
||||
def debug_hash(hash)
|
||||
hash.sort_by { |k, v| k.to_s }.map { |k, v| "#{k}: #{v.inspect}" }.join("\n")
|
||||
end
|
||||
%>
|
||||
|
||||
<h2 style="margin-top: 30px">Request</h2>
|
||||
<p><b>Parameters</b>: <pre><%=h request_dump %></pre></p>
|
||||
|
||||
<p><a href="#" onclick="document.getElementById('session_dump').style.display='block'; return false;">Show session dump</a></p>
|
||||
<div id="session_dump" style="display:none"><%= debug(@request.session.instance_variable_get("@data")) %></div>
|
||||
<div id="session_dump" style="display:none"><pre><%= debug_hash @request.session %></pre></div>
|
||||
|
||||
<p><a href="#" onclick="document.getElementById('env_dump').style.display='block'; return false;">Show env dump</a></p>
|
||||
<div id="env_dump" style="display:none"><pre><%= debug_hash @request.env %></pre></div>
|
||||
|
||||
|
||||
<h2 style="margin-top: 30px">Response</h2>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<%
|
||||
traces = [
|
||||
["Application Trace", @exception.application_backtrace],
|
||||
["Framework Trace", @exception.framework_backtrace],
|
||||
["Full Trace", @exception.clean_backtrace]
|
||||
["Application Trace", @application_trace],
|
||||
["Framework Trace", @framework_trace],
|
||||
["Full Trace", @full_trace]
|
||||
]
|
||||
names = traces.collect {|name, trace| name}
|
||||
%>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
in <%=h @request.parameters['controller'].humanize %>Controller<% if @request.parameters['action'] %>#<%=h @request.parameters['action'] %><% end %>
|
||||
<% end %>
|
||||
</h1>
|
||||
<pre><%=h @exception.clean_message %></pre>
|
||||
<pre><%=h @exception.message %></pre>
|
||||
|
||||
<%= render :file => "rescues/_trace.erb" %>
|
||||
<%= render :file => "rescues/_request_and_response.erb" %>
|
||||
|
||||
29
actionpack/lib/action_dispatch/railtie.rb
Normal file
29
actionpack/lib/action_dispatch/railtie.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
require "action_dispatch"
|
||||
require "rails"
|
||||
|
||||
module ActionDispatch
|
||||
class Railtie < Rails::Railtie
|
||||
plugin_name :action_dispatch
|
||||
|
||||
require "action_dispatch/railties/subscriber"
|
||||
subscriber ActionDispatch::Railties::Subscriber.new
|
||||
|
||||
# Prepare dispatcher callbacks and run 'prepare' callbacks
|
||||
initializer "action_dispatch.prepare_dispatcher" do |app|
|
||||
# TODO: This used to say unless defined?(Dispatcher). Find out why and fix.
|
||||
require 'rails/dispatcher'
|
||||
|
||||
unless app.config.cache_classes
|
||||
# Setup dev mode route reloading
|
||||
routes_last_modified = app.routes_changed_at
|
||||
reload_routes = lambda do
|
||||
unless app.routes_changed_at == routes_last_modified
|
||||
routes_last_modified = app.routes_changed_at
|
||||
app.reload_routes!
|
||||
end
|
||||
end
|
||||
ActionDispatch::Callbacks.before { |callbacks| reload_routes.call }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
17
actionpack/lib/action_dispatch/railties/subscriber.rb
Normal file
17
actionpack/lib/action_dispatch/railties/subscriber.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
module ActionDispatch
|
||||
module Railties
|
||||
class Subscriber < Rails::Subscriber
|
||||
def before_dispatch(event)
|
||||
request = Request.new(event.payload[:env])
|
||||
path = request.request_uri.inspect rescue "unknown"
|
||||
|
||||
info "\n\nProcessing #{path} to #{request.formats.join(', ')} " <<
|
||||
"(for #{request.remote_ip} at #{event.time.to_s(:db)}) [#{request.method.to_s.upcase}]"
|
||||
end
|
||||
|
||||
def logger
|
||||
ActionController::Base.logger
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -193,9 +193,10 @@ module ActionDispatch
|
||||
#
|
||||
# With conditions you can define restrictions on routes. Currently the only valid condition is <tt>:method</tt>.
|
||||
#
|
||||
# * <tt>:method</tt> - Allows you to specify which method can access the route. Possible values are <tt>:post</tt>,
|
||||
# <tt>:get</tt>, <tt>:put</tt>, <tt>:delete</tt> and <tt>:any</tt>. The default value is <tt>:any</tt>,
|
||||
# <tt>:any</tt> means that any method can access the route.
|
||||
# * <tt>:method</tt> - Allows you to specify which HTTP method(s) can access the route. Possible values are
|
||||
# <tt>:post</tt>, <tt>:get</tt>, <tt>:put</tt>, <tt>:delete</tt> and <tt>:any</tt>. Use an array to specify more
|
||||
# than one method, e.g. <tt>[ :get, :post ]</tt>. The default value is <tt>:any</tt>, <tt>:any</tt> means that any
|
||||
# method can access the route.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
|
||||
@@ -380,7 +380,7 @@ module ActionDispatch
|
||||
end
|
||||
|
||||
def controller
|
||||
plural
|
||||
options[:controller] || plural
|
||||
end
|
||||
|
||||
def member_name
|
||||
@@ -418,28 +418,12 @@ module ActionDispatch
|
||||
def resource(*resources, &block)
|
||||
options = resources.extract_options!
|
||||
|
||||
if resources.length > 1
|
||||
raise ArgumentError if block_given?
|
||||
resources.each { |r| resource(r, options) }
|
||||
return self
|
||||
end
|
||||
|
||||
if path_names = options.delete(:path_names)
|
||||
scope(:resources_path_names => path_names) do
|
||||
resource(resources, options)
|
||||
end
|
||||
if verify_common_behavior_for(:resource, resources, options, &block)
|
||||
return self
|
||||
end
|
||||
|
||||
resource = SingletonResource.new(resources.pop, options)
|
||||
|
||||
if @scope[:scope_level] == :resources
|
||||
nested do
|
||||
resource(resource.name, options, &block)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
scope(:path => resource.name.to_s, :controller => resource.controller) do
|
||||
with_scope_level(:resource, resource) do
|
||||
yield if block_given?
|
||||
@@ -459,28 +443,12 @@ module ActionDispatch
|
||||
def resources(*resources, &block)
|
||||
options = resources.extract_options!
|
||||
|
||||
if resources.length > 1
|
||||
raise ArgumentError if block_given?
|
||||
resources.each { |r| resources(r, options) }
|
||||
return self
|
||||
end
|
||||
|
||||
if path_names = options.delete(:path_names)
|
||||
scope(:resources_path_names => path_names) do
|
||||
resources(resources, options)
|
||||
end
|
||||
if verify_common_behavior_for(:resources, resources, options, &block)
|
||||
return self
|
||||
end
|
||||
|
||||
resource = Resource.new(resources.pop, options)
|
||||
|
||||
if @scope[:scope_level] == :resources
|
||||
nested do
|
||||
resources(resource.name, options, &block)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
scope(:path => resource.name.to_s, :controller => resource.controller) do
|
||||
with_scope_level(:resources, resource) do
|
||||
yield if block_given?
|
||||
@@ -595,6 +563,29 @@ module ActionDispatch
|
||||
path_names[name.to_sym] || name.to_s
|
||||
end
|
||||
|
||||
def verify_common_behavior_for(method, resources, options, &block)
|
||||
if resources.length > 1
|
||||
resources.each { |r| send(method, r, options, &block) }
|
||||
return true
|
||||
end
|
||||
|
||||
if path_names = options.delete(:path_names)
|
||||
scope(:resources_path_names => path_names) do
|
||||
send(method, resources.pop, options, &block)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
if @scope[:scope_level] == :resources
|
||||
nested do
|
||||
send(method, resources.pop, options, &block)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def with_exclusive_name_prefix(prefix)
|
||||
begin
|
||||
old_name_prefix = @scope[:name_prefix]
|
||||
|
||||
@@ -12,29 +12,29 @@ module ActionDispatch
|
||||
# and a :method containing the required HTTP verb.
|
||||
#
|
||||
# # assert that POSTing to /items will call the create action on ItemsController
|
||||
# assert_recognizes {:controller => 'items', :action => 'create'}, {:path => 'items', :method => :post}
|
||||
# assert_recognizes({:controller => 'items', :action => 'create'}, {:path => 'items', :method => :post})
|
||||
#
|
||||
# You can also pass in +extras+ with a hash containing URL parameters that would normally be in the query string. This can be used
|
||||
# to assert that values in the query string string will end up in the params hash correctly. To test query strings you must use the
|
||||
# extras argument, appending the query string on the path directly will not work. For example:
|
||||
#
|
||||
# # assert that a path of '/items/list/1?view=print' returns the correct options
|
||||
# assert_recognizes {:controller => 'items', :action => 'list', :id => '1', :view => 'print'}, 'items/list/1', { :view => "print" }
|
||||
# assert_recognizes({:controller => 'items', :action => 'list', :id => '1', :view => 'print'}, 'items/list/1', { :view => "print" })
|
||||
#
|
||||
# The +message+ parameter allows you to pass in an error message that is displayed upon failure.
|
||||
#
|
||||
# ==== Examples
|
||||
# # Check the default route (i.e., the index action)
|
||||
# assert_recognizes {:controller => 'items', :action => 'index'}, 'items'
|
||||
# assert_recognizes({:controller => 'items', :action => 'index'}, 'items')
|
||||
#
|
||||
# # Test a specific action
|
||||
# assert_recognizes {:controller => 'items', :action => 'list'}, 'items/list'
|
||||
# assert_recognizes({:controller => 'items', :action => 'list'}, 'items/list')
|
||||
#
|
||||
# # Test an action with a parameter
|
||||
# assert_recognizes {:controller => 'items', :action => 'destroy', :id => '1'}, 'items/destroy/1'
|
||||
# assert_recognizes({:controller => 'items', :action => 'destroy', :id => '1'}, 'items/destroy/1')
|
||||
#
|
||||
# # Test a custom route
|
||||
# assert_recognizes {:controller => 'items', :action => 'show', :id => '1'}, 'view/item1'
|
||||
# assert_recognizes({:controller => 'items', :action => 'show', :id => '1'}, 'view/item1')
|
||||
#
|
||||
# # Check a Simply RESTful generated route
|
||||
# assert_recognizes list_items_url, 'items/list'
|
||||
@@ -103,7 +103,7 @@ module ActionDispatch
|
||||
# assert_routing '/home', :controller => 'home', :action => 'index'
|
||||
#
|
||||
# # Test a route generated with a specific controller, action, and parameter (id)
|
||||
# assert_routing '/entries/show/23', :controller => 'entries', :action => 'show', id => 23
|
||||
# assert_routing '/entries/show/23', :controller => 'entries', :action => 'show', :id => 23
|
||||
#
|
||||
# # Assert a basic route (controller + default action), with an error message if it fails
|
||||
# assert_routing '/store', { :controller => 'store', :action => 'index' }, {}, {}, 'Route for store index not generated properly'
|
||||
@@ -112,7 +112,7 @@ module ActionDispatch
|
||||
# assert_routing 'controller/action/9', {:id => "9", :item => "square"}, {:controller => "controller", :action => "action"}, {}, {:item => "square"}
|
||||
#
|
||||
# # Tests a route with a HTTP method
|
||||
# assert_routing { :method => 'put', :path => '/product/321' }, { :controller => "product", :action => "update", :id => "321" }
|
||||
# assert_routing({ :method => 'put', :path => '/product/321' }, { :controller => "product", :action => "update", :id => "321" })
|
||||
def assert_routing(path, options, defaults={}, extras={}, message=nil)
|
||||
assert_recognizes(options, path, extras, message)
|
||||
|
||||
|
||||
@@ -27,10 +27,10 @@ module ActionView
|
||||
def debug(object)
|
||||
begin
|
||||
Marshal::dump(object)
|
||||
"<pre class='debug_dump'>#{h(object.to_yaml).gsub(" ", " ")}</pre>"
|
||||
"<pre class='debug_dump'>#{h(object.to_yaml).gsub(" ", " ")}</pre>".html_safe!
|
||||
rescue Exception => e # errors from Marshal or YAML
|
||||
# Object couldn't be dumped, perhaps because of singleton methods -- this is the fallback
|
||||
"<code class='debug_dump'>#{h(object.inspect)}</code>"
|
||||
"<code class='debug_dump'>#{h(object.inspect)}</code>".html_safe!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -55,6 +55,9 @@ module ActionView
|
||||
# * Any other key creates standard HTML attributes for the tag.
|
||||
#
|
||||
# ==== Examples
|
||||
# select_tag "people", options_from_collection_for_select(@people, "name", "id")
|
||||
# # <select id="people" name="people"><option value="1">David</option></select>
|
||||
#
|
||||
# select_tag "people", "<option>David</option>"
|
||||
# # => <select id="people" name="people"><option>David</option></select>
|
||||
#
|
||||
|
||||
@@ -22,7 +22,7 @@ module ActionView
|
||||
#
|
||||
# Custom Use (only the mentioned tags and attributes are allowed, nothing else)
|
||||
#
|
||||
# <%= sanitize @article.body, :tags => %w(table tr td), :attributes => %w(id class style)
|
||||
# <%= sanitize @article.body, :tags => %w(table tr td), :attributes => %w(id class style) %>
|
||||
#
|
||||
# Add table tags to the default allowed tags
|
||||
#
|
||||
|
||||
@@ -226,8 +226,7 @@ module ActionView
|
||||
# Returns the text with all the Textile[http://www.textism.com/tools/textile] codes turned into HTML tags.
|
||||
#
|
||||
# You can learn more about Textile's syntax at its website[http://www.textism.com/tools/textile].
|
||||
# <i>This method is only available if RedCloth[http://whytheluckystiff.net/ruby/redcloth/]
|
||||
# is available</i>.
|
||||
# <i>This method is only available if RedCloth[http://redcloth.org/] is available</i>.
|
||||
#
|
||||
# ==== Examples
|
||||
# textilize("*This is Textile!* Rejoice!")
|
||||
@@ -263,8 +262,7 @@ module ActionView
|
||||
# but without the bounding <p> tag that RedCloth adds.
|
||||
#
|
||||
# You can learn more about Textile's syntax at its website[http://www.textism.com/tools/textile].
|
||||
# <i>This method is requires RedCloth[http://whytheluckystiff.net/ruby/redcloth/]
|
||||
# to be available</i>.
|
||||
# <i>This method is only available if RedCloth[http://redcloth.org/] is available</i>.
|
||||
#
|
||||
# ==== Examples
|
||||
# textilize_without_paragraph("*This is Textile!* Rejoice!")
|
||||
|
||||
@@ -92,6 +92,7 @@ class ActionController::IntegrationTest < ActiveSupport::TestCase
|
||||
middleware.use "ActionDispatch::ShowExceptions"
|
||||
middleware.use "ActionDispatch::Callbacks"
|
||||
middleware.use "ActionDispatch::ParamsParser"
|
||||
middleware.use "ActionDispatch::Cookies"
|
||||
middleware.use "ActionDispatch::Flash"
|
||||
middleware.use "ActionDispatch::Head"
|
||||
}.build(routes || ActionController::Routing::Routes)
|
||||
|
||||
@@ -37,8 +37,8 @@ module ControllerRuntimeSubscriberTest
|
||||
get :show
|
||||
wait
|
||||
|
||||
assert_equal 2, @logger.logged(:info).size
|
||||
assert_match /\(Views: [\d\.]+ms | ActiveRecord: [\d\.]+ms\)/, @logger.logged(:info)[1]
|
||||
assert_equal 1, @logger.logged(:info).size
|
||||
assert_match /\(Views: [\d\.]+ms | ActiveRecord: [\d\.]+ms\)/, @logger.logged(:info)[0]
|
||||
end
|
||||
|
||||
class SyncSubscriberTest < ActionController::TestCase
|
||||
|
||||
@@ -54,12 +54,12 @@ class CookieTest < ActionController::TestCase
|
||||
cookies.permanent[:user_name] = "Jamie"
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
||||
def set_signed_cookie
|
||||
cookies.signed[:user_id] = 45
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
||||
def set_permanent_signed_cookie
|
||||
cookies.permanent.signed[:remember_me] = 100
|
||||
head :ok
|
||||
@@ -120,28 +120,6 @@ class CookieTest < ActionController::TestCase
|
||||
assert_equal({"user_name" => nil}, @response.cookies)
|
||||
end
|
||||
|
||||
def test_cookiejar_accessor
|
||||
@request.cookies["user_name"] = "david"
|
||||
@controller.request = @request
|
||||
jar = ActionController::CookieJar.build(@controller.request, @controller.response)
|
||||
assert_equal "david", jar["user_name"]
|
||||
assert_equal nil, jar["something_else"]
|
||||
end
|
||||
|
||||
def test_cookiejar_accessor_with_array_value
|
||||
@request.cookies["pages"] = %w{1 2 3}
|
||||
@controller.request = @request
|
||||
jar = ActionController::CookieJar.build(@controller.request, @controller.response)
|
||||
assert_equal %w{1 2 3}, jar["pages"]
|
||||
end
|
||||
|
||||
def test_cookiejar_delete_removes_item_and_returns_its_value
|
||||
@request.cookies["user_name"] = "david"
|
||||
@controller.response = @response
|
||||
jar = ActionController::CookieJar.build(@controller.request, @controller.response)
|
||||
assert_equal "david", jar.delete("user_name")
|
||||
end
|
||||
|
||||
def test_delete_cookie_with_path
|
||||
get :delete_cookie_with_path
|
||||
assert_cookie_header "user_name=; path=/beaten; expires=Thu, 01-Jan-1970 00:00:00 GMT"
|
||||
@@ -157,19 +135,24 @@ class CookieTest < ActionController::TestCase
|
||||
assert_match /Jamie/, @response.headers["Set-Cookie"]
|
||||
assert_match %r(#{20.years.from_now.utc.year}), @response.headers["Set-Cookie"]
|
||||
end
|
||||
|
||||
|
||||
def test_signed_cookie
|
||||
get :set_signed_cookie
|
||||
assert_equal 45, @controller.send(:cookies).signed[:user_id]
|
||||
end
|
||||
|
||||
|
||||
def test_accessing_nonexistant_signed_cookie_should_not_raise_an_invalid_signature
|
||||
get :set_signed_cookie
|
||||
assert_nil @controller.send(:cookies).signed[:non_existant_attribute]
|
||||
end
|
||||
|
||||
def test_permanent_signed_cookie
|
||||
get :set_permanent_signed_cookie
|
||||
assert_match %r(#{20.years.from_now.utc.year}), @response.headers["Set-Cookie"]
|
||||
assert_equal 100, @controller.send(:cookies).signed[:remember_me]
|
||||
end
|
||||
|
||||
|
||||
|
||||
private
|
||||
def assert_cookie_header(expected)
|
||||
header = @response.headers["Set-Cookie"]
|
||||
|
||||
@@ -9,23 +9,6 @@ end
|
||||
class FilterParamTest < ActionController::TestCase
|
||||
tests FilterParamController
|
||||
|
||||
class MockLogger
|
||||
attr_reader :logged
|
||||
attr_accessor :level
|
||||
|
||||
def initialize
|
||||
@level = Logger::DEBUG
|
||||
end
|
||||
|
||||
def method_missing(method, *args)
|
||||
@logged ||= []
|
||||
@logged << args.first unless block_given?
|
||||
@logged << yield if block_given?
|
||||
end
|
||||
end
|
||||
|
||||
setup :set_logger
|
||||
|
||||
def test_filter_parameters_must_have_one_word
|
||||
assert_raises RuntimeError do
|
||||
FilterParamController.filter_parameter_logging
|
||||
@@ -65,14 +48,4 @@ class FilterParamTest < ActionController::TestCase
|
||||
assert !FilterParamController.action_methods.include?('filter_parameters')
|
||||
assert_raise(NoMethodError) { @controller.filter_parameters([{'password' => '[FILTERED]'}]) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_logger
|
||||
@controller.logger = MockLogger.new
|
||||
end
|
||||
|
||||
def logs
|
||||
@logs ||= @controller.logger.logged.compact.map {|l| l.to_s.strip}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -220,7 +220,7 @@ class FlashIntegrationTest < ActionController::IntegrationTest
|
||||
def with_test_route_set
|
||||
with_routing do |set|
|
||||
set.draw do |map|
|
||||
match ':action', :to => ActionDispatch::Session::CookieStore.new(TestController, :key => SessionKey, :secret => SessionSecret)
|
||||
match ':action', :to => ActionDispatch::Session::CookieStore.new(FlashIntegrationTest::TestController, :key => FlashIntegrationTest::SessionKey, :secret => FlashIntegrationTest::SessionSecret)
|
||||
end
|
||||
yield
|
||||
end
|
||||
|
||||
@@ -3,6 +3,8 @@ require 'abstract_unit'
|
||||
# Tests the controller dispatching happy path
|
||||
module Dispatching
|
||||
class SimpleController < ActionController::Base
|
||||
before_filter :authenticate
|
||||
|
||||
def index
|
||||
render :text => "success"
|
||||
end
|
||||
@@ -12,12 +14,20 @@ module Dispatching
|
||||
end
|
||||
|
||||
def modify_response_body_twice
|
||||
ret = (self.response_body = "success")
|
||||
ret = (self.response_body = "success")
|
||||
self.response_body = "#{ret}!"
|
||||
end
|
||||
|
||||
def modify_response_headers
|
||||
end
|
||||
|
||||
def show_actions
|
||||
render :text => "actions: #{action_methods.to_a.join(', ')}"
|
||||
end
|
||||
|
||||
protected
|
||||
def authenticate
|
||||
end
|
||||
end
|
||||
|
||||
class EmptyController < ActionController::Base ; end
|
||||
@@ -64,5 +74,21 @@ module Dispatching
|
||||
assert_equal 'empty', EmptyController.controller_name
|
||||
assert_equal 'contained_empty', Submodule::ContainedEmptyController.controller_name
|
||||
end
|
||||
|
||||
test "action methods" do
|
||||
assert_equal Set.new(%w(
|
||||
modify_response_headers
|
||||
modify_response_body_twice
|
||||
index
|
||||
modify_response_body
|
||||
show_actions
|
||||
)), SimpleController.action_methods
|
||||
|
||||
assert_equal Set.new, EmptyController.action_methods
|
||||
assert_equal Set.new, Submodule::ContainedEmptyController.action_methods
|
||||
|
||||
get "/dispatching/simple/show_actions"
|
||||
assert_body "actions: modify_response_headers, modify_response_body_twice, index, modify_response_body, show_actions"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -66,15 +66,10 @@ module ActionControllerSubscriberTest
|
||||
def test_process_action
|
||||
get :show
|
||||
wait
|
||||
assert_equal 2, logs.size
|
||||
assert_match /Processed\sAnother::SubscribersController#show/, logs[0]
|
||||
end
|
||||
|
||||
def test_process_action_formats
|
||||
get :show
|
||||
wait
|
||||
assert_equal 2, logs.size
|
||||
assert_match /text\/html/, logs[0]
|
||||
assert_equal 1, logs.size
|
||||
assert_match /Completed/, logs.first
|
||||
assert_match /\[200\]/, logs.first
|
||||
assert_match /Another::SubscribersController#show/, logs.first
|
||||
end
|
||||
|
||||
def test_process_action_without_parameters
|
||||
@@ -87,23 +82,14 @@ module ActionControllerSubscriberTest
|
||||
get :show, :id => '10'
|
||||
wait
|
||||
|
||||
assert_equal 3, logs.size
|
||||
assert_equal 'Parameters: {"id"=>"10"}', logs[1]
|
||||
assert_equal 2, logs.size
|
||||
assert_equal 'Parameters: {"id"=>"10"}', logs[0]
|
||||
end
|
||||
|
||||
def test_process_action_with_view_runtime
|
||||
get :show
|
||||
wait
|
||||
assert_match /\(Views: [\d\.]+ms\)/, logs[1]
|
||||
end
|
||||
|
||||
def test_process_action_with_status_and_request_uri
|
||||
get :show
|
||||
wait
|
||||
last = logs.last
|
||||
assert_match /Completed/, last
|
||||
assert_match /200/, last
|
||||
assert_match /another\/subscribers\/show/, last
|
||||
assert_match /\(Views: [\d\.]+ms\)/, logs[0]
|
||||
end
|
||||
|
||||
def test_process_action_with_filter_parameters
|
||||
@@ -112,7 +98,7 @@ module ActionControllerSubscriberTest
|
||||
get :show, :lifo => 'Pratik', :amount => '420', :step => '1'
|
||||
wait
|
||||
|
||||
params = logs[1]
|
||||
params = logs[0]
|
||||
assert_match /"amount"=>"\[FILTERED\]"/, params
|
||||
assert_match /"lifo"=>"\[FILTERED\]"/, params
|
||||
assert_match /"step"=>"1"/, params
|
||||
@@ -122,7 +108,7 @@ module ActionControllerSubscriberTest
|
||||
get :redirector
|
||||
wait
|
||||
|
||||
assert_equal 3, logs.size
|
||||
assert_equal 2, logs.size
|
||||
assert_equal "Redirected to http://foo.bar/", logs[0]
|
||||
end
|
||||
|
||||
@@ -130,7 +116,7 @@ module ActionControllerSubscriberTest
|
||||
get :data_sender
|
||||
wait
|
||||
|
||||
assert_equal 3, logs.size
|
||||
assert_equal 2, logs.size
|
||||
assert_match /Sent data omg\.txt/, logs[0]
|
||||
end
|
||||
|
||||
@@ -138,7 +124,7 @@ module ActionControllerSubscriberTest
|
||||
get :file_sender
|
||||
wait
|
||||
|
||||
assert_equal 3, logs.size
|
||||
assert_equal 2, logs.size
|
||||
assert_match /Sent file/, logs[0]
|
||||
assert_match /test\/fixtures\/company\.rb/, logs[0]
|
||||
end
|
||||
@@ -147,7 +133,7 @@ module ActionControllerSubscriberTest
|
||||
get :xfile_sender
|
||||
wait
|
||||
|
||||
assert_equal 3, logs.size
|
||||
assert_equal 2, logs.size
|
||||
assert_match /Sent X\-Sendfile header/, logs[0]
|
||||
assert_match /test\/fixtures\/company\.rb/, logs[0]
|
||||
end
|
||||
@@ -157,7 +143,7 @@ module ActionControllerSubscriberTest
|
||||
get :with_fragment_cache
|
||||
wait
|
||||
|
||||
assert_equal 4, logs.size
|
||||
assert_equal 3, logs.size
|
||||
assert_match /Exist fragment\? views\/foo/, logs[0]
|
||||
assert_match /Write fragment views\/foo/, logs[1]
|
||||
ensure
|
||||
@@ -169,7 +155,7 @@ module ActionControllerSubscriberTest
|
||||
get :with_page_cache
|
||||
wait
|
||||
|
||||
assert_equal 3, logs.size
|
||||
assert_equal 2, logs.size
|
||||
assert_match /Write page/, logs[0]
|
||||
assert_match /\/index\.html/, logs[0]
|
||||
ensure
|
||||
|
||||
@@ -85,18 +85,6 @@ class DispatcherTest < Test::Unit::TestCase
|
||||
assert_equal 4, Foo.b
|
||||
end
|
||||
|
||||
def test_should_send_an_instrumentation_callback_for_async_processing
|
||||
ActiveSupport::Notifications.expects(:instrument).with("action_dispatch.callback")
|
||||
dispatch
|
||||
end
|
||||
|
||||
def test_should_send_an_instrumentation_callback_for_async_processing_even_on_failure
|
||||
ActiveSupport::Notifications.notifier.expects(:publish)
|
||||
assert_raise RuntimeError do
|
||||
dispatch { |env| raise "OMG" }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def dispatch(cache_classes = true, &block)
|
||||
|
||||
@@ -13,8 +13,7 @@ class ResponseTest < ActiveSupport::TestCase
|
||||
assert_equal({
|
||||
"Content-Type" => "text/html; charset=utf-8",
|
||||
"Cache-Control" => "max-age=0, private, must-revalidate",
|
||||
"ETag" => '"65a8e27d8879283831b664bd8b7f0ad4"',
|
||||
"Set-Cookie" => ""
|
||||
"ETag" => '"65a8e27d8879283831b664bd8b7f0ad4"'
|
||||
}, headers)
|
||||
|
||||
parts = []
|
||||
@@ -30,8 +29,7 @@ class ResponseTest < ActiveSupport::TestCase
|
||||
assert_equal({
|
||||
"Content-Type" => "text/html; charset=utf-8",
|
||||
"Cache-Control" => "max-age=0, private, must-revalidate",
|
||||
"ETag" => '"ebb5e89e8a94e9dd22abf5d915d112b2"',
|
||||
"Set-Cookie" => ""
|
||||
"ETag" => '"ebb5e89e8a94e9dd22abf5d915d112b2"'
|
||||
}, headers)
|
||||
end
|
||||
|
||||
@@ -44,8 +42,7 @@ class ResponseTest < ActiveSupport::TestCase
|
||||
assert_equal 200, status
|
||||
assert_equal({
|
||||
"Content-Type" => "text/html; charset=utf-8",
|
||||
"Cache-Control" => "no-cache",
|
||||
"Set-Cookie" => ""
|
||||
"Cache-Control" => "no-cache"
|
||||
}, headers)
|
||||
|
||||
parts = []
|
||||
|
||||
@@ -65,7 +65,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
|
||||
|
||||
resources :companies do
|
||||
resources :people
|
||||
resource :avatar
|
||||
resource :avatar, :controller => :avatar
|
||||
end
|
||||
|
||||
resources :images do
|
||||
@@ -294,34 +294,34 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
|
||||
def test_projects
|
||||
with_test_routes do
|
||||
get '/projects'
|
||||
assert_equal 'projects#index', @response.body
|
||||
assert_equal 'project#index', @response.body
|
||||
assert_equal '/projects', projects_path
|
||||
|
||||
post '/projects'
|
||||
assert_equal 'projects#create', @response.body
|
||||
assert_equal 'project#create', @response.body
|
||||
|
||||
get '/projects.xml'
|
||||
assert_equal 'projects#index', @response.body
|
||||
assert_equal 'project#index', @response.body
|
||||
assert_equal '/projects.xml', projects_path(:format => 'xml')
|
||||
|
||||
get '/projects/new'
|
||||
assert_equal 'projects#new', @response.body
|
||||
assert_equal 'project#new', @response.body
|
||||
assert_equal '/projects/new', new_project_path
|
||||
|
||||
get '/projects/new.xml'
|
||||
assert_equal 'projects#new', @response.body
|
||||
assert_equal 'project#new', @response.body
|
||||
assert_equal '/projects/new.xml', new_project_path(:format => 'xml')
|
||||
|
||||
get '/projects/1'
|
||||
assert_equal 'projects#show', @response.body
|
||||
assert_equal 'project#show', @response.body
|
||||
assert_equal '/projects/1', project_path(:id => '1')
|
||||
|
||||
get '/projects/1.xml'
|
||||
assert_equal 'projects#show', @response.body
|
||||
assert_equal 'project#show', @response.body
|
||||
assert_equal '/projects/1.xml', project_path(:id => '1', :format => 'xml')
|
||||
|
||||
get '/projects/1/edit'
|
||||
assert_equal 'projects#edit', @response.body
|
||||
assert_equal 'project#edit', @response.body
|
||||
assert_equal '/projects/1/edit', edit_project_path(:id => '1')
|
||||
end
|
||||
end
|
||||
@@ -383,7 +383,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
|
||||
assert_equal '/projects/1/companies/1/people', project_company_people_path(:project_id => '1', :company_id => '1')
|
||||
|
||||
get '/projects/1/companies/1/avatar'
|
||||
assert_equal 'avatars#show', @response.body
|
||||
assert_equal 'avatar#show', @response.body
|
||||
assert_equal '/projects/1/companies/1/avatar', project_company_avatar_path(:project_id => '1', :company_id => '1')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -137,7 +137,7 @@ class CookieStoreTest < ActionController::IntegrationTest
|
||||
with_test_route_set do
|
||||
get '/no_session_access'
|
||||
assert_response :success
|
||||
assert_equal "", headers['Set-Cookie']
|
||||
assert_equal nil, headers['Set-Cookie']
|
||||
end
|
||||
end
|
||||
|
||||
@@ -147,7 +147,7 @@ class CookieStoreTest < ActionController::IntegrationTest
|
||||
"fef868465920f415f2c0652d6910d3af288a0367"
|
||||
get '/no_session_access'
|
||||
assert_response :success
|
||||
assert_equal "", headers['Set-Cookie']
|
||||
assert_equal nil, headers['Set-Cookie']
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -53,19 +53,21 @@ class ShowExceptionsTest < ActionController::IntegrationTest
|
||||
|
||||
test "rescue locally from a local request" do
|
||||
@app = ProductionApp
|
||||
self.remote_addr = '127.0.0.1'
|
||||
['127.0.0.1', '::1'].each do |ip_address|
|
||||
self.remote_addr = ip_address
|
||||
|
||||
get "/"
|
||||
assert_response 500
|
||||
assert_match /puke/, body
|
||||
get "/"
|
||||
assert_response 500
|
||||
assert_match /puke/, body
|
||||
|
||||
get "/not_found"
|
||||
assert_response 404
|
||||
assert_match /#{ActionController::UnknownAction.name}/, body
|
||||
get "/not_found"
|
||||
assert_response 404
|
||||
assert_match /#{ActionController::UnknownAction.name}/, body
|
||||
|
||||
get "/method_not_allowed"
|
||||
assert_response 405
|
||||
assert_match /ActionController::MethodNotAllowed/, body
|
||||
get "/method_not_allowed"
|
||||
assert_response 405
|
||||
assert_match /ActionController::MethodNotAllowed/, body
|
||||
end
|
||||
end
|
||||
|
||||
test "localize public rescue message" do
|
||||
@@ -104,27 +106,4 @@ class ShowExceptionsTest < ActionController::IntegrationTest
|
||||
assert_response 405
|
||||
assert_match /ActionController::MethodNotAllowed/, body
|
||||
end
|
||||
|
||||
test "publishes notifications" do
|
||||
# Wait pending notifications to be published
|
||||
ActiveSupport::Notifications.notifier.wait
|
||||
|
||||
@app, event = ProductionApp, nil
|
||||
self.remote_addr = '127.0.0.1'
|
||||
|
||||
ActiveSupport::Notifications.subscribe('action_dispatch.show_exception') do |*args|
|
||||
event = args
|
||||
end
|
||||
|
||||
get "/"
|
||||
assert_response 500
|
||||
assert_match /puke/, body
|
||||
|
||||
ActiveSupport::Notifications.notifier.wait
|
||||
|
||||
assert_equal 'action_dispatch.show_exception', event.first
|
||||
assert_kind_of Hash, event.last[:env]
|
||||
assert_equal 'GET', event.last[:env]["REQUEST_METHOD"]
|
||||
assert_kind_of RuntimeError, event.last[:exception]
|
||||
end
|
||||
end
|
||||
|
||||
112
actionpack/test/dispatch/subscriber_test.rb
Normal file
112
actionpack/test/dispatch/subscriber_test.rb
Normal file
@@ -0,0 +1,112 @@
|
||||
require "abstract_unit"
|
||||
require "rails/subscriber/test_helper"
|
||||
require "action_dispatch/railties/subscriber"
|
||||
|
||||
module DispatcherSubscriberTest
|
||||
Boomer = lambda do |env|
|
||||
req = ActionDispatch::Request.new(env)
|
||||
case req.path
|
||||
when "/"
|
||||
[200, {}, []]
|
||||
else
|
||||
raise "puke!"
|
||||
end
|
||||
end
|
||||
|
||||
App = ActionDispatch::Notifications.new(Boomer)
|
||||
|
||||
def setup
|
||||
Rails::Subscriber.add(:action_dispatch, ActionDispatch::Railties::Subscriber.new)
|
||||
@app = App
|
||||
super
|
||||
|
||||
@events = []
|
||||
ActiveSupport::Notifications.subscribe do |*args|
|
||||
@events << args
|
||||
end
|
||||
end
|
||||
|
||||
def set_logger(logger)
|
||||
ActionController::Base.logger = logger
|
||||
end
|
||||
|
||||
def test_publishes_notifications
|
||||
get "/"
|
||||
wait
|
||||
|
||||
assert_equal 2, @events.size
|
||||
before, after = @events
|
||||
|
||||
assert_equal 'action_dispatch.before_dispatch', before[0]
|
||||
assert_kind_of Hash, before[4][:env]
|
||||
assert_equal 'GET', before[4][:env]["REQUEST_METHOD"]
|
||||
|
||||
assert_equal 'action_dispatch.after_dispatch', after[0]
|
||||
assert_kind_of Hash, after[4][:env]
|
||||
assert_equal 'GET', after[4][:env]["REQUEST_METHOD"]
|
||||
end
|
||||
|
||||
def test_publishes_notifications_even_on_failures
|
||||
begin
|
||||
get "/puke"
|
||||
rescue
|
||||
end
|
||||
|
||||
wait
|
||||
|
||||
assert_equal 3, @events.size
|
||||
before, after, exception = @events
|
||||
|
||||
assert_equal 'action_dispatch.before_dispatch', before[0]
|
||||
assert_kind_of Hash, before[4][:env]
|
||||
assert_equal 'GET', before[4][:env]["REQUEST_METHOD"]
|
||||
|
||||
assert_equal 'action_dispatch.after_dispatch', after[0]
|
||||
assert_kind_of Hash, after[4][:env]
|
||||
assert_equal 'GET', after[4][:env]["REQUEST_METHOD"]
|
||||
|
||||
assert_equal 'action_dispatch.exception', exception[0]
|
||||
assert_kind_of Hash, exception[4][:env]
|
||||
assert_equal 'GET', exception[4][:env]["REQUEST_METHOD"]
|
||||
assert_kind_of RuntimeError, exception[4][:exception]
|
||||
end
|
||||
|
||||
def test_subscriber_logs_notifications
|
||||
get "/"
|
||||
wait
|
||||
|
||||
log = @logger.logged(:info).first
|
||||
assert_equal 1, @logger.logged(:info).size
|
||||
|
||||
assert_match %r{^Processing "/" to text/html}, log
|
||||
assert_match %r{\(for 127\.0\.0\.1}, log
|
||||
assert_match %r{\[GET\]}, log
|
||||
end
|
||||
|
||||
def test_subscriber_has_its_logged_flushed_after_request
|
||||
assert_equal 0, @logger.flush_count
|
||||
get "/"
|
||||
wait
|
||||
assert_equal 1, @logger.flush_count
|
||||
end
|
||||
|
||||
def test_subscriber_has_its_logged_flushed_even_after_busted_requests
|
||||
assert_equal 0, @logger.flush_count
|
||||
begin
|
||||
get "/puke"
|
||||
rescue
|
||||
end
|
||||
wait
|
||||
assert_equal 1, @logger.flush_count
|
||||
end
|
||||
|
||||
class SyncSubscriberTest < ActionController::IntegrationTest
|
||||
include Rails::Subscriber::SyncTestHelper
|
||||
include DispatcherSubscriberTest
|
||||
end
|
||||
|
||||
class AsyncSubscriberTest < ActionController::IntegrationTest
|
||||
include Rails::Subscriber::AsyncTestHelper
|
||||
include DispatcherSubscriberTest
|
||||
end
|
||||
end
|
||||
2
actionpack/test/fixtures/reply.rb
vendored
2
actionpack/test/fixtures/reply.rb
vendored
@@ -1,5 +1,5 @@
|
||||
class Reply < ActiveRecord::Base
|
||||
named_scope :base
|
||||
scope :base
|
||||
belongs_to :topic, :include => [:replies]
|
||||
belongs_to :developer
|
||||
|
||||
|
||||
@@ -1,21 +1,57 @@
|
||||
Active Model
|
||||
==============
|
||||
= Active Model - defined interfaces for Rails
|
||||
|
||||
Totally experimental library that aims to extract common model mixins from
|
||||
ActiveRecord for use in ActiveResource (and other similar libraries).
|
||||
This is in a very rough state (no autotest or spec rake tasks set up yet),
|
||||
so please excuse the mess.
|
||||
Prior to Rails 3.0, if a plugin or gem developer wanted to be able to have
|
||||
an object interact with Action Pack helpers, it was required to either
|
||||
copy chunks of code from Rails, or monkey patch entire helpers to make them
|
||||
handle objects that did not look like Active Record. This generated code
|
||||
duplication and fragile applications that broke on upgrades.
|
||||
|
||||
Here's what I plan to extract:
|
||||
* ActiveModel::Observing
|
||||
* ActiveModel::Callbacks
|
||||
* ActiveModel::Validations
|
||||
Active Model is a solution for this problem.
|
||||
|
||||
# for ActiveResource params and ActiveRecord options
|
||||
* ActiveModel::Scoping
|
||||
Active Model provides a known set of interfaces that your objects can implement
|
||||
to then present a common interface to the Action Pack helpers. You can include
|
||||
functionality from the following modules:
|
||||
|
||||
# to_json, to_xml, etc
|
||||
* ActiveModel::Serialization
|
||||
* Adding callbacks to your class
|
||||
|
||||
class MyClass
|
||||
extend ActiveModel::Callbacks
|
||||
define_model_callbacks :create
|
||||
|
||||
def create
|
||||
_run_create_callbacks do
|
||||
# Your create action methods here
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
...gives you before_create, around_create and after_create class methods that
|
||||
wrap your create method.
|
||||
|
||||
{Learn more}[link:classes/ActiveModel/CallBacks.html]
|
||||
|
||||
* For classes that already look like an Active Record object
|
||||
|
||||
class MyClass
|
||||
include ActiveModel::Conversion
|
||||
end
|
||||
|
||||
...returns the class itself when sent :to_model
|
||||
|
||||
* Tracking changes in your object
|
||||
|
||||
Provides all the value tracking features implemented by ActiveRecord...
|
||||
|
||||
person = Person.new
|
||||
person.name # => nil
|
||||
person.changed? # => false
|
||||
person.name = 'bob'
|
||||
person.changed? # => true
|
||||
person.changed # => ['name']
|
||||
person.changes # => { 'name' => [nil, 'bob'] }
|
||||
person.name = 'robert'
|
||||
person.save
|
||||
person.previous_changes # => {'name' => ['bob, 'robert']}
|
||||
|
||||
{Learn more}[link:classes/ActiveModel/Dirty.html]
|
||||
|
||||
I'm trying to keep ActiveRecord compatibility where possible, but I'm
|
||||
annotating the spots where I'm diverging a bit.
|
||||
@@ -1,6 +1,52 @@
|
||||
require 'active_support/callbacks'
|
||||
|
||||
module ActiveModel
|
||||
# == Active Model Callbacks
|
||||
#
|
||||
# Provides an interface for any class to have Active Record like callbacks.
|
||||
#
|
||||
# Like the Active Record methods, the call back chain is aborted as soon as
|
||||
# one of the methods in the chain returns false.
|
||||
#
|
||||
# First, extend ActiveModel::Callbacks from the class you are creating:
|
||||
#
|
||||
# class MyModel
|
||||
# extend ActiveModel::Callbacks
|
||||
# end
|
||||
#
|
||||
# Then define a list of methods that you want call backs attached to:
|
||||
#
|
||||
# define_model_callbacks :create, :update
|
||||
#
|
||||
# This will provide all three standard callbacks (before, around and after) around
|
||||
# both the :create and :update methods. To implement, you need to wrap the methods
|
||||
# you want call backs on in a block so that the call backs get a chance to fire:
|
||||
#
|
||||
# def create
|
||||
# _run_create_callbacks do
|
||||
# # Your create action methods here
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# The _run_<method_name>_callbacks methods are dynamically created when you extend
|
||||
# the <tt>ActiveModel::Callbacks</tt> module.
|
||||
#
|
||||
# Then in your class, you can use the +before_create+, +after_create+ and +around_create+
|
||||
# methods, just as you would in an Active Record module.
|
||||
#
|
||||
# before_create :action_before_create
|
||||
#
|
||||
# def action_before_create
|
||||
# # Your code here
|
||||
# end
|
||||
#
|
||||
# You can choose not to have all three callbacks by passing an hash to the
|
||||
# define_model_callbacks method.
|
||||
#
|
||||
# define_model_callbacks :create, :only => :after, :before
|
||||
#
|
||||
# Would only create the after_create and before_create callback methods in your
|
||||
# class.
|
||||
module Callbacks
|
||||
def self.extended(base)
|
||||
base.class_eval do
|
||||
@@ -8,43 +54,39 @@ module ActiveModel
|
||||
end
|
||||
end
|
||||
|
||||
# Define callbacks similar to ActiveRecord ones. It means:
|
||||
#
|
||||
# * The callback chain is aborted whenever the block given to
|
||||
# _run_callbacks returns false.
|
||||
#
|
||||
# * If a class is given to the fallback, it will search for
|
||||
# before_create, around_create and after_create methods.
|
||||
#
|
||||
# == Usage
|
||||
#
|
||||
# First you need to define which callbacks your model will have:
|
||||
#
|
||||
# class MyModel
|
||||
# define_model_callbacks :create
|
||||
# end
|
||||
#
|
||||
# This will define three class methods: before_create, around_create,
|
||||
# and after_create. They accept a symbol, a string, an object or a block.
|
||||
#
|
||||
# After you create a callback, you need to tell when they are executed.
|
||||
# For example, you could do:
|
||||
#
|
||||
# def create
|
||||
# _run_create_callbacks do
|
||||
# super
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# == Options
|
||||
#
|
||||
# define_model_callbacks accepts all options define_callbacks does, in
|
||||
# case you want to overwrite a default. Besides that, it also accepts
|
||||
# an :only option, where you can choose if you want all types (before,
|
||||
# around or after) or just some:
|
||||
# define_model_callbacks accepts all options define_callbacks does, in case you
|
||||
# want to overwrite a default. Besides that, it also accepts an :only option,
|
||||
# where you can choose if you want all types (before, around or after) or just some.
|
||||
#
|
||||
# define_model_callbacks :initializer, :only => :after
|
||||
#
|
||||
#
|
||||
# Note, the <tt>:only => <type></tt> hash will apply to all callbacks defined on
|
||||
# that method call. To get around this you can call the define_model_callbacks
|
||||
# method as many times as you need.
|
||||
#
|
||||
# define_model_callbacks :create, :only => :after
|
||||
# define_model_callbacks :update, :only => :before
|
||||
# define_model_callbacks :destroy, :only => :around
|
||||
#
|
||||
# Would create +after_create+, +before_update+ and +around_destroy+ methods only.
|
||||
#
|
||||
# You can pass in a class to before_<type>, after_<type> and around_<type>, in which
|
||||
# case the call back will call that class's <action>_<type> method passing the object
|
||||
# that the callback is being called on.
|
||||
#
|
||||
# class MyModel
|
||||
# extend ActiveModel::Callbacks
|
||||
# define_model_callbacks :create
|
||||
#
|
||||
# before_create AnotherClass
|
||||
# end
|
||||
#
|
||||
# class AnotherClass
|
||||
# def self.before_create( obj )
|
||||
# # obj is the MyModel instance that the callback is being called on
|
||||
# end
|
||||
# end
|
||||
#
|
||||
def define_model_callbacks(*callbacks)
|
||||
options = callbacks.extract_options!
|
||||
options = { :terminator => "result == false", :scope => [:kind, :name] }.merge(options)
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
module ActiveModel
|
||||
# Include ActiveModel::Conversion if your object "acts like an ActiveModel model".
|
||||
# If your object is already designed to implement all of the Active Model featurs
|
||||
# include this module in your Class.
|
||||
#
|
||||
# class MyClass
|
||||
# include ActiveModel::Conversion
|
||||
# end
|
||||
#
|
||||
# Returns self to the <tt>:to_model</tt> method.
|
||||
#
|
||||
# If your model does not act like an Active Model object, then you should define
|
||||
# <tt>:to_model</tt> yourself returning a proxy object that wraps your object
|
||||
# with Active Model compliant methods.
|
||||
module Conversion
|
||||
def to_model
|
||||
self
|
||||
|
||||
@@ -1,5 +1,43 @@
|
||||
module ActiveModel
|
||||
# Track unsaved attribute changes.
|
||||
# <tt>ActiveModel::Dirty</tt> provides a way to track changes in your
|
||||
# object in the same way as ActiveRecord does.
|
||||
#
|
||||
# The requirements to implement ActiveModel::Dirty are:
|
||||
#
|
||||
# * <tt>include ActiveModel::Dirty</tt> in your object
|
||||
# * Call <tt>define_attribute_methods</tt> passing each method you want to track
|
||||
# * Call <tt>attr_name_will_change!</tt> before each change to the tracked attribute
|
||||
#
|
||||
# If you wish to also track previous changes on save or update, you need to add
|
||||
#
|
||||
# @previously_changed = changes
|
||||
#
|
||||
# inside of your save or update method.
|
||||
#
|
||||
# A minimal implementation could be:
|
||||
#
|
||||
# class Person
|
||||
#
|
||||
# include ActiveModel::Dirty
|
||||
#
|
||||
# define_attribute_methods [:name]
|
||||
#
|
||||
# def name
|
||||
# @name
|
||||
# end
|
||||
#
|
||||
# def name=(val)
|
||||
# name_will_change!
|
||||
# @name = val
|
||||
# end
|
||||
#
|
||||
# def save
|
||||
# @previously_changed = changes
|
||||
# end
|
||||
#
|
||||
# end
|
||||
#
|
||||
# == Examples:
|
||||
#
|
||||
# A newly instantiated object is unchanged:
|
||||
# person = Person.find_by_name('Uncle Bob')
|
||||
|
||||
@@ -60,9 +60,11 @@ module ActiveModel
|
||||
# error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>.
|
||||
# If no +messsage+ is supplied, :invalid is assumed.
|
||||
# If +message+ is a Symbol, it will be translated, using the appropriate scope (see translate_error).
|
||||
# If +message+ is a Proc, it will be called, allowing for things like Time.now to be used within an error
|
||||
def add(attribute, message = nil, options = {})
|
||||
message ||= :invalid
|
||||
message = generate_message(attribute, message, options) if message.is_a?(Symbol)
|
||||
message = message.call if message.is_a?(Proc)
|
||||
self[attribute] << message
|
||||
end
|
||||
|
||||
|
||||
@@ -57,13 +57,13 @@ module ActiveModel
|
||||
#
|
||||
# class Person < ActiveRecord::Base
|
||||
# validates_length_of :first_name, :maximum=>30
|
||||
# validates_length_of :last_name, :maximum=>30, :message=>"less than {{count}} if you don't mind"
|
||||
# validates_length_of :last_name, :maximum=>30, :message=>"less than 30 if you don't mind"
|
||||
# validates_length_of :fax, :in => 7..32, :allow_nil => true
|
||||
# validates_length_of :phone, :in => 7..32, :allow_blank => true
|
||||
# validates_length_of :user_name, :within => 6..20, :too_long => "pick a shorter name", :too_short => "pick a longer name"
|
||||
# validates_length_of :fav_bra_size, :minimum => 1, :too_short => "please enter at least {{count}} character"
|
||||
# validates_length_of :smurf_leader, :is => 4, :message => "papa is spelled with {{count}} characters... don't play me."
|
||||
# validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least {{count}} words."), :tokenizer => lambda {|str| str.scan(/\w+/) }
|
||||
# validates_length_of :zip_code, :minimum => 5, :too_short => "please enter at least 5 characters"
|
||||
# validates_length_of :smurf_leader, :is => 4, :message => "papa is spelled with 4 characters... don't play me."
|
||||
# validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least 100 words."), :tokenizer => lambda {|str| str.scan(/\w+/) }
|
||||
# end
|
||||
#
|
||||
# Configuration options:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
require 'active_support/core_ext/hash/slice'
|
||||
|
||||
module ActiveModel
|
||||
module Validations
|
||||
module ClassMethods
|
||||
|
||||
@@ -212,4 +212,12 @@ class ValidationsTest < ActiveModel::TestCase
|
||||
all_errors = t.errors.to_a
|
||||
assert_deprecated { assert_equal all_errors, t.errors.each_full{|err| err} }
|
||||
end
|
||||
|
||||
def test_validation_with_message_as_proc
|
||||
Topic.validates_presence_of(:title, :message => proc { "no blanks here".upcase })
|
||||
|
||||
t = Topic.new
|
||||
assert !t.valid?
|
||||
assert ["NO BLANKS HERE"], t.errors[:title]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
*Edge*
|
||||
|
||||
* Allow relations to be used as scope.
|
||||
|
||||
class Item
|
||||
scope :red, where(:colour => 'red')
|
||||
end
|
||||
|
||||
Item.red.limit(10) # Ten red items
|
||||
|
||||
* Rename named_scope to scope. [Pratik Naik]
|
||||
|
||||
* Changed ActiveRecord::Base.store_full_sti_class to be true by default reflecting the previously announced Rails 3 default [DHH]
|
||||
|
||||
* Add Relation#except. [Pratik Naik]
|
||||
|
||||
@@ -188,11 +188,11 @@ module ActiveRecord
|
||||
conditions << append_conditions(reflection, preload_options)
|
||||
|
||||
associated_records = reflection.klass.with_exclusive_scope do
|
||||
reflection.klass.find(:all, :conditions => [conditions, ids],
|
||||
:include => options[:include],
|
||||
:joins => "INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}",
|
||||
:select => "#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id",
|
||||
:order => options[:order])
|
||||
reflection.klass.where([conditions, ids]).
|
||||
includes(options[:include]).
|
||||
joins("INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}").
|
||||
select("#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id").
|
||||
order(options[:order]).to_a
|
||||
end
|
||||
set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'the_parent_record_id')
|
||||
end
|
||||
@@ -327,6 +327,7 @@ module ActiveRecord
|
||||
table_name = klass.quoted_table_name
|
||||
primary_key = klass.primary_key
|
||||
column_type = klass.columns.detect{|c| c.name == primary_key}.type
|
||||
|
||||
ids = id_map.keys.map do |id|
|
||||
if column_type == :integer
|
||||
id.to_i
|
||||
@@ -336,15 +337,14 @@ module ActiveRecord
|
||||
id
|
||||
end
|
||||
end
|
||||
|
||||
conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} #{in_or_equals_for_ids(ids)}"
|
||||
conditions << append_conditions(reflection, preload_options)
|
||||
|
||||
associated_records = klass.with_exclusive_scope do
|
||||
klass.find(:all, :conditions => [conditions, ids],
|
||||
:include => options[:include],
|
||||
:select => options[:select],
|
||||
:joins => options[:joins],
|
||||
:order => options[:order])
|
||||
klass.where([conditions, ids]).apply_finder_options(options.slice(:include, :select, :joins, :order)).to_a
|
||||
end
|
||||
|
||||
set_association_single_records(id_map, reflection.name, associated_records, primary_key)
|
||||
end
|
||||
end
|
||||
@@ -363,13 +363,12 @@ module ActiveRecord
|
||||
conditions << append_conditions(reflection, preload_options)
|
||||
|
||||
reflection.klass.with_exclusive_scope do
|
||||
reflection.klass.find(:all,
|
||||
:select => (preload_options[:select] || options[:select] || "#{table_name}.*"),
|
||||
:include => preload_options[:include] || options[:include],
|
||||
:conditions => [conditions, ids],
|
||||
:joins => options[:joins],
|
||||
:group => preload_options[:group] || options[:group],
|
||||
:order => preload_options[:order] || options[:order])
|
||||
reflection.klass.select(preload_options[:select] || options[:select] || "#{table_name}.*").
|
||||
includes(preload_options[:include] || options[:include]).
|
||||
where([conditions, ids]).
|
||||
joins(options[:joins]).
|
||||
group(preload_options[:group] || options[:group]).
|
||||
order(preload_options[:order] || options[:order])
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -774,13 +774,12 @@ module ActiveRecord
|
||||
# [collection.build(attributes = {}, ...)]
|
||||
# Returns one or more new objects of the collection type that have been instantiated
|
||||
# with +attributes+ and linked to this object through a foreign key, but have not yet
|
||||
# been saved. <b>Note:</b> This only works if an associated object already exists, not if
|
||||
# it's +nil+!
|
||||
# been saved.
|
||||
# [collection.create(attributes = {})]
|
||||
# Returns a new object of the collection type that has been instantiated
|
||||
# with +attributes+, linked to this object through a foreign key, and that has already
|
||||
# been saved (if it passed the validation). <b>Note:</b> This only works if an associated
|
||||
# object already exists, not if it's +nil+!
|
||||
# been saved (if it passed the validation). *Note*: This only works if the base model
|
||||
# already exists in the DB, not if it is a new (unsaved) record!
|
||||
#
|
||||
# (*Note*: +collection+ is replaced with the symbol passed as the first argument, so
|
||||
# <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.)
|
||||
@@ -1040,7 +1039,6 @@ module ActiveRecord
|
||||
# A Post class declares <tt>belongs_to :author</tt>, which will add:
|
||||
# * <tt>Post#author</tt> (similar to <tt>Author.find(author_id)</tt>)
|
||||
# * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
|
||||
# * <tt>Post#author?</tt> (similar to <tt>post.author == some_author</tt>)
|
||||
# * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>)
|
||||
# * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>)
|
||||
# The declaration can also include an options hash to specialize the behavior of the association.
|
||||
@@ -1703,30 +1701,19 @@ module ActiveRecord
|
||||
end
|
||||
|
||||
def construct_finder_arel_with_included_associations(options, join_dependency)
|
||||
relation = active_relation
|
||||
relation = scoped
|
||||
|
||||
for association in join_dependency.join_associations
|
||||
relation = association.join_relation(relation)
|
||||
end
|
||||
|
||||
relation = relation.joins(options[:joins]).
|
||||
select(column_aliases(join_dependency)).
|
||||
group(options[:group]).
|
||||
having(options[:having]).
|
||||
order(options[:order]).
|
||||
where(options[:conditions]).
|
||||
from(options[:from])
|
||||
relation = relation.apply_finder_options(options).select(column_aliases(join_dependency))
|
||||
|
||||
scoped_relation = current_scoped_methods
|
||||
scoped_relation_limit = scoped_relation.taken if scoped_relation
|
||||
|
||||
relation = current_scoped_methods.except(:limit).merge(relation) if current_scoped_methods
|
||||
|
||||
if !using_limitable_reflections?(join_dependency.reflections) && ((scoped_relation && scoped_relation.taken) || options[:limit])
|
||||
if !using_limitable_reflections?(join_dependency.reflections) && relation.limit_value
|
||||
relation = relation.where(construct_arel_limited_ids_condition(options, join_dependency))
|
||||
end
|
||||
|
||||
relation = relation.limit(options[:limit] || scoped_relation_limit) if using_limitable_reflections?(join_dependency.reflections)
|
||||
relation = relation.except(:limit, :offset) unless using_limitable_reflections?(join_dependency.reflections)
|
||||
|
||||
relation
|
||||
end
|
||||
@@ -1754,23 +1741,14 @@ module ActiveRecord
|
||||
end
|
||||
|
||||
def construct_finder_sql_for_association_limiting(options, join_dependency)
|
||||
relation = active_relation
|
||||
relation = scoped
|
||||
|
||||
for association in join_dependency.join_associations
|
||||
relation = association.join_relation(relation)
|
||||
end
|
||||
|
||||
relation = relation.joins(options[:joins]).
|
||||
where(options[:conditions]).
|
||||
group(options[:group]).
|
||||
having(options[:having]).
|
||||
order(options[:order]).
|
||||
limit(options[:limit]).
|
||||
offset(options[:offset]).
|
||||
from(options[:from])
|
||||
|
||||
relation = current_scoped_methods.except(:select, :includes, :eager_load).merge(relation) if current_scoped_methods
|
||||
relation = relation.select(connection.distinct("#{connection.quote_table_name table_name}.#{primary_key}", options[:order]))
|
||||
relation = relation.apply_finder_options(options).except(:select)
|
||||
relation = relation.select(connection.distinct("#{connection.quote_table_name table_name}.#{primary_key}", relation.order_values.join(", ")))
|
||||
|
||||
relation.to_sql
|
||||
end
|
||||
|
||||
@@ -403,8 +403,6 @@ module ActiveRecord
|
||||
else
|
||||
super
|
||||
end
|
||||
elsif @reflection.klass.scopes.include?(method)
|
||||
@reflection.klass.scopes[method].call(self, *args)
|
||||
else
|
||||
with_scope(construct_scope) do
|
||||
if block_given?
|
||||
|
||||
@@ -37,7 +37,7 @@ module ActiveRecord
|
||||
if force
|
||||
record.save!
|
||||
else
|
||||
return false unless record.save(validate)
|
||||
return false unless record.save(:validate => validate)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ module ActiveRecord
|
||||
|
||||
def insert_record(record, force = false, validate = true)
|
||||
set_belongs_to_association_for(record)
|
||||
force ? record.save! : record.save(validate)
|
||||
force ? record.save! : record.save(:validate => validate)
|
||||
end
|
||||
|
||||
# Deletes the records according to the <tt>:dependent</tt> option.
|
||||
|
||||
@@ -60,7 +60,7 @@ module ActiveRecord
|
||||
if force
|
||||
record.save!
|
||||
else
|
||||
return false unless record.save(validate)
|
||||
return false unless record.save(:validate => validate)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -116,14 +116,14 @@ module ActiveRecord
|
||||
# post = Post.find(1)
|
||||
# post.author.name = ''
|
||||
# post.save # => false
|
||||
# post.errors # => #<ActiveRecord::Errors:0x174498c @errors={"author_name"=>["can't be blank"]}, @base=#<Post ...>>
|
||||
# post.errors # => #<ActiveRecord::Errors:0x174498c @errors={"author.name"=>["can't be blank"]}, @base=#<Post ...>>
|
||||
#
|
||||
# No validations will be performed on the associated models when validations
|
||||
# are skipped for the parent:
|
||||
#
|
||||
# post = Post.find(1)
|
||||
# post.author.name = ''
|
||||
# post.save(false) # => true
|
||||
# post.save(:validate => false) # => true
|
||||
module AutosaveAssociation
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
@@ -302,7 +302,7 @@ module ActiveRecord
|
||||
association.send(:insert_record, record)
|
||||
end
|
||||
elsif autosave
|
||||
saved = record.save(false)
|
||||
saved = record.save(:validate => false)
|
||||
end
|
||||
|
||||
raise ActiveRecord::Rollback if saved == false
|
||||
@@ -332,7 +332,7 @@ module ActiveRecord
|
||||
key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
|
||||
if autosave != false && (new_record? || association.new_record? || association[reflection.primary_key_name] != key || autosave)
|
||||
association[reflection.primary_key_name] = key
|
||||
saved = association.save(!autosave)
|
||||
saved = association.save(:validate => !autosave)
|
||||
raise ActiveRecord::Rollback if !saved && autosave
|
||||
saved
|
||||
end
|
||||
@@ -355,7 +355,7 @@ module ActiveRecord
|
||||
if autosave && association.marked_for_destruction?
|
||||
association.destroy
|
||||
elsif autosave != false
|
||||
saved = association.save(!autosave) if association.new_record? || autosave
|
||||
saved = association.save(:validate => !autosave) if association.new_record? || autosave
|
||||
|
||||
if association.updated?
|
||||
association_id = association.send(reflection.options[:primary_key] || :id)
|
||||
|
||||
@@ -869,10 +869,9 @@ module ActiveRecord #:nodoc:
|
||||
# # Update all books that match our conditions, but limit it to 5 ordered by date
|
||||
# Book.update_all "author = 'David'", "title LIKE '%Rails%'", :order => 'created_at', :limit => 5
|
||||
def update_all(updates, conditions = nil, options = {})
|
||||
relation = active_relation
|
||||
relation = unscoped
|
||||
|
||||
relation = relation.where(conditions) if conditions
|
||||
relation = relation.where(type_condition) if finder_needs_type_condition?
|
||||
relation = relation.limit(options[:limit]) if options[:limit].present?
|
||||
relation = relation.order(options[:order]) if options[:order].present?
|
||||
|
||||
@@ -1389,7 +1388,7 @@ module ActiveRecord #:nodoc:
|
||||
def reset_column_information
|
||||
undefine_attribute_methods
|
||||
@column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @inheritance_column = nil
|
||||
@active_relation = @arel_engine = nil
|
||||
@arel_engine = @unscoped = @arel_table = nil
|
||||
end
|
||||
|
||||
def reset_column_information_and_inheritable_attributes_for_all_subclasses#:nodoc:
|
||||
@@ -1502,12 +1501,13 @@ module ActiveRecord #:nodoc:
|
||||
"(#{segments.join(') AND (')})" unless segments.empty?
|
||||
end
|
||||
|
||||
def active_relation
|
||||
@active_relation ||= Relation.new(self, arel_table)
|
||||
def unscoped
|
||||
@unscoped ||= Relation.new(self, arel_table)
|
||||
finder_needs_type_condition? ? @unscoped.where(type_condition) : @unscoped
|
||||
end
|
||||
|
||||
def arel_table(table_name_alias = nil)
|
||||
Arel::Table.new(table_name, :as => table_name_alias, :engine => arel_engine)
|
||||
def arel_table
|
||||
@arel_table ||= Arel::Table.new(table_name, :engine => arel_engine)
|
||||
end
|
||||
|
||||
def arel_engine
|
||||
@@ -1563,24 +1563,7 @@ module ActiveRecord #:nodoc:
|
||||
end
|
||||
|
||||
def construct_finder_arel(options = {}, scope = nil)
|
||||
validate_find_options(options)
|
||||
|
||||
relation = active_relation.
|
||||
joins(options[:joins]).
|
||||
where(options[:conditions]).
|
||||
select(options[:select]).
|
||||
group(options[:group]).
|
||||
having(options[:having]).
|
||||
order(options[:order]).
|
||||
limit(options[:limit]).
|
||||
offset(options[:offset]).
|
||||
from(options[:from]).
|
||||
includes(options[:include])
|
||||
|
||||
relation = relation.where(type_condition) if finder_needs_type_condition?
|
||||
relation = relation.lock(options[:lock]) if options[:lock].present?
|
||||
relation = relation.readonly(options[:readonly]) if options.has_key?(:readonly)
|
||||
|
||||
relation = unscoped.apply_finder_options(options)
|
||||
relation = scope.merge(relation) if scope
|
||||
relation
|
||||
end
|
||||
@@ -1600,14 +1583,9 @@ module ActiveRecord #:nodoc:
|
||||
end
|
||||
end
|
||||
|
||||
# Merges includes so that the result is a valid +include+
|
||||
def merge_includes(first, second)
|
||||
(Array.wrap(first) + Array.wrap(second)).uniq
|
||||
end
|
||||
|
||||
def build_association_joins(joins)
|
||||
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, joins, nil)
|
||||
relation = active_relation.table
|
||||
relation = unscoped.table
|
||||
join_dependency.join_associations.map { |association|
|
||||
if (association_relation = association.relation).is_a?(Array)
|
||||
[Arel::InnerJoin.new(relation, association_relation.first, *association.association_join.first).joins(relation),
|
||||
@@ -1746,10 +1724,10 @@ module ActiveRecord #:nodoc:
|
||||
# class Article < ActiveRecord::Base
|
||||
# def self.find_with_scope
|
||||
# with_scope(:find => { :conditions => "blog_id = 1", :limit => 1 }, :create => { :blog_id => 1 }) do
|
||||
# with_scope(:find => { :limit => 10 })
|
||||
# with_scope(:find => { :limit => 10 }) do
|
||||
# find(:all) # => SELECT * from articles WHERE blog_id = 1 LIMIT 10
|
||||
# end
|
||||
# with_scope(:find => { :conditions => "author_id = 3" })
|
||||
# with_scope(:find => { :conditions => "author_id = 3" }) do
|
||||
# find(:all) # => SELECT * from articles WHERE blog_id = 1 AND author_id = 3 LIMIT 1
|
||||
# end
|
||||
# end
|
||||
@@ -1781,11 +1759,6 @@ module ActiveRecord #:nodoc:
|
||||
end
|
||||
|
||||
method_scoping.assert_valid_keys([ :find, :create ])
|
||||
|
||||
if f = method_scoping[:find]
|
||||
f.assert_valid_keys(VALID_FIND_OPTIONS)
|
||||
end
|
||||
|
||||
relation = construct_finder_arel(method_scoping[:find] || {})
|
||||
|
||||
if current_scoped_methods && current_scoped_methods.create_with_value && method_scoping[:create]
|
||||
@@ -2047,13 +2020,6 @@ module ActiveRecord #:nodoc:
|
||||
end
|
||||
end
|
||||
|
||||
VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset,
|
||||
:order, :select, :readonly, :group, :having, :from, :lock ]
|
||||
|
||||
def validate_find_options(options) #:nodoc:
|
||||
options.assert_valid_keys(VALID_FIND_OPTIONS)
|
||||
end
|
||||
|
||||
def encode_quoted_value(value) #:nodoc:
|
||||
quoted_value = connection.quote(value)
|
||||
quoted_value = "'#{quoted_value[1..-2].gsub(/\'/, "\\\\'")}'" if quoted_value.include?("\\\'") # (for ruby mode) "
|
||||
@@ -2170,16 +2136,16 @@ module ActiveRecord #:nodoc:
|
||||
end
|
||||
|
||||
# :call-seq:
|
||||
# save(perform_validation = true)
|
||||
# save(options)
|
||||
#
|
||||
# Saves the model.
|
||||
#
|
||||
# If the model is new a record gets created in the database, otherwise
|
||||
# the existing record gets updated.
|
||||
#
|
||||
# If +perform_validation+ is true validations run. If any of them fail
|
||||
# the action is cancelled and +save+ returns +false+. If the flag is
|
||||
# false validations are bypassed altogether. See
|
||||
# By default, save always run validations. If any of them fail the action
|
||||
# is cancelled and +save+ returns +false+. However, if you supply
|
||||
# :validate => false, validations are bypassed altogether. See
|
||||
# ActiveRecord::Validations for more information.
|
||||
#
|
||||
# There's a series of callbacks associated with +save+. If any of the
|
||||
@@ -2227,7 +2193,7 @@ module ActiveRecord #:nodoc:
|
||||
# be made (since they can't be persisted).
|
||||
def destroy
|
||||
unless new_record?
|
||||
self.class.active_relation.where(self.class.active_relation[self.class.primary_key].eq(id)).delete_all
|
||||
self.class.unscoped.where(self.class.arel_table[self.class.primary_key].eq(id)).delete_all
|
||||
end
|
||||
|
||||
@destroyed = true
|
||||
@@ -2254,7 +2220,7 @@ module ActiveRecord #:nodoc:
|
||||
# in Base is replaced with this when the validations module is mixed in, which it is by default.
|
||||
def update_attribute(name, value)
|
||||
send(name.to_s + '=', value)
|
||||
save(false)
|
||||
save(:validate => false)
|
||||
end
|
||||
|
||||
# Updates all the attributes from the passed-in Hash and saves the record. If the object is invalid, the saving will
|
||||
@@ -2514,7 +2480,7 @@ module ActiveRecord #:nodoc:
|
||||
def update(attribute_names = @attributes.keys)
|
||||
attributes_with_values = arel_attributes_values(false, false, attribute_names)
|
||||
return 0 if attributes_with_values.empty?
|
||||
self.class.active_relation.where(self.class.active_relation[self.class.primary_key].eq(id)).update(attributes_with_values)
|
||||
self.class.unscoped.where(self.class.arel_table[self.class.primary_key].eq(id)).update(attributes_with_values)
|
||||
end
|
||||
|
||||
# Creates a record with values matching those of the instance attributes
|
||||
@@ -2527,9 +2493,9 @@ module ActiveRecord #:nodoc:
|
||||
attributes_values = arel_attributes_values
|
||||
|
||||
new_id = if attributes_values.empty?
|
||||
self.class.active_relation.insert connection.empty_insert_statement_value
|
||||
self.class.unscoped.insert connection.empty_insert_statement_value
|
||||
else
|
||||
self.class.active_relation.insert attributes_values
|
||||
self.class.unscoped.insert attributes_values
|
||||
end
|
||||
|
||||
self.id ||= new_id
|
||||
@@ -2624,7 +2590,7 @@ module ActiveRecord #:nodoc:
|
||||
if value && ((self.class.serialized_attributes.has_key?(name) && (value.acts_like?(:date) || value.acts_like?(:time))) || value.is_a?(Hash) || value.is_a?(Array))
|
||||
value = value.to_yaml
|
||||
end
|
||||
attrs[self.class.active_relation[name]] = value
|
||||
attrs[self.class.arel_table[name]] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,8 +2,6 @@ module ActiveRecord
|
||||
module Calculations #:nodoc:
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
CALCULATIONS_OPTIONS = [:conditions, :joins, :order, :select, :group, :having, :distinct, :limit, :offset, :include, :from]
|
||||
|
||||
module ClassMethods
|
||||
# Count operates using three different approaches.
|
||||
#
|
||||
@@ -46,19 +44,19 @@ module ActiveRecord
|
||||
def count(*args)
|
||||
case args.size
|
||||
when 0
|
||||
construct_calculation_arel({}, current_scoped_methods).count
|
||||
construct_calculation_arel.count
|
||||
when 1
|
||||
if args[0].is_a?(Hash)
|
||||
options = args[0]
|
||||
distinct = options.has_key?(:distinct) ? options.delete(:distinct) : false
|
||||
construct_calculation_arel(options, current_scoped_methods).count(options[:select], :distinct => distinct)
|
||||
construct_calculation_arel(options).count(options[:select], :distinct => distinct)
|
||||
else
|
||||
construct_calculation_arel({}, current_scoped_methods).count(args[0])
|
||||
construct_calculation_arel.count(args[0])
|
||||
end
|
||||
when 2
|
||||
column_name, options = args
|
||||
distinct = options.has_key?(:distinct) ? options.delete(:distinct) : false
|
||||
construct_calculation_arel(options, current_scoped_methods).count(column_name, :distinct => distinct)
|
||||
construct_calculation_arel(options).count(column_name, :distinct => distinct)
|
||||
else
|
||||
raise ArgumentError, "Unexpected parameters passed to count(): #{args.inspect}"
|
||||
end
|
||||
@@ -141,87 +139,17 @@ module ActiveRecord
|
||||
# Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name) # Selects the minimum age for any family without any minors
|
||||
# Person.sum("2 * age")
|
||||
def calculate(operation, column_name, options = {})
|
||||
construct_calculation_arel(options, current_scoped_methods).calculate(operation, column_name, options.slice(:distinct))
|
||||
construct_calculation_arel(options).calculate(operation, column_name, options.slice(:distinct))
|
||||
rescue ThrowResult
|
||||
0
|
||||
end
|
||||
|
||||
private
|
||||
def validate_calculation_options(options = {})
|
||||
options.assert_valid_keys(CALCULATIONS_OPTIONS)
|
||||
end
|
||||
|
||||
def construct_calculation_arel(options = {}, merge_with_relation = nil)
|
||||
validate_calculation_options(options)
|
||||
options = options.except(:distinct)
|
||||
|
||||
includes = merge_includes(merge_with_relation ? merge_with_relation.includes_values : [], options[:include])
|
||||
|
||||
if includes.any?
|
||||
merge_with_joins = merge_with_relation ? merge_with_relation.joins_values : []
|
||||
joins = (merge_with_joins + Array.wrap(options[:joins])).uniq
|
||||
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, includes, construct_join(joins))
|
||||
construct_calculation_arel_with_included_associations(options, join_dependency, merge_with_relation)
|
||||
else
|
||||
relation = active_relation.
|
||||
joins(options[:joins]).
|
||||
where(options[:conditions]).
|
||||
order(options[:order]).
|
||||
limit(options[:limit]).
|
||||
offset(options[:offset]).
|
||||
group(options[:group]).
|
||||
having(options[:having])
|
||||
|
||||
if merge_with_relation
|
||||
relation = merge_with_relation.except(:select, :order, :limit, :offset, :group, :from).merge(relation)
|
||||
else
|
||||
relation = relation.where(type_condition) if finder_needs_type_condition?
|
||||
end
|
||||
|
||||
from = merge_with_relation.from_value if merge_with_relation && merge_with_relation.from_value.present?
|
||||
from = options[:from] if from.blank? && options[:from].present?
|
||||
relation = relation.from(from)
|
||||
|
||||
select = options[:select].presence || (merge_with_relation ? merge_with_relation.select_values.join(", ") : nil)
|
||||
relation = relation.select(select)
|
||||
|
||||
relation
|
||||
end
|
||||
end
|
||||
|
||||
def construct_calculation_arel_with_included_associations(options, join_dependency, merge_with_relation = nil)
|
||||
relation = active_relation
|
||||
|
||||
for association in join_dependency.join_associations
|
||||
relation = association.join_relation(relation)
|
||||
end
|
||||
|
||||
if merge_with_relation
|
||||
relation.joins_values = (merge_with_relation.joins_values + relation.joins_values).uniq
|
||||
relation.where_values = merge_with_relation.where_values
|
||||
|
||||
merge_limit = merge_with_relation.taken
|
||||
else
|
||||
relation = relation.where(type_condition) if finder_needs_type_condition?
|
||||
end
|
||||
|
||||
relation = relation.joins(options[:joins]).
|
||||
select(column_aliases(join_dependency)).
|
||||
group(options[:group]).
|
||||
having(options[:having]).
|
||||
order(options[:order]).
|
||||
where(options[:conditions]).
|
||||
from(options[:from])
|
||||
|
||||
|
||||
if !using_limitable_reflections?(join_dependency.reflections) && (merge_limit || options[:limit])
|
||||
relation = relation.where(construct_arel_limited_ids_condition(options, join_dependency))
|
||||
end
|
||||
|
||||
relation = relation.limit(options[:limit] || merge_limit) if using_limitable_reflections?(join_dependency.reflections)
|
||||
|
||||
relation
|
||||
end
|
||||
def construct_calculation_arel(options = {})
|
||||
relation = scoped.apply_finder_options(options.except(:distinct))
|
||||
(relation.eager_loading? || relation.includes_values.present?) ? relation.send(:construct_relation_for_association_calculations) : relation
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
@@ -291,7 +291,7 @@ module ActiveRecord
|
||||
end
|
||||
|
||||
def deprecated_callback_method(symbol) #:nodoc:
|
||||
if respond_to?(symbol)
|
||||
if respond_to?(symbol, true)
|
||||
ActiveSupport::Deprecation.warn("Overwriting #{symbol} in your models has been deprecated, please use Base##{symbol} :method_name instead")
|
||||
send(symbol)
|
||||
end
|
||||
|
||||
@@ -13,7 +13,6 @@ module ActiveRecord
|
||||
module Format
|
||||
ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
|
||||
ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
|
||||
NEW_ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(?:\.(\d+))?\z/
|
||||
end
|
||||
|
||||
attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale
|
||||
@@ -168,11 +167,10 @@ module ActiveRecord
|
||||
end
|
||||
|
||||
protected
|
||||
# Rational(123456, 1_000_000) -> 123456
|
||||
# The sec_fraction component returned by Date._parse is a Rational fraction of a second or nil
|
||||
# NB: This method is optimized for performance by immediately converting away from Rational.
|
||||
# '0.123456' -> 123456
|
||||
# '1.123456' -> 123456
|
||||
def microseconds(time)
|
||||
((time[:sec_fraction].to_f % 1) * 1_000_000).round
|
||||
((time[:sec_fraction].to_f % 1) * 1_000_000).to_i
|
||||
end
|
||||
|
||||
def new_date(year, mon, mday)
|
||||
@@ -196,8 +194,9 @@ module ActiveRecord
|
||||
|
||||
# Doesn't handle time zones.
|
||||
def fast_string_to_time(string)
|
||||
if md = Format::NEW_ISO_DATETIME.match(string)
|
||||
new_time *md.to_a[1..7].map(&:to_i)
|
||||
if string =~ Format::ISO_DATETIME
|
||||
microsec = ($7.to_f * 1_000_000).to_i
|
||||
new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -335,7 +335,6 @@ end
|
||||
# george:
|
||||
# id: 1
|
||||
# name: George the Monkey
|
||||
# pirate_id: 1
|
||||
#
|
||||
# ### in fruits.yml
|
||||
#
|
||||
@@ -370,8 +369,8 @@ end
|
||||
# ### in monkeys.yml
|
||||
#
|
||||
# george:
|
||||
# id: 1
|
||||
# name: George the Monkey
|
||||
# pirate: reginald
|
||||
# fruits: apple, orange, grape
|
||||
#
|
||||
# ### in fruits.yml
|
||||
|
||||
@@ -78,7 +78,7 @@ module ActiveRecord
|
||||
attribute_names.uniq!
|
||||
|
||||
begin
|
||||
relation = self.class.active_relation
|
||||
relation = self.class.unscoped
|
||||
|
||||
affected_rows = relation.where(
|
||||
relation[self.class.primary_key].eq(quoted_id).and(
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
require 'active_support/core_ext/object/metaclass'
|
||||
|
||||
module ActiveRecord
|
||||
class IrreversibleMigration < ActiveRecordError#:nodoc:
|
||||
# Exception that can be raised to stop migrations from going backwards.
|
||||
class IrreversibleMigration < ActiveRecordError
|
||||
end
|
||||
|
||||
class DuplicateMigrationVersionError < ActiveRecordError#:nodoc:
|
||||
|
||||
@@ -9,7 +9,7 @@ module ActiveRecord
|
||||
module ClassMethods
|
||||
# Returns a relation if invoked without any arguments.
|
||||
#
|
||||
# posts = Post.scoped
|
||||
# posts = Post.scoped
|
||||
# posts.size # Fires "select count(*) from posts" and returns the count
|
||||
# posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects
|
||||
#
|
||||
@@ -24,15 +24,9 @@ module ActiveRecord
|
||||
# You can define a scope that applies to all finders using ActiveRecord::Base.default_scope.
|
||||
def scoped(options = {}, &block)
|
||||
if options.present?
|
||||
Scope.new(self, options, &block)
|
||||
Scope.init(self, options, &block)
|
||||
else
|
||||
current_scope = current_scoped_methods
|
||||
|
||||
unless current_scope
|
||||
finder_needs_type_condition? ? active_relation.where(type_condition) : active_relation.spawn
|
||||
else
|
||||
construct_finder_arel({}, current_scoped_methods)
|
||||
end
|
||||
current_scoped_methods ? unscoped.merge(current_scoped_methods) : unscoped.spawn
|
||||
end
|
||||
end
|
||||
|
||||
@@ -44,11 +38,11 @@ module ActiveRecord
|
||||
# such as <tt>:conditions => {:color => :red}, :select => 'shirts.*', :include => :washing_instructions</tt>.
|
||||
#
|
||||
# class Shirt < ActiveRecord::Base
|
||||
# named_scope :red, :conditions => {:color => 'red'}
|
||||
# named_scope :dry_clean_only, :joins => :washing_instructions, :conditions => ['washing_instructions.dry_clean_only = ?', true]
|
||||
# scope :red, :conditions => {:color => 'red'}
|
||||
# scope :dry_clean_only, :joins => :washing_instructions, :conditions => ['washing_instructions.dry_clean_only = ?', true]
|
||||
# end
|
||||
#
|
||||
# The above calls to <tt>named_scope</tt> define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red,
|
||||
#
|
||||
# The above calls to <tt>scope</tt> define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red,
|
||||
# in effect, represents the query <tt>Shirt.find(:all, :conditions => {:color => 'red'})</tt>.
|
||||
#
|
||||
# Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it resembles the association object
|
||||
@@ -74,7 +68,7 @@ module ActiveRecord
|
||||
# Named \scopes can also be procedural:
|
||||
#
|
||||
# class Shirt < ActiveRecord::Base
|
||||
# named_scope :colored, lambda { |color|
|
||||
# scope :colored, lambda { |color|
|
||||
# { :conditions => { :color => color } }
|
||||
# }
|
||||
# end
|
||||
@@ -84,7 +78,7 @@ module ActiveRecord
|
||||
# Named \scopes can also have extensions, just as with <tt>has_many</tt> declarations:
|
||||
#
|
||||
# class Shirt < ActiveRecord::Base
|
||||
# named_scope :red, :conditions => {:color => 'red'} do
|
||||
# scope :red, :conditions => {:color => 'red'} do
|
||||
# def dom_id
|
||||
# 'red_shirts'
|
||||
# end
|
||||
@@ -96,18 +90,23 @@ module ActiveRecord
|
||||
# <tt>proxy_options</tt> method on the proxy itself.
|
||||
#
|
||||
# class Shirt < ActiveRecord::Base
|
||||
# named_scope :colored, lambda { |color|
|
||||
# scope :colored, lambda { |color|
|
||||
# { :conditions => { :color => color } }
|
||||
# }
|
||||
# end
|
||||
#
|
||||
# expected_options = { :conditions => { :colored => 'red' } }
|
||||
# assert_equal expected_options, Shirt.colored('red').proxy_options
|
||||
def named_scope(name, options = {}, &block)
|
||||
def scope(name, options = {}, &block)
|
||||
name = name.to_sym
|
||||
|
||||
if !scopes[name] && respond_to?(name, true)
|
||||
raise ArgumentError, "Cannot define scope :#{name} because #{self.name}.#{name} method already exists."
|
||||
end
|
||||
|
||||
scopes[name] = lambda do |parent_scope, *args|
|
||||
Scope.new(parent_scope, case options
|
||||
when Hash
|
||||
Scope.init(parent_scope, case options
|
||||
when Hash, Relation
|
||||
options
|
||||
when Proc
|
||||
options.call(*args)
|
||||
@@ -119,104 +118,90 @@ module ActiveRecord
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def named_scope(*args, &block)
|
||||
ActiveSupport::Deprecation.warn("Base.named_scope has been deprecated, please use Base.scope instead", caller)
|
||||
scope(*args, &block)
|
||||
end
|
||||
end
|
||||
|
||||
class Scope
|
||||
attr_reader :proxy_scope, :proxy_options, :current_scoped_methods_when_defined
|
||||
NON_DELEGATE_METHODS = %w(nil? send object_id class extend find size count sum average maximum minimum paginate first last empty? any? many? respond_to?).to_set
|
||||
[].methods.each do |m|
|
||||
unless m =~ /^__/ || NON_DELEGATE_METHODS.include?(m.to_s)
|
||||
delegate m, :to => :proxy_found
|
||||
class Scope < Relation
|
||||
attr_accessor :current_scoped_methods_when_defined
|
||||
|
||||
delegate :scopes, :with_scope, :with_exclusive_scope, :scoped_methods, :scoped, :to => :klass
|
||||
|
||||
def self.init(klass, options, &block)
|
||||
relation = new(klass, klass.arel_table)
|
||||
|
||||
scope = if options.is_a?(Hash)
|
||||
klass.scoped.apply_finder_options(options.except(:extend))
|
||||
else
|
||||
options ? klass.scoped.merge(options) : klass.scoped
|
||||
end
|
||||
|
||||
relation = relation.merge(scope)
|
||||
|
||||
Array.wrap(options[:extend]).each {|extension| relation.send(:extend, extension) } if options.is_a?(Hash)
|
||||
relation.send(:extend, Module.new(&block)) if block_given?
|
||||
|
||||
relation.current_scoped_methods_when_defined = klass.send(:current_scoped_methods)
|
||||
relation
|
||||
end
|
||||
|
||||
delegate :scopes, :with_scope, :scoped_methods, :to => :proxy_scope
|
||||
def find(*args)
|
||||
options = args.extract_options!
|
||||
relation = options.present? ? apply_finder_options(options) : self
|
||||
|
||||
def initialize(proxy_scope, options, &block)
|
||||
options ||= {}
|
||||
[options[:extend]].flatten.each { |extension| extend extension } if options[:extend]
|
||||
extend Module.new(&block) if block_given?
|
||||
unless Scope === proxy_scope
|
||||
@current_scoped_methods_when_defined = proxy_scope.send(:current_scoped_methods)
|
||||
case args.first
|
||||
when :first, :last, :all
|
||||
relation.send(args.first)
|
||||
else
|
||||
options.present? ? relation.find(*args) : super
|
||||
end
|
||||
@proxy_scope, @proxy_options = proxy_scope, options.except(:extend)
|
||||
end
|
||||
|
||||
def reload
|
||||
load_found; self
|
||||
end
|
||||
|
||||
def first(*args)
|
||||
if args.first.kind_of?(Integer) || (@found && !args.first.kind_of?(Hash))
|
||||
proxy_found.first(*args)
|
||||
if args.first.kind_of?(Integer) || (loaded? && !args.first.kind_of?(Hash))
|
||||
to_a.first(*args)
|
||||
else
|
||||
find(:first, *args)
|
||||
args.first.present? ? apply_finder_options(args.first).first : super
|
||||
end
|
||||
end
|
||||
|
||||
def last(*args)
|
||||
if args.first.kind_of?(Integer) || (@found && !args.first.kind_of?(Hash))
|
||||
proxy_found.last(*args)
|
||||
if args.first.kind_of?(Integer) || (loaded? && !args.first.kind_of?(Hash))
|
||||
to_a.last(*args)
|
||||
else
|
||||
find(:last, *args)
|
||||
args.first.present? ? apply_finder_options(args.first).last : super
|
||||
end
|
||||
end
|
||||
|
||||
def size
|
||||
@found ? @found.length : count
|
||||
def count(*args)
|
||||
options = args.extract_options!
|
||||
options.present? ? apply_finder_options(options).count(*args) : super
|
||||
end
|
||||
|
||||
def empty?
|
||||
@found ? @found.empty? : count.zero?
|
||||
end
|
||||
|
||||
def respond_to?(method, include_private = false)
|
||||
super || @proxy_scope.respond_to?(method, include_private)
|
||||
end
|
||||
|
||||
def any?
|
||||
if block_given?
|
||||
proxy_found.any? { |*block_args| yield(*block_args) }
|
||||
else
|
||||
!empty?
|
||||
end
|
||||
end
|
||||
|
||||
# Returns true if the named scope has more than 1 matching record.
|
||||
def many?
|
||||
if block_given?
|
||||
proxy_found.many? { |*block_args| yield(*block_args) }
|
||||
else
|
||||
size > 1
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
def proxy_found
|
||||
@found || load_found
|
||||
def ==(other)
|
||||
to_a == other.to_a
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def method_missing(method, *args, &block)
|
||||
if scopes.include?(method)
|
||||
scopes[method].call(self, *args)
|
||||
else
|
||||
with_scope({:find => proxy_options, :create => proxy_options[:conditions].is_a?(Hash) ? proxy_options[:conditions] : {}}, :reverse_merge) do
|
||||
method = :new if method == :build
|
||||
if current_scoped_methods_when_defined && !scoped_methods.include?(current_scoped_methods_when_defined)
|
||||
with_scope current_scoped_methods_when_defined do
|
||||
proxy_scope.send(method, *args, &block)
|
||||
end
|
||||
if klass.respond_to?(method)
|
||||
with_scope(self) do
|
||||
if current_scoped_methods_when_defined && !scoped_methods.include?(current_scoped_methods_when_defined) && !scopes.include?(method)
|
||||
with_scope(current_scoped_methods_when_defined) { klass.send(method, *args, &block) }
|
||||
else
|
||||
proxy_scope.send(method, *args, &block)
|
||||
klass.send(method, *args, &block)
|
||||
end
|
||||
end
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def load_found
|
||||
@found = find(:all)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
@@ -49,14 +49,14 @@ module ActiveRecord
|
||||
# create the member and avatar in one go:
|
||||
#
|
||||
# params = { :member => { :name => 'Jack', :avatar_attributes => { :icon => 'smiling' } } }
|
||||
# member = Member.create(params)
|
||||
# member = Member.create(params[:member])
|
||||
# member.avatar.id # => 2
|
||||
# member.avatar.icon # => 'smiling'
|
||||
#
|
||||
# It also allows you to update the avatar through the member:
|
||||
#
|
||||
# params = { :member' => { :avatar_attributes => { :id => '2', :icon => 'sad' } } }
|
||||
# member.update_attributes params['member']
|
||||
# params = { :member => { :avatar_attributes => { :id => '2', :icon => 'sad' } } }
|
||||
# member.update_attributes params[:member]
|
||||
# member.avatar.icon # => 'sad'
|
||||
#
|
||||
# By default you will only be able to set and update attributes on the
|
||||
@@ -75,7 +75,7 @@ module ActiveRecord
|
||||
# member.avatar_attributes = { :id => '2', :_destroy => '1' }
|
||||
# member.avatar.marked_for_destruction? # => true
|
||||
# member.save
|
||||
# member.avatar #=> nil
|
||||
# member.reload.avatar #=> nil
|
||||
#
|
||||
# Note that the model will _not_ be destroyed until the parent is saved.
|
||||
#
|
||||
@@ -179,7 +179,7 @@ module ActiveRecord
|
||||
# member.posts.detect { |p| p.id == 2 }.marked_for_destruction? # => true
|
||||
# member.posts.length #=> 2
|
||||
# member.save
|
||||
# member.posts.length # => 1
|
||||
# member.reload.posts.length # => 1
|
||||
#
|
||||
# === Saving
|
||||
#
|
||||
|
||||
@@ -428,7 +428,7 @@ namespace :db do
|
||||
task :create => :environment do
|
||||
raise "Task unavailable to this database (no migration support)" unless ActiveRecord::Base.connection.supports_migrations?
|
||||
require 'rails/generators'
|
||||
require 'rails/generators/rails/session_migration/session_migration_generator'
|
||||
require 'generators/rails/session_migration/session_migration_generator'
|
||||
Rails::Generators::SessionMigrationGenerator.start [ ENV["MIGRATION"] || "add_sessions_table" ]
|
||||
end
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ module ActiveRecord
|
||||
name = color(name, :magenta, true)
|
||||
end
|
||||
|
||||
debug "#{name} #{sql}"
|
||||
debug " #{name} #{sql}"
|
||||
end
|
||||
|
||||
def odd?
|
||||
|
||||
@@ -7,7 +7,7 @@ module ActiveRecord
|
||||
|
||||
include FinderMethods, CalculationMethods, SpawnMethods, QueryMethods
|
||||
|
||||
delegate :length, :collect, :map, :each, :all?, :to => :to_a
|
||||
delegate :length, :collect, :map, :each, :all?, :include?, :to => :to_a
|
||||
|
||||
attr_reader :table, :klass
|
||||
|
||||
@@ -20,6 +20,8 @@ module ActiveRecord
|
||||
with_create_scope { @klass.new(*args, &block) }
|
||||
end
|
||||
|
||||
alias build new
|
||||
|
||||
def create(*args, &block)
|
||||
with_create_scope { @klass.create(*args, &block) }
|
||||
end
|
||||
@@ -43,33 +45,10 @@ module ActiveRecord
|
||||
def to_a
|
||||
return @records if loaded?
|
||||
|
||||
find_with_associations = @eager_load_values.any? || (@includes_values.any? && references_eager_loaded_tables?)
|
||||
|
||||
@records = if find_with_associations
|
||||
begin
|
||||
options = {
|
||||
:select => @select_values.any? ? @select_values.join(", ") : nil,
|
||||
:joins => arel.joins(arel),
|
||||
:group => @group_values.any? ? @group_values.join(", ") : nil,
|
||||
:order => order_clause,
|
||||
:conditions => where_clause,
|
||||
:limit => arel.taken,
|
||||
:offset => arel.skipped,
|
||||
:from => (arel.send(:from_clauses) if arel.send(:sources).present?)
|
||||
}
|
||||
|
||||
including = (@eager_load_values + @includes_values).uniq
|
||||
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, nil)
|
||||
@klass.send(:find_with_associations, options, join_dependency)
|
||||
rescue ThrowResult
|
||||
[]
|
||||
end
|
||||
else
|
||||
@klass.find_by_sql(arel.to_sql)
|
||||
end
|
||||
@records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql)
|
||||
|
||||
preload = @preload_values
|
||||
preload += @includes_values unless find_with_associations
|
||||
preload += @includes_values unless eager_loading?
|
||||
preload.each {|associations| @klass.send(:preload_associations, @records, associations) }
|
||||
|
||||
# @readonly_value is true only if set explicity. @implicit_readonly is true if there are JOINS and no explicit SELECT.
|
||||
@@ -124,12 +103,14 @@ module ActiveRecord
|
||||
end
|
||||
|
||||
def reload
|
||||
@loaded = false
|
||||
reset
|
||||
to_a # force reload
|
||||
self
|
||||
end
|
||||
|
||||
def reset
|
||||
@first = @last = @to_sql = @order_clause = @scope_for_create = @arel = nil
|
||||
@first = @last = @to_sql = @order_clause = @scope_for_create = @arel = @loaded = nil
|
||||
@should_eager_load = @join_dependency = nil
|
||||
@records = []
|
||||
self
|
||||
end
|
||||
@@ -151,6 +132,10 @@ module ActiveRecord
|
||||
end
|
||||
end
|
||||
|
||||
def eager_loading?
|
||||
@should_eager_load ||= (@eager_load_values.any? || (@includes_values.any? && references_eager_loaded_tables?))
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def method_missing(method, *args, &block)
|
||||
@@ -172,6 +157,8 @@ module ActiveRecord
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def with_create_scope
|
||||
@klass.send(:with_scope, :create => scope_for_create, :find => {}) { yield }
|
||||
end
|
||||
@@ -180,10 +167,6 @@ module ActiveRecord
|
||||
arel.send(:where_clauses).join(join_string)
|
||||
end
|
||||
|
||||
def order_clause
|
||||
@order_clause ||= arel.send(:order_clauses).join(', ')
|
||||
end
|
||||
|
||||
def references_eager_loaded_tables?
|
||||
joined_tables = (tables_in_string(arel.joins(arel)) + [table.name, table.table_alias]).compact.uniq
|
||||
(tables_in_string(to_sql) - joined_tables).any?
|
||||
|
||||
@@ -53,7 +53,7 @@ module ActiveRecord
|
||||
|
||||
def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
|
||||
column = if @klass.column_names.include?(column_name.to_s)
|
||||
Arel::Attribute.new(@klass.active_relation, column_name)
|
||||
Arel::Attribute.new(@klass.unscoped, column_name)
|
||||
else
|
||||
Arel::SqlLiteral.new(column_name == :all ? "*" : column_name.to_s)
|
||||
end
|
||||
@@ -77,7 +77,7 @@ module ActiveRecord
|
||||
select_statement = if operation == 'count' && column_name == :all
|
||||
"COUNT(*) AS count_all"
|
||||
else
|
||||
Arel::Attribute.new(@klass.active_relation, column_name).send(operation).as(aggregate_alias).to_sql
|
||||
Arel::Attribute.new(@klass.unscoped, column_name).send(operation).as(aggregate_alias).to_sql
|
||||
end
|
||||
|
||||
select_statement << ", #{group_field} AS #{group_alias}"
|
||||
|
||||
@@ -44,6 +44,48 @@ module ActiveRecord
|
||||
|
||||
protected
|
||||
|
||||
def find_with_associations
|
||||
including = (@eager_load_values + @includes_values).uniq
|
||||
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, nil)
|
||||
rows = construct_relation_for_association_find(join_dependency).to_a
|
||||
join_dependency.instantiate(rows)
|
||||
rescue ThrowResult
|
||||
[]
|
||||
end
|
||||
|
||||
def construct_relation_for_association_calculations
|
||||
including = (@eager_load_values + @includes_values).uniq
|
||||
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, arel.joins(arel))
|
||||
construct_relation_for_association_find(join_dependency)
|
||||
end
|
||||
|
||||
def construct_relation_for_association_find(join_dependency)
|
||||
relation = except(:includes, :eager_load, :preload, :select).select(@klass.send(:column_aliases, join_dependency))
|
||||
|
||||
for association in join_dependency.join_associations
|
||||
relation = association.join_relation(relation)
|
||||
end
|
||||
|
||||
limitable_reflections = @klass.send(:using_limitable_reflections?, join_dependency.reflections)
|
||||
|
||||
if !limitable_reflections && relation.limit_value
|
||||
limited_id_condition = construct_limited_ids_condition(relation.except(:select))
|
||||
relation = relation.where(limited_id_condition)
|
||||
end
|
||||
|
||||
relation = relation.except(:limit, :offset) unless limitable_reflections
|
||||
|
||||
relation
|
||||
end
|
||||
|
||||
def construct_limited_ids_condition(relation)
|
||||
orders = relation.order_values.join(", ")
|
||||
values = @klass.connection.distinct("#{@klass.connection.quote_table_name @klass.table_name}.#{@klass.primary_key}", orders)
|
||||
|
||||
ids_array = relation.select(values).collect {|row| row[@klass.primary_key]}
|
||||
ids_array.empty? ? raise(ThrowResult) : primary_key.in(ids_array)
|
||||
end
|
||||
|
||||
def find_by_attributes(match, attributes, *args)
|
||||
conditions = attributes.inject({}) {|h, a| h[a] = args[attributes.index(a)]; h}
|
||||
result = where(conditions).send(match.finder)
|
||||
|
||||
@@ -24,7 +24,8 @@ module ActiveRecord
|
||||
|
||||
case value
|
||||
when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::NamedScope::Scope
|
||||
attribute.in(value)
|
||||
values = value.to_a
|
||||
values.any? ? attribute.in(values) : attribute.eq(nil)
|
||||
when Range
|
||||
# TODO : Arel should handle ranges with excluded end.
|
||||
if value.exclude_end?
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module ActiveRecord
|
||||
module SpawnMethods
|
||||
def spawn(arel_table = self.table)
|
||||
relation = Relation.new(@klass, arel_table)
|
||||
relation = self.class.new(@klass, arel_table)
|
||||
|
||||
(Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS).each do |query_method|
|
||||
relation.send(:"#{query_method}_values=", send(:"#{query_method}_values"))
|
||||
@@ -15,9 +15,10 @@ module ActiveRecord
|
||||
end
|
||||
|
||||
def merge(r)
|
||||
raise ArgumentError, "Cannot merge a #{r.klass.name} relation with #{@klass.name} relation" if r.klass != @klass
|
||||
merged_relation = spawn
|
||||
return merged_relation unless r
|
||||
|
||||
merged_relation = spawn.eager_load(r.eager_load_values).preload(r.preload_values).includes(r.includes_values)
|
||||
merged_relation = merged_relation.eager_load(r.eager_load_values).preload(r.preload_values).includes(r.includes_values)
|
||||
|
||||
merged_relation.readonly_value = r.readonly_value unless r.readonly_value.nil?
|
||||
merged_relation.limit_value = r.limit_value if r.limit_value.present?
|
||||
@@ -31,7 +32,7 @@ module ActiveRecord
|
||||
from(r.from_value).
|
||||
having(r.having_values)
|
||||
|
||||
merged_relation.order_values = Array.wrap(order_values) + Array.wrap(r.order_values)
|
||||
merged_relation.order_values = r.order_values if r.order_values.present?
|
||||
|
||||
merged_relation.create_with_value = @create_with_value
|
||||
|
||||
@@ -59,7 +60,7 @@ module ActiveRecord
|
||||
alias :& :merge
|
||||
|
||||
def except(*skips)
|
||||
result = Relation.new(@klass, table)
|
||||
result = self.class.new(@klass, table)
|
||||
|
||||
(Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS).each do |method|
|
||||
result.send(:"#{method}_values=", send(:"#{method}_values")) unless skips.include?(method)
|
||||
@@ -73,7 +74,7 @@ module ActiveRecord
|
||||
end
|
||||
|
||||
def only(*onlies)
|
||||
result = Relation.new(@klass, table)
|
||||
result = self.class.new(@klass, table)
|
||||
|
||||
onlies.each do |only|
|
||||
if (Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS).include?(only)
|
||||
@@ -88,5 +89,31 @@ module ActiveRecord
|
||||
result
|
||||
end
|
||||
|
||||
VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset,
|
||||
:order, :select, :readonly, :group, :having, :from, :lock ]
|
||||
|
||||
def apply_finder_options(options)
|
||||
relation = spawn
|
||||
return relation unless options
|
||||
|
||||
options.assert_valid_keys(VALID_FIND_OPTIONS)
|
||||
|
||||
relation = relation.joins(options[:joins]).
|
||||
where(options[:conditions]).
|
||||
select(options[:select]).
|
||||
group(options[:group]).
|
||||
having(options[:having]).
|
||||
order(options[:order]).
|
||||
limit(options[:limit]).
|
||||
offset(options[:offset]).
|
||||
from(options[:from]).
|
||||
includes(options[:include])
|
||||
|
||||
relation = relation.lock(options[:lock]) if options[:lock].present?
|
||||
relation = relation.readonly(options[:readonly]) if options.has_key?(:readonly)
|
||||
|
||||
relation
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
@@ -192,8 +192,8 @@ module ActiveRecord
|
||||
with_transaction_returning_status(:destroy_without_transactions)
|
||||
end
|
||||
|
||||
def save_with_transactions(perform_validation = true) #:nodoc:
|
||||
rollback_active_record_state! { with_transaction_returning_status(:save_without_transactions, perform_validation) }
|
||||
def save_with_transactions(*args) #:nodoc:
|
||||
rollback_active_record_state! { with_transaction_returning_status(:save_without_transactions, *args) }
|
||||
end
|
||||
|
||||
def save_with_transactions! #:nodoc:
|
||||
|
||||
@@ -42,7 +42,17 @@ module ActiveRecord
|
||||
module InstanceMethods
|
||||
# The validation process on save can be skipped by passing false. The regular Base#save method is
|
||||
# replaced with this when the validations module is mixed in, which it is by default.
|
||||
def save_with_validation(perform_validation = true)
|
||||
def save_with_validation(options=nil)
|
||||
perform_validation = case options
|
||||
when NilClass
|
||||
true
|
||||
when Hash
|
||||
options[:validate] != false
|
||||
else
|
||||
ActiveSupport::Deprecation.warn "save(#{options}) is deprecated, please give save(:validate => #{options}) instead", caller
|
||||
options
|
||||
end
|
||||
|
||||
if perform_validation && valid? || !perform_validation
|
||||
save_without_validation
|
||||
else
|
||||
|
||||
@@ -12,7 +12,7 @@ module ActiveRecord
|
||||
|
||||
def validate_each(record, attribute, value)
|
||||
finder_class = find_finder_class_for(record)
|
||||
table = finder_class.active_relation
|
||||
table = finder_class.unscoped
|
||||
|
||||
table_name = record.class.quoted_table_name
|
||||
sql, params = mount_sql_and_params(finder_class, table_name, attribute, value)
|
||||
|
||||
@@ -8,6 +8,14 @@ module ActiveRecord
|
||||
class Base < Rails::Generators::NamedBase #:nodoc:
|
||||
include Rails::Generators::Migration
|
||||
|
||||
def self.source_root
|
||||
@_ar_source_root ||= begin
|
||||
if base_name && generator_name
|
||||
File.expand_path(File.join(base_name, generator_name, 'templates'), File.dirname(__FILE__))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
# Implement the required interface for Rails::Generators::Migration.
|
||||
#
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'rails/generators/active_record'
|
||||
require 'generators/active_record'
|
||||
|
||||
module ActiveRecord
|
||||
module Generators
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'rails/generators/active_record'
|
||||
require 'generators/active_record'
|
||||
|
||||
module ActiveRecord
|
||||
module Generators
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'rails/generators/active_record'
|
||||
require 'generators/active_record'
|
||||
|
||||
module ActiveRecord
|
||||
module Generators
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'rails/generators/active_record'
|
||||
require 'generators/active_record'
|
||||
|
||||
module ActiveRecord
|
||||
module Generators
|
||||
@@ -805,7 +805,7 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
|
||||
def test_should_still_allow_to_bypass_validations_on_the_associated_model
|
||||
@pirate.catchphrase = ''
|
||||
@pirate.ship.name = ''
|
||||
@pirate.save(false)
|
||||
@pirate.save(:validate => false)
|
||||
# Oracle saves empty string as NULL
|
||||
if current_adapter?(:OracleAdapter)
|
||||
assert_equal [nil, nil], [@pirate.reload.catchphrase, @pirate.ship.name]
|
||||
@@ -820,7 +820,7 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
|
||||
@pirate.catchphrase = ''
|
||||
@pirate.ship.name = ''
|
||||
@pirate.ship.parts.each { |part| part.name = '' }
|
||||
@pirate.save(false)
|
||||
@pirate.save(:validate => false)
|
||||
|
||||
values = [@pirate.reload.catchphrase, @pirate.ship.name, *@pirate.ship.parts.map(&:name)]
|
||||
# Oracle saves empty string as NULL
|
||||
@@ -917,7 +917,7 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase
|
||||
def test_should_still_allow_to_bypass_validations_on_the_associated_model
|
||||
@ship.pirate.catchphrase = ''
|
||||
@ship.name = ''
|
||||
@ship.save(false)
|
||||
@ship.save(:validate => false)
|
||||
# Oracle saves empty string as NULL
|
||||
if current_adapter?(:OracleAdapter)
|
||||
assert_equal [nil, nil], [@ship.reload.name, @ship.pirate.catchphrase]
|
||||
@@ -1029,7 +1029,7 @@ module AutosaveAssociationOnACollectionAssociationTests
|
||||
@pirate.catchphrase = ''
|
||||
@pirate.send(@association_name).each { |child| child.name = '' }
|
||||
|
||||
assert @pirate.save(false)
|
||||
assert @pirate.save(:validate => false)
|
||||
# Oracle saves empty string as NULL
|
||||
if current_adapter?(:OracleAdapter)
|
||||
assert_equal [nil, nil, nil], [
|
||||
@@ -1049,14 +1049,14 @@ module AutosaveAssociationOnACollectionAssociationTests
|
||||
def test_should_validation_the_associated_models_on_create
|
||||
assert_no_difference("#{ @association_name == :birds ? 'Bird' : 'Parrot' }.count") do
|
||||
2.times { @pirate.send(@association_name).build }
|
||||
@pirate.save(true)
|
||||
@pirate.save
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_allow_to_bypass_validations_on_the_associated_models_on_create
|
||||
assert_difference("#{ @association_name == :birds ? 'Bird' : 'Parrot' }.count", +2) do
|
||||
2.times { @pirate.send(@association_name).build }
|
||||
@pirate.save(false)
|
||||
@pirate.save(:validate => false)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1865,7 +1865,9 @@ class BasicsTest < ActiveRecord::TestCase
|
||||
end
|
||||
assert scoped_developers.include?(developers(:poor_jamis))
|
||||
assert scoped_developers.include?(developers(:david))
|
||||
assert scoped_developers.include?(developers(:dev_10))
|
||||
assert ! scoped_developers.include?(developers(:jamis))
|
||||
assert_equal 3, scoped_developers.size
|
||||
|
||||
# Test without scoped find conditions to ensure we get the right thing
|
||||
developers = Developer.find(:all, :order => 'id', :limit => 1)
|
||||
assert scoped_developers.include?(developers(:david))
|
||||
|
||||
@@ -246,23 +246,6 @@ class CalculationsTest < ActiveRecord::TestCase
|
||||
assert_equal 8, c['Jadedpixel']
|
||||
end
|
||||
|
||||
def test_should_reject_invalid_options
|
||||
assert_nothing_raised do
|
||||
# empty options are valid
|
||||
Company.send(:validate_calculation_options)
|
||||
# these options are valid for all calculations
|
||||
[:select, :conditions, :joins, :order, :group, :having, :distinct].each do |opt|
|
||||
Company.send(:validate_calculation_options, opt => true)
|
||||
end
|
||||
|
||||
# :include is only valid on :count
|
||||
Company.send(:validate_calculation_options, :include => true)
|
||||
end
|
||||
|
||||
assert_raise(ArgumentError) { Company.send(:validate_calculation_options, :sum, :foo => :bar) }
|
||||
assert_raise(ArgumentError) { Company.send(:validate_calculation_options, :count, :foo => :bar) }
|
||||
end
|
||||
|
||||
def test_should_count_selected_field_with_include
|
||||
assert_equal 6, Account.count(:distinct => true, :include => :firm)
|
||||
assert_equal 4, Account.count(:distinct => true, :include => :firm, :select => :credit_limit)
|
||||
|
||||
@@ -608,7 +608,7 @@ class DefaultScopingTest < ActiveRecord::TestCase
|
||||
|
||||
def test_default_scoping_with_threads
|
||||
2.times do
|
||||
Thread.new { assert_equal 'salary DESC', DeveloperOrderedBySalary.scoped.send(:order_clause) }.join
|
||||
Thread.new { assert_equal ['salary DESC'], DeveloperOrderedBySalary.scoped.order_values }.join
|
||||
end
|
||||
end
|
||||
|
||||
@@ -618,28 +618,28 @@ class DefaultScopingTest < ActiveRecord::TestCase
|
||||
klass.send :default_scope, {}
|
||||
|
||||
# Scopes added on children should append to parent scope
|
||||
assert klass.scoped.send(:order_clause).blank?
|
||||
assert klass.scoped.order_values.blank?
|
||||
|
||||
# Parent should still have the original scope
|
||||
assert_equal 'salary DESC', DeveloperOrderedBySalary.scoped.send(:order_clause)
|
||||
assert_equal ['salary DESC'], DeveloperOrderedBySalary.scoped.order_values
|
||||
end
|
||||
|
||||
def test_method_scope
|
||||
expected = Developer.find(:all, :order => 'name DESC, salary DESC').collect { |dev| dev.salary }
|
||||
expected = Developer.find(:all, :order => 'name DESC').collect { |dev| dev.salary }
|
||||
received = DeveloperOrderedBySalary.all_ordered_by_name.collect { |dev| dev.salary }
|
||||
assert_equal expected, received
|
||||
end
|
||||
|
||||
def test_nested_scope
|
||||
expected = Developer.find(:all, :order => 'name DESC, salary DESC').collect { |dev| dev.salary }
|
||||
expected = Developer.find(:all, :order => 'name DESC').collect { |dev| dev.salary }
|
||||
received = DeveloperOrderedBySalary.send(:with_scope, :find => { :order => 'name DESC'}) do
|
||||
DeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary }
|
||||
end
|
||||
assert_equal expected, received
|
||||
end
|
||||
|
||||
def test_named_scope_order_appended_to_default_scope_order
|
||||
expected = Developer.find(:all, :order => 'name DESC, salary DESC').collect { |dev| dev.name }
|
||||
def test_named_scope_overwrites_default
|
||||
expected = Developer.find(:all, :order => 'name DESC').collect { |dev| dev.name }
|
||||
received = DeveloperOrderedBySalary.by_name.find(:all).collect { |dev| dev.name }
|
||||
assert_equal expected, received
|
||||
end
|
||||
|
||||
@@ -31,7 +31,7 @@ class NamedScopeTest < ActiveRecord::TestCase
|
||||
|
||||
def test_reload_expires_cache_of_found_items
|
||||
all_posts = Topic.base
|
||||
all_posts.inspect
|
||||
all_posts.all
|
||||
|
||||
new_post = Topic.create!
|
||||
assert !all_posts.include?(new_post)
|
||||
@@ -48,14 +48,14 @@ class NamedScopeTest < ActiveRecord::TestCase
|
||||
end
|
||||
|
||||
def test_scope_should_respond_to_own_methods_and_methods_of_the_proxy
|
||||
assert Topic.approved.respond_to?(:proxy_found)
|
||||
assert Topic.approved.respond_to?(:limit)
|
||||
assert Topic.approved.respond_to?(:count)
|
||||
assert Topic.approved.respond_to?(:length)
|
||||
end
|
||||
|
||||
def test_respond_to_respects_include_private_parameter
|
||||
assert !Topic.approved.respond_to?(:load_found)
|
||||
assert Topic.approved.respond_to?(:load_found, true)
|
||||
assert !Topic.approved.respond_to?(:with_create_scope)
|
||||
assert Topic.approved.respond_to?(:with_create_scope, true)
|
||||
end
|
||||
|
||||
def test_subclasses_inherit_scopes
|
||||
@@ -150,13 +150,13 @@ class NamedScopeTest < ActiveRecord::TestCase
|
||||
end
|
||||
|
||||
def test_named_scopes_honor_current_scopes_from_when_defined
|
||||
assert !Post.ranked_by_comments.limit(5).empty?
|
||||
assert !authors(:david).posts.ranked_by_comments.limit(5).empty?
|
||||
assert_not_equal Post.ranked_by_comments.limit(5), authors(:david).posts.ranked_by_comments.limit(5)
|
||||
assert !Post.ranked_by_comments.limit_by(5).empty?
|
||||
assert !authors(:david).posts.ranked_by_comments.limit_by(5).empty?
|
||||
assert_not_equal Post.ranked_by_comments.limit_by(5), authors(:david).posts.ranked_by_comments.limit_by(5)
|
||||
assert_not_equal Post.top(5), authors(:david).posts.top(5)
|
||||
# Oracle sometimes sorts differently if WHERE condition is changed
|
||||
assert_equal authors(:david).posts.ranked_by_comments.limit(5).sort_by(&:id), authors(:david).posts.top(5).sort_by(&:id)
|
||||
assert_equal Post.ranked_by_comments.limit(5), Post.top(5)
|
||||
assert_equal authors(:david).posts.ranked_by_comments.limit_by(5).to_a.sort_by(&:id), authors(:david).posts.top(5).to_a.sort_by(&:id)
|
||||
assert_equal Post.ranked_by_comments.limit_by(5), Post.top(5)
|
||||
end
|
||||
|
||||
def test_active_records_have_scope_named__all__
|
||||
@@ -171,11 +171,6 @@ class NamedScopeTest < ActiveRecord::TestCase
|
||||
assert_equal Topic.find(:all, scope), Topic.scoped(scope)
|
||||
end
|
||||
|
||||
def test_proxy_options
|
||||
expected_proxy_options = { :conditions => { :approved => true } }
|
||||
assert_equal expected_proxy_options, Topic.approved.proxy_options
|
||||
end
|
||||
|
||||
def test_first_and_last_should_support_find_options
|
||||
assert_equal Topic.base.first(:order => 'title'), Topic.base.find(:first, :order => 'title')
|
||||
assert_equal Topic.base.last(:order => 'title'), Topic.base.find(:last, :order => 'title')
|
||||
@@ -297,7 +292,7 @@ class NamedScopeTest < ActiveRecord::TestCase
|
||||
end
|
||||
|
||||
def test_find_all_should_behave_like_select
|
||||
assert_equal Topic.base.select(&:approved), Topic.base.find_all(&:approved)
|
||||
assert_equal Topic.base.to_a.select(&:approved), Topic.base.to_a.find_all(&:approved)
|
||||
end
|
||||
|
||||
def test_rand_should_select_a_random_object_from_proxy
|
||||
@@ -345,14 +340,14 @@ class NamedScopeTest < ActiveRecord::TestCase
|
||||
|
||||
def test_chaining_should_use_latest_conditions_when_searching
|
||||
# Normal hash conditions
|
||||
assert_equal Topic.where(:approved => true).to_a, Topic.rejected.approved.all.to_a
|
||||
assert_equal Topic.where(:approved => false).to_a, Topic.approved.rejected.all.to_a
|
||||
assert_equal Topic.where(:approved => true).to_a, Topic.rejected.approved.all
|
||||
assert_equal Topic.where(:approved => false).to_a, Topic.approved.rejected.all
|
||||
|
||||
# Nested hash conditions with same keys
|
||||
assert_equal [posts(:sti_comments)], Post.with_special_comments.with_very_special_comments.all.to_a
|
||||
assert_equal [posts(:sti_comments)], Post.with_special_comments.with_very_special_comments.all
|
||||
|
||||
# Nested hash conditions with different keys
|
||||
assert_equal [posts(:sti_comments)], Post.with_special_comments.with_post(4).all.to_a.uniq
|
||||
assert_equal [posts(:sti_comments)], Post.with_special_comments.with_post(4).all.uniq
|
||||
end
|
||||
|
||||
def test_named_scopes_batch_finders
|
||||
@@ -374,6 +369,16 @@ class NamedScopeTest < ActiveRecord::TestCase
|
||||
Comment.for_first_post.for_first_author.all
|
||||
end
|
||||
end
|
||||
|
||||
def test_named_scopes_with_reserved_names
|
||||
[:where, :with_scope].each do |protected_method|
|
||||
assert_raises(ArgumentError) { Topic.scope protected_method }
|
||||
end
|
||||
end
|
||||
|
||||
def test_deprecated_named_scope_method
|
||||
assert_deprecated('named_scope has been deprecated') { Topic.named_scope :deprecated_named_scope }
|
||||
end
|
||||
end
|
||||
|
||||
class DynamicScopeMatchTest < ActiveRecord::TestCase
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user