diff options
author | Pratik Naik <pratiknaik@gmail.com> | 2008-10-05 19:46:48 +0100 |
---|---|---|
committer | Pratik Naik <pratiknaik@gmail.com> | 2008-10-05 19:46:48 +0100 |
commit | 6090513cfb8acb5554a6653a6f2cb87648585d41 (patch) | |
tree | 99bfd589a48153e33f19ae72baa6e98f5708a9b8 | |
parent | 01159a6431bbc2dc7d7d95ce294c8567c954f39e (diff) | |
parent | 4df45d86097efbeabceecfe53d8ea2da9ccbb107 (diff) | |
download | rails-6090513cfb8acb5554a6653a6f2cb87648585d41.tar.gz rails-6090513cfb8acb5554a6653a6f2cb87648585d41.tar.bz2 rails-6090513cfb8acb5554a6653a6f2cb87648585d41.zip |
Merge commit 'mainstream/master'
Conflicts:
activerecord/lib/active_record/association_preload.rb
66 files changed, 1142 insertions, 345 deletions
diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index bfe435550b..043f56ba17 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -296,6 +296,9 @@ module ActionMailer #:nodoc: @@default_implicit_parts_order = [ "text/html", "text/enriched", "text/plain" ] cattr_accessor :default_implicit_parts_order + cattr_reader :protected_instance_variables + @@protected_instance_variables = %w(@body) + # Specify the BCC addresses for the message adv_attr_accessor :bcc diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb index 107b2e8bbe..905f25c9f7 100644 --- a/actionmailer/test/abstract_unit.rb +++ b/actionmailer/test/abstract_unit.rb @@ -1,6 +1,8 @@ require 'test/unit' $:.unshift "#{File.dirname(__FILE__)}/../lib" +$:.unshift "#{File.dirname(__FILE__)}/../../activesupport/lib" +$:.unshift "#{File.dirname(__FILE__)}/../../actionpack/lib" require 'action_mailer' require 'action_mailer/test_case' diff --git a/actionmailer/test/fixtures/test_mailer/body_ivar.erb b/actionmailer/test/fixtures/test_mailer/body_ivar.erb new file mode 100644 index 0000000000..1421e5c908 --- /dev/null +++ b/actionmailer/test/fixtures/test_mailer/body_ivar.erb @@ -0,0 +1,2 @@ +body: <%= @body %> +bar: <%= @bar %>
\ No newline at end of file diff --git a/actionmailer/test/mail_service_test.rb b/actionmailer/test/mail_service_test.rb index f57c6f3fb8..7f9540c44b 100644 --- a/actionmailer/test/mail_service_test.rb +++ b/actionmailer/test/mail_service_test.rb @@ -273,6 +273,13 @@ class TestMailer < ActionMailer::Base headers "return-path" => "another@somewhere.test" end + def body_ivar(recipient) + recipients recipient + subject "Body as a local variable" + from "test@example.com" + body :body => "foo", :bar => "baz" + end + class <<self attr_accessor :received_body end @@ -926,6 +933,11 @@ EOF TestMailer.deliver_return_path assert_match %r{^Return-Path: <another@somewhere.test>}, MockSMTP.deliveries[0][0] end + + def test_body_is_stored_as_an_ivar + mail = TestMailer.create_body_ivar(@recipient) + assert_equal "body: foo\nbar: baz", mail.body + end end end # uses_mocha diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 91f531f12c..413f6d48e5 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -290,8 +290,6 @@ module ActionController #:nodoc: @@allow_concurrency = false cattr_accessor :allow_concurrency - @@guard = Monitor.new - # Modern REST web services often need to submit complex data to the web application. # The <tt>@@param_parsers</tt> hash lets you register handlers which will process the HTTP body and add parameters to the # <tt>params</tt> hash. These handlers are invoked for POST and PUT requests. @@ -532,12 +530,7 @@ module ActionController #:nodoc: assign_names log_processing - - if @@allow_concurrency - send(method, *arguments) - else - @@guard.synchronize { send(method, *arguments) } - end + send(method, *arguments) send_response ensure @@ -975,13 +968,15 @@ module ActionController #:nodoc: # Sets the Last-Modified response header. Returns 304 Not Modified if the # If-Modified-Since request header is <= last modified. def last_modified!(utc_time) - head(:not_modified) if response.last_modified!(utc_time) + response.last_modified= utc_time + head(:not_modified) if response.last_modified == request.if_modified_since end # Sets the ETag response header. Returns 304 Not Modified if the # If-None-Match request header matches. def etag!(etag) - head(:not_modified) if response.etag!(etag) + response.etag = etag + head(:not_modified) if response.etag == request.if_none_match end # Clears the rendered results, allowing for another render to be performed. @@ -1256,7 +1251,7 @@ module ActionController #:nodoc: action_name = strip_out_controller(action_name) end end - "#{self.class.controller_path}/#{action_name}" + "#{self.controller_path}/#{action_name}" end def strip_out_controller(path) @@ -1264,7 +1259,7 @@ module ActionController #:nodoc: end def template_path_includes_controller?(path) - self.class.controller_path.split('/')[-1] == path.split('/')[0] + self.controller_path.split('/')[-1] == path.split('/')[0] end def process_cleanup diff --git a/actionpack/lib/action_controller/dispatcher.rb b/actionpack/lib/action_controller/dispatcher.rb index bdae5f9d86..90c8400c11 100644 --- a/actionpack/lib/action_controller/dispatcher.rb +++ b/actionpack/lib/action_controller/dispatcher.rb @@ -2,6 +2,8 @@ module ActionController # Dispatches requests to the appropriate controller and takes care of # reloading the app after each request when Dependencies.load? is true. class Dispatcher + @@guard = Mutex.new + class << self def define_dispatcher_callbacks(cache_classes) unless cache_classes @@ -20,6 +22,7 @@ module ActionController end if defined?(ActiveRecord) + after_dispatch :checkin_connections before_dispatch { ActiveRecord::Base.verify_active_connections! } to_prepare(:activerecord_instantiate_observers) { ActiveRecord::Base.instantiate_observers } end @@ -98,7 +101,7 @@ module ActionController @output, @request, @response = output, request, response end - def dispatch + def dispatch_unlocked begin run_callbacks :before_dispatch handle_request @@ -109,6 +112,16 @@ module ActionController end end + def dispatch + if ActionController::Base.allow_concurrency + dispatch_unlocked + else + @@guard.synchronize do + dispatch_unlocked + end + end + end + def dispatch_cgi(cgi, session_options) if cgi ||= self.class.failsafe_response(@output, '400 Bad Request') { CGI.new } @request = CgiRequest.new(cgi, session_options) @@ -145,6 +158,21 @@ module ActionController Base.logger.flush end + def mark_as_test_request! + @test_request = true + self + end + + def test_request? + @test_request + end + + def checkin_connections + # Don't return connection (and peform implicit rollback) if this request is a part of integration test + return if test_request? + ActiveRecord::Base.clear_active_connections! + end + protected def handle_request @controller = Routing::Routes.recognize(@request) diff --git a/actionpack/lib/action_controller/integration.rb b/actionpack/lib/action_controller/integration.rb index a98c1af7f9..fc473c269c 100644 --- a/actionpack/lib/action_controller/integration.rb +++ b/actionpack/lib/action_controller/integration.rb @@ -276,7 +276,7 @@ module ActionController ActionController::Base.clear_last_instantiation! env['rack.input'] = data.is_a?(IO) ? data : StringIO.new(data || '') - @status, @headers, result_body = ActionController::Dispatcher.new.call(env) + @status, @headers, result_body = ActionController::Dispatcher.new.mark_as_test_request!.call(env) @request_count += 1 @controller = ActionController::Base.last_instantiation diff --git a/actionpack/lib/action_controller/request.rb b/actionpack/lib/action_controller/request.rb index 8e6cfb41dc..5e492e3ee1 100755 --- a/actionpack/lib/action_controller/request.rb +++ b/actionpack/lib/action_controller/request.rb @@ -167,7 +167,7 @@ module ActionController parameter_format = parameters[:format] if parameter_format - parameter_format.to_sym + parameter_format elsif xhr? :js else @@ -176,8 +176,7 @@ module ActionController end def cache_format - parameter_format = parameters[:format] - parameter_format && parameter_format.to_sym + parameters[:format] end # Returns true if the request's "X-Requested-With" header contains diff --git a/actionpack/lib/action_controller/rescue.rb b/actionpack/lib/action_controller/rescue.rb index 83c4218af4..ec8e9b92d5 100644 --- a/actionpack/lib/action_controller/rescue.rb +++ b/actionpack/lib/action_controller/rescue.rb @@ -41,10 +41,9 @@ module ActionController #:nodoc: base.rescue_templates = Hash.new(DEFAULT_RESCUE_TEMPLATE) base.rescue_templates.update DEFAULT_RESCUE_TEMPLATES - base.class_inheritable_array :rescue_handlers - base.rescue_handlers = [] - base.extend(ClassMethods) + base.send :include, ActiveSupport::Rescuable + base.class_eval do alias_method_chain :perform_action, :rescue end @@ -54,82 +53,12 @@ module ActionController #:nodoc: def process_with_exception(request, response, exception) #:nodoc: new.process(request, response, :rescue_action, exception) end - - # Rescue exceptions raised in controller actions. - # - # <tt>rescue_from</tt> receives a series of exception classes or class - # names, and a trailing <tt>:with</tt> option with the name of a method - # or a Proc object to be called to handle them. Alternatively a block can - # be given. - # - # Handlers that take one argument will be called with the exception, so - # that the exception can be inspected when dealing with it. - # - # Handlers are inherited. They are searched from right to left, from - # bottom to top, and up the hierarchy. The handler of the first class for - # which <tt>exception.is_a?(klass)</tt> holds true is the one invoked, if - # any. - # - # class ApplicationController < ActionController::Base - # rescue_from User::NotAuthorized, :with => :deny_access # self defined exception - # rescue_from ActiveRecord::RecordInvalid, :with => :show_errors - # - # rescue_from 'MyAppError::Base' do |exception| - # render :xml => exception, :status => 500 - # end - # - # protected - # def deny_access - # ... - # end - # - # def show_errors(exception) - # exception.record.new_record? ? ... - # end - # end - def rescue_from(*klasses, &block) - options = klasses.extract_options! - unless options.has_key?(:with) - block_given? ? options[:with] = block : raise(ArgumentError, "Need a handler. Supply an options hash that has a :with key as the last argument.") - end - - klasses.each do |klass| - key = if klass.is_a?(Class) && klass <= Exception - klass.name - elsif klass.is_a?(String) - klass - else - raise(ArgumentError, "#{klass} is neither an Exception nor a String") - end - - # Order is important, we put the pair at the end. When dealing with an - # exception we will follow the documented order going from right to left. - rescue_handlers << [key, options[:with]] - end - end end protected # Exception handler called when the performance of an action raises an exception. def rescue_action(exception) - if handler_for_rescue(exception) - rescue_action_with_handler(exception) - else - log_error(exception) if logger - erase_results if performed? - - # Let the exception alter the response if it wants. - # For example, MethodNotAllowed sets the Allow header. - if exception.respond_to?(:handle_response!) - exception.handle_response!(response) - end - - if consider_all_requests_local || local_request? - rescue_action_locally(exception) - else - rescue_action_in_public(exception) - end - end + rescue_with_handler(exception) || rescue_action_without_handler(exception) end # Overwrite to implement custom logging of errors. By default logs as fatal. @@ -185,15 +114,20 @@ module ActionController #:nodoc: render_for_file(rescues_path("layout"), response_code_for_rescue(exception)) end - # Tries to rescue the exception by looking up and calling a registered handler. - def rescue_action_with_handler(exception) - if handler = handler_for_rescue(exception) - if handler.arity != 0 - handler.call(exception) - else - handler.call - end - true # don't rely on the return value of the handler + def rescue_action_without_handler(exception) + log_error(exception) if logger + erase_results if performed? + + # Let the exception alter the response if it wants. + # For example, MethodNotAllowed sets the Allow header. + if exception.respond_to?(:handle_response!) + exception.handle_response!(response) + end + + if consider_all_requests_local || local_request? + rescue_action_locally(exception) + else + rescue_action_in_public(exception) end end @@ -216,36 +150,6 @@ module ActionController #:nodoc: rescue_responses[exception.class.name] end - def handler_for_rescue(exception) - # We go from right to left because pairs are pushed onto rescue_handlers - # as rescue_from declarations are found. - _, handler = *rescue_handlers.reverse.detect do |klass_name, handler| - # The purpose of allowing strings in rescue_from is to support the - # declaration of handler associations for exception classes whose - # definition is yet unknown. - # - # Since this loop needs the constants it would be inconsistent to - # assume they should exist at this point. An early raised exception - # could trigger some other handler and the array could include - # precisely a string whose corresponding constant has not yet been - # seen. This is why we are tolerant to unknown constants. - # - # Note that this tolerance only matters if the exception was given as - # a string, otherwise a NameError will be raised by the interpreter - # itself when rescue_from CONSTANT is executed. - klass = self.class.const_get(klass_name) rescue nil - klass ||= klass_name.constantize rescue nil - exception.is_a?(klass) if klass - end - - case handler - when Symbol - method(handler) - when Proc - handler.bind(self) - end - end - def clean_backtrace(exception) if backtrace = exception.backtrace if defined?(RAILS_ROOT) diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 3e66947d5f..6a39039504 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -84,7 +84,7 @@ module ActionController module RaiseActionExceptions attr_accessor :exception - def rescue_action(e) + def rescue_action_without_handler(e) self.exception = e if request.remote_addr == "0.0.0.0" diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index 8c00670087..8df10c40cc 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -290,21 +290,23 @@ module ActionView #:nodoc: private attr_accessor :_first_render, :_last_render - # Evaluate the local assigns and pushes them to the view. + # Evaluates the local assigns and controller ivars, pushes them to the view. def _evaluate_assigns_and_ivars #:nodoc: unless @assigns_added @assigns.each { |key, value| instance_variable_set("@#{key}", value) } - - if @controller - variables = @controller.instance_variables - variables -= @controller.protected_instance_variables if @controller.respond_to?(:protected_instance_variables) - variables.each {|name| instance_variable_set(name, @controller.instance_variable_get(name)) } - end - + _copy_ivars_from_controller @assigns_added = true end end + def _copy_ivars_from_controller #:nodoc: + if @controller + variables = @controller.instance_variable_names + variables -= @controller.protected_instance_variables if @controller.respond_to?(:protected_instance_variables) + variables.each { |name| instance_variable_set(name, @controller.instance_variable_get(name)) } + end + end + def _set_controller_content_type(content_type) #:nodoc: if controller.respond_to?(:response) controller.response.content_type ||= content_type diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb index 294c22521e..208bf91dd4 100644 --- a/actionpack/lib/action_view/helpers/form_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -403,6 +403,7 @@ module ActionView # Creates a field set for grouping HTML form elements. # # <tt>legend</tt> will become the fieldset's title (optional as per W3C). + # <tt>options</tt> accept the same values as tag. # # === Examples # <% field_set_tag do %> @@ -414,9 +415,14 @@ module ActionView # <p><%= text_field_tag 'name' %></p> # <% end %> # # => <fieldset><legend>Your details</legend><p><input id="name" name="name" type="text" /></p></fieldset> - def field_set_tag(legend = nil, &block) + # + # <% field_set_tag nil, :class => 'format' do %> + # <p><%= text_field_tag 'name' %></p> + # <% end %> + # # => <fieldset class="format"><p><input id="name" name="name" type="text" /></p></fieldset> + def field_set_tag(legend = nil, options = nil, &block) content = capture(&block) - concat(tag(:fieldset, {}, true)) + concat(tag(:fieldset, options, true)) concat(content_tag(:legend, legend)) unless legend.blank? concat(content) concat("</fieldset>") diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 9db4cddd6a..673efa6af0 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -1,5 +1,5 @@ $:.unshift(File.dirname(__FILE__) + '/../lib') -$:.unshift(File.dirname(__FILE__) + '/../../activesupport/lib/active_support') +$:.unshift(File.dirname(__FILE__) + '/../../activesupport/lib') $:.unshift(File.dirname(__FILE__) + '/fixtures/helpers') require 'yaml' diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index af7b5dde62..5a6ca98b2e 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -39,6 +39,16 @@ class TestController < ActionController::Base render :action => 'hello_world' end end + + def conditional_hello_with_bangs + render :action => 'hello_world' + end + before_filter :handle_last_modified_and_etags, :only=>:conditional_hello_with_bangs + + def handle_last_modified_and_etags + last_modified! Time.now.utc.beginning_of_day + etag! [:foo, 123] + end def render_hello_world render :template => "test/hello_world" @@ -1306,6 +1316,7 @@ class EtagRenderTest < Test::Unit::TestCase @controller = TestController.new @request.host = "www.nextangle.com" + @expected_bang_etag = etag_for(expand_key([:foo, 123])) end def test_render_200_should_set_etag @@ -1365,11 +1376,27 @@ class EtagRenderTest < Test::Unit::TestCase assert_equal "<wrapper>\n<html>\n <p>Hello </p>\n<p>This is grand!</p>\n</html>\n</wrapper>\n", @response.body assert_equal etag_for("<wrapper>\n<html>\n <p>Hello </p>\n<p>This is grand!</p>\n</html>\n</wrapper>\n"), @response.headers['ETag'] end - + + def test_etag_with_bang_should_set_etag + get :conditional_hello_with_bangs + assert_equal @expected_bang_etag, @response.headers["ETag"] + assert_response :success + end + + def test_etag_with_bang_should_obey_if_none_match + @request.if_none_match = @expected_bang_etag + get :conditional_hello_with_bangs + assert_response :not_modified + end + protected def etag_for(text) %("#{Digest::MD5.hexdigest(text)}") end + + def expand_key(args) + ActiveSupport::Cache.expand_cache_key(args) + end end class LastModifiedRenderTest < Test::Unit::TestCase @@ -1402,6 +1429,18 @@ class LastModifiedRenderTest < Test::Unit::TestCase assert !@response.body.blank? assert_equal @last_modified, @response.headers['Last-Modified'] end + + def test_request_with_bang_gets_last_modified + get :conditional_hello_with_bangs + assert_equal @last_modified, @response.headers['Last-Modified'] + assert_response :success + end + + def test_request_with_bang_obeys_last_modified + @request.if_modified_since = @last_modified + get :conditional_hello_with_bangs + assert_response :not_modified + end end class RenderingLoggingTest < Test::Unit::TestCase diff --git a/actionpack/test/controller/rescue_test.rb b/actionpack/test/controller/rescue_test.rb index da076d2090..32c6c013f1 100644 --- a/actionpack/test/controller/rescue_test.rb +++ b/actionpack/test/controller/rescue_test.rb @@ -75,7 +75,7 @@ class RescueController < ActionController::Base def method_not_allowed raise ActionController::MethodNotAllowed.new(:get, :head, :put) end - + def not_implemented raise ActionController::NotImplemented.new(:get, :put) end @@ -107,7 +107,7 @@ class RescueController < ActionController::Base def record_invalid_raise_as_string raise RecordInvalidToRescueAsString end - + def bad_gateway raise BadGateway end @@ -135,18 +135,19 @@ class RescueController < ActionController::Base end end -class RescueTest < Test::Unit::TestCase +class RescueControllerTest < ActionController::TestCase FIXTURE_PUBLIC = "#{File.dirname(__FILE__)}/../fixtures".freeze - def setup - @controller = RescueController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new + setup :set_all_requests_local + setup :populate_exception_object + def set_all_requests_local RescueController.consider_all_requests_local = true @request.remote_addr = '1.2.3.4' @request.host = 'example.com' + end + def populate_exception_object begin raise 'foo' rescue => @exception @@ -307,7 +308,7 @@ class RescueTest < Test::Unit::TestCase assert_nil @controller.send(:clean_backtrace, Exception.new) end end - + def test_not_implemented with_all_requests_local false do with_rails_public_path(".") do @@ -463,14 +464,7 @@ class ExceptionInheritanceRescueController < ActionController::Base end end -class ExceptionInheritanceRescueTest < Test::Unit::TestCase - - def setup - @controller = ExceptionInheritanceRescueController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new - end - +class ExceptionInheritanceRescueControllerTest < ActionController::TestCase def test_bottom_first get :raise_grandchild_exception assert_response :no_content @@ -500,14 +494,7 @@ class ControllerInheritanceRescueController < ExceptionInheritanceRescueControll end end -class ControllerInheritanceRescueControllerTest < Test::Unit::TestCase - - def setup - @controller = ControllerInheritanceRescueController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new - end - +class ControllerInheritanceRescueControllerTest < ActionController::TestCase def test_first_exception_in_child_controller get :raise_first_exception_in_child_controller assert_response :gone diff --git a/actionpack/test/template/form_tag_helper_test.rb b/actionpack/test/template/form_tag_helper_test.rb index 6473d011d5..ad8baef5e4 100644 --- a/actionpack/test/template/form_tag_helper_test.rb +++ b/actionpack/test/template/form_tag_helper_test.rb @@ -271,6 +271,12 @@ class FormTagHelperTest < ActionView::TestCase expected = %(<fieldset>Hello world!</fieldset>) assert_dom_equal expected, output_buffer + + self.output_buffer = '' + field_set_tag('', :class => 'format') { concat "Hello world!" } + + expected = %(<fieldset class="format">Hello world!</fieldset>) + assert_dom_equal expected, output_buffer end def protect_against_forgery? diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index cef8cd8647..99e3973830 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -161,12 +161,13 @@ module ActiveRecord # the objects' IDs to the relevant objects. Returns a 2-tuple # <tt>(id_to_record_map, ids)</tt> where +id_to_record_map+ is the Hash, # and +ids+ is an Array of record IDs. - def construct_id_map(records) + def construct_id_map(records, primary_key=nil) id_to_record_map = {} ids = [] records.each do |record| - ids << record.id - mapped_records = (id_to_record_map[record.id.to_s] ||= []) + primary_key ||= record.class.primary_key + ids << record[primary_key] + mapped_records = (id_to_record_map[ids.last.to_s] ||= []) mapped_records << record end ids.uniq! @@ -180,7 +181,7 @@ module ActiveRecord options = reflection.options conditions = "t0.#{reflection.primary_key_name} #{in_or_equals_for_ids(ids)}" - conditions << append_conditions(options, preload_options) + conditions << append_conditions(reflection, preload_options) associated_records = reflection.klass.find(:all, :conditions => [conditions, ids], :include => options[:include], @@ -213,23 +214,24 @@ module ActiveRecord end def preload_has_many_association(records, reflection, preload_options={}) - id_to_record_map, ids = construct_id_map(records) - records.each {|record| record.send(reflection.name).loaded} options = reflection.options + primary_key_name = reflection.through_reflection_primary_key_name + id_to_record_map, ids = construct_id_map(records, primary_key_name) + records.each {|record| record.send(reflection.name).loaded} + if options[:through] through_records = preload_through_records(records, reflection, options[:through]) through_reflection = reflections[options[:through]] - through_primary_key = through_reflection.primary_key_name unless through_records.empty? source = reflection.source_reflection.name - #add conditions from reflection! - through_records.first.class.preload_associations(through_records, source, reflection.options) + through_records.first.class.preload_associations(through_records, source, options) through_records.each do |through_record| - add_preloaded_records_to_collection(id_to_record_map[through_record[through_primary_key].to_s], - reflection.name, through_record.send(source)) + through_record_id = through_record[reflection.through_reflection_primary_key].to_s + add_preloaded_records_to_collection(id_to_record_map[through_record_id], reflection.name, through_record.send(source)) end end + else set_association_collection_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.primary_key_name) @@ -317,7 +319,7 @@ module ActiveRecord end end conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} #{in_or_equals_for_ids(ids)}" - conditions << append_conditions(options, preload_options) + conditions << append_conditions(reflection, preload_options) associated_records = klass.find(:all, :conditions => [conditions, ids], :include => options[:include], :select => options[:select], @@ -338,7 +340,7 @@ module ActiveRecord conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} #{in_or_equals_for_ids(ids)}" end - conditions << append_conditions(options, preload_options) + conditions << append_conditions(reflection, preload_options) reflection.klass.find(:all, :select => (preload_options[:select] || options[:select] || "#{table_name}.*"), @@ -354,9 +356,9 @@ module ActiveRecord instance_eval("%@#{sql.gsub('@', '\@')}@") end - def append_conditions(options, preload_options) + def append_conditions(reflection, preload_options) sql = "" - sql << " AND (#{interpolate_sql_for_preload(sanitize_sql(options[:conditions]))})" if options[:conditions] + sql << " AND (#{interpolate_sql_for_preload(reflection.sanitized_conditions)})" if reflection.sanitized_conditions sql << " AND (#{sanitize_sql preload_options[:conditions]})" if preload_options[:conditions] sql end diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index acdcd14ec8..b617147f67 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -240,7 +240,7 @@ module ActiveRecord # the kind of the class of the associated objects. Meant to be used as # a sanity check when you are about to assign an associated record. def raise_on_type_mismatch(record) - unless record.is_a?(@reflection.klass) + unless record.is_a?(@reflection.klass) || record.is_a?(@reflection.class_name.constantize) message = "#{@reflection.class_name}(##{@reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})" raise ActiveRecord::AssociationTypeMismatch, message end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 3171665e19..a0bb3a45b0 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -32,6 +32,14 @@ module ActiveRecord end protected + def target_reflection_has_associated_record? + if @reflection.through_reflection.macro == :belongs_to && @owner[@reflection.through_reflection.primary_key_name].blank? + false + else + true + end + end + def construct_find_options!(options) options[:select] = construct_select(options[:select]) options[:from] ||= construct_from @@ -61,6 +69,7 @@ module ActiveRecord end def find_target + return [] unless target_reflection_has_associated_record? @reflection.klass.find(:all, :select => construct_select, :conditions => construct_conditions, @@ -102,6 +111,8 @@ module ActiveRecord "#{as}_type" => reflection.klass.quote_value( @owner.class.base_class.name.to_s, reflection.klass.columns_hash["#{as}_type"]) } + elsif reflection.macro == :belongs_to + { reflection.klass.primary_key => @owner[reflection.primary_key_name] } else { reflection.primary_key_name => owner_quoted_id } end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index e5486738f0..1c753524de 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -342,7 +342,9 @@ module ActiveRecord method_name = method.to_s if super return true - elsif self.private_methods.include?(method_name) && !include_private_methods + elsif !include_private_methods && super(method, true) + # If we're here than we haven't found among non-private methods + # but found among all methods. Which means that given method is private. return false elsif !self.class.generated_methods? self.class.define_attribute_methods diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 6fb05b5551..6a1a3794a2 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1780,10 +1780,10 @@ module ActiveRecord #:nodoc: #{'result = ' if bang}if options[:conditions] with_scope(:find => finder_options) do - ActiveSupport::Deprecation.silence { send(:#{finder}, options) } + find(:#{finder}, options) end else - ActiveSupport::Deprecation.silence { send(:#{finder}, options.merge(finder_options)) } + find(:#{finder}, options.merge(finder_options)) end #{'result || raise(RecordNotFound)' if bang} end @@ -1806,9 +1806,9 @@ module ActiveRecord #:nodoc: options = { :conditions => find_attributes } set_readonly_option!(options) - record = find_initial(options) + record = find(:first, options) - if record.nil? + if record.nil? record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) } #{'yield(record) if block_given?'} #{'record.save' if instantiator == :create} diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb index 80992dd34b..5e33cf1bd4 100644 --- a/activerecord/lib/active_record/calculations.rb +++ b/activerecord/lib/active_record/calculations.rb @@ -217,7 +217,7 @@ module ActiveRecord sql << " ORDER BY #{options[:order]} " if options[:order] add_limit!(sql, options, scope) - sql << ") AS #{aggregate_alias}_subquery" if use_workaround + sql << ") #{aggregate_alias}_subquery" if use_workaround sql end @@ -285,11 +285,15 @@ module ActiveRecord operation = operation.to_s.downcase case operation when 'count' then value.to_i - when 'sum' then value =~ /\./ ? value.to_f : value.to_i - when 'avg' then value && value.to_f - else column ? column.type_cast(value) : value + when 'sum' then type_cast_using_column(value || '0', column) + when 'avg' then value && value.to_d + else type_cast_using_column(value, column) end end + + def type_cast_using_column(value, column) + column ? column.type_cast(value) : value + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 10cae5d840..432c341e6c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -172,21 +172,24 @@ module ActiveRecord # within the timeout period. def checkout # Checkout an available connection - conn = @connection_mutex.synchronize do - if @checked_out.size < @connections.size - checkout_existing_connection - elsif @connections.size < @size - checkout_new_connection - end - end - return conn if conn - - # No connections available; wait for one @connection_mutex.synchronize do - if @queue.wait(@timeout) - checkout_existing_connection - else - raise ConnectionTimeoutError, "could not obtain a database connection within #{@timeout} seconds. The pool size is currently #{@size}, perhaps you need to increase it?" + loop do + conn = if @checked_out.size < @connections.size + checkout_existing_connection + elsif @connections.size < @size + checkout_new_connection + end + return conn if conn + # No connections available; wait for one + if @queue.wait(@timeout) + next + else + # try looting dead threads + clear_stale_cached_connections! + if @size == @checked_out.size + raise ConnectionTimeoutError, "could not obtain a database connection within #{@timeout} seconds. The pool size is currently #{@size}, perhaps you need to increase it?" + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index a26fd02b90..1e452ae88a 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -218,7 +218,7 @@ module ActiveRecord s = column.class.string_to_binary(value).unpack("H*")[0] "x'#{s}'" elsif value.kind_of?(BigDecimal) - "'#{value.to_s("F")}'" + value.to_s("F") else super end @@ -371,9 +371,9 @@ module ActiveRecord end end - def recreate_database(name) #:nodoc: + def recreate_database(name, options = {}) #:nodoc: drop_database(name) - create_database(name) + create_database(name, options) end # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>. diff --git a/activerecord/lib/active_record/dynamic_finder_match.rb b/activerecord/lib/active_record/dynamic_finder_match.rb index f4a5712981..8f9f05ce36 100644 --- a/activerecord/lib/active_record/dynamic_finder_match.rb +++ b/activerecord/lib/active_record/dynamic_finder_match.rb @@ -6,11 +6,11 @@ module ActiveRecord end def initialize(method) - @finder = :find_initial + @finder = :first case method.to_s when /^find_(all_by|last_by|by)_([_a-zA-Z]\w*)$/ - @finder = :find_last if $1 == 'last_by' - @finder = :find_every if $1 == 'all_by' + @finder = :last if $1 == 'last_by' + @finder = :all if $1 == 'all_by' names = $2 when /^find_by_([_a-zA-Z]\w*)\!$/ @bang = true @@ -31,7 +31,7 @@ module ActiveRecord end def instantiator? - @finder == :find_initial && !@instantiator.nil? + @finder == :first && !@instantiator.nil? end def bang? diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index a1b498eceb..dbff4f24d6 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -13,14 +13,15 @@ module ActiveRecord def create_reflection(macro, name, options, active_record) case macro when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many - reflection = AssociationReflection.new(macro, name, options, active_record) + klass = options[:through] ? ThroughReflection : AssociationReflection + reflection = klass.new(macro, name, options, active_record) when :composed_of reflection = AggregateReflection.new(macro, name, options, active_record) end write_inheritable_hash :reflections, name => reflection reflection end - + # Returns a hash containing all AssociationReflection objects for the current class # Example: # @@ -30,7 +31,7 @@ module ActiveRecord def reflections read_inheritable_attribute(:reflections) || write_inheritable_attribute(:reflections, {}) end - + # Returns an array of AggregateReflection objects for all the aggregations in the class. def reflect_on_all_aggregations reflections.values.select { |reflection| reflection.is_a?(AggregateReflection) } @@ -116,6 +117,11 @@ module ActiveRecord @sanitized_conditions ||= klass.send(:sanitize_sql, options[:conditions]) if options[:conditions] end + # Returns +true+ if +self+ is a +belongs_to+ reflection. + def belongs_to? + macro == :belongs_to + end + private def derive_class_name name.to_s.camelize @@ -192,6 +198,52 @@ module ActiveRecord end end + def check_validity! + end + + def through_reflection + false + end + + def through_reflection_primary_key_name + end + + def source_reflection + nil + end + + private + def derive_class_name + class_name = name.to_s.camelize + class_name = class_name.singularize if [ :has_many, :has_and_belongs_to_many ].include?(macro) + class_name + end + + def derive_primary_key_name + if belongs_to? + "#{name}_id" + elsif options[:as] + "#{options[:as]}_id" + else + active_record.name.foreign_key + end + end + end + + # Holds all the meta-data about a :through association as it was specified in the Active Record class. + class ThroughReflection < AssociationReflection #:nodoc: + # Gets the source of the through reflection. It checks both a singularized and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>. + # (The <tt>:tags</tt> association on Tagging below.) + # + # class Post < ActiveRecord::Base + # has_many :taggings + # has_many :tags, :through => :taggings + # end + # + def source_reflection + @source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first + end + # Returns the AssociationReflection object specified in the <tt>:through</tt> option # of a HasManyThrough or HasOneThrough association. Example: # @@ -204,7 +256,7 @@ module ActiveRecord # taggings_reflection = tags_reflection.through_reflection # def through_reflection - @through_reflection ||= options[:through] ? active_record.reflect_on_association(options[:through]) : false + @through_reflection ||= active_record.reflect_on_association(options[:through]) end # Gets an array of possible <tt>:through</tt> source reflection names: @@ -215,63 +267,40 @@ module ActiveRecord @source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym } end - # Gets the source of the through reflection. It checks both a singularized and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>. - # (The <tt>:tags</tt> association on Tagging below.) - # - # class Post < ActiveRecord::Base - # has_many :taggings - # has_many :tags, :through => :taggings - # end - # - def source_reflection - return nil unless through_reflection - @source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first - end - def check_validity! - if options[:through] - if through_reflection.nil? - raise HasManyThroughAssociationNotFoundError.new(active_record.name, self) - end - - if source_reflection.nil? - raise HasManyThroughSourceAssociationNotFoundError.new(self) - end + if through_reflection.nil? + raise HasManyThroughAssociationNotFoundError.new(active_record.name, self) + end - if options[:source_type] && source_reflection.options[:polymorphic].nil? - raise HasManyThroughAssociationPointlessSourceTypeError.new(active_record.name, self, source_reflection) - end - - if source_reflection.options[:polymorphic] && options[:source_type].nil? - raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection) - end - - unless [:belongs_to, :has_many].include?(source_reflection.macro) && source_reflection.options[:through].nil? - raise HasManyThroughSourceAssociationMacroError.new(self) - end + if source_reflection.nil? + raise HasManyThroughSourceAssociationNotFoundError.new(self) + end + + if options[:source_type] && source_reflection.options[:polymorphic].nil? + raise HasManyThroughAssociationPointlessSourceTypeError.new(active_record.name, self, source_reflection) end + + if source_reflection.options[:polymorphic] && options[:source_type].nil? + raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection) + end + + unless [:belongs_to, :has_many].include?(source_reflection.macro) && source_reflection.options[:through].nil? + raise HasManyThroughSourceAssociationMacroError.new(self) + end + end + + def through_reflection_primary_key + through_reflection.belongs_to? ? through_reflection.klass.primary_key : through_reflection.primary_key_name + end + + def through_reflection_primary_key_name + through_reflection.primary_key_name if through_reflection.belongs_to? end private def derive_class_name # get the class_name of the belongs_to association of the through reflection - if through_reflection - options[:source_type] || source_reflection.class_name - else - class_name = name.to_s.camelize - class_name = class_name.singularize if [ :has_many, :has_and_belongs_to_many ].include?(macro) - class_name - end - end - - def derive_primary_key_name - if macro == :belongs_to - "#{name}_id" - elsif options[:as] - "#{options[:as]}_id" - else - active_record.name.foreign_key - end + options[:source_type] || source_reflection.class_name end end end diff --git a/activerecord/test/cases/active_schema_test_mysql.rb b/activerecord/test/cases/active_schema_test_mysql.rb index 2a42dc3517..9aff538ce9 100644 --- a/activerecord/test/cases/active_schema_test_mysql.rb +++ b/activerecord/test/cases/active_schema_test_mysql.rb @@ -25,6 +25,11 @@ class ActiveSchemaTest < ActiveRecord::TestCase assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, {:charset => 'latin1'}) assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, {:charset => :big5, :collation => :big5_chinese_ci}) end + + def test_recreate_mysql_database_with_encoding + create_database(:luca, {:charset => 'latin1'}) + assert_equal "CREATE DATABASE `luca` DEFAULT CHARACTER SET `latin1`", recreate_database(:luca, {:charset => 'latin1'}) + end end def test_add_column diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index e78624a98d..7f42577ab0 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -116,6 +116,13 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal 2, posts.first.comments.size end + def test_loading_from_an_association_that_has_a_hash_of_conditions + assert_nothing_raised do + Author.find(:all, :include => :hello_posts_with_hash_conditions) + end + assert !Author.find(authors(:david).id, :include => :hello_posts_with_hash_conditions).hello_posts.empty? + end + def test_loading_with_no_associations assert_nil Post.find(posts(:authorless).id, :include => :author).author end @@ -268,6 +275,15 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal authors(:david), assert_no_queries { posts_with_comments_and_author.first.author } end + def test_eager_with_has_many_through_a_belongs_to_association + author = authors(:mary) + post = Post.create!(:author => author, :title => "TITLE", :body => "BODY") + author.author_favorites.create(:favorite_author_id => 1) + author.author_favorites.create(:favorite_author_id => 2) + posts_with_author_favorites = author.posts.find(:all, :include => :author_favorites) + assert_no_queries { posts_with_author_favorites.first.author_favorites.first.author_id } + end + def test_eager_with_has_many_through_an_sti_join_model author = Author.find(:first, :include => :special_post_comments, :order => 'authors.id') assert_equal [comments(:does_it_hurt)], assert_no_queries { author.special_post_comments } diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 315d77de07..1bc9c39c19 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -1081,3 +1081,4 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end + diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 12cce98c26..a07f4bcbdd 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -5,7 +5,7 @@ require 'models/reader' require 'models/comment' class HasManyThroughAssociationsTest < ActiveRecord::TestCase - fixtures :posts, :readers, :people, :comments + fixtures :posts, :readers, :people, :comments, :authors def test_associate_existing assert_queries(2) { posts(:thinking);people(:david) } @@ -229,4 +229,19 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end end end + + def test_has_many_association_through_a_belongs_to_association_where_the_association_doesnt_exist + author = authors(:mary) + post = Post.create!(:title => "TITLE", :body => "BODY") + assert_equal [], post.author_favorites + end + + def test_has_many_association_through_a_belongs_to_association + author = authors(:mary) + post = Post.create!(:author => author, :title => "TITLE", :body => "BODY") + author.author_favorites.create(:favorite_author_id => 1) + author.author_favorites.create(:favorite_author_id => 2) + author.author_favorites.create(:favorite_author_id => 3) + assert_equal post.author.author_favorites, post.author_favorites + end end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 754fd58f35..0fa61500c0 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -18,8 +18,8 @@ class CalculationsTest < ActiveRecord::TestCase def test_should_average_field value = Account.average(:credit_limit) - assert_kind_of Float, value - assert_in_delta 53.0, value, 0.001 + assert_kind_of BigDecimal, value + assert_equal BigDecimal.new('53.0'), value end def test_should_return_nil_as_average @@ -273,7 +273,7 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_sum_expression - assert_equal 636, Account.sum("2 * credit_limit") + assert_equal '636', Account.sum("2 * credit_limit") end def test_count_with_from_option diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 292b88edbc..853474916c 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -21,7 +21,7 @@ class DynamicFinderMatchTest < ActiveRecord::TestCase match = ActiveRecord::DynamicFinderMatch.match("find_by_age_and_sex_and_location") assert_not_nil match assert match.finder? - assert_equal :find_initial, match.finder + assert_equal :first, match.finder assert_equal %w(age sex location), match.attribute_names end @@ -30,7 +30,7 @@ class DynamicFinderMatchTest < ActiveRecord::TestCase assert_not_nil match assert match.finder? assert match.bang? - assert_equal :find_initial, match.finder + assert_equal :first, match.finder assert_equal %w(age sex location), match.attribute_names end @@ -38,7 +38,7 @@ class DynamicFinderMatchTest < ActiveRecord::TestCase match = ActiveRecord::DynamicFinderMatch.match("find_all_by_age_and_sex_and_location") assert_not_nil match assert match.finder? - assert_equal :find_every, match.finder + assert_equal :all, match.finder assert_equal %w(age sex location), match.attribute_names end @@ -47,7 +47,7 @@ class DynamicFinderMatchTest < ActiveRecord::TestCase assert_not_nil match assert !match.finder? assert match.instantiator? - assert_equal :find_initial, match.finder + assert_equal :first, match.finder assert_equal :new, match.instantiator assert_equal %w(age sex location), match.attribute_names end @@ -57,7 +57,7 @@ class DynamicFinderMatchTest < ActiveRecord::TestCase assert_not_nil match assert !match.finder? assert match.instantiator? - assert_equal :find_initial, match.finder + assert_equal :first, match.finder assert_equal :create, match.instantiator assert_equal %w(age sex location), match.attribute_names end @@ -500,6 +500,23 @@ class FinderTest < ActiveRecord::TestCase assert_equal(2, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 1])) end + uses_mocha('test_dynamic_finder_should_go_through_the_find_class_method') do + def test_dynamic_finders_should_go_through_the_find_class_method + Topic.expects(:find).with(:first, :conditions => { :title => 'The First Topic!' }) + Topic.find_by_title("The First Topic!") + + Topic.expects(:find).with(:last, :conditions => { :title => 'The Last Topic!' }) + Topic.find_last_by_title("The Last Topic!") + + Topic.expects(:find).with(:all, :conditions => { :title => 'A Topic.' }) + Topic.find_all_by_title("A Topic.") + + Topic.expects(:find).with(:first, :conditions => { :title => 'Does not exist yet for sure!' }).times(2) + Topic.find_or_initialize_by_title('Does not exist yet for sure!') + Topic.find_or_create_by_title('Does not exist yet for sure!') + end + end + def test_find_by_one_attribute assert_equal topics(:first), Topic.find_by_title("The First Topic") assert_nil Topic.find_by_title("The First Topic!") diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index e339ef41ab..e0ed3e5886 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -170,6 +170,10 @@ class ReflectionTest < ActiveRecord::TestCase assert_nothing_raised { Firm.reflections[:clients] == Object.new } end + def test_has_many_through_reflection + assert_kind_of ActiveRecord::Reflection::ThroughReflection, Subscriber.reflect_on_association(:books) + end + private def assert_reflection(klass, association, options) assert reflection = klass.reflect_on_association(association) diff --git a/activerecord/test/cases/reload_models_test.rb b/activerecord/test/cases/reload_models_test.rb new file mode 100644 index 0000000000..411b5f6afa --- /dev/null +++ b/activerecord/test/cases/reload_models_test.rb @@ -0,0 +1,20 @@ +require "cases/helper" +require 'models/owner' +require 'models/pet' + +class ReloadModelsTest < ActiveRecord::TestCase + def test_has_one_with_reload + pet = Pet.find_by_name('parrot') + pet.owner = Owner.find_by_name('ashley') + + # Reload the class Owner, simulating auto-reloading of model classes in a + # development environment. Note that meanwhile the class Pet is not + # reloaded, simulating a class that is present in a plugin. + Object.class_eval { remove_const :Owner } + Kernel.load(File.expand_path(File.join(File.dirname(__FILE__), "../models/owner.rb"))) + + pet = Pet.find_by_name('parrot') + pet.owner = Owner.find_by_name('ashley') + assert_equal pet.owner, Owner.find_by_name('ashley') + end +end
\ No newline at end of file diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 3adbc0ce1f..6da37c31ff 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -22,6 +22,8 @@ class Post < ActiveRecord::Base end end + has_many :author_favorites, :through => :author + has_many :comments_with_interpolated_conditions, :class_name => 'Comment', :conditions => ['#{"#{aliased_table_name}." rescue ""}body = ?', 'Thank you for the welcome'] diff --git a/activeresource/lib/active_resource/base.rb b/activeresource/lib/active_resource/base.rb index d966062c7f..74d8128c0e 100644 --- a/activeresource/lib/active_resource/base.rb +++ b/activeresource/lib/active_resource/base.rb @@ -884,6 +884,7 @@ module ActiveResource # # ==== Examples # my_attrs = {:name => 'J&J Textiles', :industry => 'Cloth and textiles'} + # my_attrs = {:name => 'Marty', :colors => ["red", "green", "blue"]} # # the_supplier = Supplier.find(:first) # the_supplier.name # => 'J&M Textiles' @@ -906,7 +907,7 @@ module ActiveResource case value when Array resource = find_or_create_resource_for_collection(key) - value.map { |attrs| resource.new(attrs) } + value.map { |attrs| attrs.is_a?(String) ? attrs.dup : resource.new(attrs) } when Hash resource = find_or_create_resource_for(key) resource.new(value) diff --git a/activeresource/test/base_test.rb b/activeresource/test/base_test.rb index 7460fd45b0..bb08098683 100644 --- a/activeresource/test/base_test.rb +++ b/activeresource/test/base_test.rb @@ -46,10 +46,24 @@ class BaseTest < Test::Unit::TestCase :children => [{:name => 'Natacha'}]}, {:name => 'Milena', :children => []}]}]}.to_xml(:root => 'customer') + # - resource with yaml array of strings; for ActiveRecords using serialize :bar, Array + @marty = <<-eof + <?xml version=\"1.0\" encoding=\"UTF-8\"?> + <person> + <id type=\"integer\">5</id> + <name>Marty</name> + <colors type=\"yaml\">--- + - \"red\" + - \"green\" + - \"blue\" + </colors> + </person> + eof ActiveResource::HttpMock.respond_to do |mock| mock.get "/people/1.xml", {}, @matz mock.get "/people/2.xml", {}, @david + mock.get "/people/5.xml", {}, @marty mock.get "/people/Greg.xml", {}, @greg mock.get "/people/4.xml", {'key' => 'value'}, nil, 404 mock.put "/people/1.xml", {}, nil, 204 @@ -851,4 +865,14 @@ class BaseTest < Test::Unit::TestCase end end end + + def test_load_yaml_array + assert_nothing_raised do + marty = Person.find(5) + assert_equal 3, marty.colors.size + marty.colors.each do |color| + assert_kind_of String, color + end + end + end end diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG index 9700a11531..2c6f4ed582 100644 --- a/activesupport/CHANGELOG +++ b/activesupport/CHANGELOG @@ -1,5 +1,7 @@ *Edge* +* Add ActiveSupport::Rescuable module abstracting ActionController::Base rescue_from features. [Norbert Crombach, Pratik] + * Switch from String#chars to String#mb_chars for the unicode proxy. [Manfred Stienstra] This helps with 1.8.7 compatibility and also improves performance for some operations by reducing indirection. diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index b30faff06d..0ff09067ec 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -56,6 +56,8 @@ require 'active_support/time_with_zone' require 'active_support/secure_random' +require 'active_support/rescuable' + I18n.load_path << File.dirname(__FILE__) + '/active_support/locale/en-US.yml' Inflector = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Inflector', 'ActiveSupport::Inflector') diff --git a/activesupport/lib/active_support/core_ext/object/extending.rb b/activesupport/lib/active_support/core_ext/object/extending.rb index 082e98a297..bbf6f8563b 100644 --- a/activesupport/lib/active_support/core_ext/object/extending.rb +++ b/activesupport/lib/active_support/core_ext/object/extending.rb @@ -3,22 +3,43 @@ class Object Class.remove_class(*subclasses_of(*superclasses)) end - # Exclude this class unless it's a subclass of our supers and is defined. - # We check defined? in case we find a removed class that has yet to be - # garbage collected. This also fails for anonymous classes -- please - # submit a patch if you have a workaround. - def subclasses_of(*superclasses) #:nodoc: - subclasses = [] - - superclasses.each do |sup| - ObjectSpace.each_object(class << sup; self; end) do |k| - if k != sup && (k.name.blank? || eval("defined?(::#{k}) && ::#{k}.object_id == k.object_id")) - subclasses << k + begin + ObjectSpace.each_object(Class.new) {} + + # Exclude this class unless it's a subclass of our supers and is defined. + # We check defined? in case we find a removed class that has yet to be + # garbage collected. This also fails for anonymous classes -- please + # submit a patch if you have a workaround. + def subclasses_of(*superclasses) #:nodoc: + subclasses = [] + + superclasses.each do |sup| + ObjectSpace.each_object(class << sup; self; end) do |k| + if k != sup && (k.name.blank? || eval("defined?(::#{k}) && ::#{k}.object_id == k.object_id")) + subclasses << k + end end end + + subclasses end + rescue RuntimeError + # JRuby and any implementations which cannot handle the objectspace traversal + # above fall back to this implementation + def subclasses_of(*superclasses) #:nodoc: + subclasses = [] - subclasses + superclasses.each do |sup| + ObjectSpace.each_object(Class) do |k| + if superclasses.any? { |superclass| k < superclass } && + (k.name.blank? || eval("defined?(::#{k}) && ::#{k}.object_id == k.object_id")) + subclasses << k + end + end + subclasses.uniq! + end + subclasses + end end def extended_by #:nodoc: diff --git a/activesupport/lib/active_support/rescuable.rb b/activesupport/lib/active_support/rescuable.rb new file mode 100644 index 0000000000..f2bc12e832 --- /dev/null +++ b/activesupport/lib/active_support/rescuable.rb @@ -0,0 +1,108 @@ +module ActiveSupport + # Rescuable module adds support for easier exception handling. + module Rescuable + def self.included(base) # :nodoc: + base.class_inheritable_accessor :rescue_handlers + base.rescue_handlers = [] + + base.extend(ClassMethods) + end + + module ClassMethods + # Rescue exceptions raised in controller actions. + # + # <tt>rescue_from</tt> receives a series of exception classes or class + # names, and a trailing <tt>:with</tt> option with the name of a method + # or a Proc object to be called to handle them. Alternatively a block can + # be given. + # + # Handlers that take one argument will be called with the exception, so + # that the exception can be inspected when dealing with it. + # + # Handlers are inherited. They are searched from right to left, from + # bottom to top, and up the hierarchy. The handler of the first class for + # which <tt>exception.is_a?(klass)</tt> holds true is the one invoked, if + # any. + # + # class ApplicationController < ActionController::Base + # rescue_from User::NotAuthorized, :with => :deny_access # self defined exception + # rescue_from ActiveRecord::RecordInvalid, :with => :show_errors + # + # rescue_from 'MyAppError::Base' do |exception| + # render :xml => exception, :status => 500 + # end + # + # protected + # def deny_access + # ... + # end + # + # def show_errors(exception) + # exception.record.new_record? ? ... + # end + # end + def rescue_from(*klasses, &block) + options = klasses.extract_options! + + unless options.has_key?(:with) + if block_given? + options[:with] = block + else + raise ArgumentError, "Need a handler. Supply an options hash that has a :with key as the last argument." + end + end + + klasses.each do |klass| + key = if klass.is_a?(Class) && klass <= Exception + klass.name + elsif klass.is_a?(String) + klass + else + raise ArgumentError, "#{klass} is neither an Exception nor a String" + end + + # put the new handler at the end because the list is read in reverse + rescue_handlers << [key, options[:with]] + end + end + end + + # Tries to rescue the exception by looking up and calling a registered handler. + def rescue_with_handler(exception) + if handler = handler_for_rescue(exception) + handler.arity != 0 ? handler.call(exception) : handler.call + true # don't rely on the return value of the handler + end + end + + def handler_for_rescue(exception) + # We go from right to left because pairs are pushed onto rescue_handlers + # as rescue_from declarations are found. + _, handler = Array(rescue_handlers).reverse.detect do |klass_name, handler| + # The purpose of allowing strings in rescue_from is to support the + # declaration of handler associations for exception classes whose + # definition is yet unknown. + # + # Since this loop needs the constants it would be inconsistent to + # assume they should exist at this point. An early raised exception + # could trigger some other handler and the array could include + # precisely a string whose corresponding constant has not yet been + # seen. This is why we are tolerant to unknown constants. + # + # Note that this tolerance only matters if the exception was given as + # a string, otherwise a NameError will be raised by the interpreter + # itself when rescue_from CONSTANT is executed. + klass = self.class.const_get(klass_name) rescue nil + klass ||= klass_name.constantize rescue nil + exception.is_a?(klass) if klass + end + + case handler + when Symbol + method(handler) + when Proc + handler.bind(self) + end + end + end +end diff --git a/activesupport/lib/active_support/secure_random.rb b/activesupport/lib/active_support/secure_random.rb index 688165f9a3..97971e8830 100644 --- a/activesupport/lib/active_support/secure_random.rb +++ b/activesupport/lib/active_support/secure_random.rb @@ -164,13 +164,13 @@ module ActiveSupport hex = n.to_s(16) hex = '0' + hex if (hex.length & 1) == 1 bin = [hex].pack("H*") - mask = bin[0].ord + mask = bin[0] mask |= mask >> 1 mask |= mask >> 2 mask |= mask >> 4 begin rnd = SecureRandom.random_bytes(bin.length) - rnd[0] = (rnd[0].ord & mask).chr + rnd[0] = rnd[0] & mask end until rnd < bin rnd.unpack("H*")[0].hex else diff --git a/activesupport/lib/active_support/testing/core_ext/test/unit/assertions.rb b/activesupport/lib/active_support/testing/core_ext/test/unit/assertions.rb index 63d1ba6507..e5853bf828 100644 --- a/activesupport/lib/active_support/testing/core_ext/test/unit/assertions.rb +++ b/activesupport/lib/active_support/testing/core_ext/test/unit/assertions.rb @@ -37,15 +37,18 @@ module Test # end def assert_difference(expressions, difference = 1, message = nil, &block) expression_evaluations = Array(expressions).map do |expression| - lambda do + [expression, lambda do eval(expression, block.__send__(:binding)) - end + end] end - original_values = expression_evaluations.inject([]) { |memo, expression| memo << expression.call } + original_values = expression_evaluations.inject([]) { |memo, expression| memo << expression[1].call } yield expression_evaluations.each_with_index do |expression, i| - assert_equal original_values[i] + difference, expression.call, message + full_message = "" + full_message << "#{message}.\n" if message + full_message << "<#{expression[0]}> was the expression that failed" + assert_equal original_values[i] + difference, expression[1].call, full_message end end diff --git a/activesupport/test/rescuable_test.rb b/activesupport/test/rescuable_test.rb new file mode 100644 index 0000000000..9f2b783b2e --- /dev/null +++ b/activesupport/test/rescuable_test.rb @@ -0,0 +1,75 @@ +require 'abstract_unit' + +class WraithAttack < StandardError +end + +class NuclearExplosion < StandardError +end + +class MadRonon < StandardError + attr_accessor :message + + def initialize(message) + @message = message + super() + end +end + +class Stargate + attr_accessor :result + + include ActiveSupport::Rescuable + + rescue_from WraithAttack, :with => :sos + + rescue_from NuclearExplosion do + @result = 'alldead' + end + + rescue_from MadRonon do |e| + @result = e.message + end + + def dispatch(method) + send(method) + rescue Exception => e + rescue_with_handler(e) + end + + def attack + raise WraithAttack + end + + def nuke + raise NuclearExplosion + end + + def ronanize + raise MadRonon.new("dex") + end + + def sos + @result = 'killed' + end +end + +class RescueableTest < Test::Unit::TestCase + def setup + @stargate = Stargate.new + end + + def test_rescue_from_with_method + @stargate.dispatch :attack + assert_equal 'killed', @stargate.result + end + + def test_rescue_from_with_block + @stargate.dispatch :nuke + assert_equal 'alldead', @stargate.result + end + + def test_rescue_from_with_block_with_args + @stargate.dispatch :ronanize + assert_equal 'dex', @stargate.result + end +end diff --git a/activesupport/test/secure_random_test.rb b/activesupport/test/secure_random_test.rb index b0b6c21a81..44694cd811 100644 --- a/activesupport/test/secure_random_test.rb +++ b/activesupport/test/secure_random_test.rb @@ -12,4 +12,8 @@ class SecureRandomTest < Test::Unit::TestCase b2 = ActiveSupport::SecureRandom.hex(64) assert_not_equal b1, b2 end + + def test_random_number + assert ActiveSupport::SecureRandom.random_number(5000) < 5000 + end end diff --git a/activesupport/test/test_test.rb b/activesupport/test/test_test.rb index 26a45af255..4e253848f6 100644 --- a/activesupport/test/test_test.rb +++ b/activesupport/test/test_test.rb @@ -60,6 +60,24 @@ class AssertDifferenceTest < Test::Unit::TestCase @object.increment end end + + def test_array_of_expressions_identify_failure + assert_difference ['@object.num', '1 + 1'] do + @object.increment + end + fail 'should not get to here' + rescue Test::Unit::AssertionFailedError => e + assert_equal "<1 + 1> was the expression that failed.\n<3> expected but was\n<2>.", e.message + end + + def test_array_of_expressions_identify_failure_when_message_provided + assert_difference ['@object.num', '1 + 1'], 1, 'something went wrong' do + @object.increment + end + fail 'should not get to here' + rescue Test::Unit::AssertionFailedError => e + assert_equal "something went wrong.\n<1 + 1> was the expression that failed.\n<3> expected but was\n<2>.", e.message + end else def default_test; end end diff --git a/railties/configs/databases/ibm_db.yml b/railties/configs/databases/ibm_db.yml new file mode 100644 index 0000000000..a9716ddb44 --- /dev/null +++ b/railties/configs/databases/ibm_db.yml @@ -0,0 +1,62 @@ +# IBM Dataservers +# +# Home Page +# http://rubyforge.org/projects/rubyibm/ +# +# To install the ibm_db gem: +# On Linux: +# Source the db2profile file and set the necessary environment variables: +# +# . /home/db2inst1/sqllib/db2profile +# export IBM_DB_DIR=/opt/ibm/db2/V9.1 +# export IBM_DB_LIB=/opt/ibm/db2/V9.1/lib32 +# +# Then issue the command: gem install ibm_db +# +# On Windows: +# Issue the command: gem install ibm_db +# If prompted, select the mswin32 option +# +# For more details on the installation refer to http://rubyforge.org/docman/view.php/2361/7682/IBM_DB_GEM.pdf +# +# For more details on the connection parameters below refer to: +# http://rubyibm.rubyforge.org/docs/adapter/0.9.0/rdoc/classes/ActiveRecord/ConnectionAdapters/IBM_DBAdapter.html + +development: + adapter: ibm_db + username: db2inst1 + password: + database: <%= app_name[0,4] %>_dev + #schema: db2inst1 + #host: localhost + #port: 50000 + #account: my_account + #app_user: my_app_user + #application: my_application + #workstation: my_workstation + +test: + adapter: ibm_db + username: db2inst1 + password: + database: <%= app_name[0,4] %>_tst + #schema: db2inst1 + #host: localhost + #port: 50000 + #account: my_account + #app_user: my_app_user + #application: my_application + #workstation: my_workstation + +production: + adapter: ibm_db + username: db2inst1 + password: + database: <%= app_name[0,4] %>_prd + #schema: db2inst1 + #host: localhost + #port: 50000 + #account: my_account + #app_user: my_app_user + #application: my_application + #workstation: my_workstation
\ No newline at end of file diff --git a/railties/lib/initializer.rb b/railties/lib/initializer.rb index 74d2daa34b..0aec97dece 100644 --- a/railties/lib/initializer.rb +++ b/railties/lib/initializer.rb @@ -212,6 +212,7 @@ module Rails Gem.loaded_specs[stub] = Gem::Specification.new do |s| s.name = stub s.version = Rails::VERSION::STRING + s.loaded_from = "" end end end @@ -878,7 +879,6 @@ Run `rake gems:install` to install the missing gems. components config lib - vendor ).map { |dir| "#{root_path}/#{dir}" }.select { |dir| File.directory?(dir) } paths.concat builtin_directories diff --git a/railties/lib/rails/gem_builder.rb b/railties/lib/rails/gem_builder.rb index e7e06d0008..79c61cc034 100644 --- a/railties/lib/rails/gem_builder.rb +++ b/railties/lib/rails/gem_builder.rb @@ -12,10 +12,10 @@ module Rails @spec = spec @gem_dir = gem_dir end - + # silence the underlying builder def say(message) end - + end -end
\ No newline at end of file +end diff --git a/railties/lib/rails/gem_dependency.rb b/railties/lib/rails/gem_dependency.rb index d58ae450eb..3b2f4846a6 100644 --- a/railties/lib/rails/gem_dependency.rb +++ b/railties/lib/rails/gem_dependency.rb @@ -1,50 +1,76 @@ +require 'rails/vendor_gem_source_index' + +module Gem + def self.source_index=(index) + @@source_index = index + end +end + module Rails class GemDependency - attr_accessor :name, :requirement, :version, :lib, :source + attr_accessor :lib, :source def self.unpacked_path @unpacked_path ||= File.join(RAILS_ROOT, 'vendor', 'gems') end + @@framework_gems = {} + + def self.add_frozen_gem_path + @@paths_loaded ||= begin + Gem.source_index = Rails::VendorGemSourceIndex.new(Gem.source_index) + # loaded before us - we can't change them, so mark them + Gem.loaded_specs.each do |name, spec| + @@framework_gems[name] = spec + end + end + end + + def framework_gem? + @@framework_gems.has_key?(name) + end + + def vendor_rails? + Gem.loaded_specs.has_key?(name) && Gem.loaded_specs[name].loaded_from.empty? + end + + def vendor_gem? + Gem.loaded_specs.has_key?(name) && Gem.loaded_specs[name].loaded_from.include?(self.class.unpacked_path) + end + def initialize(name, options = {}) require 'rubygems' unless Object.const_defined?(:Gem) if options[:requirement] - @requirement = options[:requirement] + req = options[:requirement] elsif options[:version] - @requirement = Gem::Requirement.create(options[:version]) + req = Gem::Requirement.create(options[:version]) + else + req = Gem::Requirement.default end - @version = @requirement.instance_variable_get("@requirements").first.last if @requirement - @name = name.to_s + @dep = Gem::Dependency.new(name, req) @lib = options[:lib] @source = options[:source] @loaded = @frozen = @load_paths_added = false - @unpack_directory = nil - end - - def unpacked_paths - Dir[File.join(self.class.unpacked_path, "#{@name}-#{@version || "*"}")] end def add_load_paths + self.class.add_frozen_gem_path return if @loaded || @load_paths_added - unpacked_paths = self.unpacked_paths - if unpacked_paths.empty? - args = [@name] - args << @requirement.to_s if @requirement - gem *args - else - $LOAD_PATH.unshift File.join(unpacked_paths.first, 'lib') - ext = File.join(unpacked_paths.first, 'ext') - $LOAD_PATH.unshift(ext) if File.exist?(ext) - @frozen = true + if framework_gem? + @load_paths_added = @loaded = @frozen = true + return end + gem @dep + @spec = Gem.loaded_specs[name] + @frozen = @spec.loaded_from.include?(self.class.unpacked_path) if @spec @load_paths_added = true rescue Gem::LoadError end def dependencies + return [] if framework_gem? all_dependencies = specification.dependencies.map do |dependency| GemDependency.new(dependency.name, :requirement => dependency.version_requirements) end @@ -58,22 +84,51 @@ module Rails def load return if @loaded || @load_paths_added == false - require(@lib || @name) unless @lib == false + require(@lib || name) unless @lib == false @loaded = true rescue LoadError puts $!.to_s $!.backtrace.each { |b| puts b } end + def name + @dep.name.to_s + end + + def requirement + r = @dep.version_requirements + (r == Gem::Requirement.default) ? nil : r + end + def frozen? - @frozen + @frozen ||= vendor_rails? || vendor_gem? end def loaded? - @loaded + @loaded ||= begin + if vendor_rails? + true + else + # check if the gem is loaded by inspecting $" + # specification.files lists all the files contained in the gem + gem_files = specification.files + # select only the files contained in require_paths - typically in bin and lib + require_paths_regexp = Regexp.new("^(#{specification.require_paths*'|'})/") + gem_lib_files = gem_files.select { |f| require_paths_regexp.match(f) } + # chop the leading directory off - a typical file might be in + # lib/gem_name/file_name.rb, but it will be 'require'd as gem_name/file_name.rb + gem_lib_files.map! { |f| f.split('/', 2)[1] } + # if any of the files from the above list appear in $", the gem is assumed to + # have been loaded + !(gem_lib_files & $").empty? + end + end end def load_paths_added? + # always try to add load paths - even if a gem is loaded, it may not + # be a compatible version (ie random_gem 0.4 is loaded and a later spec + # needs >= 0.5 - gem 'random_gem' will catch this and error out) @load_paths_added end @@ -93,17 +148,44 @@ module Rails # we can access information about the gem on deployment systems # without having the gem installed spec_filename = File.join(gem_dir(directory), '.specification') + # Gem.activate changes the spec - get the original + spec = Gem::Specification.load(specification.loaded_from) File.open(spec_filename, 'w') do |file| - file.puts specification.to_yaml + file.puts spec.to_yaml end end def ==(other) self.name == other.name && self.requirement == other.requirement end + alias_method :"eql?", :"==" + + def hash + @dep.hash + end def specification - @spec ||= Gem.source_index.search(Gem::Dependency.new(@name, @requirement)).sort_by { |s| s.version }.last + # code repeated from Gem.activate. Find a matching spec, or the currently loaded version. + # error out if loaded version and requested version are incompatible. + @spec ||= begin + matches = Gem.source_index.search(@dep) + matches << @@framework_gems[name] if framework_gem? + if Gem.loaded_specs[name] then + # This gem is already loaded. If the currently loaded gem is not in the + # list of candidate gems, then we have a version conflict. + existing_spec = Gem.loaded_specs[name] + unless matches.any? { |spec| spec.version == existing_spec.version } then + raise Gem::Exception, + "can't activate #{@dep}, already activated #{existing_spec.full_name}" + end + # we're stuck with it, so change to match + @dep.version_requirements = Gem::Requirement.create("=#{existing_spec.version}") + existing_spec + else + # new load + matches.last + end + end end private @@ -112,17 +194,15 @@ module Rails end def install_command - cmd = %w(install) << @name - cmd << "--version" << %("#{@requirement.to_s}") if @requirement + cmd = %w(install) << name + cmd << "--version" << %("#{requirement.to_s}") if requirement cmd << "--source" << @source if @source cmd end def unpack_command - cmd = %w(unpack) << @name - # We don't quote this requirement as it's run through GemRunner instead - # of shelling out to gem - cmd << "--version" << @requirement.to_s if @requirement + cmd = %w(unpack) << name + cmd << "--version" << "= "+specification.version.to_s if requirement cmd end end diff --git a/railties/lib/rails/plugin.rb b/railties/lib/rails/plugin.rb index b8b2b57038..4d983843af 100644 --- a/railties/lib/rails/plugin.rb +++ b/railties/lib/rails/plugin.rb @@ -112,7 +112,7 @@ module Rails class GemPlugin < Plugin # Initialize this plugin from a Gem::Specification. def initialize(spec, gem) - directory = (gem.frozen? && gem.unpacked_paths.first) || File.join(spec.full_gem_path) + directory = spec.full_gem_path super(directory) @name = spec.name end diff --git a/railties/lib/rails/vendor_gem_source_index.rb b/railties/lib/rails/vendor_gem_source_index.rb new file mode 100644 index 0000000000..c8701101e2 --- /dev/null +++ b/railties/lib/rails/vendor_gem_source_index.rb @@ -0,0 +1,93 @@ +require 'rubygems' +require 'yaml' + +module Rails + + class VendorGemSourceIndex + # VendorGemSourceIndex acts as a proxy for the Gem source index, allowing + # gems to be loaded from vendor/gems. Rather than the standard gem repository format, + # vendor/gems contains unpacked gems, with YAML specifications in .specification in + # each gem directory. + include Enumerable + + attr_reader :installed_source_index + attr_reader :vendor_source_index + + def initialize(installed_index, vendor_dir=Rails::GemDependency.unpacked_path) + @installed_source_index = installed_index + @vendor_dir = vendor_dir + refresh! + end + + def refresh! + # reload the installed gems + @installed_source_index.refresh! + vendor_gems = {} + + # handle vendor Rails gems - they are identified by having loaded_from set to "" + # we add them manually to the list, so that other gems can find them via dependencies + Gem.loaded_specs.each do |n, s| + next unless s.loaded_from.empty? + vendor_gems[s.full_name] = s + end + + # load specifications from vendor/gems + Dir[File.join(Rails::GemDependency.unpacked_path, '*')].each do |d| + spec = load_specification(d) + next unless spec + # NOTE: this is a bit of a hack - the gem system expects a different structure + # than we have. + # It's looking for: + # repository + # -> specifications + # - gem_name.spec <= loaded_from points to this + # -> gems + # - gem_name <= gem files here + # and therefore goes up one directory from loaded_from, then adds gems/gem_name + # to the path. + # But we have: + # vendor + # -> gems + # -> gem_name <= gem files here + # - .specification + # so we set loaded_from to vendor/gems/.specification (not a real file) to + # get the correct behavior. + spec.loaded_from = File.join(Rails::GemDependency.unpacked_path, '.specification') + vendor_gems[File.basename(d)] = spec + end + @vendor_source_index = Gem::SourceIndex.new(vendor_gems) + end + + def load_specification(gem_dir) + spec_file = File.join(gem_dir, '.specification') + YAML.load_file(spec_file) if File.exist?(spec_file) + end + + def find_name(gem_name, version_requirement = Gem::Requirement.default) + search(/^#{gem_name}$/, version_requirement) + end + + def search(*args) + # look for vendor gems, and then installed gems - later elements take priority + @installed_source_index.search(*args) + @vendor_source_index.search(*args) + end + + def each(&block) + @vendor_source_index.each(&block) + @installed_source_index.each(&block) + end + + def add_spec(spec) + @vendor_source_index.add_spec spec + end + + def remove_spec(spec) + @vendor_source_index.remove_spec spec + end + + def size + @vendor_source_index.size + @installed_source_index.size + end + + end +end
\ No newline at end of file diff --git a/railties/lib/rails_generator/generators/applications/app/app_generator.rb b/railties/lib/rails_generator/generators/applications/app/app_generator.rb index 9849948339..acd3dc8c8c 100644 --- a/railties/lib/rails_generator/generators/applications/app/app_generator.rb +++ b/railties/lib/rails_generator/generators/applications/app/app_generator.rb @@ -5,7 +5,7 @@ class AppGenerator < Rails::Generator::Base DEFAULT_SHEBANG = File.join(Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name']) - DATABASES = %w(mysql oracle postgresql sqlite2 sqlite3 frontbase) + DATABASES = %w(mysql oracle postgresql sqlite2 sqlite3 frontbase ibm_db) DEFAULT_DATABASE = 'sqlite3' default_options :db => (ENV["RAILS_DEFAULT_DATABASE"] || DEFAULT_DATABASE), diff --git a/railties/lib/tasks/databases.rake b/railties/lib/tasks/databases.rake index 1431aa6944..5cb27f1f10 100644 --- a/railties/lib/tasks/databases.rake +++ b/railties/lib/tasks/databases.rake @@ -341,7 +341,7 @@ namespace :db do case abcs["test"]["adapter"] when "mysql" ActiveRecord::Base.establish_connection(:test) - ActiveRecord::Base.connection.recreate_database(abcs["test"]["database"]) + ActiveRecord::Base.connection.recreate_database(abcs["test"]["database"], abcs["test"]) when "postgresql" ActiveRecord::Base.clear_active_connections! drop_database(abcs['test']) diff --git a/railties/lib/tasks/gems.rake b/railties/lib/tasks/gems.rake index 0321e60e0f..9abdfc56b6 100644 --- a/railties/lib/tasks/gems.rake +++ b/railties/lib/tasks/gems.rake @@ -1,14 +1,19 @@ desc "List the gems that this rails application depends on" task :gems => 'gems:base' do Rails.configuration.gems.each do |gem| - code = gem.loaded? ? (gem.frozen? ? "F" : "I") : " " - puts "[#{code}] #{gem.name} #{gem.requirement.to_s}" + print_gem_status(gem) end puts puts "I = Installed" puts "F = Frozen" end +def print_gem_status(gem, indent=1) + code = gem.loaded? ? (gem.frozen? ? "F" : "I") : " " + puts " "*(indent-1)+" - [#{code}] #{gem.name} #{gem.requirement.to_s}" + gem.dependencies.each { |g| print_gem_status(g, indent+1)} +end + namespace :gems do task :base do $rails_gem_installer = true @@ -19,7 +24,7 @@ namespace :gems do task :build do $rails_gem_installer = true require 'rails/gem_builder' - Dir[File.join(RAILS_ROOT, 'vendor', 'gems', '*')].each do |gem_dir| + Dir[File.join(Rails::GemDependency.unpacked_path, '*')].each do |gem_dir| spec_file = File.join(gem_dir, '.specification') next unless File.exists?(spec_file) specification = YAML::load_file(spec_file) @@ -28,7 +33,7 @@ namespace :gems do puts "Built gem: '#{gem_dir}'" end end - + desc "Installs all required gems for this application." task :install => :base do require 'rubygems' @@ -42,10 +47,10 @@ namespace :gems do require 'rubygems/gem_runner' Rails.configuration.gems.each do |gem| next unless !gem.frozen? && (ENV['GEM'].blank? || ENV['GEM'] == gem.name) - gem.unpack_to(File.join(RAILS_ROOT, 'vendor', 'gems')) if gem.loaded? + gem.unpack_to(Rails::GemDependency.unpacked_path) if gem.loaded? end end - + namespace :unpack do desc "Unpacks the specified gems and its dependencies into vendor/gems" task :dependencies => :unpack do @@ -54,9 +59,8 @@ namespace :gems do Rails.configuration.gems.each do |gem| next unless ENV['GEM'].blank? || ENV['GEM'] == gem.name gem.dependencies.each do |dependency| - dependency.add_load_paths # double check that we have not already unpacked next if dependency.frozen? - dependency.unpack_to(File.join(RAILS_ROOT, 'vendor', 'gems')) + dependency.unpack_to(Rails::GemDependency.unpacked_path) end end end diff --git a/railties/test/gem_dependency_test.rb b/railties/test/gem_dependency_test.rb index c94c950455..89e25341d1 100644 --- a/railties/test/gem_dependency_test.rb +++ b/railties/test/gem_dependency_test.rb @@ -37,39 +37,78 @@ uses_mocha "Plugin Tests" do end def test_gem_with_version_unpack_install_command + # stub out specification method, or else test will fail if hpricot 0.6 isn't installed + mock_spec = mock() + mock_spec.stubs(:version).returns('0.6') + @gem_with_version.stubs(:specification).returns(mock_spec) assert_equal ["unpack", "hpricot", "--version", '= 0.6'], @gem_with_version.unpack_command end def test_gem_adds_load_paths - @gem.expects(:gem).with(@gem.name) + @gem.expects(:gem).with(Gem::Dependency.new(@gem.name, nil)) @gem.add_load_paths end def test_gem_with_version_adds_load_paths - @gem_with_version.expects(:gem).with(@gem_with_version.name, @gem_with_version.requirement.to_s) + @gem_with_version.expects(:gem).with(Gem::Dependency.new(@gem_with_version.name, @gem_with_version.requirement.to_s)) @gem_with_version.add_load_paths end def test_gem_loading - @gem.expects(:gem).with(@gem.name) + @gem.expects(:gem).with(Gem::Dependency.new(@gem.name, nil)) @gem.expects(:require).with(@gem.name) @gem.add_load_paths @gem.load end def test_gem_with_lib_loading - @gem_with_lib.expects(:gem).with(@gem_with_lib.name) + @gem_with_lib.expects(:gem).with(Gem::Dependency.new(@gem_with_lib.name, nil)) @gem_with_lib.expects(:require).with(@gem_with_lib.lib) @gem_with_lib.add_load_paths @gem_with_lib.load end def test_gem_without_lib_loading - @gem_without_load.expects(:gem).with(@gem_without_load.name) + @gem_without_load.expects(:gem).with(Gem::Dependency.new(@gem_without_load.name, nil)) @gem_without_load.expects(:require).with(@gem_without_load.lib).never @gem_without_load.add_load_paths @gem_without_load.load end + def test_gem_dependencies_compare_for_uniq + gem1 = Rails::GemDependency.new "gem1" + gem1a = Rails::GemDependency.new "gem1" + gem2 = Rails::GemDependency.new "gem2" + gem2a = Rails::GemDependency.new "gem2" + gem3 = Rails::GemDependency.new "gem2", :version => ">=0.1" + gem3a = Rails::GemDependency.new "gem2", :version => ">=0.1" + gem4 = Rails::GemDependency.new "gem3" + gems = [gem1, gem2, gem1a, gem3, gem2a, gem4, gem3a, gem2, gem4] + assert_equal 4, gems.uniq.size + end + + def test_gem_load_frozen + dummy_gem = Rails::GemDependency.new "dummy-gem-a" + dummy_gem.add_load_paths + dummy_gem.load + assert_not_nil DUMMY_GEM_A_VERSION + end + + def test_gem_load_frozen_specific_version + dummy_gem = Rails::GemDependency.new "dummy-gem-b", :version => '0.4.0' + dummy_gem.add_load_paths + dummy_gem.load + assert_not_nil DUMMY_GEM_B_VERSION + assert_equal '0.4.0', DUMMY_GEM_B_VERSION + end + + def test_gem_load_frozen_minimum_version + dummy_gem = Rails::GemDependency.new "dummy-gem-c", :version => '>=0.5.0' + dummy_gem.add_load_paths + dummy_gem.load + assert_not_nil DUMMY_GEM_C_VERSION + assert_equal '0.6.0', DUMMY_GEM_C_VERSION + end + end end diff --git a/railties/test/vendor/gems/dummy-gem-a-0.4.0/.specification b/railties/test/vendor/gems/dummy-gem-a-0.4.0/.specification new file mode 100644 index 0000000000..86dba2092c --- /dev/null +++ b/railties/test/vendor/gems/dummy-gem-a-0.4.0/.specification @@ -0,0 +1,28 @@ +--- !ruby/object:Gem::Specification +name: dummy-gem-a +version: !ruby/object:Gem::Version + version: 0.4.0 +platform: ruby +authors: +- "Nobody" +date: 2008-10-03 00:00:00 -04:00 +files: +- lib +- lib/dummy-gem-a.rb +require_paths: +- lib +required_ruby_version: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: "0" + version: +required_rubygems_version: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: "0" + version: +requirements: [] +specification_version: 2 +summary: Dummy Gem A diff --git a/railties/test/vendor/gems/dummy-gem-a-0.4.0/lib/dummy-gem-a.rb b/railties/test/vendor/gems/dummy-gem-a-0.4.0/lib/dummy-gem-a.rb new file mode 100644 index 0000000000..0453b38ab8 --- /dev/null +++ b/railties/test/vendor/gems/dummy-gem-a-0.4.0/lib/dummy-gem-a.rb @@ -0,0 +1 @@ +DUMMY_GEM_A_VERSION="0.4.0" diff --git a/railties/test/vendor/gems/dummy-gem-b-0.4.0/.specification b/railties/test/vendor/gems/dummy-gem-b-0.4.0/.specification new file mode 100644 index 0000000000..5ea692d7a1 --- /dev/null +++ b/railties/test/vendor/gems/dummy-gem-b-0.4.0/.specification @@ -0,0 +1,28 @@ +--- !ruby/object:Gem::Specification +name: dummy-gem-b +version: !ruby/object:Gem::Version + version: 0.4.0 +platform: ruby +authors: +- "Nobody" +date: 2008-10-03 00:00:00 -04:00 +files: +- lib +- lib/dummy-gem-b.rb +require_paths: +- lib +required_ruby_version: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: "0" + version: +required_rubygems_version: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: "0" + version: +requirements: [] +specification_version: 2 +summary: Dummy Gem B diff --git a/railties/test/vendor/gems/dummy-gem-b-0.4.0/lib/dummy-gem-b.rb b/railties/test/vendor/gems/dummy-gem-b-0.4.0/lib/dummy-gem-b.rb new file mode 100644 index 0000000000..850b5dda83 --- /dev/null +++ b/railties/test/vendor/gems/dummy-gem-b-0.4.0/lib/dummy-gem-b.rb @@ -0,0 +1 @@ +DUMMY_GEM_B_VERSION="0.4.0" diff --git a/railties/test/vendor/gems/dummy-gem-b-0.6.0/.specification b/railties/test/vendor/gems/dummy-gem-b-0.6.0/.specification new file mode 100644 index 0000000000..ab4707124a --- /dev/null +++ b/railties/test/vendor/gems/dummy-gem-b-0.6.0/.specification @@ -0,0 +1,28 @@ +--- !ruby/object:Gem::Specification +name: dummy-gem-b +version: !ruby/object:Gem::Version + version: 0.6.0 +platform: ruby +authors: +- "Nobody" +date: 2008-10-03 00:00:00 -04:00 +files: +- lib +- lib/dummy-gem-b.rb +require_paths: +- lib +required_ruby_version: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: "0" + version: +required_rubygems_version: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: "0" + version: +requirements: [] +specification_version: 2 +summary: Dummy Gem B diff --git a/railties/test/vendor/gems/dummy-gem-b-0.6.0/lib/dummy-gem-b.rb b/railties/test/vendor/gems/dummy-gem-b-0.6.0/lib/dummy-gem-b.rb new file mode 100644 index 0000000000..7d6d01cd48 --- /dev/null +++ b/railties/test/vendor/gems/dummy-gem-b-0.6.0/lib/dummy-gem-b.rb @@ -0,0 +1 @@ +DUMMY_GEM_B_VERSION="0.6.0" diff --git a/railties/test/vendor/gems/dummy-gem-c-0.4.0/.specification b/railties/test/vendor/gems/dummy-gem-c-0.4.0/.specification new file mode 100644 index 0000000000..f90f60424c --- /dev/null +++ b/railties/test/vendor/gems/dummy-gem-c-0.4.0/.specification @@ -0,0 +1,28 @@ +--- !ruby/object:Gem::Specification +name: dummy-gem-c +version: !ruby/object:Gem::Version + version: 0.4.0 +platform: ruby +authors: +- "Nobody" +date: 2008-10-03 00:00:00 -04:00 +files: +- lib +- lib/dummy-gem-c.rb +require_paths: +- lib +required_ruby_version: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: "0" + version: +required_rubygems_version: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: "0" + version: +requirements: [] +specification_version: 2 +summary: Dummy Gem C diff --git a/railties/test/vendor/gems/dummy-gem-c-0.4.0/lib/dummy-gem-c.rb b/railties/test/vendor/gems/dummy-gem-c-0.4.0/lib/dummy-gem-c.rb new file mode 100644 index 0000000000..1a416bef82 --- /dev/null +++ b/railties/test/vendor/gems/dummy-gem-c-0.4.0/lib/dummy-gem-c.rb @@ -0,0 +1 @@ +DUMMY_GEM_C_VERSION="0.4.0" diff --git a/railties/test/vendor/gems/dummy-gem-c-0.6.0/.specification b/railties/test/vendor/gems/dummy-gem-c-0.6.0/.specification new file mode 100644 index 0000000000..e75c0aa66a --- /dev/null +++ b/railties/test/vendor/gems/dummy-gem-c-0.6.0/.specification @@ -0,0 +1,28 @@ +--- !ruby/object:Gem::Specification +name: dummy-gem-c +version: !ruby/object:Gem::Version + version: 0.6.0 +platform: ruby +authors: +- "Nobody" +date: 2008-10-03 00:00:00 -04:00 +files: +- lib +- lib/dummy-gem-c.rb +require_paths: +- lib +required_ruby_version: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: "0" + version: +required_rubygems_version: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: "0" + version: +requirements: [] +specification_version: 2 +summary: Dummy Gem C diff --git a/railties/test/vendor/gems/dummy-gem-c-0.6.0/lib/dummy-gem-c.rb b/railties/test/vendor/gems/dummy-gem-c-0.6.0/lib/dummy-gem-c.rb new file mode 100644 index 0000000000..9ba2ca8bb5 --- /dev/null +++ b/railties/test/vendor/gems/dummy-gem-c-0.6.0/lib/dummy-gem-c.rb @@ -0,0 +1 @@ +DUMMY_GEM_C_VERSION="0.6.0" |