diff options
81 files changed, 2027 insertions, 865 deletions
diff --git a/railties/lib/rails/generators/rails/mailer/USAGE b/actionmailer/lib/rails/generators/mailer/USAGE index a08d459739..a08d459739 100644 --- a/railties/lib/rails/generators/rails/mailer/USAGE +++ b/actionmailer/lib/rails/generators/mailer/USAGE diff --git a/railties/lib/rails/generators/rails/mailer/mailer_generator.rb b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb index 8993181d79..dd7fa640c9 100644 --- a/railties/lib/rails/generators/rails/mailer/mailer_generator.rb +++ b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb @@ -1,6 +1,8 @@ module Rails module Generators class MailerGenerator < NamedBase + source_root File.expand_path("../templates", __FILE__) + argument :actions, :type => :array, :default => [], :banner => "method method" check_class_collision diff --git a/railties/lib/rails/generators/rails/mailer/templates/mailer.rb b/actionmailer/lib/rails/generators/mailer/templates/mailer.rb index 7343eb28b3..7343eb28b3 100644 --- a/railties/lib/rails/generators/rails/mailer/templates/mailer.rb +++ b/actionmailer/lib/rails/generators/mailer/templates/mailer.rb diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index 04e44be291..4db9c4b84d 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,5 +1,11 @@ *Rails 3.0.0 [beta 4/release candidate] (unreleased)* +* OAuth 2: HTTP Token Authorization support to complement Basic and Digest Authorization. [Rick Olson] + +* Fixed inconsistencies in form builder and view helpers #4432 [Neeraj Singh] + +* Both :xml and :json renderers now forwards the given options to the model, allowing you to invoke them as render :xml => @projects, :include => :tasks [José Valim, Yehuda Katz] + * Renamed the field error CSS class from fieldWithErrors to field_with_errors for consistency. [Jeremy Kemper] * Add support for shorthand routes like /projects/status(.:format) #4423 [Diego Carrion] diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 092fe98588..4297d9bbf6 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -35,6 +35,7 @@ module ActionController RecordIdentifier, HttpAuthentication::Basic::ControllerMethods, HttpAuthentication::Digest::ControllerMethods, + HttpAuthentication::Token::ControllerMethods, # Add instrumentations hooks at the bottom, to ensure they instrument # all the methods properly. diff --git a/actionpack/lib/action_controller/caching/actions.rb b/actionpack/lib/action_controller/caching/actions.rb index 43ddf6435a..546f043c58 100644 --- a/actionpack/lib/action_controller/caching/actions.rb +++ b/actionpack/lib/action_controller/caching/actions.rb @@ -133,7 +133,7 @@ module ActionController #:nodoc: body = controller._save_fragment(cache_path.path, @store_options) end - body = controller.render_to_string(:text => cache, :layout => true) unless @cache_layout + body = controller.render_to_string(:text => body, :layout => true) unless @cache_layout controller.response_body = body controller.content_type = Mime[cache_path.extension || :html] diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 6bd6c15990..be7448ce01 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -300,5 +300,163 @@ module ActionController end end + + # Makes it dead easy to do HTTP Token authentication. + # + # Simple Token example: + # + # class PostsController < ApplicationController + # TOKEN = "secret" + # + # before_filter :authenticate, :except => [ :index ] + # + # def index + # render :text => "Everyone can see me!" + # end + # + # def edit + # render :text => "I'm only accessible if you know the password" + # end + # + # private + # def authenticate + # authenticate_or_request_with_http_token do |token, options| + # token == TOKEN + # end + # end + # end + # + # + # Here is a more advanced Token example where only Atom feeds and the XML API is protected by HTTP token authentication, + # the regular HTML interface is protected by a session approach: + # + # class ApplicationController < ActionController::Base + # before_filter :set_account, :authenticate + # + # protected + # def set_account + # @account = Account.find_by_url_name(request.subdomains.first) + # end + # + # def authenticate + # case request.format + # when Mime::XML, Mime::ATOM + # if user = authenticate_with_http_token { |t, o| @account.users.authenticate(t, o) } + # @current_user = user + # else + # request_http_token_authentication + # end + # else + # if session_authenticated? + # @current_user = @account.users.find(session[:authenticated][:user_id]) + # else + # redirect_to(login_url) and return false + # end + # end + # end + # end + # + # + # In your integration tests, you can do something like this: + # + # def test_access_granted_from_xml + # get( + # "/notes/1.xml", nil, + # :authorization => ActionController::HttpAuthentication::Token.encode_credentials(users(:dhh).token) + # ) + # + # assert_equal 200, status + # end + # + # + # On shared hosts, Apache sometimes doesn't pass authentication headers to + # FCGI instances. If your environment matches this description and you cannot + # authenticate, try this rule in your Apache setup: + # + # RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L] + module Token + + extend self + + module ControllerMethods + def authenticate_or_request_with_http_token(realm = "Application", &login_procedure) + authenticate_with_http_token(&login_procedure) || request_http_token_authentication(realm) + end + + def authenticate_with_http_token(&login_procedure) + Token.authenticate(self, &login_procedure) + end + + def request_http_token_authentication(realm = "Application") + Token.authentication_request(self, realm) + end + end + + # If token Authorization header is present, call the login procedure with + # the present token and options. + # + # controller - ActionController::Base instance for the current request. + # login_procedure - Proc to call if a token is present. The Proc should + # take 2 arguments: + # authenticate(controller) { |token, options| ... } + # + # Returns the return value of `&login_procedure` if a token is found. + # Returns nil if no token is found. + def authenticate(controller, &login_procedure) + token, options = token_and_options(controller.request) + if !token.blank? + login_procedure.call(token, options) + end + end + + # Parses the token and options out of the token authorization header. If + # the header looks like this: + # Authorization: Token token="abc", nonce="def" + # Then the returned token is "abc", and the options is {:nonce => "def"} + # + # request - ActionController::Request instance with the current headers. + # + # Returns an Array of [String, Hash] if a token is present. + # Returns nil if no token is found. + def token_and_options(request) + if header = request.authorization.to_s[/^Token (.*)/] + values = $1.split(','). + inject({}) do |memo, value| + value.strip! # remove any spaces between commas and values + key, value = value.split(/\=\"?/) # split key=value pairs + value.chomp!('"') # chomp trailing " in value + value.gsub!(/\\\"/, '"') # unescape remaining quotes + memo.update(key => value) + end + [values.delete("token"), values.with_indifferent_access] + end + end + + # Encodes the given token and options into an Authorization header value. + # + # token - String token. + # options - optional Hash of the options. + # + # Returns String. + def encode_credentials(token, options = {}) + values = ["token=#{token.to_s.inspect}"] + options.each do |key, value| + values << "#{key}=#{value.to_s.inspect}" + end + "Token #{values * ", "}" + end + + # Sets a WWW-Authenticate to let the client know a token is desired. + # + # controller - ActionController::Base instance for the outgoing response. + # realm - String realm to use in the header. + # + # Returns nothing. + def authentication_request(controller, realm) + controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.gsub(/"/, "")}") + controller.__send__ :render, :text => "HTTP Token: Access denied.\n", :status => :unauthorized + end + end + end end diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index 43440e5f7c..12a93d6a24 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -45,7 +45,17 @@ module ActionDispatch end def call(env) - @app.call(env) + status, headers, body = @app.call(env) + + # Only this middleware cares about RoutingError. So, let's just raise + # it here. + # TODO: refactor this middleware to handle the X-Cascade scenario without + # having to raise an exception. + if headers['X-Cascade'] == 'pass' + raise ActionController::RoutingError, "No route matches #{env['PATH_INFO'].inspect}" + end + + [status, headers, body] rescue Exception => exception raise exception if env['action_dispatch.show_exceptions'] == false render_exception(env, exception) diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index fdbff74071..0d071dd7fe 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -6,10 +6,6 @@ require 'action_dispatch/routing/deprecated_mapper' module ActionDispatch module Routing class RouteSet #:nodoc: - NotFound = lambda { |env| - raise ActionController::RoutingError, "No route matches #{env['PATH_INFO'].inspect}" - } - PARAMETERS_KEY = 'action_dispatch.request.path_parameters' class Dispatcher #:nodoc: @@ -224,7 +220,6 @@ module ActionDispatch def finalize! return if @finalized @finalized = true - @set.add_route(NotFound) @set.freeze end diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index 42018ee261..7d846a01dd 100644 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -589,56 +589,50 @@ module ActionView @options = options.dup @html_options = html_options.dup @datetime = datetime + @options[:datetime_separator] ||= ' — ' + @options[:time_separator] ||= ' : ' end def select_datetime - # TODO: Remove tag conditional - # Ideally we could just join select_date and select_date for the tag case + order = date_order.dup + order -= [:hour, :minute, :second] + @options[:discard_year] ||= true unless order.include?(:year) + @options[:discard_month] ||= true unless order.include?(:month) + @options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day) + @options[:discard_minute] ||= true if @options[:discard_hour] + @options[:discard_second] ||= true unless @options[:include_seconds] && !@options[:discard_minute] + + # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are + # valid (otherwise it could be 31 and february wouldn't be a valid date) + if @datetime && @options[:discard_day] && !@options[:discard_month] + @datetime = @datetime.change(:day => 1) + end + if @options[:tag] && @options[:ignore_date] select_time - elsif @options[:tag] - order = date_order.dup - order -= [:hour, :minute, :second] - - @options[:discard_year] ||= true unless order.include?(:year) - @options[:discard_month] ||= true unless order.include?(:month) - @options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day) - @options[:discard_minute] ||= true if @options[:discard_hour] - @options[:discard_second] ||= true unless @options[:include_seconds] && !@options[:discard_minute] - - # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are - # valid (otherwise it could be 31 and february wouldn't be a valid date) - if @datetime && @options[:discard_day] && !@options[:discard_month] - @datetime = @datetime.change(:day => 1) - end - + else [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) } order += [:hour, :minute, :second] unless @options[:discard_hour] build_selects_from_types(order) - else - "#{select_date}#{@options[:datetime_separator]}#{select_time}".html_safe end end def select_date order = date_order.dup - # TODO: Remove tag conditional - if @options[:tag] - @options[:discard_hour] = true - @options[:discard_minute] = true - @options[:discard_second] = true + @options[:discard_hour] = true + @options[:discard_minute] = true + @options[:discard_second] = true - @options[:discard_year] ||= true unless order.include?(:year) - @options[:discard_month] ||= true unless order.include?(:month) - @options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day) + @options[:discard_year] ||= true unless order.include?(:year) + @options[:discard_month] ||= true unless order.include?(:month) + @options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day) - # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are - # valid (otherwise it could be 31 and february wouldn't be a valid date) - if @datetime && @options[:discard_day] && !@options[:discard_month] - @datetime = @datetime.change(:day => 1) - end + # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are + # valid (otherwise it could be 31 and february wouldn't be a valid date) + if @datetime && @options[:discard_day] && !@options[:discard_month] + @datetime = @datetime.change(:day => 1) end [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) } @@ -649,15 +643,12 @@ module ActionView def select_time order = [] - # TODO: Remove tag conditional - if @options[:tag] - @options[:discard_month] = true - @options[:discard_year] = true - @options[:discard_day] = true - @options[:discard_second] ||= true unless @options[:include_seconds] + @options[:discard_month] = true + @options[:discard_year] = true + @options[:discard_day] = true + @options[:discard_second] ||= true unless @options[:include_seconds] - order += [:year, :month, :day] unless @options[:ignore_date] - end + order += [:year, :month, :day] unless @options[:ignore_date] order += [:hour, :minute] order << :second if @options[:include_seconds] @@ -937,10 +928,8 @@ module ActionView options[:include_position] = true options[:prefix] ||= @object_name options[:index] = @auto_index if @auto_index && !options.has_key?(:index) - options[:datetime_separator] ||= ' — ' - options[:time_separator] ||= ' : ' - DateTimeSelector.new(datetime, options.merge(:tag => true), html_options) + DateTimeSelector.new(datetime, options, html_options) end def default_datetime(options) diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb index 4ffc5ea127..210f148c02 100644 --- a/actionpack/lib/action_view/helpers/url_helper.rb +++ b/actionpack/lib/action_view/helpers/url_helper.rb @@ -596,10 +596,8 @@ module ActionView html_options = {} if html_options.nil? html_options = html_options.stringify_keys - if (options.is_a?(Hash) && options.key?('remote')) || (html_options.is_a?(Hash) && html_options.key?('remote')) + if (options.is_a?(Hash) && options.key?('remote') && options.delete('remote')) || (html_options.is_a?(Hash) && html_options.key?('remote') && html_options.delete('remote')) html_options['data-remote'] = 'true' - options.delete('remote') if options.is_a?(Hash) - html_options.delete('remote') if html_options.is_a?(Hash) end confirm = html_options.delete("confirm") diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index 115cc91467..4431eb2e2a 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -265,23 +265,27 @@ class ActionCacheTest < ActionController::TestCase def test_simple_action_cache get :index + assert_response :success cached_time = content_to_cache assert_equal cached_time, @response.body assert fragment_exist?('hostname.com/action_caching_test') reset! get :index + assert_response :success assert_equal cached_time, @response.body end def test_simple_action_not_cached get :destroy + assert_response :success cached_time = content_to_cache assert_equal cached_time, @response.body assert !fragment_exist?('hostname.com/action_caching_test/destroy') reset! get :destroy + assert_response :success assert_not_equal cached_time, @response.body end @@ -289,12 +293,14 @@ class ActionCacheTest < ActionController::TestCase def test_action_cache_with_layout get :with_layout + assert_response :success cached_time = content_to_cache assert_not_equal cached_time, @response.body assert fragment_exist?('hostname.com/action_caching_test/with_layout') reset! get :with_layout + assert_response :success assert_not_equal cached_time, @response.body body = body_to_string(read_fragment('hostname.com/action_caching_test/with_layout')) assert_equal @response.body, body @@ -302,12 +308,14 @@ class ActionCacheTest < ActionController::TestCase def test_action_cache_with_layout_and_layout_cache_false get :layout_false + assert_response :success cached_time = content_to_cache assert_not_equal cached_time, @response.body assert fragment_exist?('hostname.com/action_caching_test/layout_false') reset! get :layout_false + assert_response :success assert_not_equal cached_time, @response.body body = body_to_string(read_fragment('hostname.com/action_caching_test/layout_false')) @@ -317,6 +325,7 @@ class ActionCacheTest < ActionController::TestCase def test_action_cache_conditional_options @request.env['HTTP_ACCEPT'] = 'application/json' get :index + assert_response :success assert !fragment_exist?('hostname.com/action_caching_test') end @@ -325,41 +334,50 @@ class ActionCacheTest < ActionController::TestCase @controller.expects(:read_fragment).with('hostname.com/action_caching_test', :expires_in => 1.hour).once @controller.expects(:write_fragment).with('hostname.com/action_caching_test', '12345.0', :expires_in => 1.hour).once get :index + assert_response :success end def test_action_cache_with_custom_cache_path get :show + assert_response :success cached_time = content_to_cache assert_equal cached_time, @response.body assert fragment_exist?('test.host/custom/show') reset! get :show + assert_response :success assert_equal cached_time, @response.body end def test_action_cache_with_custom_cache_path_in_block get :edit + assert_response :success assert fragment_exist?('test.host/edit') reset! get :edit, :id => 1 + assert_response :success assert fragment_exist?('test.host/1;edit') end def test_cache_expiration get :index + assert_response :success cached_time = content_to_cache reset! get :index + assert_response :success assert_equal cached_time, @response.body reset! get :expire + assert_response :success reset! get :index + assert_response :success new_cached_time = content_to_cache assert_not_equal cached_time, @response.body reset! @@ -376,9 +394,11 @@ class ActionCacheTest < ActionController::TestCase @request.request_uri = "/action_caching_test/expire.xml" get :expire, :format => :xml + assert_response :success reset! get :index + assert_response :success new_cached_time = content_to_cache assert_not_equal cached_time, @response.body end @@ -386,12 +406,14 @@ class ActionCacheTest < ActionController::TestCase def test_cache_is_scoped_by_subdomain @request.host = 'jamis.hostname.com' get :index + assert_response :success jamis_cache = content_to_cache reset! @request.host = 'david.hostname.com' get :index + assert_response :success david_cache = content_to_cache assert_not_equal jamis_cache, @response.body @@ -399,12 +421,14 @@ class ActionCacheTest < ActionController::TestCase @request.host = 'jamis.hostname.com' get :index + assert_response :success assert_equal jamis_cache, @response.body reset! @request.host = 'david.hostname.com' get :index + assert_response :success assert_equal david_cache, @response.body end @@ -433,20 +457,24 @@ class ActionCacheTest < ActionController::TestCase end get :index, :format => 'xml' + assert_response :success cached_time = content_to_cache assert_equal cached_time, @response.body assert fragment_exist?('hostname.com/action_caching_test/index.xml') reset! get :index, :format => 'xml' + assert_response :success assert_equal cached_time, @response.body assert_equal 'application/xml', @response.content_type reset! get :expire_xml + assert_response :success reset! get :index, :format => 'xml' + assert_response :success assert_not_equal cached_time, @response.body end end @@ -455,6 +483,7 @@ class ActionCacheTest < ActionController::TestCase # run it twice to cache it the first time get :index, :id => 'content-type', :format => 'xml' get :index, :id => 'content-type', :format => 'xml' + assert_response :success assert_equal 'application/xml', @response.content_type end @@ -462,6 +491,7 @@ class ActionCacheTest < ActionController::TestCase # run it twice to cache it the first time get :show, :format => 'xml' get :show, :format => 'xml' + assert_response :success assert_equal 'application/xml', @response.content_type end @@ -469,6 +499,7 @@ class ActionCacheTest < ActionController::TestCase # run it twice to cache it the first time get :edit, :id => 1, :format => 'xml' get :edit, :id => 1, :format => 'xml' + assert_response :success assert_equal 'application/xml', @response.content_type end diff --git a/actionpack/test/controller/http_token_authentication_test.rb b/actionpack/test/controller/http_token_authentication_test.rb new file mode 100644 index 0000000000..3dfccae3db --- /dev/null +++ b/actionpack/test/controller/http_token_authentication_test.rb @@ -0,0 +1,113 @@ +require 'abstract_unit' + +class HttpTokenAuthenticationTest < ActionController::TestCase + class DummyController < ActionController::Base + before_filter :authenticate, :only => :index + before_filter :authenticate_with_request, :only => :display + before_filter :authenticate_long_credentials, :only => :show + + def index + render :text => "Hello Secret" + end + + def display + render :text => 'Definitely Maybe' + end + + def show + render :text => 'Only for loooooong credentials' + end + + private + + def authenticate + authenticate_or_request_with_http_token do |token, options| + token == 'lifo' + end + end + + def authenticate_with_request + if authenticate_with_http_token { |token, options| token == '"quote" pretty' && options[:algorithm] == 'test' } + @logged_in = true + else + request_http_token_authentication("SuperSecret") + end + end + + def authenticate_long_credentials + authenticate_or_request_with_http_token do |token, options| + token == '1234567890123456789012345678901234567890' && options[:algorithm] == 'test' + end + end + end + + AUTH_HEADERS = ['HTTP_AUTHORIZATION', 'X-HTTP_AUTHORIZATION', 'X_HTTP_AUTHORIZATION', 'REDIRECT_X_HTTP_AUTHORIZATION'] + + tests DummyController + + AUTH_HEADERS.each do |header| + test "successful authentication with #{header.downcase}" do + @request.env[header] = encode_credentials('lifo') + get :index + + assert_response :success + assert_equal 'Hello Secret', @response.body, "Authentication failed for request header #{header}" + end + test "successful authentication with #{header.downcase} and long credentials" do + @request.env[header] = encode_credentials('1234567890123456789012345678901234567890', :algorithm => 'test') + get :show + + assert_response :success + assert_equal 'Only for loooooong credentials', @response.body, "Authentication failed for request header #{header} and long credentials" + end + end + + AUTH_HEADERS.each do |header| + test "unsuccessful authentication with #{header.downcase}" do + @request.env[header] = encode_credentials('h4x0r') + get :index + + assert_response :unauthorized + assert_equal "HTTP Token: Access denied.\n", @response.body, "Authentication didn't fail for request header #{header}" + end + test "unsuccessful authentication with #{header.downcase} and long credentials" do + @request.env[header] = encode_credentials('h4x0rh4x0rh4x0rh4x0rh4x0rh4x0rh4x0rh4x0r') + get :show + + assert_response :unauthorized + assert_equal "HTTP Token: Access denied.\n", @response.body, "Authentication didn't fail for request header #{header} and long credentials" + end + end + + test "authentication request without credential" do + get :display + + assert_response :unauthorized + assert_equal "HTTP Token: Access denied.\n", @response.body + assert_equal 'Token realm="SuperSecret"', @response.headers['WWW-Authenticate'] + end + + test "authentication request with invalid credential" do + @request.env['HTTP_AUTHORIZATION'] = encode_credentials('"quote" pretty') + get :display + + assert_response :unauthorized + assert_equal "HTTP Token: Access denied.\n", @response.body + assert_equal 'Token realm="SuperSecret"', @response.headers['WWW-Authenticate'] + end + + test "authentication request with valid credential" do + @request.env['HTTP_AUTHORIZATION'] = encode_credentials('"quote" pretty', :algorithm => 'test') + get :display + + assert_response :success + assert assigns(:logged_in) + assert_equal 'Definitely Maybe', @response.body + end + + private + + def encode_credentials(token, options = {}) + ActionController::HttpAuthentication::Token.encode_credentials(token, options) + end +end diff --git a/actionpack/test/controller/rescue_test.rb b/actionpack/test/controller/rescue_test.rb index dd991898a8..0f64b77647 100644 --- a/actionpack/test/controller/rescue_test.rb +++ b/actionpack/test/controller/rescue_test.rb @@ -326,7 +326,8 @@ class RescueTest < ActionController::IntegrationTest end test 'rescue routing exceptions' do - @app = ActionDispatch::Rescue.new(SharedTestRoutes) do + raiser = proc { |env| raise ActionController::RoutingError, "Did not handle the request" } + @app = ActionDispatch::Rescue.new(raiser) do rescue_from ActionController::RoutingError, lambda { |env| [200, {"Content-Type" => "text/html"}, ["Gotcha!"]] } end @@ -335,7 +336,8 @@ class RescueTest < ActionController::IntegrationTest end test 'unrescued exception' do - @app = ActionDispatch::Rescue.new(SharedTestRoutes) + raiser = proc { |env| raise ActionController::RoutingError, "Did not handle the request" } + @app = ActionDispatch::Rescue.new(raiser) assert_raise(ActionController::RoutingError) { get '/b00m' } end diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index b508996467..651a7a6be0 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -392,12 +392,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest get '/admin', {}, {'REMOTE_ADDR' => '192.168.1.100'} assert_equal 'queenbee#index', @response.body - assert_raise(ActionController::RoutingError) { get '/admin', {}, {'REMOTE_ADDR' => '10.0.0.100'} } + get '/admin', {}, {'REMOTE_ADDR' => '10.0.0.100'} + assert_equal 'pass', @response.headers['X-Cascade'] get '/admin/accounts', {}, {'REMOTE_ADDR' => '192.168.1.100'} assert_equal 'queenbee#accounts', @response.body - assert_raise(ActionController::RoutingError) { get '/admin/accounts', {}, {'REMOTE_ADDR' => '10.0.0.100'} } + get '/admin/accounts', {}, {'REMOTE_ADDR' => '10.0.0.100'} + assert_equal 'pass', @response.headers['X-Cascade'] end end @@ -648,10 +650,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal 'comments#index', @response.body assert_equal '/posts/1/comments', post_comments_path(:post_id => 1) - assert_raise(ActionController::RoutingError) { post '/posts' } - assert_raise(ActionController::RoutingError) { put '/posts/1' } - assert_raise(ActionController::RoutingError) { delete '/posts/1' } - assert_raise(ActionController::RoutingError) { delete '/posts/1/comments' } + post '/posts' + assert_equal 'pass', @response.headers['X-Cascade'] + put '/posts/1' + assert_equal 'pass', @response.headers['X-Cascade'] + delete '/posts/1' + assert_equal 'pass', @response.headers['X-Cascade'] + delete '/posts/1/comments' + assert_equal 'pass', @response.headers['X-Cascade'] end end @@ -775,7 +781,8 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest get '/articles/rails/1' assert_equal 'articles#with_id', @response.body - assert_raise(ActionController::RoutingError) { get '/articles/123/1' } + get '/articles/123/1' + assert_equal 'pass', @response.headers['X-Cascade'] assert_equal '/articles/rails/1', article_with_title_path(:title => 'rails', :id => 1) end @@ -953,19 +960,22 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest def test_resource_constraints with_test_routes do - assert_raise(ActionController::RoutingError) { get '/products/1' } + get '/products/1' + assert_equal 'pass', @response.headers['X-Cascade'] get '/products' assert_equal 'products#index', @response.body get '/products/0001' assert_equal 'products#show', @response.body - assert_raise(ActionController::RoutingError) { get '/products/1/images' } + get '/products/1/images' + assert_equal 'pass', @response.headers['X-Cascade'] get '/products/0001/images' assert_equal 'images#index', @response.body get '/products/0001/images/1' assert_equal 'images#show', @response.body - assert_raise(ActionController::RoutingError) { get '/dashboard', {}, {'REMOTE_ADDR' => '10.0.0.100'} } + get '/dashboard', {}, {'REMOTE_ADDR' => '10.0.0.100'} + assert_equal 'pass', @response.headers['X-Cascade'] get '/dashboard', {}, {'REMOTE_ADDR' => '192.168.1.100'} assert_equal 'dashboards#show', @response.body end diff --git a/actionpack/test/template/date_helper_test.rb b/actionpack/test/template/date_helper_test.rb index da2477b6f8..053fcc4d24 100644 --- a/actionpack/test/template/date_helper_test.rb +++ b/actionpack/test/template/date_helper_test.rb @@ -669,19 +669,11 @@ class DateHelperTest < ActionView::TestCase end def test_select_date_with_incomplete_order - # NOTE: modified this test because of minimal API change - expected = %(<select id="date_first_year" name="date[first][year]">\n) - expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) - expected << "</select>\n" - - expected << %(<select id="date_first_month" name="date[first][month]">\n) - expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n) - expected << "</select>\n" - - expected << %(<select id="date_first_day" name="date[first][day]">\n) - expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) - expected << "</select>\n" - + # Since the order is incomplete nothing will be shown + expected = %(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003" />\n) + expected << %(<input id="date_first_month" name="date[first][month]" type="hidden" value="8" />\n) + expected << %(<input id="date_first_day" name="date[first][day]" type="hidden" value="16" />\n) + assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", :order => [:day]) end @@ -903,10 +895,14 @@ class DateHelperTest < ActionView::TestCase expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) expected << "</select>\n" + expected << " — " + expected << %(<select id="date_first_hour" name="date[first][hour]">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) expected << "</select>\n" + expected << " : " + expected << %(<select id="date_first_minute" name="date[first][minute]">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -955,10 +951,14 @@ class DateHelperTest < ActionView::TestCase expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) expected << "</select>\n" + expected << " — " + expected << %(<select id="date_first_hour" name="date[first][hour]">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) expected << "</select>\n" + expected << " : " + expected << %(<select id="date_first_minute" name="date[first][minute]">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -971,6 +971,7 @@ class DateHelperTest < ActionView::TestCase expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" + expected << %(<select id="date_first_month" name="date[first][month]" class="selector">\n) expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n) expected << "</select>\n" @@ -979,10 +980,14 @@ class DateHelperTest < ActionView::TestCase expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) expected << "</select>\n" + expected << " — " + expected << %(<select id="date_first_hour" name="date[first][hour]" class="selector">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) expected << "</select>\n" + expected << " : " + expected << %(<select id="date_first_minute" name="date[first][minute]" class="selector">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -1039,10 +1044,14 @@ class DateHelperTest < ActionView::TestCase expected << %(<option value="">Day</option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) expected << "</select>\n" + expected << " — " + expected << %(<select id="date_first_hour" name="date[first][hour]">\n) expected << %(<option value="">Hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) expected << "</select>\n" + expected << " : " + expected << %(<select id="date_first_minute" name="date[first][minute]">\n) expected << %(<option value="">Minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -1065,10 +1074,14 @@ class DateHelperTest < ActionView::TestCase expected << %(<option value="">Choose day</option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) expected << "</select>\n" + expected << " — " + expected << %(<select id="date_first_hour" name="date[first][hour]">\n) expected << %(<option value="">Choose hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) expected << "</select>\n" + expected << " : " + expected << %(<select id="date_first_minute" name="date[first][minute]">\n) expected << %(<option value="">Choose minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -1078,10 +1091,16 @@ class DateHelperTest < ActionView::TestCase end def test_select_time - expected = %(<select id="date_hour" name="date[hour]">\n) + expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) + expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) + expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) + + expected << %(<select id="date_hour" name="date[hour]">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) expected << "</select>\n" + expected << " : " + expected << %(<select id="date_minute" name="date[minute]">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -1091,7 +1110,10 @@ class DateHelperTest < ActionView::TestCase end def test_select_time_with_separator - expected = %(<select id="date_hour" name="date[hour]">\n) + expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) + expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) + expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) + expected << %(<select id="date_hour" name="date[hour]">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) expected << "</select>\n" @@ -1106,14 +1128,22 @@ class DateHelperTest < ActionView::TestCase end def test_select_time_with_seconds - expected = %(<select id="date_hour" name="date[hour]">\n) + expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) + expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) + expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) + + expected << %(<select id="date_hour" name="date[hour]">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) expected << "</select>\n" + expected << ' : ' + expected << %(<select id="date_minute" name="date[minute]">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" + expected << ' : ' + expected << %(<select id="date_second" name="date[second]">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -1122,7 +1152,11 @@ class DateHelperTest < ActionView::TestCase end def test_select_time_with_seconds_and_separator - expected = %(<select id="date_hour" name="date[hour]">\n) + expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) + expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) + expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) + + expected << %(<select id="date_hour" name="date[hour]">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) expected << "</select>\n" @@ -1142,10 +1176,16 @@ class DateHelperTest < ActionView::TestCase end def test_select_time_with_html_options - expected = %(<select id="date_hour" name="date[hour]" class="selector">\n) + expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) + expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) + expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) + + expected << %(<select id="date_hour" name="date[hour]" class="selector">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) expected << "</select>\n" + expected << " : " + expected << %(<select id="date_minute" name="date[minute]" class="selector">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -1159,14 +1199,22 @@ class DateHelperTest < ActionView::TestCase end def test_select_time_with_default_prompt - expected = %(<select id="date_hour" name="date[hour]">\n) + expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) + expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) + expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) + + expected << %(<select id="date_hour" name="date[hour]">\n) expected << %(<option value="">Hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) expected << "</select>\n" + + expected << " : " expected << %(<select id="date_minute" name="date[minute]">\n) expected << %(<option value="">Minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" + expected << " : " + expected << %(<select id="date_second" name="date[second]">\n) expected << %(<option value="">Seconds</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -1175,15 +1223,22 @@ class DateHelperTest < ActionView::TestCase end def test_select_time_with_custom_prompt - - expected = %(<select id="date_hour" name="date[hour]">\n) + expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n) + expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n) + expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n) + + expected << %(<select id="date_hour" name="date[hour]">\n) expected << %(<option value="">Choose hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) expected << "</select>\n" + expected << " : " + expected << %(<select id="date_minute" name="date[minute]">\n) expected << %(<option value="">Choose minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" + expected << " : " + expected << %(<select id="date_second" name="date[second]">\n) expected << %(<option value="">Choose seconds</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" @@ -2006,10 +2061,14 @@ class DateHelperTest < ActionView::TestCase expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) expected << "</select>\n" + expected << " — " + expected << %(<select id="date_first_hour" name="date[first][hour]">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n) expected << "</select>\n" + expected << " : " + expected << %(<select id="date_first_minute" name="date[first][minute]">\n) expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n) expected << "</select>\n" diff --git a/actionpack/test/template/form_tag_helper_test.rb b/actionpack/test/template/form_tag_helper_test.rb index 8756bd310f..abb0e1df93 100644 --- a/actionpack/test/template/form_tag_helper_test.rb +++ b/actionpack/test/template/form_tag_helper_test.rb @@ -59,6 +59,12 @@ class FormTagHelperTest < ActionView::TestCase assert_dom_equal expected, actual end + def test_form_tag_with_remote_false + actual = form_tag({}, :remote => false) + expected = %(<form action="http://www.example.com" method="post">) + assert_dom_equal expected, actual + end + def test_form_tag_with_block_in_erb output_buffer = form_tag("http://example.com") { concat "Hello world!" } diff --git a/actionpack/test/template/url_helper_test.rb b/actionpack/test/template/url_helper_test.rb index 5120870f50..299d6dd5bd 100644 --- a/actionpack/test/template/url_helper_test.rb +++ b/actionpack/test/template/url_helper_test.rb @@ -103,6 +103,13 @@ class UrlHelperTest < ActiveSupport::TestCase ) end + def test_button_to_with_remote_false + assert_dom_equal( + "<form method=\"post\" action=\"http://www.example.com\" class=\"button_to\"><div><input type=\"submit\" value=\"Hello\" /></div></form>", + button_to("Hello", "http://www.example.com", :remote => false) + ) + end + def test_button_to_enabled_disabled assert_dom_equal( "<form method=\"post\" action=\"http://www.example.com\" class=\"button_to\"><div><input type=\"submit\" value=\"Hello\" /></div></form>", @@ -205,6 +212,13 @@ class UrlHelperTest < ActiveSupport::TestCase ) end + def test_link_to_with_remote_false + assert_dom_equal( + "<a href=\"http://www.example.com\">Hello</a>", + link_to("Hello", "http://www.example.com", :remote => false) + ) + end + def test_link_tag_using_post_javascript assert_dom_equal( "<a href='http://www.example.com' data-method=\"post\" rel=\"nofollow\">Hello</a>", diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb index ee3e0eab06..df7026b3ec 100644 --- a/activemodel/lib/active_model/serializers/xml.rb +++ b/activemodel/lib/active_model/serializers/xml.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/hash/conversions' require 'active_support/core_ext/hash/slice' @@ -15,65 +16,29 @@ module ActiveModel def initialize(name, serializable, raw_value=nil) @name, @serializable = name, serializable - @raw_value = raw_value || @serializable.send(name) - + @value = value || @serializable.send(name) @type = compute_type - @value = compute_value - end - - # There is a significant speed improvement if the value - # does not need to be escaped, as <tt>tag!</tt> escapes all values - # to ensure that valid XML is generated. For known binary - # values, it is at least an order of magnitude faster to - # Base64 encode binary values and directly put them in the - # output XML than to pass the original value or the Base64 - # encoded value to the <tt>tag!</tt> method. It definitely makes - # no sense to Base64 encode the value and then give it to - # <tt>tag!</tt>, since that just adds additional overhead. - def needs_encoding? - ![ :binary, :date, :datetime, :boolean, :float, :integer ].include?(type) end - def decorations(include_types = true) + def decorations decorations = {} - - if type == :binary - decorations[:encoding] = 'base64' - end - - if include_types && type != :string - decorations[:type] = type - end - - if value.nil? - decorations[:nil] = true - end - + decorations[:encoding] = 'base64' if type == :binary + decorations[:type] = type unless type == :string + decorations[:nil] = true if value.nil? decorations end - protected - def compute_type - type = Hash::XML_TYPE_NAMES[@raw_value.class.name] - type ||= :string if @raw_value.respond_to?(:to_str) - type ||= :yaml - type - end + protected - def compute_value - if formatter = Hash::XML_FORMATTING[type.to_s] - @raw_value ? formatter.call(@raw_value) : nil - else - @raw_value - end - end + def compute_type + type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] + type ||= :string if value.respond_to?(:to_str) + type ||= :yaml + type + end end class MethodAttribute < Attribute #:nodoc: - protected - def compute_type - Hash::XML_TYPE_NAMES[@raw_value.class.name] || :string - end end attr_reader :options @@ -92,7 +57,7 @@ module ActiveModel # then because <tt>:except</tt> is set to a default value, the second # level model can have both <tt>:except</tt> and <tt>:only</tt> set. So if # <tt>:only</tt> is set, always delete <tt>:except</tt>. - def serializable_attributes_hash + def attributes_hash attributes = @serializable.attributes if options[:only].any? attributes.slice(*options[:only]) @@ -104,10 +69,12 @@ module ActiveModel end def serializable_attributes - serializable_attributes_hash.map { |name, value| self.class::Attribute.new(name, @serializable, value) } + attributes_hash.map do |name, value| + self.class::Attribute.new(name, @serializable, value) + end end - def serializable_method_attributes + def serializable_methods Array.wrap(options[:methods]).inject([]) do |methods, name| methods << self.class::MethodAttribute.new(name.to_s, @serializable) if @serializable.respond_to?(name.to_s) methods @@ -115,80 +82,53 @@ module ActiveModel end def serialize - args = [root] - - if options[:namespace] - args << {:xmlns => options[:namespace]} - end + require 'builder' unless defined? ::Builder - if options[:type] - args << {:type => options[:type]} - end - - builder.tag!(*args) do - add_attributes - procs = options.delete(:procs) - options[:procs] = procs - add_procs - yield builder if block_given? - end - end + options[:indent] ||= 2 + options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent]) - private - def builder - @builder ||= begin - require 'builder' unless defined? ::Builder - options[:indent] ||= 2 - builder = options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent]) + @builder = options[:builder] + @builder.instruct! unless options[:skip_instruct] - unless options[:skip_instruct] - builder.instruct! - options[:skip_instruct] = true - end + root = (options[:root] || @serializable.class.model_name.singular).to_s + root = ActiveSupport::XmlMini.rename_key(root, options) - builder - end - end - - def root - root = (options[:root] || @serializable.class.model_name.singular).to_s - reformat_name(root) - end + args = [root] + args << {:xmlns => options[:namespace]} if options[:namespace] + args << {:type => options[:type]} if options[:type] && !options[:skip_types] - def dasherize? - !options.has_key?(:dasherize) || options[:dasherize] + @builder.tag!(*args) do + add_attributes_and_methods + add_extra_behavior + add_procs + yield @builder if block_given? end + end - def camelize? - options.has_key?(:camelize) && options[:camelize] - end + private - def reformat_name(name) - name = name.camelize if camelize? - dasherize? ? name.dasherize : name - end + def add_extra_behavior + end - def add_attributes - (serializable_attributes + serializable_method_attributes).each do |attribute| - builder.tag!( - reformat_name(attribute.name), - attribute.value.to_s, - attribute.decorations(!options[:skip_types]) - ) - end + def add_attributes_and_methods + (serializable_attributes + serializable_methods).each do |attribute| + key = ActiveSupport::XmlMini.rename_key(attribute.name, options) + ActiveSupport::XmlMini.to_tag(key, attribute.value, + options.merge(attribute.decorations)) end + end - def add_procs - if procs = options.delete(:procs) - [ *procs ].each do |proc| - if proc.arity > 1 - proc.call(options, @serializable) - else - proc.call(options) - end + def add_procs + if procs = options.delete(:procs) + Array.wrap(procs).each do |proc| + if proc.arity == 1 + proc.call(options) + else + proc.call(options, @serializable) end end end + end end def to_xml(options = {}, &block) diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 708557f4ae..c69cabc888 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -133,6 +133,9 @@ module ActiveModel _validators[attribute.to_sym] end + def attribute_method?(attribute) + method_defined?(attribute) + end private def _merge_attributes(attr_names) diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index 0423fcd17f..fbd622eb6d 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -14,8 +14,10 @@ module ActiveModel def setup(klass) # Note: instance_methods.map(&:to_s) is important for 1.9 compatibility # as instance_methods returns symbols unlike 1.8 which returns strings. - new_attributes = attributes.reject { |name| klass.instance_methods.map(&:to_s).include?("#{name}=") } - klass.send(:attr_accessor, *new_attributes) + attr_readers = attributes.reject { |name| klass.attribute_method?(name) } + attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") } + klass.send(:attr_reader, *attr_readers) + klass.send(:attr_writer, *attr_writers) end end diff --git a/activemodel/test/cases/serializeration/xml_serialization_test.rb b/activemodel/test/cases/serializeration/xml_serialization_test.rb index 6340aad531..3ba826a8d0 100644 --- a/activemodel/test/cases/serializeration/xml_serialization_test.rb +++ b/activemodel/test/cases/serializeration/xml_serialization_test.rb @@ -1,6 +1,7 @@ require 'cases/helper' require 'models/contact' require 'active_support/core_ext/object/instance_variables' +require 'ostruct' class Contact extend ActiveModel::Naming @@ -23,7 +24,9 @@ class XmlSerializationTest < ActiveModel::TestCase @contact.age = 25 @contact.created_at = Time.utc(2006, 8, 1) @contact.awesome = false - @contact.preferences = { :gem => 'ruby' } + customer = OpenStruct.new + customer.name = "John" + @contact.preferences = customer end test "should serialize default root" do @@ -92,8 +95,16 @@ class XmlSerializationTest < ActiveModel::TestCase assert_match %r{<awesome type=\"boolean\">false</awesome>}, @contact.to_xml end + test "should serialize array" do + assert_match %r{<social type=\"array\">\s*<social>twitter</social>\s*<social>github</social>\s*</social>}, @contact.to_xml(:methods => :social) + end + + test "should serialize hash" do + assert_match %r{<network>\s*<git type=\"symbol\">github</git>\s*</network>}, @contact.to_xml(:methods => :network) + end + test "should serialize yaml" do - assert_match %r{<preferences type=\"yaml\">--- \n:gem: ruby\n</preferences>}, @contact.to_xml + assert_match %r{<preferences type=\"yaml\">--- !ruby/object:OpenStruct \ntable:\s*:name: John\n</preferences>}, @contact.to_xml end test "should call proc on object" do diff --git a/activemodel/test/models/contact.rb b/activemodel/test/models/contact.rb index a9009fbdef..605e435f39 100644 --- a/activemodel/test/models/contact.rb +++ b/activemodel/test/models/contact.rb @@ -3,6 +3,14 @@ class Contact attr_accessor :id, :name, :age, :created_at, :awesome, :preferences + def social + %w(twitter github) + end + + def network + {:git => :github} + end + def initialize(options = {}) options.each { |name, value| send("#{name}=", value) } end diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index fcb0e31f79..ac5bd8e635 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,9 @@ *Rails 3.0.0 [beta 4/release candidate] (unreleased)* +* New callbacks: after_commit and after_rollback. Do expensive operations like image thumbnailing after_commit instead of after_save. #2991 [Brian Durand] + +* Serialized attributes are not converted to YAML if they are any of the formats that can be serialized to XML (like Hash, Array and Strings). [José Valim] + * Destroy uses optimistic locking. If lock_version on the record you're destroying doesn't match lock_version in the database, a StaleObjectError is raised. #1966 [Curtis Hawthorne] * PostgreSQL: drop support for old postgres driver. Use pg 0.9.0 or later. [Jeremy Kemper] diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 6dbee9f4bf..6c64210c92 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1737,6 +1737,14 @@ module ActiveRecord build(associations) end + def graft(*associations) + associations.each do |association| + join_associations.detect {|a| association == a} || + build(association.reflection.name, association.find_parent_in(self), association.join_class) + end + self + end + def join_associations @joins[1..-1].to_a end @@ -1745,6 +1753,16 @@ module ActiveRecord @joins[0] end + def count_aliases_from_table_joins(name) + quoted_name = join_base.active_record.connection.quote_table_name(name.downcase) + join_sql = join_base.table_joins.to_s.downcase + join_sql.blank? ? 0 : + # Table names + join_sql.scan(/join(?:\s+\w+)?\s+#{quoted_name}\son/).size + + # Table aliases + join_sql.scan(/join(?:\s+\w+)?\s+\S+\s+#{quoted_name}\son/).size + end + def instantiate(rows) rows.each_with_index do |row, i| primary_id = join_base.record_id(row) @@ -1789,22 +1807,22 @@ module ActiveRecord end protected - def build(associations, parent = nil) + def build(associations, parent = nil, join_class = Arel::InnerJoin) parent ||= @joins.last case associations when Symbol, String reflection = parent.reflections[associations.to_s.intern] or raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?" @reflections << reflection - @joins << build_join_association(reflection, parent) + @joins << build_join_association(reflection, parent).with_join_class(join_class) when Array associations.each do |association| - build(association, parent) + build(association, parent, join_class) end when Hash associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name| - build(name, parent) - build(associations[name]) + build(name, parent, join_class) + build(associations[name], nil, join_class) end else raise ConfigurationError, associations.inspect @@ -1881,6 +1899,12 @@ module ActiveRecord @table_joins = joins end + def ==(other) + other.is_a?(JoinBase) && + other.active_record == active_record && + other.table_joins == table_joins + end + def aliased_prefix "t0" end @@ -1946,6 +1970,27 @@ module ActiveRecord end end + def ==(other) + other.is_a?(JoinAssociation) && + other.reflection == reflection && + other.parent == parent + end + + def find_parent_in(other_join_dependency) + other_join_dependency.joins.detect do |join| + self.parent == join + end + end + + def join_class + @join_class ||= Arel::InnerJoin + end + + def with_join_class(join_class) + @join_class = join_class + self + end + def association_join return @join if @join @@ -2045,27 +2090,25 @@ module ActiveRecord end def join_relation(joining_relation, join = nil) - if (relations = relation).is_a?(Array) - joining_relation.joins(Relation::JoinOperation.new(relations.first, Arel::OuterJoin, association_join.first)). - joins(Relation::JoinOperation.new(relations.last, Arel::OuterJoin, association_join.last)) - else - joining_relation.joins(Relation::JoinOperation.new(relations, Arel::OuterJoin, association_join)) - end + joining_relation.joins(self.with_join_class(Arel::OuterJoin)) end protected def aliased_table_name_for(name, suffix = nil) - if !parent.table_joins.blank? && parent.table_joins.to_s.downcase =~ %r{join(\s+\w+)?\s+#{active_record.connection.quote_table_name name.downcase}\son} - @join_dependency.table_aliases[name] += 1 + if @join_dependency.table_aliases[name].zero? + @join_dependency.table_aliases[name] = @join_dependency.count_aliases_from_table_joins(name) end - unless @join_dependency.table_aliases[name].zero? - # if the table name has been used, then use an alias + if !@join_dependency.table_aliases[name].zero? # We need an alias name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}" - table_index = @join_dependency.table_aliases[name] @join_dependency.table_aliases[name] += 1 - name = name[0..active_record.connection.table_alias_length-3] + "_#{table_index+1}" if table_index > 0 + if @join_dependency.table_aliases[name] == 1 # First time we've seen this name + # Also need to count the aliases from the table_aliases to avoid incorrect count + @join_dependency.table_aliases[name] += @join_dependency.count_aliases_from_table_joins(name) + end + table_index = @join_dependency.table_aliases[name] + name = name[0..active_record.connection.table_alias_length-3] + "_#{table_index}" if table_index > 1 else @join_dependency.table_aliases[name] += 1 end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 2d7cfad80d..9ed53cc4af 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -931,6 +931,10 @@ module ActiveRecord #:nodoc: subclasses.each { |klass| klass.reset_inheritable_attributes; klass.reset_column_information } end + def attribute_method?(attribute) + super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, ''))) + end + # Set the lookup ancestors for ActiveModel. def lookup_ancestors #:nodoc: klass = 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 0c87e052c4..b9fb452eee 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -122,6 +122,8 @@ module ActiveRecord requires_new = options[:requires_new] || !last_transaction_joinable transaction_open = false + @_current_transaction_records ||= [] + begin if block_given? if requires_new || open_transactions == 0 @@ -132,6 +134,7 @@ module ActiveRecord end increment_open_transactions transaction_open = true + @_current_transaction_records.push([]) end yield end @@ -141,8 +144,10 @@ module ActiveRecord decrement_open_transactions if open_transactions == 0 rollback_db_transaction + rollback_transaction_records(true) else rollback_to_savepoint + rollback_transaction_records(false) end end raise unless database_transaction_rollback.is_a?(ActiveRecord::Rollback) @@ -157,20 +162,35 @@ module ActiveRecord begin if open_transactions == 0 commit_db_transaction + commit_transaction_records else release_savepoint + save_point_records = @_current_transaction_records.pop + unless save_point_records.blank? + @_current_transaction_records.push([]) if @_current_transaction_records.empty? + @_current_transaction_records.last.concat(save_point_records) + end end rescue Exception => database_transaction_rollback if open_transactions == 0 rollback_db_transaction + rollback_transaction_records(true) else rollback_to_savepoint + rollback_transaction_records(false) end raise end end end + # Register a record with the current transaction so that its after_commit and after_rollback callbacks + # can be called. + def add_transaction_record(record) + last_batch = @_current_transaction_records.last + last_batch << record if last_batch + end + # Begins the transaction (and turns off auto-committing). def begin_db_transaction() end @@ -268,6 +288,42 @@ module ActiveRecord limit.to_i end end + + # Send a rollback message to all records after they have been rolled back. If rollback + # is false, only rollback records since the last save point. + def rollback_transaction_records(rollback) #:nodoc + if rollback + records = @_current_transaction_records.flatten + @_current_transaction_records.clear + else + records = @_current_transaction_records.pop + end + + unless records.blank? + records.uniq.each do |record| + begin + record.rolledback!(rollback) + rescue Exception => e + record.logger.error(e) if record.respond_to?(:logger) + end + end + end + end + + # Send a commit message to all records after they have been committed. + def commit_transaction_records #:nodoc + records = @_current_transaction_records.flatten + @_current_transaction_records.clear + unless records.blank? + records.uniq.each do |record| + begin + record.committed! + rescue Exception => e + record.logger.error(e) if record.respond_to?(:logger) + end + end + end + end end end end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index c163fb982a..940f825038 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -384,9 +384,13 @@ module ActiveRecord class << self def migrate(migrations_path, target_version = nil) case - when target_version.nil? then up(migrations_path, target_version) - when current_version > target_version then down(migrations_path, target_version) - else up(migrations_path, target_version) + when target_version.nil? + up(migrations_path, target_version) + when current_version == 0 && target_version == 0 + when current_version > target_version + down(migrations_path, target_version) + else + up(migrations_path, target_version) end end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index f3d21d4969..898df0a67a 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -61,13 +61,8 @@ module ActiveRecord # Setup database middleware after initializers have run initializer "active_record.initialize_database_middleware", :after => "action_controller.set_configs" do |app| middleware = app.config.middleware - if middleware.include?("ActiveRecord::SessionStore") - middleware.insert_before "ActiveRecord::SessionStore", ActiveRecord::ConnectionAdapters::ConnectionManagement - middleware.insert_before "ActiveRecord::SessionStore", ActiveRecord::QueryCache - else - middleware.use ActiveRecord::ConnectionAdapters::ConnectionManagement - middleware.use ActiveRecord::QueryCache - end + middleware.insert_after "::ActionDispatch::Callbacks", ActiveRecord::QueryCache + middleware.insert_after "::ActionDispatch::Callbacks", ActiveRecord::ConnectionAdapters::ConnectionManagement end initializer "active_record.set_dispatch_hooks", :before => :set_clear_dependencies_hook do |app| diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 3514d0a259..d6144dc206 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -188,7 +188,6 @@ module ActiveRecord def construct_relation_for_association_calculations including = (@eager_load_values + @includes_values).uniq join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, arel.joins(arel)) - relation = except(:includes, :eager_load, :preload) apply_join_dependency(relation, join_dependency) end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 58af930446..7bca12d85e 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -80,6 +80,26 @@ module ActiveRecord @arel ||= build_arel end + def custom_join_sql(*joins) + arel = table + joins.each do |join| + next if join.blank? + + @implicit_readonly = true + + case join + when Hash, Array, Symbol + if array_of_strings?(join) + join_string = join.join(' ') + arel = arel.join(join_string) + end + else + arel = arel.join(join) + end + end + arel.joins(arel) + end + def build_arel arel = table @@ -88,50 +108,41 @@ module ActiveRecord joins = @joins_values.map {|j| j.respond_to?(:strip) ? j.strip : j}.uniq - # Build association joins first joins.each do |join| association_joins << join if [Hash, Array, Symbol].include?(join.class) && !array_of_strings?(join) end - if association_joins.any? - join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, association_joins.uniq, nil) - to_join = [] + stashed_association_joins = joins.select {|j| j.is_a?(ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation)} - join_dependency.join_associations.each do |association| - if (association_relation = association.relation).is_a?(Array) - to_join << [association_relation.first, association.association_join.first] - to_join << [association_relation.last, association.association_join.last] - else - to_join << [association_relation, association.association_join] - end - end + non_association_joins = (joins - association_joins - stashed_association_joins).reject {|j| j.blank?} + custom_joins = custom_join_sql(*non_association_joins) - to_join.each do |tj| - unless joined_associations.detect {|ja| ja[0] == tj[0] && ja[1] == tj[1] } - joined_associations << tj - arel = arel.join(tj[0]).on(*tj[1]) - end - end - end + join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, association_joins, custom_joins) - joins.each do |join| - next if join.blank? + join_dependency.graft(*stashed_association_joins) - @implicit_readonly = true + @implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty? - case join - when Relation::JoinOperation - arel = arel.join(join.relation, join.join_class).on(*join.on) - when Hash, Array, Symbol - if array_of_strings?(join) - join_string = join.join(' ') - arel = arel.join(join_string) - end + to_join = [] + + join_dependency.join_associations.each do |association| + if (association_relation = association.relation).is_a?(Array) + to_join << [association_relation.first, association.join_class, association.association_join.first] + to_join << [association_relation.last, association.join_class, association.association_join.last] else - arel = arel.join(join) + to_join << [association_relation, association.join_class, association.association_join] end end + to_join.each do |tj| + unless joined_associations.detect {|ja| ja[0] == tj[0] && ja[1] == tj[1] && ja[2] == tj[2] } + joined_associations << tj + arel = arel.join(tj[0], tj[1]).on(*tj[2]) + end + end + + arel = arel.join(custom_joins) + @where_values.uniq.each do |where| next if where.blank? diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index 255b03433d..b2d4a48945 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -182,16 +182,31 @@ module ActiveRecord #:nodoc: options[:except] |= Array.wrap(@serializable.class.inheritance_column) end + def add_extra_behavior + add_includes + end + + def add_includes + procs = options.delete(:procs) + @serializable.send(:serializable_add_includes, options) do |association, records, opts| + add_associations(association, records, opts) + end + options[:procs] = procs + end + + # TODO This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well. def add_associations(association, records, opts) + association_name = association.to_s.singularize + merged_options = options.merge(opts).merge!(:root => association_name, :skip_instruct => true) + if records.is_a?(Enumerable) - tag = reformat_name(association.to_s) - type = options[:skip_types] ? {} : {:type => "array"} + tag = ActiveSupport::XmlMini.rename_key(association.to_s, options) + type = options[:skip_types] ? { } : {:type => "array"} if records.empty? - builder.tag!(tag, type) + @builder.tag!(tag, type) else - builder.tag!(tag, type) do - association_name = association.to_s.singularize + @builder.tag!(tag, type) do records.each do |record| if options[:skip_types] record_type = {} @@ -200,60 +215,30 @@ module ActiveRecord #:nodoc: record_type = {:type => record_class} end - record.to_xml opts.merge(:root => association_name).merge(record_type) + record.to_xml merged_options.merge(record_type) end end end - else - if record = @serializable.send(association) - record.to_xml(opts.merge(:root => association)) - end - end - end - - def serialize - args = [root] - if options[:namespace] - args << {:xmlns=>options[:namespace]} - end - - if options[:type] - args << {:type=>options[:type]} - end - - builder.tag!(*args) do - add_attributes - procs = options.delete(:procs) - @serializable.send(:serializable_add_includes, options) { |association, records, opts| - add_associations(association, records, opts) - } - options[:procs] = procs - add_procs - yield builder if block_given? + elsif record = @serializable.send(association) + record.to_xml(merged_options) end end class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc: - protected - def compute_type - type = @serializable.class.serialized_attributes.has_key?(name) ? :yaml : @serializable.class.columns_hash[name].type - - case type - when :text - :string - when :time - :datetime - else - type - end - end - end + def compute_type + type = @serializable.class.serialized_attributes.has_key?(name) ? + super : @serializable.class.columns_hash[name].type - class MethodAttribute < Attribute #:nodoc: - protected - def compute_type - Hash::XML_TYPE_NAMES[@serializable.send(name).class.name] || :string + case type + when :text + :string + when :time + :datetime + else + type end + end + protected :compute_type end end end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index cf0fe8934d..796dd99f02 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -12,6 +12,9 @@ module ActiveRecord [:destroy, :save, :save!].each do |method| alias_method_chain method, :transactions end + + define_model_callbacks :commit, :commit_on_update, :commit_on_create, :commit_on_destroy, :only => :after + define_model_callbacks :rollback, :rollback_on_update, :rollback_on_create, :rollback_on_destroy end # Transactions are protective blocks where SQL statements are only permanent @@ -108,7 +111,7 @@ module ActiveRecord # rescue ActiveRecord::StatementInvalid # # ...which we ignore. # end - # + # # # On PostgreSQL, the transaction is now unusable. The following # # statement will cause a PostgreSQL error, even though the unique # # constraint is no longer violated: @@ -132,7 +135,7 @@ module ActiveRecord # raise ActiveRecord::Rollback # end # end - # + # # User.find(:all) # => empty # # It is also possible to requires a sub-transaction by passing @@ -147,7 +150,7 @@ module ActiveRecord # raise ActiveRecord::Rollback # end # end - # + # # User.find(:all) # => Returns only Kotori # # Most databases don't support true nested transactions. At the time of @@ -157,16 +160,31 @@ module ActiveRecord # http://dev.mysql.com/doc/refman/5.0/en/savepoints.html # for more information about savepoints. # + # === Callbacks + # + # There are two types of callbacks associated with committing and rolling back transactions: + # +after_commit+ and +after_rollback+. + # + # +after_commit+ callbacks are called on every record saved or destroyed within a + # transaction immediately after the transaction is committed. +after_rollback+ callbacks + # are called on every record saved or destroyed within a transaction immediately after the + # transaction or savepoint is rolled back. + # + # These callbacks are useful for interacting with other systems since you will be guaranteed + # that the callback is only executed when the database is in a permanent state. For example, + # +after_commit+ is a good spot to put in a hook to clearing a cache since clearing it from + # within a transaction could trigger the cache to be regenerated before the database is updated. + # # === Caveats # # If you're on MySQL, then do not use DDL operations in nested transactions # blocks that are emulated with savepoints. That is, do not execute statements # like 'CREATE TABLE' inside such blocks. This is because MySQL automatically - # releases all savepoints upon executing a DDL operation. When #transaction + # releases all savepoints upon executing a DDL operation. When +transaction+ # is finished and tries to release the savepoint it created earlier, a # database error will occur because the savepoint has already been # automatically released. The following example demonstrates the problem: - # + # # Model.connection.transaction do # BEGIN # Model.connection.transaction(:requires_new => true) do # CREATE SAVEPOINT active_record_1 # Model.connection.create_table(...) # active_record_1 now automatically released @@ -197,24 +215,55 @@ module ActiveRecord end def save_with_transactions! #:nodoc: - rollback_active_record_state! { self.class.transaction { save_without_transactions! } } + with_transaction_returning_status(:save_without_transactions!) end # Reset id and @new_record if the transaction rolls back. def rollback_active_record_state! - id_present = has_attribute?(self.class.primary_key) - previous_id = id - previous_new_record = new_record? + remember_transaction_record_state yield rescue Exception - @new_record = previous_new_record - if id_present - self.id = previous_id + restore_transaction_record_state + raise + ensure + clear_transaction_record_state + end + + # Call the after_commit callbacks + def committed! #:nodoc: + if transaction_record_state(:new_record) + _run_commit_on_create_callbacks + elsif transaction_record_state(:destroyed) + _run_commit_on_destroy_callbacks else - @attributes.delete(self.class.primary_key) - @attributes_cache.delete(self.class.primary_key) + _run_commit_on_update_callbacks + end + _run_commit_callbacks + ensure + clear_transaction_record_state + end + + # Call the after rollback callbacks. The restore_state argument indicates if the record + # state should be rolled back to the beginning or just to the last savepoint. + def rolledback!(force_restore_state = false) #:nodoc: + if transaction_record_state(:new_record) + _run_rollback_on_create_callbacks + elsif transaction_record_state(:destroyed) + _run_rollback_on_destroy_callbacks + else + _run_rollback_on_update_callbacks + end + _run_rollback_callbacks + ensure + restore_transaction_record_state(force_restore_state) + end + + # Add the record to the current transaction so that the :after_rollback and :after_commit callbacks + # can be called. + def add_to_transaction + if self.class.connection.add_transaction_record(self) + remember_transaction_record_state end - raise end # Executes +method+ within a transaction and captures its return value as a @@ -226,10 +275,59 @@ module ActiveRecord def with_transaction_returning_status(method, *args) status = nil self.class.transaction do + add_to_transaction status = send(method, *args) raise ActiveRecord::Rollback unless status end status end + + protected + + # Save the new record state and id of a record so it can be restored later if a transaction fails. + def remember_transaction_record_state #:nodoc + @_start_transaction_state ||= {} + unless @_start_transaction_state.include?(:new_record) + @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key) + @_start_transaction_state[:new_record] = @new_record + end + unless @_start_transaction_state.include?(:destroyed) + @_start_transaction_state[:destroyed] = @new_record + end + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 + end + + # Clear the new record state and id of a record. + def clear_transaction_record_state #:nodoc + if defined?(@_start_transaction_state) + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 + remove_instance_variable(:@_start_transaction_state) if @_start_transaction_state[:level] < 1 + end + end + + # Restore the new record state and id of a record that was previously saved by a call to save_record_state. + def restore_transaction_record_state(force = false) #:nodoc + if defined?(@_start_transaction_state) + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 + if @_start_transaction_state[:level] < 1 + restore_state = remove_instance_variable(:@_start_transaction_state) + if restore_state + @new_record = restore_state[:new_record] + @destroyed = restore_state[:destroyed] + if restore_state[:id] + self.id = restore_state[:id] + else + @attributes.delete(self.class.primary_key) + @attributes_cache.delete(self.class.primary_key) + end + end + end + end + end + + # Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed. + def transaction_record_state(state) #:nodoc + @_start_transaction_state[state] if defined?(@_start_transaction_state) + end end end diff --git a/activerecord/lib/rails/generators/active_record.rb b/activerecord/lib/rails/generators/active_record.rb index d2b1e86857..5d8a8e81bc 100644 --- a/activerecord/lib/rails/generators/active_record.rb +++ b/activerecord/lib/rails/generators/active_record.rb @@ -8,16 +8,12 @@ module ActiveRecord class Base < Rails::Generators::NamedBase #:nodoc: include Rails::Generators::Migration - def self.source_root - @_ar_source_root ||= begin - if base_name && generator_name - File.expand_path(File.join(base_name, generator_name, 'templates'), File.dirname(__FILE__)) - end - end + # Set the current directory as base for the inherited generators. + def self.base_root + File.dirname(__FILE__) end # Implement the required interface for Rails::Generators::Migration. - # def self.next_migration_number(dirname) #:nodoc: next_migration_number = current_migration_number(dirname) + 1 if ActiveRecord::Base.timestamped_migrations diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index ed2e2e9f8f..fe558f9d3b 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -29,6 +29,15 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase assert_equal 2, authors[1].categorizations.size end + def test_eager_association_loading_with_hmt_does_not_table_name_collide_when_joining_associations + assert_nothing_raised do + Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).all + end + authors = Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).all + assert_equal 1, assert_no_queries { authors.size } + assert_equal 9, assert_no_queries { authors[0].comments.size } + end + def test_eager_association_loading_with_cascaded_two_levels_with_two_has_many_associations authors = Author.find(:all, :include=>{:posts=>[:comments, :categorizations]}, :order=>"authors.id") assert_equal 2, authors.size diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 2f4243a6aa..3623680de9 100755 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -2085,6 +2085,7 @@ class BasicsTest < ActiveRecord::TestCase assert_equal "topic", xml.root.name assert_equal "The First Topic" , xml.elements["//title"].text assert_equal "David" , xml.elements["//author-name"].text + assert_match "Have a nice day", xml.elements["//content"].text assert_equal "1", xml.elements["//id"].text assert_equal "integer" , xml.elements["//id"].attributes['type'] @@ -2095,10 +2096,6 @@ class BasicsTest < ActiveRecord::TestCase assert_equal written_on_in_current_timezone, xml.elements["//written-on"].text assert_equal "datetime" , xml.elements["//written-on"].attributes['type'] - assert_match(/^--- Have a nice day\n/ , xml.elements["//content"].text) - assert_equal 'Have a nice day' , YAML.load(xml.elements["//content"].text) - assert_equal "yaml" , xml.elements["//content"].attributes['type'] - assert_equal "david@loudthinking.com", xml.elements["//author-email-address"].text assert_equal nil, xml.elements["//parent-id"].text diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 7a26ee072d..a3d1ceaa1f 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -1136,6 +1136,25 @@ if ActiveRecord::Base.connection.supports_migrations? load(MIGRATIONS_ROOT + "/valid/1_people_have_last_names.rb") end + def test_target_version_zero_should_run_only_once + # migrate up to 1 + ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", 1) + + # migrate down to 0 + ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", 0) + + # now unload the migrations that have been defined + PeopleHaveLastNames.unloadable + ActiveSupport::Dependencies.remove_unloadable_constants! + + # migrate down to 0 again + ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", 0) + + assert !defined? PeopleHaveLastNames + ensure + load(MIGRATIONS_ROOT + "/valid/1_people_have_last_names.rb") + end + def test_migrator_db_has_no_schema_migrations_table # Oracle adapter raises error if semicolon is present as last character if current_adapter?(:OracleAdapter) diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index 8841694271..8c385af97c 100644 --- a/activerecord/test/cases/serialization_test.rb +++ b/activerecord/test/cases/serialization_test.rb @@ -44,4 +44,11 @@ class SerializationTest < ActiveRecord::TestCase assert_equal @contact_attributes[:awesome], contact.awesome, "For #{format}" end end + + def test_serialize_should_xml_skip_instruct_for_included_records + @contact.alternative = Contact.new(:name => 'Copa Cabana') + @serialized = @contact.to_xml(:include => [ :alternative ]) + assert_equal @serialized.index('<?xml '), 0 + assert_nil @serialized.index('<?xml ', 1) + end end diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb new file mode 100644 index 0000000000..a07da093f1 --- /dev/null +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -0,0 +1,244 @@ +require "cases/helper" +require 'models/topic' +require 'models/reply' + +class TransactionCallbacksTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + fixtures :topics + + class TopicWithCallbacks < ActiveRecord::Base + set_table_name :topics + + after_commit{|record| record.send(:do_after_commit, nil)} + after_commit(:on => :create){|record| record.send(:do_after_commit, :create)} + after_commit(:on => :update){|record| record.send(:do_after_commit, :update)} + after_commit(:on => :destroy){|record| record.send(:do_after_commit, :destroy)} + after_rollback{|record| record.send(:do_after_rollback, nil)} + after_rollback(:on => :create){|record| record.send(:do_after_rollback, :create)} + after_rollback(:on => :update){|record| record.send(:do_after_rollback, :update)} + after_rollback(:on => :destroy){|record| record.send(:do_after_rollback, :destroy)} + + def history + @history ||= [] + end + + def after_commit_block(on = nil, &block) + @after_commit ||= {} + @after_commit[on] ||= [] + @after_commit[on] << block + end + + def after_rollback_block(on = nil, &block) + @after_rollback ||= {} + @after_rollback[on] ||= [] + @after_rollback[on] << block + end + + def do_after_commit(on) + blocks = @after_commit[on] if defined?(@after_commit) + blocks.each{|b| b.call(self)} if blocks + end + + def do_after_rollback(on) + blocks = @after_rollback[on] if defined?(@after_rollback) + blocks.each{|b| b.call(self)} if blocks + end + end + + def setup + @first, @second = TopicWithCallbacks.find(1, 3).sort_by { |t| t.id } + end + + def test_call_after_commit_after_transaction_commits + @first.after_commit_block{|r| r.history << :after_commit} + @first.after_rollback_block{|r| r.history << :after_rollback} + + @first.save! + assert @first.history, [:after_commit] + end + + def test_only_call_after_commit_on_update_after_transaction_commits_for_existing_record + commit_callback = [] + @first.after_commit_block(:create){|r| r.history << :commit_on_create} + @first.after_commit_block(:update){|r| r.history << :commit_on_update} + @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} + @first.after_commit_block(:create){|r| r.history << :rollback_on_create} + @first.after_commit_block(:update){|r| r.history << :rollback_on_update} + @first.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy} + + @first.save! + assert @first.history, [:commit_on_update] + end + + def test_only_call_after_commit_on_destroy_after_transaction_commits_for_destroyed_record + commit_callback = [] + @first.after_commit_block(:create){|r| r.history << :commit_on_create} + @first.after_commit_block(:update){|r| r.history << :commit_on_update} + @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} + @first.after_commit_block(:create){|r| r.history << :rollback_on_create} + @first.after_commit_block(:update){|r| r.history << :rollback_on_update} + @first.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy} + + @first.destroy + assert @first.history, [:commit_on_destroy] + end + + def test_only_call_after_commit_on_create_after_transaction_commits_for_new_record + @new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today) + @new_record.after_commit_block(:create){|r| r.history << :commit_on_create} + @new_record.after_commit_block(:update){|r| r.history << :commit_on_update} + @new_record.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} + @new_record.after_commit_block(:create){|r| r.history << :rollback_on_create} + @new_record.after_commit_block(:update){|r| r.history << :rollback_on_update} + @new_record.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy} + + @new_record.save! + assert @new_record.history, [:commit_on_create] + end + + def test_call_after_rollback_after_transaction_rollsback + @first.after_commit_block{|r| r.history << :after_commit} + @first.after_rollback_block{|r| r.history << :after_rollback} + + Topic.transaction do + @first.save! + raise ActiveRecord::Rollback + end + + assert @first.history, [:after_rollback] + end + + def test_only_call_after_rollback_on_update_after_transaction_rollsback_for_existing_record + commit_callback = [] + @first.after_commit_block(:create){|r| r.history << :commit_on_create} + @first.after_commit_block(:update){|r| r.history << :commit_on_update} + @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} + @first.after_commit_block(:create){|r| r.history << :rollback_on_create} + @first.after_commit_block(:update){|r| r.history << :rollback_on_update} + @first.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy} + + Topic.transaction do + @first.save! + raise ActiveRecord::Rollback + end + + assert @first.history, [:rollback_on_update] + end + + def test_only_call_after_rollback_on_destroy_after_transaction_rollsback_for_destroyed_record + commit_callback = [] + @first.after_commit_block(:create){|r| r.history << :commit_on_create} + @first.after_commit_block(:update){|r| r.history << :commit_on_update} + @first.after_commit_block(:destroy){|r| r.history << :commit_on_update} + @first.after_commit_block(:create){|r| r.history << :rollback_on_create} + @first.after_commit_block(:update){|r| r.history << :rollback_on_update} + @first.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy} + + Topic.transaction do + @first.destroy + raise ActiveRecord::Rollback + end + + assert @first.history, [:rollback_on_destroy] + end + + def test_only_call_after_rollback_on_create_after_transaction_rollsback_for_new_record + @new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today) + @new_record.after_commit_block(:create){|r| r.history << :commit_on_create} + @new_record.after_commit_block(:update){|r| r.history << :commit_on_update} + @new_record.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} + @new_record.after_commit_block(:create){|r| r.history << :rollback_on_create} + @new_record.after_commit_block(:update){|r| r.history << :rollback_on_update} + @new_record.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy} + + Topic.transaction do + @new_record.save! + raise ActiveRecord::Rollback + end + + assert @new_record.history, [:rollback_on_create] + end + + def test_call_after_rollback_when_commit_fails + @first.connection.class.send(:alias_method, :real_method_commit_db_transaction, :commit_db_transaction) + begin + @first.connection.class.class_eval do + def commit_db_transaction; raise "boom!"; end + end + + @first.after_commit_block{|r| r.history << :after_commit} + @first.after_rollback_block{|r| r.history << :after_rollback} + + assert !@first.save rescue nil + assert @first.history == [:after_rollback] + ensure + @first.connection.class.send(:remove_method, :commit_db_transaction) + @first.connection.class.send(:alias_method, :commit_db_transaction, :real_method_commit_db_transaction) + end + end + + def test_only_call_after_rollback_on_records_rolled_back_to_a_savepoint + def @first.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end + def @first.commits(i=0); @commits ||= 0; @commits += i if i; end + @first.after_rollback_block{|r| r.rollbacks(1)} + @first.after_commit_block{|r| r.commits(1)} + + def @second.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end + def @second.commits(i=0); @commits ||= 0; @commits += i if i; end + @second.after_rollback_block{|r| r.rollbacks(1)} + @second.after_commit_block{|r| r.commits(1)} + + Topic.transaction do + @first.save! + Topic.transaction(:requires_new => true) do + @second.save! + raise ActiveRecord::Rollback + end + end + + assert 1, @first.commits + assert 0, @first.rollbacks + assert 1, @second.commits + assert 1, @second.rollbacks + end + + def test_only_call_after_rollback_on_records_rolled_back_to_a_savepoint_when_release_savepoint_fails + def @first.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end + def @first.commits(i=0); @commits ||= 0; @commits += i if i; end + + @second.after_rollback_block{|r| r.rollbacks(1)} + @second.after_commit_block{|r| r.commits(1)} + + Topic.transaction do + @first.save + Topic.transaction(:requires_new => true) do + @first.save! + raise ActiveRecord::Rollback + end + Topic.transaction(:requires_new => true) do + @first.save! + raise ActiveRecord::Rollback + end + end + + assert 1, @first.commits + assert 2, @first.rollbacks + end + + def test_after_transaction_callbacks_should_not_raise_errors + def @first.last_after_transaction_error=(e); @last_transaction_error = e; end + def @first.last_after_transaction_error; @last_transaction_error; end + @first.after_commit_block{|r| r.last_after_transaction_error = :commit; raise "fail!";} + @first.after_rollback_block{|r| r.last_after_transaction_error = :rollback; raise "fail!";} + + @first.save! + assert_equal @first.last_after_transaction_error, :commit + + Topic.transaction do + @first.save! + raise ActiveRecord::Rollback + end + + assert_equal @first.last_after_transaction_error, :rollback + end +end diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index c550030329..958a4e4f94 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -262,22 +262,22 @@ class TransactionTest < ActiveRecord::TestCase assert !@first.reload.approved? assert !@second.reload.approved? end if Topic.connection.supports_savepoints? - + def test_many_savepoints Topic.transaction do @first.content = "One" @first.save! - + begin Topic.transaction :requires_new => true do @first.content = "Two" @first.save! - + begin Topic.transaction :requires_new => true do @first.content = "Three" @first.save! - + begin Topic.transaction :requires_new => true do @first.content = "Four" @@ -286,22 +286,22 @@ class TransactionTest < ActiveRecord::TestCase end rescue end - + @three = @first.reload.content raise end rescue end - + @two = @first.reload.content raise end rescue end - + @one = @first.reload.content end - + assert_equal "One", @one assert_equal "Two", @two assert_equal "Three", @three @@ -319,7 +319,34 @@ class TransactionTest < ActiveRecord::TestCase end end end - + + def test_restore_active_record_state_for_all_records_in_a_transaction + topic_1 = Topic.new(:title => 'test_1') + topic_2 = Topic.new(:title => 'test_2') + Topic.transaction do + assert topic_1.save + assert topic_2.save + @first.save + @second.destroy + assert_equal false, topic_1.new_record? + assert_not_nil topic_1.id + assert_equal false, topic_2.new_record? + assert_not_nil topic_2.id + assert_equal false, @first.new_record? + assert_not_nil @first.id + assert_equal true, @second.destroyed? + raise ActiveRecord::Rollback + end + + assert_equal true, topic_1.new_record? + assert_nil topic_1.id + assert_equal true, topic_2.new_record? + assert_nil topic_2.id + assert_equal false, @first.new_record? + assert_not_nil @first.id + assert_equal false, @second.destroyed? + end + if current_adapter?(:PostgreSQLAdapter) && defined?(PGconn::PQTRANS_IDLE) def test_outside_transaction_works assert Topic.connection.outside_transaction? @@ -328,7 +355,7 @@ class TransactionTest < ActiveRecord::TestCase Topic.connection.rollback_db_transaction assert Topic.connection.outside_transaction? end - + def test_rollback_wont_be_executed_if_no_transaction_active assert_raise RuntimeError do Topic.transaction do @@ -338,7 +365,7 @@ class TransactionTest < ActiveRecord::TestCase end end end - + def test_open_transactions_count_is_reset_to_zero_if_no_transaction_active Topic.transaction do Topic.transaction do @@ -358,12 +385,12 @@ class TransactionTest < ActiveRecord::TestCase # # We go back to the connection for the column queries because # Topic.columns is cached and won't report changes to the DB - + assert_nothing_raised do Topic.reset_column_information Topic.connection.add_column('topics', 'stuff', :string) assert Topic.column_names.include?('stuff') - + Topic.reset_column_information Topic.connection.remove_column('topics', 'stuff') assert !Topic.column_names.include?('stuff') @@ -382,6 +409,12 @@ class TransactionTest < ActiveRecord::TestCase end private + def define_callback_method(callback_method) + define_method(callback_method) do + self.history << [callback_method, :method] + end + end + def add_exception_raising_after_save_callback_to_topic Topic.class_eval <<-eoruby, __FILE__, __LINE__ + 1 remove_method(:after_save_for_transaction) @@ -440,7 +473,7 @@ class TransactionsWithTransactionalFixturesTest < ActiveRecord::TestCase def test_automatic_savepoint_in_outer_transaction @first = Topic.find(1) - + begin Topic.transaction do @first.approved = true diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb index b1c75ec8cd..751946ffc5 100644 --- a/activerecord/test/cases/xml_serialization_test.rb +++ b/activerecord/test/cases/xml_serialization_test.rb @@ -79,8 +79,8 @@ class DefaultXmlSerializationTest < ActiveRecord::TestCase assert_match %r{<awesome type=\"boolean\">false</awesome>}, @xml end - def test_should_serialize_yaml - assert_match %r{<preferences type=\"yaml\">---\s?\n:gem: ruby\n</preferences>}, @xml + def test_should_serialize_hash + assert_match %r{<preferences>\s*<gem>ruby</gem>\s*</preferences>}m, @xml end end @@ -234,4 +234,12 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase assert types.include?('StiPost') end + def test_should_produce_xml_for_methods_returning_array + xml = authors(:david).to_xml(:methods => :social) + array = Hash.from_xml(xml)['author']['social'] + assert_equal 2, array.size + assert array.include? 'twitter' + assert array.include? 'github' + end + end diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 025f6207f8..655b45bf57 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -104,6 +104,10 @@ class Author < ActiveRecord::Base "#{id}-#{name}" end + def social + %w(twitter github) + end + private def log_before_adding(object) @post_log << "before_adding#{object.id || '<new>'}" diff --git a/activerecord/test/models/contact.rb b/activerecord/test/models/contact.rb index dbfa57bf49..975a885331 100644 --- a/activerecord/test/models/contact.rb +++ b/activerecord/test/models/contact.rb @@ -13,4 +13,6 @@ class Contact < ActiveRecord::Base column :preferences, :string serialize :preferences -end
\ No newline at end of file + + belongs_to :alternative, :class_name => 'Contact' +end diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG index 7bfc377ff1..f24a1b1c6c 100644 --- a/activesupport/CHANGELOG +++ b/activesupport/CHANGELOG @@ -1,5 +1,7 @@ *Rails 3.0.0 [beta 4/release candidate] (unreleased)* +* Array#to_xml is more powerful and able to handle the same types as Hash#to_xml #4490 [Neeraj Singh] + * Harmonize the caching API and refactor the backends. #4452 [Brian Durand] All caches: * Add default options to initializer that will be sent to all read, write, fetch, exist?, increment, and decrement diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index ad1401bfa9..0fea84a6ef 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -19,7 +19,7 @@ Gem::Specification.new do |s| s.has_rdoc = true - s.add_dependency('i18n', '~> 0.3.6') + s.add_dependency('i18n', '~> 0.4.0.beta') s.add_dependency('tzinfo', '~> 0.3.16') s.add_dependency('builder', '~> 2.1.2') s.add_dependency('memcache-client', '>= 1.7.5') diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb index 8942587ac8..efb5ad26ab 100644 --- a/activesupport/lib/active_support/cache/strategy/local_cache.rb +++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb @@ -56,6 +56,13 @@ module ActiveSupport @middleware ||= begin klass = Class.new klass.class_eval(<<-EOS, __FILE__, __LINE__ + 1) + class << self + def name + "ActiveSupport::Cache::Strategy::LocalCache" + end + alias :to_s :name + end + def initialize(app) @app = app end @@ -67,11 +74,6 @@ module ActiveSupport Thread.current[:#{thread_local_key}] = nil end EOS - - def klass.to_s - "ActiveSupport::Cache::Strategy::LocalCache" - end - klass end end @@ -140,7 +142,7 @@ module ActiveSupport private def thread_local_key - @thread_local_key ||= "#{self.class.name.underscore}_local_cache_#{self.object_id}".gsub("/", "_").to_sym + @thread_local_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, '_').to_sym end def local_cache diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb index 5d8e78e6e5..2b07f05d27 100644 --- a/activesupport/lib/active_support/core_ext/array/conversions.rb +++ b/activesupport/lib/active_support/core_ext/array/conversions.rb @@ -1,6 +1,7 @@ +require 'active_support/xml_mini' require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/hash/reverse_merge' -require 'active_support/inflector' +require 'active_support/core_ext/string/inflections' class Array # Converts the array to a comma-separated sentence where the last element is joined by the connector word. Options: @@ -127,34 +128,31 @@ class Array # </messages> # def to_xml(options = {}) - raise "Not all elements respond to to_xml" unless all? { |e| e.respond_to? :to_xml } require 'builder' unless defined?(Builder) options = options.dup - options[:root] ||= all? { |e| e.is_a?(first.class) && first.class.to_s != "Hash" } ? ActiveSupport::Inflector.pluralize(ActiveSupport::Inflector.underscore(first.class.name)).tr('/', '_') : "records" - options[:children] ||= options[:root].singularize - options[:indent] ||= 2 - options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent]) - - root = options.delete(:root).to_s - children = options.delete(:children) - - if !options.has_key?(:dasherize) || options[:dasherize] - root = root.dasherize + options[:indent] ||= 2 + options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent]) + options[:root] ||= if first.class.to_s != "Hash" && all? { |e| e.is_a?(first.class) } + underscored = ActiveSupport::Inflector.underscore(first.class.name) + ActiveSupport::Inflector.pluralize(underscored).tr('/', '_') + else + "objects" end - options[:builder].instruct! unless options.delete(:skip_instruct) + builder = options[:builder] + builder.instruct! unless options.delete(:skip_instruct) - opts = options.merge({ :root => children }) + root = ActiveSupport::XmlMini.rename_key(options[:root].to_s, options) + children = options.delete(:children) || root.singularize - xml = options[:builder] - if empty? - xml.tag!(root, options[:skip_types] ? {} : {:type => "array"}) - else - xml.tag!(root, options[:skip_types] ? {} : {:type => "array"}) { - yield xml if block_given? - each { |e| e.to_xml(opts.merge({ :skip_instruct => true })) } - } + attributes = options[:skip_types] ? {} : {:type => "array"} + return builder.tag!(root, attributes) if empty? + + builder.__send__(:method_missing, root, attributes) do + each { |value| ActiveSupport::XmlMini.to_tag(children, value, options) } + yield builder if block_given? end end + end diff --git a/activesupport/lib/active_support/core_ext/hash/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb index c882434f78..14e5d2f8ac 100644 --- a/activesupport/lib/active_support/core_ext/hash/conversions.rb +++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb @@ -1,3 +1,4 @@ +require 'active_support/xml_mini' require 'active_support/time' require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/hash/reverse_merge' @@ -5,79 +6,6 @@ require 'active_support/core_ext/object/blank' require 'active_support/core_ext/string/inflections' class Hash - # This module exists to decorate files deserialized using Hash.from_xml with - # the <tt>original_filename</tt> and <tt>content_type</tt> methods. - module FileLike #:nodoc: - attr_writer :original_filename, :content_type - - def original_filename - @original_filename || 'untitled' - end - - def content_type - @content_type || 'application/octet-stream' - end - end - - XML_TYPE_NAMES = { - "Symbol" => "symbol", - "Fixnum" => "integer", - "Bignum" => "integer", - "BigDecimal" => "decimal", - "Float" => "float", - "TrueClass" => "boolean", - "FalseClass" => "boolean", - "Date" => "date", - "DateTime" => "datetime", - "Time" => "datetime" - } unless defined?(XML_TYPE_NAMES) - - XML_FORMATTING = { - "symbol" => Proc.new { |symbol| symbol.to_s }, - "date" => Proc.new { |date| date.to_s(:db) }, - "datetime" => Proc.new { |time| time.xmlschema }, - "binary" => Proc.new { |binary| ActiveSupport::Base64.encode64(binary) }, - "yaml" => Proc.new { |yaml| yaml.to_yaml } - } unless defined?(XML_FORMATTING) - - # TODO: use Time.xmlschema instead of Time.parse; - # use regexp instead of Date.parse - unless defined?(XML_PARSING) - XML_PARSING = { - "symbol" => Proc.new { |symbol| symbol.to_sym }, - "date" => Proc.new { |date| ::Date.parse(date) }, - "datetime" => Proc.new { |time| ::Time.parse(time).utc rescue ::DateTime.parse(time).utc }, - "integer" => Proc.new { |integer| integer.to_i }, - "float" => Proc.new { |float| float.to_f }, - "decimal" => Proc.new { |number| BigDecimal(number) }, - "boolean" => Proc.new { |boolean| %w(1 true).include?(boolean.strip) }, - "string" => Proc.new { |string| string.to_s }, - "yaml" => Proc.new { |yaml| YAML::load(yaml) rescue yaml }, - "base64Binary" => Proc.new { |bin| ActiveSupport::Base64.decode64(bin) }, - "binary" => Proc.new do |bin, entity| - case entity['encoding'] - when 'base64' - ActiveSupport::Base64.decode64(bin) - # TODO: Add support for other encodings - else - bin - end - end, - "file" => Proc.new do |file, entity| - f = StringIO.new(ActiveSupport::Base64.decode64(file)) - f.extend(FileLike) - f.original_filename = entity['name'] - f.content_type = entity['content_type'] - f - end - } - - XML_PARSING.update( - "double" => XML_PARSING["float"], - "dateTime" => XML_PARSING["datetime"] - ) - end - # Returns a string containing an XML representation of its receiver: # # {"foo" => 1, "bar" => 2}.to_xml @@ -130,62 +58,19 @@ class Hash require 'builder' unless defined?(Builder) options = options.dup - options[:indent] ||= 2 - options.reverse_merge!({ :builder => Builder::XmlMarkup.new(:indent => options[:indent]), - :root => "hash" }) - options[:builder].instruct! unless options.delete(:skip_instruct) - root = rename_key(options[:root].to_s, options) + options[:indent] ||= 2 + options[:root] ||= "hash" + options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent]) - options[:builder].__send__(:method_missing, root) do - each do |key, value| - case value - when ::Hash - value.to_xml(options.merge({ :root => key, :skip_instruct => true })) - when ::Array - value.to_xml(options.merge({ :root => key, :children => key.to_s.singularize, :skip_instruct => true})) - when ::Method, ::Proc - # If the Method or Proc takes two arguments, then - # pass the suggested child element name. This is - # used if the Method or Proc will be operating over - # multiple records and needs to create an containing - # element that will contain the objects being - # serialized. - if 1 == value.arity - value.call(options.merge({ :root => key, :skip_instruct => true })) - else - value.call(options.merge({ :root => key, :skip_instruct => true }), key.to_s.singularize) - end - else - if value.respond_to?(:to_xml) - value.to_xml(options.merge({ :root => key, :skip_instruct => true })) - else - type_name = XML_TYPE_NAMES[value.class.name] + builder = options[:builder] + builder.instruct! unless options.delete(:skip_instruct) - key = rename_key(key.to_s, options) - - attributes = options[:skip_types] || value.nil? || type_name.nil? ? { } : { :type => type_name } - if value.nil? - attributes[:nil] = true - end + root = ActiveSupport::XmlMini.rename_key(options[:root].to_s, options) - options[:builder].tag!(key, - XML_FORMATTING[type_name] ? XML_FORMATTING[type_name].call(value) : value, - attributes - ) - end - end - end - - yield options[:builder] if block_given? + builder.__send__(:method_missing, root) do + each { |key, value| ActiveSupport::XmlMini.to_tag(key, value, options) } + yield builder if block_given? end - - end - - def rename_key(key, options = {}) - camelize = options.has_key?(:camelize) && options[:camelize] - dasherize = !options.has_key?(:dasherize) || options[:dasherize] - key = key.camelize if camelize - dasherize ? key.dasherize : key end class << self @@ -213,12 +98,8 @@ class Hash end elsif value.has_key?("__content__") content = value["__content__"] - if parser = XML_PARSING[value["type"]] - if parser.arity == 2 - XML_PARSING[value["type"]].call(content, value) - else - XML_PARSING[value["type"]].call(content) - end + if parser = ActiveSupport::XmlMini::PARSING[value["type"]] + parser.arity == 1 ? parser.call(content) : parser.call(content, value) else content end @@ -244,11 +125,7 @@ class Hash end when 'Array' value.map! { |i| typecast_xml_value(i) } - case value.length - when 0 then nil - when 1 then value.first - else value - end + value.length > 1 ? value : value.first when 'String' value else diff --git a/activesupport/lib/active_support/inflector/transliterate.rb b/activesupport/lib/active_support/inflector/transliterate.rb index 9c99dcfb01..5ec87372d0 100644 --- a/activesupport/lib/active_support/inflector/transliterate.rb +++ b/activesupport/lib/active_support/inflector/transliterate.rb @@ -3,45 +3,62 @@ require 'active_support/core_ext/string/multibyte' module ActiveSupport module Inflector - extend self - # UTF-8 byte => ASCII approximate UTF-8 byte(s) - ASCII_APPROXIMATIONS = { - 198 => [65, 69], # Æ => AE - 208 => 68, # Ð => D - 216 => 79, # Ø => O - 222 => [84, 104], # Þ => Þ - 223 => [115, 115], # ß => ss - 230 => [97, 101], # æ => ae - 240 => 100, # ð => d - 248 => 111, # ø => o - 254 => [116, 104], # þ => th - 272 => 68, # Đ => D - 273 => 100, # đ => đ - 294 => 72, # Ħ => H - 295 => 104, # ħ => h - 305 => 105, # ı => i - 306 => [73, 74], # IJ =>IJ - 307 => [105, 106], # ij => ij - 312 => 107, # ĸ => k - 319 => 76, # Ŀ => L - 320 => 108, # ŀ => l - 321 => 76, # Ł => L - 322 => 108, # ł => l - 329 => 110, # ʼn => n - 330 => [78, 71], # Ŋ => NG - 331 => [110, 103], # ŋ => ng - 338 => [79, 69], # Œ => OE - 339 => [111, 101], # œ => oe - 358 => 84, # Ŧ => T - 359 => 116 # ŧ => t - } - - # Replaces accented characters with an ASCII approximation, or deletes it if none exsits. - def transliterate(string) - ActiveSupport::Multibyte::Chars.new(string).tidy_bytes.normalize(:d).unpack("U*").map do |char| - ASCII_APPROXIMATIONS[char] || (char if char < 128) - end.compact.flatten.pack("U*") + # Replaces non-ASCII characters with an ASCII approximation, or if none + # exists, a replacement character which defaults to "?". + # + # transliterate("Ærøskøbing") + # # => "AEroskobing" + # + # Default approximations are provided for Western/Latin characters, + # e.g, "ø", "ñ", "é", "ß", etc. + # + # This method is I18n aware, so you can set up custom approximations for a + # locale. This can be useful, for example, to transliterate German's "ü" + # and "ö" to "ue" and "oe", or to add support for transliterating Russian + # to ASCII. + # + # In order to make your custom transliterations available, you must set + # them as the <tt>i18n.transliterate.rule</tt> i18n key: + # + # # Store the transliterations in locales/de.yml + # i18n: + # transliterate: + # ü: "ue" + # ö: "oe" + # + # # Or set them using Ruby + # I18n.backend.store_translations(:de, :i18n => { + # :transliterate => { + # :rule => { + # "ü" => "ue", + # "ö" => "oe" + # } + # } + # }) + # + # The value for <tt>i18n.transliterate.rule</tt> can be a simple Hash that maps + # characters to ASCII approximations as shown above, or, for more complex + # requirements, a Proc: + # + # I18n.backend.store_translations(:de, :i18n => { + # :transliterate => { + # :rule => lambda {|string| MyTransliterator.transliterate(string)} + # } + # }) + # + # Now you can have different transliterations for each locale: + # + # I18n.locale = :en + # transliterate("Jürgen") + # # => "Jurgen" + # + # I18n.locale = :de + # transliterate("Jürgen") + # # => "Juergen" + def transliterate(string, replacement = "?") + I18n.transliterate(Multibyte::Chars.normalize( + Multibyte::Chars.tidy_bytes(string), :c), :replacement => replacement) end # Replaces special characters in a string so that it may be used as part of a 'pretty' URL. @@ -73,5 +90,6 @@ module ActiveSupport end parameterized_string.downcase end + end end diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index 4ade1158fd..cca30d1141 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -75,8 +75,6 @@ module ActiveSupport #:nodoc: UNICODE_TRAILERS_PAT = /(#{codepoints_to_pattern(UNICODE_LEADERS_AND_TRAILERS)})+\Z/u UNICODE_LEADERS_PAT = /\A(#{codepoints_to_pattern(UNICODE_LEADERS_AND_TRAILERS)})+/u - UTF8_PAT = ActiveSupport::Multibyte::VALID_CHARACTER['UTF-8'] - attr_reader :wrapped_string alias to_s wrapped_string alias to_str wrapped_string @@ -409,25 +407,11 @@ module ActiveSupport #:nodoc: # Returns the KC normalization of the string by default. NFKC is considered the best normalization form for # passing strings to databases and validations. # - # * <tt>str</tt> - The string to perform normalization on. # * <tt>form</tt> - The form you want to normalize in. Should be one of the following: # <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>. Default is # ActiveSupport::Multibyte.default_normalization_form def normalize(form=ActiveSupport::Multibyte.default_normalization_form) - # See http://www.unicode.org/reports/tr15, Table 1 - codepoints = self.class.u_unpack(@wrapped_string) - chars(case form - when :d - self.class.reorder_characters(self.class.decompose_codepoints(:canonical, codepoints)) - when :c - self.class.compose_codepoints(self.class.reorder_characters(self.class.decompose_codepoints(:canonical, codepoints))) - when :kd - self.class.reorder_characters(self.class.decompose_codepoints(:compatability, codepoints)) - when :kc - self.class.compose_codepoints(self.class.reorder_characters(self.class.decompose_codepoints(:compatability, codepoints))) - else - raise ArgumentError, "#{form} is not a valid normalization variant", caller - end.pack('U*')) + chars(self.class.normalize(@wrapped_string, form)) end # Performs canonical decomposition on all the characters. @@ -659,7 +643,7 @@ module ActiveSupport #:nodoc: # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent resulting in a valid UTF-8 string. # - # Passing +true+ will forcibly tidy all bytes, assuming that the string's encoding is entirely CP-1252 or ISO-8859-1. + # Passing +true+ will forcibly tidy all bytes, assuming that the string's encoding is entirely CP1252 or ISO-8859-1. def tidy_bytes(string, force = false) if force return string.unpack("C*").map do |b| @@ -708,6 +692,31 @@ module ActiveSupport #:nodoc: end bytes.empty? ? "" : bytes.flatten.compact.pack("C*").unpack("U*").pack("U*") end + + # Returns the KC normalization of the string by default. NFKC is considered the best normalization form for + # passing strings to databases and validations. + # + # * <tt>string</tt> - The string to perform normalization on. + # * <tt>form</tt> - The form you want to normalize in. Should be one of the following: + # <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>. Default is + # ActiveSupport::Multibyte.default_normalization_form + def normalize(string, form=ActiveSupport::Multibyte.default_normalization_form) + # See http://www.unicode.org/reports/tr15, Table 1 + codepoints = u_unpack(string) + case form + when :d + reorder_characters(decompose_codepoints(:canonical, codepoints)) + when :c + compose_codepoints(reorder_characters(decompose_codepoints(:canonical, codepoints))) + when :kd + reorder_characters(decompose_codepoints(:compatability, codepoints)) + when :kc + compose_codepoints(reorder_characters(decompose_codepoints(:compatability, codepoints))) + else + raise ArgumentError, "#{form} is not a valid normalization variant", caller + end.pack('U*') + end + end protected diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb index f22fbcc0e1..7594d7b68b 100644 --- a/activesupport/lib/active_support/xml_mini.rb +++ b/activesupport/lib/active_support/xml_mini.rb @@ -9,6 +9,71 @@ module ActiveSupport module XmlMini extend self + # This module exists to decorate files deserialized using Hash.from_xml with + # the <tt>original_filename</tt> and <tt>content_type</tt> methods. + module FileLike #:nodoc: + attr_writer :original_filename, :content_type + + def original_filename + @original_filename || 'untitled' + end + + def content_type + @content_type || 'application/octet-stream' + end + end + + DEFAULT_ENCODINGS = { + "binary" => "base64" + } unless defined?(TYPE_NAMES) + + TYPE_NAMES = { + "Symbol" => "symbol", + "Fixnum" => "integer", + "Bignum" => "integer", + "BigDecimal" => "decimal", + "Float" => "float", + "TrueClass" => "boolean", + "FalseClass" => "boolean", + "Date" => "date", + "DateTime" => "datetime", + "Time" => "datetime", + "Array" => "array", + "Hash" => "hash" + } unless defined?(TYPE_NAMES) + + FORMATTING = { + "symbol" => Proc.new { |symbol| symbol.to_s }, + "date" => Proc.new { |date| date.to_s(:db) }, + "datetime" => Proc.new { |time| time.xmlschema }, + "binary" => Proc.new { |binary| ActiveSupport::Base64.encode64(binary) }, + "yaml" => Proc.new { |yaml| yaml.to_yaml } + } unless defined?(FORMATTING) + + # TODO: use Time.xmlschema instead of Time.parse; + # use regexp instead of Date.parse + unless defined?(PARSING) + PARSING = { + "symbol" => Proc.new { |symbol| symbol.to_sym }, + "date" => Proc.new { |date| ::Date.parse(date) }, + "datetime" => Proc.new { |time| ::Time.parse(time).utc rescue ::DateTime.parse(time).utc }, + "integer" => Proc.new { |integer| integer.to_i }, + "float" => Proc.new { |float| float.to_f }, + "decimal" => Proc.new { |number| BigDecimal(number) }, + "boolean" => Proc.new { |boolean| %w(1 true).include?(boolean.strip) }, + "string" => Proc.new { |string| string.to_s }, + "yaml" => Proc.new { |yaml| YAML::load(yaml) rescue yaml }, + "base64Binary" => Proc.new { |bin| ActiveSupport::Base64.decode64(bin) }, + "binary" => Proc.new { |bin, entity| _parse_binary(bin, entity) }, + "file" => Proc.new { |file, entity| _parse_file(file, entity) } + } + + PARSING.update( + "double" => PARSING["float"], + "dateTime" => PARSING["datetime"] + ) + end + attr_reader :backend delegate :parse, :to => :backend @@ -16,7 +81,7 @@ module ActiveSupport if name.is_a?(Module) @backend = name else - require "active_support/xml_mini/#{name.to_s.downcase}.rb" + require "active_support/xml_mini/#{name.to_s.downcase}" @backend = ActiveSupport.const_get("XmlMini_#{name}") end end @@ -27,6 +92,66 @@ module ActiveSupport ensure self.backend = old_backend end + + def to_tag(key, value, options) + type_name = options.delete(:type) + merged_options = options.merge(:root => key, :skip_instruct => true) + + if value.is_a?(::Method) || value.is_a?(::Proc) + if value.arity == 1 + value.call(merged_options) + else + value.call(merged_options, key.to_s.singularize) + end + elsif value.respond_to?(:to_xml) + value.to_xml(merged_options) + else + type_name ||= TYPE_NAMES[value.class.name] + type_name ||= value.class.name if value && !value.respond_to?(:to_str) + type_name = type_name.to_s if type_name + + key = rename_key(key.to_s, options) + + attributes = options[:skip_types] || type_name.nil? ? { } : { :type => type_name } + attributes[:nil] = true if value.nil? + + encoding = options[:encoding] || DEFAULT_ENCODINGS[type_name] + attributes[:encoding] = encoding if encoding + + formatted_value = FORMATTING[type_name] && !value.nil? ? + FORMATTING[type_name].call(value) : value + + options[:builder].tag!(key, formatted_value, attributes) + end + end + + def rename_key(key, options = {}) + camelize = options.has_key?(:camelize) && options[:camelize] + dasherize = !options.has_key?(:dasherize) || options[:dasherize] + key = key.camelize if camelize + key = key.dasherize if dasherize + key + end + + protected + + # TODO: Add support for other encodings + def _parse_binary(bin, entity) #:nodoc: + case entity['encoding'] + when 'base64' + ActiveSupport::Base64.decode64(bin) + else + bin + end + end + + def _parse_file(file, entity) + f = StringIO.new(ActiveSupport::Base64.decode64(file)) + f.extend(FileLike) + f.original_filename = entity['name'] + f.content_type = entity['content_type'] + f + end end XmlMini.backend = 'REXML' diff --git a/activesupport/test/core_ext/array_ext_test.rb b/activesupport/test/core_ext/array_ext_test.rb index aecc644549..e7617466c2 100644 --- a/activesupport/test/core_ext/array_ext_test.rb +++ b/activesupport/test/core_ext/array_ext_test.rb @@ -211,7 +211,7 @@ class ArrayToXmlTests < Test::Unit::TestCase { :name => "Jason", :age => 31, :age_in_millis => BigDecimal.new('1.0') } ].to_xml(:skip_instruct => true, :indent => 0) - assert_equal '<records type="array"><record>', xml.first(30) + assert_equal '<objects type="array"><object>', xml.first(30) assert xml.include?(%(<age type="integer">26</age>)), xml assert xml.include?(%(<age-in-millis type="integer">820497600000</age-in-millis>)), xml assert xml.include?(%(<name>David</name>)), xml @@ -233,7 +233,7 @@ class ArrayToXmlTests < Test::Unit::TestCase { :name => "David", :street_address => "Paulina" }, { :name => "Jason", :street_address => "Evergreen" } ].to_xml(:skip_instruct => true, :skip_types => true, :indent => 0) - assert_equal "<records><record>", xml.first(17) + assert_equal "<objects><object>", xml.first(17) assert xml.include?(%(<street-address>Paulina</street-address>)) assert xml.include?(%(<name>David</name>)) assert xml.include?(%(<street-address>Evergreen</street-address>)) @@ -245,7 +245,7 @@ class ArrayToXmlTests < Test::Unit::TestCase { :name => "David", :street_address => "Paulina" }, { :name => "Jason", :street_address => "Evergreen" } ].to_xml(:skip_instruct => true, :skip_types => true, :indent => 0, :dasherize => false) - assert_equal "<records><record>", xml.first(17) + assert_equal "<objects><object>", xml.first(17) assert xml.include?(%(<street_address>Paulina</street_address>)) assert xml.include?(%(<street_address>Evergreen</street_address>)) end @@ -255,7 +255,7 @@ class ArrayToXmlTests < Test::Unit::TestCase { :name => "David", :street_address => "Paulina" }, { :name => "Jason", :street_address => "Evergreen" } ].to_xml(:skip_instruct => true, :skip_types => true, :indent => 0, :dasherize => true) - assert_equal "<records><record>", xml.first(17) + assert_equal "<objects><object>", xml.first(17) assert xml.include?(%(<street-address>Paulina</street-address>)) assert xml.include?(%(<street-address>Evergreen</street-address>)) end @@ -319,7 +319,7 @@ class ArrayExtractOptionsTests < Test::Unit::TestCase assert_equal({}, options) assert_equal [hash], array end - + def test_extract_options_extracts_extractable_subclass hash = ExtractableHashSubclass.new hash[:foo] = 1 diff --git a/activesupport/test/transliterate_test.rb b/activesupport/test/transliterate_test.rb index d689b6be73..b054855d08 100644 --- a/activesupport/test/transliterate_test.rb +++ b/activesupport/test/transliterate_test.rb @@ -4,36 +4,6 @@ require 'active_support/inflector/transliterate' class TransliterateTest < Test::Unit::TestCase - APPROXIMATIONS = { - "À"=>"A", "Á"=>"A", "Â"=>"A", "Ã"=>"A", "Ä"=>"A", "Å"=>"A", "Æ"=>"AE", - "Ç"=>"C", "È"=>"E", "É"=>"E", "Ê"=>"E", "Ë"=>"E", "Ì"=>"I", "Í"=>"I", - "Î"=>"I", "Ï"=>"I", "Ð"=>"D", "Ñ"=>"N", "Ò"=>"O", "Ó"=>"O", "Ô"=>"O", - "Õ"=>"O", "Ö"=>"O", "Ø"=>"O", "Ù"=>"U", "Ú"=>"U", "Û"=>"U", "Ü"=>"U", - "Ý"=>"Y", "Þ"=>"Th", "ß"=>"ss", "à"=>"a", "á"=>"a", "â"=>"a", "ã"=>"a", - "ä"=>"a", "å"=>"a", "æ"=>"ae", "ç"=>"c", "è"=>"e", "é"=>"e", "ê"=>"e", - "ë"=>"e", "ì"=>"i", "í"=>"i", "î"=>"i", "ï"=>"i", "ð"=>"d", "ñ"=>"n", - "ò"=>"o", "ó"=>"o", "ô"=>"o", "õ"=>"o", "ö"=>"o", "ø"=>"o", "ù"=>"u", - "ú"=>"u", "û"=>"u", "ü"=>"u", "ý"=>"y", "þ"=>"th", "ÿ"=>"y", "Ā"=>"A", - "ā"=>"a", "Ă"=>"A", "ă"=>"a", "Ą"=>"A", "ą"=>"a", "Ć"=>"C", "ć"=>"c", - "Ĉ"=>"C", "ĉ"=>"c", "Ċ"=>"C", "ċ"=>"c", "Č"=>"C", "č"=>"c", "Ď"=>"D", - "ď"=>"d", "Đ"=>"D", "đ"=>"d", "Ē"=>"E", "ē"=>"e", "Ĕ"=>"E", "ĕ"=>"e", - "Ė"=>"E", "ė"=>"e", "Ę"=>"E", "ę"=>"e", "Ě"=>"E", "ě"=>"e", "Ĝ"=>"G", - "ĝ"=>"g", "Ğ"=>"G", "ğ"=>"g", "Ġ"=>"G", "ġ"=>"g", "Ģ"=>"G", "ģ"=>"g", - "Ĥ"=>"H", "ĥ"=>"h", "Ħ"=>"H", "ħ"=>"h", "Ĩ"=>"I", "ĩ"=>"i", "Ī"=>"I", - "ī"=>"i", "Ĭ"=>"I", "ĭ"=>"i", "Į"=>"I", "į"=>"i", "İ"=>"I", "ı"=>"i", - "IJ"=>"IJ", "ij"=>"ij", "Ĵ"=>"J", "ĵ"=>"j", "Ķ"=>"K", "ķ"=>"k", "ĸ"=>"k", - "Ĺ"=>"L", "ĺ"=>"l", "Ļ"=>"L", "ļ"=>"l", "Ľ"=>"L", "ľ"=>"l", "Ŀ"=>"L", - "ŀ"=>"l", "Ł"=>"L", "ł"=>"l", "Ń"=>"N", "ń"=>"n", "Ņ"=>"N", "ņ"=>"n", - "Ň"=>"N", "ň"=>"n", "ʼn"=>"n", "Ŋ"=>"NG", "ŋ"=>"ng", "Ō"=>"O", "ō"=>"o", - "Ŏ"=>"O", "ŏ"=>"o", "Ő"=>"O", "ő"=>"o", "Œ"=>"OE", "œ"=>"oe", "Ŕ"=>"R", - "ŕ"=>"r", "Ŗ"=>"R", "ŗ"=>"r", "Ř"=>"R", "ř"=>"r", "Ś"=>"S", "ś"=>"s", - "Ŝ"=>"S", "ŝ"=>"s", "Ş"=>"S", "ş"=>"s", "Š"=>"S", "š"=>"s", "Ţ"=>"T", - "ţ"=>"t", "Ť"=>"T", "ť"=>"t", "Ŧ"=>"T", "ŧ"=>"t", "Ũ"=>"U", "ũ"=>"u", - "Ū"=>"U", "ū"=>"u", "Ŭ"=>"U", "ŭ"=>"u", "Ů"=>"U", "ů"=>"u", "Ű"=>"U", - "ű"=>"u", "Ų"=>"U", "ų"=>"u", "Ŵ"=>"W", "ŵ"=>"w", "Ŷ"=>"Y", "ŷ"=>"y", - "Ÿ"=>"Y", "Ź"=>"Z", "ź"=>"z", "Ż"=>"Z", "ż"=>"z", "Ž"=>"Z", "ž"=>"z" - } - def test_transliterate_should_not_change_ascii_chars (0..127).each do |byte| char = [byte].pack("U") @@ -41,10 +11,25 @@ class TransliterateTest < Test::Unit::TestCase end end - def test_should_convert_accented_chars_to_approximate_ascii_chars - APPROXIMATIONS.each do |given, expected| - assert_equal expected, ActiveSupport::Inflector.transliterate(given) + def test_transliterate_should_approximate_ascii + # create string with range of Unicode"s western characters with + # diacritics, excluding the division and multiplication signs which for + # some reason or other are floating in the middle of all the letters. + string = (0xC0..0x17E).to_a.reject {|c| [0xD7, 0xF7].include? c}.pack("U*") + string.each_char do |char| + assert_match %r{^[a-zA-Z']*$}, ActiveSupport::Inflector.transliterate(string) end end + def test_transliterate_should_work_with_custom_i18n_rules_and_uncomposed_utf8 + char = [117, 776].pack("U*") # "ü" as ASCII "u" plus COMBINING DIAERESIS + I18n.backend.store_translations(:de, :i18n => {:transliterate => {:rule => {"ü" => "ue"}}}) + I18n.locale = :de + assert_equal "ue", ActiveSupport::Inflector.transliterate(char) + end + + def test_transliterate_should_allow_a_custom_replacement_char + assert_equal "a*b", ActiveSupport::Inflector.transliterate("a索b", "*") + end + end diff --git a/railties/guides/source/generators.textile b/railties/guides/source/generators.textile index 4387fe3bd5..d3757e9733 100644 --- a/railties/guides/source/generators.textile +++ b/railties/guides/source/generators.textile @@ -88,9 +88,7 @@ And it will create a new generator as follow: <ruby> class InitializerGenerator < Rails::Generators::NamedBase - def self.source_root - @source_root ||= File.expand_path(File.join(File.dirname(__FILE__), 'templates')) - end + source_root File.expand_path("../templates", __FILE__) end </ruby> @@ -115,9 +113,7 @@ And now let's change the generator to copy this template when invoked: <ruby> class InitializerGenerator < Rails::Generators::NamedBase - def self.source_root - @source_root ||= File.expand_path(File.join(File.dirname(__FILE__), 'templates')) - end + source_root File.expand_path("../templates", __FILE__) def copy_initializer_file copy_file "initializer.rb", "config/initializers/#{file_name}.rb" @@ -135,21 +131,18 @@ We can see that now a initializer named foo was created at +config/initializers/ h3. Generators lookup -Now that we know how to create generators, we must know where Rails looks for generators before invoking them. When we invoke the initializer generator, Rails looks at the following paths in the given order: +With our first generator created, we must discuss briefly generators lookup. The way Rails finds generators is exactly the same way Ruby find files, i.e. using +$LOAD_PATHS+. + +For instance, when you say +rails g initializer foo+, rails knows you want to invoke the initializer generator and then search for the following generators in the $LOAD_PATHS: <shell> -RAILS_APP/lib/generators -RAILS_APP/lib/rails_generators -RAILS_APP/vendor/plugins/*/lib/generators -RAILS_APP/vendor/plugins/*/lib/rails_generators -GEMS_PATH/*/lib/generators -GEMS_PATH/*/lib/rails_generators -~/rails/generators -~/rails/rails_generators -RAILS_GEM/lib/rails/generators +rails/generators/initializer/initializer_generator.rb +generators/initializer/initializer_generator.rb +rails/generators/initializer_generator.rb +generators/initializer_generator.rb </shell> -First Rails looks for generators in your application, then in plugins and/or gems, then in your home and finally the builtin generators. One very important thing to keep in mind is that in Rails 3.0 and after it only looks for generators in gems being used in your application. So if you have rspec installed as a gem, but it's not declared in your application, Rails won't be able to invoke it. +If none of them is found, it raises an error message. h3. Customizing your workflow @@ -183,7 +176,6 @@ $ rails generate scaffold User name:string create app/views/users/show.html.erb create app/views/users/new.html.erb create app/views/users/_form.html.erb - create app/views/layouts/users.html.erb invoke test_unit create test/functional/users_controller_test.rb invoke helper @@ -284,7 +276,7 @@ end end </ruby> -Now, when the helper generator is invoked and let's say test unit is configured as test framework, it will try to invoke both +MyHelper::Generators::TestUnitGenerator+ and +TestUnit::Generators::MyHelperGenerator+. Since none of those are defined, we can tell our generator to invoke +TestUnit::Generators::HelperGenerator+ instead, which is defined since it's a Rails hook. To do that, we just need to add: +Now, when the helper generator is invoked and let's say test unit is configured as test framework, it will try to invoke both +MyHelper::Generators::TestUnitGenerator+ and +TestUnit::Generators::MyHelperGenerator+. Since none of those are defined, we can tell our generator to invoke +TestUnit::Generators::HelperGenerator+ instead, which is defined since it's a Rails generator. To do that, we just need to add: <ruby> # Search for :helper instead of :my_helper @@ -375,4 +367,6 @@ h3. Changelog "Lighthouse Ticket":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/102 -* November 20, 2009: First release version by José Valim +* April 30, 2010: Reviewed by José Valim + +* November 20, 2009: First version by José Valim diff --git a/railties/guides/source/index.html.erb b/railties/guides/source/index.html.erb index f47975917c..5a715cf9f7 100644 --- a/railties/guides/source/index.html.erb +++ b/railties/guides/source/index.html.erb @@ -143,7 +143,7 @@ Ruby on Rails Guides <%= guide("Adding Generators", 'generators.html') do %> <p>This guide covers the process of adding a brand new generator to your extension or providing an alternative to an element of a built-in Rails generator (such as - providing alternative test stubs for the scaffold generator)</p> + providing alternative test stubs for the scaffold generator).</p> <% end %> </dl> diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 7cec14c738..d39f9a2ae9 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -85,7 +85,7 @@ module Rails delegate :metal_loader, :to => :config def require_environment! - environment = config.paths.config.environment.to_a.first + environment = paths.config.environment.to_a.first require environment if environment end @@ -153,7 +153,7 @@ module Rails require "rails/tasks" task :environment do $rails_rake_task = true - initialize! + require_environment! end end diff --git a/railties/lib/rails/application/bootstrap.rb b/railties/lib/rails/application/bootstrap.rb index 06243f2e5e..022e1a91d8 100644 --- a/railties/lib/rails/application/bootstrap.rb +++ b/railties/lib/rails/application/bootstrap.rb @@ -6,7 +6,8 @@ module Rails include Initializable initializer :load_environment_config do - require_environment! + environment = config.paths.config.environments.to_a.first + require environment if environment end initializer :load_all_active_support do diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 874b3a78b6..1ad77fdfec 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -1,3 +1,4 @@ +require 'active_support/deprecation' require 'rails/engine/configuration' module Rails @@ -43,14 +44,15 @@ module Rails @paths ||= begin paths = super paths.app.controllers << builtin_controller if builtin_controller - paths.config.database "config/database.yml" - paths.config.environment "config/environments", :glob => "#{Rails.env}.rb" - paths.lib.templates "lib/templates" - paths.log "log/#{Rails.env}.log" - paths.tmp "tmp" - paths.tmp.cache "tmp/cache" - paths.vendor "vendor", :load_path => true - paths.vendor.plugins "vendor/plugins" + paths.config.database "config/database.yml" + paths.config.environment "config/environment.rb" + paths.config.environments "config/environments", :glob => "#{Rails.env}.rb" + paths.lib.templates "lib/templates" + paths.log "log/#{Rails.env}.log" + paths.tmp "tmp" + paths.tmp.cache "tmp/cache" + paths.vendor "vendor", :load_path => true + paths.vendor.plugins "vendor/plugins" if File.exists?("#{root}/test/mocks/#{Rails.env}") ActiveSupport::Deprecation.warn "\"Rails.root/test/mocks/#{Rails.env}\" won't be added " << @@ -142,7 +144,7 @@ module Rails middleware.use('::Rack::Runtime') middleware.use('::Rails::Rack::Logger') middleware.use('::ActionDispatch::ShowExceptions', lambda { consider_all_requests_local }, :if => lambda { action_dispatch.show_exceptions }) - middleware.use("::ActionDispatch::RemoteIp", lambda { action_dispatch.ip_spoofing_check }, lambda { action_dispatch.trusted_proxies }) + middleware.use('::ActionDispatch::RemoteIp', lambda { action_dispatch.ip_spoofing_check }, lambda { action_dispatch.trusted_proxies }) middleware.use('::Rack::Sendfile', lambda { action_dispatch.x_sendfile_header }) middleware.use('::ActionDispatch::Callbacks', lambda { !cache_classes }) middleware.use('::ActionDispatch::Cookies') diff --git a/railties/lib/rails/commands.rb b/railties/lib/rails/commands.rb index 12748da18b..de93a87615 100644 --- a/railties/lib/rails/commands.rb +++ b/railties/lib/rails/commands.rb @@ -1,8 +1,50 @@ -if ARGV.empty? - ARGV << '--help' -end +ARGV << '--help' if ARGV.empty? -HELP_TEXT = <<-EOT +aliases = { + "g" => "generate", + "c" => "console", + "s" => "server", + "db" => "dbconsole" +} + +command = ARGV.shift +command = aliases[command] || command + +case command +when 'generate', 'destroy', 'plugin', 'benchmarker', 'profiler' + require APP_PATH + Rails::Application.require_environment! + require "rails/commands/#{command}" + +when 'console' + require 'rails/commands/console' + require APP_PATH + Rails::Application.require_environment! + Rails::Console.start(Rails::Application) + +when 'server' + require 'rails/commands/server' + Rails::Server.new.tap { |server| + require APP_PATH + Dir.chdir(Rails::Application.root) + server.start + } + +when 'dbconsole' + require 'rails/commands/dbconsole' + require APP_PATH + Rails::DBConsole.start(Rails::Application) + +when 'application', 'runner' + require "rails/commands/#{command}" + +when '--version', '-v' + ARGV.unshift '--version' + require 'rails/commands/application' + +else + puts "Error: Command not recognized" unless %w(-h --help).include?(command) + puts <<-EOT Usage: rails COMMAND [ARGS] The most common rails commands are: @@ -21,53 +63,5 @@ In addition to those, there are: runner Run a piece of code in the application environment All commands can be run with -h for more information. -EOT - - -case ARGV.shift -when 'g', 'generate' - require ENV_PATH - require 'rails/commands/generate' -when 'c', 'console' - require 'rails/commands/console' - require ENV_PATH - Rails::Console.start(Rails::Application) -when 's', 'server' - require 'rails/commands/server' - # Initialize the server first, so environment options are set - server = Rails::Server.new - require APP_PATH - - Dir.chdir(Rails::Application.root) - server.start -when 'db', 'dbconsole' - require 'rails/commands/dbconsole' - require APP_PATH - Rails::DBConsole.start(Rails::Application) - -when 'application' - require 'rails/commands/application' -when 'destroy' - require ENV_PATH - require 'rails/commands/destroy' -when 'benchmarker' - require ENV_PATH - require 'rails/commands/performance/benchmarker' -when 'profiler' - require ENV_PATH - require 'rails/commands/performance/profiler' -when 'plugin' - require APP_PATH - require 'rails/commands/plugin' -when 'runner' - require 'rails/commands/runner' - -when '--help', '-h' - puts HELP_TEXT -when '--version', '-v' - ARGV.unshift '--version' - require 'rails/commands/application' -else - puts "Error: Command not recognized" - puts HELP_TEXT -end + EOT +end
\ No newline at end of file diff --git a/railties/lib/rails/commands/application.rb b/railties/lib/rails/commands/application.rb index 438c976b35..8a8143e00e 100644 --- a/railties/lib/rails/commands/application.rb +++ b/railties/lib/rails/commands/application.rb @@ -10,4 +10,4 @@ require 'rubygems' if ARGV.include?("--dev") require 'rails/generators' require 'rails/generators/rails/app/app_generator' -Rails::Generators::AppGenerator.start
\ No newline at end of file +Rails::Generators::AppGenerator.start diff --git a/railties/lib/rails/commands/performance/benchmarker.rb b/railties/lib/rails/commands/benchmarker.rb index 0432261802..0432261802 100644 --- a/railties/lib/rails/commands/performance/benchmarker.rb +++ b/railties/lib/rails/commands/benchmarker.rb diff --git a/railties/lib/rails/commands/performance/profiler.rb b/railties/lib/rails/commands/profiler.rb index 6d9717b5cd..6d9717b5cd 100644 --- a/railties/lib/rails/commands/performance/profiler.rb +++ b/railties/lib/rails/commands/profiler.rb diff --git a/railties/lib/rails/commands/runner.rb b/railties/lib/rails/commands/runner.rb index 1dd11e1241..278548558e 100644 --- a/railties/lib/rails/commands/runner.rb +++ b/railties/lib/rails/commands/runner.rb @@ -36,7 +36,8 @@ ARGV.delete(code_or_file) ENV["RAILS_ENV"] = options[:environment] -require ENV_PATH +require APP_PATH +Rails::Application.require_environment! begin if code_or_file.nil? diff --git a/railties/lib/rails/configuration.rb b/railties/lib/rails/configuration.rb index dfd849b4bb..bd404f4a14 100644 --- a/railties/lib/rails/configuration.rb +++ b/railties/lib/rails/configuration.rb @@ -1,3 +1,4 @@ +require 'active_support/deprecation' require 'active_support/ordered_options' require 'rails/paths' require 'rails/rack' diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index 36fcc896ae..ab0ead65a9 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -42,7 +42,7 @@ module Rails # config.load_paths << File.expand_path("../lib/some/path", __FILE__) # # initializer "my_engine.add_middleware" do |app| - # app.middlewares.use MyEngine::Middleware + # app.middleware.use MyEngine::Middleware # end # end # diff --git a/railties/lib/rails/generators/base.rb b/railties/lib/rails/generators/base.rb index 0da85ea4a4..766644bbc2 100644 --- a/railties/lib/rails/generators/base.rb +++ b/railties/lib/rails/generators/base.rb @@ -20,24 +20,19 @@ module Rails add_runtime_options! - # Automatically sets the source root based on the class name. - # - def self.source_root - @_rails_source_root ||= begin - if base_name && generator_name - File.expand_path(File.join(base_name, generator_name, 'templates'), File.dirname(__FILE__)) - end - end + # Returns the source root for this generator using default_source_root as default. + def self.source_root(path=nil) + @_source_root = path if path + @_source_root ||= default_source_root end # Tries to get the description from a USAGE file one folder above the source # root otherwise uses a default description. - # def self.desc(description=nil) return super if description - usage = File.expand_path(File.join(source_root, "..", "USAGE")) + usage = source_root && File.expand_path("../USAGE", source_root) - @desc ||= if File.exist?(usage) + @desc ||= if usage && File.exist?(usage) File.read(usage) else "Description:\n Create #{base_name.humanize.downcase} files for #{generator_name} generator." @@ -47,7 +42,6 @@ module Rails # Convenience method to get the namespace from the class name. It's the # same as Thor default except that the Generator at the end of the class # is removed. - # def self.namespace(name=nil) return super if name @namespace ||= super.sub(/_generator$/, '').sub(/:generators:/, ':') @@ -200,7 +194,6 @@ module Rails end # Make class option aware of Rails::Generators.options and Rails::Generators.aliases. - # def self.class_option(name, options={}) #:nodoc: options[:desc] = "Indicates when to generate #{name.to_s.humanize.downcase}" unless options.key?(:desc) options[:aliases] = default_aliases_for_option(name, options) @@ -208,14 +201,27 @@ module Rails super(name, options) end + # Returns the default source root for a given generator. This is used internally + # by rails to set its generators source root. If you want to customize your source + # root, you should use source_root. + def self.default_source_root + return unless base_name && generator_name + path = File.expand_path(File.join(base_name, generator_name, 'templates'), base_root) + path if File.exists?(path) + end + + # Returns the base root for a common set of generators. This is used to dynamically + # guess the default source root. + def self.base_root + File.dirname(__FILE__) + end + # Cache source root and add lib/generators/base/generator/templates to # source paths. - # def self.inherited(base) #:nodoc: super - # Cache source root, we need to do this, since __FILE__ is a relative value - # and can point to wrong directions when inside an specified directory. + # Invoke source_root so the default_source_root is set. base.source_root if base.name && base.name !~ /Base$/ diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index aa066fe3c4..10d8b8f85a 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -2,87 +2,61 @@ require 'digest/md5' require 'active_support/secure_random' require 'rails/version' unless defined?(Rails::VERSION) require 'rbconfig' +require 'open-uri' +require 'uri' -module Rails::Generators - # We need to store the RAILS_DEV_PATH in a constant, otherwise the path - # can change in Ruby 1.8.7 when we FileUtils.cd. - RAILS_DEV_PATH = File.expand_path("../../../../../..", File.dirname(__FILE__)) +module Rails + module ActionMethods + attr_reader :options - RESERVED_NAMES = %w[generate console server dbconsole - application destroy benchmarker profiler - plugin runner test] - - class AppGenerator < Base - DATABASES = %w( mysql oracle postgresql sqlite3 frontbase ibm_db ) - - attr_accessor :rails_template - add_shebang_option! - - argument :app_path, :type => :string - - class_option :database, :type => :string, :aliases => "-d", :default => "sqlite3", - :desc => "Preconfigure for selected database (options: #{DATABASES.join('/')})" - - class_option :template, :type => :string, :aliases => "-m", - :desc => "Path to an application template (can be a filesystem path or URL)." - - class_option :dev, :type => :boolean, :default => false, - :desc => "Setup the application with Gemfile pointing to your Rails checkout" - - class_option :edge, :type => :boolean, :default => false, - :desc => "Setup the application with Gemfile pointing to Rails repository" - - class_option :skip_gemfile, :type => :boolean, :default => false, - :desc => "Don't create a Gemfile" - - class_option :skip_activerecord, :type => :boolean, :aliases => "-O", :default => false, - :desc => "Skip ActiveRecord files" - - class_option :skip_testunit, :type => :boolean, :aliases => "-T", :default => false, - :desc => "Skip TestUnit files" - - class_option :skip_prototype, :type => :boolean, :aliases => "-J", :default => false, - :desc => "Skip Prototype files" - - class_option :skip_git, :type => :boolean, :aliases => "-G", :default => false, - :desc => "Skip Git ignores and keeps" - - # Add bin/rails options - class_option :version, :type => :boolean, :aliases => "-v", :group => :rails, - :desc => "Show Rails version number and quit" + def initialize(generator) + @generator = generator + @options = generator.options + end - class_option :help, :type => :boolean, :aliases => "-h", :group => :rails, - :desc => "Show this help message and quit" + private + %w(template copy_file directory empty_directory inside + empty_directory_with_gitkeep create_file chmod shebang).each do |method| + class_eval <<-RUBY + def #{method}(*args, &block) + @generator.send(:#{method}, *args, &block) + end + RUBY + end - def initialize(*args) - super - if !options[:skip_activerecord] && !DATABASES.include?(options[:database]) - raise Error, "Invalid value for --database option. Supported for preconfiguration are: #{DATABASES.join(", ")}." + # TODO: Remove once this is fully in place + def method_missing(meth, *args, &block) + STDERR.puts "Calling #{meth} with #{args.inspect} with #{block}" + @generator.send(meth, *args, &block) end + end + + class AppBuilder + def rakefile + template "Rakefile" end - def create_root - self.destination_root = File.expand_path(app_path, destination_root) - valid_app_const? + def readme + copy_file "README" + end - empty_directory '.' - set_default_accessors! - FileUtils.cd(destination_root) + def gemfile + template "Gemfile" end - def create_root_files - copy_file "README" - copy_file "gitignore", ".gitignore" unless options[:skip_git] - template "Rakefile" + def configru template "config.ru" - template "Gemfile" unless options[:skip_gemfile] end - def create_app_files + def gitignore + copy_file "gitignore", ".gitignore" + end + + def app directory 'app' end - def create_config_files + def config empty_directory "config" inside "config" do @@ -96,29 +70,24 @@ module Rails::Generators end end - def create_boot_file - template "config/boot.rb" + def database_yml + template "config/databases/#{@options[:database]}.yml", "config/database.yml" end - def create_activerecord_files - return if options[:skip_activerecord] - template "config/databases/#{options[:database]}.yml", "config/database.yml" - end - - def create_db_files + def db directory "db" end - def create_doc_files + def doc directory "doc" end - def create_lib_files + def lib empty_directory "lib" empty_directory_with_gitkeep "lib/tasks" end - def create_log_files + def log empty_directory "log" inside "log" do @@ -129,19 +98,19 @@ module Rails::Generators end end - def create_public_files - directory "public", "public", :recursive => false # Do small steps, so anyone can overwrite it. + def public_directory + directory "public", "public", :recursive => false end - def create_public_image_files + def images directory "public/images" end - def create_public_stylesheets_files + def stylesheets empty_directory_with_gitkeep "public/stylesheets" end - def create_prototype_files + def javascripts unless options[:skip_prototype] directory "public/javascripts" else @@ -149,19 +118,18 @@ module Rails::Generators end end - def create_script_files + def script directory "script" do |content| "#{shebang}\n" + content end chmod "script", 0755, :verbose => false end - def create_test_files - return if options[:skip_testunit] + def test directory "test" end - def create_tmp_files + def tmp empty_directory "tmp" inside "tmp" do @@ -171,20 +139,170 @@ module Rails::Generators end end - def create_vendor_files + def vendor_plugins empty_directory_with_gitkeep "vendor/plugins" end + end - def apply_rails_template - apply rails_template if rails_template - rescue Thor::Error, LoadError, Errno::ENOENT => e - raise Error, "The template [#{rails_template}] could not be loaded. Error: #{e}" - end + module Generators + # We need to store the RAILS_DEV_PATH in a constant, otherwise the path + # can change in Ruby 1.8.7 when we FileUtils.cd. + RAILS_DEV_PATH = File.expand_path("../../../../../..", File.dirname(__FILE__)) - def bundle_if_dev_or_edge - bundle_command = File.basename(Thor::Util.ruby_command).sub(/ruby/, 'bundle') - run "#{bundle_command} install" if dev_or_edge? - end + RESERVED_NAMES = %w[generate console server dbconsole + application destroy benchmarker profiler + plugin runner test] + + class AppGenerator < Base + DATABASES = %w( mysql oracle postgresql sqlite3 frontbase ibm_db ) + + attr_accessor :rails_template + add_shebang_option! + + argument :app_path, :type => :string + + class_option :database, :type => :string, :aliases => "-d", :default => "sqlite3", + :desc => "Preconfigure for selected database (options: #{DATABASES.join('/')})" + + class_option :builder, :type => :string, :aliases => "-b", + :desc => "Path to an application builder (can be a filesystem path or URL)" + + class_option :template, :type => :string, :aliases => "-m", + :desc => "Path to an application template (can be a filesystem path or URL)." + + class_option :dev, :type => :boolean, :default => false, + :desc => "Setup the application with Gemfile pointing to your Rails checkout" + + class_option :edge, :type => :boolean, :default => false, + :desc => "Setup the application with Gemfile pointing to Rails repository" + + class_option :skip_gemfile, :type => :boolean, :default => false, + :desc => "Don't create a Gemfile" + + class_option :skip_activerecord, :type => :boolean, :aliases => "-O", :default => false, + :desc => "Skip ActiveRecord files" + + class_option :skip_testunit, :type => :boolean, :aliases => "-T", :default => false, + :desc => "Skip TestUnit files" + + class_option :skip_prototype, :type => :boolean, :aliases => "-J", :default => false, + :desc => "Skip Prototype files" + + class_option :skip_git, :type => :boolean, :aliases => "-G", :default => false, + :desc => "Skip Git ignores and keeps" + + # Add bin/rails options + class_option :version, :type => :boolean, :aliases => "-v", :group => :rails, + :desc => "Show Rails version number and quit" + + class_option :help, :type => :boolean, :aliases => "-h", :group => :rails, + :desc => "Show this help message and quit" + + def initialize(*args) + raise Error, "Options should be given after the application name. For details run: rails --help" if args[0].blank? + super + + if !options[:skip_activerecord] && !DATABASES.include?(options[:database]) + raise Error, "Invalid value for --database option. Supported for preconfiguration are: #{DATABASES.join(", ")}." + end + end + + def create_root + self.destination_root = File.expand_path(app_path, destination_root) + valid_app_const? + + empty_directory '.' + set_default_accessors! + FileUtils.cd(destination_root) + end + + def create_root_files + build(:readme) + build(:rakefile) + build(:configru) + build(:gitignore) unless options[:skip_git] + build(:gemfile) unless options[:skip_gemfile] + end + + def create_app_files + build(:app) + end + + def create_config_files + build(:config) + end + + def create_boot_file + template "config/boot.rb" + end + + def create_activerecord_files + return if options[:skip_activerecord] + build(:database_yml) + end + + def create_db_files + build(:db) + end + + def create_doc_files + build(:doc) + end + + def create_lib_files + build(:lib) + end + + def create_log_files + build(:log) + end + + def create_public_files + build(:public_directory) + end + + def create_public_image_files + build(:images) + end + + def create_public_stylesheets_files + build(:stylesheets) + end + + def create_prototype_files + build(:javascripts) + end + + def create_script_files + build(:script) + end + + def create_test_files + build(:test) unless options[:skip_testunit] + end + + def create_tmp_files + build(:tmp) + end + + def create_vendor_files + build(:vendor_plugins) + end + + def finish_template + build(:leftovers) + end + + def apply_rails_template + apply rails_template if rails_template + rescue Thor::Error, LoadError, Errno::ENOENT => e + raise Error, "The template [#{rails_template}] could not be loaded. Error: #{e}" + end + + def bundle_if_dev_or_edge + bundle_command = File.basename(Thor::Util.ruby_command).sub(/ruby/, 'bundle') + run "#{bundle_command} install" if dev_or_edge? + end protected @@ -192,6 +310,29 @@ module Rails::Generators "rails #{self.arguments.map(&:usage).join(' ')} [options]" end + def builder + @builder ||= begin + if path = options[:builder] + if URI(path).is_a?(URI::HTTP) + contents = open(path, "Accept" => "application/x-thor-template") {|io| io.read } + else + contents = open(path) {|io| io.read } + end + + prok = eval("proc { #{contents} }", TOPLEVEL_BINDING, path, 1) + instance_eval(&prok) + end + + builder_class = defined?(::AppBuilder) ? ::AppBuilder : Rails::AppBuilder + builder_class.send(:include, ActionMethods) + builder_class.new(self) + end + end + + def build(meth, *args) + builder.send(meth, *args) if builder.respond_to?(meth) + end + def set_default_accessors! self.rails_template = case options[:template] when /^http:\/\// @@ -273,5 +414,6 @@ module Rails::Generators empty_directory(destination, config) create_file("#{destination}/.gitkeep") unless options[:skip_git] end + end end end diff --git a/railties/lib/rails/generators/rails/app/templates/script/rails b/railties/lib/rails/generators/rails/app/templates/script/rails index b01d1ee183..11bc1edde9 100644 --- a/railties/lib/rails/generators/rails/app/templates/script/rails +++ b/railties/lib/rails/generators/rails/app/templates/script/rails @@ -1,8 +1,5 @@ # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. -ENV_PATH = File.expand_path('../../config/environment', __FILE__) -BOOT_PATH = File.expand_path('../../config/boot', __FILE__) -APP_PATH = File.expand_path('../../config/application', __FILE__) - -require BOOT_PATH +APP_PATH = File.expand_path('../../config/application', __FILE__) +require File.expand_path('../../config/boot', __FILE__) require 'rails/commands' diff --git a/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt b/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt index d8757460e4..d0575772bc 100644 --- a/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt +++ b/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt @@ -1,5 +1,3 @@ class <%= class_name %>Generator < Rails::Generators::NamedBase - def self.source_root - @source_root ||= File.expand_path('../templates', __FILE__) - end + source_root File.expand_path('../templates', __FILE__) end diff --git a/railties/lib/rails/railtie.rb b/railties/lib/rails/railtie.rb index 6ac6be092e..b6b57bc5b5 100644 --- a/railties/lib/rails/railtie.rb +++ b/railties/lib/rails/railtie.rb @@ -1,6 +1,7 @@ require 'rails/initializable' require 'rails/configuration' require 'active_support/inflector' +require 'active_support/deprecation' module Rails # Railtie is the core of the Rails Framework and provides several hooks to extend diff --git a/railties/railties.gemspec b/railties/railties.gemspec index b9278c0399..99537d6d24 100644 --- a/railties/railties.gemspec +++ b/railties/railties.gemspec @@ -20,7 +20,7 @@ Gem::Specification.new do |s| s.has_rdoc = false s.add_dependency('rake', '>= 0.8.3') - s.add_dependency('thor', '~> 0.13.4') + s.add_dependency('thor', '~> 0.13.6') s.add_dependency('activesupport', version) s.add_dependency('actionpack', version) end diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb index 27374dcb28..d08f04bddb 100644 --- a/railties/test/application/middleware_test.rb +++ b/railties/test/application/middleware_test.rb @@ -20,20 +20,21 @@ module ApplicationTests assert_equal [ "ActionDispatch::Static", "Rack::Lock", + "ActiveSupport::Cache::Strategy::LocalCache", "Rack::Runtime", "Rails::Rack::Logger", "ActionDispatch::ShowExceptions", "ActionDispatch::RemoteIp", "Rack::Sendfile", "ActionDispatch::Callbacks", + "ActiveRecord::ConnectionAdapters::ConnectionManagement", + "ActiveRecord::QueryCache", "ActionDispatch::Cookies", "ActionDispatch::Session::CookieStore", "ActionDispatch::Flash", "ActionDispatch::ParamsParser", "Rack::MethodOverride", - "ActionDispatch::Head", - "ActiveRecord::ConnectionAdapters::ConnectionManagement", - "ActiveRecord::QueryCache" + "ActionDispatch::Head" ], middleware end diff --git a/railties/test/application/model_initialization_test.rb b/railties/test/application/model_initialization_test.rb new file mode 100644 index 0000000000..6a22f8d8df --- /dev/null +++ b/railties/test/application/model_initialization_test.rb @@ -0,0 +1,33 @@ +require 'isolation/abstract_unit' + +class PostTest < Test::Unit::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + boot_rails + end + + def test_reload_should_reload_constants + app_file "app/models/post.rb", <<-MODEL + class Post < ActiveRecord::Base + validates_acceptance_of :title, :accept => "omg" + end + MODEL + + require "#{rails_root}/config/environment" + ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:") + ActiveRecord::Migration.verbose = false + ActiveRecord::Schema.define(:version => 1) do + create_table :posts do |t| + t.string :title + end + end + + p = Post.create(:title => 'omg') + assert_equal 1, Post.count + assert_equal 'omg', p.title + p = Post.first + assert_equal 'omg', p.title + end +end diff --git a/railties/test/application/paths_test.rb b/railties/test/application/paths_test.rb index 5ab558c026..978d677efc 100644 --- a/railties/test/application/paths_test.rb +++ b/railties/test/application/paths_test.rb @@ -48,7 +48,8 @@ module ApplicationTests assert_path @paths.tmp.cache, "tmp", "cache" assert_path @paths.config, "config" assert_path @paths.config.locales, "config", "locales", "en.yml" - assert_path @paths.config.environment, "config", "environments", "development.rb" + assert_path @paths.config.environment, "config", "environment.rb" + assert_path @paths.config.environments, "config", "environments", "development.rb" assert_equal root("app", "controllers"), @paths.app.controllers.to_a.first end @@ -61,7 +62,7 @@ module ApplicationTests end test "environments has a glob equal to the current environment" do - assert_equal "#{Rails.env}.rb", @paths.config.environment.glob + assert_equal "#{Rails.env}.rb", @paths.config.environments.glob end test "load path includes each of the paths in config.paths as long as the directories exist" do diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index bf2da866f4..6b7a471494 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -19,5 +19,19 @@ module ApplicationTests ::Rails.application.load_tasks assert $task_loaded end + + def test_environment_is_required_in_rake_tasks + app_file "config/environment.rb", <<-RUBY + SuperMiddleware = Struct.new(:app) + + Rails::Application.configure do + config.middleware.use SuperMiddleware + end + + Rails::Application.initialize! + RUBY + + assert_match "SuperMiddleware", Dir.chdir(app_path){ `rake middleware` } + end end end
\ No newline at end of file diff --git a/railties/test/fixtures/lib/empty_builder.rb b/railties/test/fixtures/lib/empty_builder.rb new file mode 100644 index 0000000000..babd9c2461 --- /dev/null +++ b/railties/test/fixtures/lib/empty_builder.rb @@ -0,0 +1,2 @@ +class AppBuilder +end
\ No newline at end of file diff --git a/railties/test/fixtures/lib/simple_builder.rb b/railties/test/fixtures/lib/simple_builder.rb new file mode 100644 index 0000000000..47dcdc0d96 --- /dev/null +++ b/railties/test/fixtures/lib/simple_builder.rb @@ -0,0 +1,7 @@ +class AppBuilder + def configru + create_file "config.ru", <<-R.strip +run proc { |env| [200, { "Content-Type" => "text/html" }, ["Hello World"]] } + R + end +end
\ No newline at end of file diff --git a/railties/test/fixtures/lib/tweak_builder.rb b/railties/test/fixtures/lib/tweak_builder.rb new file mode 100644 index 0000000000..eed20ecc9b --- /dev/null +++ b/railties/test/fixtures/lib/tweak_builder.rb @@ -0,0 +1,7 @@ +class AppBuilder < Rails::AppBuilder + def configru + create_file "config.ru", <<-R.strip +run proc { |env| [200, { "Content-Type" => "text/html" }, ["Hello World"]] } + R + end +end
\ No newline at end of file diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb index 44e0640552..e6fab93a87 100644 --- a/railties/test/generators/actions_test.rb +++ b/railties/test/generators/actions_test.rb @@ -209,7 +209,7 @@ class ActionsTest < Rails::Generators::TestCase def test_readme run_generator - Rails::Generators::AppGenerator.expects(:source_root).returns(destination_root) + Rails::Generators::AppGenerator.expects(:source_root).times(2).returns(destination_root) assert_match(/Welcome to Rails/, action(:readme, "README")) end diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 24e6d541c2..1a93867013 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -2,6 +2,40 @@ require 'abstract_unit' require 'generators/generators_test_helper' require 'rails/generators/rails/app/app_generator' +DEFAULT_APP_FILES = %w( + .gitignore + Gemfile + Rakefile + config.ru + app/controllers + app/helpers + app/models + app/views/layouts + config/environments + config/initializers + config/locales + db + doc + lib + lib/tasks + log + public/images + public/javascripts + public/stylesheets + script/rails + test/fixtures + test/functional + test/integration + test/performance + test/unit + vendor + vendor/plugins + tmp/sessions + tmp/sockets + tmp/cache + tmp/pids +) + class AppGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper arguments [destination_root] @@ -20,35 +54,7 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_application_skeleton_is_created run_generator - %w( - app/controllers - app/helpers - app/models - app/views/layouts - config/environments - config/initializers - config/locales - db - doc - lib - lib/tasks - log - public/images - public/javascripts - public/stylesheets - script/rails - test/fixtures - test/functional - test/integration - test/performance - test/unit - vendor - vendor/plugins - tmp/sessions - tmp/sockets - tmp/cache - tmp/pids - ).each{ |path| assert_file path } + DEFAULT_APP_FILES.each{ |path| assert_file path } end def test_application_controller_and_layout_files @@ -58,6 +64,11 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_no_file "public/stylesheets/application.css" end + def test_options_before_application_name_raises_an_error + content = capture(:stderr){ run_generator(["--skip-activerecord", destination_root]) } + assert_equal "Options should be given after the application name. For details run: rails --help\n", content + end + def test_name_collision_raises_an_error content = capture(:stderr){ run_generator [File.join(destination_root, "generate")] } assert_equal "Invalid application name generate. Please give a name which does not match one of the reserved rails words.\n", content @@ -152,7 +163,7 @@ class AppGeneratorTest < Rails::Generators::TestCase template = %{ say "It works!" } template.instance_eval "def read; self; end" # Make the string respond to read - generator([destination_root], :template => path).expects(:open).with(path).returns(template) + generator([destination_root], :template => path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template) assert_match /It works!/, silence(:stdout){ generator.invoke } end @@ -188,10 +199,66 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file 'Gemfile', /^gem\s+["']rails["'],\s+:git\s+=>\s+["']#{Regexp.escape("git://github.com/rails/rails.git")}["']$/ end - protected +protected - def action(*args, &block) - silence(:stdout){ generator.send(*args, &block) } - end + def action(*args, &block) + silence(:stdout){ generator.send(*args, &block) } + end end + +class CustomAppGeneratorTest < Rails::Generators::TestCase + include GeneratorsTestHelper + tests Rails::Generators::AppGenerator + + arguments [destination_root] + + def setup + super + Rails::Generators::AppGenerator.instance_variable_set('@desc', nil) + @bundle_command = File.basename(Thor::Util.ruby_command).sub(/ruby/, 'bundle') + end + + def teardown + super + Rails::Generators::AppGenerator.instance_variable_set('@desc', nil) + Object.class_eval { remove_const :AppBuilder if const_defined?(:AppBuilder) } + end + + def test_builder_option_with_empty_app_builder + FileUtils.cd(Rails.root) + run_generator([destination_root, "-b", "#{Rails.root}/lib/empty_builder.rb"]) + DEFAULT_APP_FILES.each{ |path| assert_no_file path } + end + + def test_builder_option_with_simple_app_builder + FileUtils.cd(Rails.root) + run_generator([destination_root, "-b", "#{Rails.root}/lib/simple_builder.rb"]) + (DEFAULT_APP_FILES - ['config.ru']).each{ |path| assert_no_file path } + assert_file "config.ru", %[run proc { |env| [200, { "Content-Type" => "text/html" }, ["Hello World"]] }] + end + + def test_builder_option_with_tweak_app_builder + FileUtils.cd(Rails.root) + run_generator([destination_root, "-b", "#{Rails.root}/lib/tweak_builder.rb"]) + DEFAULT_APP_FILES.each{ |path| assert_file path } + assert_file "config.ru", %[run proc { |env| [200, { "Content-Type" => "text/html" }, ["Hello World"]] }] + end + + def test_builder_option_with_http + path = "http://gist.github.com/103208.txt" + template = "class AppBuilder; end" + template.instance_eval "def read; self; end" # Make the string respond to read + + generator([destination_root], :builder => path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template) + capture(:stdout) { generator.invoke } + + DEFAULT_APP_FILES.each{ |path| assert_no_file path } + end + +protected + + def action(*args, &block) + silence(:stdout){ generator.send(*args, &block) } + end +end
\ No newline at end of file diff --git a/railties/test/generators/mailer_generator_test.rb b/railties/test/generators/mailer_generator_test.rb index 81d6afa221..850b45ff74 100644 --- a/railties/test/generators/mailer_generator_test.rb +++ b/railties/test/generators/mailer_generator_test.rb @@ -1,5 +1,6 @@ require 'generators/generators_test_helper' -require 'rails/generators/rails/mailer/mailer_generator' +require 'rails/generators/mailer/mailer_generator' + class MailerGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper |