diff options
113 files changed, 1349 insertions, 168 deletions
@@ -22,7 +22,7 @@ end gem 'uglifier', '>= 1.0.3', :require => false gem 'rake', '>= 0.8.7' -gem 'mocha', '>= 0.12.1' +gem 'mocha', '>= 0.13.0', :require => false group :doc do # The current sdoc cannot generate GitHub links due @@ -47,7 +47,7 @@ instance_eval File.read '.Gemfile' if File.exists? '.Gemfile' platforms :mri do group :test do - gem 'ruby-prof', '~> 0.11.2' + gem 'ruby-prof', '~> 0.11.2' if RUBY_VERSION < '2.0' end end diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index fffadd18a0..e8257fd63c 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -1,11 +1,31 @@ +## Rails 3.2.11 (unreleased) ## + ## Rails 3.2.10 ## ## Rails 3.2.9 (Nov 12, 2012) ## -* Do not render views when mail() isn't called. - Fix #7761 +* The return value from mailer methods is no longer relevant. This fixes a bug, + which was introduced with 3.2.9. + Backport #8450 + Fix #8448 + + class ExampleMailer < ActionMailer::Base + # in 3.2.9, returning a falsy value from a mailer action, prevented the email from beeing sent. + # With 3.2.10 the return value is no longer relevant. If you call mail() the email will be sent. + def nil_returning_mailer_action + mail() + nil + end + end + + *Yves Senn* + +## Rails 3.2.9 (Nov 12, 2012) ## + +* Do not render views when mail() isn't called. + Fix #7761 - *Yves Senn* + *Yves Senn* ## Rails 3.2.8 (Aug 9, 2012) ## diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec index 3603017814..c2c8e9b8e2 100644 --- a/actionmailer/actionmailer.gemspec +++ b/actionmailer/actionmailer.gemspec @@ -17,5 +17,5 @@ Gem::Specification.new do |s| s.requirements << 'none' s.add_dependency('actionpack', version) - s.add_dependency('mail', '~> 2.4.4') + s.add_dependency('mail', '~> 2.5.3') end diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index a9fb49a303..9e2f640915 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -123,8 +123,8 @@ module ActionMailer #:nodoc: # # <%= users_url(:host => "example.com") %> # - # You should use the <tt>named_route_url</tt> style (which generates absolute URLs) and avoid using the - # <tt>named_route_path</tt> style (which generates relative URLs), since clients reading the mail will + # You should use the <tt>named_route_url</tt> style (which generates absolute URLs) and avoid using the + # <tt>named_route_path</tt> style (which generates relative URLs), since clients reading the mail will # have no concept of a current URL from which to determine a relative path. # # It is also possible to set a default host that will be used in all mailers by setting the <tt>:host</tt> @@ -133,7 +133,7 @@ module ActionMailer #:nodoc: # config.action_mailer.default_url_options = { :host => "example.com" } # # When you decide to set a default <tt>:host</tt> for your mailers, then you need to make sure to use the - # <tt>:only_path => false</tt> option when using <tt>url_for</tt>. Since the <tt>url_for</tt> view helper + # <tt>:only_path => false</tt> option when using <tt>url_for</tt>. Since the <tt>url_for</tt> view helper # will generate relative URLs by default when a <tt>:host</tt> option isn't explicitly provided, passing # <tt>:only_path => false</tt> will ensure that absolute URLs are generated. # @@ -150,8 +150,8 @@ module ActionMailer #:nodoc: # # = Multipart Emails # - # Multipart messages can also be used implicitly because Action Mailer will automatically detect and use - # multipart templates, where each template is named after the name of the action, followed by the content + # Multipart messages can also be used implicitly because Action Mailer will automatically detect and use + # multipart templates, where each template is named after the name of the action, followed by the content # type. Each such detected template will be added as a separate part to the message. # # For example, if the following templates exist: @@ -448,6 +448,7 @@ module ActionMailer #:nodoc: # method, for instance). def initialize(method_name=nil, *args) super() + @mail_was_called = false @_message = Mail.new process(method_name, *args) if method_name end @@ -455,10 +456,8 @@ module ActionMailer #:nodoc: def process(*args) #:nodoc: lookup_context.skip_default_locale! - generated_mail = super - unless generated_mail - @_message = NullMail.new - end + super + @_message = NullMail.new unless @mail_was_called end class NullMail #:nodoc: @@ -616,8 +615,9 @@ module ActionMailer #:nodoc: # end # def mail(headers={}, &block) - # Guard flag to prevent both the old and the new API from firing - # Should be removed when old API is removed + # Guard flag to prevent both the old and the new API from firing. + # On master this flag was renamed to `@_mail_was_called`. + # On master there is only one API and this flag is no longer used as a guard. @mail_was_called = true m = @_message diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index b69b26faf0..f648cb1546 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -477,6 +477,12 @@ class BaseTest < ActiveSupport::TestCase mail.deliver end + test 'the return value of mailer methods is not relevant' do + mail = BaseMailer.with_nil_as_return_value + assert_equal('Welcome', mail.body.to_s.strip) + mail.deliver + end + # Before and After hooks class MyObserver diff --git a/actionmailer/test/mailers/base_mailer.rb b/actionmailer/test/mailers/base_mailer.rb index 8c4430b046..50438ead2a 100644 --- a/actionmailer/test/mailers/base_mailer.rb +++ b/actionmailer/test/mailers/base_mailer.rb @@ -118,4 +118,9 @@ class BaseMailer < ActionMailer::Base def without_mail_call end + + def with_nil_as_return_value(hash = {}) + mail(:template_name => "welcome") + nil + end end diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 4d7035e63f..6917d0e06c 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,77 @@ +## Rails 3.2.11 (unreleased) ## + +* Clear url helper methods when routes are reloaded by removing the methods + explicitly rather than just clearing the module because it didn't work + properly and could be the source of a memory leak. + + *Andrew White* + +* Fix a bug in ActionDispatch::Request#raw_post that caused env['rack.input'] + to be read but not rewound. + + *Matt Venables* + +* More descriptive error messages when calling `render :partial` with + an invalid `:layout` argument. + #8376 + + render :partial => 'partial', :layout => true + + # results in ActionView::MissingTemplate: Missing partial /true + + *Yves Senn* + +* Accept symbols as #send_data :disposition value. [Backport #8329] *Elia Schito* + +* Add i18n scope to distance_of_time_in_words. [Backport #7997] *Steve Klabnik* + +* Fix side effect of `url_for` changing the `:controller` string option. [Backport #6003] + Before: + + controller = '/projects' + url_for :controller => controller, :action => 'status' + + puts controller #=> 'projects' + + After + + puts controller #=> '/projects' + + [Nikita Beloglazov + Andrew White] + +* Introduce `ActionView::Template::Handlers::ERB.escape_whitelist`. This is a list + of mime types where template text is not html escaped by default. It prevents `Jack & Joe` + from rendering as `Jack & Joe` for the whitelisted mime types. The default whitelist + contains text/plain. Fix #7976 [Backport #8235] + + *Joost Baaij* + +* `BestStandardsSupport` middleware now appends it's `X-UA-Compatible` value to app's + returned value if any. Fix #8086 [Backport #8093] + + *Nikita Afanasenko* + +* prevent double slashes in engine urls when `Rails.application.default_url_options[:trailing_slash] = true` is set + Fix #7842 + + *Yves Senn* + +* Fix input name when `:multiple => true` and `:index` are set. + + Before: + + check_box("post", "comment_ids", { :multiple => true, :index => "foo" }, 1) + #=> <input name=\"post[foo][comment_ids]\" type=\"hidden\" value=\"0\" /><input id=\"post_foo_comment_ids_1\" name=\"post[foo][comment_ids]\" type=\"checkbox\" value=\"1\" /> + + After: + + check_box("post", "comment_ids", { :multiple => true, :index => "foo" }, 1) + #=> <input name=\"post[foo][comment_ids][]\" type=\"hidden\" value=\"0\" /><input id=\"post_foo_comment_ids_1\" name=\"post[foo][comment_ids][]\" type=\"checkbox\" value=\"1\" /> + + Fix #8108 + + *Daniel Fox, Grant Hutchins & Trace Wax* + ## Rails 3.2.10 ## ## Rails 3.2.9 (Nov 12, 2012) ## diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb index 62e16ea047..95c058fc86 100644 --- a/actionpack/lib/action_controller/metal/data_streaming.rb +++ b/actionpack/lib/action_controller/metal/data_streaming.rb @@ -141,7 +141,7 @@ module ActionController #:nodoc: raise ArgumentError, ":#{arg} option required" if options[arg].nil? end - disposition = options[:disposition] + disposition = options[:disposition].to_s disposition += %(; filename="#{options[:filename]}") if options[:filename] content_type = options[:type] diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index afc0496ef9..0413346d94 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -179,8 +179,9 @@ module ActionDispatch # work with raw requests directly. def raw_post unless @env.include? 'RAW_POST_DATA' - @env['RAW_POST_DATA'] = body.read(@env['CONTENT_LENGTH'].to_i) - body.rewind if body.respond_to?(:rewind) + raw_post_body = body + @env['RAW_POST_DATA'] = raw_post_body.read(@env['CONTENT_LENGTH'].to_i) + raw_post_body.rewind if raw_post_body.respond_to?(:rewind) end @env['RAW_POST_DATA'] end diff --git a/actionpack/lib/action_dispatch/middleware/best_standards_support.rb b/actionpack/lib/action_dispatch/middleware/best_standards_support.rb index 69adcc419f..d338996240 100644 --- a/actionpack/lib/action_dispatch/middleware/best_standards_support.rb +++ b/actionpack/lib/action_dispatch/middleware/best_standards_support.rb @@ -15,7 +15,13 @@ module ActionDispatch def call(env) status, headers, body = @app.call(env) - headers["X-UA-Compatible"] = @header + + if headers["X-UA-Compatible"] && @header + headers["X-UA-Compatible"] << "," << @header.to_s + else + headers["X-UA-Compatible"] = @header + end + [status, headers, body] end end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 952219631a..9a474d2e3a 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -451,7 +451,7 @@ module ActionDispatch # we must actually delete prefix segment keys to avoid passing them to next url_for _route.segment_keys.each { |k| options.delete(k) } prefix = _routes.url_helpers.send("#{name}_path", prefix_options) - prefix = '' if prefix == '/' + prefix = prefix.gsub(%r{/\z}, '') prefix end end diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb index dc28389360..50b20a2a25 100644 --- a/actionpack/lib/action_dispatch/routing/redirection.rb +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -78,10 +78,10 @@ module ActionDispatch # params, depending of how many arguments your block accepts. A string is required as a # return value. # - # match 'jokes/:number', :to => redirect do |params, request| - # path = (params[:number].to_i.even? ? "/wheres-the-beef" : "/i-love-lamp") + # match 'jokes/:number', :to => redirect { |params, request| + # path = (params[:number].to_i.even? ? "wheres-the-beef" : "i-love-lamp") # "http://#{request.host_with_port}/#{path}" - # end + # } # # The options version of redirect allows you to supply only the parts of the url which need # to change, it also supports interpolation of the path similar to the first example. diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 667094c469..a993699e05 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -94,7 +94,12 @@ module ActionDispatch attr_reader :routes, :helpers, :module def initialize - clear! + @routes = {} + @helpers = [] + + @module = Module.new do + instance_methods.each { |selector| remove_method(selector) } + end end def helper_names @@ -102,12 +107,14 @@ module ActionDispatch end def clear! + @helpers.each do |helper| + @module.module_eval do + remove_possible_method helper + end + end + @routes = {} @helpers = [] - - @module ||= Module.new do - instance_methods.each { |selector| remove_method(selector) } - end end def add(name, route) @@ -291,7 +298,6 @@ module ActionDispatch def clear! @finalized = false - @url_helpers = nil named_routes.clear set.clear formatter.clear @@ -442,12 +448,12 @@ module ActionDispatch normalize_options! normalize_controller_action_id! use_relative_controller! - controller.sub!(%r{^/}, '') if controller + normalize_controller! handle_nil_action! end def controller - @controller ||= @options[:controller] + @options[:controller] end def current_controller @@ -504,10 +510,15 @@ module ActionDispatch old_parts = current_controller.split('/') size = controller.count("/") + 1 parts = old_parts[0...-size] << controller - @controller = @options[:controller] = parts.join("/") + @options[:controller] = parts.join("/") end end + # Remove leading slashes from controllers + def normalize_controller! + @options[:controller] = controller.sub(%r{^/}, '') if controller + end + # This handles the case of :action => nil being explicitly passed. # It is identical to :action => "index" def handle_nil_action! diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index 87e2332c42..99aa144d3a 100644 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -65,12 +65,17 @@ module ActionView # distance_of_time_in_words(Time.now, Time.now) # => less than a minute # def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false, options = {}) + options = { + :scope => :'datetime.distance_in_words', + }.merge!(options) + from_time = from_time.to_time if from_time.respond_to?(:to_time) to_time = to_time.to_time if to_time.respond_to?(:to_time) - distance_in_minutes = (((to_time - from_time).abs)/60).round - distance_in_seconds = ((to_time - from_time).abs).round + distance = (to_time.to_f - from_time.to_f).abs + distance_in_minutes = (distance / 60.0).round + distance_in_seconds = distance.round - I18n.with_options :locale => options[:locale], :scope => :'datetime.distance_in_words' do |locale| + I18n.with_options :locale => options[:locale], :scope => options[:scope] do |locale| case distance_in_minutes when 0..1 return distance_in_minutes == 0 ? @@ -129,8 +134,8 @@ module ActionView # from_time = Time.now - 3.days - 14.minutes - 25.seconds # time_ago_in_words(from_time) # => 3 days # - def time_ago_in_words(from_time, include_seconds = false) - distance_of_time_in_words(from_time, Time.now, include_seconds) + def time_ago_in_words(from_time, include_seconds = false, options = {}) + distance_of_time_in_words(from_time, Time.now, include_seconds, options) end alias_method :distance_of_time_in_words_to_now, :time_ago_in_words diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index a3409ee3c7..d00bad7608 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -1205,9 +1205,11 @@ module ActionView options["name"] ||= tag_name_with_index(@auto_index) options["id"] = options.fetch("id"){ tag_id_with_index(@auto_index) } else - options["name"] ||= tag_name + (options['multiple'] ? '[]' : '') + options["name"] ||= tag_name options["id"] = options.fetch("id"){ tag_id } end + + options["name"] += "[]" if options["multiple"] options["id"] = [options.delete('namespace'), options["id"]].compact.join("_").presence end diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb index 670ff18a66..f0573437ca 100644 --- a/actionpack/lib/action_view/helpers/form_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -45,7 +45,7 @@ module ActionView # # => <form action="/posts" method="post"> # # form_tag('/posts/1', :method => :put) - # # => <form action="/posts/1" method="put"> + # # => <form action="/posts/1" method="post"> ... <input name="_method" type="hidden" value="put" /> ... # # form_tag('/upload', :multipart => true) # # => <form action="/upload" method="post" enctype="multipart/form-data"> @@ -53,7 +53,7 @@ module ActionView # <%= form_tag('/posts') do -%> # <div><%= submit_tag 'Save' %></div> # <% end -%> - # # => <form action="/posts" method="post"><div><input type="submit" name="submit" value="Save" /></div></form> + # # => <form action="/posts" method="post"><div><input type="submit" name="commit" value="Save" /></div></form> # # <%= form_tag('/posts', :remote => true) %> # # => <form action="/posts" method="post" data-remote="true"> diff --git a/actionpack/lib/action_view/renderer/partial_renderer.rb b/actionpack/lib/action_view/renderer/partial_renderer.rb index 3093aca91e..71fa05ab3e 100644 --- a/actionpack/lib/action_view/renderer/partial_renderer.rb +++ b/actionpack/lib/action_view/renderer/partial_renderer.rb @@ -256,7 +256,7 @@ module ActionView object, as = @object, @variable if !block && (layout = @options[:layout]) - layout = find_template(layout) + layout = find_template(layout.to_s) end object ||= locals[as] diff --git a/actionpack/lib/action_view/template/handlers/erb.rb b/actionpack/lib/action_view/template/handlers/erb.rb index ea495ea9ca..118b18a81c 100644 --- a/actionpack/lib/action_view/template/handlers/erb.rb +++ b/actionpack/lib/action_view/template/handlers/erb.rb @@ -15,6 +15,17 @@ module ActionView src << "@output_buffer.safe_concat('" << escape_text(text) << "');" end + # Erubis toggles <%= and <%== behavior when escaping is enabled. + # We override to always treat <%== as escaped. + def add_expr(src, code, indicator) + case indicator + when '==' + add_expr_escaped(src, code) + else + super + end + end + BLOCK_EXPR = /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/ def add_expr_literal(src, code) @@ -48,6 +59,10 @@ module ActionView class_attribute :erb_implementation self.erb_implementation = Erubis + # Do not escape templates of these mime types. + class_attribute :escape_whitelist + self.escape_whitelist = ["text/plain"] + ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*") def self.call(template) @@ -83,6 +98,7 @@ module ActionView self.class.erb_implementation.new( erb, + :escape => (self.class.escape_whitelist.include? template.mime_type), :trim => (self.class.erb_trim_mode == "-") ).src end diff --git a/actionpack/test/controller/new_base/render_template_test.rb b/actionpack/test/controller/new_base/render_template_test.rb index 29c8885d9f..c727f03346 100644 --- a/actionpack/test/controller/new_base/render_template_test.rb +++ b/actionpack/test/controller/new_base/render_template_test.rb @@ -9,7 +9,8 @@ module RenderTemplate "locals.html.erb" => "The secret is <%= secret %>", "xml_template.xml.builder" => "xml.html do\n xml.p 'Hello'\nend", "with_raw.html.erb" => "Hello <%=raw '<strong>this is raw</strong>' %>", - "with_implicit_raw.html.erb" => "Hello <%== '<strong>this is also raw</strong>' %>", + "with_implicit_raw.html.erb" => "Hello <%== '<strong>this is also raw</strong>' %> in a html template", + "with_implicit_raw.text.erb" => "Hello <%== '<strong>this is also raw</strong>' %> in a text template", "test/with_json.html.erb" => "<%= render :template => 'test/with_json', :formats => [:json] %>", "test/with_json.json.erb" => "<%= render :template => 'test/final', :formats => [:json] %>", "test/final.json.erb" => "{ final: json }", @@ -113,7 +114,12 @@ module RenderTemplate get :with_implicit_raw - assert_body "Hello <strong>this is also raw</strong>" + assert_body "Hello <strong>this is also raw</strong> in a html template" + assert_status 200 + + get :with_implicit_raw, :format => 'text' + + assert_body "Hello <strong>this is also raw</strong> in a text template" assert_status 200 end diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb index 8f885ff28e..4e4dd8d005 100644 --- a/actionpack/test/controller/send_file_test.rb +++ b/actionpack/test/controller/send_file_test.rb @@ -118,6 +118,18 @@ class SendFileTest < ActionController::TestCase assert_equal 'private', h['Cache-Control'] end + def test_send_file_headers_with_disposition_as_a_symbol + options = { + :type => Mime::PNG, + :disposition => :disposition, + :filename => 'filename' + } + + @controller.headers = {} + @controller.send(:send_file_headers!, options) + assert_equal 'disposition; filename="filename"', @controller.headers['Content-Disposition'] + end + def test_send_file_headers_with_mime_lookup_with_symbol options = { :type => :png @@ -138,7 +150,7 @@ class SendFileTest < ActionController::TestCase @controller.headers = {} assert_raise(ArgumentError){ @controller.send(:send_file_headers!, options) } end - + def test_send_file_headers_guess_type_from_extension { 'image.png' => 'image/png', diff --git a/actionpack/test/dispatch/best_standards_support_test.rb b/actionpack/test/dispatch/best_standards_support_test.rb new file mode 100644 index 0000000000..0737c40a39 --- /dev/null +++ b/actionpack/test/dispatch/best_standards_support_test.rb @@ -0,0 +1,34 @@ +require 'abstract_unit' + +class BestStandardsSupportTest < ActiveSupport::TestCase + def test_with_best_standards_support + _, headers, _ = app(true, {}).call({}) + assert_equal "IE=Edge,chrome=1", headers["X-UA-Compatible"] + end + + def test_with_builtin_best_standards_support + _, headers, _ = app(:builtin, {}).call({}) + assert_equal "IE=Edge", headers["X-UA-Compatible"] + end + + def test_without_best_standards_support + _, headers, _ = app(false, {}).call({}) + assert_equal nil, headers["X-UA-Compatible"] + end + + def test_appends_to_app_headers + app_headers = { "X-UA-Compatible" => "requiresActiveX=true" } + _, headers, _ = app(true, app_headers).call({}) + + expects = "requiresActiveX=true,IE=Edge,chrome=1" + assert_equal expects, headers["X-UA-Compatible"] + end + + private + + def app(type, headers) + app = proc { [200, headers, "response"] } + ActionDispatch::BestStandardsSupport.new(app, type) + end + +end diff --git a/actionpack/test/dispatch/prefix_generation_test.rb b/actionpack/test/dispatch/prefix_generation_test.rb index bd5b5edab0..88dc2c093b 100644 --- a/actionpack/test/dispatch/prefix_generation_test.rb +++ b/actionpack/test/dispatch/prefix_generation_test.rb @@ -248,6 +248,11 @@ module TestGenerationPrefix assert_equal "/something/", app_object.root_path end + test "[OBJECT] generating application's route includes default_url_options[:trailing_slash]" do + RailsApplication.routes.default_url_options[:trailing_slash] = true + assert_equal "/awesome/blog/posts", engine_object.posts_path + end + test "[OBJECT] generating engine's route with url_for" do path = engine_object.url_for(:controller => "inside_engine_generating", :action => "show", diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index 5b3d38c48c..56431b4daf 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -530,6 +530,13 @@ class RequestTest < ActiveSupport::TestCase assert_equal Mime::XML, request.negotiate_mime([Mime::XML, Mime::CSV]) end + test "raw_post rewinds rack.input if RAW_POST_DATA is nil" do + request = stub_request('rack.input' => StringIO.new("foo"), + 'CONTENT_LENGTH' => 3) + assert_equal "foo", request.raw_post + assert_equal "foo", request.env['rack.input'].read + end + test "process parameter filter" do test_hashes = [ [{'foo'=>'bar'},{'foo'=>'bar'},%w'food'], diff --git a/actionpack/test/dispatch/routing/route_set_test.rb b/actionpack/test/dispatch/routing/route_set_test.rb new file mode 100644 index 0000000000..629b8ed8bd --- /dev/null +++ b/actionpack/test/dispatch/routing/route_set_test.rb @@ -0,0 +1,86 @@ +require 'abstract_unit' + +module ActionDispatch + module Routing + class RouteSetTest < ActiveSupport::TestCase + class SimpleApp + def initialize(response) + @response = response + end + + def call(env) + [ 200, { 'Content-Type' => 'text/plain' }, [response] ] + end + end + + setup do + @set = RouteSet.new + end + + test "url helpers are added when route is added" do + draw do + get 'foo', :to => SimpleApp.new('foo#index') + end + + assert_equal '/foo', url_helpers.foo_path + assert_raises NoMethodError do + assert_equal '/bar', url_helpers.bar_path + end + + draw do + get 'foo', :to => SimpleApp.new('foo#index') + get 'bar', :to => SimpleApp.new('bar#index') + end + + assert_equal '/foo', url_helpers.foo_path + assert_equal '/bar', url_helpers.bar_path + end + + test "url helpers are updated when route is updated" do + draw do + get 'bar', :to => SimpleApp.new('bar#index'), :as => :bar + end + + assert_equal '/bar', url_helpers.bar_path + + draw do + get 'baz', :to => SimpleApp.new('baz#index'), :as => :bar + end + + assert_equal '/baz', url_helpers.bar_path + end + + test "url helpers are removed when route is removed" do + draw do + get 'foo', :to => SimpleApp.new('foo#index') + get 'bar', :to => SimpleApp.new('bar#index') + end + + assert_equal '/foo', url_helpers.foo_path + assert_equal '/bar', url_helpers.bar_path + + draw do + get 'foo', :to => SimpleApp.new('foo#index') + end + + assert_equal '/foo', url_helpers.foo_path + assert_raises NoMethodError do + assert_equal '/bar', url_helpers.bar_path + end + end + + private + def clear! + @set.clear! + end + + def draw(&block) + @set.draw(&block) + end + + def url_helpers + @set.url_helpers + end + end + end +end diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index e61b4e2d8f..46d16598f7 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -887,6 +887,15 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal original_options, options end + def test_url_for_does_not_modify_controller + controller = '/projects' + options = {:controller => controller, :action => 'status', :only_path => true} + url = url_for(options) + + assert_equal '/projects/status', url + assert_equal '/projects', controller + end + # tests the arguments modification free version of define_hash_access def test_named_route_with_no_side_effects original_options = { :host => 'test.host' } diff --git a/actionpack/test/template/date_helper_i18n_test.rb b/actionpack/test/template/date_helper_i18n_test.rb index d45215acfd..b0b407b473 100644 --- a/actionpack/test/template/date_helper_i18n_test.rb +++ b/actionpack/test/template/date_helper_i18n_test.rb @@ -36,16 +36,25 @@ class DateHelperDistanceOfTimeInWordsI18nTests < Test::Unit::TestCase end end - def assert_distance_of_time_in_words_translates_key(passed, expected) + def test_distance_of_time_in_words_calls_i18n_with_custom_scope + { + [30.days, false] => [:'about_x_months', 1], + [60.days, false] => [:'x_months', 2], + }.each do |passed, expected| + assert_distance_of_time_in_words_translates_key(passed, expected, {:scope => :'datetime.distance_in_words_ago'}) + end + end + + def assert_distance_of_time_in_words_translates_key(passed, expected, options = {}) diff, include_seconds = *passed key, count = *expected to = @from + diff - options = {:locale => 'en', :scope => :'datetime.distance_in_words'} + options = {:locale => 'en', :scope => :'datetime.distance_in_words'}.merge(options) options[:count] = count if count I18n.expects(:t).with(key, options) - distance_of_time_in_words(@from, to, include_seconds, :locale => 'en') + distance_of_time_in_words(@from, to, include_seconds, options) end def test_distance_of_time_pluralizations diff --git a/actionpack/test/template/date_helper_test.rb b/actionpack/test/template/date_helper_test.rb index cf707e1de5..e4f84f8dd7 100644 --- a/actionpack/test/template/date_helper_test.rb +++ b/actionpack/test/template/date_helper_test.rb @@ -128,12 +128,39 @@ class DateHelperTest < ActionView::TestCase end def test_distance_in_words_with_integers - assert_equal "less than a minute", distance_of_time_in_words(59) + assert_equal "1 minute", distance_of_time_in_words(59) assert_equal "about 1 hour", distance_of_time_in_words(60*60) - assert_equal "less than a minute", distance_of_time_in_words(0, 59) + assert_equal "1 minute", distance_of_time_in_words(0, 59) assert_equal "about 1 hour", distance_of_time_in_words(60*60, 0) end + def test_distance_in_words_with_times + assert_equal "1 minute", distance_of_time_in_words(30.seconds) + assert_equal "1 minute", distance_of_time_in_words(59.seconds) + assert_equal "2 minutes", distance_of_time_in_words(119.seconds) + assert_equal "2 minutes", distance_of_time_in_words(1.minute + 59.seconds) + assert_equal "3 minutes", distance_of_time_in_words(2.minute + 30.seconds) + assert_equal "44 minutes", distance_of_time_in_words(44.minutes + 29.seconds) + assert_equal "about 1 hour", distance_of_time_in_words(44.minutes + 30.seconds) + assert_equal "about 1 hour", distance_of_time_in_words(60.minutes) + + # include seconds + assert_equal "half a minute", distance_of_time_in_words(39.seconds, 0, true) + assert_equal "less than a minute", distance_of_time_in_words(40.seconds, 0, true) + assert_equal "less than a minute", distance_of_time_in_words(59.seconds, 0, true) + assert_equal "1 minute", distance_of_time_in_words(60.seconds, 0, true) + end + + def test_distance_in_words_with_offset_datetimes + start_date = DateTime.new 1975, 1, 31, 0, 0, 0, '+6' + end_date = DateTime.new 1977, 1, 31, 0, 0, 0, '+6' + assert_equal("about 2 years", distance_of_time_in_words(start_date, end_date)) + + start_date = DateTime.new 1982, 12, 3, 0, 0, 0, '+6' + end_date = DateTime.new 2010, 11, 30, 0, 0, 0, '+6' + assert_equal("almost 28 years", distance_of_time_in_words(start_date, end_date)) + end + def test_time_ago_in_words assert_equal "about 1 year", time_ago_in_words(1.year.ago - 1.day) end diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb index 19af01e2c8..49a325af79 100644 --- a/actionpack/test/template/form_helper_test.rb +++ b/actionpack/test/template/form_helper_test.rb @@ -393,6 +393,19 @@ class FormHelperTest < ActionView::TestCase ) end + def test_check_box_with_multiple_behavior_and_index + @post.comment_ids = [2,3] + assert_dom_equal( + '<input name="post[foo][comment_ids][]" type="hidden" value="0" /><input id="post_foo_comment_ids_1" name="post[foo][comment_ids][]" type="checkbox" value="1" />', + check_box("post", "comment_ids", { :multiple => true, :index => "foo" }, 1) + ) + assert_dom_equal( + '<input name="post[bar][comment_ids][]" type="hidden" value="0" /><input checked="checked" id="post_bar_comment_ids_3" name="post[bar][comment_ids][]" type="checkbox" value="3" />', + check_box("post", "comment_ids", { :multiple => true, :index => "bar" }, 3) + ) + + end + def test_checkbox_disabled_disables_hidden_field assert_dom_equal( '<input name="post[secret]" type="hidden" value="0" disabled="disabled"/><input checked="checked" disabled="disabled" id="post_secret" name="post[secret]" type="checkbox" value="1" />', diff --git a/actionpack/test/template/render_test.rb b/actionpack/test/template/render_test.rb index bfeb6aee1a..b907e3297b 100644 --- a/actionpack/test/template/render_test.rb +++ b/actionpack/test/template/render_test.rb @@ -336,7 +336,7 @@ module RenderTestCases ActionView::Template.register_template_handler :foo, CustomHandler assert_equal 'source: "Hello, <%= name %>!"', @view.render(:inline => "Hello, <%= name %>!", :locals => { :name => "Josh" }, :type => :foo) end - + def test_render_knows_about_types_registered_when_extensions_are_checked_earlier_in_initialization ActionView::Template::Handlers.extensions ActionView::Template.register_template_handler :foo, CustomHandler @@ -406,6 +406,11 @@ module RenderTestCases @view.render(:partial => 'test/partial_with_layout_block_content', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'}) end + def test_render_partial_with_layout_raises_descriptive_error + e = assert_raises(ActionView::MissingTemplate) { @view.render(:partial => 'test/partial', :layout => true) } + assert_match "Missing partial /true with", e.message + end + def test_render_with_nested_layout assert_equal %(<title>title</title>\n\n<div id="column">column</div>\n<div id="content">content</div>\n), @view.render(:file => "test/nested_layout", :layout => "layouts/yield") diff --git a/actionpack/test/template/sprockets_helper_test.rb b/actionpack/test/template/sprockets_helper_test.rb index 72d03e43e9..e944cfaee3 100644 --- a/actionpack/test/template/sprockets_helper_test.rb +++ b/actionpack/test/template/sprockets_helper_test.rb @@ -1,7 +1,7 @@ require 'abstract_unit' require 'sprockets' require 'sprockets/helpers/rails_helper' -require 'mocha' +require 'mocha/setup' class SprocketsHelperTest < ActionView::TestCase include Sprockets::Helpers::RailsHelper diff --git a/actionpack/test/template/sprockets_helper_with_routes_test.rb b/actionpack/test/template/sprockets_helper_with_routes_test.rb index bcbd81a7dd..bc253ea0fd 100644 --- a/actionpack/test/template/sprockets_helper_with_routes_test.rb +++ b/actionpack/test/template/sprockets_helper_with_routes_test.rb @@ -1,7 +1,7 @@ require 'abstract_unit' require 'sprockets' require 'sprockets/helpers/rails_helper' -require 'mocha' +require 'mocha/setup' class SprocketsHelperWithRoutesTest < ActionView::TestCase include Sprockets::Helpers::RailsHelper @@ -54,4 +54,4 @@ class SprocketsHelperWithRoutesTest < ActionView::TestCase stylesheet_link_tag(:application) end -end
\ No newline at end of file +end diff --git a/actionpack/test/template/template_test.rb b/actionpack/test/template/template_test.rb index 5880eb2bd4..56943381d8 100644 --- a/actionpack/test/template/template_test.rb +++ b/actionpack/test/template/template_test.rb @@ -25,6 +25,10 @@ class TestERBTemplate < ActiveSupport::TestCase "Hello" end + def apostrophe + "l'apostrophe" + end + def partial ActionView::Template.new( "<%= @virtual_path %>", @@ -47,7 +51,7 @@ class TestERBTemplate < ActiveSupport::TestCase end end - def new_template(body = "<%= hello %>", details = {}) + def new_template(body = "<%= hello %>", details = { :format => :html }) ActionView::Template.new(body, "hello template", ERBHandler, {:virtual_path => "hello"}.merge!(details)) end @@ -64,6 +68,16 @@ class TestERBTemplate < ActiveSupport::TestCase assert_equal "Hello", render end + def test_basic_template_does_html_escape + @template = new_template("<%= apostrophe %>") + assert_equal "l'apostrophe", render + end + + def test_text_template_does_not_html_escape + @template = new_template("<%= apostrophe %> <%== apostrophe %>", :format => :text) + assert_equal "l'apostrophe l'apostrophe", render + end + def test_template_loses_its_source_after_rendering @template = new_template render diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 6f09c1cb43..2cb51945fd 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,3 +1,7 @@ +## Rails 3.2.11 (unreleased) ## + +* Specify type of singular association during serialization *Steve Klabnik* + ## Rails 3.2.10 ## ## Rails 3.2.9 (Nov 12, 2012) ## diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 00bd68512b..7eaee050cd 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -84,7 +84,7 @@ module ActiveModel end # Backport dup from 1.9 so that #initialize_dup gets called - unless Object.respond_to?(:initialize_dup) + unless Object.respond_to?(:initialize_dup, true) def dup # :nodoc: copy = super copy.initialize_dup(self) diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb index 2dc7d00f52..a63856045f 100644 --- a/activemodel/lib/active_model/serializers/xml.rb +++ b/activemodel/lib/active_model/serializers/xml.rb @@ -144,7 +144,12 @@ module ActiveModel end else merged_options[:root] = association.to_s - records.to_xml(merged_options) + + unless records.class.to_s.underscore == association.to_s + merged_options[:type] = records.class.name + end + + records.to_xml merged_options end end diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 45d8677fa0..c7e508958b 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -173,7 +173,7 @@ module ActiveModel end # Backport dup from 1.9 so that #initialize_dup gets called - unless Object.respond_to?(:initialize_dup) + unless Object.respond_to?(:initialize_dup, true) def dup # :nodoc: copy = super copy.initialize_dup(self) diff --git a/activemodel/test/cases/serializers/xml_serialization_test.rb b/activemodel/test/cases/serializers/xml_serialization_test.rb index 83db8d21f4..62c2b2035f 100644 --- a/activemodel/test/cases/serializers/xml_serialization_test.rb +++ b/activemodel/test/cases/serializers/xml_serialization_test.rb @@ -6,12 +6,12 @@ require 'ostruct' class Contact include ActiveModel::Serializers::Xml - attr_accessor :address, :friends + attr_accessor :address, :friends, :contact remove_method :attributes if method_defined?(:attributes) def attributes - instance_values.except("address", "friends") + instance_values.except("address", "friends", "contact") end end @@ -55,6 +55,9 @@ class XmlSerializationTest < ActiveModel::TestCase @contact.address.state = "CA" @contact.address.zip = 11111 @contact.friends = [Contact.new, Contact.new] + @related_contact = SerializableContact.new + @related_contact.name = "related" + @contact.contact = @related_contact end test "should serialize default root" do @@ -203,4 +206,9 @@ class XmlSerializationTest < ActiveModel::TestCase assert_match %r{<friends>}, xml assert_match %r{<friend>}, xml end + + test "association with sti" do + xml = @contact.to_xml(:include => :contact) + assert xml.include?(%(<contact type="SerializableContact">)) + end end diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index bd8a0bc039..1d682e03bf 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,185 @@ +## Rails 3.2.11 (unreleased) + +* Serialized attributes can be serialized in integer columns. + Fix #8575. + + *Rafael Mendonça França* + +* Keep index names when using `alter_table` with sqlite3. + Fix #3489. + Backport #8522. + + *Yves Senn* + +* Recognize migrations placed in directories containing numbers and 'rb'. + Fix #8492. + Backport of #8500. + + *Yves Senn* + +* Add `ActiveRecord::Base.cache_timestamp_format` class attribute to control + the format of the timestamp value in the cache key. + This allows users to improve the precision of the cache key. + Fixes #8195. + + *Rafael Mendonça França* + +* Add `:nsec` date format. This can be used to improve the precision of cache key. + Please note that this format only works with Ruby 1.9, Ruby 1.8 will ignore it completely. + + *Jamie Gaskins* + +* Unscope `update_column(s)` query to ignore default scope. + + When applying `default_scope` to a class with a where clause, using + `update_column(s)` could generate a query that would not properly update + the record due to the where clause from the `default_scope` being applied + to the update query. + + class User < ActiveRecord::Base + default_scope where(active: true) + end + + user = User.first + user.active = false + user.save! + + user.update_column(:active, true) # => false + + In this situation we want to skip the default_scope clause and just + update the record based on the primary key. With this change: + + user.update_column(:active, true) # => true + + Backport of #8436 fix. + + *Carlos Antonio da Silva* + +* Fix performance problem with primary_key method in PostgreSQL adapter when having many schemas. + Uses pg_constraint table instead of pg_depend table which has many records in general. + Fix #8414 + + *kennyj* + +* Do not instantiate intermediate Active Record objects when eager loading. + These records caused `after_find` to run more than expected. + Fix #3313 + Backport of #8403 + + *Yves Senn* + +* Fix `pluck` to work with joins. Backport of #4942. + + *Carlos Antonio da Silva* + +* Fix a problem with `translate_exception` method in a non English environment. + Backport of #6397. + + *kennyj* + +* Fix dirty attribute checks for TimeZoneConversion with nil and blank + datetime attributes. Setting a nil datetime to a blank string should not + result in a change being flagged. + Fixes #8310. + Backport of #8311. + + *Alisdair McDiarmid* + +* Prevent mass assignment to the type column of polymorphic associations when using `build`. + Fixes #8265. + Backport of #8291. + + *Yves Senn* + +* When running migrations on Postgresql, the `:limit` option for `binary` and `text` columns is + silently dropped. + Previously, these migrations caused sql exceptions, because Postgresql doesn't support limits + on these types. + + *Victor Costan* + +* Calling `include?` on `has_many` associations on unsaved records no longer + returns `true` when passed a record with a `nil` foreign key. + Fixes #7950. + + *George Brocklehurst* + +* `#pluck` can be used on a relation with `select` clause. + Fixes #7551. + Backport of #8176. + + Example: + + Topic.select([:approved, :id]).order(:id).pluck(:id) + + *Yves Senn* + +* Use `nil?` instead of `blank?` to check whether dynamic finder with a bang + should raise RecordNotFound. + Fixes #7238. + + *Nikita Afanasenko* + +* Fix deleting from a HABTM join table upon destroying an object of a model + with optimistic locking enabled. + Fixes #5332. + + *Nick Rogers* + +* Use query cache/uncache when using ENV["DATABASE_URL"]. + Fixes #6951. + Backport of #8074. + + *kennyj* + +* Do not create useless database transaction when building `has_one` association. + + Example: + + User.has_one :profile + User.new.build_profile + + Backport of #8154. + + *Bogdan Gusiev* + +* `AR::Base#attributes_before_type_cast` now returns unserialized values for serialized attributes. + + *Nikita Afanasenko* + +* Fix issue that raises `NameError` when overriding the `accepts_nested_attributes` in child classes. + + Before: + + class Shared::Person < ActiveRecord::Base + has_one :address + + accepts_nested_attributes :address, :reject_if => :all_blank + end + + class Person < Shared::Person + accepts_nested_attributes :address + end + + Person + #=> NameError: method `address_attributes=' not defined in Person + + After: + + Person + #=> Person(id: integer, ...) + + Fixes #8131. + + *Gabriel Sobrinho, Ricardo Henrique* + + +## Rails 3.2.9 (Nov 12, 2012) ## + +* Fix `find_in_batches` crashing when IDs are strings and start option is not specified. + + *Alexis Bernard* + ## Rails 3.2.10 ## * CVE-2012-5664 options hashes should only be extracted if there are extra diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 59c1bad559..ab0d888b16 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -231,7 +231,8 @@ module ActiveRecord def build_record(attributes, options) reflection.build_association(attributes, options) do |record| - attributes = create_scope.except(*(record.changed - [reflection.foreign_key])) + skip_assign = [reflection.foreign_key, reflection.type].compact + attributes = create_scope.except(*(record.changed - skip_assign)) record.assign_attributes(attributes, :without_protection => true) end end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index f9cffa40c8..226639ad37 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -33,6 +33,18 @@ module ActiveRecord private + def column_for(table_name, column_name) + columns = alias_tracker.connection.schema_cache.columns_hash[table_name] + columns[column_name] + end + + def bind(scope, column, value) + substitute = alias_tracker.connection.substitute_at( + column, scope.bind_values.length) + scope.bind_values += [[column, value]] + substitute + end + def add_constraints(scope) tables = construct_tables @@ -67,7 +79,9 @@ module ActiveRecord conditions = self.conditions[i] if reflection == chain.last - scope = scope.where(table[key].eq(owner[foreign_key])) + column = column_for(table.table_name, key.to_s) + bind_val = bind(scope, column, owner[foreign_key]) + scope = scope.where(table[key].eq(bind_val)) if reflection.type scope = scope.where(table[reflection.type].eq(owner.class.base_class.name)) diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 501ebe7c5b..56f9013d61 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -11,7 +11,7 @@ module ActiveRecord # If target and record are nil, or target is equal to record, # we don't need to have transaction. if (target || record) && target != record - reflection.klass.transaction do + transaction_if(save) do remove_target!(options[:dependent]) if target && !target.destroyed? if record @@ -70,6 +70,14 @@ module ActiveRecord def nullify_owner_attributes(record) record[reflection.foreign_key] = nil end + + def transaction_if(value) + if value + reflection.klass.transaction { yield } + else + yield + end + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 00023b0b6c..deb89f3ac1 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -90,6 +90,14 @@ module ActiveRecord end end + def _field_changed?(attr, old, value) + if self.class.serialized_attributes.include?(attr) + old != value + else + super + end + end + def read_attribute_before_type_cast(attr_name) if serialized_attributes.include?(attr_name) super.unserialized_value @@ -97,6 +105,16 @@ module ActiveRecord super end end + + def attributes_before_type_cast + super.dup.tap do |attributes| + self.class.serialized_attributes.each_key do |key| + if attributes.key?(key) + attributes[key] = attributes[key].unserialized_value + end + end + end + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index 4c3d5eed4c..39d81cf6ef 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -37,6 +37,7 @@ module ActiveRecord if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) method_body, line = <<-EOV, __LINE__ + 1 def #{attr_name}=(original_time) + original_time = nil if original_time.blank? time = original_time unless time.acts_like?(:time) time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 62c8110274..612114bbce 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -557,7 +557,7 @@ module ActiveRecord #:nodoc: end # Backport dup from 1.9 so that initialize_dup() gets called - unless Object.respond_to?(:initialize_dup) + unless Object.respond_to?(:initialize_dup, true) def dup # :nodoc: copy = super copy.initialize_dup(self) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index db99c3fbef..c49aed7069 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -4,6 +4,7 @@ module ActiveRecord # Converts an arel AST to SQL def to_sql(arel, binds = []) if arel.respond_to?(:ast) + binds = binds.dup visitor.accept(arel.ast) do quote(*binds.shift.reverse) end @@ -20,14 +21,14 @@ module ActiveRecord # Returns a record hash with the column names as keys and column values # as values. - def select_one(arel, name = nil) - result = select_all(arel, name) + def select_one(arel, name = nil, binds = []) + result = select_all(arel, name, binds) result.first if result end # Returns a single value from a record - def select_value(arel, name = nil) - if result = select_one(arel, name) + def select_value(arel, name = nil, binds = []) + if result = select_one(arel, name, binds) result.values.first end end @@ -266,7 +267,7 @@ module ActiveRecord # Inserts the given fixture into the table. Overridden in adapters that require # something beyond a simple insert (eg. Oracle). def insert_fixture(fixture, table_name) - columns = Hash[columns(table_name).map { |c| [c.name, c] }] + columns = schema_cache.columns_hash(table_name) key_list = [] value_list = fixture.map do |name, value| diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index c132692249..4850b68051 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -2,7 +2,7 @@ require 'active_record/connection_adapters/abstract_mysql_adapter' require 'active_record/connection_adapters/statement_pool' require 'active_support/core_ext/hash/keys' -gem 'mysql', '~> 2.8.1' +gem 'mysql', '~> 2.8' require 'mysql' class Mysql diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 80a0c6d13c..5a53135901 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -988,12 +988,11 @@ module ActiveRecord # Returns just a table's primary key def primary_key(table) row = exec_query(<<-end_sql, 'SCHEMA').rows.first - SELECT DISTINCT(attr.attname) + SELECT attr.attname FROM pg_attribute attr - INNER JOIN pg_depend dep ON attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1] WHERE cons.contype = 'p' - AND dep.refobjid = '#{quote_table_name(table)}'::regclass + AND cons.conrelid = '#{quote_table_name(table)}'::regclass end_sql row && row.first @@ -1078,6 +1077,13 @@ module ActiveRecord when nil, 0..0x3fffffff; super(type) else raise(ActiveRecordError, "No binary type has byte size #{limit}.") end + when 'text' + # PostgreSQL doesn't support limits on text columns. + # The hard limit is 1Gb, according to section 8.3 in the manual. + case limit + when nil, 0..0x3fffffff; super(type) + else raise(ActiveRecordError, "The limit on text can be at most 1GB - 1byte.") + end when 'integer' return 'integer' unless limit @@ -1135,11 +1141,15 @@ module ActiveRecord @connection.server_version end + # See http://www.postgresql.org/docs/9.1/static/errcodes-appendix.html + FOREIGN_KEY_VIOLATION = "23503" + UNIQUE_VIOLATION = "23505" + def translate_exception(exception, message) - case exception.message - when /duplicate key value violates unique constraint/ + case exception.result.error_field(PGresult::PG_DIAG_SQLSTATE) + when UNIQUE_VIOLATION RecordNotUnique.new(message, exception) - when /violates foreign key constraint/ + when FOREIGN_KEY_VIOLATION InvalidForeignKey.new(message, exception) else super diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index 4e8932a695..bc8d24a03f 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters class SchemaCache - attr_reader :columns, :columns_hash, :primary_keys, :tables + attr_reader :primary_keys, :tables attr_reader :connection def initialize(conn) @@ -30,6 +30,25 @@ module ActiveRecord @tables[name] = connection.table_exists?(name) end + # Get the columns for a table + def columns(table = nil) + if table + @columns[table] + else + @columns + end + end + + # Get the columns for a table as a hash, key is the column name + # value is the column object. + def columns_hash(table = nil) + if table + @columns_hash[table] + else + @columns_hash + end + end + # Clears out internal caches def clear! @columns.clear diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index e80b465bab..ca84c95bdc 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -530,7 +530,7 @@ module ActiveRecord unless columns.empty? # index name can't be the same - opts = { :name => name.gsub(/_(#{from})_/, "_#{to}_") } + opts = { :name => name.gsub(/(^|_)(#{from})_/, "\\1#{to}_") } opts[:unique] = true if index.unique add_index(to, columns, opts) end diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 2c42f4cca5..f2ace18d44 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -1,5 +1,16 @@ module ActiveRecord module Integration + extend ActiveSupport::Concern + + included do + ## + # :singleton-method: + # Indicates the format used to generate the timestamp format in the cache key. + # This is +:number+, by default. + class_attribute :cache_timestamp_format, :instance_writer => false + self.cache_timestamp_format = :number + end + # Returns a String, which Action Pack uses for constructing an URL to this # object. The default implementation returns this record's id as a String, # or nil if this record's unsaved. @@ -39,7 +50,7 @@ module ActiveRecord when new_record? "#{self.class.model_name.cache_key}/new" when timestamp = self[:updated_at] - timestamp = timestamp.utc.to_s(:number) + timestamp = timestamp.utc.to_s(cache_timestamp_format) "#{self.class.model_name.cache_key}/#{id}-#{timestamp}" else "#{self.class.model_name.cache_key}/#{id}" diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 7deac2588a..b2881991d5 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -102,6 +102,8 @@ module ActiveRecord def destroy #:nodoc: return super unless locking_enabled? + destroy_associations + if persisted? table = self.class.arel_table lock_col = self.class.locking_column diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index e6686597b7..e8ec93470d 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -458,7 +458,7 @@ module ActiveRecord say_with_time "#{method}(#{arg_list})" do unless reverting? unless arguments.empty? || method == :execute - arguments[0] = Migrator.proper_table_name(arguments.first) + arguments[0] = Migrator.proper_table_name(arguments.first) unless method == :assume_migrated_upto_version arguments[1] = Migrator.proper_table_name(arguments.second) if method == :rename_table end end @@ -627,7 +627,7 @@ module ActiveRecord seen = Hash.new false migrations = files.map do |file| - version, name, scope = file.scan(/([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?.rb/).first + version, name, scope = file.scan(/([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/).first raise IllegalMigrationNameError.new(file) unless version version = version.to_i diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index d2065d701f..05091654c0 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -277,13 +277,14 @@ module ActiveRecord type = (reflection.collection? ? :collection : :one_to_one) + # remove_possible_method :pirate_attributes= + # # def pirate_attributes=(attributes) # assign_nested_attributes_for_one_to_one_association(:pirate, attributes, mass_assignment_options) # end class_eval <<-eoruby, __FILE__, __LINE__ + 1 - if method_defined?(:#{association_name}_attributes=) - remove_method(:#{association_name}_attributes=) - end + remove_possible_method(:#{association_name}_attributes=) + def #{association_name}_attributes=(attributes) assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes, mass_assignment_options) end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index fd3380a53c..36b6b5fce2 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -194,7 +194,7 @@ module ActiveRecord raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name) raise ActiveRecordError, "can not update on a new record object" unless persisted? - updated_count = self.class.update_all({ name => value }, self.class.primary_key => id) + updated_count = self.class.unscoped.update_all({ name => value }, self.class.primary_key => id) raw_write_attribute(name, value) diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index 466d148901..2156889a0f 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -6,19 +6,19 @@ module ActiveRecord module ClassMethods # Enable the query cache within the block if Active Record is configured. def cache(&block) - if ActiveRecord::Base.configurations.blank? - yield - else + if ActiveRecord::Base.connected? connection.cache(&block) + else + yield end end # Disable the query cache within the block if Active Record is configured. def uncached(&block) - if ActiveRecord::Base.configurations.blank? - yield - else + if ActiveRecord::Base.connected? connection.uncached(&block) + else + yield end end end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index ae6d9c8653..1bdb841398 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -64,10 +64,21 @@ db_namespace = namespace :db do end end + # If neither encoding nor collation is specified, use the utf-8 defaults. def mysql_creation_options(config) - @charset = ENV['CHARSET'] || 'utf8' - @collation = ENV['COLLATION'] || 'utf8_unicode_ci' - {:charset => (config['encoding'] || @charset), :collation => (config['collation'] || @collation)} + default_charset = ENV['CHARSET'] || 'utf8' + default_collation = ENV['COLLATION'] || 'utf8_unicode_ci' + + Hash.new.tap do |options| + options[:charset] = config['encoding'] if config.include? 'encoding' + options[:collation] = config['collation'] if config.include? 'collation' + + # Set default charset only when collation isn't set. + options[:charset] ||= default_charset unless options[:collation] + + # Set default collation only when charset is also default. + options[:collation] ||= default_collation if options[:charset] == default_charset + end end def create_database(config) @@ -101,9 +112,12 @@ db_namespace = namespace :db do error_class = config['adapter'] =~ /mysql2/ ? Mysql2::Error : Mysql::Error end access_denied_error = 1045 + + create_options = mysql_creation_options(config) + begin ActiveRecord::Base.establish_connection(config.merge('database' => nil)) - ActiveRecord::Base.connection.create_database(config['database'], mysql_creation_options(config)) + ActiveRecord::Base.connection.create_database(config['database'], create_options) ActiveRecord::Base.establish_connection(config) rescue error_class => sqlerr if sqlerr.errno == access_denied_error @@ -119,7 +133,7 @@ db_namespace = namespace :db do ActiveRecord::Base.establish_connection(config) else $stderr.puts sqlerr.error - $stderr.puts "Couldn't create database for #{config.inspect}, charset: #{config['encoding'] || @charset}, collation: #{config['collation'] || @collation}" + $stderr.puts "Couldn't create database for #{config.inspect}, charset: #{create_options[:charset]}, collation: #{create_options[:collation]}" $stderr.puts "(if you set the charset manually, make sure you have a matching collation)" if config['encoding'] end end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 4b3b30d6ed..a727f40194 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -464,7 +464,12 @@ module ActiveRecord node.left.relation.name == table_name } - Hash[equalities.map { |where| [where.left.name, where.right] }] + binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }] + + Hash[equalities.map { |where| + name = where.left.name + [name, binds.fetch(name.to_s) { where.right }] + }] end def scope_for_create diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 2fd89882ff..14701f668c 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -59,11 +59,11 @@ module ActiveRecord relation = apply_finder_options(finder_options) end - start = options.delete(:start).to_i + start = options.delete(:start) batch_size = options.delete(:batch_size) || 1000 relation = relation.reorder(batch_order).limit(batch_size) - records = relation.where(table[primary_key].gteq(start)).all + records = start ? relation.where(table[primary_key].gteq(start)).to_a : relation.to_a while records.any? records_size = records.size diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 802059db21..20c5fa03a2 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -177,8 +177,15 @@ module ActiveRecord # Person.where(:confirmed => true).limit(5).pluck(:id) # def pluck(column_name) - column_name = column_name.to_s - klass.connection.select_all(select(column_name).arel).map! do |attributes| + if column_name.is_a?(Symbol) && column_names.include?(column_name.to_s) + column_name = "#{table_name}.#{column_name}" + else + column_name = column_name.to_s + end + + relation = clone + relation.select_values = [column_name] + klass.connection.select_all(relation.arel, nil, bind_values).map! do |attributes| klass.type_cast_attribute(attributes.keys.first, klass.initialize_attributes(attributes)) end end @@ -240,7 +247,8 @@ module ActiveRecord query_builder = relation.arel end - type_cast_calculated_value(@klass.connection.select_value(query_builder), column_for(column_name), operation) + result = @klass.connection.select_value(query_builder, nil, relation.bind_values) + type_cast_calculated_value(result, column_for(column_name), operation) end def execute_grouped_calculation(operation, column_name, distinct) #:nodoc: @@ -286,7 +294,7 @@ module ActiveRecord relation = except(:group).group(group) relation.select_values = select_values - calculated_data = @klass.connection.select_all(relation) + calculated_data = @klass.connection.select_all(relation, nil, bind_values) if association key_ids = calculated_data.collect { |row| row[group_aliases.first] } diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index abc67d9c15..cbb2f676c8 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -199,7 +199,7 @@ module ActiveRecord relation = relation.where(table[primary_key].eq(id)) if id end - connection.select_value(relation, "#{name} Exists") ? true : false + connection.select_value(relation, "#{name} Exists", relation.bind_values) ? true : false rescue ThrowResult false end @@ -253,9 +253,11 @@ module ActiveRecord orders = relation.order_values.map { |val| val.presence }.compact values = @klass.connection.distinct("#{@klass.connection.quote_table_name table_name}.#{primary_key}", orders) - relation = relation.dup + relation = relation.dup.select(values) + + id_rows = @klass.connection.select_all(relation.arel, 'SQL', relation.bind_values) + ids_array = id_rows.map {|row| row[primary_key]} - ids_array = relation.select(values).collect {|row| row[primary_key]} ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array) end @@ -263,7 +265,7 @@ module ActiveRecord conditions = Hash[attributes.map {|a| [a, args[attributes.index(a)]]}] result = where(conditions).send(match.finder) - if match.bang? && result.blank? + if match.bang? && result.nil? raise RecordNotFound, "Couldn't find #{@klass.name} with #{conditions.to_a.collect {|p| p.join(' = ')}.join(', ')}" else yield(result) if block_given? @@ -332,7 +334,7 @@ module ActiveRecord substitute = connection.substitute_at(column, @bind_values.length) relation = where(table[primary_key].eq(substitute)) - relation.bind_values = [[column, id]] + relation.bind_values += [[column, id]] record = relation.first unless record diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index c25570d758..3eead56e47 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -22,7 +22,7 @@ module ActiveRecord end end - (Relation::MULTI_VALUE_METHODS - [:joins, :where, :order]).each do |method| + (Relation::MULTI_VALUE_METHODS - [:joins, :where, :order, :binds]).each do |method| value = r.send(:"#{method}_values") merged_relation.send(:"#{method}_values=", merged_relation.send(:"#{method}_values") + value) if value.present? end @@ -31,6 +31,8 @@ module ActiveRecord merged_wheres = @where_values + r.where_values + merged_binds = (@bind_values + r.bind_values).uniq(&:first) + unless @where_values.empty? # Remove duplicates, last one wins. seen = Hash.new { |h,table| h[table] = {} } @@ -47,6 +49,7 @@ module ActiveRecord end merged_relation.where_values = merged_wheres + merged_relation.bind_values = merged_binds (Relation::SINGLE_VALUE_METHODS - [:lock, :create_with, :reordering]).each do |method| value = r.send(:"#{method}_value") diff --git a/activerecord/test/cases/adapters/postgresql/sql_types_test.rb b/activerecord/test/cases/adapters/postgresql/sql_types_test.rb new file mode 100644 index 0000000000..d7d40f6385 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/sql_types_test.rb @@ -0,0 +1,18 @@ +require "cases/helper" + +class SqlTypesTest < ActiveRecord::TestCase + def test_binary_types + assert_equal 'bytea', type_to_sql(:binary, 100_000) + assert_raise ActiveRecord::ActiveRecordError do + type_to_sql :binary, 4294967295 + end + assert_equal 'text', type_to_sql(:text, 100_000) + assert_raise ActiveRecord::ActiveRecordError do + type_to_sql :text, 4294967295 + end + end + + def type_to_sql(*args) + ActiveRecord::Base.connection.type_to_sql(*args) + end +end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index ec69a36174..614be8760a 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -157,7 +157,6 @@ module ActiveRecord binary = DualEncoding.new :name => 'いただきます!', :data => str binary.save! assert_equal str, binary.data - ensure if "<3".respond_to?(:encode) DualEncoding.connection.drop_table('dual_encodings') @@ -167,8 +166,9 @@ module ActiveRecord def test_type_cast_should_not_mutate_encoding return skip('only test encoding on 1.9') unless "<3".encoding_aware? - name = 'hello'.force_encoding(Encoding::ASCII_8BIT) - owner = Owner.create(:name => name) + name = 'hello'.force_encoding(Encoding::ASCII_8BIT) + Owner.create(:name => name) + assert_equal Encoding::ASCII_8BIT, name.encoding end diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index 588adc38e3..ee338a3b99 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -11,23 +11,30 @@ if ActiveRecord::Base.connection.supports_migrations? def teardown @connection.drop_table :fruits rescue nil + @connection.drop_table :"_pre_fruits_suf_" rescue nil + @connection.drop_table :"_pre_schema_migrations_suf_" rescue nil end def test_schema_define - ActiveRecord::Schema.define(:version => 7) do - create_table :fruits do |t| - t.column :color, :string - t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle - t.column :texture, :string - t.column :flavor, :string - end - end + perform_schema_define! assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" } assert_nothing_raised { @connection.select_all "SELECT * FROM schema_migrations" } assert_equal 7, ActiveRecord::Migrator::current_version end + def test_schema_define_with_table_prefix_and_suffix + ActiveRecord::Base.table_name_prefix = '_pre_' + ActiveRecord::Base.table_name_suffix = '_suf_' + + perform_schema_define! + + assert_equal 7, ActiveRecord::Migrator::current_version + ensure + ActiveRecord::Base.table_name_prefix = '' + ActiveRecord::Base.table_name_suffix = '' + end + def test_schema_raises_an_error_for_invalid_column_type assert_raise NoMethodError do ActiveRecord::Schema.define(:version => 8) do @@ -39,4 +46,17 @@ if ActiveRecord::Base.connection.supports_migrations? end end + private + + def perform_schema_define! + ActiveRecord::Schema.define(:version => 7) do + create_table :fruits do |t| + t.column :color, :string + t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle + t.column :texture, :string + t.column :flavor, :string + end + end + end + end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index bf01b46852..d3601e0dba 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -916,6 +916,12 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal 3, Developer.find(:all, :include => 'projects', :conditions => 'developers_projects.access_level = 1', :limit => 5).size end + def test_dont_create_temporary_active_record_instances + Developer.instance_count = 0 + developers = Developer.find(:all, :include => 'projects', :conditions => 'developers_projects.access_level = 1', :limit => 5).to_a + assert_equal developers.count, Developer.instance_count + end + def test_order_on_join_table_with_include_and_limit assert_equal 5, Developer.find(:all, :include => 'projects', :order => 'developers_projects.joined_on DESC', :limit => 5).size end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index ed4475770b..b4788e0a3d 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -1225,6 +1225,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert companies(:first_firm).clients.include?(Client.find(2)) end + def test_included_in_collection_for_new_records + client = Client.create(:name => 'Persisted') + assert_nil client.client_of + assert !Firm.new.clients_of_firm.include?(client), + 'includes a client that does not belong to any firm' + end + def test_adding_array_and_collection assert_nothing_raised { Firm.find(:first).clients + Firm.find(:all).last.clients } end @@ -1697,6 +1704,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [tagging], post.taggings end + def test_build_with_polymotphic_has_many_does_not_allow_to_override_type_and_id + welcome = posts(:welcome) + tagging = welcome.taggings.build(:taggable_id => 99, :taggable_type => 'ShouldNotChange') + + assert_equal welcome.id, tagging.taggable_id + assert_equal 'Post', tagging.taggable_type + end + def test_dont_call_save_callbacks_twice_on_has_many firm = companies(:first_firm) contract = firm.contracts.create! diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 08831a42ba..31aa3788c7 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -173,6 +173,12 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal account, firm.account end + def test_build_association_dont_create_transaction + assert_no_queries { + Firm.new.build_account + } + end + def test_build_and_create_should_not_happen_within_scope pirate = pirates(:blackbeard) scoped_count = pirate.association(:foo_bulb).scoped.where_values.count diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 073e856e5e..b849eead26 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -23,6 +23,8 @@ require 'models/edge' require 'models/joke' require 'models/bulb' require 'models/bird' +require 'models/car' +require 'models/bulb' require 'rexml/document' require 'active_support/core_ext/exception' require 'bcrypt' @@ -1293,6 +1295,16 @@ class BasicsTest < ActiveRecord::TestCase assert_equal({ :foo => :bar }, t.content_before_type_cast) end + def test_serialized_attributes_before_type_cast_returns_unserialized_value + Topic.serialize :content, Hash + + t = Topic.new(:content => { :foo => :bar }) + assert_equal({ :foo => :bar }, t.attributes_before_type_cast["content"]) + t.save! + t.reload + assert_equal({ :foo => :bar }, t.attributes_before_type_cast["content"]) + end + def test_serialized_attribute_calling_dup_method klass = Class.new(ActiveRecord::Base) klass.table_name = "topics" @@ -1430,6 +1442,29 @@ class BasicsTest < ActiveRecord::TestCase Topic.serialize(:content) end + def test_serialize_attribute_via_select_method_when_time_zone_available + ActiveRecord::Base.time_zone_aware_attributes = true + Topic.serialize(:content, MyObject) + + myobj = MyObject.new('value1', 'value2') + topic = Topic.create(:content => myobj) + + ActiveRecord::IdentityMap.without do + assert_equal(myobj, Topic.select(:content).find(topic.id).content) + assert_raise(ActiveModel::MissingAttributeError) { Topic.select(:id).find(topic.id).content } + end + ensure + ActiveRecord::Base.time_zone_aware_attributes = false + end + + def test_serialize_attribute_can_be_serialized_in_an_integer_column + insures = ['life'] + person = SerializedPerson.new(:first_name => 'David', :insures => insures) + assert person.save + person = person.reload + assert_equal(insures, person.insures) + end + def test_quote author_name = "\\ \001 ' \n \\n \"" topic = Topic.create('author_name' => author_name) @@ -2144,6 +2179,29 @@ class BasicsTest < ActiveRecord::TestCase assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:number)}", dev.cache_key end + def test_cache_key_format_for_existing_record_with_updated_at_and_custom_cache_timestamp_format + return skip "Only in Ruby 1.9" if RUBY_VERSION < '1.9' + + dev = CachedDeveloper.first + assert_equal "cached_developers/#{dev.id}-#{dev.updated_at.utc.to_s(:nsec)}", dev.cache_key + end + + def test_cache_key_changes_when_child_touched + return skip "Only in Ruby 1.9" if RUBY_VERSION < '1.9' + + old_timestamp_format = Car.cache_timestamp_format + Car.cache_timestamp_format = :nsec + car = Car.create + Bulb.create(:car => car) + + key = car.cache_key + car.bulb.touch + car.reload + assert_not_equal key, car.cache_key + ensure + Car.cache_timestamp_format = old_timestamp_format + end + def test_cache_key_format_for_existing_record_with_nil_updated_at dev = Developer.first dev.update_attribute(:updated_at, nil) diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index 660098b9ad..ad2a749ab4 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -1,8 +1,9 @@ require 'cases/helper' require 'models/post' +require 'models/subscriber' class EachTest < ActiveRecord::TestCase - fixtures :posts + fixtures :posts, :subscribers def setup @posts = Post.order("id asc") @@ -136,4 +137,24 @@ class EachTest < ActiveRecord::TestCase assert_equal special_posts_ids, posts.map(&:id) end + def test_find_in_batches_should_use_any_column_as_primary_key + nick_order_subscribers = Subscriber.order('nick asc') + start_nick = nick_order_subscribers.second.nick + + subscribers = [] + Subscriber.find_in_batches(:batch_size => 1, :start => start_nick) do |batch| + subscribers.concat(batch) + end + + assert_equal nick_order_subscribers[1..-1].map(&:id), subscribers.map(&:id) + end + + def test_find_in_batches_should_use_any_column_as_primary_key_when_start_is_not_specified + Subscriber.count('nick') # preheat arel's table cache + assert_queries(Subscriber.count + 1) do + Subscriber.find_each(:batch_size => 1) do |subscriber| + assert_kind_of Subscriber, subscriber + end + end + end end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index cf1181e829..f931b39998 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -487,4 +487,20 @@ class CalculationsTest < ActiveRecord::TestCase def test_pluck_with_qualified_column_name assert_equal [1,2,3,4], Topic.order(:id).pluck("topics.id") end + + def test_pluck_replaces_select_clause + taks_relation = Topic.select([:approved, :id]).order(:id) + assert_equal [1,2,3,4], taks_relation.pluck(:id) + assert_equal [false, true, true, true], taks_relation.pluck(:approved) + end + + def test_pluck_auto_table_name_prefix + c = Company.create!(:name => "test", :contracts => [Contract.new]) + assert_equal [c.id], Company.joins(:contracts).pluck(:id) + end + + def test_pluck_not_auto_table_name_prefix_if_column_joined + Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)]) + assert_equal [7], Company.joins(:contracts).pluck(:developer_id).map(&:to_i) + end end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 4fc7e77786..86a28d95ad 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -201,6 +201,20 @@ class DirtyTest < ActiveRecord::TestCase end end + def test_nullable_datetime_not_marked_as_changed_if_new_value_is_blank + in_time_zone 'Edinburgh' do + target = Class.new(ActiveRecord::Base) + target.table_name = 'topics' + + topic = target.create + assert_equal nil, topic.written_on + + topic.written_on = "" + assert_equal nil, topic.written_on + assert !topic.written_on_changed? + end + end + def test_integer_zero_to_string_zero_not_marked_as_changed pirate = Pirate.new pirate.parrot_id = 0 diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 77ca3b574d..7d63d76c34 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -664,6 +664,11 @@ class FinderTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordNotFound) { Topic.find_by_title!("The First Topic!") } end + def test_find_by_one_attribute_bang_with_blank_defined + blank_topic = BlankTopic.create(:title => "The Blank One") + assert_equal blank_topic, BlankTopic.find_by_title!("The Blank One") + end + def test_find_by_one_attribute_with_order_option assert_equal accounts(:signals37), Account.find_by_credit_limit(50, :order => 'id') assert_equal accounts(:rails_core_account), Account.find_by_credit_limit(50, :order => 'id DESC') diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index e352a55104..cf49b125d7 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -4,7 +4,7 @@ require 'config' require 'test/unit' require 'stringio' -require 'mocha' +require 'mocha/setup' require 'active_record' require 'active_support/dependencies' diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 015a3ccefd..066a60f81a 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -7,6 +7,7 @@ require 'models/ship' require 'models/legacy_thing' require 'models/reference' require 'models/string_key_object' +require 'models/treasure' class LockWithoutDefault < ActiveRecord::Base; end @@ -20,7 +21,7 @@ class ReadonlyNameShip < Ship end class OptimisticLockingTest < ActiveRecord::TestCase - fixtures :people, :legacy_things, :references, :string_key_objects + fixtures :people, :legacy_things, :references, :string_key_objects, :peoples_treasures def test_non_integer_lock_existing s1 = StringKeyObject.find("record1") @@ -267,6 +268,15 @@ class SetLockingColumnTest < ActiveRecord::TestCase assert_equal "omg", k.original_locking_column end end + + def test_removing_has_and_belongs_to_many_associations_upon_destroy + p = RichPerson.create! :first_name => 'Jon' + p.treasures.create! + assert !p.treasures.empty? + p.destroy + assert p.treasures.empty? + assert RichPerson.connection.select_all("SELECT * FROM peoples_treasures WHERE rich_person_id = 1").empty? + end end class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index f0c55b1535..7bb71b7b6e 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -1051,6 +1051,18 @@ if ActiveRecord::Base.connection.supports_migrations? Person.connection.remove_column("people", "administrator") rescue nil end + def test_change_column_with_custom_index_name + Person.connection.add_column "people", "category", :string, :default => 'human' + Person.connection.add_index :people, :category, :name => 'people_categories_idx' + + assert_equal ['people_categories_idx'], Person.connection.indexes('people').map(&:name) + Person.connection.change_column "people", "category", :string, :null => false, :default => 'article' + + assert_equal ['people_categories_idx'], Person.connection.indexes('people').map(&:name) + ensure + Person.connection.remove_column("people", "category") rescue nil + end + def test_change_column_default Person.connection.change_column_default "people", "first_name", "Tester" Person.reset_column_information @@ -1430,6 +1442,12 @@ if ActiveRecord::Base.connection.supports_migrations? end end + def test_finds_migrations_in_numbered_directory + migrations = ActiveRecord::Migrator.migrations [MIGRATIONS_ROOT + '/10_urban'] + assert_equal 9, migrations[0].version + assert_equal 'AddExpressions', migrations[0].name + end + def test_dump_schema_information_outputs_lexically_ordered_versions migration_path = MIGRATIONS_ROOT + '/valid_with_timestamps' ActiveRecord::Migrator.run(:up, migration_path, 20100301010101) diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 16b1eb040e..85b9d3c1a1 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -172,6 +172,17 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase man.interests_attributes = [{:id => interest.id, :topic => 'gardening'}] assert_equal man.interests.first.topic, man.interests[0].topic end + + def test_accepts_nested_attributes_for_can_be_overridden_in_subclasses + Pirate.accepts_nested_attributes_for(:parrot) + + mean_pirate_class = Class.new(Pirate) do + accepts_nested_attributes_for :parrot + end + mean_pirate = mean_pirate_class.new + mean_pirate.parrot_attributes = { :name => "James" } + assert_equal "James", mean_pirate.parrot.name + end end class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index a35cb3c4a5..00dc1f6d72 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -519,6 +519,14 @@ class PersistencesTest < ActiveRecord::TestCase assert return_value end + def test_update_column_with_default_scope + developer = DeveloperCalledDavid.first + developer.name = 'John' + developer.save! + + assert developer.update_column(:name, 'Will'), 'did not update record due to default scope' + end + def test_update_attributes topic = Topic.find(1) assert !topic.approved? diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index dd881f8230..dfc6ea2457 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -173,6 +173,17 @@ class QueryCacheTest < ActiveRecord::TestCase assert_queries(2) { task.lock!; task.lock! } end end + + def test_cache_is_available_when_connection_is_connected + conf = ActiveRecord::Base.configurations + + ActiveRecord::Base.configurations = {} + Task.cache do + assert_queries(1) { Task.find(1); Task.find(1) } + end + ensure + ActiveRecord::Base.configurations = conf + end end class QueryCacheExpiryTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index b24df47efd..ada4294401 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -1185,4 +1185,18 @@ class RelationTest < ActiveRecord::TestCase end assert_equal ['Foo', 'Foo'], query.uniq(true).uniq(false).map(&:name) end + + test 'group with select and includes' do + authors_count = Post.select('author_id, COUNT(author_id) AS num_posts'). + group('author_id').order('author_id').includes(:author).to_a + + assert_no_queries do + result = authors_count.map do |post| + [post.num_posts.to_i, post.author.try(:name)] + end + + expected = [[1, nil], [5, "David"], [3, "Mary"], [2, "Bob"]] + assert_equal expected, result + end + end end diff --git a/activerecord/test/fixtures/peoples_treasures.yml b/activerecord/test/fixtures/peoples_treasures.yml new file mode 100644 index 0000000000..a72b190d0c --- /dev/null +++ b/activerecord/test/fixtures/peoples_treasures.yml @@ -0,0 +1,3 @@ +michael_diamond: + rich_person_id: <%= ActiveRecord::Fixtures.identify(:michael) %> + treasure_id: <%= ActiveRecord::Fixtures.identify(:diamond) %> diff --git a/activerecord/test/migrations/10_urban/9_add_expressions.rb b/activerecord/test/migrations/10_urban/9_add_expressions.rb new file mode 100644 index 0000000000..79a342e574 --- /dev/null +++ b/activerecord/test/migrations/10_urban/9_add_expressions.rb @@ -0,0 +1,11 @@ +class AddExpressions < ActiveRecord::Migration + def self.up + create_table("expressions") do |t| + t.column :expression, :string + end + end + + def self.down + drop_table "expressions" + end +end diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb index 888afc7604..ab3caa3f79 100644 --- a/activerecord/test/models/bulb.rb +++ b/activerecord/test/models/bulb.rb @@ -1,6 +1,6 @@ class Bulb < ActiveRecord::Base default_scope where(:name => 'defaulty') - belongs_to :car + belongs_to :car, :touch => true attr_protected :car_id, :frickinawesome diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index 4dc9fff9fd..f8b758b3c2 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -63,6 +63,15 @@ class Developer < ActiveRecord::Base self.all end end + + after_find :track_instance_count + cattr_accessor :instance_count + + def track_instance_count + self.class.instance_count ||= 0 + self.class.instance_count += 1 + end + private :track_instance_count end class AuditLog < ActiveRecord::Base @@ -236,3 +245,8 @@ class ThreadsafeDeveloper < ActiveRecord::Base limit(1) end end + +class CachedDeveloper < ActiveRecord::Base + self.table_name = "developers" + self.cache_timestamp_format = :nsec +end diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb index 5b92227f4a..07c529d685 100644 --- a/activerecord/test/models/person.rb +++ b/activerecord/test/models/person.rb @@ -86,3 +86,30 @@ class TightPerson < ActiveRecord::Base end class TightDescendant < TightPerson; end + +class RichPerson < ActiveRecord::Base + self.table_name = 'people' + + has_and_belongs_to_many :treasures, :join_table => 'peoples_treasures' +end + +class Insure + INSURES = %W{life annuality} + + def self.load mask + INSURES.select do |insure| + (1 << INSURES.index(insure)) & mask.to_i > 0 + end + end + + def self.dump insures + numbers = insures.map { |insure| INSURES.index(insure) } + numbers.inject(0) { |sum, n| sum + (1 << n) } + end +end + +class SerializedPerson < ActiveRecord::Base + self.table_name = 'people' + + serialize :insures, Insure +end diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index 1a1a18166a..5166fefe81 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -112,6 +112,12 @@ class ImportantTopic < Topic serialize :important, Hash end +class BlankTopic < Topic + def blank? + true + end +end + module Web class Topic < ActiveRecord::Base has_many :replies, :dependent => :destroy, :foreign_key => "parent_id", :class_name => 'Web::Reply' diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index 34e24b5a2e..b2c655ddcd 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -132,5 +132,10 @@ _SQL _SQL rescue #This version of PostgreSQL either has no XML support or is was not compiled with XML support: skipping table end + + create_table :limitless_fields, :force => true do |t| + t.binary :binary, :limit => 100_000 + t.text :text, :limit => 100_000 + end end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 8a3dfbb35a..1a993fef11 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -112,6 +112,7 @@ ActiveRecord::Schema.define do t.string :name t.integer :engines_count t.integer :wheels_count + t.timestamps end create_table :categories, :force => true do |t| @@ -478,9 +479,15 @@ ActiveRecord::Schema.define do t.integer :followers_count, :default => 0 t.references :best_friend t.references :best_friend_of + t.integer :insures, :null => false, :default => 0 t.timestamps end + create_table :peoples_treasures, :id => false, :force => true do |t| + t.column :rich_person_id, :integer + t.column :treasure_id, :integer + end + create_table :pets, :primary_key => :pet_id ,:force => true do |t| t.string :name t.integer :owner_id, :integer diff --git a/activeresource/CHANGELOG.md b/activeresource/CHANGELOG.md index 8d251bdce2..e77980f3b5 100644 --- a/activeresource/CHANGELOG.md +++ b/activeresource/CHANGELOG.md @@ -1,3 +1,5 @@ +## Rails 3.2.11 ## + ## Rails 3.2.10 ## ## Rails 3.2.9 (Nov 12, 2012) ## diff --git a/activeresource/test/cases/base_test.rb b/activeresource/test/cases/base_test.rb index 7b42f64a35..5ef8a51ef7 100644 --- a/activeresource/test/cases/base_test.rb +++ b/activeresource/test/cases/base_test.rb @@ -10,7 +10,7 @@ require "fixtures/subscription_plan" require 'active_support/json' require 'active_support/ordered_hash' require 'active_support/core_ext/hash/conversions' -require 'mocha' +require 'mocha/setup' class BaseTest < Test::Unit::TestCase def setup diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 7faf55bf52..834dff60c9 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,26 @@ +## Rails 3.2.11 (unreleased) + +* Remove surrogate unicode character encoding from ActiveSupport::JSON.encode + The encoding scheme was broken for unicode characters outside the basic + multilingual plane; since json is assumed to be UTF-8, and we already force the + encoding to UTF-8 simply pass through the un-encoded characters. + + *Brett Carter* + +* Fix mocha v0.13.0 compatibility. *James Mead* + +* `#as_json` isolates options when encoding a hash. [Backport #8185] + Fix #8182 + + *Yves Senn* + +* Handle the possible Permission Denied errors atomic.rb might trigger due to + its chown and chmod calls. [Backport #8027] + + *Daniele Sluijters* + +## Rails 3.2.10 ## + ## Rails 3.2.9 (Nov 12, 2012) ## * Add logger.push_tags and .pop_tags to complement logger.tagged: diff --git a/activesupport/lib/active_support/concern.rb b/activesupport/lib/active_support/concern.rb index ae1d0633ce..677f7c0bb0 100644 --- a/activesupport/lib/active_support/concern.rb +++ b/activesupport/lib/active_support/concern.rb @@ -6,7 +6,9 @@ module ActiveSupport # module M # def self.included(base) # base.extend ClassMethods - # scope :disabled, where(:disabled => true) + # base.class_eval do + # scope :disabled, where(:disabled => true) + # end # end # # module ClassMethods diff --git a/activesupport/lib/active_support/core_ext/file/atomic.rb b/activesupport/lib/active_support/core_ext/file/atomic.rb index fc3277f4d2..b0daf6db9e 100644 --- a/activesupport/lib/active_support/core_ext/file/atomic.rb +++ b/activesupport/lib/active_support/core_ext/file/atomic.rb @@ -36,7 +36,12 @@ class File FileUtils.mv(temp_file.path, file_name) # Set correct permissions on new file - chown(old_stat.uid, old_stat.gid, file_name) - chmod(old_stat.mode, file_name) + begin + chown(old_stat.uid, old_stat.gid, file_name) + # This operation will affect filesystem ACL's + chmod(old_stat.mode, file_name) + rescue Errno::EPERM + # Changing file ownership failed, moving on. + end end end diff --git a/activesupport/lib/active_support/core_ext/time/conversions.rb b/activesupport/lib/active_support/core_ext/time/conversions.rb index 49ac18d245..d180e1e588 100644 --- a/activesupport/lib/active_support/core_ext/time/conversions.rb +++ b/activesupport/lib/active_support/core_ext/time/conversions.rb @@ -13,6 +13,8 @@ class Time :rfc822 => lambda { |time| time.strftime("%a, %d %b %Y %H:%M:%S #{time.formatted_offset(false)}") } } + DATE_FORMATS[:nsec] = '%Y%m%d%H%M%S%9N' if RUBY_VERSION >= '1.9' + # Converts to a formatted string. See DATE_FORMATS for builtin formats. # # This method is aliased to <tt>to_s</tt>. diff --git a/activesupport/lib/active_support/core_ext/time/marshal.rb b/activesupport/lib/active_support/core_ext/time/marshal.rb index 457d3f5b62..ce5948d126 100644 --- a/activesupport/lib/active_support/core_ext/time/marshal.rb +++ b/activesupport/lib/active_support/core_ext/time/marshal.rb @@ -20,7 +20,7 @@ if !Marshal.load(Marshal.dump(Time.now.utc)).utc? def _dump(*args) obj = dup obj.instance_variable_set('@marshal_with_utc_coercion', utc?) - obj._dump_without_utc_flag(*args) + obj.send :_dump_without_utc_flag, *args end end end @@ -51,7 +51,7 @@ if Time.local(2010).zone != Marshal.load(Marshal.dump(Time.local(2010))).zone def _dump(*args) obj = dup obj.instance_variable_set('@_zone', zone) - obj._dump_without_zone(*args) + obj.send :_dump_without_zone, *args end end end diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index 91e4f07c6c..a50e6524c6 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -61,7 +61,7 @@ module ActiveSupport # hashes and arrays need to get encoder in the options, so that they can detect circular references options.merge(:encoder => self) else - options + options.dup end end @@ -122,13 +122,7 @@ module ActiveSupport if string.respond_to?(:force_encoding) string = string.encode(::Encoding::UTF_8, :undef => :replace).force_encoding(::Encoding::BINARY) end - json = string. - gsub(escape_regex) { |s| ESCAPED_CHARS[s] }. - gsub(/([\xC0-\xDF][\x80-\xBF]| - [\xE0-\xEF][\x80-\xBF]{2}| - [\xF0-\xF7][\x80-\xBF]{3})+/nx) { |s| - s.unpack("U*").pack("n*").unpack("H*")[0].gsub(/.{4}/n, '\\\\u\&') - } + json = string.gsub(escape_regex) { |s| ESCAPED_CHARS[s] } json = %("#{json}") json.force_encoding(::Encoding::UTF_8) if json.respond_to?(:force_encoding) json diff --git a/activesupport/lib/active_support/testing/mochaing.rb b/activesupport/lib/active_support/testing/mochaing.rb index 4ad75a6681..ae4e7e9a1f 100644 --- a/activesupport/lib/active_support/testing/mochaing.rb +++ b/activesupport/lib/active_support/testing/mochaing.rb @@ -1,7 +1,7 @@ begin - silence_warnings { require 'mocha' } + silence_warnings { require 'mocha/setup' } rescue LoadError # Fake Mocha::ExpectationError so we can rescue it in #run. Bleh. Object.const_set :Mocha, Module.new Mocha.const_set :ExpectationError, Class.new(StandardError) -end
\ No newline at end of file +end diff --git a/activesupport/lib/active_support/testing/setup_and_teardown.rb b/activesupport/lib/active_support/testing/setup_and_teardown.rb index f2268d1767..e5353f65cf 100644 --- a/activesupport/lib/active_support/testing/setup_and_teardown.rb +++ b/activesupport/lib/active_support/testing/setup_and_teardown.rb @@ -61,7 +61,7 @@ module ActiveSupport def run(result) return if @method_name.to_s == "default_test" - mocha_counter = retrieve_mocha_counter(result) + mocha_counter = retrieve_mocha_counter(self, result) yield(Test::Unit::TestCase::STARTED, name) @_result = result @@ -83,6 +83,8 @@ module ActiveSupport begin teardown run_callbacks :teardown + rescue Mocha::ExpectationError => e + add_failure(e.message, e.backtrace) rescue Test::Unit::AssertionFailedError => e add_failure(e.message, e.backtrace) rescue Exception => e @@ -100,19 +102,20 @@ module ActiveSupport protected - def retrieve_mocha_counter(result) #:nodoc: + def retrieve_mocha_counter(test_case, result) #:nodoc: if respond_to?(:mocha_verify) # using mocha if defined?(Mocha::TestCaseAdapter::AssertionCounter) Mocha::TestCaseAdapter::AssertionCounter.new(result) elsif defined?(Mocha::Integration::TestUnit::AssertionCounter) Mocha::Integration::TestUnit::AssertionCounter.new(result) - else + elsif defined?(Mocha::MonkeyPatching::TestUnit::AssertionCounter) Mocha::MonkeyPatching::TestUnit::AssertionCounter.new(result) + else + Mocha::Integration::AssertionCounter.new(test_case) end end end end - end end end diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb index 0382739871..c26a48a490 100644 --- a/activesupport/test/abstract_unit.rb +++ b/activesupport/test/abstract_unit.rb @@ -25,7 +25,7 @@ end require 'test/unit' require 'empty_bool' -silence_warnings { require 'mocha' } +silence_warnings { require 'mocha/setup' } ENV['NO_RELOAD'] = '1' require 'active_support' diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb index c030611de3..7a818411d0 100644 --- a/activesupport/test/core_ext/time_ext_test.rb +++ b/activesupport/test/core_ext/time_ext_test.rb @@ -538,14 +538,16 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase end def test_to_s - time = Time.utc(2005, 2, 21, 17, 44, 30) - assert_equal time.to_default_s, time.to_s - assert_equal time.to_default_s, time.to_s(:doesnt_exist) - assert_equal "2005-02-21 17:44:30", time.to_s(:db) - assert_equal "21 Feb 17:44", time.to_s(:short) - assert_equal "17:44", time.to_s(:time) - assert_equal "February 21, 2005 17:44", time.to_s(:long) - assert_equal "February 21st, 2005 17:44", time.to_s(:long_ordinal) + time = Time.utc(2005, 2, 21, 17, 44, 30.12345678901) + assert_equal time.to_default_s, time.to_s + assert_equal time.to_default_s, time.to_s(:doesnt_exist) + assert_equal "2005-02-21 17:44:30", time.to_s(:db) + assert_equal "21 Feb 17:44", time.to_s(:short) + assert_equal "17:44", time.to_s(:time) + assert_equal "20050221174430", time.to_s(:number) + assert_equal "20050221174430123456789", time.to_s(:nsec) if RUBY_VERSION >= '1.9' + assert_equal "February 21, 2005 17:44", time.to_s(:long) + assert_equal "February 21st, 2005 17:44", time.to_s(:long_ordinal) with_env_tz "UTC" do assert_equal "Mon, 21 Feb 2005 17:44:30 +0000", time.to_s(:rfc822) end diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb index a9590f164f..e4e13c3f25 100644 --- a/activesupport/test/json/encoding_test.rb +++ b/activesupport/test/json/encoding_test.rb @@ -22,6 +22,15 @@ class TestJSONEncoding < Test::Unit::TestCase end end + class CustomWithOptions + attr_accessor :foo, :bar + + def as_json(options={}) + options[:only] = %w(foo bar) + super(options) + end + end + TrueTests = [[ true, %(true) ]] FalseTests = [[ false, %(false) ]] NilTests = [[ nil, %(null) ]] @@ -91,11 +100,11 @@ class TestJSONEncoding < Test::Unit::TestCase def test_utf8_string_encoded_properly_when_kcode_is_utf8 with_kcode 'UTF8' do result = ActiveSupport::JSON.encode('€2.99') - assert_equal '"\\u20ac2.99"', result + assert_equal '"€2.99"', result assert_equal(Encoding::UTF_8, result.encoding) if result.respond_to?(:encoding) result = ActiveSupport::JSON.encode('✎☺') - assert_equal '"\\u270e\\u263a"', result + assert_equal '"✎☺"', result assert_equal(Encoding::UTF_8, result.encoding) if result.respond_to?(:encoding) end end @@ -104,11 +113,24 @@ class TestJSONEncoding < Test::Unit::TestCase def test_non_utf8_string_transcodes s = '二'.encode('Shift_JIS') result = ActiveSupport::JSON.encode(s) - assert_equal '"\\u4e8c"', result + assert_equal '"二"', result assert_equal Encoding::UTF_8, result.encoding end end + def test_wide_utf8_chars + w = '𠜎' + result = ActiveSupport::JSON.encode(w) + assert_equal '"𠜎"', result + end + + def test_wide_utf8_roundtrip + hash = { :string => "𐒑" } + json = ActiveSupport::JSON.encode(hash) + decoded_hash = ActiveSupport::JSON.decode(json) + assert_equal "𐒑", decoded_hash['string'] + end + def test_exception_raised_when_encoding_circular_reference_in_array a = [1] a << a @@ -239,6 +261,16 @@ class TestJSONEncoding < Test::Unit::TestCase assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json) end + def test_to_json_should_not_keep_options_around + f = CustomWithOptions.new + f.foo = "hello" + f.bar = "world" + + hash = {"foo" => f, "other_hash" => {"foo" => "other_foo", "test" => "other_test"}} + assert_equal({ "foo" => { "foo" => "hello", "bar" => "world" }, "other_hash" => { "foo" => "other_foo", "test" => "other_test"} }, + JSON.parse(hash.to_json)) + end + def test_struct_encoding Struct.new('UserNameAndEmail', :name, :email) Struct.new('UserNameAndDate', :name, :date) diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index adca0839d9..33ba020e0a 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,7 +1,19 @@ +## Rails 3.2.11 ## + ## Rails 3.2.10 ## ## Rails 3.2.9 (Nov 12, 2012) ## +* Engines with a dummy app include the rake tasks of dependencies in the app namespace. [Backport: #8262] + Fix #8229 + + *Yves Senn* + +* Add dummy app Rake tasks when --skip-test-unit and --dummy-path is passed to the plugin generator. [Backport #8139] + Fix #8121 + + *Yves Senn* + * Update supported ruby versions error message in ruby_version_check.rb *Lihan Li* ## Rails 3.2.8 (Aug 9, 2012) ## diff --git a/railties/guides/source/active_record_validations_callbacks.textile b/railties/guides/source/active_record_validations_callbacks.textile index 15d24f9ac1..d2a445b508 100644 --- a/railties/guides/source/active_record_validations_callbacks.textile +++ b/railties/guides/source/active_record_validations_callbacks.textile @@ -901,8 +901,12 @@ Below is a simple example where we change the Rails behavior to always display t <ruby> ActionView::Base.field_error_proc = Proc.new do |html_tag, instance| - errors = Array(instance.error_message).join(',') - %(#{html_tag}<span class="validation-error"> #{errors}</span>).html_safe + if html_tag =~ /\<label/ + html_tag + else + errors = Array(instance.error_message).join(',') + %(#{html_tag}<span class="validation-error"> #{errors}</span>).html_safe + end end </ruby> diff --git a/railties/guides/source/active_support_core_extensions.textile b/railties/guides/source/active_support_core_extensions.textile index fbccff5005..83e35214a5 100644 --- a/railties/guides/source/active_support_core_extensions.textile +++ b/railties/guides/source/active_support_core_extensions.textile @@ -3615,7 +3615,9 @@ File.atomic_write(joined_asset_path) do |cache| end </ruby> -To accomplish this +atomic_write+ creates a temporary file. That's the file the code in the block actually writes to. On completion, the temporary file is renamed, which is an atomic operation on POSIX systems. If the target file exists +atomic_write+ overwrites it and keeps owners and permissions. +To accomplish this `atomic_write` creates a temporary file. That's the file the code in the block actually writes to. On completion, the temporary file is renamed, which is an atomic operation on POSIX systems. If the target file exists `atomic_write` overwrites it and keeps owners and permissions. However there are a few cases where `atomic_write` cannot change the file ownership or permissions, this error is caught and skipped over trusting in the user/filesystem to ensure the file is accessible to the processes that need it. + +NOTE. Due to the chmod operation `atomic_write` performs, if the target file has an ACL set on it this ACL will be recalculated/modified. WARNING. Note you can't append with +atomic_write+. diff --git a/railties/guides/source/configuring.textile b/railties/guides/source/configuring.textile index 3466e1c3f4..f5b39dd4ac 100644 --- a/railties/guides/source/configuring.textile +++ b/railties/guides/source/configuring.textile @@ -277,6 +277,8 @@ h4. Configuring Active Record * +config.active_record.auto_explain_threshold_in_seconds+ configures the threshold for automatic EXPLAINs (+nil+ disables this feature). Queries exceeding the threshold get their query plan logged. Default is 0.5 in development mode. +* +config.active_record.cache_timestamp_format+ controls the format of the timestamp value in the cache key. Default is +:number+. + The MySQL adapter adds one additional configuration option: * +ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans+ controls whether Active Record will consider all +tinyint(1)+ columns in a MySQL database to be booleans and is true by default. diff --git a/railties/guides/source/performance_testing.textile b/railties/guides/source/performance_testing.textile index 958b13cd9e..0cf7d4a4fb 100644 --- a/railties/guides/source/performance_testing.textile +++ b/railties/guides/source/performance_testing.textile @@ -447,7 +447,7 @@ h4. Using Ruby-Prof on MRI and REE Add Ruby-Prof to your applications' Gemfile if you want to benchmark/profile under MRI or REE: <ruby> -gem 'ruby-prof', :git => 'git://github.com/wycats/ruby-prof.git' +gem 'ruby-prof' </ruby> Now run +bundle install+ and you're ready to go. diff --git a/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb b/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb index cd7d51e628..6c53d8bebb 100644 --- a/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb +++ b/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb @@ -204,7 +204,7 @@ task :default => :test end def create_test_dummy_files - return if options[:skip_test_unit] && options[:dummy_path] == 'test/dummy' + return unless with_dummy_app? create_dummy_app end @@ -242,6 +242,10 @@ task :default => :test options[:mountable] end + def with_dummy_app? + options[:skip_test_unit].blank? || options[:dummy_path] != 'test/dummy' + end + def self.banner "rails plugin new #{self.arguments.map(&:usage).join(' ')} [options]" end @@ -282,7 +286,7 @@ task :default => :test dummy_application_path = File.expand_path("#{dummy_path}/config/application.rb", destination_root) unless options[:pretend] || !File.exists?(dummy_application_path) contents = File.read(dummy_application_path) - contents[(contents.index("module Dummy"))..-1] + contents[(contents.index(/module ([\w]+)\n(.*)class Application/m))..-1] end end end diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/Rakefile b/railties/lib/rails/generators/rails/plugin_new/templates/Rakefile index 564fda3c49..9b3b8cc03f 100755 --- a/railties/lib/rails/generators/rails/plugin_new/templates/Rakefile +++ b/railties/lib/rails/generators/rails/plugin_new/templates/Rakefile @@ -20,7 +20,7 @@ RDoc::Task.new(:rdoc) do |rdoc| rdoc.rdoc_files.include('lib/**/*.rb') end -<% if full? && !options[:skip_active_record] && !options[:skip_test_unit] -%> +<% if full? && !options[:skip_active_record] && with_dummy_app? -%> APP_RAKEFILE = File.expand_path("../<%= dummy_path -%>/Rakefile", __FILE__) load 'rails/tasks/engine.rake' <% end %> diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/rails/application.rb b/railties/lib/rails/generators/rails/plugin_new/templates/rails/application.rb index 996ea79e67..6cb7e38b56 100644 --- a/railties/lib/rails/generators/rails/plugin_new/templates/rails/application.rb +++ b/railties/lib/rails/generators/rails/plugin_new/templates/rails/application.rb @@ -12,7 +12,7 @@ require "active_resource/railtie" <%= comment_if :skip_test_unit %>require "rails/test_unit/railtie" <% end -%> -Bundler.require +Bundler.require(*Rails.groups) require "<%= name %>" <%= application_definition %> diff --git a/railties/test/application/rack/logger_test.rb b/railties/test/application/rack/logger_test.rb index c13f9a49e7..984d8374fb 100644 --- a/railties/test/application/rack/logger_test.rb +++ b/railties/test/application/rack/logger_test.rb @@ -1,6 +1,7 @@ require "isolation/abstract_unit" require "active_support/log_subscriber/test_helper" require "rack/test" +require "mocha/setup" module ApplicationTests module RackTests diff --git a/railties/test/application/route_inspect_test.rb b/railties/test/application/route_inspect_test.rb index b897cf15b8..5c920cb33a 100644 --- a/railties/test/application/route_inspect_test.rb +++ b/railties/test/application/route_inspect_test.rb @@ -1,5 +1,5 @@ require 'test/unit' -require 'mocha' +require 'mocha/setup' require 'rails/application/route_inspector' require 'action_controller' require 'rails/engine' diff --git a/railties/test/application/routing_test.rb b/railties/test/application/routing_test.rb index a05e39658d..e50d744e26 100644 --- a/railties/test/application/routing_test.rb +++ b/railties/test/application/routing_test.rb @@ -229,6 +229,77 @@ module ApplicationTests end end + test 'routes are added and removed when reloading' do + app('development') + + controller :foo, <<-RUBY + class FooController < ApplicationController + def index + render :text => "foo" + end + end + RUBY + + controller :bar, <<-RUBY + class BarController < ApplicationController + def index + render :text => "bar" + end + end + RUBY + + app_file 'config/routes.rb', <<-RUBY + AppTemplate::Application.routes.draw do + get 'foo', :to => 'foo#index' + end + RUBY + + get '/foo' + assert_equal 'foo', last_response.body + assert_equal '/foo', Rails.application.routes.url_helpers.foo_path + + get '/bar' + assert_equal 404, last_response.status + assert_raises NoMethodError do + assert_equal '/bar', Rails.application.routes.url_helpers.bar_path + end + + app_file 'config/routes.rb', <<-RUBY + AppTemplate::Application.routes.draw do + get 'foo', :to => 'foo#index' + get 'bar', :to => 'bar#index' + end + RUBY + + Rails.application.reload_routes! + + get '/foo' + assert_equal 'foo', last_response.body + assert_equal '/foo', Rails.application.routes.url_helpers.foo_path + + get '/bar' + assert_equal 'bar', last_response.body + assert_equal '/bar', Rails.application.routes.url_helpers.bar_path + + app_file 'config/routes.rb', <<-RUBY + AppTemplate::Application.routes.draw do + get 'foo', :to => 'foo#index' + end + RUBY + + Rails.application.reload_routes! + + get '/foo' + assert_equal 'foo', last_response.body + assert_equal '/foo', Rails.application.routes.url_helpers.foo_path + + get '/bar' + assert_equal 404, last_response.status + assert_raises NoMethodError do + assert_equal '/bar', Rails.application.routes.url_helpers.bar_path + end + end + test 'resource routing with irregular inflection' do app_file 'config/initializers/inflection.rb', <<-RUBY ActiveSupport::Inflector.inflections do |inflect| diff --git a/railties/test/generators/plugin_new_generator_test.rb b/railties/test/generators/plugin_new_generator_test.rb index dcf4f95194..a33c6b096c 100644 --- a/railties/test/generators/plugin_new_generator_test.rb +++ b/railties/test/generators/plugin_new_generator_test.rb @@ -65,6 +65,12 @@ class PluginNewGeneratorTest < Rails::Generators::TestCase assert_no_match(/APP_RAKEFILE/, File.read(File.join(destination_root, "Rakefile"))) end + def test_generating_adds_dummy_app_rake_tasks_without_unit_test_files + run_generator [destination_root, "-T", "--mountable", '--dummy-path', 'my_dummy_app'] + + assert_match(/APP_RAKEFILE/, File.read(File.join(destination_root, "Rakefile"))) + end + def test_ensure_that_plugin_options_are_not_passed_to_app_generator FileUtils.cd(Rails.root) assert_no_match(/It works from file!.*It works_from_file/, run_generator([destination_root, "-m", "lib/template.rb"])) diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb index 5f9ee220dc..01bd11e1ab 100644 --- a/railties/test/generators_test.rb +++ b/railties/test/generators_test.rb @@ -1,7 +1,7 @@ require 'generators/generators_test_helper' require 'rails/generators/rails/model/model_generator' require 'rails/generators/test_unit/model/model_generator' -require 'mocha' +require 'mocha/setup' class GeneratorsTest < Rails::Generators::TestCase include GeneratorsTestHelper @@ -201,7 +201,7 @@ class GeneratorsTest < Rails::Generators::TestCase mspec = Rails::Generators.find_by_namespace :fixjour assert mspec.source_paths.include?(File.join(Rails.root, "lib", "templates", "fixjour")) end - + def test_usage_with_embedded_ruby require File.expand_path("fixtures/lib/generators/usage_template/usage_template_generator", File.dirname(__FILE__)) output = capture(:stdout) { Rails::Generators.invoke :usage_template, ['--help'] } |