Rails 2.1.1

Among other things, a security fix.
This commit is contained in:
Jacques Distler 2008-09-07 00:54:05 -05:00
parent d2c4c8737c
commit d4f97345db
354 changed files with 21027 additions and 3072 deletions

View file

@ -1,3 +1,8 @@
*2.1.1 (September 4th, 2008)*
* Included in Rails 2.1.1
*2.1.0 (May 31st, 2008)* *2.1.0 (May 31st, 2008)*
* Fixed that a return-path header would be ignored #7572 [joost] * Fixed that a return-path header would be ignored #7572 [joost]

View file

@ -5,6 +5,8 @@ require 'rake/rdoctask'
require 'rake/packagetask' require 'rake/packagetask'
require 'rake/gempackagetask' require 'rake/gempackagetask'
require 'rake/contrib/sshpublisher' require 'rake/contrib/sshpublisher'
require 'rake/contrib/rubyforgepublisher'
require File.join(File.dirname(__FILE__), 'lib', 'action_mailer', 'version') require File.join(File.dirname(__FILE__), 'lib', 'action_mailer', 'version')
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : '' PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
@ -35,7 +37,7 @@ Rake::RDocTask.new { |rdoc|
rdoc.title = "Action Mailer -- Easy email delivery and testing" rdoc.title = "Action Mailer -- Easy email delivery and testing"
rdoc.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object' rdoc.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
rdoc.options << '--charset' << 'utf-8' rdoc.options << '--charset' << 'utf-8'
rdoc.template = "#{ENV['template']}.rb" if ENV['template'] rdoc.template = ENV['template'] ? "#{ENV['template']}.rb" : '../doc/template/horo'
rdoc.rdoc_files.include('README', 'CHANGELOG') rdoc.rdoc_files.include('README', 'CHANGELOG')
rdoc.rdoc_files.include('lib/action_mailer.rb') rdoc.rdoc_files.include('lib/action_mailer.rb')
rdoc.rdoc_files.include('lib/action_mailer/*.rb') rdoc.rdoc_files.include('lib/action_mailer/*.rb')
@ -55,7 +57,7 @@ spec = Gem::Specification.new do |s|
s.rubyforge_project = "actionmailer" s.rubyforge_project = "actionmailer"
s.homepage = "http://www.rubyonrails.org" s.homepage = "http://www.rubyonrails.org"
s.add_dependency('actionpack', '= 2.1.0' + PKG_BUILD) s.add_dependency('actionpack', '= 2.1.1' + PKG_BUILD)
s.has_rdoc = true s.has_rdoc = true
s.requirements << 'none' s.requirements << 'none'
@ -76,12 +78,13 @@ end
desc "Publish the API documentation" desc "Publish the API documentation"
task :pgem => [:package] do task :pgem => [:package] do
Rake::SshFilePublisher.new("davidhh@wrath.rubyonrails.org", "public_html/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload Rake::SshFilePublisher.new("david@greed.loudthinking.com", "/u/sites/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
`ssh david@greed.loudthinking.com '/u/sites/gems/gemupdate.sh'`
end end
desc "Publish the API documentation" desc "Publish the API documentation"
task :pdoc => [:rdoc] do task :pdoc => [:rdoc] do
Rake::SshDirPublisher.new("davidhh@wrath.rubyonrails.org", "public_html/am", "doc").upload Rake::SshDirPublisher.new("wrath.rubyonrails.org", "public_html/am", "doc").upload
end end
desc "Publish the release files to RubyForge." desc "Publish the release files to RubyForge."

View file

@ -530,7 +530,7 @@ module ActionMailer #:nodoc:
end end
def render_message(method_name, body) def render_message(method_name, body)
render :file => method_name, :body => body render :file => method_name, :body => body, :use_full_path => true
end end
def render(opts) def render(opts)
@ -538,6 +538,7 @@ module ActionMailer #:nodoc:
if opts[:file] && opts[:file] !~ /\// if opts[:file] && opts[:file] !~ /\//
opts[:file] = "#{mailer_name}/#{opts[:file]}" opts[:file] = "#{mailer_name}/#{opts[:file]}"
end end
opts[:use_full_path] = true
initialize_template_class(body).render(opts) initialize_template_class(body).render(opts)
end end

View file

@ -2,7 +2,7 @@ module ActionMailer
module VERSION #:nodoc: module VERSION #:nodoc:
MAJOR = 2 MAJOR = 2
MINOR = 1 MINOR = 1
TINY = 0 TINY = 1
STRING = [MAJOR, MINOR, TINY].join('.') STRING = [MAJOR, MINOR, TINY].join('.')
end end

View file

@ -30,13 +30,20 @@ class Net::SMTP
end end
end end
# Wrap tests that use Mocha and skip if unavailable. def uses_gem(gem_name, test_name, version = '> 0')
def uses_mocha(test_name) require 'rubygems'
gem 'mocha', ">=0.5" gem gem_name.to_s, version
require 'stubba' require gem_name.to_s
yield yield
rescue Gem::LoadError rescue LoadError
$stderr.puts "Skipping #{test_name} tests (Mocha >= 0.5 is required). `gem install mocha` and try again." $stderr.puts "Skipping #{test_name} tests. `gem install #{gem_name}` and try again."
end
# Wrap tests that use Mocha and skip if unavailable.
unless defined? uses_mocha
def uses_mocha(test_name, &block)
uses_gem('mocha', test_name, '>= 0.5.5', &block)
end
end end
def set_delivery_method(delivery_method) def set_delivery_method(delivery_method)

View file

@ -1,3 +1,20 @@
*2.1.1 (September 4th, 2008)*
* All 2xx requests are considered successful [Josh Peek]
* Deprecate the limited follow_redirect in functional tests. If you wish to follow redirects, use integration tests. [Michael Koziarski]
* Fixed that AssetTagHelper#compute_public_path shouldn't cache the asset_host along with the source or per-request proc's won't run [DHH]
* Deprecate define_javascript_functions, javascript_include_tag and friends are much better [Michael Koziarski]
* Fix polymorphic_url with singleton resources. #461 [Tammer Saleh]
* Deprecate ActionView::Base.erb_variable. Use the concat helper method instead of appending to it directly. [Jeremy Kemper]
* Fixed Request#remote_ip to only raise hell if the HTTP_CLIENT_IP and HTTP_X_FORWARDED_FOR doesn't match (not just if they're both present) [Mark Imbriaco, Bradford Folkens]
*2.1.0 (May 31st, 2008)* *2.1.0 (May 31st, 2008)*
* InstanceTag#default_time_from_options overflows to DateTime [Geoff Buesing] * InstanceTag#default_time_from_options overflows to DateTime [Geoff Buesing]

View file

@ -5,6 +5,8 @@ require 'rake/rdoctask'
require 'rake/packagetask' require 'rake/packagetask'
require 'rake/gempackagetask' require 'rake/gempackagetask'
require 'rake/contrib/sshpublisher' require 'rake/contrib/sshpublisher'
require 'rake/contrib/rubyforgepublisher'
require File.join(File.dirname(__FILE__), 'lib', 'action_pack', 'version') require File.join(File.dirname(__FILE__), 'lib', 'action_pack', 'version')
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : '' PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
@ -49,12 +51,14 @@ Rake::RDocTask.new { |rdoc|
rdoc.title = "Action Pack -- On rails from request to response" rdoc.title = "Action Pack -- On rails from request to response"
rdoc.options << '--line-numbers' << '--inline-source' rdoc.options << '--line-numbers' << '--inline-source'
rdoc.options << '--charset' << 'utf-8' rdoc.options << '--charset' << 'utf-8'
rdoc.template = "#{ENV['template']}.rb" if ENV['template'] rdoc.template = ENV['template'] ? "#{ENV['template']}.rb" : '../doc/template/horo'
if ENV['DOC_FILES'] if ENV['DOC_FILES']
rdoc.rdoc_files.include(ENV['DOC_FILES'].split(/,\s*/)) rdoc.rdoc_files.include(ENV['DOC_FILES'].split(/,\s*/))
else else
rdoc.rdoc_files.include('README', 'RUNNING_UNIT_TESTS', 'CHANGELOG') rdoc.rdoc_files.include('README', 'RUNNING_UNIT_TESTS', 'CHANGELOG')
rdoc.rdoc_files.include('lib/**/*.rb') rdoc.rdoc_files.include(Dir['lib/**/*.rb'] -
Dir['lib/*/vendor/**/*.rb'])
rdoc.rdoc_files.exclude('lib/actionpack.rb')
end end
} }
@ -76,7 +80,7 @@ spec = Gem::Specification.new do |s|
s.has_rdoc = true s.has_rdoc = true
s.requirements << 'none' s.requirements << 'none'
s.add_dependency('activesupport', '= 2.1.0' + PKG_BUILD) s.add_dependency('activesupport', '= 2.1.1' + PKG_BUILD)
s.require_path = 'lib' s.require_path = 'lib'
s.autorequire = 'action_controller' s.autorequire = 'action_controller'
@ -132,13 +136,13 @@ task :update_js => [ :update_scriptaculous ]
desc "Publish the API documentation" desc "Publish the API documentation"
task :pgem => [:package] do task :pgem => [:package] do
Rake::SshFilePublisher.new("davidhh@wrath.rubyonrails.org", "public_html/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload Rake::SshFilePublisher.new("david@greed.loudthinking.com", "/u/sites/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
`ssh davidhh@wrath.rubyonrails.org './gemupdate.sh'` `ssh david@greed.loudthinking.com '/u/sites/gems/gemupdate.sh'`
end end
desc "Publish the API documentation" desc "Publish the API documentation"
task :pdoc => [:rdoc] do task :pdoc => [:rdoc] do
Rake::SshDirPublisher.new("davidhh@wrath.rubyonrails.org", "public_html/ap", "doc").upload Rake::SshDirPublisher.new("wrath.rubyonrails.org", "public_html/ap", "doc").upload
end end
desc "Publish the release files to RubyForge." desc "Publish the release files to RubyForge."

View file

@ -97,7 +97,7 @@ module ActionController
value['controller'] = value['controller'].to_s value['controller'] = value['controller'].to_s
if key == :actual && value['controller'].first != '/' && !value['controller'].include?('/') if key == :actual && value['controller'].first != '/' && !value['controller'].include?('/')
new_controller_path = ActionController::Routing.controller_relative_to(value['controller'], @controller.class.controller_path) new_controller_path = ActionController::Routing.controller_relative_to(value['controller'], @controller.class.controller_path)
value['controller'] = new_controller_path if value['controller'] != new_controller_path && ActionController::Routing.possible_controllers.include?(new_controller_path) value['controller'] = new_controller_path if value['controller'] != new_controller_path && ActionController::Routing.possible_controllers.include?(new_controller_path) && @response.redirected_to.is_a?(Hash)
end end
value['controller'] = value['controller'][1..-1] if value['controller'].first == '/' # strip leading hash value['controller'] = value['controller'][1..-1] if value['controller'].first == '/' # strip leading hash
end end

View file

@ -398,47 +398,31 @@ module ActionController
# # The same, but shorter. # # The same, but shorter.
# assert_select "ol>li", 4 # assert_select "ol>li", 4
def assert_select_rjs(*args, &block) def assert_select_rjs(*args, &block)
rjs_type = nil rjs_type = args.first.is_a?(Symbol) ? args.shift : nil
arg = args.shift id = args.first.is_a?(String) ? args.shift : nil
# If the first argument is a symbol, it's the type of RJS statement we're looking # If the first argument is a symbol, it's the type of RJS statement we're looking
# for (update, replace, insertion, etc). Otherwise, we're looking for just about # for (update, replace, insertion, etc). Otherwise, we're looking for just about
# any RJS statement. # any RJS statement.
if arg.is_a?(Symbol) if rjs_type
rjs_type = arg
if rjs_type == :insert if rjs_type == :insert
arg = args.shift position = args.shift
insertion = "insert_#{arg}".to_sym insertion = "insert_#{position}".to_sym
raise ArgumentError, "Unknown RJS insertion type #{arg}" unless RJS_STATEMENTS[insertion] raise ArgumentError, "Unknown RJS insertion type #{position}" unless RJS_STATEMENTS[insertion]
statement = "(#{RJS_STATEMENTS[insertion]})" statement = "(#{RJS_STATEMENTS[insertion]})"
else else
raise ArgumentError, "Unknown RJS statement type #{rjs_type}" unless RJS_STATEMENTS[rjs_type] raise ArgumentError, "Unknown RJS statement type #{rjs_type}" unless RJS_STATEMENTS[rjs_type]
statement = "(#{RJS_STATEMENTS[rjs_type]})" statement = "(#{RJS_STATEMENTS[rjs_type]})"
end end
arg = args.shift
else else
statement = "#{RJS_STATEMENTS[:any]}" statement = "#{RJS_STATEMENTS[:any]}"
end end
# Next argument we're looking for is the element identifier. If missing, we pick # Next argument we're looking for is the element identifier. If missing, we pick
# any element. # any element, otherwise we replace it in the statement.
if arg.is_a?(String) pattern = Regexp.new(
id = Regexp.quote(arg) id ? statement.gsub(RJS_ANY_ID, "\"#{id}\"") : statement
arg = args.shift )
else
id = "[^\"]*"
end
pattern =
case rjs_type
when :chained_replace, :chained_replace_html
Regexp.new("\\$\\(\"#{id}\"\\)#{statement}\\(#{RJS_PATTERN_HTML}\\)", Regexp::MULTILINE)
when :remove, :show, :hide, :toggle
Regexp.new("#{statement}\\(\"#{id}\"\\)")
else
Regexp.new("#{statement}\\(\"#{id}\", #{RJS_PATTERN_HTML}\\)", Regexp::MULTILINE)
end
# Duplicate the body since the next step involves destroying it. # Duplicate the body since the next step involves destroying it.
matches = nil matches = nil
@ -447,7 +431,7 @@ module ActionController
matches = @response.body.match(pattern) matches = @response.body.match(pattern)
else else
@response.body.gsub(pattern) do |match| @response.body.gsub(pattern) do |match|
html = unescape_rjs($2) html = unescape_rjs(match)
matches ||= [] matches ||= []
matches.concat HTML::Document.new(html).root.children.select { |n| n.tag? } matches.concat HTML::Document.new(html).root.children.select { |n| n.tag? }
"" ""
@ -577,27 +561,23 @@ module ActionController
protected protected
unless const_defined?(:RJS_STATEMENTS) unless const_defined?(:RJS_STATEMENTS)
RJS_STATEMENTS = { RJS_PATTERN_HTML = "\"((\\\\\"|[^\"])*)\""
:replace => /Element\.replace/, RJS_ANY_ID = "\"([^\"])*\""
:replace_html => /Element\.update/, RJS_STATEMENTS = {
:chained_replace => /\.replace/, :chained_replace => "\\$\\(#{RJS_ANY_ID}\\)\\.replace\\(#{RJS_PATTERN_HTML}\\)",
:chained_replace_html => /\.update/, :chained_replace_html => "\\$\\(#{RJS_ANY_ID}\\)\\.update\\(#{RJS_PATTERN_HTML}\\)",
:remove => /Element\.remove/, :replace_html => "Element\\.update\\(#{RJS_ANY_ID}, #{RJS_PATTERN_HTML}\\)",
:show => /Element\.show/, :replace => "Element\\.replace\\(#{RJS_ANY_ID}, #{RJS_PATTERN_HTML}\\)"
:hide => /Element\.hide/,
:toggle => /Element\.toggle/
} }
RJS_INSERTIONS = [:top, :bottom, :before, :after] [:remove, :show, :hide, :toggle].each do |action|
RJS_INSERTIONS.each do |insertion| RJS_STATEMENTS[action] = "Element\\.#{action}\\(#{RJS_ANY_ID}\\)"
RJS_STATEMENTS["insert_#{insertion}".to_sym] = Regexp.new(Regexp.quote("new Insertion.#{insertion.to_s.camelize}"))
end end
RJS_INSERTIONS = ["top", "bottom", "before", "after"]
RJS_INSERTIONS.each do |insertion|
RJS_STATEMENTS["insert_#{insertion}".to_sym] = "Element.insert\\(#{RJS_ANY_ID}, \\{ #{insertion}: #{RJS_PATTERN_HTML} \\}\\)"
end
RJS_STATEMENTS[:insert_html] = "Element.insert\\(#{RJS_ANY_ID}, \\{ (#{RJS_INSERTIONS.join('|')}): #{RJS_PATTERN_HTML} \\}\\)"
RJS_STATEMENTS[:any] = Regexp.new("(#{RJS_STATEMENTS.values.join('|')})") RJS_STATEMENTS[:any] = Regexp.new("(#{RJS_STATEMENTS.values.join('|')})")
RJS_STATEMENTS[:insert_html] = Regexp.new(RJS_INSERTIONS.collect do |insertion|
Regexp.quote("new Insertion.#{insertion.to_s.camelize}")
end.join('|'))
RJS_PATTERN_HTML = /"((\\"|[^"])*)"/
RJS_PATTERN_EVERYTHING = Regexp.new("#{RJS_STATEMENTS[:any]}\\(\"([^\"]*)\", #{RJS_PATTERN_HTML}\\)",
Regexp::MULTILINE)
RJS_PATTERN_UNICODE_ESCAPED_CHAR = /\\u([0-9a-zA-Z]{4})/ RJS_PATTERN_UNICODE_ESCAPED_CHAR = /\\u([0-9a-zA-Z]{4})/
end end
@ -611,8 +591,8 @@ module ActionController
root = HTML::Node.new(nil) root = HTML::Node.new(nil)
while true while true
next if body.sub!(RJS_PATTERN_EVERYTHING) do |match| next if body.sub!(RJS_STATEMENTS[:any]) do |match|
html = unescape_rjs($3) html = unescape_rjs(match)
matches = HTML::Document.new(html).root.children.select { |n| n.tag? } matches = HTML::Document.new(html).root.children.select { |n| n.tag? }
root.children.concat matches root.children.concat matches
"" ""

View file

@ -613,8 +613,9 @@ module ActionController #:nodoc:
# #
# This takes the current URL as is and only exchanges the action. In contrast, <tt>url_for :action => 'print'</tt> # This takes the current URL as is and only exchanges the action. In contrast, <tt>url_for :action => 'print'</tt>
# would have slashed-off the path components after the changed action. # would have slashed-off the path components after the changed action.
def url_for(options = nil) #:doc: def url_for(options = {})
case options || {} options ||= {}
case options
when String when String
options options
when Hash when Hash
@ -743,6 +744,9 @@ module ActionController #:nodoc:
# # Renders the template located in [TEMPLATE_ROOT]/weblog/show.r(html|xml) (in Rails, app/views/weblog/show.erb) # # Renders the template located in [TEMPLATE_ROOT]/weblog/show.r(html|xml) (in Rails, app/views/weblog/show.erb)
# render :template => "weblog/show" # render :template => "weblog/show"
# #
# # Renders the template with a local variable
# render :template => "weblog/show", :locals => {:customer => Customer.new}
#
# === Rendering a file # === Rendering a file
# #
# File rendering works just like action rendering except that it takes a filesystem path. By default, the path # File rendering works just like action rendering except that it takes a filesystem path. By default, the path
@ -865,7 +869,7 @@ module ActionController #:nodoc:
render_for_file(file, options[:status], options[:use_full_path], options[:locals] || {}) render_for_file(file, options[:status], options[:use_full_path], options[:locals] || {})
elsif template = options[:template] elsif template = options[:template]
render_for_file(template, options[:status], true) render_for_file(template, options[:status], true, options[:locals] || {})
elsif inline = options[:inline] elsif inline = options[:inline]
add_variables_to_assigns add_variables_to_assigns
@ -1147,7 +1151,7 @@ module ActionController #:nodoc:
def log_processing def log_processing
if logger && logger.info? if logger && logger.info?
logger.info "\n\nProcessing #{controller_class_name}\##{action_name} (for #{request_origin}) [#{request.method.to_s.upcase}]" logger.info "\n\nProcessing #{self.class.name}\##{action_name} (for #{request_origin}) [#{request.method.to_s.upcase}]"
logger.info " Session ID: #{@_session.session_id}" if @_session and @_session.respond_to?(:session_id) logger.info " Session ID: #{@_session.session_id}" if @_session and @_session.respond_to?(:session_id)
logger.info " Parameters: #{respond_to?(:filter_parameters) ? filter_parameters(params).inspect : params.inspect}" logger.info " Parameters: #{respond_to?(:filter_parameters) ? filter_parameters(params).inspect : params.inspect}"
end end

View file

@ -135,7 +135,7 @@ module ActionController
# be reloaded on the next request without restarting the server. # be reloaded on the next request without restarting the server.
def cleanup_application def cleanup_application
ActiveRecord::Base.reset_subclasses if defined?(ActiveRecord) ActiveRecord::Base.reset_subclasses if defined?(ActiveRecord)
Dependencies.clear ActiveSupport::Dependencies.clear
ActiveRecord::Base.clear_reloadable_connections! if defined?(ActiveRecord) ActiveRecord::Base.clear_reloadable_connections! if defined?(ActiveRecord)
end end

View file

@ -7,6 +7,200 @@ module ActionController #:nodoc:
end end
end end
class FilterChain < ActiveSupport::Callbacks::CallbackChain #:nodoc:
def append_filter_to_chain(filters, filter_type, &block)
pos = find_filter_append_position(filters, filter_type)
update_filter_chain(filters, filter_type, pos, &block)
end
def prepend_filter_to_chain(filters, filter_type, &block)
pos = find_filter_prepend_position(filters, filter_type)
update_filter_chain(filters, filter_type, pos, &block)
end
def create_filters(filters, filter_type, &block)
filters, conditions = extract_options(filters, &block)
filters.map! { |filter| find_or_create_filter(filter, filter_type, conditions) }
filters
end
def skip_filter_in_chain(*filters, &test)
filters, conditions = extract_options(filters)
filters.each do |filter|
if callback = find(filter) then delete(callback) end
end if conditions.empty?
update_filter_in_chain(filters, :skip => conditions, &test)
end
private
def update_filter_chain(filters, filter_type, pos, &block)
new_filters = create_filters(filters, filter_type, &block)
insert(pos, new_filters).flatten!
end
def find_filter_append_position(filters, filter_type)
# appending an after filter puts it at the end of the call chain
# before and around filters go before the first after filter in the chain
unless filter_type == :after
each_with_index do |f,i|
return i if f.after?
end
end
return -1
end
def find_filter_prepend_position(filters, filter_type)
# prepending a before or around filter puts it at the front of the call chain
# after filters go before the first after filter in the chain
if filter_type == :after
each_with_index do |f,i|
return i if f.after?
end
return -1
end
return 0
end
def find_or_create_filter(filter, filter_type, options = {})
update_filter_in_chain([filter], options)
if found_filter = find(filter) { |f| f.type == filter_type }
found_filter
else
filter_kind = case
when filter.respond_to?(:before) && filter_type == :before
:before
when filter.respond_to?(:after) && filter_type == :after
:after
else
:filter
end
case filter_type
when :before
BeforeFilter.new(filter_kind, filter, options)
when :after
AfterFilter.new(filter_kind, filter, options)
else
AroundFilter.new(filter_kind, filter, options)
end
end
end
def update_filter_in_chain(filters, options, &test)
filters.map! { |f| block_given? ? find(f, &test) : find(f) }
filters.compact!
map! do |filter|
if filters.include?(filter)
new_filter = filter.dup
new_filter.options.merge!(options)
new_filter
else
filter
end
end
end
end
class Filter < ActiveSupport::Callbacks::Callback #:nodoc:
def before?
self.class == BeforeFilter
end
def after?
self.class == AfterFilter
end
def around?
self.class == AroundFilter
end
private
def should_not_skip?(controller)
if options[:skip]
!included_in_action?(controller, options[:skip])
else
true
end
end
def included_in_action?(controller, options)
if options[:only]
Array(options[:only]).map(&:to_s).include?(controller.action_name)
elsif options[:except]
!Array(options[:except]).map(&:to_s).include?(controller.action_name)
else
true
end
end
def should_run_callback?(controller)
should_not_skip?(controller) && included_in_action?(controller, options) && super
end
end
class AroundFilter < Filter #:nodoc:
def type
:around
end
def call(controller, &block)
if should_run_callback?(controller)
method = filter_responds_to_before_and_after? ? around_proc : self.method
# For around_filter do |controller, action|
if method.is_a?(Proc) && method.arity == 2
evaluate_method(method, controller, block)
else
evaluate_method(method, controller, &block)
end
else
block.call
end
end
private
def filter_responds_to_before_and_after?
method.respond_to?(:before) && method.respond_to?(:after)
end
def around_proc
Proc.new do |controller, action|
method.before(controller)
if controller.send!(:performed?)
controller.send!(:halt_filter_chain, method, :rendered_or_redirected)
else
begin
action.call
ensure
method.after(controller)
end
end
end
end
end
class BeforeFilter < Filter #:nodoc:
def type
:before
end
def call(controller, &block)
super
if controller.send!(:performed?)
controller.send!(:halt_filter_chain, method, :rendered_or_redirected)
end
end
end
class AfterFilter < Filter #:nodoc:
def type
:after
end
end
# Filters enable controllers to run shared pre- and post-processing code for its actions. These filters can be used to do # Filters enable controllers to run shared pre- and post-processing code for its actions. These filters can be used to do
# authentication, caching, or auditing before the intended action is performed. Or to do localization or output # authentication, caching, or auditing before the intended action is performed. Or to do localization or output
# compression after the action has been performed. Filters have access to the request, response, and all the instance # compression after the action has been performed. Filters have access to the request, response, and all the instance
@ -245,201 +439,6 @@ module ActionController #:nodoc:
# filter and controller action will not be run. If +before+ renders or redirects, # filter and controller action will not be run. If +before+ renders or redirects,
# the second half of +around+ and will still run but +after+ and the # the second half of +around+ and will still run but +after+ and the
# action will not. If +around+ fails to yield, +after+ will not be run. # action will not. If +around+ fails to yield, +after+ will not be run.
class FilterChain < ActiveSupport::Callbacks::CallbackChain #:nodoc:
def append_filter_to_chain(filters, filter_type, &block)
pos = find_filter_append_position(filters, filter_type)
update_filter_chain(filters, filter_type, pos, &block)
end
def prepend_filter_to_chain(filters, filter_type, &block)
pos = find_filter_prepend_position(filters, filter_type)
update_filter_chain(filters, filter_type, pos, &block)
end
def create_filters(filters, filter_type, &block)
filters, conditions = extract_options(filters, &block)
filters.map! { |filter| find_or_create_filter(filter, filter_type, conditions) }
filters
end
def skip_filter_in_chain(*filters, &test)
filters, conditions = extract_options(filters)
filters.each do |filter|
if callback = find(filter) then delete(callback) end
end if conditions.empty?
update_filter_in_chain(filters, :skip => conditions, &test)
end
private
def update_filter_chain(filters, filter_type, pos, &block)
new_filters = create_filters(filters, filter_type, &block)
insert(pos, new_filters).flatten!
end
def find_filter_append_position(filters, filter_type)
# appending an after filter puts it at the end of the call chain
# before and around filters go before the first after filter in the chain
unless filter_type == :after
each_with_index do |f,i|
return i if f.after?
end
end
return -1
end
def find_filter_prepend_position(filters, filter_type)
# prepending a before or around filter puts it at the front of the call chain
# after filters go before the first after filter in the chain
if filter_type == :after
each_with_index do |f,i|
return i if f.after?
end
return -1
end
return 0
end
def find_or_create_filter(filter, filter_type, options = {})
update_filter_in_chain([filter], options)
if found_filter = find(filter) { |f| f.type == filter_type }
found_filter
else
filter_kind = case
when filter.respond_to?(:before) && filter_type == :before
:before
when filter.respond_to?(:after) && filter_type == :after
:after
else
:filter
end
case filter_type
when :before
BeforeFilter.new(filter_kind, filter, options)
when :after
AfterFilter.new(filter_kind, filter, options)
else
AroundFilter.new(filter_kind, filter, options)
end
end
end
def update_filter_in_chain(filters, options, &test)
filters.map! { |f| block_given? ? find(f, &test) : find(f) }
filters.compact!
map! do |filter|
if filters.include?(filter)
new_filter = filter.dup
new_filter.options.merge!(options)
new_filter
else
filter
end
end
end
end
class Filter < ActiveSupport::Callbacks::Callback #:nodoc:
def before?
self.class == BeforeFilter
end
def after?
self.class == AfterFilter
end
def around?
self.class == AroundFilter
end
private
def should_not_skip?(controller)
if options[:skip]
!included_in_action?(controller, options[:skip])
else
true
end
end
def included_in_action?(controller, options)
if options[:only]
Array(options[:only]).map(&:to_s).include?(controller.action_name)
elsif options[:except]
!Array(options[:except]).map(&:to_s).include?(controller.action_name)
else
true
end
end
def should_run_callback?(controller)
should_not_skip?(controller) && included_in_action?(controller, options) && super
end
end
class AroundFilter < Filter #:nodoc:
def type
:around
end
def call(controller, &block)
if should_run_callback?(controller)
method = filter_responds_to_before_and_after? ? around_proc : self.method
# For around_filter do |controller, action|
if method.is_a?(Proc) && method.arity == 2
evaluate_method(method, controller, block)
else
evaluate_method(method, controller, &block)
end
else
block.call
end
end
private
def filter_responds_to_before_and_after?
method.respond_to?(:before) && method.respond_to?(:after)
end
def around_proc
Proc.new do |controller, action|
method.before(controller)
if controller.send!(:performed?)
controller.send!(:halt_filter_chain, method, :rendered_or_redirected)
else
begin
action.call
ensure
method.after(controller)
end
end
end
end
end
class BeforeFilter < Filter #:nodoc:
def type
:before
end
def call(controller, &block)
super
if controller.send!(:performed?)
controller.send!(:halt_filter_chain, method, :rendered_or_redirected)
end
end
end
class AfterFilter < Filter #:nodoc:
def type
:after
end
end
module ClassMethods module ClassMethods
# The passed <tt>filters</tt> will be appended to the filter_chain and # The passed <tt>filters</tt> will be appended to the filter_chain and
# will execute before the action on this controller is performed. # will execute before the action on this controller is performed.

View file

@ -48,6 +48,9 @@ module ActionController
# #
# # calls post_url(post) # # calls post_url(post)
# polymorphic_url(post) # => "http://example.com/posts/1" # polymorphic_url(post) # => "http://example.com/posts/1"
# polymorphic_url([blog, post]) # => "http://example.com/blogs/1/posts/1"
# polymorphic_url([:admin, blog, post]) # => "http://example.com/admin/blogs/1/posts/1"
# polymorphic_url([user, :blog, post]) # => "http://example.com/users/1/blog/posts/1"
# #
# ==== Options # ==== Options
# #
@ -83,8 +86,6 @@ module ActionController
else [ record_or_hash_or_array ] else [ record_or_hash_or_array ]
end end
args << format if format
inflection = inflection =
case case
when options[:action].to_s == "new" when options[:action].to_s == "new"
@ -96,6 +97,9 @@ module ActionController
else else
:singular :singular
end end
args.delete_if {|arg| arg.is_a?(Symbol) || arg.is_a?(String)}
args << format if format
named_route = build_named_route_call(record_or_hash_or_array, namespace, inflection, options) named_route = build_named_route_call(record_or_hash_or_array, namespace, inflection, options)
send!(named_route, *args) send!(named_route, *args)
@ -136,11 +140,19 @@ module ActionController
else else
record = records.pop record = records.pop
route = records.inject("") do |string, parent| route = records.inject("") do |string, parent|
string << "#{RecordIdentifier.send!("singular_class_name", parent)}_" if parent.is_a?(Symbol) || parent.is_a?(String)
string << "#{parent}_"
else
string << "#{RecordIdentifier.send!("singular_class_name", parent)}_"
end
end end
end end
route << "#{RecordIdentifier.send!("#{inflection}_class_name", record)}_" if record.is_a?(Symbol) || record.is_a?(String)
route << "#{record}_"
else
route << "#{RecordIdentifier.send!("#{inflection}_class_name", record)}_"
end
action_prefix(options) + namespace + route + routing_type(options).to_s action_prefix(options) + namespace + route + routing_type(options).to_s
end end
@ -163,16 +175,17 @@ module ActionController
end end
end end
# Remove the first symbols from the array and return the url prefix
# implied by those symbols.
def extract_namespace(record_or_hash_or_array) def extract_namespace(record_or_hash_or_array)
returning "" do |namespace| return "" unless record_or_hash_or_array.is_a?(Array)
if record_or_hash_or_array.is_a?(Array)
record_or_hash_or_array.delete_if do |record_or_namespace| namespace_keys = []
if record_or_namespace.is_a?(String) || record_or_namespace.is_a?(Symbol) while (key = record_or_hash_or_array.first) && key.is_a?(String) || key.is_a?(Symbol)
namespace << "#{record_or_namespace}_" namespace_keys << record_or_hash_or_array.shift
end
end
end
end end
namespace_keys.map {|k| "#{k}_"}.join
end end
end end
end end

View file

@ -31,18 +31,21 @@ module ActionController
module RecordIdentifier module RecordIdentifier
extend self extend self
JOIN = '_'.freeze
NEW = 'new'.freeze
# Returns plural/singular for a record or class. Example: # Returns plural/singular for a record or class. Example:
# #
# partial_path(post) # => "posts/post" # partial_path(post) # => "posts/post"
# partial_path(Person) # => "people/person" # partial_path(Person) # => "people/person"
# partial_path(Person, "admin/games") # => "admin/people/person" # partial_path(Person, "admin/games") # => "admin/people/person"
def partial_path(record_or_class, controller_path = nil) def partial_path(record_or_class, controller_path = nil)
klass = class_from_record_or_class(record_or_class) name = model_name_from_record_or_class(record_or_class)
if controller_path && controller_path.include?("/") if controller_path && controller_path.include?("/")
"#{File.dirname(controller_path)}/#{klass.name.tableize}/#{klass.name.demodulize.underscore}" "#{File.dirname(controller_path)}/#{name.partial_path}"
else else
"#{klass.name.tableize}/#{klass.name.demodulize.underscore}" name.partial_path
end end
end end
@ -56,21 +59,25 @@ module ActionController
# dom_class(post, :edit) # => "edit_post" # dom_class(post, :edit) # => "edit_post"
# dom_class(Person, :edit) # => "edit_person" # dom_class(Person, :edit) # => "edit_person"
def dom_class(record_or_class, prefix = nil) def dom_class(record_or_class, prefix = nil)
[ prefix, singular_class_name(record_or_class) ].compact * '_' singular = singular_class_name(record_or_class)
prefix ? "#{prefix}#{JOIN}#{singular}" : singular
end end
# The DOM id convention is to use the singular form of an object or class with the id following an underscore. # The DOM id convention is to use the singular form of an object or class with the id following an underscore.
# If no id is found, prefix with "new_" instead. Examples: # If no id is found, prefix with "new_" instead. Examples:
# #
# dom_id(Post.new(:id => 45)) # => "post_45" # dom_id(Post.find(45)) # => "post_45"
# dom_id(Post.new) # => "new_post" # dom_id(Post.new) # => "new_post"
# #
# If you need to address multiple instances of the same class in the same view, you can prefix the dom_id: # If you need to address multiple instances of the same class in the same view, you can prefix the dom_id:
# #
# dom_id(Post.new(:id => 45), :edit) # => "edit_post_45" # dom_id(Post.find(45), :edit) # => "edit_post_45"
def dom_id(record, prefix = nil) def dom_id(record, prefix = nil)
prefix ||= 'new' unless record.id if record_id = record.id
[ prefix, singular_class_name(record), record.id ].compact * '_' "#{dom_class(record, prefix)}#{JOIN}#{record_id}"
else
dom_class(record, prefix || NEW)
end
end end
# Returns the plural class name of a record or class. Examples: # Returns the plural class name of a record or class. Examples:
@ -78,7 +85,7 @@ module ActionController
# plural_class_name(post) # => "posts" # plural_class_name(post) # => "posts"
# plural_class_name(Highrise::Person) # => "highrise_people" # plural_class_name(Highrise::Person) # => "highrise_people"
def plural_class_name(record_or_class) def plural_class_name(record_or_class)
singular_class_name(record_or_class).pluralize model_name_from_record_or_class(record_or_class).plural
end end
# Returns the singular class name of a record or class. Examples: # Returns the singular class name of a record or class. Examples:
@ -86,12 +93,12 @@ module ActionController
# singular_class_name(post) # => "post" # singular_class_name(post) # => "post"
# singular_class_name(Highrise::Person) # => "highrise_person" # singular_class_name(Highrise::Person) # => "highrise_person"
def singular_class_name(record_or_class) def singular_class_name(record_or_class)
class_from_record_or_class(record_or_class).name.underscore.tr('/', '_') model_name_from_record_or_class(record_or_class).singular
end end
private private
def class_from_record_or_class(record_or_class) def model_name_from_record_or_class(record_or_class)
record_or_class.is_a?(Class) ? record_or_class : record_or_class.class (record_or_class.is_a?(Class) ? record_or_class : record_or_class.class).model_name
end end
end end
end end

View file

@ -134,14 +134,17 @@ module ActionController
# REMOTE_ADDR is a proxy. HTTP_X_FORWARDED_FOR may be a comma- # REMOTE_ADDR is a proxy. HTTP_X_FORWARDED_FOR may be a comma-
# delimited list in the case of multiple chained proxies; the last # delimited list in the case of multiple chained proxies; the last
# address which is not trusted is the originating IP. # address which is not trusted is the originating IP.
def remote_ip def remote_ip
if TRUSTED_PROXIES !~ @env['REMOTE_ADDR'] remote_addr_list = @env['REMOTE_ADDR'] && @env['REMOTE_ADDR'].split(',').collect(&:strip)
return @env['REMOTE_ADDR']
unless remote_addr_list.blank?
not_trusted_addrs = remote_addr_list.reject {|addr| addr =~ TRUSTED_PROXIES}
return not_trusted_addrs.first unless not_trusted_addrs.empty?
end end
remote_ips = @env['HTTP_X_FORWARDED_FOR'] && @env['HTTP_X_FORWARDED_FOR'].split(',')
if @env.include? 'HTTP_CLIENT_IP' if @env.include? 'HTTP_CLIENT_IP'
if @env.include? 'HTTP_X_FORWARDED_FOR' if remote_ips && !remote_ips.include?(@env['HTTP_CLIENT_IP'])
# We don't know which came from the proxy, and which from the user # We don't know which came from the proxy, and which from the user
raise ActionControllerError.new(<<EOM) raise ActionControllerError.new(<<EOM)
IP spoofing attack?! IP spoofing attack?!
@ -149,11 +152,11 @@ HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect}
HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect} HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}
EOM EOM
end end
return @env['HTTP_CLIENT_IP'] return @env['HTTP_CLIENT_IP']
end end
if @env.include? 'HTTP_X_FORWARDED_FOR' then if remote_ips
remote_ips = @env['HTTP_X_FORWARDED_FOR'].split(',')
while remote_ips.size > 1 && TRUSTED_PROXIES =~ remote_ips.last.strip while remote_ips.size > 1 && TRUSTED_PROXIES =~ remote_ips.last.strip
remote_ips.pop remote_ips.pop
end end

View file

@ -88,6 +88,10 @@ module ActionController
# #
# map.connect ':controller/:action/:id', :action => 'show', :defaults => { :page => 'Dashboard' } # map.connect ':controller/:action/:id', :action => 'show', :defaults => { :page => 'Dashboard' }
# #
# Note: The default routes, as provided by the Rails generator, make all actions in every
# controller accessible via GET requests. You should consider removing them or commenting
# them out if you're using named routes and resources.
#
# == Named routes # == Named routes
# #
# Routes can be named with the syntax <tt>map.name_of_route options</tt>, # Routes can be named with the syntax <tt>map.name_of_route options</tt>,
@ -369,7 +373,7 @@ module ActionController
Routes = RouteSet.new Routes = RouteSet.new
::Inflector.module_eval do ActiveSupport::Inflector.module_eval do
# Ensures that routes are reloaded when Rails inflections are updated. # Ensures that routes are reloaded when Rails inflections are updated.
def inflections_with_route_reloading(&block) def inflections_with_route_reloading(&block)
returning(inflections_without_route_reloading(&block)) { returning(inflections_without_route_reloading(&block)) {

View file

@ -67,10 +67,9 @@ module ActionController
options = options.dup options = options.dup
if options[:namespace] if options[:namespace]
options[:controller] = "#{options[:path_prefix]}/#{options[:controller]}" options[:controller] = "#{options.delete(:namespace).sub(/\/$/, '')}/#{options[:controller]}"
options.delete(:path_prefix) options.delete(:path_prefix)
options.delete(:name_prefix) options.delete(:name_prefix)
options.delete(:namespace)
end end
requirements = (options.delete(:requirements) || {}).dup requirements = (options.delete(:requirements) || {}).dup

View file

@ -248,7 +248,7 @@ module ActionController
end end
def extract_value def extract_value
"#{local_name} = hash[:#{key}] && hash[:#{key}].collect { |path_component| CGI.escape(path_component.to_param, ActionController::Routing::Segment::UNSAFE_PCHAR) }.to_param #{"|| #{default.inspect}" if default}" "#{local_name} = hash[:#{key}] && Array(hash[:#{key}]).collect { |path_component| CGI.escape(path_component.to_param, ActionController::Routing::Segment::UNSAFE_PCHAR) }.to_param #{"|| #{default.inspect}" if default}"
end end
def default def default

View file

@ -1,4 +1,4 @@
<html> <html xmlns="http://www.w3.org/1999/xhtml">
<head> <head>
<title>Action Controller: Exception caught</title> <title>Action Controller: Exception caught</title>
<style> <style>

View file

@ -171,7 +171,7 @@ module ActionController #:nodoc:
# Was the response successful? # Was the response successful?
def success? def success?
response_code == 200 (200..299).include?(response_code)
end end
# Was the URL not found? # Was the URL not found?
@ -333,7 +333,7 @@ module ActionController #:nodoc:
attr_reader :original_filename attr_reader :original_filename
# The content type of the "uploaded" file # The content type of the "uploaded" file
attr_reader :content_type attr_accessor :content_type
def initialize(path, content_type = Mime::TEXT, binary = false) def initialize(path, content_type = Mime::TEXT, binary = false)
raise "#{path} file does not exist" unless File.exist?(path) raise "#{path} file does not exist" unless File.exist?(path)
@ -413,6 +413,8 @@ module ActionController #:nodoc:
get(@response.redirected_to.delete(:action), @response.redirected_to.stringify_keys) get(@response.redirected_to.delete(:action), @response.redirected_to.stringify_keys)
end end
deprecate :follow_redirect => "If you wish to follow redirects, you should use integration tests"
def assigns(key = nil) def assigns(key = nil)
if key.nil? if key.nil?
@response.template.assigns @response.template.assigns

View file

@ -17,7 +17,7 @@ module HTML #:nodoc:
@root = Node.new(nil) @root = Node.new(nil)
node_stack = [ @root ] node_stack = [ @root ]
while token = tokenizer.next while token = tokenizer.next
node = Node.parse(node_stack.last, tokenizer.line, tokenizer.position, token) node = Node.parse(node_stack.last, tokenizer.line, tokenizer.position, token, strict)
node_stack.last.children << node unless node.tag? && node.closing == :close node_stack.last.children << node unless node.tag? && node.closing == :close
if node.tag? if node.tag?

View file

@ -116,7 +116,7 @@ module ActionController #:nodoc:
end end
def apply_redirect_to(redirect_to_option) # :nodoc: def apply_redirect_to(redirect_to_option) # :nodoc:
redirect_to_option.is_a?(Symbol) ? self.send!(redirect_to_option) : redirect_to_option (redirect_to_option.is_a?(Symbol) && redirect_to_option != :back) ? self.send!(redirect_to_option) : redirect_to_option
end end
def apply_remaining_actions(options) # :nodoc: def apply_remaining_actions(options) # :nodoc:

View file

@ -2,7 +2,7 @@ module ActionPack #:nodoc:
module VERSION #:nodoc: module VERSION #:nodoc:
MAJOR = 2 MAJOR = 2
MINOR = 1 MINOR = 1
TINY = 0 TINY = 1
STRING = [MAJOR, MINOR, TINY].join('.') STRING = [MAJOR, MINOR, TINY].join('.')
end end

View file

@ -178,10 +178,13 @@ module ActionView #:nodoc:
# that alert()s the caught exception (and then re-raises it). # that alert()s the caught exception (and then re-raises it).
@@debug_rjs = false @@debug_rjs = false
cattr_accessor :debug_rjs cattr_accessor :debug_rjs
@@erb_variable = '_erbout' @@erb_variable = '_erbout'
cattr_accessor :erb_variable cattr_accessor :erb_variable
class << self
deprecate :erb_variable= => 'The erb variable will no longer be configurable. Use the concat helper method instead of appending to it directly.'
end
attr_internal :request attr_internal :request
delegate :request_forgery_protection_token, :template, :params, :session, :cookies, :response, :headers, delegate :request_forgery_protection_token, :template, :params, :session, :cookies, :response, :headers,
@ -253,6 +256,7 @@ If you are rendering a subtemplate, you must now use controller-like partial syn
elsif options == :update elsif options == :update
update_page(&block) update_page(&block)
elsif options.is_a?(Hash) elsif options.is_a?(Hash)
use_full_path = options[:use_full_path]
options = options.reverse_merge(:locals => {}, :use_full_path => true) options = options.reverse_merge(:locals => {}, :use_full_path => true)
if partial_layout = options.delete(:layout) if partial_layout = options.delete(:layout)
@ -266,7 +270,7 @@ If you are rendering a subtemplate, you must now use controller-like partial syn
end end
end end
elsif options[:file] elsif options[:file]
render_file(options[:file], options[:use_full_path], options[:locals]) render_file(options[:file], use_full_path || false, options[:locals])
elsif options[:partial] && options[:collection] elsif options[:partial] && options[:collection]
render_partial_collection(options[:partial], options[:collection], options[:spacer_template], options[:locals]) render_partial_collection(options[:partial], options[:collection], options[:spacer_template], options[:locals])
elsif options[:partial] elsif options[:partial]

View file

@ -485,21 +485,24 @@ module ActionView
source = "#{@controller.request.relative_url_root}#{source}" source = "#{@controller.request.relative_url_root}#{source}"
end end
end end
source = rewrite_asset_path(source)
if include_host rewrite_asset_path(source)
host = compute_asset_host(source)
if has_request && !host.blank? && host !~ %r{^[-a-z]+://}
host = "#{@controller.request.protocol}#{host}"
end
"#{host}#{source}"
else
source
end
end end
end end
source = ActionView::Base.computed_public_paths[cache_key]
if include_host && source !~ %r{^[-a-z]+://}
host = compute_asset_host(source)
if has_request && !host.blank? && host !~ %r{^[-a-z]+://}
host = "#{@controller.request.protocol}#{host}"
end
"#{host}#{source}"
else
source
end
end end
# Pick an asset host for this source. Returns +nil+ if no host is set, # Pick an asset host for this source. Returns +nil+ if no host is set,

View file

@ -696,15 +696,15 @@ module ActionView
class FormBuilder class FormBuilder
def date_select(method, options = {}, html_options = {}) def date_select(method, options = {}, html_options = {})
@template.date_select(@object_name, method, options.merge(:object => @object)) @template.date_select(@object_name, method, options.merge(:object => @object), html_options)
end end
def time_select(method, options = {}, html_options = {}) def time_select(method, options = {}, html_options = {})
@template.time_select(@object_name, method, options.merge(:object => @object)) @template.time_select(@object_name, method, options.merge(:object => @object), html_options)
end end
def datetime_select(method, options = {}, html_options = {}) def datetime_select(method, options = {}, html_options = {})
@template.datetime_select(@object_name, method, options.merge(:object => @object)) @template.datetime_select(@object_name, method, options.merge(:object => @object), html_options)
end end
end end
end end

View file

@ -601,7 +601,11 @@ module ActionView
end end
def object def object
@object || (@template_object.instance_variable_get("@#{@object_name}") rescue nil) @object || @template_object.instance_variable_get("@#{@object_name}")
rescue NameError
# As @object_name may contain the nested syntax (item[subobject]) we
# need to fallback to nil.
nil
end end
def value(object) def value(object)

View file

@ -304,7 +304,7 @@ module ActionView
# #
# NOTE: Only the option tags are returned, you have to wrap this call in # NOTE: Only the option tags are returned, you have to wrap this call in
# a regular HTML select tag. # a regular HTML select tag.
def time_zone_options_for_select(selected = nil, priority_zones = nil, model = TimeZone) def time_zone_options_for_select(selected = nil, priority_zones = nil, model = ::ActiveSupport::TimeZone)
zone_options = "" zone_options = ""
zones = model.all zones = model.all
@ -417,7 +417,7 @@ module ActionView
value = value(object) value = value(object)
content_tag("select", content_tag("select",
add_options( add_options(
time_zone_options_for_select(value || options[:default], priority_zones, options[:model] || TimeZone), time_zone_options_for_select(value || options[:default], priority_zones, options[:model] || ActiveSupport::TimeZone),
options, value options, value
), html_options ), html_options
) )

View file

@ -129,7 +129,7 @@ module ActionView
# label_tag 'name', nil, :class => 'small_label' # label_tag 'name', nil, :class => 'small_label'
# # => <label for="name" class="small_label">Name</label> # # => <label for="name" class="small_label">Name</label>
def label_tag(name, text = nil, options = {}) def label_tag(name, text = nil, options = {})
content_tag :label, text || name.humanize, { "for" => name }.update(options.stringify_keys) content_tag :label, text || name.to_s.humanize, { "for" => name }.update(options.stringify_keys)
end end
# Creates a hidden form input field used to transmit data that would be lost due to HTTP's statelessness or # Creates a hidden form input field used to transmit data that would be lost due to HTTP's statelessness or
@ -348,11 +348,13 @@ module ActionView
options.stringify_keys! options.stringify_keys!
if disable_with = options.delete("disable_with") if disable_with = options.delete("disable_with")
disable_with = "this.value='#{disable_with}'"
disable_with << ";#{options.delete('onclick')}" if options['onclick']
options["onclick"] = [ options["onclick"] = [
"this.setAttribute('originalValue', this.value)", "this.setAttribute('originalValue', this.value)",
"this.disabled=true", "this.disabled=true",
"this.value='#{disable_with}'", disable_with,
"#{options["onclick"]}",
"result = (this.form.onsubmit ? (this.form.onsubmit() ? this.form.submit() : false) : this.form.submit())", "result = (this.form.onsubmit ? (this.form.onsubmit() ? this.form.submit() : false) : this.form.submit())",
"if (result == false) { this.value = this.getAttribute('originalValue'); this.disabled = false }", "if (result == false) { this.value = this.getAttribute('originalValue'); this.disabled = false }",
"return result;", "return result;",

View file

@ -80,10 +80,9 @@ module ActionView
# return false;">Show me more</a> # return false;">Show me more</a>
# #
def link_to_function(name, *args, &block) def link_to_function(name, *args, &block)
html_options = args.extract_options! html_options = args.extract_options!.symbolize_keys
function = args[0] || '' function = args[0] || ''
html_options.symbolize_keys!
function = update_page(&block) if block_given? function = update_page(&block) if block_given?
content_tag( content_tag(
"a", name, "a", name,
@ -111,10 +110,9 @@ module ActionView
# page[:details].visual_effect :toggle_slide # page[:details].visual_effect :toggle_slide
# end # end
def button_to_function(name, *args, &block) def button_to_function(name, *args, &block)
html_options = args.extract_options! html_options = args.extract_options!.symbolize_keys
function = args[0] || '' function = args[0] || ''
html_options.symbolize_keys!
function = update_page(&block) if block_given? function = update_page(&block) if block_given?
tag(:input, html_options.merge({ tag(:input, html_options.merge({
:type => "button", :value => name, :type => "button", :value => name,
@ -147,6 +145,8 @@ module ActionView
javascript << '</script>' javascript << '</script>'
end end
deprecate :define_javascript_functions=>"use javascript_include_tag instead"
# Escape carrier returns and single and double quotes for JavaScript segments. # Escape carrier returns and single and double quotes for JavaScript segments.
def escape_javascript(javascript) def escape_javascript(javascript)
(javascript || '').gsub('\\','\0\0').gsub('</','<\/').gsub(/\r\n|\n|\r/, "\\n").gsub(/["']/) { |m| "\\#{m}" } (javascript || '').gsub('\\','\0\0').gsub('</','<\/').gsub(/\r\n|\n|\r/, "\\n").gsub(/["']/) { |m| "\\#{m}" }

View file

@ -111,7 +111,7 @@ module ActionView
(100..599).to_a) (100..599).to_a)
AJAX_OPTIONS = Set.new([ :before, :after, :condition, :url, AJAX_OPTIONS = Set.new([ :before, :after, :condition, :url,
:asynchronous, :method, :insertion, :position, :asynchronous, :method, :insertion, :position,
:form, :with, :update, :script ]).merge(CALLBACKS) :form, :with, :update, :script, :type ]).merge(CALLBACKS)
end end
# Returns a link to a remote action defined by <tt>options[:url]</tt> # Returns a link to a remote action defined by <tt>options[:url]</tt>
@ -603,7 +603,7 @@ module ActionView
# Example: # Example:
# #
# # Generates: # # Generates:
# # new Insertion.Bottom("list", "<li>Some item</li>"); # # new Element.insert("list", { bottom: <li>Some item</li>" });
# # new Effect.Highlight("list"); # # new Effect.Highlight("list");
# # ["status-indicator", "cancel-link"].each(Element.hide); # # ["status-indicator", "cancel-link"].each(Element.hide);
# update_page do |page| # update_page do |page|
@ -736,16 +736,16 @@ module ActionView
# #
# # Insert the rendered 'navigation' partial just before the DOM # # Insert the rendered 'navigation' partial just before the DOM
# # element with ID 'content'. # # element with ID 'content'.
# # Generates: new Insertion.Before("content", "-- Contents of 'navigation' partial --"); # # Generates: Element.insert("content", { before: "-- Contents of 'navigation' partial --" });
# page.insert_html :before, 'content', :partial => 'navigation' # page.insert_html :before, 'content', :partial => 'navigation'
# #
# # Add a list item to the bottom of the <ul> with ID 'list'. # # Add a list item to the bottom of the <ul> with ID 'list'.
# # Generates: new Insertion.Bottom("list", "<li>Last item</li>"); # # Generates: Element.insert("list", { bottom: "<li>Last item</li>" });
# page.insert_html :bottom, 'list', '<li>Last item</li>' # page.insert_html :bottom, 'list', '<li>Last item</li>'
# #
def insert_html(position, id, *options_for_render) def insert_html(position, id, *options_for_render)
insertion = position.to_s.camelize content = javascript_object_for(render(*options_for_render))
call "new Insertion.#{insertion}", id, render(*options_for_render) record "Element.insert(\"#{id}\", { #{position.to_s.downcase}: #{content} });"
end end
# Replaces the inner HTML of the DOM element with the given +id+. # Replaces the inner HTML of the DOM element with the given +id+.
@ -1039,7 +1039,7 @@ module ActionView
js_options['asynchronous'] = options[:type] != :synchronous js_options['asynchronous'] = options[:type] != :synchronous
js_options['method'] = method_option_to_s(options[:method]) if options[:method] js_options['method'] = method_option_to_s(options[:method]) if options[:method]
js_options['insertion'] = "Insertion.#{options[:position].to_s.camelize}" if options[:position] js_options['insertion'] = "'#{options[:position].to_s.downcase}'" if options[:position]
js_options['evalScripts'] = options[:script].nil? || options[:script] js_options['evalScripts'] = options[:script].nil? || options[:script]
if options[:form] if options[:form]

View file

@ -1,5 +1,6 @@
require 'cgi' require 'cgi'
require 'erb' require 'erb'
require 'set'
module ActionView module ActionView
module Helpers #:nodoc: module Helpers #:nodoc:
@ -8,7 +9,8 @@ module ActionView
module TagHelper module TagHelper
include ERB::Util include ERB::Util
BOOLEAN_ATTRIBUTES = Set.new(%w(disabled readonly multiple)) BOOLEAN_ATTRIBUTES = %w(disabled readonly multiple).to_set
BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map(&:to_sym))
# Returns an empty HTML tag of type +name+ which by default is XHTML # Returns an empty HTML tag of type +name+ which by default is XHTML
# compliant. Set +open+ to true to create an open tag compatible # compliant. Set +open+ to true to create an open tag compatible
@ -37,7 +39,7 @@ module ActionView
# tag("img", { :src => "open &amp; shut.png" }, false, false) # tag("img", { :src => "open &amp; shut.png" }, false, false)
# # => <img src="open &amp; shut.png" /> # # => <img src="open &amp; shut.png" />
def tag(name, options = nil, open = false, escape = true) def tag(name, options = nil, open = false, escape = true)
"<#{name}#{tag_options(options, escape) if options}" + (open ? ">" : " />") "<#{name}#{tag_options(options, escape) if options}#{open ? ">" : " />"}"
end end
# Returns an HTML block tag of type +name+ surrounding the +content+. Add # Returns an HTML block tag of type +name+ surrounding the +content+. Add
@ -114,7 +116,6 @@ module ActionView
if escape if escape
options.each do |key, value| options.each do |key, value|
next unless value next unless value
key = key.to_s
value = BOOLEAN_ATTRIBUTES.include?(key) ? key : escape_once(value) value = BOOLEAN_ATTRIBUTES.include?(key) ? key : escape_once(value)
attrs << %(#{key}="#{value}") attrs << %(#{key}="#{value}")
end end

View file

@ -464,7 +464,7 @@ module ActionView
[-\w]+ # subdomain or domain [-\w]+ # subdomain or domain
(?:\.[-\w]+)* # remaining subdomains or domain (?:\.[-\w]+)* # remaining subdomains or domain
(?::\d+)? # port (?::\d+)? # port
(?:/(?:(?:[~\w\+@%=\(\)-]|(?:[,.;:][^\s$]))+)?)* # path (?:/(?:(?:[~\w\+@%=\(\)-]|(?:[,.;:'][^\s$]))+)?)* # path
(?:\?[\w\+@%&=.;-]+)? # query string (?:\?[\w\+@%&=.;-]+)? # query string
(?:\#[\w\-]*)? # trailing anchor (?:\#[\w\-]*)? # trailing anchor
) )

View file

@ -63,17 +63,15 @@ module ActionView
# # calls @workshop.to_s # # calls @workshop.to_s
# # => /workshops/5 # # => /workshops/5
def url_for(options = {}) def url_for(options = {})
options ||= {}
case options case options
when Hash when Hash
show_path = options[:host].nil? ? true : false options = { :only_path => options[:host].nil? }.update(options.symbolize_keys)
options = { :only_path => show_path }.update(options.symbolize_keys)
escape = options.key?(:escape) ? options.delete(:escape) : true escape = options.key?(:escape) ? options.delete(:escape) : true
url = @controller.send(:url_for, options) url = @controller.send(:url_for, options)
when String when String
escape = true escape = true
url = options url = options
when NilClass
url = @controller.send(:url_for, nil)
else else
escape = false escape = false
url = polymorphic_path(options) url = polymorphic_path(options)
@ -444,7 +442,7 @@ module ActionView
email_address_obfuscated.gsub!(/\./, html_options.delete("replace_dot")) if html_options.has_key?("replace_dot") email_address_obfuscated.gsub!(/\./, html_options.delete("replace_dot")) if html_options.has_key?("replace_dot")
if encode == "javascript" if encode == "javascript"
"document.write('#{content_tag("a", name || email_address, html_options.merge({ "href" => "mailto:"+email_address+extras }))}');".each_byte do |c| "document.write('#{content_tag("a", name || email_address_obfuscated, html_options.merge({ "href" => "mailto:"+email_address+extras }))}');".each_byte do |c|
string << sprintf("%%%x", c) string << sprintf("%%%x", c)
end end
"<script type=\"#{Mime::JS}\">eval(unescape('#{string}'))</script>" "<script type=\"#{Mime::JS}\">eval(unescape('#{string}'))</script>"

View file

@ -22,10 +22,10 @@ module ActionView #:nodoc:
end end
def render_member(object) def render_member(object)
@locals[@counter_name] += 1
@locals[:object] = @locals[@variable_name] = object @locals[:object] = @locals[@variable_name] = object
template = render_template template = render_template
@locals[@counter_name] += 1
@locals.delete(@variable_name) @locals.delete(@variable_name)
@locals.delete(:object) @locals.delete(:object)

View file

@ -137,6 +137,9 @@ class AssertResponseWithUnexpectedErrorController < ActionController::Base
end end
end end
class UserController < ActionController::Base
end
module Admin module Admin
class InnerModuleController < ActionController::Base class InnerModuleController < ActionController::Base
def index def index
@ -174,7 +177,7 @@ class ActionPackAssertionsControllerTest < Test::Unit::TestCase
# let's get this party started # let's get this party started
def setup def setup
ActionController::Routing::Routes.reload ActionController::Routing::Routes.reload
ActionController::Routing.use_controllers!(%w(action_pack_assertions admin/inner_module content admin/user)) ActionController::Routing.use_controllers!(%w(action_pack_assertions admin/inner_module user content admin/user))
@controller = ActionPackAssertionsController.new @controller = ActionPackAssertionsController.new
@request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new @request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new
end end
@ -268,7 +271,7 @@ class ActionPackAssertionsControllerTest < Test::Unit::TestCase
assert_redirected_to admin_inner_module_path assert_redirected_to admin_inner_module_path
end end
end end
def test_assert_redirected_to_top_level_named_route_from_nested_controller def test_assert_redirected_to_top_level_named_route_from_nested_controller
with_routing do |set| with_routing do |set|
set.draw do |map| set.draw do |map|
@ -277,11 +280,25 @@ class ActionPackAssertionsControllerTest < Test::Unit::TestCase
end end
@controller = Admin::InnerModuleController.new @controller = Admin::InnerModuleController.new
process :redirect_to_top_level_named_route process :redirect_to_top_level_named_route
# passes -> assert_redirected_to "http://test.host/action_pack_assertions/foo" # assert_redirected_to "http://test.host/action_pack_assertions/foo" would pass because of exact match early return
assert_redirected_to "/action_pack_assertions/foo" assert_redirected_to "/action_pack_assertions/foo"
end end
end end
def test_assert_redirected_to_top_level_named_route_with_same_controller_name_in_both_namespaces
with_routing do |set|
set.draw do |map|
# this controller exists in the admin namespace as well which is the only difference from previous test
map.top_level '/user/:id', :controller => 'user', :action => 'index'
map.connect ':controller/:action/:id'
end
@controller = Admin::InnerModuleController.new
process :redirect_to_top_level_named_route
# assert_redirected_to top_level_url('foo') would pass because of exact match early return
assert_redirected_to top_level_path('foo')
end
end
# -- standard request/response object testing -------------------------------- # -- standard request/response object testing --------------------------------
# make sure that the template objects exist # make sure that the template objects exist
@ -406,7 +423,7 @@ class ActionPackAssertionsControllerTest < Test::Unit::TestCase
process :redirect_to_action process :redirect_to_action
assert_redirected_to :action => "flash_me" assert_redirected_to :action => "flash_me"
follow_redirect assert_deprecated { follow_redirect }
assert_equal 1, @request.parameters["id"].to_i assert_equal 1, @request.parameters["id"].to_i
assert "Inconceivable!", @response.body assert "Inconceivable!", @response.body
@ -416,7 +433,9 @@ class ActionPackAssertionsControllerTest < Test::Unit::TestCase
process :redirect_to_controller process :redirect_to_controller
assert_redirected_to :controller => "elsewhere", :action => "flash_me" assert_redirected_to :controller => "elsewhere", :action => "flash_me"
assert_raises(RuntimeError, "Can't follow redirects outside of current controller (elsewhere)") { follow_redirect } assert_raises(RuntimeError, "Can't follow redirects outside of current controller (elsewhere)") do
assert_deprecated { follow_redirect }
end
end end
def test_assert_redirection_fails_with_incorrect_controller def test_assert_redirection_fails_with_incorrect_controller

View file

@ -568,7 +568,12 @@ class AssertSelectTest < Test::Unit::TestCase
assert_select "div", 4 assert_select "div", 4
end end
end end
def test_assert_select_rjs_raise_errors
assert_raises(ArgumentError) { assert_select_rjs(:destroy) }
assert_raises(ArgumentError) { assert_select_rjs(:insert, :left) }
end
# Simple selection from a single result. # Simple selection from a single result.
def test_nested_assert_select_rjs_with_single_result def test_nested_assert_select_rjs_with_single_result
render_rjs do |page| render_rjs do |page|

View file

@ -7,6 +7,7 @@ module Submodule
end end
class ContainedNonEmptyController < ActionController::Base class ContainedNonEmptyController < ActionController::Base
def public_action def public_action
render :nothing => true
end end
hide_action :hidden_action hide_action :hidden_action
@ -105,6 +106,18 @@ end
class PerformActionTest < Test::Unit::TestCase class PerformActionTest < Test::Unit::TestCase
class MockLogger
attr_reader :logged
def initialize
@logged = []
end
def method_missing(method, *args)
@logged << args.first
end
end
def use_controller(controller_class) def use_controller(controller_class)
@controller = controller_class.new @controller = controller_class.new
@ -142,6 +155,13 @@ class PerformActionTest < Test::Unit::TestCase
get :another_hidden_action get :another_hidden_action
assert_response 404 assert_response 404
end end
def test_namespaced_action_should_log_module_name
use_controller Submodule::ContainedNonEmptyController
@controller.logger = MockLogger.new
get :public_action
assert_match /Processing\sSubmodule::ContainedNonEmptyController#public_action/, @controller.logger.logged[1]
end
end end
class DefaultUrlOptionsTest < Test::Unit::TestCase class DefaultUrlOptionsTest < Test::Unit::TestCase
@ -169,6 +189,22 @@ class DefaultUrlOptionsTest < Test::Unit::TestCase
end end
end end
class EmptyUrlOptionsTest < Test::Unit::TestCase
def setup
@controller = NonEmptyController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
@request.host = 'www.example.com'
end
def test_ensure_url_for_works_as_expected_when_called_with_no_options_if_default_url_options_is_not_set
get :public_action
assert_equal "http://www.example.com/non_empty/public_action", @controller.url_for
end
end
class EnsureNamedRoutesWorksTicket22BugTest < Test::Unit::TestCase class EnsureNamedRoutesWorksTicket22BugTest < Test::Unit::TestCase
def test_named_routes_still_work def test_named_routes_still_work
ActionController::Routing::Routes.draw do |map| ActionController::Routing::Routes.draw do |map|
@ -180,4 +216,4 @@ class EnsureNamedRoutesWorksTicket22BugTest < Test::Unit::TestCase
ensure ensure
ActionController::Routing::Routes.load! ActionController::Routing::Routes.load!
end end
end end

View file

@ -27,14 +27,14 @@ class DispatcherTest < Test::Unit::TestCase
def test_clears_dependencies_after_dispatch_if_in_loading_mode def test_clears_dependencies_after_dispatch_if_in_loading_mode
ActionController::Routing::Routes.expects(:reload).once ActionController::Routing::Routes.expects(:reload).once
Dependencies.expects(:clear).once ActiveSupport::Dependencies.expects(:clear).once
dispatch(@output, false) dispatch(@output, false)
end end
def test_leaves_dependencies_after_dispatch_if_not_in_loading_mode def test_leaves_dependencies_after_dispatch_if_not_in_loading_mode
ActionController::Routing::Routes.expects(:reload).never ActionController::Routing::Routes.expects(:reload).never
Dependencies.expects(:clear).never ActiveSupport::Dependencies.expects(:clear).never
dispatch dispatch
end end

View file

@ -120,4 +120,29 @@ HTML
assert doc.find(:tag => "div", :attributes => { :id => "map" }, :content => "") assert doc.find(:tag => "div", :attributes => { :id => "map" }, :content => "")
assert doc.find(:tag => "div", :attributes => { :id => "map" }, :content => nil) assert doc.find(:tag => "div", :attributes => { :id => "map" }, :content => nil)
end end
def test_parse_invalid_document
assert_nothing_raised do
doc = HTML::Document.new("<html>
<table>
<tr>
<td style=\"color: #FFFFFF; height: 17px; onclick=\"window.location.href='http://www.rmeinc.com/about_rme.aspx'\" style=\"cursor:pointer; height: 17px;\"; nowrap onclick=\"window.location.href='http://www.rmeinc.com/about_rme.aspx'\" onmouseout=\"this.bgColor='#0066cc'; this.style.color='#FFFFFF'\" onmouseover=\"this.bgColor='#ffffff'; this.style.color='#0033cc'\">About Us</td>
</tr>
</table>
</html>")
end
end
def test_invalid_document_raises_exception_when_strict
assert_raises RuntimeError do
doc = HTML::Document.new("<html>
<table>
<tr>
<td style=\"color: #FFFFFF; height: 17px; onclick=\"window.location.href='http://www.rmeinc.com/about_rme.aspx'\" style=\"cursor:pointer; height: 17px;\"; nowrap onclick=\"window.location.href='http://www.rmeinc.com/about_rme.aspx'\" onmouseout=\"this.bgColor='#0066cc'; this.style.color='#FFFFFF'\" onmouseover=\"this.bgColor='#ffffff'; this.style.color='#0033cc'\">About Us</td>
</tr>
</table>
</html>", true)
end
end
end end

View file

@ -28,7 +28,7 @@ class SessionUploadTest < ActionController::IntegrationTest
# end # end
def test_post_with_upload def test_post_with_upload
uses_mocha "test_post_with_upload" do uses_mocha "test_post_with_upload" do
Dependencies.stubs(:load?).returns(false) ActiveSupport::Dependencies.stubs(:load?).returns(false)
with_routing do |set| with_routing do |set|
set.draw do |map| set.draw do |map|
map.update 'update', :controller => "upload_test", :action => "update", :method => :post map.update 'update', :controller => "upload_test", :action => "update", :method => :post

View file

@ -68,6 +68,11 @@ class NewRenderTestController < ActionController::Base
path = File.join(File.dirname(__FILE__), '../fixtures/test/render_file_with_ivar.erb') path = File.join(File.dirname(__FILE__), '../fixtures/test/render_file_with_ivar.erb')
render :file => path render :file => path
end end
def render_file_from_template
@secret = 'in the sauce'
@path = File.expand_path(File.join(File.dirname(__FILE__), '../fixtures/test/render_file_with_ivar.erb'))
end
def render_file_with_locals def render_file_with_locals
path = File.join(File.dirname(__FILE__), '../fixtures/test/render_file_with_locals.erb') path = File.join(File.dirname(__FILE__), '../fixtures/test/render_file_with_locals.erb')
@ -259,6 +264,10 @@ class NewRenderTestController < ActionController::Base
render :template => "test/hello_world" render :template => "test/hello_world"
end end
def render_with_explicit_template_with_locals
render :template => "test/render_file_with_locals", :locals => { :secret => 'area51' }
end
def double_render def double_render
render :text => "hello" render :text => "hello"
render :text => "world" render :text => "world"
@ -531,6 +540,11 @@ class NewRenderTest < Test::Unit::TestCase
get :render_file_with_locals get :render_file_with_locals
assert_equal "The secret is in the sauce\n", @response.body assert_equal "The secret is in the sauce\n", @response.body
end end
def test_render_file_from_template
get :render_file_from_template
assert_equal "The secret is in the sauce\n", @response.body
end
def test_attempt_to_access_object_method def test_attempt_to_access_object_method
assert_raises(ActionController::UnknownAction, "No action responded to [clone]") { get :clone } assert_raises(ActionController::UnknownAction, "No action responded to [clone]") { get :clone }
@ -742,7 +756,7 @@ EOS
def test_partial_collection_with_counter def test_partial_collection_with_counter
get :partial_collection_with_counter get :partial_collection_with_counter
assert_equal "david1mary2", @response.body assert_equal "david0mary1", @response.body
end end
def test_partial_collection_with_locals def test_partial_collection_with_locals
@ -762,7 +776,7 @@ EOS
def test_partial_collection_shorthand_with_different_types_of_records def test_partial_collection_shorthand_with_different_types_of_records
get :partial_collection_shorthand_with_different_types_of_records get :partial_collection_shorthand_with_different_types_of_records
assert_equal "Bonjour bad customer: mark1Bonjour good customer: craig2Bonjour bad customer: john3Bonjour good customer: zach4Bonjour good customer: brandon5Bonjour bad customer: dan6", @response.body assert_equal "Bonjour bad customer: mark0Bonjour good customer: craig1Bonjour bad customer: john2Bonjour good customer: zach3Bonjour good customer: brandon4Bonjour bad customer: dan5", @response.body
end end
def test_empty_partial_collection def test_empty_partial_collection
@ -800,7 +814,12 @@ EOS
get :render_text_with_assigns get :render_text_with_assigns
assert_equal "world", assigns["hello"] assert_equal "world", assigns["hello"]
end end
def test_template_with_locals
get :render_with_explicit_template_with_locals
assert_equal "The secret is area51\n", @response.body
end
def test_update_page def test_update_page
get :update_page get :update_page
assert_template nil assert_template nil

View file

@ -118,6 +118,39 @@ uses_mocha 'polymorphic URL helpers' do
polymorphic_url([:site, :admin, @article, @response, @tag]) polymorphic_url([:site, :admin, @article, @response, @tag])
end end
def test_nesting_with_array_ending_in_singleton_resource
expects(:article_response_url).with(@article)
polymorphic_url([@article, :response])
end
def test_nesting_with_array_containing_singleton_resource
@tag = Tag.new
@tag.save
expects(:article_response_tag_url).with(@article, @tag)
polymorphic_url([@article, :response, @tag])
end
def test_nesting_with_array_containing_namespace_and_singleton_resource
@tag = Tag.new
@tag.save
expects(:admin_article_response_tag_url).with(@article, @tag)
polymorphic_url([:admin, @article, :response, @tag])
end
def test_nesting_with_array_containing_singleton_resource_and_format
@tag = Tag.new
@tag.save
expects(:formatted_article_response_tag_url).with(@article, @tag, :pdf)
formatted_polymorphic_url([@article, :response, @tag, :pdf])
end
def test_nesting_with_array_containing_singleton_resource_and_format_option
@tag = Tag.new
@tag.save
expects(:article_response_tag_url).with(@article, @tag, :pdf)
polymorphic_url([@article, :response, @tag], :format => :pdf)
end
# TODO: Needs to be updated to correctly know about whether the object is in a hash or not # TODO: Needs to be updated to correctly know about whether the object is in a hash or not
def xtest_with_hash def xtest_with_hash
expects(:article_url).with(@article) expects(:article_url).with(@article)

View file

@ -103,7 +103,7 @@ class TestController < ActionController::Base
def render_line_offset def render_line_offset
begin begin
render :inline => '<% raise %>', :locals => {:foo => 'bar'} render :inline => '<% raise %>', :locals => {:foo => 'bar'}
rescue => exc rescue RuntimeError => exc
end end
line = exc.backtrace.first line = exc.backtrace.first
render :text => line render :text => line

View file

@ -12,6 +12,9 @@ class RequestTest < Test::Unit::TestCase
@request.remote_addr = '1.2.3.4' @request.remote_addr = '1.2.3.4'
assert_equal '1.2.3.4', @request.remote_ip assert_equal '1.2.3.4', @request.remote_ip
@request.remote_addr = '1.2.3.4,3.4.5.6'
assert_equal '1.2.3.4', @request.remote_ip
@request.env['HTTP_CLIENT_IP'] = '2.3.4.5' @request.env['HTTP_CLIENT_IP'] = '2.3.4.5'
assert_equal '1.2.3.4', @request.remote_ip assert_equal '1.2.3.4', @request.remote_ip
@ -59,6 +62,9 @@ class RequestTest < Test::Unit::TestCase
assert_match /HTTP_X_FORWARDED_FOR="9.9.9.9, 3.4.5.6, 10.0.0.1, 172.31.4.4"/, e.message assert_match /HTTP_X_FORWARDED_FOR="9.9.9.9, 3.4.5.6, 10.0.0.1, 172.31.4.4"/, e.message
assert_match /HTTP_CLIENT_IP="8.8.8.8"/, e.message assert_match /HTTP_CLIENT_IP="8.8.8.8"/, e.message
@request.env['HTTP_X_FORWARDED_FOR'] = '8.8.8.8, 9.9.9.9'
assert_equal '8.8.8.8', @request.remote_ip
@request.env.delete 'HTTP_CLIENT_IP' @request.env.delete 'HTTP_CLIENT_IP'
@request.env.delete 'HTTP_X_FORWARDED_FOR' @request.env.delete 'HTTP_X_FORWARDED_FOR'
end end

View file

@ -28,18 +28,16 @@ module Backoffice
end end
class ResourcesTest < Test::Unit::TestCase class ResourcesTest < Test::Unit::TestCase
# The assertions in these tests are incompatible with the hash method # The assertions in these tests are incompatible with the hash method
# optimisation. This could indicate user level problems # optimisation. This could indicate user level problems
def setup def setup
ActionController::Base.optimise_named_routes = false ActionController::Base.optimise_named_routes = false
end end
def tear_down def teardown
ActionController::Base.optimise_named_routes = true ActionController::Base.optimise_named_routes = true
end end
def test_should_arrange_actions def test_should_arrange_actions
resource = ActionController::Resources::Resource.new(:messages, resource = ActionController::Resources::Resource.new(:messages,
:collection => { :rss => :get, :reorder => :post, :csv => :post }, :collection => { :rss => :get, :reorder => :post, :csv => :post },
@ -159,14 +157,14 @@ class ResourcesTest < Test::Unit::TestCase
def test_with_collection_actions_and_name_prefix def test_with_collection_actions_and_name_prefix
actions = { 'a' => :get, 'b' => :put, 'c' => :post, 'd' => :delete } actions = { 'a' => :get, 'b' => :put, 'c' => :post, 'd' => :delete }
with_restful_routing :messages, :path_prefix => '/threads/:thread_id', :name_prefix => "thread_", :collection => actions do with_restful_routing :messages, :path_prefix => '/threads/:thread_id', :name_prefix => "thread_", :collection => actions do
assert_restful_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options| assert_restful_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options|
actions.each do |action, method| actions.each do |action, method|
assert_recognizes(options.merge(:action => action), :path => "/threads/1/messages/#{action}", :method => method) assert_recognizes(options.merge(:action => action), :path => "/threads/1/messages/#{action}", :method => method)
end end
end end
assert_restful_named_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options| assert_restful_named_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options|
actions.keys.each do |action| actions.keys.each do |action|
assert_named_route "/threads/1/messages/#{action}", "#{action}_thread_messages_path", :action => action assert_named_route "/threads/1/messages/#{action}", "#{action}_thread_messages_path", :action => action
@ -177,14 +175,14 @@ class ResourcesTest < Test::Unit::TestCase
def test_with_collection_action_and_name_prefix_and_formatted def test_with_collection_action_and_name_prefix_and_formatted
actions = { 'a' => :get, 'b' => :put, 'c' => :post, 'd' => :delete } actions = { 'a' => :get, 'b' => :put, 'c' => :post, 'd' => :delete }
with_restful_routing :messages, :path_prefix => '/threads/:thread_id', :name_prefix => "thread_", :collection => actions do with_restful_routing :messages, :path_prefix => '/threads/:thread_id', :name_prefix => "thread_", :collection => actions do
assert_restful_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options| assert_restful_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options|
actions.each do |action, method| actions.each do |action, method|
assert_recognizes(options.merge(:action => action, :format => 'xml'), :path => "/threads/1/messages/#{action}.xml", :method => method) assert_recognizes(options.merge(:action => action, :format => 'xml'), :path => "/threads/1/messages/#{action}.xml", :method => method)
end end
end end
assert_restful_named_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options| assert_restful_named_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options|
actions.keys.each do |action| actions.keys.each do |action|
assert_named_route "/threads/1/messages/#{action}.xml", "formatted_#{action}_thread_messages_path", :action => action, :format => 'xml' assert_named_route "/threads/1/messages/#{action}.xml", "formatted_#{action}_thread_messages_path", :action => action, :format => 'xml'
@ -279,7 +277,7 @@ class ResourcesTest < Test::Unit::TestCase
end end
end end
end end
def test_with_new_action_with_name_prefix def test_with_new_action_with_name_prefix
with_restful_routing :messages, :new => { :preview => :post }, :path_prefix => '/threads/:thread_id', :name_prefix => 'thread_' do with_restful_routing :messages, :new => { :preview => :post }, :path_prefix => '/threads/:thread_id', :name_prefix => 'thread_' do
preview_options = {:action => 'preview', :thread_id => '1'} preview_options = {:action => 'preview', :thread_id => '1'}
@ -293,7 +291,7 @@ class ResourcesTest < Test::Unit::TestCase
end end
end end
end end
def test_with_formatted_new_action_with_name_prefix def test_with_formatted_new_action_with_name_prefix
with_restful_routing :messages, :new => { :preview => :post }, :path_prefix => '/threads/:thread_id', :name_prefix => 'thread_' do with_restful_routing :messages, :new => { :preview => :post }, :path_prefix => '/threads/:thread_id', :name_prefix => 'thread_' do
preview_options = {:action => 'preview', :thread_id => '1', :format => 'xml'} preview_options = {:action => 'preview', :thread_id => '1', :format => 'xml'}
@ -307,7 +305,7 @@ class ResourcesTest < Test::Unit::TestCase
end end
end end
end end
def test_override_new_method def test_override_new_method
with_restful_routing :messages do with_restful_routing :messages do
assert_restful_routes_for :messages do |options| assert_restful_routes_for :messages do |options|
@ -524,9 +522,9 @@ class ResourcesTest < Test::Unit::TestCase
map.resources :messages, :collection => {:search => :get}, :new => {:preview => :any}, :name_prefix => 'thread_', :path_prefix => '/threads/:thread_id' map.resources :messages, :collection => {:search => :get}, :new => {:preview => :any}, :name_prefix => 'thread_', :path_prefix => '/threads/:thread_id'
map.resource :account, :member => {:login => :get}, :new => {:preview => :any}, :name_prefix => 'admin_', :path_prefix => '/admin' map.resource :account, :member => {:login => :get}, :new => {:preview => :any}, :name_prefix => 'admin_', :path_prefix => '/admin'
end end
action_separator = ActionController::Base.resource_action_separator action_separator = ActionController::Base.resource_action_separator
assert_simply_restful_for :messages, :name_prefix => 'thread_', :path_prefix => 'threads/1/', :options => { :thread_id => '1' } assert_simply_restful_for :messages, :name_prefix => 'thread_', :path_prefix => 'threads/1/', :options => { :thread_id => '1' }
assert_named_route "/threads/1/messages#{action_separator}search", "search_thread_messages_path", {} assert_named_route "/threads/1/messages#{action_separator}search", "search_thread_messages_path", {}
assert_named_route "/threads/1/messages/new", "new_thread_message_path", {} assert_named_route "/threads/1/messages/new", "new_thread_message_path", {}
@ -623,7 +621,7 @@ class ResourcesTest < Test::Unit::TestCase
assert_simply_restful_for :products, :controller => "backoffice/products" assert_simply_restful_for :products, :controller => "backoffice/products"
end end
end end
def test_nested_resources_using_namespace def test_nested_resources_using_namespace
with_routing do |set| with_routing do |set|
set.draw do |map| set.draw do |map|
@ -795,7 +793,7 @@ class ResourcesTest < Test::Unit::TestCase
yield options[:options] if block_given? yield options[:options] if block_given?
end end
def assert_singleton_routes_for(singleton_name, options = {}) def assert_singleton_routes_for(singleton_name, options = {})
options[:options] ||= {} options[:options] ||= {}
options[:options][:controller] = options[:controller] || singleton_name.to_s.pluralize options[:options][:controller] = options[:controller] || singleton_name.to_s.pluralize
@ -855,7 +853,7 @@ class ResourcesTest < Test::Unit::TestCase
actual = @controller.send(route, options) rescue $!.class.name actual = @controller.send(route, options) rescue $!.class.name
assert_equal expected, actual, "Error on route: #{route}(#{options.inspect})" assert_equal expected, actual, "Error on route: #{route}(#{options.inspect})"
end end
def assert_resource_methods(expected, resource, action_method, method) def assert_resource_methods(expected, resource, action_method, method)
assert_equal expected.length, resource.send("#{action_method}_methods")[method].size, "#{resource.send("#{action_method}_methods")[method].inspect}" assert_equal expected.length, resource.send("#{action_method}_methods")[method].size, "#{resource.send("#{action_method}_methods")[method].inspect}"
expected.each do |action| expected.each do |action|

View file

@ -1983,6 +1983,26 @@ class RouteSetTest < Test::Unit::TestCase
Object.send(:remove_const, :Api) Object.send(:remove_const, :Api)
end end
def test_namespace_with_path_prefix
Object.const_set(:Api, Module.new { |m| m.const_set(:ProductsController, Class.new) })
set.draw do |map|
map.namespace 'api', :path_prefix => 'prefix' do |api|
api.route 'inventory', :controller => "products", :action => 'inventory'
end
end
request.path = "/prefix/inventory"
request.method = :get
assert_nothing_raised { set.recognize(request) }
assert_equal("api/products", request.path_parameters[:controller])
assert_equal("inventory", request.path_parameters[:action])
ensure
Object.send(:remove_const, :Api)
end
def test_generate_finds_best_fit def test_generate_finds_best_fit
set.draw do |map| set.draw do |map|
map.connect "/people", :controller => "people", :action => "index" map.connect "/people", :controller => "people", :action => "index"
@ -2392,10 +2412,10 @@ uses_mocha 'route loading' do
end end
def test_adding_inflections_forces_reload def test_adding_inflections_forces_reload
Inflector::Inflections.instance.expects(:uncountable).with('equipment') ActiveSupport::Inflector::Inflections.instance.expects(:uncountable).with('equipment')
routes.expects(:reload!) routes.expects(:reload!)
Inflector.inflections { |inflect| inflect.uncountable('equipment') } ActiveSupport::Inflector.inflections { |inflect| inflect.uncountable('equipment') }
end end
def test_load_with_configuration def test_load_with_configuration

View file

@ -531,6 +531,11 @@ XML
assert_equal content_type, file.content_type assert_equal content_type, file.content_type
assert_equal file.path, file.local_path assert_equal file.path, file.local_path
assert_equal expected, file.read assert_equal expected, file.read
new_content_type = "new content_type"
file.content_type = new_content_type
assert_equal new_content_type, file.content_type
end end
def test_test_uploaded_file_with_binary def test_test_uploaded_file_with_binary
@ -571,7 +576,9 @@ XML
get :redirect_to_same_controller get :redirect_to_same_controller
assert_response :redirect assert_response :redirect
assert_redirected_to :controller => 'test_test/test', :action => 'test_uri', :id => 5 assert_redirected_to :controller => 'test_test/test', :action => 'test_uri', :id => 5
assert_nothing_raised { follow_redirect } assert_deprecated 'follow_redirect' do
assert_nothing_raised { follow_redirect }
end
end end
end end
@ -580,7 +587,9 @@ XML
get :redirect_to_different_controller get :redirect_to_different_controller
assert_response :redirect assert_response :redirect
assert_redirected_to :controller => 'fail', :id => 5 assert_redirected_to :controller => 'fail', :id => 5
assert_raise(RuntimeError) { follow_redirect } assert_raise(RuntimeError) do
assert_deprecated { follow_redirect }
end
end end
end end

View file

@ -21,10 +21,10 @@ class VerificationTest < Test::Unit::TestCase
verify :only => :guarded_by_method, :method => :post, verify :only => :guarded_by_method, :method => :post,
:redirect_to => { :action => "unguarded" } :redirect_to => { :action => "unguarded" }
verify :only => :guarded_by_xhr, :xhr => true, verify :only => :guarded_by_xhr, :xhr => true,
:redirect_to => { :action => "unguarded" } :redirect_to => { :action => "unguarded" }
verify :only => :guarded_by_not_xhr, :xhr => false, verify :only => :guarded_by_not_xhr, :xhr => false,
:redirect_to => { :action => "unguarded" } :redirect_to => { :action => "unguarded" }
@ -39,10 +39,13 @@ class VerificationTest < Test::Unit::TestCase
verify :only => :no_default_action, :params => "santa" verify :only => :no_default_action, :params => "santa"
verify :only => :guarded_with_back, :method => :post,
:redirect_to => :back
def guarded_one def guarded_one
render :text => "#{params[:one]}" render :text => "#{params[:one]}"
end end
def guarded_one_for_named_route_test def guarded_one_for_named_route_test
render :text => "#{params[:one]}" render :text => "#{params[:one]}"
end end
@ -70,11 +73,11 @@ class VerificationTest < Test::Unit::TestCase
def guarded_by_method def guarded_by_method
render :text => "#{request.method}" render :text => "#{request.method}"
end end
def guarded_by_xhr def guarded_by_xhr
render :text => "#{request.xhr?}" render :text => "#{request.xhr?}"
end end
def guarded_by_not_xhr def guarded_by_not_xhr
render :text => "#{request.xhr?}" render :text => "#{request.xhr?}"
end end
@ -86,15 +89,19 @@ class VerificationTest < Test::Unit::TestCase
def two_redirects def two_redirects
render :nothing => true render :nothing => true
end end
def must_be_post def must_be_post
render :text => "Was a post!" render :text => "Was a post!"
end end
def guarded_with_back
render :text => "#{params[:one]}"
end
def no_default_action def no_default_action
# Will never run # Will never run
end end
protected protected
def rescue_action(e) raise end def rescue_action(e) raise end
@ -109,7 +116,17 @@ class VerificationTest < Test::Unit::TestCase
@response = ActionController::TestResponse.new @response = ActionController::TestResponse.new
ActionController::Routing::Routes.add_named_route :foo, '/foo', :controller => 'test', :action => 'foo' ActionController::Routing::Routes.add_named_route :foo, '/foo', :controller => 'test', :action => 'foo'
end end
def test_using_symbol_back_with_no_referrer
assert_raise(ActionController::RedirectBackError) { get :guarded_with_back }
end
def test_using_symbol_back_redirects_to_referrer
@request.env["HTTP_REFERER"] = "/foo"
get :guarded_with_back
assert_redirected_to '/foo'
end
def test_no_deprecation_warning_for_named_route def test_no_deprecation_warning_for_named_route
assert_not_deprecated do assert_not_deprecated do
get :guarded_one_for_named_route_test, :two => "not one" get :guarded_one_for_named_route_test, :two => "not one"
@ -209,44 +226,44 @@ class VerificationTest < Test::Unit::TestCase
get :guarded_by_method get :guarded_by_method
assert_redirected_to :action => "unguarded" assert_redirected_to :action => "unguarded"
end end
def test_guarded_by_xhr_with_prereqs def test_guarded_by_xhr_with_prereqs
xhr :post, :guarded_by_xhr xhr :post, :guarded_by_xhr
assert_equal "true", @response.body assert_equal "true", @response.body
end end
def test_guarded_by_xhr_without_prereqs def test_guarded_by_xhr_without_prereqs
get :guarded_by_xhr get :guarded_by_xhr
assert_redirected_to :action => "unguarded" assert_redirected_to :action => "unguarded"
end end
def test_guarded_by_not_xhr_with_prereqs def test_guarded_by_not_xhr_with_prereqs
get :guarded_by_not_xhr get :guarded_by_not_xhr
assert_equal "false", @response.body assert_equal "false", @response.body
end end
def test_guarded_by_not_xhr_without_prereqs def test_guarded_by_not_xhr_without_prereqs
xhr :post, :guarded_by_not_xhr xhr :post, :guarded_by_not_xhr
assert_redirected_to :action => "unguarded" assert_redirected_to :action => "unguarded"
end end
def test_guarded_post_and_calls_render_succeeds def test_guarded_post_and_calls_render_succeeds
post :must_be_post post :must_be_post
assert_equal "Was a post!", @response.body assert_equal "Was a post!", @response.body
end end
def test_default_failure_should_be_a_bad_request def test_default_failure_should_be_a_bad_request
post :no_default_action post :no_default_action
assert_response :bad_request assert_response :bad_request
end end
def test_guarded_post_and_calls_render_fails_and_sets_allow_header def test_guarded_post_and_calls_render_fails_and_sets_allow_header
get :must_be_post get :must_be_post
assert_response 405 assert_response 405
assert_equal "Must be post", @response.body assert_equal "Must be post", @response.body
assert_equal "POST", @response.headers["Allow"] assert_equal "POST", @response.headers["Allow"]
end end
def test_second_redirect def test_second_redirect
assert_nothing_raised { get :two_redirects } assert_nothing_raised { get :two_redirects }
end end

View file

@ -0,0 +1 @@
<%= render :file => @path %>

View file

@ -1157,6 +1157,32 @@ class DateHelperTest < ActionView::TestCase
assert_dom_equal expected, date_select("post", "written_on", {}, :class => 'selector') assert_dom_equal expected, date_select("post", "written_on", {}, :class => 'selector')
end end
def test_date_select_with_html_options_within_fields_for
@post = Post.new
@post.written_on = Date.new(2004, 6, 15)
_erbout = ''
fields_for :post, @post do |f|
_erbout.concat f.date_select(:written_on, {}, :class => 'selector')
end
expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]" class="selector">\n}
expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
expected << "</select>\n"
expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]" class="selector">\n}
expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
expected << "</select>\n"
expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]" class="selector">\n}
expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
expected << "</select>\n"
assert_dom_equal expected, _erbout
end
def test_time_select def test_time_select
@post = Post.new @post = Post.new
@post.written_on = Time.local(2004, 6, 15, 15, 16, 35) @post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
@ -1218,6 +1244,31 @@ class DateHelperTest < ActionView::TestCase
assert_dom_equal expected, time_select("post", "written_on", {}, :class => 'selector') assert_dom_equal expected, time_select("post", "written_on", {}, :class => 'selector')
end end
def test_time_select_with_html_options_within_fields_for
@post = Post.new
@post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
_erbout = ''
fields_for :post, @post do |f|
_erbout.concat f.time_select(:written_on, {}, :class => 'selector')
end
expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n}
expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]" class="selector">\n)
0.upto(23) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 15}>#{leading_zero_on_single_digits(i)}</option>\n) }
expected << "</select>\n"
expected << " : "
expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]" class="selector">\n)
0.upto(59) { |i| expected << %(<option value="#{leading_zero_on_single_digits(i)}"#{' selected="selected"' if i == 16}>#{leading_zero_on_single_digits(i)}</option>\n) }
expected << "</select>\n"
assert_dom_equal expected, _erbout
end
def test_datetime_select def test_datetime_select
@post = Post.new @post = Post.new
@post.updated_at = Time.local(2004, 6, 15, 16, 35) @post.updated_at = Time.local(2004, 6, 15, 16, 35)
@ -1283,23 +1334,23 @@ class DateHelperTest < ActionView::TestCase
end end
end end
def test_datetime_select_within_fields_for def test_datetime_select_with_html_options_within_fields_for
@post = Post.new @post = Post.new
@post.updated_at = Time.local(2004, 6, 15, 16, 35) @post.updated_at = Time.local(2004, 6, 15, 16, 35)
_erbout = '' _erbout = ''
fields_for :post, @post do |f| fields_for :post, @post do |f|
_erbout.concat f.datetime_select(:updated_at) _erbout.concat f.datetime_select(:updated_at, {}, :class => 'selector')
end end
expected = "<select id='post_updated_at_1i' name='post[updated_at(1i)]'>\n<option value='1999'>1999</option>\n<option value='2000'>2000</option>\n<option value='2001'>2001</option>\n<option value='2002'>2002</option>\n<option value='2003'>2003</option>\n<option selected='selected' value='2004'>2004</option>\n<option value='2005'>2005</option>\n<option value='2006'>2006</option>\n<option value='2007'>2007</option>\n<option value='2008'>2008</option>\n<option value='2009'>2009</option>\n</select>\n" expected = "<select id='post_updated_at_1i' name='post[updated_at(1i)]' class='selector'>\n<option value='1999'>1999</option>\n<option value='2000'>2000</option>\n<option value='2001'>2001</option>\n<option value='2002'>2002</option>\n<option value='2003'>2003</option>\n<option selected='selected' value='2004'>2004</option>\n<option value='2005'>2005</option>\n<option value='2006'>2006</option>\n<option value='2007'>2007</option>\n<option value='2008'>2008</option>\n<option value='2009'>2009</option>\n</select>\n"
expected << "<select id='post_updated_at_2i' name='post[updated_at(2i)]'>\n<option value='1'>January</option>\n<option value='2'>February</option>\n<option value='3'>March</option>\n<option value='4'>April</option>\n<option value='5'>May</option>\n<option selected='selected' value='6'>June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n" expected << "<select id='post_updated_at_2i' name='post[updated_at(2i)]' class='selector'>\n<option value='1'>January</option>\n<option value='2'>February</option>\n<option value='3'>March</option>\n<option value='4'>April</option>\n<option value='5'>May</option>\n<option selected='selected' value='6'>June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n"
expected << "<select id='post_updated_at_3i' name='post[updated_at(3i)]'>\n<option value='1'>1</option>\n<option value='2'>2</option>\n<option value='3'>3</option>\n<option value='4'>4</option>\n<option value='5'>5</option>\n<option value='6'>6</option>\n<option value='7'>7</option>\n<option value='8'>8</option>\n<option value='9'>9</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option selected='selected' value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n</select>\n" expected << "<select id='post_updated_at_3i' name='post[updated_at(3i)]' class='selector'>\n<option value='1'>1</option>\n<option value='2'>2</option>\n<option value='3'>3</option>\n<option value='4'>4</option>\n<option value='5'>5</option>\n<option value='6'>6</option>\n<option value='7'>7</option>\n<option value='8'>8</option>\n<option value='9'>9</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option selected='selected' value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n</select>\n"
expected << " &mdash; <select id='post_updated_at_4i' name='post[updated_at(4i)]'>\n<option value='00'>00</option>\n<option value='01'>01</option>\n<option value='02'>02</option>\n<option value='03'>03</option>\n<option value='04'>04</option>\n<option value='05'>05</option>\n<option value='06'>06</option>\n<option value='07'>07</option>\n<option value='08'>08</option>\n<option value='09'>09</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option value='15'>15</option>\n<option selected='selected' value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n</select>\n" expected << " &mdash; <select id='post_updated_at_4i' name='post[updated_at(4i)]' class='selector'>\n<option value='00'>00</option>\n<option value='01'>01</option>\n<option value='02'>02</option>\n<option value='03'>03</option>\n<option value='04'>04</option>\n<option value='05'>05</option>\n<option value='06'>06</option>\n<option value='07'>07</option>\n<option value='08'>08</option>\n<option value='09'>09</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option value='15'>15</option>\n<option selected='selected' value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n</select>\n"
expected << " : <select id='post_updated_at_5i' name='post[updated_at(5i)]'>\n<option value='00'>00</option>\n<option value='01'>01</option>\n<option value='02'>02</option>\n<option value='03'>03</option>\n<option value='04'>04</option>\n<option value='05'>05</option>\n<option value='06'>06</option>\n<option value='07'>07</option>\n<option value='08'>08</option>\n<option value='09'>09</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n<option value='32'>32</option>\n<option value='33'>33</option>\n<option value='34'>34</option>\n<option selected='selected' value='35'>35</option>\n<option value='36'>36</option>\n<option value='37'>37</option>\n<option value='38'>38</option>\n<option value='39'>39</option>\n<option value='40'>40</option>\n<option value='41'>41</option>\n<option value='42'>42</option>\n<option value='43'>43</option>\n<option value='44'>44</option>\n<option value='45'>45</option>\n<option value='46'>46</option>\n<option value='47'>47</option>\n<option value='48'>48</option>\n<option value='49'>49</option>\n<option value='50'>50</option>\n<option value='51'>51</option>\n<option value='52'>52</option>\n<option value='53'>53</option>\n<option value='54'>54</option>\n<option value='55'>55</option>\n<option value='56'>56</option>\n<option value='57'>57</option>\n<option value='58'>58</option>\n<option value='59'>59</option>\n</select>\n" expected << " : <select id='post_updated_at_5i' name='post[updated_at(5i)]' class='selector'>\n<option value='00'>00</option>\n<option value='01'>01</option>\n<option value='02'>02</option>\n<option value='03'>03</option>\n<option value='04'>04</option>\n<option value='05'>05</option>\n<option value='06'>06</option>\n<option value='07'>07</option>\n<option value='08'>08</option>\n<option value='09'>09</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n<option value='32'>32</option>\n<option value='33'>33</option>\n<option value='34'>34</option>\n<option selected='selected' value='35'>35</option>\n<option value='36'>36</option>\n<option value='37'>37</option>\n<option value='38'>38</option>\n<option value='39'>39</option>\n<option value='40'>40</option>\n<option value='41'>41</option>\n<option value='42'>42</option>\n<option value='43'>43</option>\n<option value='44'>44</option>\n<option value='45'>45</option>\n<option value='46'>46</option>\n<option value='47'>47</option>\n<option value='48'>48</option>\n<option value='49'>49</option>\n<option value='50'>50</option>\n<option value='51'>51</option>\n<option value='52'>52</option>\n<option value='53'>53</option>\n<option value='54'>54</option>\n<option value='55'>55</option>\n<option value='56'>56</option>\n<option value='57'>57</option>\n<option value='58'>58</option>\n<option value='59'>59</option>\n</select>\n"
assert_dom_equal(expected, _erbout) assert_dom_equal expected, _erbout
end end
def test_date_select_with_zero_value_and_no_start_year def test_date_select_with_zero_value_and_no_start_year

View file

@ -0,0 +1,9 @@
require 'abstract_unit'
class DeprecatedErbVariableTest < ActionView::TestCase
def test_setting_erb_variable_warns
assert_deprecated 'erb_variable' do
ActionView::Base.erb_variable = '_erbout'
end
end
end

File diff suppressed because it is too large Load diff

View file

@ -190,6 +190,12 @@ class FormTagHelperTest < ActionView::TestCase
assert_dom_equal expected, actual assert_dom_equal expected, actual
end end
def test_label_tag_with_symbol
actual = label_tag :title
expected = %(<label for="title">Title</label>)
assert_dom_equal expected, actual
end
def test_label_tag_with_text def test_label_tag_with_text
actual = label_tag "title", "My Title" actual = label_tag "title", "My Title"
expected = %(<label for="title">My Title</label>) expected = %(<label for="title">My Title</label>)
@ -222,6 +228,13 @@ class FormTagHelperTest < ActionView::TestCase
) )
end end
def test_submit_tag_with_no_onclick_options
assert_dom_equal(
%(<input name='commit' type='submit' value='Save' onclick="this.setAttribute('originalValue', this.value);this.disabled=true;this.value='Saving...';result = (this.form.onsubmit ? (this.form.onsubmit() ? this.form.submit() : false) : this.form.submit());if (result == false) { this.value = this.getAttribute('originalValue'); this.disabled = false };return result;" />),
submit_tag("Save", :disable_with => "Saving...")
)
end
def test_submit_tag_with_confirmation def test_submit_tag_with_confirmation
assert_dom_equal( assert_dom_equal(
%(<input name='commit' type='submit' value='Save' onclick="return confirm('Are you sure?');"/>), %(<input name='commit' type='submit' value='Save' onclick="return confirm('Are you sure?');"/>),

View file

@ -4,11 +4,14 @@ class JavaScriptHelperTest < ActionView::TestCase
tests ActionView::Helpers::JavaScriptHelper tests ActionView::Helpers::JavaScriptHelper
def test_define_javascript_functions def test_define_javascript_functions
# check if prototype.js is included first assert_deprecated(/javascript_include_tag/) do
assert_not_nil define_javascript_functions.split("\n")[1].match(/Prototype JavaScript framework/) # check if prototype.js is included first
src = define_javascript_functions
assert_not_nil src.split("\n")[1].match(/Prototype JavaScript framework/)
# check that scriptaculous.js is not in here, only needed if loaded remotely # check that scriptaculous.js is not in here, only needed if loaded remotely
assert_nil define_javascript_functions.split("\n")[1].match(/var Scriptaculous = \{/) assert_nil src.split("\n")[1].match(/var Scriptaculous = \{/)
end
end end
def test_escape_javascript def test_escape_javascript

View file

@ -77,6 +77,10 @@ class PrototypeHelperTest < PrototypeHelperBaseTest
link_to_remote("Remote outauthor", :failure => "alert(request.responseText)", :url => { :action => "whatnot" }) link_to_remote("Remote outauthor", :failure => "alert(request.responseText)", :url => { :action => "whatnot" })
assert_dom_equal %(<a href=\"#\" onclick=\"new Ajax.Request('http://www.example.com/whatnot?a=10&amp;b=20', {asynchronous:true, evalScripts:true, onFailure:function(request){alert(request.responseText)}}); return false;\">Remote outauthor</a>), assert_dom_equal %(<a href=\"#\" onclick=\"new Ajax.Request('http://www.example.com/whatnot?a=10&amp;b=20', {asynchronous:true, evalScripts:true, onFailure:function(request){alert(request.responseText)}}); return false;\">Remote outauthor</a>),
link_to_remote("Remote outauthor", :failure => "alert(request.responseText)", :url => { :action => "whatnot", :a => '10', :b => '20' }) link_to_remote("Remote outauthor", :failure => "alert(request.responseText)", :url => { :action => "whatnot", :a => '10', :b => '20' })
assert_dom_equal %(<a href=\"#\" onclick=\"new Ajax.Request('http://www.example.com/whatnot', {asynchronous:false, evalScripts:true}); return false;\">Remote outauthor</a>),
link_to_remote("Remote outauthor", :url => { :action => "whatnot" }, :type => :synchronous)
assert_dom_equal %(<a href=\"#\" onclick=\"new Ajax.Request('http://www.example.com/whatnot', {asynchronous:true, evalScripts:true, insertion:'bottom'}); return false;\">Remote outauthor</a>),
link_to_remote("Remote outauthor", :url => { :action => "whatnot" }, :position => :bottom)
end end
def test_link_to_remote_html_options def test_link_to_remote_html_options
@ -288,13 +292,13 @@ class JavaScriptGeneratorTest < PrototypeHelperBaseTest
end end
def test_insert_html_with_string def test_insert_html_with_string
assert_equal 'new Insertion.Top("element", "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E");', assert_equal 'Element.insert("element", { top: "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E" });',
@generator.insert_html(:top, 'element', '<p>This is a test</p>') @generator.insert_html(:top, 'element', '<p>This is a test</p>')
assert_equal 'new Insertion.Bottom("element", "\\u003Cp\u003EThis is a test\\u003C/p\u003E");', assert_equal 'Element.insert("element", { bottom: "\\u003Cp\u003EThis is a test\\u003C/p\u003E" });',
@generator.insert_html(:bottom, 'element', '<p>This is a test</p>') @generator.insert_html(:bottom, 'element', '<p>This is a test</p>')
assert_equal 'new Insertion.Before("element", "\\u003Cp\u003EThis is a test\\u003C/p\u003E");', assert_equal 'Element.insert("element", { before: "\\u003Cp\u003EThis is a test\\u003C/p\u003E" });',
@generator.insert_html(:before, 'element', '<p>This is a test</p>') @generator.insert_html(:before, 'element', '<p>This is a test</p>')
assert_equal 'new Insertion.After("element", "\\u003Cp\u003EThis is a test\\u003C/p\u003E");', assert_equal 'Element.insert("element", { after: "\\u003Cp\u003EThis is a test\\u003C/p\u003E" });',
@generator.insert_html(:after, 'element', '<p>This is a test</p>') @generator.insert_html(:after, 'element', '<p>This is a test</p>')
end end
@ -362,8 +366,8 @@ class JavaScriptGeneratorTest < PrototypeHelperBaseTest
@generator.replace_html('baz', '<p>This is a test</p>') @generator.replace_html('baz', '<p>This is a test</p>')
assert_equal <<-EOS.chomp, @generator.to_s assert_equal <<-EOS.chomp, @generator.to_s
new Insertion.Top("element", "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E"); Element.insert("element", { top: "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E" });
new Insertion.Bottom("element", "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E"); Element.insert("element", { bottom: "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E" });
["foo", "bar"].each(Element.remove); ["foo", "bar"].each(Element.remove);
Element.update("baz", "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E"); Element.update("baz", "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E");
EOS EOS
@ -425,6 +429,8 @@ Element.update("baz", "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E");
def test_sortable def test_sortable
assert_equal %(Sortable.create("blah", {onUpdate:function(){new Ajax.Request('http://www.example.com/order', {asynchronous:true, evalScripts:true, parameters:Sortable.serialize("blah")})}});), assert_equal %(Sortable.create("blah", {onUpdate:function(){new Ajax.Request('http://www.example.com/order', {asynchronous:true, evalScripts:true, parameters:Sortable.serialize("blah")})}});),
@generator.sortable('blah', :url => { :action => "order" }) @generator.sortable('blah', :url => { :action => "order" })
assert_equal %(Sortable.create("blah", {onUpdate:function(){new Ajax.Request('http://www.example.com/order', {asynchronous:false, evalScripts:true, parameters:Sortable.serialize("blah")})}});),
@generator.sortable('blah', :url => { :action => "order" }, :type => :synchronous)
end end
def test_draggable def test_draggable
@ -435,6 +441,8 @@ Element.update("baz", "\\u003Cp\\u003EThis is a test\\u003C/p\\u003E");
def test_drop_receiving def test_drop_receiving
assert_equal %(Droppables.add("blah", {onDrop:function(element){new Ajax.Request('http://www.example.com/order', {asynchronous:true, evalScripts:true, parameters:'id=' + encodeURIComponent(element.id)})}});), assert_equal %(Droppables.add("blah", {onDrop:function(element){new Ajax.Request('http://www.example.com/order', {asynchronous:true, evalScripts:true, parameters:'id=' + encodeURIComponent(element.id)})}});),
@generator.drop_receiving('blah', :url => { :action => "order" }) @generator.drop_receiving('blah', :url => { :action => "order" })
assert_equal %(Droppables.add("blah", {onDrop:function(element){new Ajax.Request('http://www.example.com/order', {asynchronous:false, evalScripts:true, parameters:'id=' + encodeURIComponent(element.id)})}});),
@generator.drop_receiving('blah', :url => { :action => "order" }, :type => :synchronous)
end end
def test_collection_first_and_last def test_collection_first_and_last

View file

@ -187,6 +187,7 @@ class TextHelperTest < ActionView::TestCase
http://www.mail-archive.com/rails@lists.rubyonrails.org/ http://www.mail-archive.com/rails@lists.rubyonrails.org/
http://www.amazon.com/Testing-Equal-Sign-In-Path/ref=pd_bbs_sr_1?ie=UTF8&s=books&qid=1198861734&sr=8-1 http://www.amazon.com/Testing-Equal-Sign-In-Path/ref=pd_bbs_sr_1?ie=UTF8&s=books&qid=1198861734&sr=8-1
http://en.wikipedia.org/wiki/Sprite_(computer_graphics) http://en.wikipedia.org/wiki/Sprite_(computer_graphics)
http://en.wikipedia.org/wiki/Texas_hold'em
) )
urls.each do |url| urls.each do |url|

View file

@ -284,6 +284,7 @@ class UrlHelperTest < ActionView::TestCase
assert_dom_equal "<a href=\"&#109;&#97;&#105;&#108;&#116;&#111;&#58;%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d\">My email</a>", mail_to("me@domain.com", "My email", :encode => "hex", :replace_at => "(at)") assert_dom_equal "<a href=\"&#109;&#97;&#105;&#108;&#116;&#111;&#58;%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d\">My email</a>", mail_to("me@domain.com", "My email", :encode => "hex", :replace_at => "(at)")
assert_dom_equal "<a href=\"&#109;&#97;&#105;&#108;&#116;&#111;&#58;%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d\">&#109;&#101;&#40;&#97;&#116;&#41;&#100;&#111;&#109;&#97;&#105;&#110;&#40;&#100;&#111;&#116;&#41;&#99;&#111;&#109;</a>", mail_to("me@domain.com", nil, :encode => "hex", :replace_at => "(at)", :replace_dot => "(dot)") assert_dom_equal "<a href=\"&#109;&#97;&#105;&#108;&#116;&#111;&#58;%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d\">&#109;&#101;&#40;&#97;&#116;&#41;&#100;&#111;&#109;&#97;&#105;&#110;&#40;&#100;&#111;&#116;&#41;&#99;&#111;&#109;</a>", mail_to("me@domain.com", nil, :encode => "hex", :replace_at => "(at)", :replace_dot => "(dot)")
assert_dom_equal "<script type=\"text/javascript\">eval(unescape('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%22%3e%4d%79%20%65%6d%61%69%6c%3c%2f%61%3e%27%29%3b'))</script>", mail_to("me@domain.com", "My email", :encode => "javascript", :replace_at => "(at)", :replace_dot => "(dot)") assert_dom_equal "<script type=\"text/javascript\">eval(unescape('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%22%3e%4d%79%20%65%6d%61%69%6c%3c%2f%61%3e%27%29%3b'))</script>", mail_to("me@domain.com", "My email", :encode => "javascript", :replace_at => "(at)", :replace_dot => "(dot)")
assert_dom_equal "<script type=\"text/javascript\">eval(unescape('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%22%3e%6d%65%28%61%74%29%64%6f%6d%61%69%6e%28%64%6f%74%29%63%6f%6d%3c%2f%61%3e%27%29%3b'))</script>", mail_to("me@domain.com", nil, :encode => "javascript", :replace_at => "(at)", :replace_dot => "(dot)")
end end
def protect_against_forgery? def protect_against_forgery?
@ -305,6 +306,10 @@ class UrlHelperWithControllerTest < ActionView::TestCase
render :inline => "<%= show_named_route_#{params[:kind]} %>" render :inline => "<%= show_named_route_#{params[:kind]} %>"
end end
def nil_url_for
render :inline => '<%= url_for(nil) %>'
end
def rescue_action(e) raise e end def rescue_action(e) raise e end
end end
@ -321,7 +326,7 @@ class UrlHelperWithControllerTest < ActionView::TestCase
assert_equal '/url_helper_with_controller/show_url_for', @response.body assert_equal '/url_helper_with_controller/show_url_for', @response.body
end end
def test_named_route_shows_host_and_path def test_named_route_url_shows_host_and_path
with_url_helper_routing do with_url_helper_routing do
get :show_named_route, :kind => 'url' get :show_named_route, :kind => 'url'
assert_equal 'http://test.host/url_helper_with_controller/show_named_route', @response.body assert_equal 'http://test.host/url_helper_with_controller/show_named_route', @response.body
@ -335,6 +340,11 @@ class UrlHelperWithControllerTest < ActionView::TestCase
end end
end end
def test_url_for_nil_returns_current_path
get :nil_url_for
assert_equal '/url_helper_with_controller/nil_url_for', @response.body
end
protected protected
def with_url_helper_routing def with_url_helper_routing
with_routing do |set| with_routing do |set|

View file

@ -10,7 +10,7 @@ Rake::RDocTask.new { |rdoc|
rdoc.title = "Active Model" rdoc.title = "Active Model"
rdoc.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object' rdoc.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
rdoc.options << '--charset' << 'utf-8' rdoc.options << '--charset' << 'utf-8'
rdoc.template = "#{ENV['template']}.rb" if ENV['template'] rdoc.template = ENV['template'] ? "#{ENV['template']}.rb" : '../doc/template/horo'
rdoc.rdoc_files.include('README', 'CHANGES') rdoc.rdoc_files.include('README', 'CHANGES')
rdoc.rdoc_files.include('lib/**/*.rb') rdoc.rdoc_files.include('lib/**/*.rb')
} }

View file

@ -1,3 +1,37 @@
*2.1.1 (September 4th, 2008)*
* Set config.active_record.timestamped_migrations = false to have migrations with numeric prefix instead of UTC timestamp. #446. [Andrew Stone, Nik Wakelin]
* Fixed that create database statements would always include "DEFAULT NULL" (Nick Sieger) [#334]
* change_column_default preserves the not-null constraint. #617 [Tarmo Tänav]
* Add :tokenizer option to validates_length_of to specify how to split up the attribute string. #507. [David Lowenfels] Example :
# Ensure essay contains at least 100 words.
validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least %d words."), :tokenizer => lambda {|str| str.scan(/\w+/) }
* Always treat integer :limit as byte length. #420 [Tarmo Tänav]
* Partial updates don't update lock_version if nothing changed. #426 [Daniel Morrison]
* Fix column collision with named_scope and :joins. #46 [Duncan Beevers, Mark Catley]
* db:migrate:down and :up update schema_migrations. #369 [Michael Raidel, RaceCondition]
* PostgreSQL: support :conditions => [':foo::integer', { :foo => 1 }] without treating the ::integer typecast as a bind variable. [Tarmo Tänav]
* MySQL: rename_column preserves column defaults. #466 [Diego Algorta]
* Add :from option to calculations. #397 [Ben Munat]
* Add :validate option to associations to enable/disable the automatic validation of associated models. Resolves #301. [Jan De Poorter]
* PostgreSQL: use 'INSERT ... RETURNING id' for 8.2 and later. [Jeremy Kemper]
* Added SQL escaping for :limit and :offset in MySQL [Jonathan Wiess]
*2.1.0 (May 31st, 2008)* *2.1.0 (May 31st, 2008)*
* Add ActiveRecord::Base.sti_name that checks ActiveRecord::Base#store_full_sti_class? and returns either the full or demodulized name. [rick] * Add ActiveRecord::Base.sti_name that checks ActiveRecord::Base#store_full_sti_class? and returns either the full or demodulized name. [rick]

View file

@ -5,6 +5,7 @@ require 'rake/rdoctask'
require 'rake/packagetask' require 'rake/packagetask'
require 'rake/gempackagetask' require 'rake/gempackagetask'
require 'rake/contrib/sshpublisher' require 'rake/contrib/sshpublisher'
require 'rake/contrib/rubyforgepublisher'
require File.join(File.dirname(__FILE__), 'lib', 'active_record', 'version') require File.join(File.dirname(__FILE__), 'lib', 'active_record', 'version')
require File.expand_path(File.dirname(__FILE__)) + "/test/config" require File.expand_path(File.dirname(__FILE__)) + "/test/config"
@ -141,7 +142,7 @@ Rake::RDocTask.new { |rdoc|
rdoc.title = "Active Record -- Object-relation mapping put on rails" rdoc.title = "Active Record -- Object-relation mapping put on rails"
rdoc.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object' rdoc.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
rdoc.options << '--charset' << 'utf-8' rdoc.options << '--charset' << 'utf-8'
rdoc.template = "#{ENV['template']}.rb" if ENV['template'] rdoc.template = ENV['template'] ? "#{ENV['template']}.rb" : '../doc/template/horo'
rdoc.rdoc_files.include('README', 'RUNNING_UNIT_TESTS', 'CHANGELOG') rdoc.rdoc_files.include('README', 'RUNNING_UNIT_TESTS', 'CHANGELOG')
rdoc.rdoc_files.include('lib/**/*.rb') rdoc.rdoc_files.include('lib/**/*.rb')
rdoc.rdoc_files.exclude('lib/active_record/vendor/*') rdoc.rdoc_files.exclude('lib/active_record/vendor/*')
@ -171,7 +172,7 @@ spec = Gem::Specification.new do |s|
s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) } s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
end end
s.add_dependency('activesupport', '= 2.1.0' + PKG_BUILD) s.add_dependency('activesupport', '= 2.1.1' + PKG_BUILD)
s.files.delete FIXTURES_ROOT + "/fixture_database.sqlite" s.files.delete FIXTURES_ROOT + "/fixture_database.sqlite"
s.files.delete FIXTURES_ROOT + "/fixture_database_2.sqlite" s.files.delete FIXTURES_ROOT + "/fixture_database_2.sqlite"
@ -225,13 +226,13 @@ end
desc "Publish the beta gem" desc "Publish the beta gem"
task :pgem => [:package] do task :pgem => [:package] do
Rake::SshFilePublisher.new("davidhh@wrath.rubyonrails.org", "public_html/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload Rake::SshFilePublisher.new("david@greed.loudthinking.com", "/u/sites/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
`ssh davidhh@wrath.rubyonrails.org './gemupdate.sh'` `ssh david@greed.loudthinking.com '/u/sites/gems/gemupdate.sh'`
end end
desc "Publish the API documentation" desc "Publish the API documentation"
task :pdoc => [:rdoc] do task :pdoc => [:rdoc] do
Rake::SshDirPublisher.new("davidhh@wrath.rubyonrails.org", "public_html/ar", "doc").upload Rake::SshDirPublisher.new("wrath.rubyonrails.org", "public_html/ar", "doc").upload
end end
desc "Publish the release files to RubyForge." desc "Publish the release files to RubyForge."

View file

@ -24,16 +24,14 @@
$:.unshift(File.dirname(__FILE__)) unless $:.unshift(File.dirname(__FILE__)) unless
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__))) $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
unless defined? ActiveSupport active_support_path = File.dirname(__FILE__) + "/../../activesupport/lib"
active_support_path = File.dirname(__FILE__) + "/../../activesupport/lib" if File.exist?(active_support_path)
if File.exist?(active_support_path) $:.unshift active_support_path
$:.unshift active_support_path require 'active_support'
require 'active_support' else
else require 'rubygems'
require 'rubygems' gem 'activesupport'
gem 'activesupport' require 'active_support'
require 'active_support'
end
end end
require 'active_record/base' require 'active_record/base'

View file

@ -51,9 +51,7 @@ module ActiveRecord
def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record) def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record)
parent_records.each do |parent_record| parent_records.each do |parent_record|
association_proxy = parent_record.send(reflection_name) parent_record.send("set_#{reflection_name}_target", associated_record)
association_proxy.loaded
association_proxy.target = associated_record
end end
end end
@ -103,17 +101,17 @@ module ActiveRecord
associated_records = reflection.klass.find(:all, :conditions => [conditions, ids], associated_records = reflection.klass.find(:all, :conditions => [conditions, ids],
:include => options[:include], :include => options[:include],
:joins => "INNER JOIN #{connection.quote_table_name options[:join_table]} as t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}", :joins => "INNER JOIN #{connection.quote_table_name options[:join_table]} as 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 _parent_record_id", :select => "#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id",
:order => options[:order]) :order => options[:order])
set_association_collection_records(id_to_record_map, reflection.name, associated_records, '_parent_record_id') set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'the_parent_record_id')
end end
def preload_has_one_association(records, reflection, preload_options={}) def preload_has_one_association(records, reflection, preload_options={})
id_to_record_map, ids = construct_id_map(records) id_to_record_map, ids = construct_id_map(records)
options = reflection.options options = reflection.options
records.each {|record| record.send("set_#{reflection.name}_target", nil)}
if options[:through] if options[:through]
records.each {|record| record.send(reflection.name) && record.send(reflection.name).loaded}
through_records = preload_through_records(records, reflection, options[:through]) through_records = preload_through_records(records, reflection, options[:through])
through_reflection = reflections[options[:through]] through_reflection = reflections[options[:through]]
through_primary_key = through_reflection.primary_key_name through_primary_key = through_reflection.primary_key_name
@ -126,8 +124,6 @@ module ActiveRecord
end end
end end
else else
records.each {|record| record.send("set_#{reflection.name}_target", nil)}
set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.primary_key_name) set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.primary_key_name)
end end
end end
@ -188,7 +184,6 @@ module ActiveRecord
through_records through_records
end end
# FIXME: quoting
def preload_belongs_to_association(records, reflection, preload_options={}) def preload_belongs_to_association(records, reflection, preload_options={})
options = reflection.options options = reflection.options
primary_key_name = reflection.primary_key_name primary_key_name = reflection.primary_key_name
@ -227,9 +222,19 @@ module ActiveRecord
table_name = klass.quoted_table_name table_name = klass.quoted_table_name
primary_key = klass.primary_key primary_key = klass.primary_key
conditions = "#{table_name}.#{primary_key} IN (?)" conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} IN (?)"
conditions << append_conditions(options, preload_options) conditions << append_conditions(options, preload_options)
associated_records = klass.find(:all, :conditions => [conditions, id_map.keys.uniq], column_type = klass.columns.detect{|c| c.name == primary_key}.type
ids = id_map.keys.uniq.map do |id|
if column_type == :integer
id.to_i
elsif column_type == :float
id.to_f
else
id
end
end
associated_records = klass.find(:all, :conditions => [conditions, ids],
:include => options[:include], :include => options[:include],
:select => options[:select], :select => options[:select],
:joins => options[:joins], :joins => options[:joins],
@ -243,7 +248,7 @@ module ActiveRecord
table_name = reflection.klass.quoted_table_name table_name = reflection.klass.quoted_table_name
if interface = reflection.options[:as] if interface = reflection.options[:as]
conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} IN (?) and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.name.demodulize}'" conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} IN (?) and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.sti_name}'"
else else
foreign_key = reflection.primary_key_name foreign_key = reflection.primary_key_name
conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} IN (?)" conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} IN (?)"

View file

@ -690,6 +690,7 @@ module ActiveRecord
# association is a polymorphic +belongs_to+. # association is a polymorphic +belongs_to+.
# * <tt>:uniq</tt> - If true, duplicates will be omitted from the collection. Useful in conjunction with <tt>:through</tt>. # * <tt>:uniq</tt> - If true, duplicates will be omitted from the collection. Useful in conjunction with <tt>:through</tt>.
# * <tt>:readonly</tt> - If true, all the associated objects are readonly through the association. # * <tt>:readonly</tt> - If true, all the associated objects are readonly through the association.
# * <tt>:validate</tt> - If false, don't validate the associated objects when saving the parent object. true by default.
# #
# Option examples: # Option examples:
# has_many :comments, :order => "posted_on" # has_many :comments, :order => "posted_on"
@ -710,6 +711,7 @@ module ActiveRecord
configure_dependency_for_has_many(reflection) configure_dependency_for_has_many(reflection)
add_multiple_associated_validation_callbacks(reflection.name) unless options[:validate] == false
add_multiple_associated_save_callbacks(reflection.name) add_multiple_associated_save_callbacks(reflection.name)
add_association_callbacks(reflection.name, reflection.options) add_association_callbacks(reflection.name, reflection.options)
@ -769,6 +771,7 @@ module ActiveRecord
# * <tt>:source_type</tt> - Specifies type of the source association used by <tt>has_one :through</tt> queries where the source # * <tt>:source_type</tt> - Specifies type of the source association used by <tt>has_one :through</tt> queries where the source
# association is a polymorphic +belongs_to+. # association is a polymorphic +belongs_to+.
# * <tt>:readonly</tt> - If true, the associated object is readonly through the association. # * <tt>:readonly</tt> - If true, the associated object is readonly through the association.
# * <tt>:validate</tt> - If false, don't validate the associated object when saving the parent object. +false+ by default.
# #
# Option examples: # Option examples:
# has_one :credit_card, :dependent => :destroy # destroys the associated credit card # has_one :credit_card, :dependent => :destroy # destroys the associated credit card
@ -799,7 +802,7 @@ module ActiveRecord
end end
after_save method_name after_save method_name
add_single_associated_save_callbacks(reflection.name) add_single_associated_validation_callbacks(reflection.name) if options[:validate] == true
association_accessor_methods(reflection, HasOneAssociation) association_accessor_methods(reflection, HasOneAssociation)
association_constructor_method(:build, reflection, HasOneAssociation) association_constructor_method(:build, reflection, HasOneAssociation)
association_constructor_method(:create, reflection, HasOneAssociation) association_constructor_method(:create, reflection, HasOneAssociation)
@ -857,6 +860,7 @@ module ActiveRecord
# Note: If you've enabled the counter cache, then you may want to add the counter cache attribute # Note: If you've enabled the counter cache, then you may want to add the counter cache attribute
# to the +attr_readonly+ list in the associated classes (e.g. <tt>class Post; attr_readonly :comments_count; end</tt>). # to the +attr_readonly+ list in the associated classes (e.g. <tt>class Post; attr_readonly :comments_count; end</tt>).
# * <tt>:readonly</tt> - If true, the associated object is readonly through the association. # * <tt>:readonly</tt> - If true, the associated object is readonly through the association.
# * <tt>:validate</tt> - If false, don't validate the associated objects when saving the parent object. +false+ by default.
# #
# Option examples: # Option examples:
# belongs_to :firm, :foreign_key => "client_of" # belongs_to :firm, :foreign_key => "client_of"
@ -937,6 +941,8 @@ module ActiveRecord
) )
end end
add_single_associated_validation_callbacks(reflection.name) if options[:validate] == true
configure_dependency_for_belongs_to(reflection) configure_dependency_for_belongs_to(reflection)
end end
@ -1025,6 +1031,7 @@ module ActiveRecord
# * <tt>:select</tt> - By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join # * <tt>:select</tt> - By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join
# but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error. # but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error.
# * <tt>:readonly</tt> - If true, all the associated objects are readonly through the association. # * <tt>:readonly</tt> - If true, all the associated objects are readonly through the association.
# * <tt>:validate</tt> - If false, don't validate the associated objects when saving the parent object. +true+ by default.
# #
# Option examples: # Option examples:
# has_and_belongs_to_many :projects # has_and_belongs_to_many :projects
@ -1037,6 +1044,7 @@ module ActiveRecord
def has_and_belongs_to_many(association_id, options = {}, &extension) def has_and_belongs_to_many(association_id, options = {}, &extension)
reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension) reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension)
add_multiple_associated_validation_callbacks(reflection.name) unless options[:validate] == false
add_multiple_associated_save_callbacks(reflection.name) add_multiple_associated_save_callbacks(reflection.name)
collection_accessor_methods(reflection, HasAndBelongsToManyAssociation) collection_accessor_methods(reflection, HasAndBelongsToManyAssociation)
@ -1103,10 +1111,9 @@ module ActiveRecord
association.create_through_record(new_value) association.create_through_record(new_value)
self.send(reflection.name, new_value) self.send(reflection.name, new_value)
else else
association.replace(new_value) association.replace(new_value)
instance_variable_set(ivar, new_value.nil? ? nil : association)
end end
instance_variable_set(ivar, new_value.nil? ? nil : association)
end end
define_method("set_#{reflection.name}_target") do |target| define_method("set_#{reflection.name}_target") do |target|
@ -1157,7 +1164,7 @@ module ActiveRecord
end end
end end
def add_single_associated_save_callbacks(association_name) def add_single_associated_validation_callbacks(association_name)
method_name = "validate_associated_records_for_#{association_name}".to_sym method_name = "validate_associated_records_for_#{association_name}".to_sym
define_method(method_name) do define_method(method_name) do
association = instance_variable_get("@#{association_name}") association = instance_variable_get("@#{association_name}")
@ -1169,7 +1176,7 @@ module ActiveRecord
validate method_name validate method_name
end end
def add_multiple_associated_save_callbacks(association_name) def add_multiple_associated_validation_callbacks(association_name)
method_name = "validate_associated_records_for_#{association_name}".to_sym method_name = "validate_associated_records_for_#{association_name}".to_sym
ivar = "@#{association_name}" ivar = "@#{association_name}"
@ -1190,6 +1197,10 @@ module ActiveRecord
end end
validate method_name validate method_name
end
def add_multiple_associated_save_callbacks(association_name)
ivar = "@#{association_name}"
method_name = "before_save_associated_records_for_#{association_name}".to_sym method_name = "before_save_associated_records_for_#{association_name}".to_sym
define_method(method_name) do define_method(method_name) do
@ -1211,7 +1222,6 @@ module ActiveRecord
else else
[] []
end end
records_to_save.each { |record| association.send(:insert_record, record) } unless records_to_save.blank? records_to_save.each { |record| association.send(:insert_record, record) } unless records_to_save.blank?
# reconstruct the SQL queries now that we know the owner's id # reconstruct the SQL queries now that we know the owner's id
@ -1343,7 +1353,8 @@ module ActiveRecord
:uniq, :uniq,
:finder_sql, :counter_sql, :finder_sql, :counter_sql,
:before_add, :after_add, :before_remove, :after_remove, :before_add, :after_add, :before_remove, :after_remove,
:extend, :readonly :extend, :readonly,
:validate
) )
options[:extend] = create_extension_modules(association_id, extension, options[:extend]) options[:extend] = create_extension_modules(association_id, extension, options[:extend])
@ -1353,7 +1364,7 @@ module ActiveRecord
def create_has_one_reflection(association_id, options) def create_has_one_reflection(association_id, options)
options.assert_valid_keys( options.assert_valid_keys(
:class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :readonly :class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :readonly, :validate
) )
create_reflection(:has_one, association_id, options, self) create_reflection(:has_one, association_id, options, self)
@ -1361,7 +1372,7 @@ module ActiveRecord
def create_has_one_through_reflection(association_id, options) def create_has_one_through_reflection(association_id, options)
options.assert_valid_keys( options.assert_valid_keys(
:class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :through, :source, :source_type :class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :through, :source, :source_type, :validate
) )
create_reflection(:has_one, association_id, options, self) create_reflection(:has_one, association_id, options, self)
end end
@ -1369,7 +1380,7 @@ module ActiveRecord
def create_belongs_to_reflection(association_id, options) def create_belongs_to_reflection(association_id, options)
options.assert_valid_keys( options.assert_valid_keys(
:class_name, :foreign_key, :foreign_type, :remote, :select, :conditions, :include, :dependent, :class_name, :foreign_key, :foreign_type, :remote, :select, :conditions, :include, :dependent,
:counter_cache, :extend, :polymorphic, :readonly :counter_cache, :extend, :polymorphic, :readonly, :validate
) )
reflection = create_reflection(:belongs_to, association_id, options, self) reflection = create_reflection(:belongs_to, association_id, options, self)
@ -1388,7 +1399,8 @@ module ActiveRecord
:uniq, :uniq,
:finder_sql, :delete_sql, :insert_sql, :finder_sql, :delete_sql, :insert_sql,
:before_add, :after_add, :before_remove, :after_remove, :before_add, :after_add, :before_remove, :after_remove,
:extend, :readonly :extend, :readonly,
:validate
) )
options[:extend] = create_extension_modules(association_id, extension, options[:extend]) options[:extend] = create_extension_modules(association_id, extension, options[:extend])
@ -1465,10 +1477,15 @@ module ActiveRecord
join_dependency.joins_for_table_name(table) join_dependency.joins_for_table_name(table)
}.flatten.compact.uniq }.flatten.compact.uniq
order = options[:order]
if scoped_order = (scope && scope[:order])
order = order ? "#{order}, #{scoped_order}" : scoped_order
end
is_distinct = !options[:joins].blank? || include_eager_conditions?(options, tables_from_conditions) || include_eager_order?(options, tables_from_order) is_distinct = !options[:joins].blank? || include_eager_conditions?(options, tables_from_conditions) || include_eager_order?(options, tables_from_order)
sql = "SELECT " sql = "SELECT "
if is_distinct if is_distinct
sql << connection.distinct("#{connection.quote_table_name table_name}.#{primary_key}", options[:order]) sql << connection.distinct("#{connection.quote_table_name table_name}.#{primary_key}", order)
else else
sql << primary_key sql << primary_key
end end
@ -1482,8 +1499,8 @@ module ActiveRecord
add_conditions!(sql, options[:conditions], scope) add_conditions!(sql, options[:conditions], scope)
add_group!(sql, options[:group], scope) add_group!(sql, options[:group], scope)
if options[:order] && is_distinct if order && is_distinct
connection.add_order_by_for_association_limiting!(sql, options) connection.add_order_by_for_association_limiting!(sql, :order => order)
else else
add_order!(sql, options[:order], scope) add_order!(sql, options[:order], scope)
end end
@ -1502,19 +1519,19 @@ module ActiveRecord
else all << cond else all << cond
end end
end end
conditions.join(' ').scan(/([\.\w]+).?\./).flatten conditions.join(' ').scan(/([\.a-zA-Z_]+).?\./).flatten
end end
def order_tables(options) def order_tables(options)
order = options[:order] order = [options[:order], scope(:find, :order) ].join(", ")
return [] unless order && order.is_a?(String) return [] unless order && order.is_a?(String)
order.scan(/([\.\w]+).?\./).flatten order.scan(/([\.a-zA-Z_]+).?\./).flatten
end end
def selects_tables(options) def selects_tables(options)
select = options[:select] select = options[:select]
return [] unless select && select.is_a?(String) return [] unless select && select.is_a?(String)
select.scan(/"?([\.\w]+)"?.?\./).flatten select.scan(/"?([\.a-zA-Z_]+)"?.?\./).flatten
end end
# Checks if the conditions reference a table other than the current model table # Checks if the conditions reference a table other than the current model table
@ -1638,7 +1655,9 @@ module ActiveRecord
end end
def join_for_table_name(table_name) def join_for_table_name(table_name)
@joins.select{|j|j.aliased_table_name == table_name.gsub(/^\"(.*)\"$/){$1} }.first rescue nil join = (@joins.select{|j|j.aliased_table_name == table_name.gsub(/^\"(.*)\"$/){$1} }.first) rescue nil
return join unless join.nil?
@joins.select{|j|j.is_a?(JoinAssociation) && j.aliased_join_table_name == table_name.gsub(/^\"(.*)\"$/){$1} }.first rescue nil
end end
def joins_for_table_name(table_name) def joins_for_table_name(table_name)
@ -1714,6 +1733,7 @@ module ActiveRecord
collection.target.push(association) collection.target.push(association)
when :has_one when :has_one
return if record.id.to_s != join.parent.record_id(row).to_s return if record.id.to_s != join.parent.record_id(row).to_s
return if record.instance_variable_defined?("@#{join.reflection.name}")
association = join.instantiate(row) unless row[join.aliased_primary_key].nil? association = join.instantiate(row) unless row[join.aliased_primary_key].nil?
record.send("set_#{join.reflection.name}_target", association) record.send("set_#{join.reflection.name}_target", association)
when :belongs_to when :belongs_to
@ -1795,7 +1815,7 @@ module ActiveRecord
@aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join") @aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join")
end end
if reflection.macro == :has_many && reflection.options[:through] if [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through]
@aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join") @aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join")
end end
end end
@ -1819,7 +1839,7 @@ module ActiveRecord
] ]
when :has_many, :has_one when :has_many, :has_one
case case
when reflection.macro == :has_many && reflection.options[:through] when reflection.options[:through]
through_conditions = through_reflection.options[:conditions] ? "AND #{interpolate_sql(sanitize_sql(through_reflection.options[:conditions]))}" : '' through_conditions = through_reflection.options[:conditions] ? "AND #{interpolate_sql(sanitize_sql(through_reflection.options[:conditions]))}" : ''
jt_foreign_key = jt_as_extra = jt_source_extra = jt_sti_extra = nil jt_foreign_key = jt_as_extra = jt_source_extra = jt_sti_extra = nil
@ -1855,7 +1875,7 @@ module ActiveRecord
jt_sti_extra = " AND %s.%s = %s" % [ jt_sti_extra = " AND %s.%s = %s" % [
connection.quote_table_name(aliased_join_table_name), connection.quote_table_name(aliased_join_table_name),
connection.quote_column_name(through_reflection.active_record.inheritance_column), connection.quote_column_name(through_reflection.active_record.inheritance_column),
through_reflection.klass.quote_value(through_reflection.klass.name.demodulize)] through_reflection.klass.quote_value(through_reflection.klass.sti_name)]
end end
when :belongs_to when :belongs_to
first_key = primary_key first_key = primary_key
@ -1920,10 +1940,8 @@ module ActiveRecord
else else
"" ""
end || '' end || ''
join << %(AND %s.%s = %s ) % [ join << %(AND %s) % [
connection.quote_table_name(aliased_table_name), klass.send(:type_condition, aliased_table_name)] unless klass.descends_from_active_record?
connection.quote_column_name(klass.inheritance_column),
klass.quote_value(klass.name.demodulize)] unless klass.descends_from_active_record?
[through_reflection, reflection].each do |ref| [through_reflection, reflection].each do |ref|
join << "AND #{interpolate_sql(sanitize_sql(ref.options[:conditions]))} " if ref && ref.options[:conditions] join << "AND #{interpolate_sql(sanitize_sql(ref.options[:conditions]))} " if ref && ref.options[:conditions]

View file

@ -78,11 +78,14 @@ module ActiveRecord
@loaded = false @loaded = false
end end
def build(attributes = {}) def build(attributes = {}, &block)
if attributes.is_a?(Array) if attributes.is_a?(Array)
attributes.collect { |attr| build(attr) } attributes.collect { |attr| build(attr, &block) }
else else
build_record(attributes) { |record| set_belongs_to_association_for(record) } build_record(attributes) do |record|
block.call(record) if block_given?
set_belongs_to_association_for(record)
end
end end
end end
@ -187,7 +190,7 @@ module ActiveRecord
if @owner.new_record? || (loaded? && !@reflection.options[:uniq]) if @owner.new_record? || (loaded? && !@reflection.options[:uniq])
@target.size @target.size
elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array) elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array)
unsaved_records = Array(@target.detect { |r| r.new_record? }) unsaved_records = @target.select { |r| r.new_record? }
unsaved_records.size + count_records unsaved_records.size + count_records
else else
count_records count_records
@ -335,7 +338,7 @@ module ActiveRecord
callback(:before_add, record) callback(:before_add, record)
yield(record) if block_given? yield(record) if block_given?
@target ||= [] unless loaded? @target ||= [] unless loaded?
@target << record @target << record unless @reflection.options[:uniq] && @target.include?(record)
callback(:after_add, record) callback(:after_add, record)
record record
end end

View file

@ -69,8 +69,8 @@ module ActiveRecord
@target @target
end end
def respond_to?(symbol, include_priv = false) def respond_to?(*args)
proxy_respond_to?(symbol, include_priv) || (load_target && @target.respond_to?(symbol, include_priv)) proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args))
end end
# Explicitly proxy === because the instance method removal above # Explicitly proxy === because the instance method removal above
@ -131,10 +131,6 @@ module ActiveRecord
records.map { |record| record.quoted_id }.join(',') records.map { |record| record.quoted_id }.join(',')
end end
def interpolate_sql_options!(options, *keys)
keys.each { |key| options[key] &&= interpolate_sql(options[key]) }
end
def interpolate_sql(sql, record = nil) def interpolate_sql(sql, record = nil)
@owner.send(:interpolate_sql, sql, record) @owner.send(:interpolate_sql, sql, record)
end end

View file

@ -70,10 +70,8 @@ module ActiveRecord
end end
def construct_sql def construct_sql
interpolate_sql_options!(@reflection.options, :finder_sql)
if @reflection.options[:finder_sql] if @reflection.options[:finder_sql]
@finder_sql = @reflection.options[:finder_sql] @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
else else
@finder_sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{@owner.quoted_id} " @finder_sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{@owner.quoted_id} "
@finder_sql << " AND (#{conditions})" if conditions @finder_sql << " AND (#{conditions})" if conditions
@ -87,6 +85,7 @@ module ActiveRecord
:joins => @join_sql, :joins => @join_sql,
:readonly => false, :readonly => false,
:order => @reflection.options[:order], :order => @reflection.options[:order],
:include => @reflection.options[:include],
:limit => @reflection.options[:limit] } } :limit => @reflection.options[:limit] } }
end end

View file

@ -14,7 +14,16 @@ module ActiveRecord
@finder_sql + " AND (#{sanitize_sql(options[:conditions])})" @finder_sql + " AND (#{sanitize_sql(options[:conditions])})"
options[:include] ||= @reflection.options[:include] options[:include] ||= @reflection.options[:include]
@reflection.klass.count(column_name, options) value = @reflection.klass.count(column_name, options)
limit = @reflection.options[:limit]
offset = @reflection.options[:offset]
if limit || offset
[ [value - offset.to_i, 0].max, limit.to_i ].min
else
value
end
end end
end end
@ -27,8 +36,11 @@ module ActiveRecord
else else
@reflection.klass.count(:conditions => @counter_sql, :include => @reflection.options[:include]) @reflection.klass.count(:conditions => @counter_sql, :include => @reflection.options[:include])
end end
@target = [] and loaded if count == 0 # If there's nothing in the database and @target has no new records
# we are certain the current target is an empty array. This is a
# documented side-effect of the method that may avoid an extra SELECT.
@target ||= [] and loaded if count == 0
if @reflection.options[:limit] if @reflection.options[:limit]
count = [ @reflection.options[:limit], count ].min count = [ @reflection.options[:limit], count ].min
@ -100,7 +112,7 @@ module ActiveRecord
create_scoping = {} create_scoping = {}
set_belongs_to_association_for(create_scoping) set_belongs_to_association_for(create_scoping)
{ {
:find => { :conditions => @finder_sql, :readonly => false, :order => @reflection.options[:order], :limit => @reflection.options[:limit] }, :find => { :conditions => @finder_sql, :readonly => false, :order => @reflection.options[:order], :limit => @reflection.options[:limit], :include => @reflection.options[:include]},
:create => create_scoping :create => create_scoping
} }
end end

View file

@ -237,7 +237,7 @@ module ActiveRecord
end end
def build_sti_condition def build_sti_condition
"#{@reflection.through_reflection.quoted_table_name}.#{@reflection.through_reflection.klass.inheritance_column} = #{@reflection.klass.quote_value(@reflection.through_reflection.klass.sti_name)}" @reflection.through_reflection.klass.send(:type_condition)
end end
alias_method :sql_conditions, :conditions alias_method :sql_conditions, :conditions

View file

@ -21,8 +21,8 @@ module ActiveRecord
def replace(obj, dont_save = false) def replace(obj, dont_save = false)
load_target load_target
unless @target.nil? unless @target.nil? || @target == obj
if dependent? && !dont_save && @target != obj if dependent? && !dont_save
@target.destroy unless @target.new_record? @target.destroy unless @target.new_record?
@owner.clear_association_cache @owner.clear_association_cache
else else

View file

@ -22,6 +22,10 @@ module ActiveRecord
def find_target def find_target
super.first super.first
end
def reset_target!
@target = nil
end end
end end
end end

View file

@ -372,7 +372,7 @@ module ActiveRecord #:nodoc:
def self.reset_subclasses #:nodoc: def self.reset_subclasses #:nodoc:
nonreloadables = [] nonreloadables = []
subclasses.each do |klass| subclasses.each do |klass|
unless Dependencies.autoloaded? klass unless ActiveSupport::Dependencies.autoloaded? klass
nonreloadables << klass nonreloadables << klass
next next
end end
@ -439,6 +439,10 @@ module ActiveRecord #:nodoc:
cattr_accessor :schema_format , :instance_writer => false cattr_accessor :schema_format , :instance_writer => false
@@schema_format = :ruby @@schema_format = :ruby
# Specify whether or not to use timestamps for migration numbers
cattr_accessor :timestamped_migrations , :instance_writer => false
@@timestamped_migrations = true
# Determine whether to store the full constant name including namespace when using STI # Determine whether to store the full constant name including namespace when using STI
superclass_delegating_accessor :store_full_sti_class superclass_delegating_accessor :store_full_sti_class
self.store_full_sti_class = false self.store_full_sti_class = false
@ -828,7 +832,7 @@ module ActiveRecord #:nodoc:
def update_counters(id, counters) def update_counters(id, counters)
updates = counters.inject([]) { |list, (counter_name, increment)| updates = counters.inject([]) { |list, (counter_name, increment)|
sign = increment < 0 ? "-" : "+" sign = increment < 0 ? "-" : "+"
list << "#{connection.quote_column_name(counter_name)} = #{connection.quote_column_name(counter_name)} #{sign} #{increment.abs}" list << "#{connection.quote_column_name(counter_name)} = COALESCE(#{connection.quote_column_name(counter_name)}, 0) #{sign} #{increment.abs}"
}.join(", ") }.join(", ")
update_all(updates, "#{connection.quote_column_name(primary_key)} = #{quote_value(id)}") update_all(updates, "#{connection.quote_column_name(primary_key)} = #{quote_value(id)}")
end end
@ -1465,7 +1469,7 @@ module ActiveRecord #:nodoc:
def construct_finder_sql(options) def construct_finder_sql(options)
scope = scope(:find) scope = scope(:find)
sql = "SELECT #{options[:select] || (scope && scope[:select]) || (options[:joins] && quoted_table_name + '.*') || '*'} " sql = "SELECT #{options[:select] || (scope && scope[:select]) || ((options[:joins] || (scope && scope[:joins])) && quoted_table_name + '.*') || '*'} "
sql << "FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} " sql << "FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} "
add_joins!(sql, options, scope) add_joins!(sql, options, scope)
@ -1577,10 +1581,11 @@ module ActiveRecord #:nodoc:
sql << "WHERE #{merged_conditions} " unless merged_conditions.blank? sql << "WHERE #{merged_conditions} " unless merged_conditions.blank?
end end
def type_condition def type_condition(table_alias=nil)
quoted_table_alias = self.connection.quote_table_name(table_alias || table_name)
quoted_inheritance_column = connection.quote_column_name(inheritance_column) quoted_inheritance_column = connection.quote_column_name(inheritance_column)
type_condition = subclasses.inject("#{quoted_table_name}.#{quoted_inheritance_column} = '#{sti_name}' ") do |condition, subclass| type_condition = subclasses.inject("#{quoted_table_alias}.#{quoted_inheritance_column} = '#{sti_name}' ") do |condition, subclass|
condition << "OR #{quoted_table_name}.#{quoted_inheritance_column} = '#{subclass.sti_name}' " condition << "OR #{quoted_table_alias}.#{quoted_inheritance_column} = '#{subclass.sti_name}' "
end end
" (#{type_condition}) " " (#{type_condition}) "
@ -1717,7 +1722,7 @@ module ActiveRecord #:nodoc:
def attribute_condition(argument) def attribute_condition(argument)
case argument case argument
when nil then "IS ?" when nil then "IS ?"
when Array, ActiveRecord::Associations::AssociationCollection then "IN (?)" when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::NamedScope::Scope then "IN (?)"
when Range then "BETWEEN ? AND ?" when Range then "BETWEEN ? AND ?"
else "= ?" else "= ?"
end end
@ -2053,9 +2058,10 @@ module ActiveRecord #:nodoc:
end end
def replace_named_bind_variables(statement, bind_vars) #:nodoc: def replace_named_bind_variables(statement, bind_vars) #:nodoc:
statement.gsub(/:([a-zA-Z]\w*)/) do statement.gsub(/(:?):([a-zA-Z]\w*)/) do
match = $1.to_sym if $1 == ':' # skip postgresql casts
if bind_vars.include?(match) $& # return the whole match
elsif bind_vars.include?(match = $2.to_sym)
quote_bound_value(bind_vars[match]) quote_bound_value(bind_vars[match])
else else
raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}" raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}"
@ -2064,13 +2070,18 @@ module ActiveRecord #:nodoc:
end end
def expand_range_bind_variables(bind_vars) #:nodoc: def expand_range_bind_variables(bind_vars) #:nodoc:
bind_vars.sum do |var| expanded = []
bind_vars.each do |var|
if var.is_a?(Range) if var.is_a?(Range)
[var.first, var.last] expanded << var.first
expanded << var.last
else else
[var] expanded << var
end end
end end
expanded
end end
def quote_bound_value(value) #:nodoc: def quote_bound_value(value) #:nodoc:
@ -2572,8 +2583,15 @@ module ActiveRecord #:nodoc:
quoted = {} quoted = {}
connection = self.class.connection connection = self.class.connection
attribute_names.each do |name| attribute_names.each do |name|
if column = column_for_attribute(name) if (column = column_for_attribute(name)) && (include_primary_key || !column.primary)
quoted[name] = connection.quote(read_attribute(name), column) unless !include_primary_key && column.primary value = read_attribute(name)
# We need explicit to_yaml because quote() does not properly convert Time/Date fields to YAML.
if value && self.class.serialized_attributes.has_key?(name) && (value.acts_like?(:date) || value.acts_like?(:time))
value = value.to_yaml
end
quoted[name] = connection.quote(value, column)
end end
end end
include_readonly_attributes ? quoted : remove_readonly_attributes(quoted) include_readonly_attributes ? quoted : remove_readonly_attributes(quoted)

View file

@ -1,6 +1,6 @@
module ActiveRecord module ActiveRecord
module Calculations #:nodoc: module Calculations #:nodoc:
CALCULATIONS_OPTIONS = [:conditions, :joins, :order, :select, :group, :having, :distinct, :limit, :offset, :include] CALCULATIONS_OPTIONS = [:conditions, :joins, :order, :select, :group, :having, :distinct, :limit, :offset, :include, :from]
def self.included(base) def self.included(base)
base.extend(ClassMethods) base.extend(ClassMethods)
end end
@ -27,6 +27,8 @@ module ActiveRecord
# * <tt>:select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you, for example, want to do a join but not # * <tt>:select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you, for example, want to do a join but not
# include the joined columns. # include the joined columns.
# * <tt>:distinct</tt>: Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ... # * <tt>:distinct</tt>: Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ...
# * <tt>:from</tt> - By default, this is the table name of the class, but can be changed to an alternate table name (or even the name
# of a database view).
# #
# Examples for counting all: # Examples for counting all:
# Person.count # returns the total count of all people # Person.count # returns the total count of all people
@ -71,7 +73,7 @@ module ActiveRecord
# #
# Person.sum('age') # Person.sum('age')
def sum(column_name, options = {}) def sum(column_name, options = {})
calculate(:sum, column_name, options) || 0 calculate(:sum, column_name, options)
end end
# This calculates aggregate values in the given column. Methods for count, sum, average, minimum, and maximum have been added as shortcuts. # This calculates aggregate values in the given column. Methods for count, sum, average, minimum, and maximum have been added as shortcuts.
@ -178,13 +180,23 @@ module ActiveRecord
sql = "SELECT COUNT(*) AS #{aggregate_alias}" if use_workaround sql = "SELECT COUNT(*) AS #{aggregate_alias}" if use_workaround
sql << ", #{options[:group_field]} AS #{options[:group_alias]}" if options[:group] sql << ", #{options[:group_field]} AS #{options[:group_alias]}" if options[:group]
sql << " FROM (SELECT #{distinct}#{column_name}" if use_workaround if options[:from]
sql << " FROM #{connection.quote_table_name(table_name)} " sql << " FROM #{options[:from]} "
else
sql << " FROM (SELECT #{distinct}#{column_name}" if use_workaround
sql << " FROM #{connection.quote_table_name(table_name)} "
end
joins = ""
add_joins!(joins, options, scope)
if merged_includes.any? if merged_includes.any?
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, options[:joins]) join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, joins)
sql << join_dependency.join_associations.collect{|join| join.association_join }.join sql << join_dependency.join_associations.collect{|join| join.association_join }.join
end end
add_joins!(sql, options, scope)
sql << joins unless joins.blank?
add_conditions!(sql, options[:conditions], scope) add_conditions!(sql, options[:conditions], scope)
add_limited_ids_condition!(sql, options, join_dependency) if join_dependency && !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit]) add_limited_ids_condition!(sql, options, join_dependency) if join_dependency && !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
@ -205,7 +217,7 @@ module ActiveRecord
sql << " ORDER BY #{options[:order]} " if options[:order] sql << " ORDER BY #{options[:order]} " if options[:order]
add_limit!(sql, options, scope) add_limit!(sql, options, scope)
sql << ')' if use_workaround sql << ') AS #{aggregate_alias}_subquery' if use_workaround
sql sql
end end
@ -266,6 +278,7 @@ module ActiveRecord
operation = operation.to_s.downcase operation = operation.to_s.downcase
case operation case operation
when 'count' then value.to_i when 'count' then value.to_i
when 'sum' then value =~ /\./ ? value.to_f : value.to_i
when 'avg' then value && value.to_f when 'avg' then value && value.to_f
else column ? column.type_cast(value) : value else column ? column.type_cast(value) : value
end end

View file

@ -257,7 +257,10 @@ module ActiveRecord
def to_sql def to_sql
column_sql = "#{base.quote_column_name(name)} #{sql_type}" column_sql = "#{base.quote_column_name(name)} #{sql_type}"
add_column_options!(column_sql, :null => null, :default => default) unless type.to_sym == :primary_key column_options = {}
column_options[:null] = null unless null.nil?
column_options[:default] = default unless default.nil?
add_column_options!(column_sql, column_options) unless type.to_sym == :primary_key
column_sql column_sql
end end
alias to_s :to_sql alias to_s :to_sql
@ -304,8 +307,7 @@ module ActiveRecord
# #
# Available options are (none of these exists by default): # Available options are (none of these exists by default):
# * <tt>:limit</tt> - # * <tt>:limit</tt> -
# Requests a maximum column length (<tt>:string</tt>, <tt>:text</tt>, # Requests a maximum column length. This is number of characters for <tt>:string</tt> and <tt>:text</tt> columns and number of bytes for :binary and :integer columns.
# <tt>:binary</tt> or <tt>:integer</tt> columns only)
# * <tt>:default</tt> - # * <tt>:default</tt> -
# The column's default value. Use nil for NULL. # The column's default value. Use nil for NULL.
# * <tt>:null</tt> - # * <tt>:null</tt> -
@ -442,9 +444,10 @@ module ActiveRecord
# Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and # Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and
# <tt>:updated_at</tt> to the table. # <tt>:updated_at</tt> to the table.
def timestamps def timestamps(*args)
column(:created_at, :datetime) options = args.extract_options!
column(:updated_at, :datetime) column(:created_at, :datetime, options)
column(:updated_at, :datetime, options)
end end
def references(*args) def references(*args)

View file

@ -331,15 +331,26 @@ module ActiveRecord
end end
def assume_migrated_upto_version(version) def assume_migrated_upto_version(version)
version = version.to_i
sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name) sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name)
migrated = select_values("SELECT version FROM #{sm_table}").map(&:to_i) migrated = select_values("SELECT version FROM #{sm_table}").map(&:to_i)
versions = Dir['db/migrate/[0-9]*_*.rb'].map do |filename| versions = Dir['db/migrate/[0-9]*_*.rb'].map do |filename|
filename.split('/').last.split('_').first.to_i filename.split('/').last.split('_').first.to_i
end end
execute "INSERT INTO #{sm_table} (version) VALUES ('#{version}')" unless migrated.include?(version.to_i) unless migrated.include?(version)
(versions - migrated).select { |v| v < version.to_i }.each do |v| execute "INSERT INTO #{sm_table} (version) VALUES ('#{version}')"
execute "INSERT INTO #{sm_table} (version) VALUES ('#{v}')" end
inserted = Set.new
(versions - migrated).each do |v|
if inserted.include?(v)
raise "Duplicate migration #{v}. Please renumber your migrations to resolve the conflict."
elsif v < version
execute "INSERT INTO #{sm_table} (version) VALUES ('#{v}')"
inserted << v
end
end end
end end
@ -372,13 +383,9 @@ module ActiveRecord
def add_column_options!(sql, options) #:nodoc: def add_column_options!(sql, options) #:nodoc:
sql << " DEFAULT #{quote(options[:default], options[:column])}" if options_include_default?(options) sql << " DEFAULT #{quote(options[:default], options[:column])}" if options_include_default?(options)
# must explcitly check for :null to allow change_column to work on migrations # must explicitly check for :null to allow change_column to work on migrations
if options.has_key? :null if options[:null] == false
if options[:null] == false sql << " NOT NULL"
sql << " NOT NULL"
else
sql << " NULL"
end
end end
end end

View file

@ -50,10 +50,7 @@ module ActiveRecord
rescue LoadError => cannot_require_mysql rescue LoadError => cannot_require_mysql
# Use the bundled Ruby/MySQL driver if no driver is already in place # Use the bundled Ruby/MySQL driver if no driver is already in place
begin begin
ActiveRecord::Base.logger.info( ActiveSupport::Deprecation.warn "You're using the Ruby-based MySQL library that ships with Rails. This library will be REMOVED FROM RAILS 2.2. Please switch to the offical mysql gem: `gem install mysql`", caller
"WARNING: You're using the Ruby-based MySQL library that ships with Rails. This library is not suited for production. " +
"Please install the C-based MySQL library instead (gem install mysql)."
) if ActiveRecord::Base.logger
require 'active_record/vendor/mysql' require 'active_record/vendor/mysql'
rescue LoadError rescue LoadError
@ -113,7 +110,8 @@ module ActiveRecord
end end
def extract_limit(sql_type) def extract_limit(sql_type)
if sql_type =~ /blob|text/i case sql_type
when /blob|text/i
case sql_type case sql_type
when /tiny/i when /tiny/i
255 255
@ -124,6 +122,11 @@ module ActiveRecord
else else
super # we could return 65535 here, but we leave it undecorated by default super # we could return 65535 here, but we leave it undecorated by default
end end
when /^bigint/i; 8
when /^int/i; 4
when /^mediumint/i; 3
when /^smallint/i; 2
when /^tinyint/i; 1
else else
super super
end end
@ -193,10 +196,10 @@ module ActiveRecord
def native_database_types #:nodoc: def native_database_types #:nodoc:
{ {
:primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY", :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY".freeze,
:string => { :name => "varchar", :limit => 255 }, :string => { :name => "varchar", :limit => 255 },
:text => { :name => "text" }, :text => { :name => "text" },
:integer => { :name => "int"}, :integer => { :name => "int", :limit => 4 },
:float => { :name => "float" }, :float => { :name => "float" },
:decimal => { :name => "decimal" }, :decimal => { :name => "decimal" },
:datetime => { :name => "datetime" }, :datetime => { :name => "datetime" },
@ -336,10 +339,11 @@ module ActiveRecord
def add_limit_offset!(sql, options) #:nodoc: def add_limit_offset!(sql, options) #:nodoc:
if limit = options[:limit] if limit = options[:limit]
limit = sanitize_limit(limit)
unless offset = options[:offset] unless offset = options[:offset]
sql << " LIMIT #{limit}" sql << " LIMIT #{limit}"
else else
sql << " LIMIT #{offset}, #{limit}" sql << " LIMIT #{offset.to_i}, #{limit}"
end end
end end
end end
@ -439,18 +443,29 @@ module ActiveRecord
end end
def change_column_default(table_name, column_name, default) #:nodoc: def change_column_default(table_name, column_name, default) #:nodoc:
current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"] column = column_for(table_name, column_name)
change_column table_name, column_name, column.sql_type, :default => default
end
execute("ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{current_type} DEFAULT #{quote(default)}") def change_column_null(table_name, column_name, null, default = nil)
column = column_for(table_name, column_name)
unless null || default.nil?
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
end
change_column table_name, column_name, column.sql_type, :null => null
end end
def change_column(table_name, column_name, type, options = {}) #:nodoc: def change_column(table_name, column_name, type, options = {}) #:nodoc:
column = column_for(table_name, column_name)
unless options_include_default?(options) unless options_include_default?(options)
if column = columns(table_name).find { |c| c.name == column_name.to_s } options[:default] = column.default
options[:default] = column.default end
else
raise "No such column: #{table_name}.#{column_name}" unless options.has_key?(:null)
end options[:null] = column.null
end end
change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
@ -459,8 +474,17 @@ module ActiveRecord
end end
def rename_column(table_name, column_name, new_column_name) #:nodoc: def rename_column(table_name, column_name, new_column_name) #:nodoc:
options = {}
if column = columns(table_name).find { |c| c.name == column_name.to_s }
options[:default] = column.default
options[:null] = column.null
else
raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
end
current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"] current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
execute "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}" rename_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
add_column_options!(rename_column_sql, options)
execute(rename_column_sql)
end end
# Maps logical Rails types to MySQL-specific data types. # Maps logical Rails types to MySQL-specific data types.
@ -468,14 +492,12 @@ module ActiveRecord
return super unless type.to_s == 'integer' return super unless type.to_s == 'integer'
case limit case limit
when 0..3 when 1; 'tinyint'
"smallint(#{limit})" when 2; 'smallint'
when 4..8 when 3; 'mediumint'
"int(#{limit})" when nil, 4, 11; 'int(11)' # compatibility with MySQL default
when 9..20 when 5..8; 'bigint'
"bigint(#{limit})" else raise(ActiveRecordError, "No integer type has byte size #{limit}")
else
'int(11)'
end end
end end
@ -525,6 +547,13 @@ module ActiveRecord
def version def version
@version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
end end
def column_for(table_name, column_name)
unless column = columns(table_name).find { |c| c.name == column_name.to_s }
raise "No such column: #{table_name}.#{column_name}"
end
column
end
end end
end end
end end

View file

@ -23,8 +23,8 @@ module ActiveRecord
config = config.symbolize_keys config = config.symbolize_keys
host = config[:host] host = config[:host]
port = config[:port] || 5432 port = config[:port] || 5432
username = config[:username].to_s username = config[:username].to_s if config[:username]
password = config[:password].to_s password = config[:password].to_s if config[:password]
if config.has_key?(:database) if config.has_key?(:database)
database = config[:database] database = config[:database]
@ -47,6 +47,14 @@ module ActiveRecord
end end
private private
def extract_limit(sql_type)
case sql_type
when /^bigint/i; 8
when /^smallint/i; 2
else super
end
end
# Extracts the scale from PostgreSQL-specific data types. # Extracts the scale from PostgreSQL-specific data types.
def extract_scale(sql_type) def extract_scale(sql_type)
# Money type has a fixed scale of 2. # Money type has a fixed scale of 2.
@ -174,8 +182,8 @@ module ActiveRecord
def self.extract_value_from_default(default) def self.extract_value_from_default(default)
case default case default
# Numeric types # Numeric types
when /\A-?\d+(\.\d*)?\z/ when /\A\(?(-?\d+(\.\d*)?\)?)\z/
default $1
# Character types # Character types
when /\A'(.*)'::(?:character varying|bpchar|text)\z/m when /\A'(.*)'::(?:character varying|bpchar|text)\z/m
$1 $1
@ -319,6 +327,10 @@ module ActiveRecord
has_support has_support
end end
def supports_insert_with_returning?
postgresql_version >= 80200
end
# Returns the configured supported identifier length supported by PostgreSQL, # Returns the configured supported identifier length supported by PostgreSQL,
# or report the default of 63 on PostgreSQL 7.x. # or report the default of 63 on PostgreSQL 7.x.
def table_alias_length def table_alias_length
@ -360,7 +372,7 @@ module ActiveRecord
# There are some incorrectly compiled postgres drivers out there # There are some incorrectly compiled postgres drivers out there
# that don't define PGconn.escape. # that don't define PGconn.escape.
self.class.instance_eval do self.class.instance_eval do
undef_method(:quote_string) remove_method(:quote_string)
end end
end end
quote_string(s) quote_string(s)
@ -411,8 +423,34 @@ module ActiveRecord
# Executes an INSERT query and returns the new record's ID # Executes an INSERT query and returns the new record's ID
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
# Extract the table from the insert sql. Yuck.
table = sql.split(" ", 4)[2].gsub('"', '') table = sql.split(" ", 4)[2].gsub('"', '')
super || pk && last_insert_id(table, sequence_name || default_sequence_name(table, pk))
# Try an insert with 'returning id' if available (PG >= 8.2)
if supports_insert_with_returning?
pk, sequence_name = *pk_and_sequence_for(table) unless pk
if pk
id = select_value("#{sql} RETURNING #{quote_column_name(pk)}")
clear_query_cache
return id
end
end
# Otherwise, insert then grab last_insert_id.
if insert_id = super
insert_id
else
# If neither pk nor sequence name is given, look them up.
unless pk || sequence_name
pk, sequence_name = *pk_and_sequence_for(table)
end
# If a pk is given, fallback to default sequence name.
# Don't fetch last insert id for a table without a pk.
if pk && sequence_name ||= default_sequence_name(table, pk)
last_insert_id(table, sequence_name)
end
end
end end
# create a 2D array representing the result set # create a 2D array representing the result set
@ -492,13 +530,13 @@ module ActiveRecord
option_string = options.symbolize_keys.sum do |key, value| option_string = options.symbolize_keys.sum do |key, value|
case key case key
when :owner when :owner
" OWNER = '#{value}'" " OWNER = \"#{value}\""
when :template when :template
" TEMPLATE = #{value}" " TEMPLATE = \"#{value}\""
when :encoding when :encoding
" ENCODING = '#{value}'" " ENCODING = '#{value}'"
when :tablespace when :tablespace
" TABLESPACE = #{value}" " TABLESPACE = \"#{value}\""
when :connection_limit when :connection_limit
" CONNECTION LIMIT = #{value}" " CONNECTION LIMIT = #{value}"
else else
@ -506,7 +544,7 @@ module ActiveRecord
end end
end end
execute "CREATE DATABASE #{name}#{option_string}" execute "CREATE DATABASE #{quote_table_name(name)}#{option_string}"
end end
# Drops a PostgreSQL database # Drops a PostgreSQL database
@ -514,7 +552,15 @@ module ActiveRecord
# Example: # Example:
# drop_database 'matt_development' # drop_database 'matt_development'
def drop_database(name) #:nodoc: def drop_database(name) #:nodoc:
execute "DROP DATABASE IF EXISTS #{name}" if postgresql_version >= 80200
execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
else
begin
execute "DROP DATABASE #{quote_table_name(name)}"
rescue ActiveRecord::StatementInvalid
@logger.warn "#{name} database doesn't exist." if @logger
end
end
end end
@ -676,7 +722,7 @@ module ActiveRecord
# Renames a table. # Renames a table.
def rename_table(name, new_name) def rename_table(name, new_name)
execute "ALTER TABLE #{name} RENAME TO #{new_name}" execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}"
end end
# Adds a new column to the named table. # Adds a new column to the named table.
@ -698,7 +744,8 @@ module ActiveRecord
begin begin
execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
rescue ActiveRecord::StatementInvalid rescue ActiveRecord::StatementInvalid => e
raise e if postgresql_version > 80000
# This is PostgreSQL 7.x, so we have to use a more arcane way of doing it. # This is PostgreSQL 7.x, so we have to use a more arcane way of doing it.
begin begin
begin_db_transaction begin_db_transaction
@ -743,15 +790,14 @@ module ActiveRecord
def type_to_sql(type, limit = nil, precision = nil, scale = nil) def type_to_sql(type, limit = nil, precision = nil, scale = nil)
return super unless type.to_s == 'integer' return super unless type.to_s == 'integer'
if limit.nil? || limit == 4 case limit
'integer' when 1..2; 'smallint'
elsif limit < 4 when 3..4, nil; 'integer'
'smallint' when 5..8; 'bigint'
else else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
'bigint'
end end
end end
# Returns a SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause. # Returns a SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
# #
# PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and # PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and

View file

@ -238,6 +238,15 @@ module ActiveRecord
end end
end end
def change_column_null(table_name, column_name, null, default = nil)
unless null || default.nil?
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
end
alter_table(table_name) do |definition|
definition[column_name].null = null
end
end
def change_column(table_name, column_name, type, options = {}) #:nodoc: def change_column(table_name, column_name, type, options = {}) #:nodoc:
alter_table(table_name) do |definition| alter_table(table_name) do |definition|
include_default = options_include_default?(options) include_default = options_include_default?(options)
@ -251,6 +260,9 @@ module ActiveRecord
end end
def rename_column(table_name, column_name, new_column_name) #:nodoc: def rename_column(table_name, column_name, new_column_name) #:nodoc:
unless columns(table_name).detect{|c| c.name == column_name.to_s }
raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}"
end
alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s}) alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s})
end end

View file

@ -123,7 +123,10 @@ module ActiveRecord
attr = attr.to_s attr = attr.to_s
# The attribute already has an unsaved change. # The attribute already has an unsaved change.
unless changed_attributes.include?(attr) if changed_attributes.include?(attr)
old = changed_attributes[attr]
changed_attributes.delete(attr) unless field_changed?(attr, old, value)
else
old = clone_attribute_value(:read_attribute, attr) old = clone_attribute_value(:read_attribute, attr)
changed_attributes[attr] = old if field_changed?(attr, old, value) changed_attributes[attr] = old if field_changed?(attr, old, value)
end end
@ -134,7 +137,9 @@ module ActiveRecord
def update_with_dirty def update_with_dirty
if partial_updates? if partial_updates?
update_without_dirty(changed) # Serialized attributes should always be written in case they've been
# changed in place.
update_without_dirty(changed | self.class.serialized_attributes.keys)
else else
update_without_dirty update_without_dirty
end end
@ -142,9 +147,11 @@ module ActiveRecord
def field_changed?(attr, old, value) def field_changed?(attr, old, value)
if column = column_for_attribute(attr) if column = column_for_attribute(attr)
if column.type == :integer && column.null && old.nil? if column.type == :integer && column.null && (old.nil? || old == 0)
# For nullable integer columns, NULL gets stored in database for blank (i.e. '') values. # For nullable integer columns, NULL gets stored in database for blank (i.e. '') values.
# Hence we don't record it as a change if the value changes from nil to ''. # Hence we don't record it as a change if the value changes from nil to ''.
# If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll
# be typecast back to 0 (''.to_i => 0)
value = nil if value.blank? value = nil if value.blank?
else else
value = column.type_cast(value) value = column.type_cast(value)

View file

@ -68,6 +68,7 @@ module ActiveRecord
def update_with_lock(attribute_names = @attributes.keys) #:nodoc: def update_with_lock(attribute_names = @attributes.keys) #:nodoc:
return update_without_lock(attribute_names) unless locking_enabled? return update_without_lock(attribute_names) unless locking_enabled?
return 0 if attribute_names.empty?
lock_col = self.class.locking_column lock_col = self.class.locking_column
previous_value = send(lock_col).to_i previous_value = send(lock_col).to_i

View file

@ -238,6 +238,22 @@ module ActiveRecord
# lower than the current schema version: when migrating up, those # lower than the current schema version: when migrating up, those
# never-applied "interleaved" migrations will be automatically applied, and # never-applied "interleaved" migrations will be automatically applied, and
# when migrating down, never-applied "interleaved" migrations will be skipped. # when migrating down, never-applied "interleaved" migrations will be skipped.
#
# == Timestamped Migrations
#
# By default, Rails generates migrations that look like:
#
# 20080717013526_your_migration_name.rb
#
# The prefix is a generation timestamp (in UTC).
#
# If you'd prefer to use numeric prefixes, you can turn timestamped migrations
# off by setting:
#
# config.active_record.timestamped_migrations = false
#
# In environment.rb.
#
class Migration class Migration
@@verbose = true @@verbose = true
cattr_accessor :verbose cattr_accessor :verbose
@ -369,11 +385,17 @@ module ActiveRecord
Base.table_name_prefix + 'schema_migrations' + Base.table_name_suffix Base.table_name_prefix + 'schema_migrations' + Base.table_name_suffix
end end
def get_all_versions
Base.connection.select_values("SELECT version FROM #{schema_migrations_table_name}").map(&:to_i).sort
end
def current_version def current_version
version = Base.connection.select_values( sm_table = schema_migrations_table_name
"SELECT version FROM #{schema_migrations_table_name}" if Base.connection.table_exists?(sm_table)
).map(&:to_i).max rescue nil get_all_versions.max || 0
version || 0 else
0
end
end end
def proper_table_name(name) def proper_table_name(name)
@ -389,7 +411,7 @@ module ActiveRecord
end end
def current_version def current_version
self.class.current_version migrated.last || 0
end end
def current_migration def current_migration
@ -399,7 +421,10 @@ module ActiveRecord
def run def run
target = migrations.detect { |m| m.version == @target_version } target = migrations.detect { |m| m.version == @target_version }
raise UnknownMigrationVersionError.new(@target_version) if target.nil? raise UnknownMigrationVersionError.new(@target_version) if target.nil?
target.migrate(@direction) unless (up? && migrated.include?(target.version.to_i)) || (down? && !migrated.include?(target.version.to_i))
target.migrate(@direction)
record_version_state_after_migrating(target.version)
end
end end
def migrate def migrate
@ -470,17 +495,19 @@ module ActiveRecord
end end
def migrated def migrated
sm_table = self.class.schema_migrations_table_name @migrated_versions ||= self.class.get_all_versions
Base.connection.select_values("SELECT version FROM #{sm_table}").map(&:to_i).sort
end end
private private
def record_version_state_after_migrating(version) def record_version_state_after_migrating(version)
sm_table = self.class.schema_migrations_table_name sm_table = self.class.schema_migrations_table_name
@migrated_versions ||= []
if down? if down?
@migrated_versions.delete(version.to_i)
Base.connection.update("DELETE FROM #{sm_table} WHERE version = '#{version}'") Base.connection.update("DELETE FROM #{sm_table} WHERE version = '#{version}'")
else else
@migrated_versions.push(version.to_i).sort!
Base.connection.insert("INSERT INTO #{sm_table} (version) VALUES ('#{version}')") Base.connection.insert("INSERT INTO #{sm_table} (version) VALUES ('#{version}')")
end end
end end

View file

@ -82,6 +82,7 @@ module ActiveRecord
# expected_options = { :conditions => { :colored => 'red' } } # expected_options = { :conditions => { :colored => 'red' } }
# assert_equal expected_options, Shirt.colored('red').proxy_options # assert_equal expected_options, Shirt.colored('red').proxy_options
def named_scope(name, options = {}, &block) def named_scope(name, options = {}, &block)
name = name.to_sym
scopes[name] = lambda do |parent_scope, *args| scopes[name] = lambda do |parent_scope, *args|
Scope.new(parent_scope, case options Scope.new(parent_scope, case options
when Hash when Hash
@ -102,7 +103,7 @@ module ActiveRecord
attr_reader :proxy_scope, :proxy_options attr_reader :proxy_scope, :proxy_options
[].methods.each do |m| [].methods.each do |m|
unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|find|count|sum|average|maximum|minimum|paginate|first|last|empty?)/ unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|^find$|count|sum|average|maximum|minimum|paginate|first|last|empty\?|respond_to\?)/
delegate m, :to => :proxy_found delegate m, :to => :proxy_found
end end
end end
@ -139,6 +140,10 @@ module ActiveRecord
@found ? @found.empty? : count.zero? @found ? @found.empty? : count.zero?
end end
def respond_to?(method, include_private = false)
super || @proxy_scope.respond_to?(method, include_private)
end
protected protected
def proxy_found def proxy_found
@found || load_found @found || load_found

View file

@ -20,7 +20,7 @@ module ActiveRecord
# ActiveRecord::Base.observers = Cacher, GarbageCollector # ActiveRecord::Base.observers = Cacher, GarbageCollector
# #
# Note: Setting this does not instantiate the observers yet. +instantiate_observers+ is # Note: Setting this does not instantiate the observers yet. +instantiate_observers+ is
# called during startup, and before each development request. # called during startup, and before each development request.
def observers=(*observers) def observers=(*observers)
@observers = observers.flatten @observers = observers.flatten
end end
@ -130,11 +130,11 @@ module ActiveRecord
# Observers register themselves in the model class they observe, since it is the class that # Observers register themselves in the model class they observe, since it is the class that
# notifies them of events when they occur. As a side-effect, when an observer is loaded its # notifies them of events when they occur. As a side-effect, when an observer is loaded its
# corresponding model class is loaded. # corresponding model class is loaded.
# #
# Up to (and including) Rails 2.0.2 observers were instantiated between plugins and # Up to (and including) Rails 2.0.2 observers were instantiated between plugins and
# application initializers. Now observers are loaded after application initializers, # application initializers. Now observers are loaded after application initializers,
# so observed models can make use of extensions. # so observed models can make use of extensions.
# #
# If by any chance you are using observed models in the initialization you can still # If by any chance you are using observed models in the initialization you can still
# load their observers by calling <tt>ModelObserver.instance</tt> before. Observers are # load their observers by calling <tt>ModelObserver.instance</tt> before. Observers are
# singletons and that call instantiates and registers them. # singletons and that call instantiates and registers them.
@ -189,7 +189,9 @@ module ActiveRecord
def add_observer!(klass) def add_observer!(klass)
klass.add_observer(self) klass.add_observer(self)
klass.class_eval 'def after_find() end' unless klass.respond_to?(:after_find) if respond_to?(:after_find) && !klass.method_defined?(:after_find)
klass.class_eval 'def after_find() end'
end
end end
end end
end end

View file

@ -22,11 +22,22 @@ module ActiveRecord
end end
end end
def assert_queries(num = 1) def assert_sql(*patterns_to_match)
$query_count = 0 $queries_executed = []
yield yield
ensure ensure
assert_equal num, $query_count, "#{$query_count} instead of #{num} queries were executed." failed_patterns = []
patterns_to_match.each do |pattern|
failed_patterns << pattern unless $queries_executed.any?{ |sql| pattern === sql }
end
assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map(&:inspect).join(', ')} not found."
end
def assert_queries(num = 1)
$queries_executed = []
yield
ensure
assert_equal num, $queries_executed.size, "#{$queries_executed.size} instead of #{num} queries were executed."
end end
def assert_no_queries(&block) def assert_no_queries(&block)

View file

@ -480,8 +480,9 @@ module ActiveRecord
# validates_length_of :fax, :in => 7..32, :allow_nil => true # validates_length_of :fax, :in => 7..32, :allow_nil => true
# validates_length_of :phone, :in => 7..32, :allow_blank => 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 :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 %d character" # validates_length_of :fav_bra_size, :minimum => 1, :too_short => "please enter at least %d character"
# validates_length_of :smurf_leader, :is=>4, :message=>"papa is spelled with %d characters... don't play me." # validates_length_of :smurf_leader, :is => 4, :message => "papa is spelled with %d characters... don't play me."
# validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least %d words."), :tokenizer => lambda {|str| str.scan(/\w+/) }
# end # end
# #
# Configuration options: # Configuration options:
@ -492,7 +493,6 @@ module ActiveRecord
# * <tt>:in</tt> - A synonym(or alias) for <tt>:within</tt>. # * <tt>:in</tt> - A synonym(or alias) for <tt>:within</tt>.
# * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation. # * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation.
# * <tt>:allow_blank</tt> - Attribute may be blank; skip validation. # * <tt>:allow_blank</tt> - Attribute may be blank; skip validation.
#
# * <tt>:too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (maximum is %d characters)"). # * <tt>:too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (maximum is %d characters)").
# * <tt>:too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min is %d characters)"). # * <tt>:too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min is %d characters)").
# * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt> method and the attribute is the wrong size (default is: "is the wrong length (should be %d characters)"). # * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt> method and the attribute is the wrong size (default is: "is the wrong length (should be %d characters)").
@ -504,12 +504,16 @@ module ActiveRecord
# * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value. # method, proc or string should return or evaluate to a true or false value.
# * <tt>:tokenizer</tt> - Specifies how to split up the attribute string. (e.g. <tt>:tokenizer => lambda {|str| str.scan(/\w+/)}</tt> to
# count words as in above example.)
# Defaults to <tt>lambda{ |value| value.split(//) }</tt> which counts individual characters.
def validates_length_of(*attrs) def validates_length_of(*attrs)
# Merge given options with defaults. # Merge given options with defaults.
options = { options = {
:too_long => ActiveRecord::Errors.default_error_messages[:too_long], :too_long => ActiveRecord::Errors.default_error_messages[:too_long],
:too_short => ActiveRecord::Errors.default_error_messages[:too_short], :too_short => ActiveRecord::Errors.default_error_messages[:too_short],
:wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length] :wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length],
:tokenizer => lambda {|value| value.split(//)}
}.merge(DEFAULT_VALIDATION_OPTIONS) }.merge(DEFAULT_VALIDATION_OPTIONS)
options.update(attrs.extract_options!.symbolize_keys) options.update(attrs.extract_options!.symbolize_keys)
@ -536,7 +540,7 @@ module ActiveRecord
too_long = options[:too_long] % option_value.end too_long = options[:too_long] % option_value.end
validates_each(attrs, options) do |record, attr, value| validates_each(attrs, options) do |record, attr, value|
value = value.split(//) if value.kind_of?(String) value = options[:tokenizer].call(value) if value.kind_of?(String)
if value.nil? or value.size < option_value.begin if value.nil? or value.size < option_value.begin
record.errors.add(attr, too_short) record.errors.add(attr, too_short)
elsif value.size > option_value.end elsif value.size > option_value.end
@ -553,7 +557,7 @@ module ActiveRecord
message = (options[:message] || options[message_options[option]]) % option_value message = (options[:message] || options[message_options[option]]) % option_value
validates_each(attrs, options) do |record, attr, value| validates_each(attrs, options) do |record, attr, value|
value = value.split(//) if value.kind_of?(String) value = options[:tokenizer].call(value) if value.kind_of?(String)
record.errors.add(attr, message) unless !value.nil? and value.size.method(validity_checks[option])[option_value] record.errors.add(attr, message) unless !value.nil? and value.size.method(validity_checks[option])[option_value]
end end
end end
@ -614,14 +618,20 @@ module ActiveRecord
# class (which has a database table to query from). # class (which has a database table to query from).
finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? } finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? }
if value.nil? || (configuration[:case_sensitive] || !finder_class.columns_hash[attr_name.to_s].text?) is_text_column = finder_class.columns_hash[attr_name.to_s].text?
if !value.nil? && is_text_column
value = value.to_s
end
if value.nil? || (configuration[:case_sensitive] || !is_text_column)
condition_sql = "#{record.class.quoted_table_name}.#{attr_name} #{attribute_condition(value)}" condition_sql = "#{record.class.quoted_table_name}.#{attr_name} #{attribute_condition(value)}"
condition_params = [value] condition_params = [value]
else else
# sqlite has case sensitive SELECT query, while MySQL/Postgresql don't. # sqlite has case sensitive SELECT query, while MySQL/Postgresql don't.
# Hence, this is needed only for sqlite. # Hence, this is needed only for sqlite.
condition_sql = "LOWER(#{record.class.quoted_table_name}.#{attr_name}) #{attribute_condition(value)}" condition_sql = "LOWER(#{record.class.quoted_table_name}.#{attr_name}) #{attribute_condition(value)}"
condition_params = [value.downcase] condition_params = [value.chars.downcase]
end end
if scope = configuration[:scope] if scope = configuration[:scope]
@ -851,7 +861,7 @@ module ActiveRecord
raw_value = raw_value.to_i raw_value = raw_value.to_i
else else
begin begin
raw_value = Kernel.Float(raw_value.to_s) raw_value = Kernel.Float(raw_value)
rescue ArgumentError, TypeError rescue ArgumentError, TypeError
record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number]) record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number])
next next

View file

@ -2,7 +2,7 @@ module ActiveRecord
module VERSION #:nodoc: module VERSION #:nodoc:
MAJOR = 2 MAJOR = 2
MINOR = 1 MINOR = 1
TINY = 0 TINY = 1
STRING = [MAJOR, MINOR, TINY].join('.') STRING = [MAJOR, MINOR, TINY].join('.')
end end

View file

@ -13,8 +13,8 @@ class PostgresqlActiveSchemaTest < Test::Unit::TestCase
end end
def test_create_database_with_encoding def test_create_database_with_encoding
assert_equal "CREATE DATABASE matt ENCODING = 'utf8'", create_database(:matt) assert_equal %(CREATE DATABASE "matt" ENCODING = 'utf8'), create_database(:matt)
assert_equal "CREATE DATABASE aimonetti ENCODING = 'latin1'", create_database(:aimonetti, :encoding => :latin1) assert_equal %(CREATE DATABASE "aimonetti" ENCODING = 'latin1'), create_database(:aimonetti, :encoding => :latin1)
end end
private private

View file

@ -118,7 +118,7 @@ class AdapterTest < ActiveRecord::TestCase
sql_inject = "1, 7 procedure help()" sql_inject = "1, 7 procedure help()"
if current_adapter?(:MysqlAdapter) if current_adapter?(:MysqlAdapter)
assert_equal " LIMIT 1,7", @connection.add_limit_offset!("", :limit=>sql_inject) assert_equal " LIMIT 1,7", @connection.add_limit_offset!("", :limit=>sql_inject)
assert_equal " LIMIT 7, 1", @connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7) assert_equal " LIMIT 7, 1", @connection.add_limit_offset!("", :limit=> '1 ; DROP TABLE USERS', :offset=>7)
else else
assert_equal " LIMIT 1,7", @connection.add_limit_offset!("", :limit=>sql_inject) assert_equal " LIMIT 1,7", @connection.add_limit_offset!("", :limit=>sql_inject)
assert_equal " LIMIT 1,7 OFFSET 7", @connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7) assert_equal " LIMIT 1,7 OFFSET 7", @connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7)

View file

@ -409,4 +409,23 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
sponsor.sponsorable = new_member sponsor.sponsorable = new_member
assert_equal nil, sponsor.sponsorable_id assert_equal nil, sponsor.sponsorable_id
end end
def test_save_fails_for_invalid_belongs_to
assert log = AuditLog.create(:developer_id=>0,:message=>"")
log.developer = Developer.new
assert !log.developer.valid?
assert !log.valid?
assert !log.save
assert_equal "is invalid", log.errors.on("developer")
end
def test_save_succeeds_for_invalid_belongs_to_with_validate_false
assert log = AuditLog.create(:developer_id=>0,:message=>"")
log.unvalidated_developer = Developer.new
assert !log.unvalidated_developer.valid?
assert log.valid?
assert log.save
end
end end

View file

@ -9,7 +9,7 @@ require 'models/topic'
require 'models/reply' require 'models/reply'
class CascadedEagerLoadingTest < ActiveRecord::TestCase class CascadedEagerLoadingTest < ActiveRecord::TestCase
fixtures :authors, :mixins, :companies, :posts, :topics fixtures :authors, :mixins, :companies, :posts, :topics, :accounts, :comments, :categorizations
def test_eager_association_loading_with_cascaded_two_levels def test_eager_association_loading_with_cascaded_two_levels
authors = Author.find(:all, :include=>{:posts=>:comments}, :order=>"authors.id") authors = Author.find(:all, :include=>{:posts=>:comments}, :order=>"authors.id")
@ -68,6 +68,18 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
end end
end end
def test_eager_association_loading_with_has_many_sti_and_subclasses
silly = SillyReply.new(:title => "gaga", :content => "boo-boo", :parent_id => 1)
silly.parent_id = 1
assert silly.save
topics = Topic.find(:all, :include => :replies, :order => 'topics.id, replies_topics.id')
assert_no_queries do
assert_equal 2, topics[0].replies.size
assert_equal 0, topics[1].replies.size
end
end
def test_eager_association_loading_with_belongs_to_sti def test_eager_association_loading_with_belongs_to_sti
replies = Reply.find(:all, :include => :topic, :order => 'topics.id') replies = Reply.find(:all, :include => :topic, :order => 'topics.id')
assert replies.include?(topics(:second)) assert replies.include?(topics(:second))

View file

@ -0,0 +1,36 @@
require 'cases/helper'
require 'models/post'
require 'models/tagging'
module Namespaced
class Post < ActiveRecord::Base
set_table_name 'posts'
has_one :tagging, :as => :taggable, :class_name => 'Tagging'
end
end
class EagerLoadIncludeFullStiClassNamesTest < ActiveRecord::TestCase
def setup
generate_test_objects
end
def generate_test_objects
post = Namespaced::Post.create( :title => 'Great stuff', :body => 'This is not', :author_id => 1 )
tagging = Tagging.create( :taggable => post )
end
def test_class_names
old = ActiveRecord::Base.store_full_sti_class
ActiveRecord::Base.store_full_sti_class = false
post = Namespaced::Post.find_by_title( 'Great stuff', :include => :tagging )
assert_nil post.tagging
ActiveRecord::Base.store_full_sti_class = true
post = Namespaced::Post.find_by_title( 'Great stuff', :include => :tagging )
assert_equal 'Tagging', post.tagging.class.name
ensure
ActiveRecord::Base.store_full_sti_class = old
end
end

View file

@ -14,11 +14,14 @@ require 'models/job'
require 'models/subscriber' require 'models/subscriber'
require 'models/subscription' require 'models/subscription'
require 'models/book' require 'models/book'
require 'models/developer'
require 'models/project'
class EagerAssociationTest < ActiveRecord::TestCase class EagerAssociationTest < ActiveRecord::TestCase
fixtures :posts, :comments, :authors, :categories, :categories_posts, fixtures :posts, :comments, :authors, :categories, :categories_posts,
:companies, :accounts, :tags, :taggings, :people, :readers, :companies, :accounts, :tags, :taggings, :people, :readers,
:owners, :pets, :author_favorites, :jobs, :references, :subscribers, :subscriptions, :books :owners, :pets, :author_favorites, :jobs, :references, :subscribers, :subscriptions, :books,
:developers, :projects, :developers_projects
def test_loading_with_one_association def test_loading_with_one_association
posts = Post.find(:all, :include => :comments) posts = Post.find(:all, :include => :comments)
@ -35,6 +38,12 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_equal Post.find(1).last_comment, post.last_comment assert_equal Post.find(1).last_comment, post.last_comment
end end
def test_loading_with_one_association_with_non_preload
posts = Post.find(:all, :include => :last_comment, :order => 'comments.id DESC')
post = posts.find { |p| p.id == 1 }
assert_equal Post.find(1).last_comment, post.last_comment
end
def test_loading_conditions_with_or def test_loading_conditions_with_or
posts = authors(:david).posts.find(:all, :include => :comments, :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE} = 'SpecialComment'") posts = authors(:david).posts.find(:all, :include => :comments, :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE} = 'SpecialComment'")
assert_nil posts.detect { |p| p.author_id != authors(:david).id }, assert_nil posts.detect { |p| p.author_id != authors(:david).id },
@ -556,6 +565,13 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_nothing_raised { Post.find(:all, :include => 'comments') } assert_nothing_raised { Post.find(:all, :include => 'comments') }
end end
def test_eager_with_floating_point_numbers
assert_queries(2) do
# Before changes, the floating point numbers will be interpreted as table names and will cause this to run in one query
Comment.find :all, :conditions => "123.456 = 123.456", :include => :post
end
end
def test_preconfigured_includes_with_belongs_to def test_preconfigured_includes_with_belongs_to
author = posts(:welcome).author_with_posts author = posts(:welcome).author_with_posts
assert_no_queries {assert_equal 5, author.posts.size} assert_no_queries {assert_equal 5, author.posts.size}
@ -609,4 +625,12 @@ class EagerAssociationTest < ActiveRecord::TestCase
Comment.find :all, :include => :post Comment.find :all, :include => :post
end end
end end
def test_conditions_on_join_table_with_include_and_limit
assert_equal 3, Developer.find(:all, :include => 'projects', :conditions => 'developers_projects.access_level = 1', :limit => 5).size
end
def test_order_on_join_table_with_include_and_limit
assert_equal 5, Developer.find(:all, :include => 'projects', :order => 'developers_projects.joined_on DESC', :limit => 5).size
end
end end

View file

@ -70,7 +70,7 @@ end
class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects, fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects,
:parrots, :pirates, :treasures, :price_estimates :parrots, :pirates, :treasures, :price_estimates, :tags, :taggings
def test_has_and_belongs_to_many def test_has_and_belongs_to_many
david = Developer.find(1) david = Developer.find(1)
@ -299,6 +299,17 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal 3, projects(:active_record, :reload).developers.size assert_equal 3, projects(:active_record, :reload).developers.size
end end
def test_uniq_option_prevents_duplicate_push
project = projects(:active_record)
project.developers << developers(:jamis)
project.developers << developers(:david)
assert_equal 3, project.developers.size
project.developers << developers(:david)
project.developers << developers(:jamis)
assert_equal 3, project.developers.size
end
def test_deleting def test_deleting
david = Developer.find(1) david = Developer.find(1)
active_record = Project.find(1) active_record = Project.find(1)
@ -439,6 +450,13 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal developers(:david), active_record.developers_with_finder_sql.find(developers(:david).id), "Ruby find" assert_equal developers(:david), active_record.developers_with_finder_sql.find(developers(:david).id), "Ruby find"
end end
def test_find_in_association_with_custom_finder_sql_and_multiple_interpolations
# interpolate once:
assert_equal [developers(:david), developers(:jamis), developers(:poor_jamis)], projects(:active_record).developers_with_finder_sql, "first interpolation"
# interpolate again, for a different project id
assert_equal [developers(:david)], projects(:action_controller).developers_with_finder_sql, "second interpolation"
end
def test_find_in_association_with_custom_finder_sql_and_string_id def test_find_in_association_with_custom_finder_sql_and_string_id
assert_equal developers(:david), projects(:active_record).developers_with_finder_sql.find(developers(:david).id.to_s), "SQL find" assert_equal developers(:david), projects(:active_record).developers_with_finder_sql.find(developers(:david).id.to_s), "SQL find"
end end
@ -629,8 +647,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
developer.save developer.save
developer.reload developer.reload
assert_equal 2, developer.projects.length assert_equal 2, developer.projects.length
assert_equal projects(:active_record), developer.projects[0] assert_equal [projects(:active_record), projects(:action_controller)].map(&:id).sort, developer.project_ids.sort
assert_equal projects(:action_controller), developer.projects[1]
end end
def test_assign_ids_ignoring_blanks def test_assign_ids_ignoring_blanks
@ -639,8 +656,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
developer.save developer.save
developer.reload developer.reload
assert_equal 2, developer.projects.length assert_equal 2, developer.projects.length
assert_equal projects(:active_record), developer.projects[0] assert_equal [projects(:active_record), projects(:action_controller)].map(&:id).sort, developer.project_ids.sort
assert_equal projects(:action_controller), developer.projects[1]
end end
def test_select_limited_ids_list def test_select_limited_ids_list
@ -681,4 +697,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal developer, project.developers.find(:first) assert_equal developer, project.developers.find(:first)
assert_equal project, developer.projects.find(:first) assert_equal project, developer.projects.find(:first)
end end
def test_dynamic_find_should_respect_association_include
# SQL error in sort clause if :include is not included
# due to Unknown column 'authors.id'
assert Category.find(1).posts_with_authors_sorted_by_author_id.find_by_title('Welcome to the weblog')
end
end end

View file

@ -14,7 +14,7 @@ require 'models/reader'
class HasManyAssociationsTest < ActiveRecord::TestCase class HasManyAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :categories, :companies, :developers, :projects, fixtures :accounts, :categories, :companies, :developers, :projects,
:developers_projects, :topics, :authors, :comments, :author_addresses, :developers_projects, :topics, :authors, :comments, :author_addresses,
:people, :posts :people, :posts, :readers
def setup def setup
Client.destroyed_client_ids.clear Client.destroyed_client_ids.clear
@ -37,15 +37,21 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end end
def test_counting_with_single_conditions def test_counting_with_single_conditions
assert_equal 2, Firm.find(:first).plain_clients.count(:conditions => '1=1') assert_equal 1, Firm.find(:first).plain_clients.count(:conditions => ['name=?', "Microsoft"])
end end
def test_counting_with_single_hash def test_counting_with_single_hash
assert_equal 2, Firm.find(:first).plain_clients.count(:conditions => '1=1') assert_equal 1, Firm.find(:first).plain_clients.count(:conditions => {:name => "Microsoft"})
end end
def test_counting_with_column_name_and_hash def test_counting_with_column_name_and_hash
assert_equal 2, Firm.find(:first).plain_clients.count(:all, :conditions => '1=1') assert_equal 2, Firm.find(:first).plain_clients.count(:name)
end
def test_counting_with_association_limit
firm = companies(:first_firm)
assert_equal firm.limited_clients.length, firm.limited_clients.size
assert_equal firm.limited_clients.length, firm.limited_clients.count
end end
def test_finding def test_finding
@ -342,6 +348,34 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert new_firm.new_record? assert new_firm.new_record?
end end
def test_invalid_adding_with_validate_false
firm = Firm.find(:first)
client = Client.new
firm.unvalidated_clients_of_firm << client
assert firm.valid?
assert !client.valid?
assert firm.save
assert client.new_record?
end
def test_valid_adding_with_validate_false
no_of_clients = Client.count
firm = Firm.find(:first)
client = Client.new("name" => "Apple")
assert firm.valid?
assert client.valid?
assert client.new_record?
firm.unvalidated_clients_of_firm << client
assert firm.save
assert !client.new_record?
assert_equal no_of_clients+1, Client.count
end
def test_build def test_build
company = companies(:first_firm) company = companies(:first_firm)
new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") } new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") }
@ -356,6 +390,25 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 2, company.clients_of_firm(true).size assert_equal 2, company.clients_of_firm(true).size
end end
def test_collection_size_after_building
company = companies(:first_firm) # company already has one client
company.clients_of_firm.build("name" => "Another Client")
company.clients_of_firm.build("name" => "Yet Another Client")
assert_equal 3, company.clients_of_firm.size
end
def test_collection_size_twice_for_regressions
post = posts(:thinking)
assert_equal 0, post.readers.size
# This test needs a post that has no readers, we assert it to ensure it holds,
# but need to reload the post because the very call to #size hides the bug.
post.reload
post.readers.build
size1 = post.readers.size
size2 = post.readers.size
assert_equal size1, size2
end
def test_build_many def test_build_many
company = companies(:first_firm) company = companies(:first_firm)
new_clients = assert_no_queries { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) } new_clients = assert_no_queries { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) }
@ -386,6 +439,37 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 2, first_topic.replies.to_ary.size assert_equal 2, first_topic.replies.to_ary.size
end end
def test_build_via_block
company = companies(:first_firm)
new_client = assert_no_queries { company.clients_of_firm.build {|client| client.name = "Another Client" } }
assert !company.clients_of_firm.loaded?
assert_equal "Another Client", new_client.name
assert new_client.new_record?
assert_equal new_client, company.clients_of_firm.last
company.name += '-changed'
assert_queries(2) { assert company.save }
assert !new_client.new_record?
assert_equal 2, company.clients_of_firm(true).size
end
def test_build_many_via_block
company = companies(:first_firm)
new_clients = assert_no_queries do
company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) do |client|
client.name = "changed"
end
end
assert_equal 2, new_clients.size
assert_equal "changed", new_clients.first.name
assert_equal "changed", new_clients.last.name
company.name += '-changed'
assert_queries(3) { assert company.save }
assert_equal 3, company.clients_of_firm(true).size
end
def test_create_without_loading_association def test_create_without_loading_association
first_firm = companies(:first_firm) first_firm = companies(:first_firm)
Firm.column_names Firm.column_names
@ -929,4 +1013,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert firm.clients.loaded? assert firm.clients.loaded?
end end
def test_joins_with_namespaced_model_should_use_correct_type
old = ActiveRecord::Base.store_full_sti_class
ActiveRecord::Base.store_full_sti_class = true
firm = Namespaced::Firm.create({ :name => 'Some Company' })
firm.clients.create({ :name => 'Some Client' })
stats = Namespaced::Firm.find(firm.id, {
:select => "#{Namespaced::Firm.table_name}.id, COUNT(#{Namespaced::Client.table_name}.id) AS num_clients",
:joins => :clients,
:group => "#{Namespaced::Firm.table_name}.id"
})
assert_equal 1, stats.num_clients.to_i
ensure
ActiveRecord::Base.store_full_sti_class = old
end
end end

View file

@ -187,4 +187,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
post.people_with_callbacks.clear post.people_with_callbacks.clear
assert_equal (%w(Michael David Julian Roger) * 2).sort, log.last(8).collect(&:last).sort assert_equal (%w(Michael David Julian Roger) * 2).sort, log.last(8).collect(&:last).sort
end end
def test_dynamic_find_should_respect_association_include
# SQL error in sort clause if :include is not included
# due to Unknown column 'comments.id'
assert Person.find(1).posts_with_comments_sorted_by_comment_id.find_by_title('Welcome to the weblog')
end
def test_count_with_include_should_alias_join_table
assert_equal 2, people(:michael).posts.count(:include => :readers)
end
end end

View file

@ -72,6 +72,16 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_raises(ActiveRecord::RecordNotFound) { Account.find(old_account_id) } assert_raises(ActiveRecord::RecordNotFound) { Account.find(old_account_id) }
end end
def test_natural_assignment_to_already_associated_record
company = companies(:first_firm)
account = accounts(:signals37)
assert_equal company.account, account
company.account = account
company.reload
account.reload
assert_equal company.account, account
end
def test_assignment_without_replacement def test_assignment_without_replacement
apple = Firm.create("name" => "Apple") apple = Firm.create("name" => "Apple")
citibank = Account.create("credit_limit" => 10) citibank = Account.create("credit_limit" => 10)
@ -275,6 +285,18 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_equal "is invalid", firm.errors.on("account") assert_equal "is invalid", firm.errors.on("account")
end end
def test_save_succeeds_for_invalid_has_one_with_validate_false
firm = Firm.find(:first)
assert firm.valid?
firm.unvalidated_account = Account.new
assert !firm.unvalidated_account.valid?
assert firm.valid?
assert firm.save
end
def test_assignment_before_either_saved def test_assignment_before_either_saved
firm = Firm.new("name" => "GlobalMegaCorp") firm = Firm.new("name" => "GlobalMegaCorp")
firm.account = a = Account.new("credit_limit" => 1000) firm.account = a = Account.new("credit_limit" => 1000)

View file

@ -44,19 +44,23 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
def test_has_one_through_polymorphic def test_has_one_through_polymorphic
assert_equal clubs(:moustache_club), @member.sponsor_club assert_equal clubs(:moustache_club), @member.sponsor_club
end end
def has_one_through_to_has_many def has_one_through_to_has_many
assert_equal 2, @member.fellow_members.size assert_equal 2, @member.fellow_members.size
end end
def test_has_one_through_eager_loading def test_has_one_through_eager_loading
members = Member.find(:all, :include => :club, :conditions => ["name = ?", "Groucho Marx"]) members = assert_queries(3) do #base table, through table, clubs table
Member.find(:all, :include => :club, :conditions => ["name = ?", "Groucho Marx"])
end
assert_equal 1, members.size assert_equal 1, members.size
assert_not_nil assert_no_queries {members[0].club} assert_not_nil assert_no_queries {members[0].club}
end end
def test_has_one_through_eager_loading_through_polymorphic def test_has_one_through_eager_loading_through_polymorphic
members = Member.find(:all, :include => :sponsor_club, :conditions => ["name = ?", "Groucho Marx"]) members = assert_queries(3) do #base table, through table, clubs table
Member.find(:all, :include => :sponsor_club, :conditions => ["name = ?", "Groucho Marx"])
end
assert_equal 1, members.size assert_equal 1, members.size
assert_not_nil assert_no_queries {members[0].sponsor_club} assert_not_nil assert_no_queries {members[0].sponsor_club}
end end
@ -71,4 +75,39 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
assert_not_nil assert_no_queries {clubs[0].sponsored_member} assert_not_nil assert_no_queries {clubs[0].sponsored_member}
end end
def test_has_one_through_nonpreload_eagerloading
members = assert_queries(1) do
Member.find(:all, :include => :club, :conditions => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name') #force fallback
end
assert_equal 1, members.size
assert_not_nil assert_no_queries {members[0].club}
end
def test_has_one_through_nonpreload_eager_loading_through_polymorphic
members = assert_queries(1) do
Member.find(:all, :include => :sponsor_club, :conditions => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name') #force fallback
end
assert_equal 1, members.size
assert_not_nil assert_no_queries {members[0].sponsor_club}
end
def test_has_one_through_nonpreload_eager_loading_through_polymorphic_with_more_than_one_through_record
Sponsor.new(:sponsor_club => clubs(:crazy_club), :sponsorable => members(:groucho)).save!
members = assert_queries(1) do
Member.find(:all, :include => :sponsor_club, :conditions => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name DESC') #force fallback
end
assert_equal 1, members.size
assert_not_nil assert_no_queries { members[0].sponsor_club }
assert_equal clubs(:crazy_club), members[0].sponsor_club
end
def test_uninitialized_has_one_through_should_return_nil_for_unsaved_record
assert_nil Member.new.club
end
def test_assigning_association_correctly_assigns_target
new_member = Member.create(:name => "Chris")
new_member.club = new_club = Club.create(:name => "LRUG")
assert_equal new_club, new_member.club.target
end
end end

View file

@ -694,6 +694,13 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
assert ! david.categories.include?(category) assert ! david.categories.include?(category)
end end
def test_has_many_through_goes_through_all_sti_classes
sub_sti_post = SubStiPost.create!(:title => 'test', :body => 'test', :author_id => 1)
new_comment = sub_sti_post.comments.create(:body => 'test')
assert_equal [9, 10, new_comment.id], authors(:david).sti_post_comments.map(&:id).sort
end
private private
# create dynamic Post models to allow different dependency options # create dynamic Post models to allow different dependency options
def find_post_with_dependency(post_id, association, association_name, dependency) def find_post_with_dependency(post_id, association, association_name, dependency)

View file

@ -27,7 +27,7 @@ require 'models/sponsor'
class AssociationsTest < ActiveRecord::TestCase class AssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :developers, :projects, :developers_projects, fixtures :accounts, :companies, :developers, :projects, :developers_projects,
:computers :computers, :people, :readers
def test_include_with_order_works def test_include_with_order_works
assert_nothing_raised {Account.find(:first, :order => 'id', :include => :firm)} assert_nothing_raised {Account.find(:first, :order => 'id', :include => :firm)}
@ -45,7 +45,7 @@ class AssociationsTest < ActiveRecord::TestCase
assert_equal [], person.readers.find(:all) assert_equal [], person.readers.find(:all)
person.save! person.save!
reader = Reader.create! :person => person, :post => Post.new(:title => "foo", :body => "bar") reader = Reader.create! :person => person, :post => Post.new(:title => "foo", :body => "bar")
assert_equal [reader], person.readers.find(:all) assert person.readers.find(reader.id)
end end
def test_force_reload def test_force_reload

View file

@ -137,7 +137,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end end
end end
end end
def test_time_attributes_are_retrieved_in_current_time_zone def test_time_attributes_are_retrieved_in_current_time_zone
in_time_zone "Pacific Time (US & Canada)" do in_time_zone "Pacific Time (US & Canada)" do
utc_time = Time.utc(2008, 1, 1) utc_time = Time.utc(2008, 1, 1)
@ -145,7 +145,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
record[:written_on] = utc_time record[:written_on] = utc_time
assert_equal utc_time, record.written_on # record.written on is equal to (i.e., simultaneous with) utc_time assert_equal utc_time, record.written_on # record.written on is equal to (i.e., simultaneous with) utc_time
assert_kind_of ActiveSupport::TimeWithZone, record.written_on # but is a TimeWithZone assert_kind_of ActiveSupport::TimeWithZone, record.written_on # but is a TimeWithZone
assert_equal TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone # and is in the current Time.zone assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone # and is in the current Time.zone
assert_equal Time.utc(2007, 12, 31, 16), record.written_on.time # and represents time values adjusted accordingly assert_equal Time.utc(2007, 12, 31, 16), record.written_on.time # and represents time values adjusted accordingly
end end
end end
@ -156,7 +156,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
record = @target.new record = @target.new
record.written_on = utc_time record.written_on = utc_time
assert_equal utc_time, record.written_on assert_equal utc_time, record.written_on
assert_equal TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone
assert_equal Time.utc(2007, 12, 31, 16), record.written_on.time assert_equal Time.utc(2007, 12, 31, 16), record.written_on.time
end end
end end
@ -168,7 +168,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
record = @target.new record = @target.new
record.written_on = cst_time record.written_on = cst_time
assert_equal utc_time, record.written_on assert_equal utc_time, record.written_on
assert_equal TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone
assert_equal Time.utc(2007, 12, 31, 16), record.written_on.time assert_equal Time.utc(2007, 12, 31, 16), record.written_on.time
end end
end end
@ -181,12 +181,12 @@ class AttributeMethodsTest < ActiveRecord::TestCase
record = @target.new record = @target.new
record.written_on = time_string record.written_on = time_string
assert_equal Time.zone.parse(time_string), record.written_on assert_equal Time.zone.parse(time_string), record.written_on
assert_equal TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone
assert_equal Time.utc(2007, 12, 31, 16), record.written_on.time assert_equal Time.utc(2007, 12, 31, 16), record.written_on.time
end end
end end
end end
def test_setting_time_zone_aware_attribute_to_blank_string_returns_nil def test_setting_time_zone_aware_attribute_to_blank_string_returns_nil
in_time_zone "Pacific Time (US & Canada)" do in_time_zone "Pacific Time (US & Canada)" do
record = @target.new record = @target.new
@ -202,7 +202,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
record = @target.new record = @target.new
record.written_on = time_string record.written_on = time_string
assert_equal Time.zone.parse(time_string), record.written_on assert_equal Time.zone.parse(time_string), record.written_on
assert_equal TimeZone[timezone_offset], record.written_on.time_zone assert_equal ActiveSupport::TimeZone[timezone_offset], record.written_on.time_zone
assert_equal Time.utc(2008, 1, 1), record.written_on.time assert_equal Time.utc(2008, 1, 1), record.written_on.time
end end
end end
@ -214,7 +214,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
record = @target.new record = @target.new
record.written_on = utc_time.in_time_zone record.written_on = utc_time.in_time_zone
assert_equal utc_time, record.written_on assert_equal utc_time, record.written_on
assert_equal TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone
assert_equal Time.utc(2007, 12, 31, 16), record.written_on.time assert_equal Time.utc(2007, 12, 31, 16), record.written_on.time
end end
end end
@ -223,12 +223,12 @@ class AttributeMethodsTest < ActiveRecord::TestCase
def time_related_columns_on_topic def time_related_columns_on_topic
Topic.columns.select{|c| [:time, :date, :datetime, :timestamp].include?(c.type)}.map(&:name) Topic.columns.select{|c| [:time, :date, :datetime, :timestamp].include?(c.type)}.map(&:name)
end end
def in_time_zone(zone) def in_time_zone(zone)
old_zone = Time.zone old_zone = Time.zone
old_tz = ActiveRecord::Base.time_zone_aware_attributes old_tz = ActiveRecord::Base.time_zone_aware_attributes
Time.zone = zone ? TimeZone[zone] : nil Time.zone = zone ? ActiveSupport::TimeZone[zone] : nil
ActiveRecord::Base.time_zone_aware_attributes = !zone.nil? ActiveRecord::Base.time_zone_aware_attributes = !zone.nil?
yield yield
ensure ensure

Some files were not shown because too many files have changed in this diff Show more