Merge branch 'master' of git://github.com/rails/rails into rails

This commit is contained in:
Mikel Lindsaar
2010-01-19 21:05:37 +11:00
357 changed files with 6812 additions and 3144 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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]

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View 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

View 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

View File

@@ -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

View File

@@ -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

View 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

View 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

View File

@@ -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

View 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

View 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

View File

@@ -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

View File

@@ -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>

View File

@@ -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}
%>

View File

@@ -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" %>

View 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

View 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

View File

@@ -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:
#

View File

@@ -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]

View File

@@ -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)

View File

@@ -27,10 +27,10 @@ module ActionView
def debug(object)
begin
Marshal::dump(object)
"<pre class='debug_dump'>#{h(object.to_yaml).gsub(" ", "&nbsp; ")}</pre>"
"<pre class='debug_dump'>#{h(object.to_yaml).gsub(" ", "&nbsp; ")}</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

View File

@@ -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>
#

View File

@@ -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
#

View File

@@ -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!")

View File

@@ -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)

View File

@@ -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

View File

@@ -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"]

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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 = []

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -1,5 +1,5 @@
class Reply < ActiveRecord::Base
named_scope :base
scope :base
belongs_to :topic, :include => [:replies]
belongs_to :developer

View File

@@ -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.

View File

@@ -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)

View File

@@ -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

View File

@@ -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')

View File

@@ -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

View File

@@ -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:

View File

@@ -1,3 +1,5 @@
require 'active_support/core_ext/hash/slice'
module ActiveModel
module Validations
module ClassMethods

View File

@@ -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

View File

@@ -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]

View File

@@ -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

View File

@@ -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

View File

@@ -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?

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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(

View File

@@ -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:

View File

@@ -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

View File

@@ -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
#

View File

@@ -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

View File

@@ -12,7 +12,7 @@ module ActiveRecord
name = color(name, :magenta, true)
end
debug "#{name} #{sql}"
debug " #{name} #{sql}"
end
def odd?

View File

@@ -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?

View File

@@ -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}"

View File

@@ -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)

View File

@@ -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?

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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)

View File

@@ -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.
#

View File

@@ -1,4 +1,4 @@
require 'rails/generators/active_record'
require 'generators/active_record'
module ActiveRecord
module Generators

View File

@@ -1,4 +1,4 @@
require 'rails/generators/active_record'
require 'generators/active_record'
module ActiveRecord
module Generators

View File

@@ -1,4 +1,4 @@
require 'rails/generators/active_record'
require 'generators/active_record'
module ActiveRecord
module Generators

View File

@@ -1,4 +1,4 @@
require 'rails/generators/active_record'
require 'generators/active_record'
module ActiveRecord
module Generators

View File

@@ -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

View File

@@ -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))

View File

@@ -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)

View File

@@ -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

View File

@@ -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