Instiki 0.17.2: Security Release

This release upgrades Instiki to Rails 2.3.4, which
patches two security holes in Rails. See

  http://weblog.rubyonrails.org/2009/9/4/ruby-on-rails-2-3-4

There are also some new features, and the usual boatload
of bugfixes. See the CHANGELOG for details.
This commit is contained in:
Jacques Distler 2009-09-05 02:01:46 -05:00
parent 34c4306867
commit 4bdf703ab2
211 changed files with 3959 additions and 1325 deletions

View file

@ -1,3 +1,34 @@
* 0.17.2
Security: Updated to Rails 2.3.4
* Fixes Timing Weakness in Rails MessageVerifier and the Cookie Store
http://weblog.rubyonrails.org/2009/9/4/timing-weakness-in-ruby-on-rails
* Fixes XSS Vulnerability in Rails
http://weblog.rubyonrails.org/2009/9/4/xss-vulnerability-in-ruby-on-rails
New Features:
* Syntax colouring (`ruby` and `html`) for code blocks.
* Updated for itex2MML 1.3.10 (supports \rlap{} and \underline{}). You should upgrade that, too.
* Add a "Create New Page" Link to the Search Page. (Based on an idea by nowa)
* Updated to Rails 2.3.4
Bugs Fixed:
* Wikilinks to published webs should be to the published action. This didn't work
right for inter-web links. (Reported by Mike Shulman)
* Use .size, rather than .length for ActiveRecord associations. A huge memory saving
in building the recently_revised page.
* Refactor the upgrade_instiki rake task, to make it database-agnostic. (Many thanks to James Herdman)
* Web#files_path and Web#blatex_pngs_path now return Pathname objects. (Thanks, again, to James Herdman)
* Workaround for Mozilla Bug 449396. (Reported by Andrew Stacey)
* Correctly Set noindex,nofollow On /diff Pages.
* Page-renaming javascript deals correctly with page names containing ampersands, slashes, and other garbage.
* List of Wanted Pages should not include redirected pages.
* The Regexp, used in Maruku to detect "email" headers (used, e.g., for S5 slideshow metadata) could, for some inputs, interact badly with Instiki's Chunk Handler. Fixed.
* Ensure "rollback" locks page for editing.
* Generate relative URLs, when possible. (Patch by Dennis Knauf)
* Expire revisions of an edited page. Use a `before_save` hook to deal with the situation where a page's name has been changed.
------------------------------------------------------------------------------
* 0.17 * 0.17
New features: New features:

View file

@ -258,7 +258,7 @@ module Instiki
module VERSION #:nodoc: module VERSION #:nodoc:
MAJOR = 0 MAJOR = 0
MINOR = 17 MINOR = 17
TINY = 0 TINY = 2
SUFFIX = '(MML+)' SUFFIX = '(MML+)'
PRERELEASE = false PRERELEASE = false
if PRERELEASE if PRERELEASE

View file

@ -1,3 +1,7 @@
*2.3.4 (September 4, 2009)*
* Minor bug fixes.
*2.3.3 (July 12, 2009)* *2.3.3 (July 12, 2009)*
* No changes, just a version bump. * No changes, just a version bump.

View file

@ -54,7 +54,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.3.3' + PKG_BUILD) s.add_dependency('actionpack', '= 2.3.4' + PKG_BUILD)
s.has_rdoc = true s.has_rdoc = true
s.requirements << 'none' s.requirements << 'none'

View file

@ -543,6 +543,7 @@ module ActionMailer #:nodoc:
@headers ||= {} @headers ||= {}
@body ||= {} @body ||= {}
@mime_version = @@default_mime_version.dup if @@default_mime_version @mime_version = @@default_mime_version.dup if @@default_mime_version
@sent_on ||= Time.now
end end
def render_message(method_name, body) def render_message(method_name, body)

View file

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

View file

@ -18,7 +18,6 @@ class TestMailer < ActionMailer::Base
@recipients = recipient @recipients = recipient
@subject = "[Signed up] Welcome #{recipient}" @subject = "[Signed up] Welcome #{recipient}"
@from = "system@loudthinking.com" @from = "system@loudthinking.com"
@sent_on = Time.local(2004, 12, 12)
@body["recipient"] = recipient @body["recipient"] = recipient
end end
@ -356,12 +355,14 @@ class ActionMailerTest < Test::Unit::TestCase
end end
def test_signed_up def test_signed_up
Time.stubs(:now => Time.now)
expected = new_mail expected = new_mail
expected.to = @recipient expected.to = @recipient
expected.subject = "[Signed up] Welcome #{@recipient}" expected.subject = "[Signed up] Welcome #{@recipient}"
expected.body = "Hello there, \n\nMr. #{@recipient}" expected.body = "Hello there, \n\nMr. #{@recipient}"
expected.from = "system@loudthinking.com" expected.from = "system@loudthinking.com"
expected.date = Time.local(2004, 12, 12) expected.date = Time.now
created = nil created = nil
assert_nothing_raised { created = TestMailer.create_signed_up(@recipient) } assert_nothing_raised { created = TestMailer.create_signed_up(@recipient) }

View file

@ -1,3 +1,12 @@
*2.3.4 (September 4, 2009)*
* Sanitize multibyte strings before escaping them with escape_once. CVE-2009-3009
* Introduce grouped_collection_select helper. #1249 [Dan Codeape, Erik Ostrom]
* Ruby 1.9: fix Content-Length for multibyte send_data streaming. #2661 [Sava Chankov]
*2.3.3 (July 12, 2009)* *2.3.3 (July 12, 2009)*
* Fixed that TestResponse.cookies was returning cookies unescaped #1867 [Doug McInnes] * Fixed that TestResponse.cookies was returning cookies unescaped #1867 [Doug McInnes]
@ -7,6 +16,8 @@
* Fixed that redirection would just log the options, not the final url (which lead to "Redirected to #<Post:0x23150b8>") [DHH] * Fixed that redirection would just log the options, not the final url (which lead to "Redirected to #<Post:0x23150b8>") [DHH]
* Don't check authenticity tokens for any AJAX requests [Ross Kaffenberger/Bryan Helmkamp]
* Added ability to pass in :public => true to fresh_when, stale?, and expires_in to make the request proxy cachable #2095 [Gregg Pollack] * Added ability to pass in :public => true to fresh_when, stale?, and expires_in to make the request proxy cachable #2095 [Gregg Pollack]
* Fixed that passing a custom form builder would be forwarded to nested fields_for calls #2023 [Eloy Duran/Nate Wiger] * Fixed that passing a custom form builder would be forwarded to nested fields_for calls #2023 [Eloy Duran/Nate Wiger]

View file

@ -29,7 +29,7 @@ Rake::TestTask.new(:test_action_pack) do |t|
# make sure we include the tests in alphabetical order as on some systems # make sure we include the tests in alphabetical order as on some systems
# this will not happen automatically and the tests (as a whole) will error # this will not happen automatically and the tests (as a whole) will error
t.test_files = Dir.glob( "test/[cft]*/**/*_test.rb" ).sort t.test_files = Dir.glob( "test/[cftv]*/**/*_test.rb" ).sort
t.verbose = true t.verbose = true
#t.warning = true #t.warning = true
@ -79,7 +79,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.3.3' + PKG_BUILD) s.add_dependency('activesupport', '= 2.3.4' + PKG_BUILD)
s.add_dependency('rack', '~> 1.0.0') s.add_dependency('rack', '~> 1.0.0')
s.require_path = 'lib' s.require_path = 'lib'

View file

@ -0,0 +1,87 @@
$:.push File.join(File.dirname(__FILE__), "..", "lib")
$:.push File.join(File.dirname(__FILE__), "..", "..", "activesupport", "lib")
require "action_controller"
class Runner
def initialize(app, output)
@app, @output = app, output
end
def puts(*)
super if @output
end
def call(env)
env['n'].to_i.times { @app.call(env) }
@app.call(env).tap { |response| report(env, response) }
end
def report(env, response)
if ENV["DEBUG"]
out = env['rack.errors']
p response.headers
out.puts response.status, response.headers.to_yaml, '---'
response.body.each { |part| out.puts part }
out.puts '---'
end
end
def self.puts(*)
super if @output
end
def self.run(app, n, label = nil, uri = "/", output = true)
@output = output
puts label, '=' * label.size if label
env = Rack::MockRequest.env_for(uri).merge('n' => n, 'rack.input' => StringIO.new(''), 'rack.errors' => $stdout)
t = Benchmark.realtime { new(app, output).call(env) }
puts "%d ms / %d req = %.1f usec/req" % [10**3 * t, n, 10**6 * t / n]
puts
end
end
N = (ENV['N'] || 1000).to_i
class BasePostController < ActionController::Base
append_view_path "#{File.dirname(__FILE__)}/views"
def index
render :text => 'Hello'
end
def partial
render :partial => "/partial"
end
def many_partials
render :partial => "/many_partials"
end
def partial_collection
render :partial => "/collection", :collection => [1,2,3,4,5,6,7,8,9,10]
end
def show_template
render :template => "template"
end
end
# p BasePostController.call(Rack::MockRequest.env_for("/?action=index").merge("REQUEST_URI" => "/")).body
Runner.run(BasePostController, N, 'index', "/?action=index", false)
Runner.run(BasePostController, N, 'partial', "/?action=partial", false)
Runner.run(BasePostController, N, 'many partials', "/?action=many_partials", false)
Runner.run(BasePostController, N, 'collection', "/?action=partial_collection", false)
Runner.run(BasePostController, N, 'template', "/?action=show_template", false)
(ENV["M"] || 1).to_i.times do
Runner.run(BasePostController, N, 'index', "/?action=index")
Runner.run(BasePostController, N, 'partial', "/?action=partial")
Runner.run(BasePostController, N, 'many partials', "/?action=many_partials")
Runner.run(BasePostController, N, 'collection', "/?action=partial_collection")
Runner.run(BasePostController, N, 'template', "/?action=show_template")
end
# Runner.run(BasePostController.action(:many_partials), N, 'index')
# Runner.run(BasePostController.action(:many_partials), N, 'many_partials')
# Runner.run(BasePostController.action(:partial_collection), N, 'collection')
# Runner.run(BasePostController.action(:show_template), N, 'template')

View file

@ -0,0 +1 @@
<%= collection %>

View file

@ -0,0 +1 @@
Hello

View file

@ -0,0 +1,10 @@
<%= render :partial => '/hello' %>
<%= render :partial => '/hello' %>
<%= render :partial => '/hello' %>
<%= render :partial => '/hello' %>
<%= render :partial => '/hello' %>
<%= render :partial => '/hello' %>
<%= render :partial => '/hello' %>
<%= render :partial => '/hello' %>
<%= render :partial => '/hello' %>
<%= render :partial => '/hello' %>

View file

@ -0,0 +1,10 @@
<%= "Hello" %>
<%= "Hello" %>
<%= "Hello" %>
<%= "Hello" %>
<%= "Hello" %>
<%= "Hello" %>
<%= "Hello" %>
<%= "Hello" %>
<%= "Hello" %>
<%= "Hello" %>

View file

@ -0,0 +1 @@
+ <%= yield %> +

View file

@ -0,0 +1 @@
Hello <%= yield %> Goodbye

View file

@ -0,0 +1 @@
Hello

View file

@ -31,12 +31,8 @@ rescue LoadError
end end
end end
begin gem 'rack', '~> 1.0.0'
gem 'rack', '~> 1.1.0' require 'rack'
require 'rack'
rescue Gem::LoadError
require 'action_controller/vendor/rack-1.1.pre/rack'
end
module ActionController module ActionController
# TODO: Review explicit to see if they will automatically be handled by # TODO: Review explicit to see if they will automatically be handled by

View file

@ -64,7 +64,9 @@ module ActionController
# Support partial arguments for hash redirections # Support partial arguments for hash redirections
if options.is_a?(Hash) && @response.redirected_to.is_a?(Hash) if options.is_a?(Hash) && @response.redirected_to.is_a?(Hash)
if options.all? {|(key, value)| @response.redirected_to[key] == value} if options.all? {|(key, value)| @response.redirected_to[key] == value}
::ActiveSupport::Deprecation.warn("Using assert_redirected_to with partial hash arguments is deprecated. Specify the full set arguments instead", caller) callstack = caller.dup
callstack.slice!(0, 2)
::ActiveSupport::Deprecation.warn("Using assert_redirected_to with partial hash arguments is deprecated. Specify the full set arguments instead", callstack)
return true return true
end end
end end

View file

@ -493,7 +493,12 @@ module ActionController #:nodoc:
filtered_parameters[key] = filter_parameters(value) filtered_parameters[key] = filter_parameters(value)
elsif value.is_a?(Array) elsif value.is_a?(Array)
filtered_parameters[key] = value.collect do |item| filtered_parameters[key] = value.collect do |item|
case item
when Hash, Array
filter_parameters(item) filter_parameters(item)
else
item
end
end end
elsif block_given? elsif block_given?
key = key.dup key = key.dup
@ -814,7 +819,6 @@ module ActionController #:nodoc:
# render :text => proc { |response, output| # render :text => proc { |response, output|
# 10_000_000.times do |i| # 10_000_000.times do |i|
# output.write("This is line #{i}\n") # output.write("This is line #{i}\n")
# output.flush
# end # end
# } # }
# #

View file

@ -51,7 +51,7 @@ module ActionController #:nodoc:
protected protected
# Returns the cookie container, which operates as described above. # Returns the cookie container, which operates as described above.
def cookies def cookies
CookieJar.new(self) @cookies ||= CookieJar.new(self)
end end
end end

View file

@ -2,13 +2,12 @@ module ActionController
# Dispatches requests to the appropriate controller and takes care of # Dispatches requests to the appropriate controller and takes care of
# reloading the app after each request when Dependencies.load? is true. # reloading the app after each request when Dependencies.load? is true.
class Dispatcher class Dispatcher
@@cache_classes = true
class << self class << self
def define_dispatcher_callbacks(cache_classes) def define_dispatcher_callbacks(cache_classes)
@@cache_classes = cache_classes
unless cache_classes unless cache_classes
unless self.middleware.include?(Reloader)
self.middleware.insert_after(Failsafe, Reloader)
end
ActionView::Helpers::AssetTagHelper.cache_asset_timestamps = false ActionView::Helpers::AssetTagHelper.cache_asset_timestamps = false
end end
@ -79,7 +78,7 @@ module ActionController
# DEPRECATE: Remove arguments, since they are only used by CGI # DEPRECATE: Remove arguments, since they are only used by CGI
def initialize(output = $stdout, request = nil, response = nil) def initialize(output = $stdout, request = nil, response = nil)
@output = output @output = output
@app = @@middleware.build(lambda { |env| self.dup._call(env) }) build_middleware_stack if @@cache_classes
end end
def dispatch def dispatch
@ -103,7 +102,18 @@ module ActionController
end end
def call(env) def call(env)
if @@cache_classes
@app.call(env) @app.call(env)
else
Reloader.run do
# When class reloading is turned on, we will want to rebuild the
# middleware stack every time we process a request. If we don't
# rebuild the middleware stack, then the stack may contain references
# to old classes metal classes, which will b0rk class reloading.
build_middleware_stack
@app.call(env)
end
end
end end
def _call(env) def _call(env)
@ -114,5 +124,10 @@ module ActionController
def flush_logger def flush_logger
Base.logger.flush Base.logger.flush
end end
private
def build_middleware_stack
@app = @@middleware.build(lambda { |env| self.dup._call(env) })
end
end end
end end

View file

@ -139,7 +139,7 @@ module ActionController
end end
def decode_credentials(request) def decode_credentials(request)
ActiveSupport::Base64.decode64(authorization(request).split.last || '') ActiveSupport::Base64.decode64(authorization(request).split(' ', 2).last || '')
end end
def encode_credentials(user_name, password) def encode_credentials(user_name, password)
@ -195,9 +195,10 @@ module ActionController
return false unless password return false unless password
method = request.env['rack.methodoverride.original_method'] || request.env['REQUEST_METHOD'] method = request.env['rack.methodoverride.original_method'] || request.env['REQUEST_METHOD']
uri = credentials[:uri][0,1] == '/' ? request.request_uri : request.url
[true, false].any? do |password_is_ha1| [true, false].any? do |password_is_ha1|
expected = expected_response(method, request.env['REQUEST_URI'], credentials, password, password_is_ha1) expected = expected_response(method, uri, credentials, password, password_is_ha1)
expected == credentials[:response] expected == credentials[:response]
end end
end end

View file

@ -47,6 +47,8 @@ module ActionController
false false
end end
rescue Exception => e # YAML, XML or Ruby code block errors rescue Exception => e # YAML, XML or Ruby code block errors
logger.debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}"
raise raise
{ "body" => request.raw_post, { "body" => request.raw_post,
"content_type" => request.content_type, "content_type" => request.content_type,
@ -67,5 +69,9 @@ module ActionController
nil nil
end end
def logger
defined?(Rails.logger) ? Rails.logger : Logger.new($stderr)
end
end end
end end

View file

@ -1,14 +1,21 @@
require 'thread'
module ActionController module ActionController
class Reloader class Reloader
@@default_lock = Mutex.new
cattr_accessor :default_lock
class BodyWrapper class BodyWrapper
def initialize(body) def initialize(body, lock)
@body = body @body = body
@lock = lock
end end
def close def close
@body.close if @body.respond_to?(:close) @body.close if @body.respond_to?(:close)
ensure ensure
Dispatcher.cleanup_application Dispatcher.cleanup_application
@lock.unlock
end end
def method_missing(*args, &block) def method_missing(*args, &block)
@ -20,13 +27,11 @@ module ActionController
end end
end end
def initialize(app) def self.run(lock = @@default_lock)
@app = app lock.lock
end begin
def call(env)
Dispatcher.reload_application Dispatcher.reload_application
status, headers, body = @app.call(env) status, headers, body = yield
# We do not want to call 'cleanup_application' in an ensure block # We do not want to call 'cleanup_application' in an ensure block
# because the returned Rack response body may lazily generate its data. This # because the returned Rack response body may lazily generate its data. This
# is for example the case if one calls # is for example the case if one calls
@ -39,7 +44,11 @@ module ActionController
# completely finished. So we wrap the body in a BodyWrapper class so that # completely finished. So we wrap the body in a BodyWrapper class so that
# when the Rack handler calls #close during the end of the request, we get to # when the Rack handler calls #close during the end of the request, we get to
# run our cleanup code. # run our cleanup code.
[status, headers, BodyWrapper.new(body)] [status, headers, BodyWrapper.new(body, lock)]
rescue Exception
lock.unlock
raise
end
end end
end end
end end

View file

@ -81,12 +81,13 @@ module ActionController #:nodoc:
# Returns true or false if a request is verified. Checks: # Returns true or false if a request is verified. Checks:
# #
# * is the format restricted? By default, only HTML and AJAX requests are checked. # * is the format restricted? By default, only HTML requests are checked.
# * is it a GET request? Gets should be safe and idempotent # * is it a GET request? Gets should be safe and idempotent
# * Does the form_authenticity_token match the given token value from the params? # * Does the form_authenticity_token match the given token value from the params?
def verified_request? def verified_request?
!protect_against_forgery? || !protect_against_forgery? ||
request.method == :get || request.method == :get ||
request.xhr? ||
!verifiable_request_format? || !verifiable_request_format? ||
form_authenticity_token == params[request_forgery_protection_token] form_authenticity_token == params[request_forgery_protection_token]
end end

View file

@ -317,9 +317,10 @@ module ActionController
# notes.resources :attachments # notes.resources :attachments
# end # end
# #
# * <tt>:path_names</tt> - Specify different names for the 'new' and 'edit' actions. For example: # * <tt>:path_names</tt> - Specify different path names for the actions. For example:
# # new_products_path == '/productos/nuevo' # # new_products_path == '/productos/nuevo'
# map.resources :products, :as => 'productos', :path_names => { :new => 'nuevo', :edit => 'editar' } # # bids_product_path(1) == '/productos/1/licitacoes'
# map.resources :products, :as => 'productos', :member => { :bids => :get }, :path_names => { :new => 'nuevo', :bids => 'licitacoes' }
# #
# You can also set default action names from an environment, like this: # You can also set default action names from an environment, like this:
# config.action_controller.resources_path_names = { :new => 'nuevo', :edit => 'editar' } # config.action_controller.resources_path_names = { :new => 'nuevo', :edit => 'editar' }
@ -525,16 +526,16 @@ module ActionController
resource = Resource.new(entities, options) resource = Resource.new(entities, options)
with_options :controller => resource.controller do |map| with_options :controller => resource.controller do |map|
map_collection_actions(map, resource)
map_default_collection_actions(map, resource)
map_new_actions(map, resource)
map_member_actions(map, resource)
map_associations(resource, options) map_associations(resource, options)
if block_given? if block_given?
with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block) with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block)
end end
map_collection_actions(map, resource)
map_default_collection_actions(map, resource)
map_new_actions(map, resource)
map_member_actions(map, resource)
end end
end end
@ -542,16 +543,16 @@ module ActionController
resource = SingletonResource.new(entities, options) resource = SingletonResource.new(entities, options)
with_options :controller => resource.controller do |map| with_options :controller => resource.controller do |map|
map_collection_actions(map, resource)
map_new_actions(map, resource)
map_member_actions(map, resource)
map_default_singleton_actions(map, resource)
map_associations(resource, options) map_associations(resource, options)
if block_given? if block_given?
with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block) with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block)
end end
map_collection_actions(map, resource)
map_new_actions(map, resource)
map_member_actions(map, resource)
map_default_singleton_actions(map, resource)
end end
end end
@ -586,7 +587,10 @@ module ActionController
resource.collection_methods.each do |method, actions| resource.collection_methods.each do |method, actions|
actions.each do |action| actions.each do |action|
[method].flatten.each do |m| [method].flatten.each do |m|
map_resource_routes(map, resource, action, "#{resource.path}#{resource.action_separator}#{action}", "#{action}_#{resource.name_prefix}#{resource.plural}", m) action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash)
action_path ||= action
map_resource_routes(map, resource, action, "#{resource.path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.name_prefix}#{resource.plural}", m)
end end
end end
end end

View file

@ -166,6 +166,12 @@ module ActionController # :nodoc:
str str
end end
def flush #:nodoc:
ActiveSupport::Deprecation.warn(
'Calling output.flush is no longer needed for streaming output ' +
'because ActionController::Response automatically handles it', caller)
end
def set_cookie(key, value) def set_cookie(key, value)
if value.has_key?(:http_only) if value.has_key?(:http_only)
ActiveSupport::Deprecation.warn( ActiveSupport::Deprecation.warn(

View file

@ -271,6 +271,9 @@ module ActionController
ALLOWED_REQUIREMENTS_FOR_OPTIMISATION = [:controller, :action].to_set ALLOWED_REQUIREMENTS_FOR_OPTIMISATION = [:controller, :action].to_set
mattr_accessor :generate_best_match
self.generate_best_match = true
# The root paths which may contain controller files # The root paths which may contain controller files
mattr_accessor :controller_paths mattr_accessor :controller_paths
self.controller_paths = [] self.controller_paths = []

View file

@ -405,11 +405,14 @@ module ActionController
end end
# don't use the recalled keys when determining which routes to check # don't use the recalled keys when determining which routes to check
routes = routes_by_controller[controller][action][options.reject {|k,v| !v}.keys.sort_by { |x| x.object_id }] future_routes, deprecated_routes = routes_by_controller[controller][action][options.reject {|k,v| !v}.keys.sort_by { |x| x.object_id }]
routes = Routing.generate_best_match ? deprecated_routes : future_routes
routes.each do |route| routes.each_with_index do |route, index|
results = route.__send__(method, options, merged, expire_on) results = route.__send__(method, options, merged, expire_on)
return results if results && (!results.is_a?(Array) || results.first) if results && (!results.is_a?(Array) || results.first)
return results
end
end end
end end
@ -448,7 +451,10 @@ module ActionController
@routes_by_controller ||= Hash.new do |controller_hash, controller| @routes_by_controller ||= Hash.new do |controller_hash, controller|
controller_hash[controller] = Hash.new do |action_hash, action| controller_hash[controller] = Hash.new do |action_hash, action|
action_hash[action] = Hash.new do |key_hash, keys| action_hash[action] = Hash.new do |key_hash, keys|
key_hash[keys] = routes_for_controller_and_action_and_keys(controller, action, keys) key_hash[keys] = [
routes_for_controller_and_action_and_keys(controller, action, keys),
deprecated_routes_for_controller_and_action_and_keys(controller, action, keys)
]
end end
end end
end end
@ -460,10 +466,11 @@ module ActionController
merged = options if expire_on[:controller] merged = options if expire_on[:controller]
action = merged[:action] || 'index' action = merged[:action] || 'index'
routes_by_controller[controller][action][merged.keys] routes_by_controller[controller][action][merged.keys][1]
end end
def routes_for_controller_and_action(controller, action) def routes_for_controller_and_action(controller, action)
ActiveSupport::Deprecation.warn "routes_for_controller_and_action() has been deprecated. Please use routes_for()"
selected = routes.select do |route| selected = routes.select do |route|
route.matches_controller_and_action? controller, action route.matches_controller_and_action? controller, action
end end
@ -471,6 +478,12 @@ module ActionController
end end
def routes_for_controller_and_action_and_keys(controller, action, keys) def routes_for_controller_and_action_and_keys(controller, action, keys)
routes.select do |route|
route.matches_controller_and_action? controller, action
end
end
def deprecated_routes_for_controller_and_action_and_keys(controller, action, keys)
selected = routes.select do |route| selected = routes.select do |route|
route.matches_controller_and_action? controller, action route.matches_controller_and_action? controller, action
end end

View file

@ -1,3 +1,5 @@
require 'active_support/core_ext/string/bytesize'
module ActionController #:nodoc: module ActionController #:nodoc:
# Methods for sending arbitrary data and for streaming files to the browser, # Methods for sending arbitrary data and for streaming files to the browser,
# instead of rendering. # instead of rendering.
@ -137,7 +139,7 @@ module ActionController #:nodoc:
# instead. See ActionController::Base#render for more information. # instead. See ActionController::Base#render for more information.
def send_data(data, options = {}) #:doc: def send_data(data, options = {}) #:doc:
logger.info "Sending data #{options[:filename]}" if logger logger.info "Sending data #{options[:filename]}" if logger
send_file_headers! options.merge(:length => data.size) send_file_headers! options.merge(:length => data.bytesize)
@performed_render = false @performed_render = false
render :status => options[:status], :text => data render :status => options[:status], :text => data
end end

View file

@ -184,7 +184,7 @@ module ActionController
path = rewrite_path(options) path = rewrite_path(options)
rewritten_url << ActionController::Base.relative_url_root.to_s unless options[:skip_relative_url_root] rewritten_url << ActionController::Base.relative_url_root.to_s unless options[:skip_relative_url_root]
rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path) rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path)
rewritten_url << "##{options[:anchor]}" if options[:anchor] rewritten_url << "##{CGI.escape(options[:anchor].to_param.to_s)}" if options[:anchor]
rewritten_url rewritten_url
end end

View file

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

View file

@ -98,7 +98,7 @@ module ActionView
options[:schema_date] = "2005" # The Atom spec copyright date options[:schema_date] = "2005" # The Atom spec copyright date
end end
xml = options[:xml] || eval("xml", block.binding) xml = options.delete(:xml) || eval("xml", block.binding)
xml.instruct! xml.instruct!
if options[:instruct] if options[:instruct]
options[:instruct].each do |target,attrs| options[:instruct].each do |target,attrs|

View file

@ -726,6 +726,7 @@ module ActionView
options = options.stringify_keys options = options.stringify_keys
tag_value = options.delete("value") tag_value = options.delete("value")
name_and_id = options.dup name_and_id = options.dup
name_and_id["id"] = name_and_id["for"]
add_default_name_and_id_for_value(tag_value, name_and_id) add_default_name_and_id_for_value(tag_value, name_and_id)
options.delete("index") options.delete("index")
options["for"] ||= name_and_id["id"] options["for"] ||= name_and_id["id"]
@ -860,7 +861,7 @@ module ActionView
private private
def add_default_name_and_id_for_value(tag_value, options) def add_default_name_and_id_for_value(tag_value, options)
if tag_value unless tag_value.nil?
pretty_tag_value = tag_value.to_s.gsub(/\s/, "_").gsub(/\W/, "").downcase pretty_tag_value = tag_value.to_s.gsub(/\s/, "_").gsub(/\W/, "").downcase
specified_id = options["id"] specified_id = options["id"]
add_default_name_and_id(options) add_default_name_and_id(options)

View file

@ -162,6 +162,60 @@ module ActionView
InstanceTag.new(object, method, self, options.delete(:object)).to_collection_select_tag(collection, value_method, text_method, options, html_options) InstanceTag.new(object, method, self, options.delete(:object)).to_collection_select_tag(collection, value_method, text_method, options, html_options)
end end
# Returns <tt><select></tt>, <tt><optgroup></tt> and <tt><option></tt> tags for the collection of existing return values of
# +method+ for +object+'s class. The value returned from calling +method+ on the instance +object+ will
# be selected. If calling +method+ returns +nil+, no selection is made without including <tt>:prompt</tt>
# or <tt>:include_blank</tt> in the +options+ hash.
#
# Parameters:
# * +object+ - The instance of the class to be used for the select tag
# * +method+ - The attribute of +object+ corresponding to the select tag
# * +collection+ - An array of objects representing the <tt><optgroup></tt> tags.
# * +group_method+ - The name of a method which, when called on a member of +collection+, returns an
# array of child objects representing the <tt><option></tt> tags.
# * +group_label_method+ - The name of a method which, when called on a member of +collection+, returns a
# string to be used as the +label+ attribute for its <tt><optgroup></tt> tag.
# * +option_key_method+ - The name of a method which, when called on a child object of a member of
# +collection+, returns a value to be used as the +value+ attribute for its <tt><option></tt> tag.
# * +option_value_method+ - The name of a method which, when called on a child object of a member of
# +collection+, returns a value to be used as the contents of its <tt><option></tt> tag.
#
# Example object structure for use with this method:
# class Continent < ActiveRecord::Base
# has_many :countries
# # attribs: id, name
# end
# class Country < ActiveRecord::Base
# belongs_to :continent
# # attribs: id, name, continent_id
# end
# class City < ActiveRecord::Base
# belongs_to :country
# # attribs: id, name, country_id
# end
#
# Sample usage:
# grouped_collection_select(:city, :country_id, @continents, :countries, :name, :id, :name)
#
# Possible output:
# <select name="city[country_id]">
# <optgroup label="Africa">
# <option value="1">South Africa</option>
# <option value="3">Somalia</option>
# </optgroup>
# <optgroup label="Europe">
# <option value="7" selected="selected">Denmark</option>
# <option value="2">Ireland</option>
# </optgroup>
# </select>
#
def grouped_collection_select(object, method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {})
InstanceTag.new(object, method, self, options.delete(:object)).to_grouped_collection_select_tag(collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options)
end
# Return select and option tags for the given object and method, using # Return select and option tags for the given object and method, using
# #time_zone_options_for_select to generate the list of option tags. # #time_zone_options_for_select to generate the list of option tags.
# #
@ -490,6 +544,15 @@ module ActionView
) )
end end
def to_grouped_collection_select_tag(collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options)
html_options = html_options.stringify_keys
add_default_name_and_id(html_options)
value = value(object)
content_tag(
"select", add_options(option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, value), options, value), html_options
)
end
def to_time_zone_select_tag(priority_zones, options, html_options) def to_time_zone_select_tag(priority_zones, options, html_options)
html_options = html_options.stringify_keys html_options = html_options.stringify_keys
add_default_name_and_id(html_options) add_default_name_and_id(html_options)
@ -508,7 +571,8 @@ module ActionView
option_tags = "<option value=\"\">#{options[:include_blank] if options[:include_blank].kind_of?(String)}</option>\n" + option_tags option_tags = "<option value=\"\">#{options[:include_blank] if options[:include_blank].kind_of?(String)}</option>\n" + option_tags
end end
if value.blank? && options[:prompt] if value.blank? && options[:prompt]
("<option value=\"\">#{options[:prompt].kind_of?(String) ? options[:prompt] : 'Please select'}</option>\n") + option_tags prompt = options[:prompt].kind_of?(String) ? options[:prompt] : I18n.translate('support.select.prompt', :default => 'Please select')
"<option value=\"\">#{prompt}</option>\n" + option_tags
else else
option_tags option_tags
end end
@ -524,6 +588,10 @@ module ActionView
@template.collection_select(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options)) @template.collection_select(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options))
end end
def grouped_collection_select(method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {})
@template.grouped_collection_select(@object_name, method, collection, group_method, group_label_method, option_key_method, option_value_method, objectify_options(options), @default_options.merge(html_options))
end
def time_zone_select(method, priority_zones = nil, options = {}, html_options = {}) def time_zone_select(method, priority_zones = nil, options = {}, html_options = {})
@template.time_zone_select(@object_name, method, priority_zones, objectify_options(options), @default_options.merge(html_options)) @template.time_zone_select(@object_name, method, priority_zones, objectify_options(options), @default_options.merge(html_options))
end end

View file

@ -103,7 +103,7 @@ module ActionView
# escape_once("&lt;&lt; Accept & Checkout") # escape_once("&lt;&lt; Accept & Checkout")
# # => "&lt;&lt; Accept &amp; Checkout" # # => "&lt;&lt; Accept &amp; Checkout"
def escape_once(html) def escape_once(html)
html.to_s.gsub(/[\"><]|&(?!([a-zA-Z]+|(#\d+));)/) { |special| ERB::Util::HTML_ESCAPE[special] } ActiveSupport::Multibyte.clean(html.to_s).gsub(/[\"><]|&(?!([a-zA-Z]+|(#\d+));)/) { |special| ERB::Util::HTML_ESCAPE[special] }
end end
private private

View file

@ -33,30 +33,31 @@ module ActionView
end end
# Truncates a given +text+ after a given <tt>:length</tt> if +text+ is longer than <tt>:length</tt> # Truncates a given +text+ after a given <tt>:length</tt> if +text+ is longer than <tt>:length</tt>
# (defaults to 30). The last characters will be replaced with the <tt>:omission</tt> (defaults to "..."). # (defaults to 30). The last characters will be replaced with the <tt>:omission</tt> (defaults to "...")
# for a total length not exceeding <tt>:length</tt>.
# #
# ==== Examples # ==== Examples
# #
# truncate("Once upon a time in a world far far away") # truncate("Once upon a time in a world far far away")
# # => Once upon a time in a world f... # # => Once upon a time in a world...
# #
# truncate("Once upon a time in a world far far away", :length => 14) # truncate("Once upon a time in a world far far away", :length => 14)
# # => Once upon a... # # => Once upon a...
# #
# truncate("And they found that many people were sleeping better.", :length => 25, "(clipped)") # truncate("And they found that many people were sleeping better.", :length => 25, "(clipped)")
# # => And they found that many (clipped) # # => And they found t(clipped)
# #
# truncate("And they found that many people were sleeping better.", :omission => "... (continued)", :length => 15) # truncate("And they found that many people were sleeping better.", :omission => "... (continued)", :length => 25)
# # => And they found... (continued) # # => And they f... (continued)
# #
# You can still use <tt>truncate</tt> with the old API that accepts the # You can still use <tt>truncate</tt> with the old API that accepts the
# +length+ as its optional second and the +ellipsis+ as its # +length+ as its optional second and the +ellipsis+ as its
# optional third parameter: # optional third parameter:
# truncate("Once upon a time in a world far far away", 14) # truncate("Once upon a time in a world far far away", 14)
# # => Once upon a time in a world f... # # => Once upon a...
# #
# truncate("And they found that many people were sleeping better.", 15, "... (continued)") # truncate("And they found that many people were sleeping better.", 25, "... (continued)")
# # => And they found... (continued) # # => And they f... (continued)
def truncate(text, *args) def truncate(text, *args)
options = args.extract_options! options = args.extract_options!
unless args.empty? unless args.empty?
@ -234,12 +235,20 @@ module ActionView
# #
# textilize("Visit the Rails website "here":http://www.rubyonrails.org/.) # textilize("Visit the Rails website "here":http://www.rubyonrails.org/.)
# # => "<p>Visit the Rails website <a href="http://www.rubyonrails.org/">here</a>.</p>" # # => "<p>Visit the Rails website <a href="http://www.rubyonrails.org/">here</a>.</p>"
def textilize(text) #
# textilize("This is worded <strong>strongly</strong>")
# # => "<p>This is worded <strong>strongly</strong></p>"
#
# textilize("This is worded <strong>strongly</strong>", :filter_html)
# # => "<p>This is worded &lt;strong&gt;strongly&lt;/strong&gt;</p>"
#
def textilize(text, *options)
options ||= [:hard_breaks]
if text.blank? if text.blank?
"" ""
else else
textilized = RedCloth.new(text, [ :hard_breaks ]) textilized = RedCloth.new(text, options)
textilized.hard_breaks = true if textilized.respond_to?(:hard_breaks=)
textilized.to_html textilized.to_html
end end
end end

View file

@ -568,7 +568,7 @@ module ActionView
when confirm && popup when confirm && popup
"if (#{confirm_javascript_function(confirm)}) { #{popup_javascript_function(popup)} };return false;" "if (#{confirm_javascript_function(confirm)}) { #{popup_javascript_function(popup)} };return false;"
when confirm && method when confirm && method
"if (#{confirm_javascript_function(confirm)}) { #{method_javascript_function(method)} };return false;" "if (#{confirm_javascript_function(confirm)}) { #{method_javascript_function(method, url, href)} };return false;"
when confirm when confirm
"return #{confirm_javascript_function(confirm)};" "return #{confirm_javascript_function(confirm)};"
when method when method

View file

@ -108,3 +108,7 @@
# The variable :count is also available # The variable :count is also available
body: "There were problems with the following fields:" body: "There were problems with the following fields:"
support:
select:
# default value for :prompt => true in FormOptionsHelper
prompt: "Please select"

View file

@ -43,3 +43,19 @@ ActionController::Base.view_paths = FIXTURE_LOAD_PATH
CACHED_VIEW_PATHS = ActionView::Base.cache_template_loading? ? CACHED_VIEW_PATHS = ActionView::Base.cache_template_loading? ?
ActionController::Base.view_paths : ActionController::Base.view_paths :
ActionController::Base.view_paths.map {|path| ActionView::Template::EagerPath.new(path.to_s)} ActionController::Base.view_paths.map {|path| ActionView::Template::EagerPath.new(path.to_s)}
class DummyMutex
def lock
@locked = true
end
def unlock
@locked = false
end
def locked?
@locked
end
end
ActionController::Reloader.default_lock = DummyMutex.new

View file

@ -52,7 +52,7 @@ class PageCachingTest < ActionController::TestCase
ActionController::Base.perform_caching = true ActionController::Base.perform_caching = true
ActionController::Routing::Routes.draw do |map| ActionController::Routing::Routes.draw do |map|
map.main '', :controller => 'posts' map.main '', :controller => 'posts', :format => nil
map.formatted_posts 'posts.:format', :controller => 'posts' map.formatted_posts 'posts.:format', :controller => 'posts'
map.resources :posts map.resources :posts
map.connect ':controller/:action/:id' map.connect ':controller/:action/:id'

View file

@ -118,4 +118,10 @@ class CookieTest < ActionController::TestCase
get :delete_cookie_with_path get :delete_cookie_with_path
assert_equal ["user_name=; path=/beaten; expires=Thu, 01-Jan-1970 00:00:00 GMT"], @response.headers["Set-Cookie"] assert_equal ["user_name=; path=/beaten; expires=Thu, 01-Jan-1970 00:00:00 GMT"], @response.headers["Set-Cookie"]
end end
def test_cookies_persist_throughout_request
get :authenticate
cookies = @controller.send(:cookies)
assert_equal 'david', cookies['user_name']
end
end end

View file

@ -2,25 +2,17 @@ require 'abstract_unit'
class DispatcherTest < Test::Unit::TestCase class DispatcherTest < Test::Unit::TestCase
Dispatcher = ActionController::Dispatcher Dispatcher = ActionController::Dispatcher
Reloader = ActionController::Reloader
def setup def setup
ENV['REQUEST_METHOD'] = 'GET' ENV['REQUEST_METHOD'] = 'GET'
reset_dispatcher
Dispatcher.middleware = ActionController::MiddlewareStack.new do |middleware|
middlewares = File.expand_path(File.join(File.dirname(__FILE__), "../../lib/action_controller/middlewares.rb"))
middleware.instance_eval(File.read(middlewares))
end
# Clear callbacks as they are redefined by Dispatcher#define_dispatcher_callbacks
Dispatcher.instance_variable_set("@prepare_dispatch_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
Dispatcher.instance_variable_set("@before_dispatch_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
Dispatcher.instance_variable_set("@after_dispatch_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
Dispatcher.stubs(:require_dependency) Dispatcher.stubs(:require_dependency)
end end
def teardown def teardown
ENV.delete 'REQUEST_METHOD' ENV.delete 'REQUEST_METHOD'
reset_dispatcher
end end
def test_clears_dependencies_after_dispatch_if_in_loading_mode def test_clears_dependencies_after_dispatch_if_in_loading_mode
@ -41,6 +33,34 @@ class DispatcherTest < Test::Unit::TestCase
dispatch dispatch
end end
def test_builds_middleware_stack_only_during_initialization_if_not_in_loading_mode
dispatcher = create_dispatcher
assert_not_nil dispatcher.instance_variable_get(:"@app")
dispatcher.instance_variable_set(:"@app", lambda { |env| })
dispatcher.expects(:build_middleware_stack).never
dispatcher.call(nil)
dispatcher.call(nil)
end
def test_rebuilds_middleware_stack_on_every_request_if_in_loading_mode
dispatcher = create_dispatcher(false)
dispatcher.instance_variable_set(:"@app", lambda { |env| })
dispatcher.expects(:build_middleware_stack).twice
dispatcher.call(nil)
Reloader.default_lock.unlock
dispatcher.call(nil)
end
def test_doesnt_wrap_call_in_reloader_if_not_in_loading_mode
Reloader.expects(:run).never
dispatch
end
def test_wraps_call_in_reloader_if_in_loading_mode
Reloader.expects(:run).once
dispatch(false)
end
# Stub out dispatch error logger # Stub out dispatch error logger
class << Dispatcher class << Dispatcher
def log_failsafe_exception(status, exception); end def log_failsafe_exception(status, exception); end
@ -99,6 +119,25 @@ class DispatcherTest < Test::Unit::TestCase
Dispatcher.new.call({'rack.input' => StringIO.new('')}) Dispatcher.new.call({'rack.input' => StringIO.new('')})
end end
def create_dispatcher(cache_classes = true)
Dispatcher.define_dispatcher_callbacks(cache_classes)
Dispatcher.new
end
def reset_dispatcher
Dispatcher.middleware = ActionController::MiddlewareStack.new do |middleware|
middlewares = File.expand_path(File.join(File.dirname(__FILE__), "../../lib/action_controller/middlewares.rb"))
middleware.instance_eval(File.read(middlewares))
end
# Clear callbacks as they are redefined by Dispatcher#define_dispatcher_callbacks
Dispatcher.instance_variable_set("@prepare_dispatch_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
Dispatcher.instance_variable_set("@before_dispatch_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
Dispatcher.instance_variable_set("@after_dispatch_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
Dispatcher.define_dispatcher_callbacks(true)
end
def assert_subclasses(howmany, klass, message = klass.subclasses.inspect) def assert_subclasses(howmany, klass, message = klass.subclasses.inspect)
assert_equal howmany, klass.subclasses.size, message assert_equal howmany, klass.subclasses.size, message
end end

View file

@ -24,7 +24,8 @@ class FilterParamTest < Test::Unit::TestCase
[{'foo'=>'bar', 'baz'=>'foo'},{'foo'=>'[FILTERED]', 'baz'=>'[FILTERED]'},%w'foo baz'], [{'foo'=>'bar', 'baz'=>'foo'},{'foo'=>'[FILTERED]', 'baz'=>'[FILTERED]'},%w'foo baz'],
[{'bar'=>{'foo'=>'bar','bar'=>'foo'}},{'bar'=>{'foo'=>'[FILTERED]','bar'=>'foo'}},%w'fo'], [{'bar'=>{'foo'=>'bar','bar'=>'foo'}},{'bar'=>{'foo'=>'[FILTERED]','bar'=>'foo'}},%w'fo'],
[{'foo'=>{'foo'=>'bar','bar'=>'foo'}},{'foo'=>'[FILTERED]'},%w'f banana'], [{'foo'=>{'foo'=>'bar','bar'=>'foo'}},{'foo'=>'[FILTERED]'},%w'f banana'],
[{'baz'=>[{'foo'=>'baz'}]}, {'baz'=>[{'foo'=>'[FILTERED]'}]}, %w(foo)]] [{'baz'=>[{'foo'=>'baz'}]}, {'baz'=>[{'foo'=>'[FILTERED]'}]}, %w(foo)],
[{'baz'=>[{'foo'=>'baz'}, 1, 2, 3]}, {'baz'=>[{'foo'=>'[FILTERED]'}, 1, 2, 3]}, %w(foo)]]
test_hashes.each do |before_filter, after_filter, filter_words| test_hashes.each do |before_filter, after_filter, filter_words|
FilterParamController.filter_parameter_logging(*filter_words) FilterParamController.filter_parameter_logging(*filter_words)

View file

@ -4,6 +4,7 @@ class HttpBasicAuthenticationTest < ActionController::TestCase
class DummyController < ActionController::Base class DummyController < ActionController::Base
before_filter :authenticate, :only => :index before_filter :authenticate, :only => :index
before_filter :authenticate_with_request, :only => :display before_filter :authenticate_with_request, :only => :display
before_filter :authenticate_long_credentials, :only => :show
def index def index
render :text => "Hello Secret" render :text => "Hello Secret"
@ -13,6 +14,10 @@ class HttpBasicAuthenticationTest < ActionController::TestCase
render :text => 'Definitely Maybe' render :text => 'Definitely Maybe'
end end
def show
render :text => 'Only for loooooong credentials'
end
private private
def authenticate def authenticate
@ -28,6 +33,12 @@ class HttpBasicAuthenticationTest < ActionController::TestCase
request_http_basic_authentication("SuperSecret") request_http_basic_authentication("SuperSecret")
end end
end end
def authenticate_long_credentials
authenticate_or_request_with_http_basic do |username, password|
username == '1234567890123456789012345678901234567890' && password == '1234567890123456789012345678901234567890'
end
end
end end
AUTH_HEADERS = ['HTTP_AUTHORIZATION', 'X-HTTP_AUTHORIZATION', 'X_HTTP_AUTHORIZATION', 'REDIRECT_X_HTTP_AUTHORIZATION'] AUTH_HEADERS = ['HTTP_AUTHORIZATION', 'X-HTTP_AUTHORIZATION', 'X_HTTP_AUTHORIZATION', 'REDIRECT_X_HTTP_AUTHORIZATION']
@ -42,6 +53,13 @@ class HttpBasicAuthenticationTest < ActionController::TestCase
assert_response :success assert_response :success
assert_equal 'Hello Secret', @response.body, "Authentication failed for request header #{header}" assert_equal 'Hello Secret', @response.body, "Authentication failed for request header #{header}"
end end
test "successful authentication with #{header.downcase} and long credentials" do
@request.env[header] = encode_credentials('1234567890123456789012345678901234567890', '1234567890123456789012345678901234567890')
get :show
assert_response :success
assert_equal 'Only for loooooong credentials', @response.body, "Authentication failed for request header #{header} and long credentials"
end
end end
AUTH_HEADERS.each do |header| AUTH_HEADERS.each do |header|
@ -52,6 +70,13 @@ class HttpBasicAuthenticationTest < ActionController::TestCase
assert_response :unauthorized assert_response :unauthorized
assert_equal "HTTP Basic: Access denied.\n", @response.body, "Authentication didn't fail for request header #{header}" assert_equal "HTTP Basic: Access denied.\n", @response.body, "Authentication didn't fail for request header #{header}"
end end
test "unsuccessful authentication with #{header.downcase} and long credentials" do
@request.env[header] = encode_credentials('h4x0rh4x0rh4x0rh4x0rh4x0rh4x0rh4x0rh4x0r', 'worldworldworldworldworldworldworldworld')
get :show
assert_response :unauthorized
assert_equal "HTTP Basic: Access denied.\n", @response.body, "Authentication didn't fail for request header #{header} and long credentials"
end
end end
test "authentication request without credential" do test "authentication request without credential" do

View file

@ -138,10 +138,33 @@ class HttpDigestAuthenticationTest < ActionController::TestCase
assert_equal "Authentication Failed", @response.body assert_equal "Authentication Failed", @response.body
end end
test "authentication request with absolute uri" do test "authentication request with absolute request uri (as in webrick)" do
@request.env['HTTP_AUTHORIZATION'] = encode_credentials(:uri => "http://test.host/http_digest_authentication_test/dummy_digest/display", @request.env['HTTP_AUTHORIZATION'] = encode_credentials(:username => 'pretty', :password => 'please')
@request.env['REQUEST_URI'] = "http://test.host/http_digest_authentication_test/dummy_digest"
get :display
assert_response :success
assert assigns(:logged_in)
assert_equal 'Definitely Maybe', @response.body
end
test "authentication request with absolute uri in credentials (as in IE)" do
@request.env['HTTP_AUTHORIZATION'] = encode_credentials(:url => "http://test.host/http_digest_authentication_test/dummy_digest",
:username => 'pretty', :password => 'please') :username => 'pretty', :password => 'please')
@request.env['REQUEST_URI'] = "http://test.host/http_digest_authentication_test/dummy_digest/display"
get :display
assert_response :success
assert assigns(:logged_in)
assert_equal 'Definitely Maybe', @response.body
end
test "authentication request with absolute uri in both request and credentials (as in Webrick with IE)" do
@request.env['HTTP_AUTHORIZATION'] = encode_credentials(:url => "http://test.host/http_digest_authentication_test/dummy_digest",
:username => 'pretty', :password => 'please')
@request.env['REQUEST_URI'] = "http://test.host/http_digest_authentication_test/dummy_digest"
get :display get :display
assert_response :success assert_response :success
@ -199,7 +222,7 @@ class HttpDigestAuthenticationTest < ActionController::TestCase
credentials = decode_credentials(@response.headers['WWW-Authenticate']) credentials = decode_credentials(@response.headers['WWW-Authenticate'])
credentials.merge!(options) credentials.merge!(options)
credentials.reverse_merge!(:uri => "#{@request.env['REQUEST_URI']}") credentials.merge!(:uri => @request.env['REQUEST_URI'].to_s)
ActionController::HttpAuthentication::Digest.encode_credentials(method, credentials, password, options[:password_is_ha1]) ActionController::HttpAuthentication::Digest.encode_credentials(method, credentials, password, options[:password_is_ha1])
end end

View file

@ -1,6 +1,6 @@
require 'abstract_unit' require 'abstract_unit'
class BaseRackTest < Test::Unit::TestCase class BaseRackTest < ActiveSupport::TestCase
def setup def setup
@env = { @env = {
"HTTP_MAX_FORWARDS" => "10", "HTTP_MAX_FORWARDS" => "10",
@ -261,6 +261,23 @@ class RackResponseTest < BaseRackTest
body.each { |part| parts << part } body.each { |part| parts << part }
assert_equal ["0", "1", "2", "3", "4"], parts assert_equal ["0", "1", "2", "3", "4"], parts
end end
def test_streaming_block_with_flush_is_deprecated
@response.body = Proc.new do |response, output|
5.times do |n|
output.write(n)
output.flush
end
end
assert_deprecated(/output.flush is no longer needed/) do
@response.prepare!
status, headers, body = @response.to_a
parts = []
body.each { |part| parts << part }
end
end
end end
class RackResponseHeadersTest < BaseRackTest class RackResponseHeadersTest < BaseRackTest

View file

@ -236,7 +236,7 @@ class RedirectTest < ActionController::TestCase
def test_redirect_with_partial_params def test_redirect_with_partial_params
get :module_redirect get :module_redirect
assert_deprecated do assert_deprecated(/test_redirect_with_partial_params/) do
assert_redirected_to :action => 'hello_world' assert_redirected_to :action => 'hello_world'
end end
end end

View file

@ -22,35 +22,62 @@ class ReloaderTests < ActiveSupport::TestCase
end end
end end
def setup_and_return_body(app = lambda { }) def setup
Dispatcher.expects(:reload_application) @lock = Mutex.new
reloader = Reloader.new(app)
headers, status, body = reloader.call({ })
body
end end
def test_it_reloads_the_application_before_the_request def test_it_reloads_the_application_before_yielding
Dispatcher.expects(:reload_application) Dispatcher.expects(:reload_application)
reloader = Reloader.new(lambda { Reloader.run(@lock) do
[200, { "Content-Type" => "text/html" }, [""]] [200, { "Content-Type" => "text/html" }, [""]]
}) end
reloader.call({ }) end
def test_it_locks_before_yielding
lock = DummyMutex.new
Dispatcher.expects(:reload_application)
Reloader.run(lock) do
assert lock.locked?
[200, { "Content-Type" => "text/html" }, [""]]
end
assert lock.locked?
end
def test_it_unlocks_upon_calling_close_on_body
lock = DummyMutex.new
Dispatcher.expects(:reload_application)
headers, status, body = Reloader.run(lock) do
[200, { "Content-Type" => "text/html" }, [""]]
end
body.close
assert !lock.locked?
end
def test_it_unlocks_if_app_object_raises_exception
lock = DummyMutex.new
Dispatcher.expects(:reload_application)
assert_raise(RuntimeError) do
Reloader.run(lock) do
raise "oh no!"
end
end
assert !lock.locked?
end end
def test_returned_body_object_always_responds_to_close def test_returned_body_object_always_responds_to_close
body = setup_and_return_body(lambda { status, headers, body = Reloader.run(@lock) do
[200, { "Content-Type" => "text/html" }, [""]] [200, { "Content-Type" => "text/html" }, [""]]
}) end
assert body.respond_to?(:close) assert body.respond_to?(:close)
end end
def test_returned_body_object_behaves_like_underlying_object def test_returned_body_object_behaves_like_underlying_object
body = setup_and_return_body(lambda { status, headers, body = Reloader.run(@lock) do
b = MyBody.new b = MyBody.new
b << "hello" b << "hello"
b << "world" b << "world"
[200, { "Content-Type" => "text/html" }, b] [200, { "Content-Type" => "text/html" }, b]
}) end
assert_equal 2, body.size assert_equal 2, body.size
assert_equal "hello", body[0] assert_equal "hello", body[0]
assert_equal "world", body[1] assert_equal "world", body[1]
@ -60,20 +87,20 @@ class ReloaderTests < ActiveSupport::TestCase
def test_it_calls_close_on_underlying_object_when_close_is_called_on_body def test_it_calls_close_on_underlying_object_when_close_is_called_on_body
close_called = false close_called = false
body = setup_and_return_body(lambda { status, headers, body = Reloader.run(@lock) do
b = MyBody.new do b = MyBody.new do
close_called = true close_called = true
end end
[200, { "Content-Type" => "text/html" }, b] [200, { "Content-Type" => "text/html" }, b]
}) end
body.close body.close
assert close_called assert close_called
end end
def test_returned_body_object_responds_to_all_methods_supported_by_underlying_object def test_returned_body_object_responds_to_all_methods_supported_by_underlying_object
body = setup_and_return_body(lambda { status, headers, body = Reloader.run(@lock) do
[200, { "Content-Type" => "text/html" }, MyBody.new] [200, { "Content-Type" => "text/html" }, MyBody.new]
}) end
assert body.respond_to?(:size) assert body.respond_to?(:size)
assert body.respond_to?(:each) assert body.respond_to?(:each)
assert body.respond_to?(:foo) assert body.respond_to?(:foo)
@ -82,16 +109,16 @@ class ReloaderTests < ActiveSupport::TestCase
def test_it_doesnt_clean_up_the_application_after_call def test_it_doesnt_clean_up_the_application_after_call
Dispatcher.expects(:cleanup_application).never Dispatcher.expects(:cleanup_application).never
body = setup_and_return_body(lambda { status, headers, body = Reloader.run(@lock) do
[200, { "Content-Type" => "text/html" }, MyBody.new] [200, { "Content-Type" => "text/html" }, MyBody.new]
}) end
end end
def test_it_cleans_up_the_application_when_close_is_called_on_body def test_it_cleans_up_the_application_when_close_is_called_on_body
Dispatcher.expects(:cleanup_application) Dispatcher.expects(:cleanup_application)
body = setup_and_return_body(lambda { status, headers, body = Reloader.run(@lock) do
[200, { "Content-Type" => "text/html" }, MyBody.new] [200, { "Content-Type" => "text/html" }, MyBody.new]
}) end
body.close body.close
end end
end end

View file

@ -30,16 +30,36 @@ class JsonParamsParsingTest < ActionController::IntegrationTest
) )
end end
private test "logs error if parsing unsuccessful" do
def assert_parses(expected, actual, headers = {}) with_test_routing do
with_routing do |set| begin
set.draw do |map| $stderr = StringIO.new
map.connect ':action', :controller => "json_params_parsing_test/test" json = "[\"person]\": {\"name\": \"David\"}}"
post "/parse", json, {'CONTENT_TYPE' => 'application/json'}
assert_response :error
$stderr.rewind && err = $stderr.read
assert err =~ /Error occurred while parsing request parameters/
ensure
$stderr = STDERR
end
end
end end
private
def assert_parses(expected, actual, headers = {})
with_test_routing do
post "/parse", actual, headers post "/parse", actual, headers
assert_response :ok assert_response :ok
assert_equal(expected, TestController.last_request_parameters) assert_equal(expected, TestController.last_request_parameters)
end end
end end
def with_test_routing
with_routing do |set|
set.draw do |map|
map.connect ':action', :controller => "json_params_parsing_test/test"
end
yield
end
end
end end

View file

@ -38,6 +38,21 @@ class XmlParamsParsingTest < ActionController::IntegrationTest
end end
end end
test "logs error if parsing unsuccessful" do
with_test_routing do
begin
$stderr = StringIO.new
xml = "<person><name>David</name><avatar type='file' name='me.jpg' content_type='image/jpg'>#{ActiveSupport::Base64.encode64('ABC')}</avatar></pineapple>"
post "/parse", xml, default_headers
assert_response :error
$stderr.rewind && err = $stderr.read
assert err =~ /Error occurred while parsing request parameters/
ensure
$stderr = STDERR
end
end
end
test "parses multiple files" do test "parses multiple files" do
xml = <<-end_body xml = <<-end_body
<person> <person>

View file

@ -155,10 +155,6 @@ module RequestForgeryProtectionTests
def test_should_allow_xhr_post_without_token def test_should_allow_xhr_post_without_token
assert_nothing_raised { xhr :post, :index } assert_nothing_raised { xhr :post, :index }
end end
def test_should_not_allow_xhr_post_with_html_without_token
@request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
assert_raise(ActionController::InvalidAuthenticityToken) { xhr :post, :index }
end
def test_should_allow_xhr_put_without_token def test_should_allow_xhr_put_without_token
assert_nothing_raised { xhr :put, :index } assert_nothing_raised { xhr :put, :index }
@ -168,6 +164,11 @@ module RequestForgeryProtectionTests
assert_nothing_raised { xhr :delete, :index } assert_nothing_raised { xhr :delete, :index }
end end
def test_should_allow_xhr_post_with_encoded_form_content_type_without_token
@request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
assert_nothing_raised { xhr :post, :index }
end
def test_should_allow_post_with_token def test_should_allow_post_with_token
post :index, :authenticity_token => @token post :index, :authenticity_token => @token
assert_response :success assert_response :success

View file

@ -76,6 +76,50 @@ class ResourcesTest < ActionController::TestCase
end end
end end
def test_override_paths_for_member_and_collection_methods
collection_methods = { 'rss' => :get, 'reorder' => :post, 'csv' => :post }
member_methods = { 'rss' => :get, :atom => :get, :upload => :post, :fix => :post }
path_names = {:new => 'nuevo', 'rss' => 'canal', :fix => 'corrigir' }
with_restful_routing :messages,
:collection => collection_methods,
:member => member_methods,
:path_names => path_names do
assert_restful_routes_for :messages,
:collection => collection_methods,
:member => member_methods,
:path_names => path_names do |options|
member_methods.each do |action, method|
assert_recognizes(options.merge(:action => action.to_s, :id => '1'),
:path => "/messages/1/#{path_names[action] || action}",
:method => method)
end
collection_methods.each do |action, method|
assert_recognizes(options.merge(:action => action),
:path => "/messages/#{path_names[action] || action}",
:method => method)
end
end
assert_restful_named_routes_for :messages,
:collection => collection_methods,
:member => member_methods,
:path_names => path_names do |options|
collection_methods.keys.each do |action|
assert_named_route "/messages/#{path_names[action] || action}", "#{action}_messages_path", :action => action
end
member_methods.keys.each do |action|
assert_named_route "/messages/1/#{path_names[action] || action}", "#{action}_message_path", :action => action, :id => "1"
end
end
end
end
def test_override_paths_for_default_restful_actions def test_override_paths_for_default_restful_actions
resource = ActionController::Resources::Resource.new(:messages, resource = ActionController::Resources::Resource.new(:messages,
:path_names => {:new => 'nuevo', :edit => 'editar'}) :path_names => {:new => 'nuevo', :edit => 'editar'})

View file

@ -1,5 +1,6 @@
require 'abstract_unit' require 'abstract_unit'
require 'controller/fake_controllers' require 'controller/fake_controllers'
require 'action_controller/routing/route_set'
class MilestonesController < ActionController::Base class MilestonesController < ActionController::Base
def index() head :ok end def index() head :ok end
@ -742,7 +743,7 @@ class MockController
end end
end end
class LegacyRouteSetTests < Test::Unit::TestCase class LegacyRouteSetTests < ActiveSupport::TestCase
attr_reader :rs attr_reader :rs
def setup def setup
@ -758,6 +759,10 @@ class LegacyRouteSetTests < Test::Unit::TestCase
@rs.clear! @rs.clear!
end end
def test_routes_for_controller_and_action_deprecated
assert_deprecated { @rs.routes_for_controller_and_action("controller", "action") }
end
def test_default_setup def test_default_setup
@rs.draw {|m| m.connect ':controller/:action/:id' } @rs.draw {|m| m.connect ':controller/:action/:id' }
assert_equal({:controller => "content", :action => 'index'}, rs.recognize_path("/content")) assert_equal({:controller => "content", :action => 'index'}, rs.recognize_path("/content"))
@ -1605,7 +1610,7 @@ class RouteTest < Test::Unit::TestCase
end end
end end
class RouteSetTest < Test::Unit::TestCase class RouteSetTest < ActiveSupport::TestCase
def set def set
@set ||= ROUTING::RouteSet.new @set ||= ROUTING::RouteSet.new
end end

View file

@ -1,9 +1,10 @@
# encoding: utf-8
require 'abstract_unit' require 'abstract_unit'
module TestFileUtils module TestFileUtils
def file_name() File.basename(__FILE__) end def file_name() File.basename(__FILE__) end
def file_path() File.expand_path(__FILE__) end def file_path() File.expand_path(__FILE__) end
def file_data() File.open(file_path, 'rb') { |f| f.read } end def file_data() @data ||= File.open(file_path, 'rb') { |f| f.read } end
end end
class SendFileController < ActionController::Base class SendFileController < ActionController::Base
@ -15,6 +16,7 @@ class SendFileController < ActionController::Base
def file() send_file(file_path, options) end def file() send_file(file_path, options) end
def data() send_data(file_data, options) end def data() send_data(file_data, options) end
def multibyte_text_data() send_data("Кирилица\n祝您好運", options) end
def rescue_action(e) raise end def rescue_action(e) raise end
end end
@ -49,6 +51,7 @@ class SendFileTest < ActionController::TestCase
require 'stringio' require 'stringio'
output = StringIO.new output = StringIO.new
output.binmode output.binmode
output.string.force_encoding(file_data.encoding) if output.string.respond_to?(:force_encoding)
assert_nothing_raised { response.body.call(response, output) } assert_nothing_raised { response.body.call(response, output) }
assert_equal file_data, output.string assert_equal file_data, output.string
end end
@ -158,4 +161,11 @@ class SendFileTest < ActionController::TestCase
assert_equal ActionController::Base::DEFAULT_RENDER_STATUS_CODE, @response.status assert_equal ActionController::Base::DEFAULT_RENDER_STATUS_CODE, @response.status
end end
end end
def test_send_data_content_length_header
@controller.headers = {}
@controller.options = { :type => :text, :filename => 'file_with_utf8_text' }
process('multibyte_text_data')
assert_equal '29', @controller.headers['Content-Length']
end
end end

View file

@ -46,6 +46,20 @@ class UrlRewriterTests < ActionController::TestCase
) )
end end
def test_anchor_should_call_to_param
assert_equal(
'http://test.host/c/a/i#anchor',
@rewriter.rewrite(:controller => 'c', :action => 'a', :id => 'i', :anchor => Struct.new(:to_param).new('anchor'))
)
end
def test_anchor_should_be_cgi_escaped
assert_equal(
'http://test.host/c/a/i#anc%2Fhor',
@rewriter.rewrite(:controller => 'c', :action => 'a', :id => 'i', :anchor => Struct.new(:to_param).new('anc/hor'))
)
end
def test_overwrite_params def test_overwrite_params
@params[:controller] = 'hi' @params[:controller] = 'hi'
@params[:action] = 'bye' @params[:action] = 'bye'
@ -110,6 +124,18 @@ class UrlWriterTests < ActionController::TestCase
) )
end end
def test_anchor_should_call_to_param
assert_equal('/c/a#anchor',
W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :anchor => Struct.new(:to_param).new('anchor'))
)
end
def test_anchor_should_be_cgi_escaped
assert_equal('/c/a#anc%2Fhor',
W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :anchor => Struct.new(:to_param).new('anc/hor'))
)
end
def test_default_host def test_default_host
add_host! add_host!
assert_equal('http://www.basecamphq.com/c/a/i', assert_equal('http://www.basecamphq.com/c/a/i',
@ -304,7 +330,7 @@ class UrlWriterTests < ActionController::TestCase
def test_named_routes_with_nil_keys def test_named_routes_with_nil_keys
ActionController::Routing::Routes.clear! ActionController::Routing::Routes.clear!
ActionController::Routing::Routes.draw do |map| ActionController::Routing::Routes.draw do |map|
map.main '', :controller => 'posts' map.main '', :controller => 'posts', :format => nil
map.resources :posts map.resources :posts
map.connect ':controller/:action/:id' map.connect ':controller/:action/:id'
end end

View file

@ -0,0 +1,23 @@
/* bank.css */
/* robber.css */
/* version.1.0.css */
/* bank.css */
/* bank.css */
/* robber.css */
/* version.1.0.css */
/* bank.css */
/* robber.css */
/* version.1.0.css */
/* robber.css */
/* version.1.0.css */

View file

@ -0,0 +1,63 @@
// prototype js
// effects js
// dragdrop js
// controls js
// prototype js
// effects js
// dragdrop js
// controls js
// application js
// bank js
// robber js
// version.1.0 js
// application js
// bank js
// prototype js
// effects js
// dragdrop js
// controls js
// prototype js
// effects js
// dragdrop js
// controls js
// application js
// bank js
// robber js
// version.1.0 js
// application js
// bank js
// robber js
// version.1.0 js
// robber js
// version.1.0 js

View file

@ -143,6 +143,26 @@ class ScrollsController < ActionController::Base
end end
entry.tag!('app:edited', Time.now) entry.tag!('app:edited', Time.now)
entry.author do |author|
author.name("DHH")
end
end
end
end
EOT
FEEDS["provide_builder"] = <<-'EOT'
# we pass in the new_xml to the helper so it doesn't
# call anything on the original builder
new_xml = Builder::XmlMarkup.new(:target=>'')
atom_feed(:xml => new_xml) do |feed|
feed.title("My great blog!")
feed.updated((@scrolls.first.created_at))
for scroll in @scrolls
feed.entry(scroll) do |entry|
entry.title(scroll.title)
entry.content(scroll.body, :type => 'html')
entry.author do |author| entry.author do |author|
author.name("DHH") author.name("DHH")
end end
@ -194,6 +214,15 @@ class AtomFeedTest < ActionController::TestCase
end end
end end
def test_providing_builder_to_atom_feed
with_restful_routing(:scrolls) do
get :index, :id=>"provide_builder"
# because we pass in the non-default builder, the content generated by the
# helper should go 'nowhere'. Leaving the response body blank.
assert @response.body.blank?
end
end
def test_entry_with_prefilled_options_should_use_those_instead_of_querying_the_record def test_entry_with_prefilled_options_should_use_those_instead_of_querying_the_record
with_restful_routing(:scrolls) do with_restful_routing(:scrolls) do
get :index, :id => "entry_options" get :index, :id => "entry_options"

View file

@ -145,6 +145,22 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal('<label for="my_for">Title</label>', label(:post, :title, nil, "for" => "my_for")) assert_dom_equal('<label for="my_for">Title</label>', label(:post, :title, nil, "for" => "my_for"))
end end
def test_label_with_id_attribute_as_symbol
assert_dom_equal('<label for="post_title" id="my_id">Title</label>', label(:post, :title, nil, :id => "my_id"))
end
def test_label_with_id_attribute_as_string
assert_dom_equal('<label for="post_title" id="my_id">Title</label>', label(:post, :title, nil, "id" => "my_id"))
end
def test_label_with_for_and_id_attributes_as_symbol
assert_dom_equal('<label for="my_for" id="my_id">Title</label>', label(:post, :title, nil, :for => "my_for", :id => "my_id"))
end
def test_label_with_for_and_id_attributes_as_string
assert_dom_equal('<label for="my_for" id="my_id">Title</label>', label(:post, :title, nil, "for" => "my_for", "id" => "my_id"))
end
def test_label_for_radio_buttons_with_value def test_label_for_radio_buttons_with_value
assert_dom_equal('<label for="post_title_great_title">The title goes here</label>', label("post", "title", "The title goes here", :value => "great_title")) assert_dom_equal('<label for="post_title_great_title">The title goes here</label>', label("post", "title", "The title goes here", :value => "great_title"))
assert_dom_equal('<label for="post_title_great_title">The title goes here</label>', label("post", "title", "The title goes here", :value => "great title")) assert_dom_equal('<label for="post_title_great_title">The title goes here</label>', label("post", "title", "The title goes here", :value => "great title"))
@ -295,6 +311,16 @@ class FormHelperTest < ActionView::TestCase
) )
end end
def test_radio_button_with_booleans
assert_dom_equal('<input id="post_secret_true" name="post[secret]" type="radio" value="true" />',
radio_button("post", "secret", true)
)
assert_dom_equal('<input id="post_secret_false" name="post[secret]" type="radio" value="false" />',
radio_button("post", "secret", false)
)
end
def test_text_area def test_text_area
assert_dom_equal( assert_dom_equal(
'<textarea cols="40" id="post_body" name="post[body]" rows="20">Back to the hill and over it again!</textarea>', '<textarea cols="40" id="post_body" name="post[body]" rows="20">Back to the hill and over it again!</textarea>',

View file

@ -0,0 +1,27 @@
require 'abstract_unit'
class FormOptionsHelperI18nTests < ActionView::TestCase
tests ActionView::Helpers::FormOptionsHelper
def setup
@prompt_message = 'Select!'
I18n.backend.send(:init_translations)
I18n.backend.store_translations :en, :support => { :select => { :prompt => @prompt_message } }
end
def teardown
I18n.backend = I18n::Backend::Simple.new
end
def test_select_with_prompt_true_translates_prompt_message
I18n.expects(:translate).with('support.select.prompt', { :default => 'Please select' })
select('post', 'category', [], :prompt => true)
end
def test_select_with_translated_prompt
assert_dom_equal(
%Q(<select id="post_category" name="post[category]"><option value="">#{@prompt_message}</option>\n</select>),
select('post', 'category', [], :prompt => true)
)
end
end

View file

@ -763,6 +763,40 @@ class FormOptionsHelperTest < ActionView::TestCase
html html
end end
def test_grouped_collection_select
@continents = [
Continent.new("<Africa>", [Country.new("<sa>", "<South Africa>"), Country.new("so", "Somalia")] ),
Continent.new("Europe", [Country.new("dk", "Denmark"), Country.new("ie", "Ireland")] )
]
@post = Post.new
@post.origin = 'dk'
assert_dom_equal(
%Q{<select id="post_origin" name="post[origin]"><optgroup label="&lt;Africa&gt;"><option value="&lt;sa&gt;">&lt;South Africa&gt;</option>\n<option value="so">Somalia</option></optgroup><optgroup label="Europe"><option value="dk" selected="selected">Denmark</option>\n<option value="ie">Ireland</option></optgroup></select>},
grouped_collection_select("post", "origin", @continents, :countries, :continent_name, :country_id, :country_name)
)
end
def test_grouped_collection_select_under_fields_for
@continents = [
Continent.new("<Africa>", [Country.new("<sa>", "<South Africa>"), Country.new("so", "Somalia")] ),
Continent.new("Europe", [Country.new("dk", "Denmark"), Country.new("ie", "Ireland")] )
]
@post = Post.new
@post.origin = 'dk'
fields_for :post, @post do |f|
concat f.grouped_collection_select("origin", @continents, :countries, :continent_name, :country_id, :country_name)
end
assert_dom_equal(
%Q{<select id="post_origin" name="post[origin]"><optgroup label="&lt;Africa&gt;"><option value="&lt;sa&gt;">&lt;South Africa&gt;</option>\n<option value="so">Somalia</option></optgroup><optgroup label="Europe"><option value="dk" selected="selected">Denmark</option>\n<option value="ie">Ireland</option></optgroup></select>},
output_buffer
)
end
private private
def dummy_posts def dummy_posts

View file

@ -1,5 +1,10 @@
require 'abstract_unit' require 'abstract_unit'
require 'testing_sandbox' require 'testing_sandbox'
begin
require 'redcloth'
rescue LoadError
$stderr.puts "Skipping textilize tests. `gem install RedCloth` to enable."
end
class TextHelperTest < ActionView::TestCase class TextHelperTest < ActionView::TestCase
tests ActionView::Helpers::TextHelper tests ActionView::Helpers::TextHelper
@ -517,4 +522,22 @@ class TextHelperTest < ActionView::TestCase
assert_equal("red", cycle("red", "blue")) assert_equal("red", cycle("red", "blue"))
assert_equal(%w{Specialized Fuji Giant}, @cycles) assert_equal(%w{Specialized Fuji Giant}, @cycles)
end end
if defined? RedCloth
def test_textilize
assert_equal("<p><strong>This is Textile!</strong> Rejoice!</p>", textilize("*This is Textile!* Rejoice!"))
end
def test_textilize_with_blank
assert_equal("", textilize(""))
end
def test_textilize_with_options
assert_equal("<p>This is worded &lt;strong&gt;strongly&lt;/strong&gt;</p>", textilize("This is worded <strong>strongly</strong>", :filter_html))
end
def test_textilize_with_hard_breaks
assert_equal("<p>This is one scary world.<br />\n True.</p>", textilize("This is one scary world.\n True."))
end
end
end end

View file

@ -219,6 +219,14 @@ class UrlHelperTest < ActionView::TestCase
) )
end end
def test_link_tag_using_delete_javascript_and_href_and_confirm
assert_dom_equal(
"<a href='\#' onclick=\"if (confirm('Are you serious?')) { var f = document.createElement('form'); f.style.display = 'none'; this.parentNode.appendChild(f); f.method = 'POST'; f.action = 'http://www.example.com';var m = document.createElement('input'); m.setAttribute('type', 'hidden'); m.setAttribute('name', '_method'); m.setAttribute('value', 'delete'); f.appendChild(m);f.submit(); };return false;\">Destroy</a>",
link_to("Destroy", "http://www.example.com", :method => :delete, :href => '#', :confirm => "Are you serious?"),
"When specifying url, form should be generated with it, but not this.href"
)
end
def test_link_tag_using_post_javascript_and_popup def test_link_tag_using_post_javascript_and_popup
assert_raise(ActionView::ActionViewError) { link_to("Hello", "http://www.example.com", :popup => true, :method => :post, :confirm => "Are you serious?") } assert_raise(ActionView::ActionViewError) { link_to("Hello", "http://www.example.com", :popup => true, :method => :post, :confirm => "Are you serious?") }
end end

View file

@ -1,3 +1,10 @@
*2.3.4 (September 4, 2009)*
* PostgreSQL: XML datatype support. #1874 [Leonardo Borges]
* SQLite: deprecate the 'dbfile' option in favor of 'database.' #2363 [Paul Hinze, Jeremy Kemper]
*2.3.3 (July 12, 2009)* *2.3.3 (July 12, 2009)*
* Added :primary_key option to belongs_to associations. #765 [Szymon Nowak, Philip Hallstrom, Noel Rocha] * Added :primary_key option to belongs_to associations. #765 [Szymon Nowak, Philip Hallstrom, Noel Rocha]

View file

@ -24,14 +24,30 @@ PKG_FILES = FileList[
"lib/**/*", "test/**/*", "examples/**/*", "doc/**/*", "[A-Z]*", "install.rb", "Rakefile" "lib/**/*", "test/**/*", "examples/**/*", "doc/**/*", "[A-Z]*", "install.rb", "Rakefile"
].exclude(/\bCVS\b|~$/) ].exclude(/\bCVS\b|~$/)
def run_without_aborting(*tasks)
errors = []
tasks.each do |task|
begin
Rake::Task[task].invoke
rescue Exception
errors << task
end
end
abort "Errors running #{errors.join(', ')}" if errors.any?
end
desc 'Run mysql, sqlite, and postgresql tests by default' desc 'Run mysql, sqlite, and postgresql tests by default'
task :default => :test task :default => :test
desc 'Run mysql, sqlite, and postgresql tests' desc 'Run mysql, sqlite, and postgresql tests'
task :test => defined?(JRUBY_VERSION) ? task :test do
tasks = defined?(JRUBY_VERSION) ?
%w(test_jdbcmysql test_jdbcsqlite3 test_jdbcpostgresql) : %w(test_jdbcmysql test_jdbcsqlite3 test_jdbcpostgresql) :
%w(test_mysql test_sqlite3 test_postgresql) %w(test_mysql test_sqlite3 test_postgresql)
run_without_aborting(*tasks)
end
for adapter in %w( mysql postgresql sqlite sqlite3 firebird db2 oracle sybase openbase frontbase jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ) for adapter in %w( mysql postgresql sqlite sqlite3 firebird db2 oracle sybase openbase frontbase jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb )
Rake::TestTask.new("test_#{adapter}") { |t| Rake::TestTask.new("test_#{adapter}") { |t|
@ -53,8 +69,8 @@ end
namespace :mysql do namespace :mysql do
desc 'Build the MySQL test databases' desc 'Build the MySQL test databases'
task :build_databases do task :build_databases do
%x( mysqladmin --user=#{MYSQL_DB_USER} create activerecord_unittest ) %x( echo "create DATABASE activerecord_unittest DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci " | mysql --user=#{MYSQL_DB_USER})
%x( mysqladmin --user=#{MYSQL_DB_USER} create activerecord_unittest2 ) %x( echo "create DATABASE activerecord_unittest2 DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci " | mysql --user=#{MYSQL_DB_USER})
end end
desc 'Drop the MySQL test databases' desc 'Drop the MySQL test databases'
@ -75,8 +91,8 @@ task :rebuild_mysql_databases => 'mysql:rebuild_databases'
namespace :postgresql do namespace :postgresql do
desc 'Build the PostgreSQL test databases' desc 'Build the PostgreSQL test databases'
task :build_databases do task :build_databases do
%x( createdb activerecord_unittest ) %x( createdb -E UTF8 activerecord_unittest )
%x( createdb activerecord_unittest2 ) %x( createdb -E UTF8 activerecord_unittest2 )
end end
desc 'Drop the PostgreSQL test databases' desc 'Drop the PostgreSQL test databases'
@ -176,7 +192,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.3.3' + PKG_BUILD) s.add_dependency('activesupport', '= 2.3.4' + 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"

View file

@ -0,0 +1 @@
performance.sql

View file

@ -0,0 +1,162 @@
#!/usr/bin/env ruby -KU
TIMES = (ENV['N'] || 10000).to_i
require 'rubygems'
gem 'addressable', '~>2.0'
gem 'faker', '~>0.3.1'
gem 'rbench', '~>0.2.3'
require 'addressable/uri'
require 'faker'
require 'rbench'
__DIR__ = File.dirname(__FILE__)
$:.unshift "#{__DIR__}/../lib"
require 'active_record'
conn = { :adapter => 'mysql',
:database => 'activerecord_unittest',
:username => 'rails', :password => '',
:encoding => 'utf8' }
conn[:socket] = Pathname.glob(%w[
/opt/local/var/run/mysql5/mysqld.sock
/tmp/mysqld.sock
/tmp/mysql.sock
/var/mysql/mysql.sock
/var/run/mysqld/mysqld.sock
]).find { |path| path.socket? }
ActiveRecord::Base.establish_connection(conn)
class User < ActiveRecord::Base
connection.create_table :users, :force => true do |t|
t.string :name, :email
t.timestamps
end
has_many :exhibits
end
class Exhibit < ActiveRecord::Base
connection.create_table :exhibits, :force => true do |t|
t.belongs_to :user
t.string :name
t.text :notes
t.timestamps
end
belongs_to :user
def look; attributes end
def feel; look; user.name end
def self.look(exhibits) exhibits.each { |e| e.look } end
def self.feel(exhibits) exhibits.each { |e| e.feel } end
end
sqlfile = "#{__DIR__}/performance.sql"
if File.exists?(sqlfile)
mysql_bin = %w[mysql mysql5].select { |bin| `which #{bin}`.length > 0 }
`#{mysql_bin} -u #{conn[:username]} #{"-p#{conn[:password]}" unless conn[:password].blank?} #{conn[:database]} < #{sqlfile}`
else
puts 'Generating data...'
# pre-compute the insert statements and fake data compilation,
# so the benchmarks below show the actual runtime for the execute
# method, minus the setup steps
# Using the same paragraph for all exhibits because it is very slow
# to generate unique paragraphs for all exhibits.
notes = Faker::Lorem.paragraphs.join($/)
today = Date.today
puts 'Inserting 10,000 users and exhibits...'
10_000.times do
user = User.create(
:created_at => today,
:name => Faker::Name.name,
:email => Faker::Internet.email
)
Exhibit.create(
:created_at => today,
:name => Faker::Company.name,
:user => user,
:notes => notes
)
end
mysqldump_bin = %w[mysqldump mysqldump5].select { |bin| `which #{bin}`.length > 0 }
`#{mysqldump_bin} -u #{conn[:username]} #{"-p#{conn[:password]}" unless conn[:password].blank?} #{conn[:database]} exhibits users > #{sqlfile}`
end
RBench.run(TIMES) do
column :times
column :ar
report 'Model#id', (TIMES * 100).ceil do
ar_obj = Exhibit.find(1)
ar { ar_obj.id }
end
report 'Model.new (instantiation)' do
ar { Exhibit.new }
end
report 'Model.new (setting attributes)' do
attrs = { :name => 'sam' }
ar { Exhibit.new(attrs) }
end
report 'Model.first' do
ar { Exhibit.first.look }
end
report 'Model.all limit(100)', (TIMES / 10).ceil do
ar { Exhibit.look Exhibit.all(:limit => 100) }
end
report 'Model.all limit(100) with relationship', (TIMES / 10).ceil do
ar { Exhibit.feel Exhibit.all(:limit => 100, :include => :user) }
end
report 'Model.all limit(10,000)', (TIMES / 1000).ceil do
ar { Exhibit.look Exhibit.all(:limit => 10000) }
end
exhibit = {
:name => Faker::Company.name,
:notes => Faker::Lorem.paragraphs.join($/),
:created_at => Date.today
}
report 'Model.create' do
ar { Exhibit.create(exhibit) }
end
report 'Resource#attributes=' do
attrs_first = { :name => 'sam' }
attrs_second = { :name => 'tom' }
ar { exhibit = Exhibit.new(attrs_first); exhibit.attributes = attrs_second }
end
report 'Resource#update' do
ar { Exhibit.first.update_attributes(:name => 'bob') }
end
report 'Resource#destroy' do
ar { Exhibit.first.destroy }
end
report 'Model.transaction' do
ar { Exhibit.transaction { Exhibit.new } }
end
summary 'Total'
end
ActiveRecord::Migration.drop_table "exhibits"
ActiveRecord::Migration.drop_table "users"

View file

@ -34,11 +34,13 @@ module ActiveRecord
end end
end end
class HasManyThroughCantAssociateThroughHasManyReflection < ActiveRecordError #:nodoc: class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc:
def initialize(owner, reflection) def initialize(owner, reflection)
super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.") super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.")
end end
end end
HasManyThroughCantAssociateThroughHasManyReflection = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('ActiveRecord::HasManyThroughCantAssociateThroughHasManyReflection', 'ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection')
class HasManyThroughCantAssociateNewRecords < ActiveRecordError #:nodoc: class HasManyThroughCantAssociateNewRecords < ActiveRecordError #:nodoc:
def initialize(owner, reflection) def initialize(owner, reflection)
super("Cannot associate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to create the has_many :through record associating them.") super("Cannot associate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to create the has_many :through record associating them.")
@ -410,6 +412,32 @@ module ActiveRecord
# @firm.clients.collect { |c| c.invoices }.flatten # select all invoices for all clients of the firm # @firm.clients.collect { |c| c.invoices }.flatten # select all invoices for all clients of the firm
# @firm.invoices # selects all invoices by going through the Client join model. # @firm.invoices # selects all invoices by going through the Client join model.
# #
# Similarly you can go through a +has_one+ association on the join model:
#
# class Group < ActiveRecord::Base
# has_many :users
# has_many :avatars, :through => :users
# end
#
# class User < ActiveRecord::Base
# belongs_to :group
# has_one :avatar
# end
#
# class Avatar < ActiveRecord::Base
# belongs_to :user
# end
#
# @group = Group.first
# @group.users.collect { |u| u.avatar }.flatten # select all avatars for all users in the group
# @group.avatars # selects all avatars by going through the User join model.
#
# An important caveat with going through +has_one+ or +has_many+ associations on the join model is that these associations are
# *read-only*. For example, the following would not work following the previous example:
#
# @group.avatars << Avatar.new # this would work if User belonged_to Avatar rather than the other way around.
# @group.avatars.delete(@group.avatars.last) # so would this
#
# === Polymorphic Associations # === Polymorphic Associations
# #
# Polymorphic associations on models are not restricted on what types of models they can be associated with. Rather, they # Polymorphic associations on models are not restricted on what types of models they can be associated with. Rather, they
@ -759,7 +787,7 @@ module ActiveRecord
# [:through] # [:through]
# Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt> # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt>
# are ignored, as the association uses the source reflection. You can only use a <tt>:through</tt> query through a <tt>belongs_to</tt> # are ignored, as the association uses the source reflection. You can only use a <tt>:through</tt> query through a <tt>belongs_to</tt>
# or <tt>has_many</tt> association on the join model. # <tt>has_one</tt> or <tt>has_many</tt> association on the join model.
# [:source] # [:source]
# Specifies the source association name used by <tt>has_many :through</tt> queries. Only use it if the name cannot be # Specifies the source association name used by <tt>has_many :through</tt> queries. Only use it if the name cannot be
# inferred from the association. <tt>has_many :subscribers, :through => :subscriptions</tt> will look for either <tt>:subscribers</tt> or # inferred from the association. <tt>has_many :subscribers, :through => :subscriptions</tt> will look for either <tt>:subscribers</tt> or
@ -1241,7 +1269,11 @@ module ActiveRecord
if association_proxy_class == HasOneThroughAssociation if association_proxy_class == HasOneThroughAssociation
association.create_through_record(new_value) association.create_through_record(new_value)
if new_record?
association_instance_set(reflection.name, new_value.nil? ? nil : association)
else
self.send(reflection.name, new_value) self.send(reflection.name, new_value)
end
else else
association.replace(new_value) association.replace(new_value)
association_instance_set(reflection.name, new_value.nil? ? nil : association) association_instance_set(reflection.name, new_value.nil? ? nil : association)
@ -1293,7 +1325,7 @@ module ActiveRecord
define_method("#{reflection.name.to_s.singularize}_ids=") do |new_value| define_method("#{reflection.name.to_s.singularize}_ids=") do |new_value|
ids = (new_value || []).reject { |nid| nid.blank? } ids = (new_value || []).reject { |nid| nid.blank? }
send("#{reflection.name}=", reflection.class_name.constantize.find(ids)) send("#{reflection.name}=", reflection.klass.find(ids))
end end
end end
end end
@ -1838,7 +1870,7 @@ module ActiveRecord
descendant descendant
end.flatten.compact end.flatten.compact
remove_duplicate_results!(reflection.class_name.constantize, parent_records, associations[name]) unless parent_records.empty? remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty?
end end
end end
end end

View file

@ -208,6 +208,7 @@ module ActiveRecord
# Note that this method will _always_ remove records from the database # Note that this method will _always_ remove records from the database
# ignoring the +:dependent+ option. # ignoring the +:dependent+ option.
def destroy(*records) def destroy(*records)
records = find(records) if records.any? {|record| record.kind_of?(Fixnum) || record.kind_of?(String)}
remove_records(records) do |records, old_records| remove_records(records) do |records, old_records|
old_records.each { |record| record.destroy } old_records.each { |record| record.destroy }
end end

View file

@ -1,6 +1,11 @@
module ActiveRecord module ActiveRecord
module Associations module Associations
class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc: class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
def initialize(owner, reflection)
super
@primary_key_list = {}
end
def create(attributes = {}) def create(attributes = {})
create_record(attributes) { |record| insert_record(record) } create_record(attributes) { |record| insert_record(record) }
end end
@ -17,6 +22,12 @@ module ActiveRecord
@reflection.reset_column_information @reflection.reset_column_information
end end
def has_primary_key?
return @has_primary_key unless @has_primary_key.nil?
@has_primary_key = (ActiveRecord::Base.connection.supports_primary_key? &&
ActiveRecord::Base.connection.primary_key(@reflection.options[:join_table]))
end
protected protected
def construct_find_options!(options) def construct_find_options!(options)
options[:joins] = @join_sql options[:joins] = @join_sql
@ -29,6 +40,11 @@ module ActiveRecord
end end
def insert_record(record, force = true, validate = true) def insert_record(record, force = true, validate = true)
if has_primary_key?
raise ActiveRecord::ConfigurationError,
"Primary key is not allowed in a has_and_belongs_to_many join table (#{@reflection.options[:join_table]})."
end
if record.new_record? if record.new_record?
if force if force
record.save! record.save!

View file

@ -74,6 +74,7 @@ module ActiveRecord
"#{@reflection.primary_key_name} = NULL", "#{@reflection.primary_key_name} = NULL",
"#{@reflection.primary_key_name} = #{owner_quoted_id} AND #{@reflection.klass.primary_key} IN (#{ids})" "#{@reflection.primary_key_name} = #{owner_quoted_id} AND #{@reflection.klass.primary_key} IN (#{ids})"
) )
@owner.class.update_counters(@owner.id, cached_counter_attribute_name => -records.size) if has_cached_counter?
end end
end end

View file

@ -17,7 +17,17 @@ module ActiveRecord
def create(attrs = nil) def create(attrs = nil)
transaction do transaction do
self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.create_association } : @reflection.create_association) object = if attrs
@reflection.klass.send(:with_scope, :create => attrs) {
@reflection.create_association
}
else
@reflection.create_association
end
raise_on_type_mismatch(object)
add_record_to_target_with_callbacks(object) do |r|
insert_record(object, false)
end
object object
end end
end end
@ -44,7 +54,7 @@ module ActiveRecord
options[:select] = construct_select(options[:select]) options[:select] = construct_select(options[:select])
options[:from] ||= construct_from options[:from] ||= construct_from
options[:joins] = construct_joins(options[:joins]) options[:joins] = construct_joins(options[:joins])
options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil? options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil? && @reflection.source_reflection.options[:include]
end end
def insert_record(record, force = true, validate = true) def insert_record(record, force = true, validate = true)
@ -96,7 +106,7 @@ module ActiveRecord
# Construct attributes for :through pointing to owner and associate. # Construct attributes for :through pointing to owner and associate.
def construct_join_attributes(associate) def construct_join_attributes(associate)
# TODO: revist this to allow it for deletion, supposing dependent option is supported # TODO: revist this to allow it for deletion, supposing dependent option is supported
raise ActiveRecord::HasManyThroughCantAssociateThroughHasManyReflection.new(@owner, @reflection) if @reflection.source_reflection.macro == :has_many raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro)
join_attributes = construct_owner_attributes(@reflection.through_reflection).merge(@reflection.source_reflection.primary_key_name => associate.id) join_attributes = construct_owner_attributes(@reflection.through_reflection).merge(@reflection.source_reflection.primary_key_name => associate.id)
if @reflection.options[:source_type] if @reflection.options[:source_type]
join_attributes.merge!(@reflection.source_reflection.options[:foreign_type] => associate.class.base_class.name.to_s) join_attributes.merge!(@reflection.source_reflection.options[:foreign_type] => associate.class.base_class.name.to_s)

View file

@ -9,8 +9,14 @@ module ActiveRecord
if current_object if current_object
new_value ? current_object.update_attributes(construct_join_attributes(new_value)) : current_object.destroy new_value ? current_object.update_attributes(construct_join_attributes(new_value)) : current_object.destroy
elsif new_value
if @owner.new_record?
self.target = new_value
through_association = @owner.send(:association_instance_get, @reflection.through_reflection.name)
through_association.build(construct_join_attributes(new_value))
else else
@owner.send(@reflection.through_reflection.name, klass.send(:create, construct_join_attributes(new_value))) if new_value @owner.send(@reflection.through_reflection.name, klass.create(construct_join_attributes(new_value)))
end
end end
end end

View file

@ -249,9 +249,10 @@ module ActiveRecord
unless valid = association.valid? unless valid = association.valid?
if reflection.options[:autosave] if reflection.options[:autosave]
unless association.marked_for_destruction? unless association.marked_for_destruction?
association.errors.each do |attribute, message| association.errors.each_error do |attribute, error|
attribute = "#{reflection.name}_#{attribute}" error = error.dup
errors.add(attribute, message) unless errors.on(attribute) error.attribute = "#{reflection.name}_#{attribute}"
errors.add(error) unless errors.on(error.attribute)
end end
end end
else else

View file

@ -1364,7 +1364,7 @@ module ActiveRecord #:nodoc:
end end
defaults << options[:default] if options[:default] defaults << options[:default] if options[:default]
defaults.flatten! defaults.flatten!
defaults << attribute_key_name.humanize defaults << attribute_key_name.to_s.humanize
options[:count] ||= 1 options[:count] ||= 1
I18n.translate(defaults.shift, options.merge(:default => defaults, :scope => [:activerecord, :attributes])) I18n.translate(defaults.shift, options.merge(:default => defaults, :scope => [:activerecord, :attributes]))
end end
@ -2294,20 +2294,24 @@ module ActiveRecord #:nodoc:
# And for value objects on a composed_of relationship: # And for value objects on a composed_of relationship:
# { :address => Address.new("123 abc st.", "chicago") } # { :address => Address.new("123 abc st.", "chicago") }
# # => "address_street='123 abc st.' and address_city='chicago'" # # => "address_street='123 abc st.' and address_city='chicago'"
def sanitize_sql_hash_for_conditions(attrs, table_name = quoted_table_name) def sanitize_sql_hash_for_conditions(attrs, default_table_name = quoted_table_name)
attrs = expand_hash_conditions_for_aggregates(attrs) attrs = expand_hash_conditions_for_aggregates(attrs)
conditions = attrs.map do |attr, value| conditions = attrs.map do |attr, value|
table_name = default_table_name
unless value.is_a?(Hash) unless value.is_a?(Hash)
attr = attr.to_s attr = attr.to_s
# Extract table name from qualified attribute names. # Extract table name from qualified attribute names.
if attr.include?('.') if attr.include?('.')
table_name, attr = attr.split('.', 2) attr_table_name, attr = attr.split('.', 2)
table_name = connection.quote_table_name(table_name) attr_table_name = connection.quote_table_name(attr_table_name)
else
attr_table_name = table_name
end end
attribute_condition("#{table_name}.#{connection.quote_column_name(attr)}", value) attribute_condition("#{attr_table_name}.#{connection.quote_column_name(attr)}", value)
else else
sanitize_sql_hash_for_conditions(value, connection.quote_table_name(attr.to_s)) sanitize_sql_hash_for_conditions(value, connection.quote_table_name(attr.to_s))
end end
@ -3028,16 +3032,22 @@ module ActiveRecord #:nodoc:
def execute_callstack_for_multiparameter_attributes(callstack) def execute_callstack_for_multiparameter_attributes(callstack)
errors = [] errors = []
callstack.each do |name, values| callstack.each do |name, values_with_empty_parameters|
begin begin
klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass
# in order to allow a date to be set without a year, we must keep the empty values.
# Otherwise, we wouldn't be able to distinguish it from a date with an empty day.
values = values_with_empty_parameters.reject(&:nil?)
if values.empty? if values.empty?
send(name + "=", nil) send(name + "=", nil)
else else
value = if Time == klass value = if Time == klass
instantiate_time_object(name, values) instantiate_time_object(name, values)
elsif Date == klass elsif Date == klass
begin begin
values = values_with_empty_parameters.collect do |v| v.nil? ? 1 : v end
Date.new(*values) Date.new(*values)
rescue ArgumentError => ex # if Date.new raises an exception on an invalid date rescue ArgumentError => ex # if Date.new raises an exception on an invalid date
instantiate_time_object(name, values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates instantiate_time_object(name, values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
@ -3065,10 +3075,8 @@ module ActiveRecord #:nodoc:
attribute_name = multiparameter_name.split("(").first attribute_name = multiparameter_name.split("(").first
attributes[attribute_name] = [] unless attributes.include?(attribute_name) attributes[attribute_name] = [] unless attributes.include?(attribute_name)
unless value.empty? parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
attributes[attribute_name] << attributes[attribute_name] << [ find_parameter_position(multiparameter_name), parameter_value ]
[ find_parameter_position(multiparameter_name), type_cast_attribute_value(multiparameter_name, value) ]
end
end end
attributes.each { |name, values| attributes[name] = values.sort_by{ |v| v.first }.collect { |v| v.last } } attributes.each { |name, values| attributes[name] = values.sort_by{ |v| v.first }.collect { |v| v.last } }

View file

@ -190,6 +190,8 @@ module ActiveRecord
sql << ", #{options[:group_field]} AS #{options[:group_alias]}" if options[:group] sql << ", #{options[:group_field]} AS #{options[:group_alias]}" if options[:group]
if options[:from] if options[:from]
sql << " FROM #{options[:from]} " sql << " FROM #{options[:from]} "
elsif scope && scope[:from] && !use_workaround
sql << " FROM #{scope[:from]} "
else else
sql << " FROM (SELECT #{distinct}#{column_name}" if use_workaround sql << " FROM (SELECT #{distinct}#{column_name}" if use_workaround
sql << " FROM #{connection.quote_table_name(table_name)} " sql << " FROM #{connection.quote_table_name(table_name)} "

View file

@ -277,7 +277,6 @@ module ActiveRecord
add_column_options!(column_sql, column_options) unless type.to_sym == :primary_key add_column_options!(column_sql, column_options) unless type.to_sym == :primary_key
column_sql column_sql
end end
alias to_s :to_sql
private private
@ -316,6 +315,20 @@ module ActiveRecord
@base = base @base = base
end end
#Handles non supported datatypes - e.g. XML
def method_missing(symbol, *args)
if symbol.to_s == 'xml'
xml_column_fallback(args)
end
end
def xml_column_fallback(*args)
case @base.adapter_name.downcase
when 'sqlite', 'mysql'
options = args.extract_options!
column(args[0], :text, options)
end
end
# Appends a primary key definition to the table definition. # Appends a primary key definition to the table definition.
# Can be called multiple times, but this is probably not a good idea. # Can be called multiple times, but this is probably not a good idea.
def primary_key(name) def primary_key(name)
@ -508,7 +521,7 @@ module ActiveRecord
# concatenated together. This string can then be prepended and appended to # concatenated together. This string can then be prepended and appended to
# to generate the final SQL to create the table. # to generate the final SQL to create the table.
def to_sql def to_sql
@columns * ', ' @columns.map(&:to_sql) * ', '
end end
private private
@ -706,3 +719,4 @@ module ActiveRecord
end end
end end

View file

@ -99,7 +99,7 @@ module ActiveRecord
# See also TableDefinition#column for details on how to create columns. # See also TableDefinition#column for details on how to create columns.
def create_table(table_name, options = {}) def create_table(table_name, options = {})
table_definition = TableDefinition.new(self) table_definition = TableDefinition.new(self)
table_definition.primary_key(options[:primary_key] || Base.get_primary_key(table_name)) unless options[:id] == false table_definition.primary_key(options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize)) unless options[:id] == false
yield table_definition yield table_definition
@ -321,7 +321,7 @@ module ActiveRecord
schema_migrations_table.column :version, :string, :null => false schema_migrations_table.column :version, :string, :null => false
end end
add_index sm_table, :version, :unique => true, add_index sm_table, :version, :unique => true,
:name => 'unique_schema_migrations' :name => "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}"
# Backwards-compatibility: if we find schema_info, assume we've # Backwards-compatibility: if we find schema_info, assume we've
# migrated up to that point: # migrated up to that point:

View file

@ -54,6 +54,13 @@ module ActiveRecord
false false
end end
# Can this adapter determine the primary key for tables not attached
# to an ActiveRecord class, such as join tables? Backend specific, as
# the abstract adapter always returns +false+.
def supports_primary_key?
false
end
# Does this adapter support using DISTINCT within COUNT? This is +true+ # Does this adapter support using DISTINCT within COUNT? This is +true+
# for all adapters except sqlite. # for all adapters except sqlite.
def supports_count_distinct? def supports_count_distinct?

View file

@ -52,12 +52,7 @@ module ActiveRecord
socket = config[:socket] socket = config[:socket]
username = config[:username] ? config[:username].to_s : 'root' username = config[:username] ? config[:username].to_s : 'root'
password = config[:password].to_s password = config[:password].to_s
if config.has_key?(:database)
database = config[:database] database = config[:database]
else
raise ArgumentError, "No database specified. Missing argument: database."
end
# Require the MySQL driver and define Mysql::Result.all_hashes # Require the MySQL driver and define Mysql::Result.all_hashes
unless defined? Mysql unless defined? Mysql
@ -80,7 +75,7 @@ module ActiveRecord
module ConnectionAdapters module ConnectionAdapters
class MysqlColumn < Column #:nodoc: class MysqlColumn < Column #:nodoc:
def extract_default(default) def extract_default(default)
if type == :binary || type == :text if sql_type =~ /blob/i || type == :text
if default.blank? if default.blank?
return null ? nil : '' return null ? nil : ''
else else
@ -94,7 +89,7 @@ module ActiveRecord
end end
def has_default? def has_default?
return false if type == :binary || type == :text #mysql forbids defaults on blob and text columns return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns
super super
end end
@ -212,6 +207,10 @@ module ActiveRecord
true true
end end
def supports_primary_key? #:nodoc:
true
end
def supports_savepoints? #:nodoc: def supports_savepoints? #:nodoc:
true true
end end
@ -554,6 +553,12 @@ module ActiveRecord
keys.length == 1 ? [keys.first, nil] : nil keys.length == 1 ? [keys.first, nil] : nil
end end
# Returns just a table's primary key
def primary_key(table)
pk_and_sequence = pk_and_sequence_for(table)
pk_and_sequence && pk_and_sequence.first
end
def case_sensitive_equality_operator def case_sensitive_equality_operator
"= BINARY" "= BINARY"
end end
@ -573,6 +578,10 @@ module ActiveRecord
@connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) @connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher])
end end
@connection.options(Mysql::OPT_CONNECT_TIMEOUT, @config[:connect_timeout]) if @config[:connect_timeout]
@connection.options(Mysql::OPT_READ_TIMEOUT, @config[:read_timeout]) if @config[:read_timeout]
@connection.options(Mysql::OPT_WRITE_TIMEOUT, @config[:write_timeout]) if @config[:write_timeout]
@connection.real_connect(*@connection_options) @connection.real_connect(*@connection_options)
# reconnect must be set after real_connect is called, because real_connect sets it to false internally # reconnect must be set after real_connect is called, because real_connect sets it to false internally

View file

@ -39,6 +39,12 @@ module ActiveRecord
end end
module ConnectionAdapters module ConnectionAdapters
class TableDefinition
def xml(*args)
options = args.extract_options!
column(args[0], 'xml', options)
end
end
# PostgreSQL-specific extensions to column definitions in a table. # PostgreSQL-specific extensions to column definitions in a table.
class PostgreSQLColumn < Column #:nodoc: class PostgreSQLColumn < Column #:nodoc:
# Instantiates a new PostgreSQL column definition in a table. # Instantiates a new PostgreSQL column definition in a table.
@ -99,7 +105,7 @@ module ActiveRecord
:string :string
# XML type # XML type
when /^xml$/ when /^xml$/
:string :xml
# Arrays # Arrays
when /^\D+\[\]$/ when /^\D+\[\]$/
:string :string
@ -194,7 +200,8 @@ module ActiveRecord
:time => { :name => "time" }, :time => { :name => "time" },
:date => { :name => "date" }, :date => { :name => "date" },
:binary => { :name => "bytea" }, :binary => { :name => "bytea" },
:boolean => { :name => "boolean" } :boolean => { :name => "boolean" },
:xml => { :name => "xml" }
} }
# Returns 'PostgreSQL' as adapter name for identification purposes. # Returns 'PostgreSQL' as adapter name for identification purposes.
@ -249,6 +256,11 @@ module ActiveRecord
true true
end end
# Does PostgreSQL support finding primary key on non-ActiveRecord tables?
def supports_primary_key? #:nodoc:
true
end
# Does PostgreSQL support standard conforming strings? # Does PostgreSQL support standard conforming strings?
def supports_standard_conforming_strings? def supports_standard_conforming_strings?
# Temporarily set the client message level above error to prevent unintentional # Temporarily set the client message level above error to prevent unintentional
@ -364,7 +376,7 @@ module ActiveRecord
if value.kind_of?(String) && column && column.type == :binary if value.kind_of?(String) && column && column.type == :binary
"#{quoted_string_prefix}'#{escape_bytea(value)}'" "#{quoted_string_prefix}'#{escape_bytea(value)}'"
elsif value.kind_of?(String) && column && column.sql_type =~ /^xml$/ elsif value.kind_of?(String) && column && column.sql_type =~ /^xml$/
"xml '#{quote_string(value)}'" "xml E'#{quote_string(value)}'"
elsif value.kind_of?(Numeric) && column && column.sql_type =~ /^money$/ elsif value.kind_of?(Numeric) && column && column.sql_type =~ /^money$/
# Not truly string input, so doesn't require (or allow) escape string syntax. # Not truly string input, so doesn't require (or allow) escape string syntax.
"'#{value.to_s}'" "'#{value.to_s}'"
@ -810,6 +822,12 @@ module ActiveRecord
nil nil
end end
# Returns just a table's primary key
def primary_key(table)
pk_and_sequence = pk_and_sequence_for(table)
pk_and_sequence && pk_and_sequence.first
end
# Renames a table. # Renames a table.
def rename_table(name, new_name) def rename_table(name, new_name)
execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}"
@ -1092,3 +1110,4 @@ module ActiveRecord
end end
end end
end end

View file

@ -16,6 +16,10 @@ module ActiveRecord
db.results_as_hash = true if defined? SQLite::Version db.results_as_hash = true if defined? SQLite::Version
db.type_translation = false db.type_translation = false
message = "Support for SQLite2Adapter and DeprecatedSQLiteAdapter has been removed from Rails 3. "
message << "You should migrate to SQLite 3+ or use the plugin from git://github.com/rails/sqlite2_adapter.git with Rails 3."
ActiveSupport::Deprecation.warn(message)
# "Downgrade" deprecated sqlite API # "Downgrade" deprecated sqlite API
if SQLite.const_defined?(:Version) if SQLite.const_defined?(:Version)
ConnectionAdapters::SQLite2Adapter.new(db, logger, config) ConnectionAdapters::SQLite2Adapter.new(db, logger, config)
@ -27,6 +31,10 @@ module ActiveRecord
private private
def parse_sqlite_config!(config) def parse_sqlite_config!(config)
if config.include?(:dbfile)
ActiveSupport::Deprecation.warn "Please update config/database.yml to use 'database' instead of 'dbfile'"
end
config[:database] ||= config[:dbfile] config[:database] ||= config[:dbfile]
# Require database. # Require database.
unless config[:database] unless config[:database]
@ -104,6 +112,10 @@ module ActiveRecord
true true
end end
def supports_primary_key? #:nodoc:
true
end
def requires_reloading? def requires_reloading?
true true
end end

View file

@ -143,7 +143,7 @@ module ActiveRecord
if partial_updates? if partial_updates?
# Serialized attributes should always be written in case they've been # Serialized attributes should always be written in case they've been
# changed in place. # changed in place.
update_without_dirty(changed | self.class.serialized_attributes.keys) update_without_dirty(changed | (attributes.keys & self.class.serialized_attributes.keys))
else else
update_without_dirty update_without_dirty
end end

View file

@ -621,7 +621,8 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
targets.each do |target| targets.each do |target|
join_fixtures["#{label}_#{target}"] = Fixture.new( join_fixtures["#{label}_#{target}"] = Fixture.new(
{ association.primary_key_name => row[primary_key_name], { association.primary_key_name => row[primary_key_name],
association.association_foreign_key => Fixtures.identify(target) }, nil) association.association_foreign_key => Fixtures.identify(target) },
nil, @connection)
end end
end end
end end
@ -710,7 +711,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
raise Fixture::FormatError, "Bad data for #{@class_name} fixture named #{name} (nil)" raise Fixture::FormatError, "Bad data for #{@class_name} fixture named #{name} (nil)"
end end
self[name] = Fixture.new(data, model_class) self[name] = Fixture.new(data, model_class, @connection)
end end
end end
end end
@ -723,7 +724,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
reader.each do |row| reader.each do |row|
data = {} data = {}
row.each_with_index { |cell, j| data[header[j].to_s.strip] = cell.to_s.strip } row.each_with_index { |cell, j| data[header[j].to_s.strip] = cell.to_s.strip }
self["#{@class_name.to_s.underscore}_#{i+=1}"] = Fixture.new(data, model_class) self["#{@class_name.to_s.underscore}_#{i+=1}"] = Fixture.new(data, model_class, @connection)
end end
end end
@ -761,7 +762,8 @@ class Fixture #:nodoc:
attr_reader :model_class attr_reader :model_class
def initialize(fixture, model_class) def initialize(fixture, model_class, connection = ActiveRecord::Base.connection)
@connection = connection
@fixture = fixture @fixture = fixture
@model_class = model_class.is_a?(Class) ? model_class : model_class.constantize rescue nil @model_class = model_class.is_a?(Class) ? model_class : model_class.constantize rescue nil
end end
@ -783,14 +785,14 @@ class Fixture #:nodoc:
end end
def key_list def key_list
columns = @fixture.keys.collect{ |column_name| ActiveRecord::Base.connection.quote_column_name(column_name) } columns = @fixture.keys.collect{ |column_name| @connection.quote_column_name(column_name) }
columns.join(", ") columns.join(", ")
end end
def value_list def value_list
list = @fixture.inject([]) do |fixtures, (key, value)| list = @fixture.inject([]) do |fixtures, (key, value)|
col = model_class.columns_hash[key] if model_class.respond_to?(:ancestors) && model_class.ancestors.include?(ActiveRecord::Base) col = model_class.columns_hash[key] if model_class.respond_to?(:ancestors) && model_class.ancestors.include?(ActiveRecord::Base)
fixtures << ActiveRecord::Base.connection.quote(value, col).gsub('[^\]\\n', "\n").gsub('[^\]\\r', "\r") fixtures << @connection.quote(value, col).gsub('[^\]\\n', "\n").gsub('[^\]\\r', "\r")
end end
list * ', ' list * ', '
end end

View file

@ -10,7 +10,7 @@ module I18n
protected protected
def interpolate_with_deprecated_syntax(locale, string, values = {}) def interpolate_with_deprecated_syntax(locale, string, values = {})
return string unless string.is_a?(String) return string unless string.is_a?(String) && !values.empty?
string = string.gsub(/%d|%s/) do |s| string = string.gsub(/%d|%s/) do |s|
instead = DEPRECATED_INTERPOLATORS[s] instead = DEPRECATED_INTERPOLATORS[s]

View file

@ -23,8 +23,12 @@ en:
less_than_or_equal_to: "must be less than or equal to {{count}}" less_than_or_equal_to: "must be less than or equal to {{count}}"
odd: "must be odd" odd: "must be odd"
even: "must be even" even: "must be even"
record_invalid: "Validation failed: {{errors}}"
# Append your own errors here or at the model/attributes scope. # Append your own errors here or at the model/attributes scope.
full_messages:
format: "{{attribute}} {{message}}"
# You can define own errors for models or model attributes. # You can define own errors for models or model attributes.
# The values :model, :attribute and :value are always available for interpolation. # The values :model, :attribute and :value are always available for interpolation.
# #

View file

@ -89,12 +89,7 @@ module ActiveRecord
when Hash when Hash
options options
when Proc when Proc
case parent_scope
when Scope
with_scope(:find => parent_scope.proxy_options) { options.call(*args) }
else
options.call(*args) options.call(*args)
end
end, &block) end, &block)
end end
(class << self; self end).instance_eval do (class << self; self end).instance_eval do

View file

@ -297,7 +297,7 @@ module ActiveRecord
raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection) raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection)
end end
unless [:belongs_to, :has_many].include?(source_reflection.macro) && source_reflection.options[:through].nil? unless [:belongs_to, :has_many, :has_one].include?(source_reflection.macro) && source_reflection.options[:through].nil?
raise HasManyThroughSourceAssociationMacroError.new(self) raise HasManyThroughSourceAssociationMacroError.new(self)
end end
end end

View file

@ -84,7 +84,6 @@ HEADER
elsif @connection.respond_to?(:primary_key) elsif @connection.respond_to?(:primary_key)
pk = @connection.primary_key(table) pk = @connection.primary_key(table)
end end
pk ||= 'id'
tbl.print " create_table #{table.inspect}" tbl.print " create_table #{table.inspect}"
if columns.detect { |c| c.name == pk } if columns.detect { |c| c.name == pk }

View file

@ -74,12 +74,14 @@ module ActiveRecord #:nodoc:
# {"comments": [{"body": "Don't think too hard"}], # {"comments": [{"body": "Don't think too hard"}],
# "title": "So I was thinking"}]} # "title": "So I was thinking"}]}
def to_json(options = {}) def to_json(options = {})
hash = Serializer.new(self, options).serializable_record super
hash = { self.class.model_name.element => hash } if include_root_in_json
ActiveSupport::JSON.encode(hash)
end end
def as_json(options = nil) self end #:nodoc: def as_json(options = nil) #:nodoc:
hash = Serializer.new(self, options).serializable_record
hash = { self.class.model_name.element => hash } if include_root_in_json
hash
end
def from_json(json) def from_json(json)
self.attributes = ActiveSupport::JSON.decode(json) self.attributes = ActiveSupport::JSON.decode(json)

View file

@ -178,7 +178,7 @@ module ActiveRecord #:nodoc:
end end
def root def root
root = (options[:root] || @record.class.to_s.underscore).to_s root = (options[:root] || @record.class.model_name.singular).to_s
reformat_name(root) reformat_name(root)
end end
@ -320,7 +320,11 @@ module ActiveRecord #:nodoc:
protected protected
def compute_type def compute_type
type = @record.class.serialized_attributes.has_key?(name) ? :yaml : @record.class.columns_hash[name].type type = if @record.class.serialized_attributes.has_key?(name)
:yaml
else
@record.class.columns_hash[name].try(:type)
end
case type case type
when :text when :text

View file

@ -10,7 +10,114 @@ module ActiveRecord
attr_reader :record attr_reader :record
def initialize(record) def initialize(record)
@record = record @record = record
super("Validation failed: #{@record.errors.full_messages.join(", ")}") errors = @record.errors.full_messages.join(I18n.t('support.array.words_connector', :default => ', '))
super(I18n.t('activerecord.errors.messages.record_invalid', :errors => errors))
end
end
class Error
attr_accessor :base, :attribute, :type, :message, :options
def initialize(base, attribute, type = nil, options = {})
self.base = base
self.attribute = attribute
self.type = type || :invalid
self.options = options
self.message = options.delete(:message) || self.type
end
def message
generate_message(@message, options.dup)
end
def full_message
attribute.to_s == 'base' ? message : generate_full_message(message, options.dup)
end
alias :to_s :message
def value
@base.respond_to?(attribute) ? @base.send(attribute) : nil
end
protected
# Translates an error message in it's default scope (<tt>activerecord.errrors.messages</tt>).
# Error messages are first looked up in <tt>models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>, if it's not there,
# it's looked up in <tt>models.MODEL.MESSAGE</tt> and if that is not there it returns the translation of the
# default message (e.g. <tt>activerecord.errors.messages.MESSAGE</tt>). The translated model name,
# translated attribute name and the value are available for interpolation.
#
# When using inheritence in your models, it will check all the inherited models too, but only if the model itself
# hasn't been found. Say you have <tt>class Admin < User; end</tt> and you wanted the translation for the <tt>:blank</tt>
# error +message+ for the <tt>title</tt> +attribute+, it looks for these translations:
#
# <ol>
# <li><tt>activerecord.errors.models.admin.attributes.title.blank</tt></li>
# <li><tt>activerecord.errors.models.admin.blank</tt></li>
# <li><tt>activerecord.errors.models.user.attributes.title.blank</tt></li>
# <li><tt>activerecord.errors.models.user.blank</tt></li>
# <li><tt>activerecord.errors.messages.blank</tt></li>
# <li>any default you provided through the +options+ hash (in the activerecord.errors scope)</li>
# </ol>
def generate_message(message, options = {})
keys = @base.class.self_and_descendants_from_active_record.map do |klass|
[ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}",
:"models.#{klass.name.underscore}.#{message}" ]
end.flatten
keys << options.delete(:default)
keys << :"messages.#{message}"
keys << message if message.is_a?(String)
keys << @type unless @type == message
keys.compact!
options.reverse_merge! :default => keys,
:scope => [:activerecord, :errors],
:model => @base.class.human_name,
:attribute => @base.class.human_attribute_name(attribute.to_s),
:value => value
I18n.translate(keys.shift, options)
end
# Wraps an error message into a full_message format.
#
# The default full_message format for any locale is <tt>"{{attribute}} {{message}}"</tt>.
# One can specify locale specific default full_message format by storing it as a
# translation for the key <tt>:"activerecord.errors.full_messages.format"</tt>.
#
# Additionally one can specify a validation specific error message format by
# storing a translation for <tt>:"activerecord.errors.full_messages.[message_key]"</tt>.
# E.g. the full_message format for any validation that uses :blank as a message
# key (such as validates_presence_of) can be stored to <tt>:"activerecord.errors.full_messages.blank".</tt>
#
# Because the message key used by a validation can be overwritten on the
# <tt>validates_*</tt> class macro level one can customize the full_message format for
# any particular validation:
#
# # app/models/article.rb
# class Article < ActiveRecord::Base
# validates_presence_of :title, :message => :"title.blank"
# end
#
# # config/locales/en.yml
# en:
# activerecord:
# errors:
# full_messages:
# title:
# blank: This title is screwed!
def generate_full_message(message, options = {})
options.reverse_merge! :message => self.message,
:model => @base.class.human_name,
:attribute => @base.class.human_attribute_name(attribute.to_s),
:value => value
key = :"full_messages.#{@message}"
defaults = [:'full_messages.format', '{{attribute}} {{message}}']
I18n.t(key, options.merge(:default => defaults, :scope => [:activerecord, :errors]))
end end
end end
@ -43,11 +150,19 @@ module ActiveRecord
# error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>. # error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>.
# If no +messsage+ is supplied, :invalid is assumed. # If no +messsage+ is supplied, :invalid is assumed.
# If +message+ is a Symbol, it will be translated, using the appropriate scope (see translate_error). # If +message+ is a Symbol, it will be translated, using the appropriate scope (see translate_error).
def add(attribute, message = nil, options = {}) # def add(attribute, message = nil, options = {})
message ||= :invalid # message ||= :invalid
message = generate_message(attribute, message, options) if message.is_a?(Symbol) # message = generate_message(attribute, message, options)) if message.is_a?(Symbol)
# @errors[attribute.to_s] ||= []
# @errors[attribute.to_s] << message
# end
def add(error_or_attr, message = nil, options = {})
error, attribute = error_or_attr.is_a?(Error) ? [error_or_attr, error_or_attr.attribute] : [nil, error_or_attr]
options[:message] = options.delete(:default) if options.has_key?(:default)
@errors[attribute.to_s] ||= [] @errors[attribute.to_s] ||= []
@errors[attribute.to_s] << message @errors[attribute.to_s] << (error || Error.new(@base, attribute, message, options))
end end
# Will add an error message to each of the attributes in +attributes+ that is empty. # Will add an error message to each of the attributes in +attributes+ that is empty.
@ -67,49 +182,6 @@ module ActiveRecord
end end
end end
# Translates an error message in it's default scope (<tt>activerecord.errrors.messages</tt>).
# Error messages are first looked up in <tt>models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>, if it's not there,
# it's looked up in <tt>models.MODEL.MESSAGE</tt> and if that is not there it returns the translation of the
# default message (e.g. <tt>activerecord.errors.messages.MESSAGE</tt>). The translated model name,
# translated attribute name and the value are available for interpolation.
#
# When using inheritence in your models, it will check all the inherited models too, but only if the model itself
# hasn't been found. Say you have <tt>class Admin < User; end</tt> and you wanted the translation for the <tt>:blank</tt>
# error +message+ for the <tt>title</tt> +attribute+, it looks for these translations:
#
# <ol>
# <li><tt>activerecord.errors.models.admin.attributes.title.blank</tt></li>
# <li><tt>activerecord.errors.models.admin.blank</tt></li>
# <li><tt>activerecord.errors.models.user.attributes.title.blank</tt></li>
# <li><tt>activerecord.errors.models.user.blank</tt></li>
# <li><tt>activerecord.errors.messages.blank</tt></li>
# <li>any default you provided through the +options+ hash (in the activerecord.errors scope)</li>
# </ol>
def generate_message(attribute, message = :invalid, options = {})
message, options[:default] = options[:default], message if options[:default].is_a?(Symbol)
defaults = @base.class.self_and_descendants_from_active_record.map do |klass|
[ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}",
:"models.#{klass.name.underscore}.#{message}" ]
end
defaults << options.delete(:default)
defaults = defaults.compact.flatten << :"messages.#{message}"
key = defaults.shift
value = @base.respond_to?(attribute) ? @base.send(attribute) : nil
options = { :default => defaults,
:model => @base.class.human_name,
:attribute => @base.class.human_attribute_name(attribute.to_s),
:value => value,
:scope => [:activerecord, :errors]
}.merge(options)
I18n.translate(key, options)
end
# Returns true if the specified +attribute+ has errors associated with it. # Returns true if the specified +attribute+ has errors associated with it.
# #
# class Company < ActiveRecord::Base # class Company < ActiveRecord::Base
@ -138,8 +210,9 @@ module ActiveRecord
# company.errors.on(:email) # => "can't be blank" # company.errors.on(:email) # => "can't be blank"
# company.errors.on(:address) # => nil # company.errors.on(:address) # => nil
def on(attribute) def on(attribute)
errors = @errors[attribute.to_s] attribute = attribute.to_s
return nil if errors.nil? return nil unless @errors.has_key?(attribute)
errors = @errors[attribute].map(&:to_s)
errors.size == 1 ? errors.first : errors errors.size == 1 ? errors.first : errors
end end
@ -163,7 +236,11 @@ module ActiveRecord
# # name - can't be blank # # name - can't be blank
# # address - can't be blank # # address - can't be blank
def each def each
@errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } } @errors.each_key { |attr| @errors[attr].each { |error| yield attr, error.message } }
end
def each_error
@errors.each_key { |attr| @errors[attr].each { |error| yield attr, error } }
end end
# Yields each full error message added. So <tt>Person.errors.add("first_name", "can't be empty")</tt> will be returned # Yields each full error message added. So <tt>Person.errors.add("first_name", "can't be empty")</tt> will be returned
@ -194,22 +271,10 @@ module ActiveRecord
# company.errors.full_messages # => # company.errors.full_messages # =>
# ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"] # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"]
def full_messages(options = {}) def full_messages(options = {})
full_messages = [] @errors.values.inject([]) do |full_messages, errors|
full_messages + errors.map { |error| error.full_message }
@errors.each_key do |attr|
@errors[attr].each do |message|
next unless message
if attr == "base"
full_messages << message
else
attr_name = @base.class.human_attribute_name(attr)
full_messages << attr_name + I18n.t('activerecord.errors.format.separator', :default => ' ') + message
end end
end end
end
full_messages
end
# Returns true if no errors have been added. # Returns true if no errors have been added.
def empty? def empty?
@ -255,6 +320,10 @@ module ActiveRecord
end end
end end
def generate_message(attribute, message = :invalid, options = {})
ActiveSupport::Deprecation.warn("ActiveRecord::Errors#generate_message has been deprecated. Please use ActiveRecord::Error#generate_message.")
Error.new(@base, attribute, message, options).to_s
end
end end
@ -726,7 +795,7 @@ module ActiveRecord
comparison_operator = "IS ?" comparison_operator = "IS ?"
elsif column.text? elsif column.text?
comparison_operator = "#{connection.case_sensitive_equality_operator} ?" comparison_operator = "#{connection.case_sensitive_equality_operator} ?"
value = column.limit ? value.to_s[0, column.limit] : value.to_s value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s
else else
comparison_operator = "= ?" comparison_operator = "= ?"
end end

View file

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

View file

@ -63,6 +63,18 @@ class AdapterTest < ActiveRecord::TestCase
def test_show_nonexistent_variable_returns_nil def test_show_nonexistent_variable_returns_nil
assert_nil @connection.show_variable('foo_bar_baz') assert_nil @connection.show_variable('foo_bar_baz')
end end
def test_not_specifying_database_name_for_cross_database_selects
assert_nothing_raised do
ActiveRecord::Base.establish_connection({
:adapter => 'mysql',
:username => 'rails'
})
ActiveRecord::Base.connection.execute "SELECT activerecord_unittest.pirates.*, activerecord_unittest2.courses.* FROM activerecord_unittest.pirates, activerecord_unittest2.courses"
end
ActiveRecord::Base.establish_connection 'arunit'
end
end end
if current_adapter?(:PostgreSQLAdapter) if current_adapter?(:PostgreSQLAdapter)

View file

@ -249,24 +249,6 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 1, Topic.find(topic.id)[:replies_count] assert_equal 1, Topic.find(topic.id)[:replies_count]
end end
def test_belongs_to_counter_after_save
topic = Topic.create("title" => "monday night")
topic.replies.create("title" => "re: monday night", "content" => "football")
assert_equal 1, Topic.find(topic.id).send(:read_attribute, "replies_count")
topic.save
assert_equal 1, Topic.find(topic.id).send(:read_attribute, "replies_count")
end
def test_belongs_to_counter_after_update_attributes
topic = Topic.create("title" => "37s")
topic.replies.create("title" => "re: 37s", "content" => "rails")
assert_equal 1, Topic.find(topic.id).send(:read_attribute, "replies_count")
topic.update_attributes("title" => "37signals")
assert_equal 1, Topic.find(topic.id).send(:read_attribute, "replies_count")
end
def test_assignment_before_child_saved def test_assignment_before_child_saved
final_cut = Client.new("name" => "Final Cut") final_cut = Client.new("name" => "Final Cut")
firm = Firm.find(1) firm = Firm.find(1)

View file

@ -1,6 +1,6 @@
require 'cases/helper' require 'cases/helper'
require 'models/author'
require 'models/post' require 'models/post'
require 'models/author'
require 'models/comment' require 'models/comment'
require 'models/category' require 'models/category'
require 'models/categorization' require 'models/categorization'

View file

@ -0,0 +1,56 @@
require 'cases/helper'
class MyReader < ActiveRecord::Base
has_and_belongs_to_many :my_books
end
class MyBook < ActiveRecord::Base
has_and_belongs_to_many :my_readers
end
class HabtmJoinTableTest < ActiveRecord::TestCase
def setup
ActiveRecord::Base.connection.create_table :my_books, :force => true do |t|
t.string :name
end
assert ActiveRecord::Base.connection.table_exists?(:my_books)
ActiveRecord::Base.connection.create_table :my_readers, :force => true do |t|
t.string :name
end
assert ActiveRecord::Base.connection.table_exists?(:my_readers)
ActiveRecord::Base.connection.create_table :my_books_my_readers, :force => true do |t|
t.integer :my_book_id
t.integer :my_reader_id
end
assert ActiveRecord::Base.connection.table_exists?(:my_books_my_readers)
end
def teardown
ActiveRecord::Base.connection.drop_table :my_books
ActiveRecord::Base.connection.drop_table :my_readers
ActiveRecord::Base.connection.drop_table :my_books_my_readers
end
uses_transaction :test_should_raise_exception_when_join_table_has_a_primary_key
def test_should_raise_exception_when_join_table_has_a_primary_key
if ActiveRecord::Base.connection.supports_primary_key?
assert_raise ActiveRecord::ConfigurationError do
jaime = MyReader.create(:name=>"Jaime")
jaime.my_books << MyBook.create(:name=>'Great Expectations')
end
end
end
uses_transaction :test_should_cache_result_of_primary_key_check
def test_should_cache_result_of_primary_key_check
if ActiveRecord::Base.connection.supports_primary_key?
ActiveRecord::Base.connection.stubs(:primary_key).with('my_books_my_readers').returns(false).once
weaz = MyReader.create(:name=>'Weaz')
weaz.my_books << MyBook.create(:name=>'Great Expectations')
weaz.my_books << MyBook.create(:name=>'Greater Expectations')
end
end
end

View file

@ -10,11 +10,12 @@ require 'models/author'
require 'models/comment' require 'models/comment'
require 'models/person' require 'models/person'
require 'models/reader' require 'models/reader'
require 'models/tagging'
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, :readers :people, :posts, :readers, :taggings
def setup def setup
Client.destroyed_client_ids.clear Client.destroyed_client_ids.clear
@ -279,6 +280,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal client2, firm.clients.find(:first, :conditions => ["#{QUOTED_TYPE} = :type", { :type => 'Client' }]) assert_equal client2, firm.clients.find(:first, :conditions => ["#{QUOTED_TYPE} = :type", { :type => 'Client' }])
end end
def test_find_all_with_include_and_conditions
assert_nothing_raised do
Developer.find(:all, :joins => :audit_logs, :conditions => {'audit_logs.message' => nil, :name => 'Smith'})
end
end
def test_find_in_collection def test_find_in_collection
assert_equal Client.find(2).name, companies(:first_firm).clients.find(2).name assert_equal Client.find(2).name, companies(:first_firm).clients.find(2).name
assert_raise(ActiveRecord::RecordNotFound) { companies(:first_firm).clients.find(6) } assert_raise(ActiveRecord::RecordNotFound) { companies(:first_firm).clients.find(6) }
@ -502,6 +509,23 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 0, new_firm.clients_of_firm.size assert_equal 0, new_firm.clients_of_firm.size
end end
def test_deleting_updates_counter_cache
topic = Topic.first
assert_equal topic.replies.to_a.size, topic.replies_count
topic.replies.delete(topic.replies.first)
topic.reload
assert_equal topic.replies.to_a.size, topic.replies_count
end
def test_deleting_updates_counter_cache_without_dependent_destroy
post = posts(:welcome)
assert_difference "post.reload.taggings_count", -1 do
post.taggings.delete(post.taggings.first)
end
end
def test_deleting_a_collection def test_deleting_a_collection
force_signal37_to_load_all_clients_of_firm force_signal37_to_load_all_clients_of_firm
companies(:first_firm).clients_of_firm.create("name" => "Another Client") companies(:first_firm).clients_of_firm.create("name" => "Another Client")
@ -547,6 +571,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end end
end end
def test_clearing_updates_counter_cache
topic = Topic.first
topic.replies.clear
topic.reload
assert_equal 0, topic.replies_count
end
def test_clearing_a_dependent_association_collection def test_clearing_a_dependent_association_collection
firm = companies(:first_firm) firm = companies(:first_firm)
client_id = firm.dependent_clients_of_firm.first.id client_id = firm.dependent_clients_of_firm.first.id
@ -691,6 +723,28 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 0, companies(:first_firm).clients_of_firm(true).size assert_equal 0, companies(:first_firm).clients_of_firm(true).size
end end
def test_destroying_by_fixnum_id
force_signal37_to_load_all_clients_of_firm
assert_difference "Client.count", -1 do
companies(:first_firm).clients_of_firm.destroy(companies(:first_firm).clients_of_firm.first.id)
end
assert_equal 0, companies(:first_firm).reload.clients_of_firm.size
assert_equal 0, companies(:first_firm).clients_of_firm(true).size
end
def test_destroying_by_string_id
force_signal37_to_load_all_clients_of_firm
assert_difference "Client.count", -1 do
companies(:first_firm).clients_of_firm.destroy(companies(:first_firm).clients_of_firm.first.id.to_s)
end
assert_equal 0, companies(:first_firm).reload.clients_of_firm.size
assert_equal 0, companies(:first_firm).clients_of_firm(true).size
end
def test_destroying_a_collection def test_destroying_a_collection
force_signal37_to_load_all_clients_of_firm force_signal37_to_load_all_clients_of_firm
companies(:first_firm).clients_of_firm.create("name" => "Another Client") companies(:first_firm).clients_of_firm.create("name" => "Another Client")
@ -861,7 +915,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
lambda { authors(:mary).comments = [comments(:greetings), comments(:more_greetings)] }, lambda { authors(:mary).comments = [comments(:greetings), comments(:more_greetings)] },
lambda { authors(:mary).comments << Comment.create!(:body => "Yay", :post_id => 424242) }, lambda { authors(:mary).comments << Comment.create!(:body => "Yay", :post_id => 424242) },
lambda { authors(:mary).comments.delete(authors(:mary).comments.first) }, lambda { authors(:mary).comments.delete(authors(:mary).comments.first) },
].each {|block| assert_raise(ActiveRecord::HasManyThroughCantAssociateThroughHasManyReflection, &block) } ].each {|block| assert_raise(ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection, &block) }
end end
def test_dynamic_find_should_respect_association_order_for_through def test_dynamic_find_should_respect_association_order_for_through

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