diff options
author | Jeremy Kemper <jeremy@bitsweat.net> | 2008-12-29 14:38:54 -0800 |
---|---|---|
committer | Jeremy Kemper <jeremy@bitsweat.net> | 2008-12-29 14:38:54 -0800 |
commit | 276ec16007b03d0a527fb0b83a7ee0b81e460fa1 (patch) | |
tree | 224491aa1948d613a551189028746d73400fccce | |
parent | 2e053aec9bafa8735d70886f36dea06ea10dc4ce (diff) | |
parent | 490c26c8433a6d278bc61118782da360e8889646 (diff) | |
download | rails-276ec16007b03d0a527fb0b83a7ee0b81e460fa1.tar.gz rails-276ec16007b03d0a527fb0b83a7ee0b81e460fa1.tar.bz2 rails-276ec16007b03d0a527fb0b83a7ee0b81e460fa1.zip |
Merge branch 'master' of git@github.com:rails/rails
125 files changed, 4177 insertions, 1651 deletions
diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index f1273eb02e..c878a8d205 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -570,7 +570,9 @@ module ActionMailer #:nodoc: end def candidate_for_layout?(options) - !@template.send(:_exempt_from_layout?, default_template_name) + !self.view_paths.find_template(default_template_name, default_template_format).exempt_from_layout? + rescue ActionView::MissingTemplate + return true end def template_root diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb index ad1eac912b..4900f6fb35 100644 --- a/actionmailer/test/abstract_unit.rb +++ b/actionmailer/test/abstract_unit.rb @@ -10,11 +10,14 @@ require 'action_mailer/test_case' ActiveSupport::Deprecation.debug = true # Bogus template processors -ActionView::Template.register_template_handler :haml, lambda { |template| "Look its HAML!" } -ActionView::Template.register_template_handler :bak, lambda { |template| "Lame backup" } +ActionView::Template.register_template_handler :haml, lambda { |template| "Look its HAML!".inspect } +ActionView::Template.register_template_handler :bak, lambda { |template| "Lame backup".inspect } $:.unshift "#{File.dirname(__FILE__)}/fixtures/helpers" -ActionMailer::Base.template_root = "#{File.dirname(__FILE__)}/fixtures" + +FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures') +ActionMailer::Base.template_root = FIXTURE_LOAD_PATH +ActionMailer::Base.template_root.load class MockSMTP def self.deliveries diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index 639cf14cd1..a8abf48441 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,5 +1,19 @@ *2.3.0 [Edge]* +* Make ActionController#render(string) work as a shortcut for render :file/:template/:action => string. [#1435] [Pratik Naik] Examples: + + # Instead of render(:action => 'other_action') + render('other_action') # argument has no '/' + render(:other_action) + + # Instead of render(:template => 'controller/action') + render('controller/action') # argument must not begin with a '/', but contain a '/' + + # Instead of render(:file => '/Users/lifo/home.html.erb') + render('/Users/lifo/home.html.erb') # argument must begin with a '/' + +* Add :prompt option to date/time select helpers. #561 [Sam Oliver] + * Fixed that send_file shouldn't set an etag #1578 [Hongli Lai] * Allow users to opt out of the spoofing checks in Request#remote_ip. Useful for sites whose traffic regularly triggers false positives. [Darren Boyd] diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index ae947820b4..98fb490d64 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -38,7 +38,7 @@ module ActionController # TODO: Review explicit to see if they will automatically be handled by # the initilizer if they are really needed. def self.load_all! - [Base, CGIHandler, CgiRequest, RackRequest, RackRequest, Http::Headers, UrlRewriter, UrlWriter] + [Base, CGIHandler, CgiRequest, Request, Response, Http::Headers, UrlRewriter, UrlWriter] end autoload :AbstractRequest, 'action_controller/request' @@ -59,7 +59,11 @@ module ActionController autoload :MiddlewareStack, 'action_controller/middleware_stack' autoload :MimeResponds, 'action_controller/mime_responds' autoload :PolymorphicRoutes, 'action_controller/polymorphic_routes' - autoload :RackRequest, 'action_controller/rack_process' + autoload :Request, 'action_controller/request' + autoload :RequestParser, 'action_controller/request_parser' + autoload :UrlEncodedPairParser, 'action_controller/url_encoded_pair_parser' + autoload :UploadedStringIO, 'action_controller/uploaded_file' + autoload :UploadedTempfile, 'action_controller/uploaded_file' autoload :RecordIdentifier, 'action_controller/record_identifier' autoload :Response, 'action_controller/response' autoload :RequestForgeryProtection, 'action_controller/request_forgery_protection' @@ -74,6 +78,7 @@ module ActionController autoload :Translation, 'action_controller/translation' autoload :UrlRewriter, 'action_controller/url_rewriter' autoload :UrlWriter, 'action_controller/url_rewriter' + autoload :VerbPiggybacking, 'action_controller/verb_piggybacking' autoload :Verification, 'action_controller/verification' module Assertions diff --git a/actionpack/lib/action_controller/assertions/routing_assertions.rb b/actionpack/lib/action_controller/assertions/routing_assertions.rb index 8a837c592c..5101751cea 100644 --- a/actionpack/lib/action_controller/assertions/routing_assertions.rb +++ b/actionpack/lib/action_controller/assertions/routing_assertions.rb @@ -134,7 +134,7 @@ module ActionController path = "/#{path}" unless path.first == '/' # Assume given controller - request = ActionController::TestRequest.new({}, {}, nil) + request = ActionController::TestRequest.new request.env["REQUEST_METHOD"] = request_method.to_s.upcase if request_method request.path = path diff --git a/actionpack/lib/action_controller/assertions/selector_assertions.rb b/actionpack/lib/action_controller/assertions/selector_assertions.rb index 248ca85994..7f8fe9ab19 100644 --- a/actionpack/lib/action_controller/assertions/selector_assertions.rb +++ b/actionpack/lib/action_controller/assertions/selector_assertions.rb @@ -402,6 +402,7 @@ module ActionController if rjs_type if rjs_type == :insert position = args.shift + id = args.shift insertion = "insert_#{position}".to_sym raise ArgumentError, "Unknown RJS insertion type #{position}" unless RJS_STATEMENTS[insertion] statement = "(#{RJS_STATEMENTS[insertion]})" diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index eae17d6dd5..da3d1f46ee 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -254,7 +254,7 @@ module ActionController #:nodoc: cattr_reader :protected_instance_variables # Controller specific instance variables which will not be accessible inside views. @@protected_instance_variables = %w(@assigns @performed_redirect @performed_render @variables_added @request_origin @url @parent_controller - @action_name @before_filter_chain_aborted @action_cache_path @_session @_cookies @_headers @_params + @action_name @before_filter_chain_aborted @action_cache_path @_session @_headers @_params @_flash @_response) # Prepends all the URL-generating helpers from AssetHelper. This makes it possible to easily move javascripts, stylesheets, @@ -382,6 +382,13 @@ module ActionController #:nodoc: attr_accessor :action_name class << self + def call(env) + # HACK: For global rescue to have access to the original request and response + request = env["actioncontroller.rescue.request"] ||= Request.new(env) + response = env["actioncontroller.rescue.response"] ||= Response.new + process(request, response) + end + # Factory for the standard create, process loop where the controller is discarded after processing. def process(request, response) #:nodoc: new.process(request, response) @@ -502,7 +509,7 @@ module ActionController #:nodoc: protected :filter_parameters end - delegate :exempt_from_layout, :to => 'ActionView::Base' + delegate :exempt_from_layout, :to => 'ActionView::Template' end public @@ -859,16 +866,23 @@ module ActionController #:nodoc: def render(options = nil, extra_options = {}, &block) #:doc: raise DoubleRenderError, "Can only render or redirect once per action" if performed? + validate_render_arguments(options, extra_options, block_given?) + if options.nil? - return render(:file => default_template_name, :layout => true) - elsif !extra_options.is_a?(Hash) - raise RenderError, "You called render with invalid options : #{options.inspect}, #{extra_options.inspect}" - else - if options == :update - options = extra_options.merge({ :update => true }) - elsif !options.is_a?(Hash) - raise RenderError, "You called render with invalid options : #{options.inspect}" + options = { :template => default_template.filename, :layout => true } + elsif options == :update + options = extra_options.merge({ :update => true }) + elsif options.is_a?(String) || options.is_a?(Symbol) + case options.to_s.index('/') + when 0 + extra_options[:file] = options + when nil + extra_options[:action] = options + else + extra_options[:template] = options end + + options = extra_options end layout = pick_layout(options) @@ -898,7 +912,7 @@ module ActionController #:nodoc: render_for_text(@template.render(options.merge(:layout => layout)), options[:status]) elsif action_name = options[:action] - render_for_file(default_template_name(action_name.to_s), options[:status], layout) + render_for_file(default_template(action_name.to_s), options[:status], layout) elsif xml = options[:xml] response.content_type ||= Mime::XML @@ -933,7 +947,7 @@ module ActionController #:nodoc: render_for_text(nil, options[:status]) else - render_for_file(default_template_name, options[:status], layout) + render_for_file(default_template, options[:status], layout) end end end @@ -1164,7 +1178,8 @@ module ActionController #:nodoc: private def render_for_file(template_path, status = nil, layout = nil, locals = {}) #:nodoc: - logger.info("Rendering #{template_path}" + (status ? " (#{status})" : '')) if logger + path = template_path.respond_to?(:path_without_format_and_extension) ? template_path.path_without_format_and_extension : template_path + logger.info("Rendering #{path}" + (status ? " (#{status})" : '')) if logger render_for_text @template.render(:file => template_path, :locals => locals, :layout => layout), status end @@ -1185,6 +1200,16 @@ module ActionController #:nodoc: end end + def validate_render_arguments(options, extra_options, has_block) + if options && (has_block && options != :update) && !options.is_a?(String) && !options.is_a?(Hash) && !options.is_a?(Symbol) + raise RenderError, "You called render with invalid options : #{options.inspect}" + end + + if !extra_options.is_a?(Hash) + raise RenderError, "You called render with invalid options : #{options.inspect}, #{extra_options.inspect}" + end + end + def initialize_template_class(response) response.template = ActionView::Base.new(self.class.view_paths, {}, self) response.template.helpers.send :include, self.class.master_helper_module @@ -1193,7 +1218,7 @@ module ActionController #:nodoc: end def assign_shortcuts(request, response) - @_request, @_params, @_cookies = request, request.parameters, request.cookies + @_request, @_params = request, request.parameters @_response = response @_response.session = request.session @@ -1241,10 +1266,17 @@ module ActionController #:nodoc: elsif respond_to? :method_missing method_missing action_name default_render unless performed? - elsif template_exists? - default_render else - raise UnknownAction, "No action responded to #{action_name}. Actions: #{action_methods.sort.to_sentence}", caller + begin + default_render + rescue ActionView::MissingTemplate => e + # Was the implicit template missing, or was it another template? + if e.path == default_template_name + raise UnknownAction, "No action responded to #{action_name}. Actions: #{action_methods.sort.to_sentence}", caller + else + raise e + end + end end end @@ -1290,10 +1322,8 @@ module ActionController #:nodoc: @_session.close if @_session && @_session.respond_to?(:close) end - def template_exists?(template_name = default_template_name) - @template.send(:_pick_template, template_name) ? true : false - rescue ActionView::MissingTemplate - false + def default_template(action_name = self.action_name) + self.view_paths.find_template(default_template_name(action_name), default_template_format) end def default_template_name(action_name = self.action_name) diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb index b4d251eb3c..1d14df0052 100644 --- a/actionpack/lib/action_controller/caching.rb +++ b/actionpack/lib/action_controller/caching.rb @@ -27,7 +27,6 @@ module ActionController #:nodoc: autoload :Actions, 'action_controller/caching/actions' autoload :Fragments, 'action_controller/caching/fragments' autoload :Pages, 'action_controller/caching/pages' - autoload :SqlCache, 'action_controller/caching/sql_cache' autoload :Sweeping, 'action_controller/caching/sweeping' def self.included(base) #:nodoc: @@ -41,7 +40,7 @@ module ActionController #:nodoc: end include Pages, Actions, Fragments - include Sweeping, SqlCache if defined?(ActiveRecord) + include Sweeping if defined?(ActiveRecord) @@perform_caching = true cattr_accessor :perform_caching diff --git a/actionpack/lib/action_controller/caching/sql_cache.rb b/actionpack/lib/action_controller/caching/sql_cache.rb deleted file mode 100644 index 139be6100d..0000000000 --- a/actionpack/lib/action_controller/caching/sql_cache.rb +++ /dev/null @@ -1,18 +0,0 @@ -module ActionController #:nodoc: - module Caching - module SqlCache - def self.included(base) #:nodoc: - if defined?(ActiveRecord) && ActiveRecord::Base.respond_to?(:cache) - base.alias_method_chain :perform_action, :caching - end - end - - protected - def perform_action_with_caching - ActiveRecord::Base.cache do - perform_action_without_caching - end - end - end - end -end
\ No newline at end of file diff --git a/actionpack/lib/action_controller/cookies.rb b/actionpack/lib/action_controller/cookies.rb index 0e058085ec..840ceb5abd 100644 --- a/actionpack/lib/action_controller/cookies.rb +++ b/actionpack/lib/action_controller/cookies.rb @@ -64,45 +64,31 @@ module ActionController #:nodoc: # Returns the value of the cookie by +name+, or +nil+ if no such cookie exists. def [](name) - cookie = @cookies[name.to_s] - if cookie && cookie.respond_to?(:value) - cookie.size > 1 ? cookie.value : cookie.value[0] - else - cookie - end + super(name.to_s) end # Sets the cookie named +name+. The second argument may be the very cookie # value, or a hash of options as documented above. - def []=(name, options) + def []=(key, options) if options.is_a?(Hash) - options = options.inject({}) { |options, pair| options[pair.first.to_s] = pair.last; options } - options["name"] = name.to_s + options.symbolize_keys! else - options = { "name" => name.to_s, "value" => options } + options = { :value => options } end - set_cookie(options) + options[:path] = "/" unless options.has_key?(:path) + super(key.to_s, options[:value]) + @controller.response.set_cookie(key, options) end # Removes the cookie on the client machine by setting the value to an empty string # and setting its expiration date into the past. Like <tt>[]=</tt>, you can pass in # an options hash to delete cookies with extra data such as a <tt>:path</tt>. - def delete(name, options = {}) - options.stringify_keys! - set_cookie(options.merge("name" => name.to_s, "value" => "", "expires" => Time.at(0))) + def delete(key, options = {}) + options.symbolize_keys! + options[:path] = "/" unless options.has_key?(:path) + super(key.to_s) + @controller.response.delete_cookie(key, options) end - - private - # Builds a CGI::Cookie object and adds the cookie to the response headers. - # - # The path of the cookie defaults to "/" if there's none in +options+, and - # everything is passed to the CGI::Cookie constructor. - def set_cookie(options) #:doc: - options["path"] = "/" unless options["path"] - cookie = CGI::Cookie.new(options) - @controller.logger.info "Cookie set: #{cookie}" unless @controller.logger.nil? - @controller.response.headers["cookie"] << cookie - end end end diff --git a/actionpack/lib/action_controller/dispatcher.rb b/actionpack/lib/action_controller/dispatcher.rb index e1eaaf7cbb..c4e7357b81 100644 --- a/actionpack/lib/action_controller/dispatcher.rb +++ b/actionpack/lib/action_controller/dispatcher.rb @@ -44,22 +44,8 @@ module ActionController cattr_accessor :middleware self.middleware = MiddlewareStack.new do |middleware| - middleware.use "ActionController::Lock", :if => lambda { - !ActionController::Base.allow_concurrency - } - middleware.use "ActionController::Failsafe" - - ["ActionController::Session::CookieStore", - "ActionController::Session::MemCacheStore", - "ActiveRecord::SessionStore"].each do |store| - middleware.use(store, ActionController::Base.session_options, - :if => lambda { - if session_store = ActionController::Base.session_store - session_store.name == store - end - } - ) - end + middlewares = File.join(File.dirname(__FILE__), "middlewares.rb") + middleware.instance_eval(File.read(middlewares)) end include ActiveSupport::Callbacks @@ -74,11 +60,10 @@ module ActionController def dispatch begin run_callbacks :before_dispatch - controller = Routing::Routes.recognize(@request) - controller.process(@request, @response).to_a + Routing::Routes.call(@env) rescue Exception => exception if controller ||= (::ApplicationController rescue Base) - controller.process_with_exception(@request, @response, exception).to_a + controller.call_with_exception(@env, exception).to_a else raise exception end @@ -97,8 +82,7 @@ module ActionController end def _call(env) - @request = RackRequest.new(env) - @response = Response.new + @env = env dispatch end @@ -124,8 +108,7 @@ module ActionController def checkin_connections # Don't return connection (and peform implicit rollback) if this request is a part of integration test - # TODO: This callback should have direct access to env - return if @request.key?("rack.test") + return if @env.key?("rack.test") ActiveRecord::Base.clear_active_connections! end end diff --git a/actionpack/lib/action_controller/helpers.rb b/actionpack/lib/action_controller/helpers.rb index 402750c57d..ba65032f6a 100644 --- a/actionpack/lib/action_controller/helpers.rb +++ b/actionpack/lib/action_controller/helpers.rb @@ -163,9 +163,9 @@ module ActionController #:nodoc: def helper_method(*methods) methods.flatten.each do |method| master_helper_module.module_eval <<-end_eval - def #{method}(*args, &block) - controller.send(%(#{method}), *args, &block) - end + def #{method}(*args, &block) # def current_user(*args, &block) + controller.send(%(#{method}), *args, &block) # controller.send(%(current_user), *args, &block) + end # end end_eval end end diff --git a/actionpack/lib/action_controller/http_authentication.rb b/actionpack/lib/action_controller/http_authentication.rb index 2ed810db7d..3cb5829eca 100644 --- a/actionpack/lib/action_controller/http_authentication.rb +++ b/actionpack/lib/action_controller/http_authentication.rb @@ -55,7 +55,31 @@ module ActionController # end # end # - # + # Simple Digest example. Note the block must return the user's password so the framework + # can appropriately hash it to check the user's credentials. Returning nil will cause authentication to fail. + # + # class PostsController < ApplicationController + # Users = {"dhh" => "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_digest(realm) do |user_name| + # Users[user_name] + # end + # end + # end + # + # # In your integration tests, you can do something like this: # # def test_access_granted_from_xml @@ -108,7 +132,10 @@ module ActionController end def decode_credentials(request) - ActiveSupport::Base64.decode64(authorization(request).split.last || '') + # Properly decode credentials spanning a new-line + auth = authorization(request) + auth.slice!('Basic ') + ActiveSupport::Base64.decode64(auth || '') end def encode_credentials(user_name, password) @@ -120,5 +147,165 @@ module ActionController controller.__send__ :render, :text => "HTTP Basic: Access denied.\n", :status => :unauthorized end end + + module Digest + extend self + + module ControllerMethods + def authenticate_or_request_with_http_digest(realm = "Application", &password_procedure) + begin + authenticate_with_http_digest!(realm, &password_procedure) + rescue ActionController::HttpAuthentication::Error => e + msg = e.message + msg = "#{msg} expected '#{e.expected}' was '#{e.was}'" unless e.expected.nil? + raise msg if e.fatal? + request_http_digest_authentication(realm, msg) + end + end + + # Authenticate using HTTP Digest, throwing ActionController::HttpAuthentication::Error on failure. + # This allows more detailed analysis of authentication failures + # to be relayed to the client. + def authenticate_with_http_digest!(realm = "Application", &login_procedure) + HttpAuthentication::Digest.authenticate(self, realm, &login_procedure) + end + + # Authenticate with HTTP Digest, returns true or false + def authenticate_with_http_digest(realm = "Application", &login_procedure) + HttpAuthentication::Digest.authenticate(self, realm, &login_procedure) rescue false + end + + # Render output including the HTTP Digest authentication header + def request_http_digest_authentication(realm = "Application", message = nil) + HttpAuthentication::Digest.authentication_request(self, realm, message) + end + + # Add HTTP Digest authentication header to result headers + def http_digest_authentication_header(realm = "Application") + HttpAuthentication::Digest.authentication_header(self, realm) + end + end + + # Raises error unless authentictaion succeeds, returns true otherwise + def authenticate(controller, realm, &password_procedure) + raise Error.new(false), "No authorization header found" unless authorization(controller.request) + validate_digest_response(controller, realm, &password_procedure) + true + end + + def authorization(request) + request.env['HTTP_AUTHORIZATION'] || + request.env['X-HTTP_AUTHORIZATION'] || + request.env['X_HTTP_AUTHORIZATION'] || + request.env['REDIRECT_X_HTTP_AUTHORIZATION'] + end + + # Raises error unless the request credentials response value matches the expected value. + def validate_digest_response(controller, realm, &password_procedure) + credentials = decode_credentials(controller.request) + + # Check the nonce, opaque and realm. + # Ignore nc, as we have no way to validate the number of times this nonce has been used + validate_nonce(controller.request, credentials[:nonce]) + raise Error.new(false, realm, credentials[:realm]), "Realm doesn't match" unless realm == credentials[:realm] + raise Error.new(true, opaque(controller.request), credentials[:opaque]),"Opaque doesn't match" unless opaque(controller.request) == credentials[:opaque] + + password = password_procedure.call(credentials[:username]) + raise Error.new(false), "No password" if password.nil? + expected = expected_response(controller.request.env['REQUEST_METHOD'], controller.request.url, credentials, password) + raise Error.new(false, expected, credentials[:response]), "Invalid response" unless expected == credentials[:response] + end + + # Returns the expected response for a request of +http_method+ to +uri+ with the decoded +credentials+ and the expected +password+ + def expected_response(http_method, uri, credentials, password) + ha1 = ::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(':')) + ha2 = ::Digest::MD5.hexdigest([http_method.to_s.upcase,uri].join(':')) + ::Digest::MD5.hexdigest([ha1,credentials[:nonce], credentials[:nc], credentials[:cnonce],credentials[:qop],ha2].join(':')) + end + + def encode_credentials(http_method, credentials, password) + credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password) + "Digest " + credentials.sort_by {|x| x[0].to_s }.inject([]) {|a, v| a << "#{v[0]}='#{v[1]}'" }.join(', ') + end + + def decode_credentials(request) + authorization(request).to_s.gsub(/^Digest\s+/,'').split(',').inject({}) do |hash, pair| + key, value = pair.split('=', 2) + hash[key.strip.to_sym] = value.to_s.gsub(/^"|"$/,'').gsub(/'/, '') + hash + end + end + + def authentication_header(controller, realm) + controller.headers["WWW-Authenticate"] = %(Digest realm="#{realm}", qop="auth", algorithm=MD5, nonce="#{nonce(controller.request)}", opaque="#{opaque(controller.request)}") + end + + def authentication_request(controller, realm, message = "HTTP Digest: Access denied") + authentication_header(controller, realm) + controller.send! :render, :text => message, :status => :unauthorized + end + + # Uses an MD5 digest based on time to generate a value to be used only once. + # + # A server-specified data string which should be uniquely generated each time a 401 response is made. + # It is recommended that this string be base64 or hexadecimal data. + # Specifically, since the string is passed in the header lines as a quoted string, the double-quote character is not allowed. + # + # The contents of the nonce are implementation dependent. + # The quality of the implementation depends on a good choice. + # A nonce might, for example, be constructed as the base 64 encoding of + # + # => time-stamp H(time-stamp ":" ETag ":" private-key) + # + # where time-stamp is a server-generated time or other non-repeating value, + # ETag is the value of the HTTP ETag header associated with the requested entity, + # and private-key is data known only to the server. + # With a nonce of this form a server would recalculate the hash portion after receiving the client authentication header and + # reject the request if it did not match the nonce from that header or + # if the time-stamp value is not recent enough. In this way the server can limit the time of the nonce's validity. + # The inclusion of the ETag prevents a replay request for an updated version of the resource. + # (Note: including the IP address of the client in the nonce would appear to offer the server the ability + # to limit the reuse of the nonce to the same client that originally got it. + # However, that would break proxy farms, where requests from a single user often go through different proxies in the farm. + # Also, IP address spoofing is not that hard.) + # + # An implementation might choose not to accept a previously used nonce or a previously used digest, in order to + # protect against a replay attack. Or, an implementation might choose to use one-time nonces or digests for + # POST or PUT requests and a time-stamp for GET requests. For more details on the issues involved see Section 4 + # of this document. + # + # The nonce is opaque to the client. + def nonce(request, time = Time.now) + session_id = request.is_a?(String) ? request : request.session.session_id + t = time.to_i + hashed = [t, session_id] + digest = ::Digest::MD5.hexdigest(hashed.join(":")) + Base64.encode64("#{t}:#{digest}").gsub("\n", '') + end + + def validate_nonce(request, value) + t = Base64.decode64(value).split(":").first.to_i + raise Error.new(true), "Stale Nonce" if (t - Time.now.to_i).abs > 10 * 60 + n = nonce(request, t) + raise Error.new(true, value, n), "Bad Nonce" unless n == value + end + + # Opaque based on digest of session_id + def opaque(request) + session_id = request.is_a?(String) ? request : request.session.session_id + @opaque ||= Base64.encode64(::Digest::MD5::hexdigest(session_id)).gsub("\n", '') + end + end + + class Error < RuntimeError + attr_accessor :expected, :was + def initialize(fatal = false, expected = nil, was = nil) + @fatal = fatal + @expected = expected + @was = was + end + + def fatal?; @fatal; end + end end end diff --git a/actionpack/lib/action_controller/integration.rb b/actionpack/lib/action_controller/integration.rb index d952c3489b..a8e54c2fc7 100644 --- a/actionpack/lib/action_controller/integration.rb +++ b/actionpack/lib/action_controller/integration.rb @@ -2,6 +2,17 @@ require 'stringio' require 'uri' require 'active_support/test_case' +# Monkey patch Rack::Lint to support rewind +module Rack + class Lint + class InputWrapper + def rewind + @input.rewind + end + end + end +end + module ActionController module Integration #:nodoc: # An integration Session instance represents a set of requests and responses @@ -57,6 +68,15 @@ module ActionController # A running counter of the number of requests processed. attr_accessor :request_count + # Nonce value for Digest Authentication, implicitly set on response with WWW-Authentication + attr_accessor :nonce + + # Opaque value for Digest Authentication, implicitly set on response with WWW-Authentication + attr_accessor :opaque + + # Opaque value for Authentication, implicitly set on response with WWW-Authentication + attr_accessor :realm + class MultiPartNeededException < Exception end @@ -232,6 +252,53 @@ module ActionController end alias xhr :xml_http_request + def request_with_noauth(http_method, uri, parameters, headers) + process_with_auth http_method, uri, parameters, headers + end + + # Performs a request with the given http_method and parameters, including HTTP Basic authorization headers. + # See get() for more details on paramters and headers. + # + # You can perform GET, POST, PUT, DELETE, and HEAD requests with #get_with_basic, #post_with_basic, + # #put_with_basic, #delete_with_basic, and #head_with_basic. + def request_with_basic(http_method, uri, parameters, headers, user_name, password) + process_with_auth http_method, uri, parameters, headers.merge(:authorization => ActionController::HttpAuthentication::Basic.encode_credentials(user_name, password)) + end + + # Performs a request with the given http_method and parameters, including HTTP Digest authorization headers. + # See get() for more details on paramters and headers. + # + # You can perform GET, POST, PUT, DELETE, and HEAD requests with #get_with_digest, #post_with_digest, + # #put_with_digest, #delete_with_digest, and #head_with_digest. + def request_with_digest(http_method, uri, parameters, headers, user_name, password) + # Realm, Nonce, and Opaque taken from previoius 401 response + + credentials = { + :username => user_name, + :realm => @realm, + :nonce => @nonce, + :qop => "auth", + :nc => "00000001", + :cnonce => "0a4f113b", + :opaque => @opaque, + :uri => uri + } + + raise "Digest request without previous 401 response" if @opaque.nil? + + process_with_auth http_method, uri, parameters, headers.merge(:authorization => ActionController::HttpAuthentication::Digest.encode_credentials(http_method, credentials, password)) + end + + # def get_with_basic, def post_with_basic, def put_with_basic, def delete_with_basic, def head_with_basic + # def get_with_digest, def post_with_digest, def put_with_digest, def delete_with_digest, def head_with_digest + [:get, :post, :put, :delete, :head].each do |method| + [:noauth, :basic, :digest].each do |auth_type| + define_method("#{method}_with_#{auth_type}") do |uri, parameters, headers, *auth| + send("request_with_#{auth_type}", method, uri, parameters, headers, *auth) + end + end + end + # Returns the URL for the given options, according to the rules specified # in the application's routes. def url_for(options) @@ -353,6 +420,32 @@ module ActionController return status end + # Same as process, but handles authentication returns to perform + # Basic or Digest authentication + def process_with_auth(method, path, parameters = nil, headers = nil) + status = process(method, path, parameters, headers) + + if status == 401 + # Extract authentication information from response + auth_data = @response.headers['WWW-Authenticate'] + if /^Basic /.match(auth_data) + # extract realm, to be used in subsequent request + @realm = auth_header.split(' ')[1] + elsif /^Digest/.match(auth_data) + creds = auth_data.to_s.gsub(/^Digest\s+/,'').split(',').inject({}) do |hash, pair| + key, value = pair.split('=', 2) + hash[key.strip.to_sym] = value.to_s.gsub(/^"|"$/,'').gsub(/'/, '') + hash + end + @realm = creds[:realm] + @nonce = creds[:nonce] + @opaque = creds[:opaque] + end + end + + return status + end + # Encode the cookies hash in a format suitable for passing to a # request. def encode_cookies @@ -371,7 +464,7 @@ module ActionController "SERVER_PORT" => https? ? "443" : "80", "HTTPS" => https? ? "on" : "off" } - UrlRewriter.new(RackRequest.new(env), {}) + UrlRewriter.new(Request.new(env), {}) end def name_with_prefix(prefix, name) diff --git a/actionpack/lib/action_controller/layout.rb b/actionpack/lib/action_controller/layout.rb index 54108df06d..159c5c7326 100644 --- a/actionpack/lib/action_controller/layout.rb +++ b/actionpack/lib/action_controller/layout.rb @@ -178,9 +178,15 @@ module ActionController #:nodoc: find_layout(layout, format) end + def layout_list #:nodoc: + Array(view_paths).sum([]) { |path| Dir["#{path}/layouts/**/*"] } + end + def find_layout(layout, *formats) #:nodoc: return layout if layout.respond_to?(:render) view_paths.find_template(layout.to_s =~ /layouts\// ? layout : "layouts/#{layout}", *formats) + rescue ActionView::MissingTemplate + nil end private @@ -188,7 +194,7 @@ module ActionController #:nodoc: inherited_without_layout(child) unless child.name.blank? layout_match = child.name.underscore.sub(/_controller$/, '').sub(/^controllers\//, '') - child.layout(layout_match, {}, true) if child.find_layout(layout_match, :all) + child.layout(layout_match, {}, true) unless child.layout_list.grep(%r{layouts/#{layout_match}(\.[a-z][0-9a-z]*)+$}).empty? end end @@ -225,8 +231,16 @@ module ActionController #:nodoc: private def candidate_for_layout?(options) - options.values_at(:text, :xml, :json, :file, :inline, :partial, :nothing, :update).compact.empty? && - !@template.__send__(:_exempt_from_layout?, options[:template] || default_template_name(options[:action])) + template = options[:template] || default_template(options[:action]) + if options.values_at(:text, :xml, :json, :file, :inline, :partial, :nothing, :update).compact.empty? + begin + !self.view_paths.find_template(template, default_template_format).exempt_from_layout? + rescue ActionView::MissingTemplate + true + end + end + rescue ActionView::MissingTemplate + false end def pick_layout(options) @@ -235,7 +249,7 @@ module ActionController #:nodoc: when FalseClass nil when NilClass, TrueClass - active_layout if action_has_layout? && !@template.__send__(:_exempt_from_layout?, default_template_name) + active_layout if action_has_layout? && candidate_for_layout?(:template => default_template_name) else active_layout(layout) end diff --git a/actionpack/lib/action_controller/middlewares.rb b/actionpack/lib/action_controller/middlewares.rb new file mode 100644 index 0000000000..793739723f --- /dev/null +++ b/actionpack/lib/action_controller/middlewares.rb @@ -0,0 +1,21 @@ +use "ActionController::Lock", :if => lambda { + !ActionController::Base.allow_concurrency +} + +use "ActionController::Failsafe" + +use "ActiveRecord::QueryCache", :if => lambda { defined?(ActiveRecord) } + +["ActionController::Session::CookieStore", + "ActionController::Session::MemCacheStore", + "ActiveRecord::SessionStore"].each do |store| + use(store, ActionController::Base.session_options, + :if => lambda { + if session_store = ActionController::Base.session_store + session_store.name == store + end + } + ) +end + +use ActionController::VerbPiggybacking diff --git a/actionpack/lib/action_controller/mime_responds.rb b/actionpack/lib/action_controller/mime_responds.rb index 29294476f7..b755363873 100644 --- a/actionpack/lib/action_controller/mime_responds.rb +++ b/actionpack/lib/action_controller/mime_responds.rb @@ -143,12 +143,27 @@ module ActionController #:nodoc: custom(@mime_type_priority.first, &block) end end + + def self.generate_method_for_mime(mime) + sym = mime.is_a?(Symbol) ? mime : mime.to_sym + const = sym.to_s.upcase + class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{sym}(&block) # def html(&block) + custom(Mime::#{const}, &block) # custom(Mime::HTML, &block) + end # end + RUBY + end - def method_missing(symbol, &block) - mime_constant = symbol.to_s.upcase + Mime::SET.each do |mime| + generate_method_for_mime(mime) + end - if Mime::SET.include?(Mime.const_get(mime_constant)) - custom(Mime.const_get(mime_constant), &block) + def method_missing(symbol, &block) + mime_constant = Mime.const_get(symbol.to_s.upcase) + + if Mime::SET.include?(mime_constant) + self.class.generate_method_for_mime(mime_constant) + send(symbol, &block) else super end diff --git a/actionpack/lib/action_controller/polymorphic_routes.rb b/actionpack/lib/action_controller/polymorphic_routes.rb index dce50c6c3b..924d1aa6bd 100644 --- a/actionpack/lib/action_controller/polymorphic_routes.rb +++ b/actionpack/lib/action_controller/polymorphic_routes.rb @@ -118,13 +118,17 @@ module ActionController %w(edit new).each do |action| module_eval <<-EOT, __FILE__, __LINE__ - def #{action}_polymorphic_url(record_or_hash, options = {}) - polymorphic_url(record_or_hash, options.merge(:action => "#{action}")) - end - - def #{action}_polymorphic_path(record_or_hash, options = {}) - polymorphic_url(record_or_hash, options.merge(:action => "#{action}", :routing_type => :path)) - end + def #{action}_polymorphic_url(record_or_hash, options = {}) # def edit_polymorphic_url(record_or_hash, options = {}) + polymorphic_url( # polymorphic_url( + record_or_hash, # record_or_hash, + options.merge(:action => "#{action}")) # options.merge(:action => "edit")) + end # end + # + def #{action}_polymorphic_path(record_or_hash, options = {}) # def edit_polymorphic_path(record_or_hash, options = {}) + polymorphic_url( # polymorphic_url( + record_or_hash, # record_or_hash, + options.merge(:action => "#{action}", :routing_type => :path)) # options.merge(:action => "edit", :routing_type => :path)) + end # end EOT end diff --git a/actionpack/lib/action_controller/rack_process.rb b/actionpack/lib/action_controller/rack_process.rb deleted file mode 100644 index 8c6db91dd0..0000000000 --- a/actionpack/lib/action_controller/rack_process.rb +++ /dev/null @@ -1,73 +0,0 @@ -require 'action_controller/cgi_ext' - -module ActionController #:nodoc: - class RackRequest < AbstractRequest #:nodoc: - attr_accessor :session_options - - class SessionFixationAttempt < StandardError #:nodoc: - end - - def initialize(env) - @env = env - super() - end - - %w[ AUTH_TYPE GATEWAY_INTERFACE PATH_INFO - PATH_TRANSLATED REMOTE_HOST - REMOTE_IDENT REMOTE_USER SCRIPT_NAME - SERVER_NAME SERVER_PROTOCOL - - HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING - HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM - HTTP_NEGOTIATE HTTP_PRAGMA HTTP_REFERER HTTP_USER_AGENT ].each do |env| - define_method(env.sub(/^HTTP_/n, '').downcase) do - @env[env] - end - end - - def query_string - qs = super - if !qs.blank? - qs - else - @env['QUERY_STRING'] - end - end - - def body_stream #:nodoc: - @env['rack.input'] - end - - def key?(key) - @env.key?(key) - end - - def cookies - Rack::Request.new(@env).cookies - end - - def server_port - @env['SERVER_PORT'].to_i - end - - def server_software - @env['SERVER_SOFTWARE'].split("/").first - end - - def session_options - @env['rack.session.options'] ||= {} - end - - def session_options=(options) - @env['rack.session.options'] = options - end - - def session - @env['rack.session'] ||= {} - end - - def reset_session - @env['rack.session'] = {} - end - end -end diff --git a/actionpack/lib/action_controller/request.rb b/actionpack/lib/action_controller/request.rb index 087fffe87d..822955d1db 100755 --- a/actionpack/lib/action_controller/request.rb +++ b/actionpack/lib/action_controller/request.rb @@ -3,32 +3,48 @@ require 'stringio' require 'strscan' require 'active_support/memoizable' +require 'action_controller/cgi_ext' module ActionController # CgiRequest and TestRequest provide concrete implementations. - class AbstractRequest + class Request extend ActiveSupport::Memoizable - def self.relative_url_root=(relative_url_root) - ActiveSupport::Deprecation.warn( - "ActionController::AbstractRequest.relative_url_root= has been renamed." + - "You can now set it with config.action_controller.relative_url_root=", caller) - ActionController::Base.relative_url_root=relative_url_root + class SessionFixationAttempt < StandardError #:nodoc: end - HTTP_METHODS = %w(get head put post delete options) - HTTP_METHOD_LOOKUP = HTTP_METHODS.inject({}) { |h, m| h[m] = h[m.upcase] = m.to_sym; h } - # The hash of environment variables for this request, # such as { 'RAILS_ENV' => 'production' }. attr_reader :env + def initialize(env) + @env = env + end + + %w[ AUTH_TYPE GATEWAY_INTERFACE PATH_INFO + PATH_TRANSLATED REMOTE_HOST + REMOTE_IDENT REMOTE_USER SCRIPT_NAME + SERVER_NAME SERVER_PROTOCOL + + HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING + HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM + HTTP_NEGOTIATE HTTP_PRAGMA HTTP_REFERER HTTP_USER_AGENT ].each do |env| + define_method(env.sub(/^HTTP_/n, '').downcase) do + @env[env] + end + end + + def key?(key) + @env.key?(key) + end + + HTTP_METHODS = %w(get head put post delete options) + HTTP_METHOD_LOOKUP = HTTP_METHODS.inject({}) { |h, m| h[m] = h[m.upcase] = m.to_sym; h } + # The true HTTP request \method as a lowercase symbol, such as <tt>:get</tt>. # UnknownHttpMethod is raised for invalid methods not listed in ACCEPTED_HTTP_METHODS. def request_method method = @env['REQUEST_METHOD'] - method = parameters[:_method] if method == 'POST' && !parameters[:_method].blank? - HTTP_METHOD_LOOKUP[method] || raise(UnknownHttpMethod, "#{method}, accepted HTTP methods are #{HTTP_METHODS.to_sentence}") end memoize :request_method @@ -85,7 +101,7 @@ module ActionController # For backward compatibility, the post \format is extracted from the # X-Post-Data-Format HTTP header if present. def content_type - Mime::Type.lookup(content_type_without_parameters) + Mime::Type.lookup(parser.content_type_without_parameters) end memoize :content_type @@ -125,15 +141,15 @@ module ActionController # supplied, both must match, or the request is not considered fresh. def fresh?(response) case - when if_modified_since && if_none_match - not_modified?(response.last_modified) && etag_matches?(response.etag) - when if_modified_since - not_modified?(response.last_modified) - when if_none_match - etag_matches?(response.etag) - else - false - end + when if_modified_since && if_none_match + not_modified?(response.last_modified) && etag_matches?(response.etag) + when if_modified_since + not_modified?(response.last_modified) + when if_none_match + etag_matches?(response.etag) + else + false + end end # Returns the Mime type for the \format used in the request. @@ -248,7 +264,6 @@ EOM end memoize :server_software - # Returns the complete URL used for this request. def url protocol + host_with_port + request_uri @@ -271,7 +286,7 @@ EOM if forwarded = env["HTTP_X_FORWARDED_HOST"] forwarded.split(/,\s?/).last else - env['HTTP_HOST'] || env['SERVER_NAME'] || "#{env['SERVER_ADDR']}:#{env['SERVER_PORT']}" + env['HTTP_HOST'] || "#{env['SERVER_NAME'] || env['SERVER_ADDR']}:#{env['SERVER_PORT']}" end end @@ -332,11 +347,7 @@ EOM # Returns the query string, accounting for server idiosyncrasies. def query_string - if uri = @env['REQUEST_URI'] - uri.split('?', 2)[1] || '' - else - @env['QUERY_STRING'] || '' - end + @env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].split('?', 2)[1] || '') end memoize :query_string @@ -378,11 +389,7 @@ EOM # Read the request \body. This is useful for web services that need to # work with raw requests directly. def raw_post - unless env.include? 'RAW_POST_DATA' - env['RAW_POST_DATA'] = body.read(content_length) - body.rewind if body.respond_to?(:rewind) - end - env['RAW_POST_DATA'] + parser.raw_post end # Returns both GET and POST \parameters in a single hash. @@ -391,7 +398,7 @@ EOM end def path_parameters=(parameters) #:nodoc: - @path_parameters = parameters + @env["rack.routing_args"] = parameters @symbolized_path_parameters = @parameters = nil end @@ -407,18 +414,11 @@ EOM # # See <tt>symbolized_path_parameters</tt> for symbolized keys. def path_parameters - @path_parameters ||= {} + @env["rack.routing_args"] ||= {} end - # The request body is an IO input stream. If the RAW_POST_DATA environment - # variable is already set, wrap it in a StringIO. def body - if raw_post = env['RAW_POST_DATA'] - raw_post.force_encoding(Encoding::BINARY) if raw_post.respond_to?(:force_encoding) - StringIO.new(raw_post) - else - body_stream - end + parser.body end def remote_addr @@ -430,441 +430,53 @@ EOM end alias referer referrer - def query_parameters - @query_parameters ||= self.class.parse_query_parameters(query_string) + @query_parameters ||= parser.query_parameters end def request_parameters - @request_parameters ||= parse_formatted_request_parameters + @request_parameters ||= parser.request_parameters end - - #-- - # Must be implemented in the concrete request - #++ - def body_stream #:nodoc: + @env['rack.input'] end - def cookies #:nodoc: + def cookies + Rack::Request.new(@env).cookies end - def session #:nodoc: + def session + @env['rack.session'] ||= {} end def session=(session) #:nodoc: @session = session end - def reset_session #:nodoc: + def reset_session + @env['rack.session'] = {} end - protected - # The raw content type string. Use when you need parameters such as - # charset or boundary which aren't included in the content_type MIME type. - # Overridden by the X-POST_DATA_FORMAT header for backward compatibility. - def content_type_with_parameters - content_type_from_legacy_post_data_format_header || - env['CONTENT_TYPE'].to_s - end - - # The raw content type string with its parameters stripped off. - def content_type_without_parameters - self.class.extract_content_type_without_parameters(content_type_with_parameters) - end - memoize :content_type_without_parameters - - private - def content_type_from_legacy_post_data_format_header - if x_post_format = @env['HTTP_X_POST_DATA_FORMAT'] - case x_post_format.to_s.downcase - when 'yaml'; 'application/x-yaml' - when 'xml'; 'application/xml' - end - end - end - - def parse_formatted_request_parameters - return {} if content_length.zero? - - content_type, boundary = self.class.extract_multipart_boundary(content_type_with_parameters) - - # Don't parse params for unknown requests. - return {} if content_type.blank? - - mime_type = Mime::Type.lookup(content_type) - strategy = ActionController::Base.param_parsers[mime_type] - - # Only multipart form parsing expects a stream. - body = (strategy && strategy != :multipart_form) ? raw_post : self.body - - case strategy - when Proc - strategy.call(body) - when :url_encoded_form - self.class.clean_up_ajax_request_body! body - self.class.parse_query_parameters(body) - when :multipart_form - self.class.parse_multipart_form_parameters(body, boundary, content_length, env) - when :xml_simple, :xml_node - body.blank? ? {} : Hash.from_xml(body).with_indifferent_access - when :yaml - YAML.load(body) - when :json - if body.blank? - {} - else - data = ActiveSupport::JSON.decode(body) - data = {:_json => data} unless data.is_a?(Hash) - data.with_indifferent_access - end - else - {} - end - rescue Exception => e # YAML, XML or Ruby code block errors - raise - { "body" => body, - "content_type" => content_type_with_parameters, - "content_length" => content_length, - "exception" => "#{e.message} (#{e.class})", - "backtrace" => e.backtrace } - end - - def named_host?(host) - !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host)) - end - - class << self - def parse_query_parameters(query_string) - return {} if query_string.blank? - - pairs = query_string.split('&').collect do |chunk| - next if chunk.empty? - key, value = chunk.split('=', 2) - next if key.empty? - value = value.nil? ? nil : CGI.unescape(value) - [ CGI.unescape(key), value ] - end.compact - - UrlEncodedPairParser.new(pairs).result - end - - def parse_request_parameters(params) - parser = UrlEncodedPairParser.new - - params = params.dup - until params.empty? - for key, value in params - if key.blank? - params.delete key - elsif !key.include?('[') - # much faster to test for the most common case first (GET) - # and avoid the call to build_deep_hash - parser.result[key] = get_typed_value(value[0]) - params.delete key - elsif value.is_a?(Array) - parser.parse(key, get_typed_value(value.shift)) - params.delete key if value.empty? - else - raise TypeError, "Expected array, found #{value.inspect}" - end - end - end - - parser.result - end - - def parse_multipart_form_parameters(body, boundary, body_size, env) - parse_request_parameters(read_multipart(body, boundary, body_size, env)) - end - - def extract_multipart_boundary(content_type_with_parameters) - if content_type_with_parameters =~ MULTIPART_BOUNDARY - ['multipart/form-data', $1.dup] - else - extract_content_type_without_parameters(content_type_with_parameters) - end - end - - def extract_content_type_without_parameters(content_type_with_parameters) - $1.strip.downcase if content_type_with_parameters =~ /^([^,\;]*)/ - end - - def clean_up_ajax_request_body!(body) - body.chop! if body[-1] == 0 - body.gsub!(/&_=$/, '') - end - - - private - def get_typed_value(value) - case value - when String - value - when NilClass - '' - when Array - value.map { |v| get_typed_value(v) } - else - if value.respond_to? :original_filename - # Uploaded file - if value.original_filename - value - # Multipart param - else - result = value.read - value.rewind - result - end - # Unknown value, neither string nor multipart. - else - raise "Unknown form value: #{value.inspect}" - end - end - end - - MULTIPART_BOUNDARY = %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n - - EOL = "\015\012" - - def read_multipart(body, boundary, body_size, env) - params = Hash.new([]) - boundary = "--" + boundary - quoted_boundary = Regexp.quote(boundary) - buf = "" - bufsize = 10 * 1024 - boundary_end="" - - # start multipart/form-data - body.binmode if defined? body.binmode - case body - when File - body.set_encoding(Encoding::BINARY) if body.respond_to?(:set_encoding) - when StringIO - body.string.force_encoding(Encoding::BINARY) if body.string.respond_to?(:force_encoding) - end - boundary_size = boundary.size + EOL.size - body_size -= boundary_size - status = body.read(boundary_size) - if nil == status - raise EOFError, "no content body" - elsif boundary + EOL != status - raise EOFError, "bad content body" - end - - loop do - head = nil - content = - if 10240 < body_size - UploadedTempfile.new("CGI") - else - UploadedStringIO.new - end - content.binmode if defined? content.binmode - - until head and /#{quoted_boundary}(?:#{EOL}|--)/n.match(buf) - - if (not head) and /#{EOL}#{EOL}/n.match(buf) - buf = buf.sub(/\A((?:.|\n)*?#{EOL})#{EOL}/n) do - head = $1.dup - "" - end - next - end - - if head and ( (EOL + boundary + EOL).size < buf.size ) - content.print buf[0 ... (buf.size - (EOL + boundary + EOL).size)] - buf[0 ... (buf.size - (EOL + boundary + EOL).size)] = "" - end - - c = if bufsize < body_size - body.read(bufsize) - else - body.read(body_size) - end - if c.nil? || c.empty? - raise EOFError, "bad content body" - end - buf.concat(c) - body_size -= c.size - end - - buf = buf.sub(/\A((?:.|\n)*?)(?:[\r\n]{1,2})?#{quoted_boundary}([\r\n]{1,2}|--)/n) do - content.print $1 - if "--" == $2 - body_size = -1 - end - boundary_end = $2.dup - "" - end - - content.rewind - - head =~ /Content-Disposition:.* filename=(?:"((?:\\.|[^\"])*)"|([^;]*))/ni - if filename = $1 || $2 - if /Mac/ni.match(env['HTTP_USER_AGENT']) and - /Mozilla/ni.match(env['HTTP_USER_AGENT']) and - (not /MSIE/ni.match(env['HTTP_USER_AGENT'])) - filename = CGI.unescape(filename) - end - content.original_path = filename.dup - end - - head =~ /Content-Type: ([^\r]*)/ni - content.content_type = $1.dup if $1 - - head =~ /Content-Disposition:.* name="?([^\";]*)"?/ni - name = $1.dup if $1 - - if params.has_key?(name) - params[name].push(content) - else - params[name] = [content] - end - break if body_size == -1 - end - raise EOFError, "bad boundary end of body part" unless boundary_end=~/--/ - - begin - body.rewind if body.respond_to?(:rewind) - rescue Errno::ESPIPE - # Handles exceptions raised by input streams that cannot be rewound - # such as when using plain CGI under Apache - end - - params - end + def session_options + @env['rack.session.options'] ||= {} end - end - class UrlEncodedPairParser < StringScanner #:nodoc: - attr_reader :top, :parent, :result - - def initialize(pairs = []) - super('') - @result = {} - pairs.each { |key, value| parse(key, value) } + def session_options=(options) + @env['rack.session.options'] = options end - KEY_REGEXP = %r{([^\[\]=&]+)} - BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]} - - # Parse the query string - def parse(key, value) - self.string = key - @top, @parent = result, nil - - # First scan the bare key - key = scan(KEY_REGEXP) or return - key = post_key_check(key) - - # Then scan as many nestings as present - until eos? - r = scan(BRACKETED_KEY_REGEXP) or return - key = self[1] - key = post_key_check(key) - end - - bind(key, value) + def server_port + @env['SERVER_PORT'].to_i end private - # After we see a key, we must look ahead to determine our next action. Cases: - # - # [] follows the key. Then the value must be an array. - # = follows the key. (A value comes next) - # & or the end of string follows the key. Then the key is a flag. - # otherwise, a hash follows the key. - def post_key_check(key) - if scan(/\[\]/) # a[b][] indicates that b is an array - container(key, Array) - nil - elsif check(/\[[^\]]/) # a[b] indicates that a is a hash - container(key, Hash) - nil - else # End of key? We do nothing. - key - end - end - - # Add a container to the stack. - def container(key, klass) - type_conflict! klass, top[key] if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass) - value = bind(key, klass.new) - type_conflict! klass, value unless value.is_a?(klass) - push(value) - end - - # Push a value onto the 'stack', which is actually only the top 2 items. - def push(value) - @parent, @top = @top, value - end - - # Bind a key (which may be nil for items in an array) to the provided value. - def bind(key, value) - if top.is_a? Array - if key - if top[-1].is_a?(Hash) && ! top[-1].key?(key) - top[-1][key] = value - else - top << {key => value}.with_indifferent_access - push top.last - value = top[key] - end - else - top << value - end - elsif top.is_a? Hash - key = CGI.unescape(key) - parent << (@top = {}) if top.key?(key) && parent.is_a?(Array) - top[key] ||= value - return top[key] - else - raise ArgumentError, "Don't know what to do: top is #{top.inspect}" - end - - return value - end - - def type_conflict!(klass, value) - raise TypeError, "Conflicting types for parameter containers. Expected an instance of #{klass} but found an instance of #{value.class}. This can be caused by colliding Array and Hash parameters like qs[]=value&qs[key]=value. (The parameters received were #{value.inspect}.)" - end - end - - module UploadedFile - def self.included(base) - base.class_eval do - attr_accessor :original_path, :content_type - alias_method :local_path, :path + def named_host?(host) + !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host)) end - end - # Take the basename of the upload's original filename. - # This handles the full Windows paths given by Internet Explorer - # (and perhaps other broken user agents) without affecting - # those which give the lone filename. - # The Windows regexp is adapted from Perl's File::Basename. - def original_filename - unless defined? @original_filename - @original_filename = - unless original_path.blank? - if original_path =~ /^(?:.*[:\\\/])?(.*)/m - $1 - else - File.basename original_path - end - end + def parser + @parser ||= ActionController::RequestParser.new(@env) end - @original_filename - end - end - - class UploadedStringIO < StringIO - include UploadedFile - end - - class UploadedTempfile < Tempfile - include UploadedFile end end diff --git a/actionpack/lib/action_controller/request_parser.rb b/actionpack/lib/action_controller/request_parser.rb new file mode 100644 index 0000000000..82ee4c84c4 --- /dev/null +++ b/actionpack/lib/action_controller/request_parser.rb @@ -0,0 +1,314 @@ +module ActionController + class RequestParser + def initialize(env) + @env = env + end + + def request_parameters + @request_parameters ||= parse_formatted_request_parameters + end + + def query_parameters + @query_parameters ||= self.class.parse_query_parameters(query_string) + end + + # Returns the query string, accounting for server idiosyncrasies. + def query_string + @env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].split('?', 2)[1] || '') + end + + # The request body is an IO input stream. If the RAW_POST_DATA environment + # variable is already set, wrap it in a StringIO. + def body + if raw_post = @env['RAW_POST_DATA'] + raw_post.force_encoding(Encoding::BINARY) if raw_post.respond_to?(:force_encoding) + StringIO.new(raw_post) + else + @env['rack.input'] + end + end + + # The raw content type string with its parameters stripped off. + def content_type_without_parameters + self.class.extract_content_type_without_parameters(content_type_with_parameters) + end + + def raw_post + unless @env.include? 'RAW_POST_DATA' + @env['RAW_POST_DATA'] = body.read(content_length) + body.rewind if body.respond_to?(:rewind) + end + @env['RAW_POST_DATA'] + end + + private + + def parse_formatted_request_parameters + return {} if content_length.zero? + + content_type, boundary = self.class.extract_multipart_boundary(content_type_with_parameters) + + # Don't parse params for unknown requests. + return {} if content_type.blank? + + mime_type = Mime::Type.lookup(content_type) + strategy = ActionController::Base.param_parsers[mime_type] + + # Only multipart form parsing expects a stream. + body = (strategy && strategy != :multipart_form) ? raw_post : self.body + + case strategy + when Proc + strategy.call(body) + when :url_encoded_form + self.class.clean_up_ajax_request_body! body + self.class.parse_query_parameters(body) + when :multipart_form + self.class.parse_multipart_form_parameters(body, boundary, content_length, @env) + when :xml_simple, :xml_node + body.blank? ? {} : Hash.from_xml(body).with_indifferent_access + when :yaml + YAML.load(body) + when :json + if body.blank? + {} + else + data = ActiveSupport::JSON.decode(body) + data = {:_json => data} unless data.is_a?(Hash) + data.with_indifferent_access + end + else + {} + end + rescue Exception => e # YAML, XML or Ruby code block errors + raise + { "body" => body, + "content_type" => content_type_with_parameters, + "content_length" => content_length, + "exception" => "#{e.message} (#{e.class})", + "backtrace" => e.backtrace } + end + + def content_length + @content_length ||= @env['CONTENT_LENGTH'].to_i + end + + # The raw content type string. Use when you need parameters such as + # charset or boundary which aren't included in the content_type MIME type. + # Overridden by the X-POST_DATA_FORMAT header for backward compatibility. + def content_type_with_parameters + content_type_from_legacy_post_data_format_header || @env['CONTENT_TYPE'].to_s + end + + def content_type_from_legacy_post_data_format_header + if x_post_format = @env['HTTP_X_POST_DATA_FORMAT'] + case x_post_format.to_s.downcase + when 'yaml'; 'application/x-yaml' + when 'xml'; 'application/xml' + end + end + end + + class << self + def parse_query_parameters(query_string) + return {} if query_string.blank? + + pairs = query_string.split('&').collect do |chunk| + next if chunk.empty? + key, value = chunk.split('=', 2) + next if key.empty? + value = value.nil? ? nil : CGI.unescape(value) + [ CGI.unescape(key), value ] + end.compact + + UrlEncodedPairParser.new(pairs).result + end + + def parse_request_parameters(params) + parser = UrlEncodedPairParser.new + + params = params.dup + until params.empty? + for key, value in params + if key.blank? + params.delete key + elsif !key.include?('[') + # much faster to test for the most common case first (GET) + # and avoid the call to build_deep_hash + parser.result[key] = get_typed_value(value[0]) + params.delete key + elsif value.is_a?(Array) + parser.parse(key, get_typed_value(value.shift)) + params.delete key if value.empty? + else + raise TypeError, "Expected array, found #{value.inspect}" + end + end + end + + parser.result + end + + def parse_multipart_form_parameters(body, boundary, body_size, env) + parse_request_parameters(read_multipart(body, boundary, body_size, env)) + end + + def extract_multipart_boundary(content_type_with_parameters) + if content_type_with_parameters =~ MULTIPART_BOUNDARY + ['multipart/form-data', $1.dup] + else + extract_content_type_without_parameters(content_type_with_parameters) + end + end + + def extract_content_type_without_parameters(content_type_with_parameters) + $1.strip.downcase if content_type_with_parameters =~ /^([^,\;]*)/ + end + + def clean_up_ajax_request_body!(body) + body.chop! if body[-1] == 0 + body.gsub!(/&_=$/, '') + end + + + private + def get_typed_value(value) + case value + when String + value + when NilClass + '' + when Array + value.map { |v| get_typed_value(v) } + else + if value.respond_to? :original_filename + # Uploaded file + if value.original_filename + value + # Multipart param + else + result = value.read + value.rewind + result + end + # Unknown value, neither string nor multipart. + else + raise "Unknown form value: #{value.inspect}" + end + end + end + + MULTIPART_BOUNDARY = %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n + + EOL = "\015\012" + + def read_multipart(body, boundary, body_size, env) + params = Hash.new([]) + boundary = "--" + boundary + quoted_boundary = Regexp.quote(boundary) + buf = "" + bufsize = 10 * 1024 + boundary_end="" + + # start multipart/form-data + body.binmode if defined? body.binmode + case body + when File + body.set_encoding(Encoding::BINARY) if body.respond_to?(:set_encoding) + when StringIO + body.string.force_encoding(Encoding::BINARY) if body.string.respond_to?(:force_encoding) + end + boundary_size = boundary.size + EOL.size + body_size -= boundary_size + status = body.read(boundary_size) + if nil == status + raise EOFError, "no content body" + elsif boundary + EOL != status + raise EOFError, "bad content body" + end + + loop do + head = nil + content = + if 10240 < body_size + UploadedTempfile.new("CGI") + else + UploadedStringIO.new + end + content.binmode if defined? content.binmode + + until head and /#{quoted_boundary}(?:#{EOL}|--)/n.match(buf) + + if (not head) and /#{EOL}#{EOL}/n.match(buf) + buf = buf.sub(/\A((?:.|\n)*?#{EOL})#{EOL}/n) do + head = $1.dup + "" + end + next + end + + if head and ( (EOL + boundary + EOL).size < buf.size ) + content.print buf[0 ... (buf.size - (EOL + boundary + EOL).size)] + buf[0 ... (buf.size - (EOL + boundary + EOL).size)] = "" + end + + c = if bufsize < body_size + body.read(bufsize) + else + body.read(body_size) + end + if c.nil? || c.empty? + raise EOFError, "bad content body" + end + buf.concat(c) + body_size -= c.size + end + + buf = buf.sub(/\A((?:.|\n)*?)(?:[\r\n]{1,2})?#{quoted_boundary}([\r\n]{1,2}|--)/n) do + content.print $1 + if "--" == $2 + body_size = -1 + end + boundary_end = $2.dup + "" + end + + content.rewind + + head =~ /Content-Disposition:.* filename=(?:"((?:\\.|[^\"])*)"|([^;]*))/ni + if filename = $1 || $2 + if /Mac/ni.match(env['HTTP_USER_AGENT']) and + /Mozilla/ni.match(env['HTTP_USER_AGENT']) and + (not /MSIE/ni.match(env['HTTP_USER_AGENT'])) + filename = CGI.unescape(filename) + end + content.original_path = filename.dup + end + + head =~ /Content-Type: ([^\r]*)/ni + content.content_type = $1.dup if $1 + + head =~ /Content-Disposition:.* name="?([^\";]*)"?/ni + name = $1.dup if $1 + + if params.has_key?(name) + params[name].push(content) + else + params[name] = [content] + end + break if body_size == -1 + end + raise EOFError, "bad boundary end of body part" unless boundary_end=~/--/ + + begin + body.rewind if body.respond_to?(:rewind) + rescue Errno::ESPIPE + # Handles exceptions raised by input streams that cannot be rewound + # such as when using plain CGI under Apache + end + + params + end + end # class << self + end +end diff --git a/actionpack/lib/action_controller/rescue.rb b/actionpack/lib/action_controller/rescue.rb index 5ef79a36ce..3a5e5071bb 100644 --- a/actionpack/lib/action_controller/rescue.rb +++ b/actionpack/lib/action_controller/rescue.rb @@ -59,7 +59,9 @@ module ActionController #:nodoc: end module ClassMethods - def process_with_exception(request, response, exception) #:nodoc: + def call_with_exception(env, exception) #:nodoc: + request = env["actioncontroller.rescue.request"] + response = env["actioncontroller.rescue.response"] new.process(request, response, :rescue_action, exception) end end diff --git a/actionpack/lib/action_controller/response.rb b/actionpack/lib/action_controller/response.rb index 866616bac3..64319fe102 100644 --- a/actionpack/lib/action_controller/response.rb +++ b/actionpack/lib/action_controller/response.rb @@ -34,14 +34,14 @@ module ActionController # :nodoc: DEFAULT_HEADERS = { "Cache-Control" => "no-cache" } attr_accessor :request - attr_accessor :session, :cookies, :assigns, :template, :layout + attr_accessor :session, :assigns, :template, :layout attr_accessor :redirected_to, :redirected_to_method_params delegate :default_charset, :to => 'ActionController::Base' def initialize @status = 200 - @header = DEFAULT_HEADERS.merge("cookie" => []) + @header = DEFAULT_HEADERS.dup @writer = lambda { |x| @body << x } @block = nil @@ -143,10 +143,9 @@ module ActionController # :nodoc: handle_conditional_get! set_content_length! convert_content_type! - convert_language! convert_expires! - set_cookies! + convert_cookies! end def each(&callback) @@ -168,6 +167,35 @@ module ActionController # :nodoc: str end + # Over Rack::Response#set_cookie to add HttpOnly option + def set_cookie(key, value) + case value + when Hash + domain = "; domain=" + value[:domain] if value[:domain] + path = "; path=" + value[:path] if value[:path] + # According to RFC 2109, we need dashes here. + # N.B.: cgi.rb uses spaces... + expires = "; expires=" + value[:expires].clone.gmtime. + strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires] + secure = "; secure" if value[:secure] + httponly = "; HttpOnly" if value[:http_only] + value = value[:value] + end + value = [value] unless Array === value + cookie = ::Rack::Utils.escape(key) + "=" + + value.map { |v| ::Rack::Utils.escape v }.join("&") + + "#{domain}#{path}#{expires}#{secure}#{httponly}" + + case self["Set-Cookie"] + when Array + self["Set-Cookie"] << cookie + when String + self["Set-Cookie"] = [self["Set-Cookie"], cookie] + when nil + self["Set-Cookie"] = cookie + end + end + private def handle_conditional_get! if etag? || last_modified? @@ -217,22 +245,8 @@ module ActionController # :nodoc: headers["Expires"] = headers.delete("") if headers["expires"] end - def set_cookies! - # Convert 'cookie' header to 'Set-Cookie' headers. - # Because Set-Cookie header can appear more the once in the response body, - # we store it in a line break separated string that will be translated to - # multiple Set-Cookie header by the handler. - if cookie = headers.delete('cookie') - cookies = [] - - case cookie - when Array then cookie.each { |c| cookies << c.to_s } - when Hash then cookie.each { |_, c| cookies << c.to_s } - else cookies << cookie.to_s - end - - headers['Set-Cookie'] = [headers['Set-Cookie'], cookies].flatten.compact - end + def convert_cookies! + headers['Set-Cookie'] = Array(headers['Set-Cookie']).compact end end end diff --git a/actionpack/lib/action_controller/routing/route_set.rb b/actionpack/lib/action_controller/routing/route_set.rb index 13646aef61..06aef6e169 100644 --- a/actionpack/lib/action_controller/routing/route_set.rb +++ b/actionpack/lib/action_controller/routing/route_set.rb @@ -145,10 +145,10 @@ module ActionController def define_hash_access(route, name, kind, options) selector = hash_access_name(name, kind) named_helper_module_eval <<-end_eval # We use module_eval to avoid leaks - def #{selector}(options = nil) - options ? #{options.inspect}.merge(options) : #{options.inspect} - end - protected :#{selector} + def #{selector}(options = nil) # def hash_for_users_url(options = nil) + options ? #{options.inspect}.merge(options) : #{options.inspect} # options ? {:only_path=>false}.merge(options) : {:only_path=>false} + end # end + protected :#{selector} # protected :hash_for_users_url end_eval helpers << selector end @@ -173,32 +173,33 @@ module ActionController # foo_url(bar, baz, bang, :sort_by => 'baz') # named_helper_module_eval <<-end_eval # We use module_eval to avoid leaks - def #{selector}(*args) - - #{generate_optimisation_block(route, kind)} - - opts = if args.empty? || Hash === args.first - args.first || {} - else - options = args.extract_options! - args = args.zip(#{route.segment_keys.inspect}).inject({}) do |h, (v, k)| - h[k] = v - h - end - options.merge(args) - end - - url_for(#{hash_access_method}(opts)) - - end - #Add an alias to support the now deprecated formatted_* URL. - def formatted_#{selector}(*args) - ActiveSupport::Deprecation.warn( - "formatted_#{selector}() has been deprecated. please pass format to the standard" + - "#{selector}() method instead.", caller) - #{selector}(*args) - end - protected :#{selector} + def #{selector}(*args) # def users_url(*args) + # + #{generate_optimisation_block(route, kind)} # #{generate_optimisation_block(route, kind)} + # + opts = if args.empty? || Hash === args.first # opts = if args.empty? || Hash === args.first + args.first || {} # args.first || {} + else # else + options = args.extract_options! # options = args.extract_options! + args = args.zip(#{route.segment_keys.inspect}).inject({}) do |h, (v, k)| # args = args.zip([]).inject({}) do |h, (v, k)| + h[k] = v # h[k] = v + h # h + end # end + options.merge(args) # options.merge(args) + end # end + # + url_for(#{hash_access_method}(opts)) # url_for(hash_for_users_url(opts)) + # + end # end + #Add an alias to support the now deprecated formatted_* URL. # #Add an alias to support the now deprecated formatted_* URL. + def formatted_#{selector}(*args) # def formatted_users_url(*args) + ActiveSupport::Deprecation.warn( # ActiveSupport::Deprecation.warn( + "formatted_#{selector}() has been deprecated. " + # "formatted_users_url() has been deprecated. " + + "please pass format to the standard" + # "please pass format to the standard" + + "#{selector}() method instead.", caller) # "users_url() method instead.", caller) + #{selector}(*args) # users_url(*args) + end # end + protected :#{selector} # protected :users_url end_eval helpers << selector end @@ -426,6 +427,12 @@ module ActionController end end + def call(env) + request = Request.new(env) + app = Routing::Routes.recognize(request) + app.call(env).to_a + end + def recognize(request) params = recognize_path(request.path, extract_request_environment(request)) request.path_parameters = params.with_indifferent_access diff --git a/actionpack/lib/action_controller/streaming.rb b/actionpack/lib/action_controller/streaming.rb index 333fb61b45..e1786913a7 100644 --- a/actionpack/lib/action_controller/streaming.rb +++ b/actionpack/lib/action_controller/streaming.rb @@ -24,7 +24,8 @@ module ActionController #:nodoc: # Options: # * <tt>:filename</tt> - suggests a filename for the browser to use. # Defaults to <tt>File.basename(path)</tt>. - # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'. + # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'. You can specify + # either a string or a symbol for a registered type register with <tt>Mime::Type.register</tt>, for example :json # * <tt>:length</tt> - used to manually override the length (in bytes) of the content that # is going to be sent to the client. Defaults to <tt>File.size(path)</tt>. # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded. @@ -107,7 +108,8 @@ module ActionController #:nodoc: # # Options: # * <tt>:filename</tt> - suggests a filename for the browser to use. - # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'. + # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'. You can specify + # either a string or a symbol for a registered type register with <tt>Mime::Type.register</tt>, for example :json # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded. # Valid values are 'inline' and 'attachment' (default). # * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'. @@ -143,9 +145,16 @@ module ActionController #:nodoc: disposition <<= %(; filename="#{options[:filename]}") if options[:filename] + content_type = options[:type] + if content_type.is_a?(Symbol) + raise ArgumentError, "Unknown MIME type #{options[:type]}" unless Mime::EXTENSION_LOOKUP.has_key?(content_type.to_s) + content_type = Mime::Type.lookup_by_extension(content_type.to_s) + end + content_type = content_type.to_s.strip # fixes a problem with extra '\r' with some browsers + headers.update( 'Content-Length' => options[:length], - 'Content-Type' => options[:type].to_s.strip, # fixes a problem with extra '\r' with some browsers + 'Content-Type' => content_type, 'Content-Disposition' => disposition, 'Content-Transfer-Encoding' => 'binary' ) diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 79a8e1364d..7ed1a3e160 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -93,10 +93,7 @@ module ActionController # and cookies, though. For sessions, you just do: # # @request.session[:key] = "value" - # - # For cookies, you need to manually create the cookie, like this: - # - # @request.cookies["key"] = CGI::Cookie.new("key", "value") + # @request.cookies["key"] = "value" # # == Testing named routes # diff --git a/actionpack/lib/action_controller/test_process.rb b/actionpack/lib/action_controller/test_process.rb index c4d7d52951..285a8b09e4 100644 --- a/actionpack/lib/action_controller/test_process.rb +++ b/actionpack/lib/action_controller/test_process.rb @@ -27,20 +27,19 @@ module ActionController #:nodoc: alias_method_chain :process, :test end - class TestRequest < AbstractRequest #:nodoc: + class TestRequest < Request #:nodoc: attr_accessor :cookies, :session_options - attr_accessor :query_parameters, :request_parameters, :path, :session - attr_accessor :host, :user_agent + attr_accessor :query_parameters, :path, :session + attr_accessor :host - def initialize(query_parameters = nil, request_parameters = nil, session = nil) - @query_parameters = query_parameters || {} - @request_parameters = request_parameters || {} - @session = session || TestSession.new + def initialize + super(Rack::MockRequest.env_for("/")) - initialize_containers - initialize_default_values + @query_parameters = {} + @session = TestSession.new - super() + initialize_default_values + initialize_containers end def reset_session @@ -55,7 +54,11 @@ module ActionController #:nodoc: # Either the RAW_POST_DATA environment variable or the URL-encoded request # parameters. def raw_post - env['RAW_POST_DATA'] ||= returning(url_encoded_request_parameters) { |b| b.force_encoding(Encoding::BINARY) if b.respond_to?(:force_encoding) } + @env['RAW_POST_DATA'] ||= begin + data = url_encoded_request_parameters + data.force_encoding(Encoding::BINARY) if data.respond_to?(:force_encoding) + data + end end def port=(number) @@ -125,26 +128,30 @@ module ActionController #:nodoc: path_parameters[key.to_s] = value end end + raw_post # populate env['RAW_POST_DATA'] @parameters = nil # reset TestRequest#parameters to use the new path_parameters end def recycle! - self.request_parameters = {} self.query_parameters = {} self.path_parameters = {} unmemoize_all end + def user_agent=(user_agent) + @env['HTTP_USER_AGENT'] = user_agent + end + private def initialize_containers - @env, @cookies = {}, {} + @cookies = {} end def initialize_default_values @host = "test.host" @request_uri = "/" - @user_agent = "Rails Testing" - self.remote_addr = "0.0.0.0" + @env['HTTP_USER_AGENT'] = "Rails Testing" + @env['REMOTE_ADDR'] = "0.0.0.0" @env["SERVER_PORT"] = 80 @env['REQUEST_METHOD'] = "GET" end @@ -260,14 +267,14 @@ module ActionController #:nodoc: !template_objects[name].nil? end - # Returns the response cookies, converted to a Hash of (name => CGI::Cookie) pairs + # Returns the response cookies, converted to a Hash of (name => value) pairs # - # assert_equal ['AuthorOfNewPage'], r.cookies['author'].value + # assert_equal 'AuthorOfNewPage', r.cookies['author'] def cookies cookies = {} Array(headers['Set-Cookie']).each do |cookie| key, value = cookie.split(";").first.split("=") - cookies[key] = [value].compact + cookies[key] = value end cookies end @@ -377,20 +384,33 @@ module ActionController #:nodoc: module TestProcess def self.included(base) - # execute the request simulating a specific HTTP method and set/volley the response - # TODO: this should be un-DRY'ed for the sake of API documentation. - %w( get post put delete head ).each do |method| - base.class_eval <<-EOV, __FILE__, __LINE__ - def #{method}(action, parameters = nil, session = nil, flash = nil) - @request.env['REQUEST_METHOD'] = "#{method.upcase}" if defined?(@request) - process(action, parameters, session, flash) - end - EOV + # Executes a request simulating GET HTTP method and set/volley the response + def get(action, parameters = nil, session = nil, flash = nil) + process(action, parameters, session, flash, "GET") + end + + # Executes a request simulating POST HTTP method and set/volley the response + def post(action, parameters = nil, session = nil, flash = nil) + process(action, parameters, session, flash, "POST") + end + + # Executes a request simulating PUT HTTP method and set/volley the response + def put(action, parameters = nil, session = nil, flash = nil) + process(action, parameters, session, flash, "PUT") + end + + # Executes a request simulating DELETE HTTP method and set/volley the response + def delete(action, parameters = nil, session = nil, flash = nil) + process(action, parameters, session, flash, "DELETE") + end + + # Executes a request simulating HEAD HTTP method and set/volley the response + def head(action, parameters = nil, session = nil, flash = nil) + process(action, parameters, session, flash, "HEAD") end end - # execute the request and set/volley the response - def process(action, parameters = nil, session = nil, flash = nil) + def process(action, parameters = nil, session = nil, flash = nil, http_method = 'GET') # Sanity check for required instance variables so we can give an # understandable error message. %w(@controller @request @response).each do |iv_name| @@ -403,7 +423,7 @@ module ActionController #:nodoc: @response.recycle! @html_document = nil - @request.env['REQUEST_METHOD'] ||= "GET" + @request.env['REQUEST_METHOD'] = http_method @request.action = action.to_s diff --git a/actionpack/lib/action_controller/uploaded_file.rb b/actionpack/lib/action_controller/uploaded_file.rb new file mode 100644 index 0000000000..ea4845c68f --- /dev/null +++ b/actionpack/lib/action_controller/uploaded_file.rb @@ -0,0 +1,37 @@ +module ActionController + module UploadedFile + def self.included(base) + base.class_eval do + attr_accessor :original_path, :content_type + alias_method :local_path, :path + end + end + + # Take the basename of the upload's original filename. + # This handles the full Windows paths given by Internet Explorer + # (and perhaps other broken user agents) without affecting + # those which give the lone filename. + # The Windows regexp is adapted from Perl's File::Basename. + def original_filename + unless defined? @original_filename + @original_filename = + unless original_path.blank? + if original_path =~ /^(?:.*[:\\\/])?(.*)/m + $1 + else + File.basename original_path + end + end + end + @original_filename + end + end + + class UploadedStringIO < StringIO + include UploadedFile + end + + class UploadedTempfile < Tempfile + include UploadedFile + end +end diff --git a/actionpack/lib/action_controller/url_encoded_pair_parser.rb b/actionpack/lib/action_controller/url_encoded_pair_parser.rb new file mode 100644 index 0000000000..bea96c711d --- /dev/null +++ b/actionpack/lib/action_controller/url_encoded_pair_parser.rb @@ -0,0 +1,95 @@ +module ActionController + class UrlEncodedPairParser < StringScanner #:nodoc: + attr_reader :top, :parent, :result + + def initialize(pairs = []) + super('') + @result = {} + pairs.each { |key, value| parse(key, value) } + end + + KEY_REGEXP = %r{([^\[\]=&]+)} + BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]} + + # Parse the query string + def parse(key, value) + self.string = key + @top, @parent = result, nil + + # First scan the bare key + key = scan(KEY_REGEXP) or return + key = post_key_check(key) + + # Then scan as many nestings as present + until eos? + r = scan(BRACKETED_KEY_REGEXP) or return + key = self[1] + key = post_key_check(key) + end + + bind(key, value) + end + + private + # After we see a key, we must look ahead to determine our next action. Cases: + # + # [] follows the key. Then the value must be an array. + # = follows the key. (A value comes next) + # & or the end of string follows the key. Then the key is a flag. + # otherwise, a hash follows the key. + def post_key_check(key) + if scan(/\[\]/) # a[b][] indicates that b is an array + container(key, Array) + nil + elsif check(/\[[^\]]/) # a[b] indicates that a is a hash + container(key, Hash) + nil + else # End of key? We do nothing. + key + end + end + + # Add a container to the stack. + def container(key, klass) + type_conflict! klass, top[key] if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass) + value = bind(key, klass.new) + type_conflict! klass, value unless value.is_a?(klass) + push(value) + end + + # Push a value onto the 'stack', which is actually only the top 2 items. + def push(value) + @parent, @top = @top, value + end + + # Bind a key (which may be nil for items in an array) to the provided value. + def bind(key, value) + if top.is_a? Array + if key + if top[-1].is_a?(Hash) && ! top[-1].key?(key) + top[-1][key] = value + else + top << {key => value}.with_indifferent_access + push top.last + value = top[key] + end + else + top << value + end + elsif top.is_a? Hash + key = CGI.unescape(key) + parent << (@top = {}) if top.key?(key) && parent.is_a?(Array) + top[key] ||= value + return top[key] + else + raise ArgumentError, "Don't know what to do: top is #{top.inspect}" + end + + return value + end + + def type_conflict!(klass, value) + raise TypeError, "Conflicting types for parameter containers. Expected an instance of #{klass} but found an instance of #{value.class}. This can be caused by colliding Array and Hash parameters like qs[]=value&qs[key]=value. (The parameters received were #{value.inspect}.)" + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_controller/verb_piggybacking.rb b/actionpack/lib/action_controller/verb_piggybacking.rb new file mode 100644 index 0000000000..86cde304a0 --- /dev/null +++ b/actionpack/lib/action_controller/verb_piggybacking.rb @@ -0,0 +1,24 @@ +module ActionController + # TODO: Use Rack::MethodOverride when it is released + class VerbPiggybacking + HTTP_METHODS = %w(GET HEAD PUT POST DELETE OPTIONS) + + def initialize(app) + @app = app + end + + def call(env) + if env["REQUEST_METHOD"] == "POST" + req = Request.new(env) + if method = (req.parameters[:_method] || env["HTTP_X_HTTP_METHOD_OVERRIDE"]) + method = method.to_s.upcase + if HTTP_METHODS.include?(method) + env["REQUEST_METHOD"] = method + end + end + end + + @app.call(env) + end + end +end diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index 33517ffb7b..8958e61e9d 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -3,7 +3,10 @@ module ActionView #:nodoc: end class MissingTemplate < ActionViewError #:nodoc: + attr_reader :path + def initialize(paths, path, template_format = nil) + @path = path full_template_path = path.include?('.') ? path : "#{path}.erb" display_paths = paths.compact.join(":") template_type = (path =~ /layouts/i) ? 'layout' : 'template' @@ -172,17 +175,6 @@ module ActionView #:nodoc: delegate :logger, :to => 'ActionController::Base' end - # Templates that are exempt from layouts - @@exempt_from_layout = Set.new([/\.rjs$/]) - - # Don't render layouts for templates with the given extensions. - def self.exempt_from_layout(*extensions) - regexps = extensions.collect do |extension| - extension.is_a?(Regexp) ? extension : /\.#{Regexp.escape(extension.to_s)}$/ - end - @@exempt_from_layout.merge(regexps) - end - @@debug_rjs = false ## # :singleton-method: @@ -190,12 +182,6 @@ module ActionView #:nodoc: # that alert()s the caught exception (and then re-raises it). cattr_accessor :debug_rjs - @@warn_cache_misses = false - ## - # :singleton-method: - # A warning will be displayed whenever an action results in a cache miss on your view paths. - cattr_accessor :warn_cache_misses - attr_internal :request delegate :request_forgery_protection_token, :template, :params, :session, :cookies, :response, :headers, @@ -257,7 +243,8 @@ module ActionView #:nodoc: if options[:layout] _render_with_layout(options, local_assigns, &block) elsif options[:file] - _pick_template(options[:file]).render_template(self, options[:locals]) + tempalte = self.view_paths.find_template(options[:file], template_format) + tempalte.render_template(self, options[:locals]) elsif options[:partial] render_partial(options) elsif options[:inline] @@ -315,45 +302,6 @@ module ActionView #:nodoc: end end - def _pick_template(template_path) - return template_path if template_path.respond_to?(:render) - - path = template_path.sub(/^\//, '') - if m = path.match(/(.*)\.(\w+)$/) - template_file_name, template_file_extension = m[1], m[2] - else - template_file_name = path - end - - # OPTIMIZE: Checks to lookup template in view path - if template = self.view_paths.find_template(template_file_name, template_format) - template - elsif (first_render = @_render_stack.first) && first_render.respond_to?(:format_and_extension) && - (template = self.view_paths["#{template_file_name}.#{first_render.format_and_extension}"]) - template - else - template = Template.new(template_path, view_paths) - - if self.class.warn_cache_misses && logger - logger.debug "[PERFORMANCE] Rendering a template that was " + - "not found in view path. Templates outside the view path are " + - "not cached and result in expensive disk operations. Move this " + - "file into #{view_paths.join(':')} or add the folder to your " + - "view path list" - end - - template - end - end - memoize :_pick_template - - def _exempt_from_layout?(template_path) #:nodoc: - template = _pick_template(template_path).to_s - @@exempt_from_layout.any? { |ext| template =~ ext } - rescue ActionView::MissingTemplate - return false - end - def _render_with_layout(options, local_assigns, &block) #:nodoc: partial_layout = options.delete(:layout) diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index a04bb8c598..4305617ac8 100644 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -136,6 +136,10 @@ module ActionView # dates. # * <tt>:default</tt> - Set a default date if the affected date isn't set or is nil. # * <tt>:disabled</tt> - Set to true if you want show the select fields as disabled. + # * <tt>:prompt</tt> - Set to true (for a generic prompt), a prompt string or a hash of prompt strings + # for <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>, <tt>:minute</tt> and <tt>:second</tt>. + # Setting this option prepends a select option with a generic prompt (Day, Month, Year, Hour, Minute, Seconds) + # or the given prompt string. # # If anything is passed in the +html_options+ hash it will be applied to every select tag in the set. # @@ -171,6 +175,9 @@ module ActionView # # that will have a default day of 20. # date_select("credit_card", "bill_due", :default => { :day => 20 }) # + # # Generates a date select with custom prompts + # date_select("post", "written_on", :prompt => { :day => 'Select day', :month => 'Select month', :year => 'Select year' }) + # # The selects are prepared for multi-parameter assignment to an Active Record object. # # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that @@ -210,6 +217,11 @@ module ActionView # # You can set the :minute_step to 15 which will give you: 00, 15, 30 and 45. # time_select 'game', 'game_time', {:minute_step => 15} # + # # Creates a time select tag with a custom prompt. Use :prompt => true for generic prompts. + # time_select("post", "written_on", :prompt => {:hour => 'Choose hour', :minute => 'Choose minute', :second => 'Choose seconds'}) + # time_select("post", "written_on", :prompt => {:hour => true}) # generic prompt for hours + # time_select("post", "written_on", :prompt => true) # generic prompts for all + # # The selects are prepared for multi-parameter assignment to an Active Record object. # # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that @@ -241,6 +253,11 @@ module ActionView # # as the written_on attribute. # datetime_select("post", "written_on", :discard_type => true) # + # # Generates a datetime select with a custom prompt. Use :prompt=>true for generic prompts. + # datetime_select("post", "written_on", :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year'}) + # datetime_select("post", "written_on", :prompt => {:hour => true}) # generic prompt for hours + # datetime_select("post", "written_on", :prompt => true) # generic prompts for all + # # The selects are prepared for multi-parameter assignment to an Active Record object. def datetime_select(object_name, method, options = {}, html_options = {}) InstanceTag.new(object_name, method, self, options.delete(:object)).to_datetime_select_tag(options, html_options) @@ -285,6 +302,11 @@ module ActionView # # prefixed with 'payday' rather than 'date' # select_datetime(my_date_time, :prefix => 'payday') # + # # Generates a datetime select with a custom prompt. Use :prompt=>true for generic prompts. + # select_datetime(my_date_time, :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year'}) + # select_datetime(my_date_time, :prompt => {:hour => true}) # generic prompt for hours + # select_datetime(my_date_time, :prompt => true) # generic prompts for all + # def select_datetime(datetime = Time.current, options = {}, html_options = {}) DateTimeSelector.new(datetime, options, html_options).select_datetime end @@ -321,6 +343,11 @@ module ActionView # # prefixed with 'payday' rather than 'date' # select_date(my_date, :prefix => 'payday') # + # # Generates a date select with a custom prompt. Use :prompt=>true for generic prompts. + # select_date(my_date, :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year'}) + # select_date(my_date, :prompt => {:hour => true}) # generic prompt for hours + # select_date(my_date, :prompt => true) # generic prompts for all + # def select_date(date = Date.current, options = {}, html_options = {}) DateTimeSelector.new(date, options, html_options).select_date end @@ -352,6 +379,11 @@ module ActionView # # separated by ':' and includes an input for seconds # select_time(my_time, :time_separator => ':', :include_seconds => true) # + # # Generates a time select with a custom prompt. Use :prompt=>true for generic prompts. + # select_time(my_time, :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year'}) + # select_time(my_time, :prompt => {:hour => true}) # generic prompt for hours + # select_time(my_time, :prompt => true) # generic prompts for all + # def select_time(datetime = Time.current, options = {}, html_options = {}) DateTimeSelector.new(datetime, options, html_options).select_time end @@ -373,6 +405,10 @@ module ActionView # # that is named 'interval' rather than 'second' # select_second(my_time, :field_name => 'interval') # + # # Generates a select field for seconds with a custom prompt. Use :prompt=>true for a + # # generic prompt. + # select_minute(14, :prompt => 'Choose seconds') + # def select_second(datetime, options = {}, html_options = {}) DateTimeSelector.new(datetime, options, html_options).select_second end @@ -395,6 +431,10 @@ module ActionView # # that is named 'stride' rather than 'second' # select_minute(my_time, :field_name => 'stride') # + # # Generates a select field for minutes with a custom prompt. Use :prompt=>true for a + # # generic prompt. + # select_minute(14, :prompt => 'Choose minutes') + # def select_minute(datetime, options = {}, html_options = {}) DateTimeSelector.new(datetime, options, html_options).select_minute end @@ -416,6 +456,10 @@ module ActionView # # that is named 'stride' rather than 'second' # select_hour(my_time, :field_name => 'stride') # + # # Generates a select field for hours with a custom prompt. Use :prompt => true for a + # # generic prompt. + # select_hour(13, :prompt =>'Choose hour') + # def select_hour(datetime, options = {}, html_options = {}) DateTimeSelector.new(datetime, options, html_options).select_hour end @@ -437,6 +481,10 @@ module ActionView # # that is named 'due' rather than 'day' # select_day(my_time, :field_name => 'due') # + # # Generates a select field for days with a custom prompt. Use :prompt => true for a + # # generic prompt. + # select_day(5, :prompt => 'Choose day') + # def select_day(date, options = {}, html_options = {}) DateTimeSelector.new(date, options, html_options).select_day end @@ -475,6 +523,10 @@ module ActionView # # will use keys like "Januar", "Marts." # select_month(Date.today, :use_month_names => %w(Januar Februar Marts ...)) # + # # Generates a select field for months with a custom prompt. Use :prompt => true for a + # # generic prompt. + # select_month(14, :prompt => 'Choose month') + # def select_month(date, options = {}, html_options = {}) DateTimeSelector.new(date, options, html_options).select_month end @@ -502,6 +554,10 @@ module ActionView # # has ascending year values # select_year(2006, :start_year => 2000, :end_year => 2010) # + # # Generates a select field for years with a custom prompt. Use :prompt => true for a + # # generic prompt. + # select_year(14, :prompt => 'Choose year') + # def select_year(date, options = {}, html_options = {}) DateTimeSelector.new(date, options, html_options).select_year end @@ -764,11 +820,30 @@ module ActionView select_html = "\n" select_html << content_tag(:option, '', :value => '') + "\n" if @options[:include_blank] + select_html << prompt_option_tag(type, @options[:prompt]) + "\n" if @options[:prompt] select_html << select_options_as_html.to_s content_tag(:select, select_html, select_options) + "\n" end + # Builds a prompt option tag with supplied options or from default options + # prompt_option_tag(:month, :prompt => 'Select month') + # => "<option value="">Select month</option>" + def prompt_option_tag(type, options) + default_options = {:year => false, :month => false, :day => false, :hour => false, :minute => false, :second => false} + + case options + when Hash + prompt = default_options.merge(options)[type.to_sym] + when String + prompt = options + else + prompt = I18n.translate(('datetime.prompts.' + type.to_s).to_sym, :locale => @options[:locale]) + end + + prompt ? content_tag(:option, prompt, :value => '') : '' + end + # Builds hidden input tag for date part and value # build_hidden(:year, 2008) # => "<input id="post_written_on_1i" name="post[written_on(1i)]" type="hidden" value="2008" />" diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index 621e2946b5..a85751c657 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -737,9 +737,13 @@ module ActionView (field_helpers - %w(label check_box radio_button fields_for)).each do |selector| src = <<-end_src - def #{selector}(method, options = {}) - @template.send(#{selector.inspect}, @object_name, method, objectify_options(options)) - end + def #{selector}(method, options = {}) # def text_field(method, options = {}) + @template.send( # @template.send( + #{selector.inspect}, # "text_field", + @object_name, # @object_name, + method, # method, + objectify_options(options)) # objectify_options(options)) + end # end end_src class_eval src, __FILE__, __LINE__ end diff --git a/actionpack/lib/action_view/helpers/prototype_helper.rb b/actionpack/lib/action_view/helpers/prototype_helper.rb index 7fab3102e7..18a209dcea 100644 --- a/actionpack/lib/action_view/helpers/prototype_helper.rb +++ b/actionpack/lib/action_view/helpers/prototype_helper.rb @@ -531,11 +531,6 @@ module ActionView # is shorthand for # :with => "'name=' + value" # This essentially just changes the key of the parameter. - # <tt>:on</tt>:: Specifies which event handler to observe. By default, - # it's set to "changed" for text fields and areas and - # "click" for radio buttons and checkboxes. With this, - # you can specify it instead to be "blur" or "focus" or - # any other event. # # Additionally, you may specify any of the options documented in the # <em>Common options</em> section at the top of this document. @@ -548,11 +543,6 @@ module ActionView # :url => 'http://example.com/books/edit/1', # :with => 'title' # - # # Sends params: {:book_title => 'Title of the book'} when the focus leaves - # # the input field. - # observe_field 'book_title', - # :url => 'http://example.com/books/edit/1', - # :on => 'blur' # def observe_field(field_id, options = {}) if options[:frequency] && options[:frequency] > 0 @@ -1094,7 +1084,6 @@ module ActionView javascript << "#{options[:frequency]}, " if options[:frequency] javascript << "function(element, value) {" javascript << "#{callback}}" - javascript << ", '#{options[:on]}'" if options[:on] javascript << ")" javascript_tag(javascript) end diff --git a/actionpack/lib/action_view/locale/en.yml b/actionpack/lib/action_view/locale/en.yml index 9542b035aa..a880fd83ef 100644 --- a/actionpack/lib/action_view/locale/en.yml +++ b/actionpack/lib/action_view/locale/en.yml @@ -80,6 +80,13 @@ over_x_years: one: "over 1 year" other: "over {{count}} years" + prompts: + year: "Year" + month: "Month" + day: "Day" + hour: "Hour" + minute: "Minute" + second: "Seconds" activerecord: errors: diff --git a/actionpack/lib/action_view/partials.rb b/actionpack/lib/action_view/partials.rb index bbc995a340..59e82b98a4 100644 --- a/actionpack/lib/action_view/partials.rb +++ b/actionpack/lib/action_view/partials.rb @@ -228,7 +228,7 @@ module ActionView path = "_#{partial_path}" end - _pick_template(path) + self.view_paths.find_template(path, self.template_format) end memoize :_pick_partial_template end diff --git a/actionpack/lib/action_view/paths.rb b/actionpack/lib/action_view/paths.rb index 623b9ff6b0..b030156889 100644 --- a/actionpack/lib/action_view/paths.rb +++ b/actionpack/lib/action_view/paths.rb @@ -2,13 +2,6 @@ module ActionView #:nodoc: class PathSet < Array #:nodoc: def self.type_cast(obj) if obj.is_a?(String) - if Base.warn_cache_misses && defined?(Rails) && Rails.initialized? - Base.logger.debug "[PERFORMANCE] Processing view path during a " + - "request. This an expense disk operation that should be done at " + - "boot. You can manually process this view path with " + - "ActionView::Base.process_view_paths(#{obj.inspect}) and set it " + - "as your view path" - end Path.new(obj) else obj @@ -92,7 +85,7 @@ module ActionView #:nodoc: else Dir.glob("#{@path}/#{path}*").each do |file| template = create_template(file) - if path == template.path_without_extension || path == template.path + if template.accessible_paths.include?(path) return template end end @@ -115,8 +108,9 @@ module ActionView #:nodoc: templates_in_path do |template| template.load! - @paths[template.path] = template - @paths[template.path_without_extension] ||= template + template.accessible_paths.each do |path| + @paths[path] = template + end end @paths.freeze @@ -143,28 +137,19 @@ module ActionView #:nodoc: each { |path| path.reload! } end - def [](template_path) - each do |path| - if template = path[template_path] - return template - end - end - nil - end + def find_template(original_template_path, format = nil) + return original_template_path if original_template_path.respond_to?(:render) + template_path = original_template_path.sub(/^\//, '') - def find_template(path, *formats) - if formats && formats.first == :all - formats = Mime::EXTENSION_LOOKUP.values.map(&:to_sym) - end - formats.each do |format| - if template = self["#{path}.#{format}"] + each do |load_path| + if format && (template = load_path["#{template_path}.#{format}"]) + return template + elsif template = load_path[template_path] return template end end - if template = self[path] - return template - end - nil + + Template.new(original_template_path, self) end end end diff --git a/actionpack/lib/action_view/renderable.rb b/actionpack/lib/action_view/renderable.rb index 7c0e62f1d7..d8e72f1179 100644 --- a/actionpack/lib/action_view/renderable.rb +++ b/actionpack/lib/action_view/renderable.rb @@ -4,10 +4,6 @@ module ActionView module Renderable #:nodoc: extend ActiveSupport::Memoizable - def self.included(base) - @@mutex = Mutex.new - end - def filename 'compiled-template' end @@ -22,6 +18,11 @@ module ActionView end memoize :compiled_source + def method_name_without_locals + ['_run', extension, method_segment].compact.join('_') + end + memoize :method_name_without_locals + def render(view, local_assigns = {}) compile(local_assigns) @@ -46,9 +47,12 @@ module ActionView def method_name(local_assigns) if local_assigns && local_assigns.any? - local_assigns_keys = "locals_#{local_assigns.keys.map { |k| k.to_s }.sort.join('_')}" + method_name = method_name_without_locals.dup + method_name << "_locals_#{local_assigns.keys.map { |k| k.to_s }.sort.join('_')}" + else + method_name = method_name_without_locals end - ['_run', extension, method_segment, local_assigns_keys].compact.join('_').to_sym + method_name.to_sym end private @@ -56,10 +60,8 @@ module ActionView def compile(local_assigns) render_symbol = method_name(local_assigns) - @@mutex.synchronize do - if recompile?(render_symbol) - compile!(render_symbol, local_assigns) - end + if recompile?(render_symbol) + compile!(render_symbol, local_assigns) end end diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index 93748638c3..5b384d0e4d 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -4,6 +4,17 @@ module ActionView #:nodoc: extend ActiveSupport::Memoizable include Renderable + # Templates that are exempt from layouts + @@exempt_from_layout = Set.new([/\.rjs$/]) + + # Don't render layouts for templates with the given extensions. + def self.exempt_from_layout(*extensions) + regexps = extensions.collect do |extension| + extension.is_a?(Regexp) ? extension : /\.#{Regexp.escape(extension.to_s)}$/ + end + @@exempt_from_layout.merge(regexps) + end + attr_accessor :filename, :load_path, :base_path, :name, :format, :extension delegate :to_s, :to => :path @@ -17,6 +28,18 @@ module ActionView #:nodoc: extend RenderablePartial if @name =~ /^_/ end + def accessible_paths + paths = [] + paths << path + paths << path_without_extension + if multipart? + formats = format.split(".") + paths << "#{path_without_format_and_extension}.#{formats.first}" + paths << "#{path_without_format_and_extension}.#{formats.second}" + end + paths + end + def format_and_extension (extensions = [format, extension].compact.join(".")).blank? ? nil : extensions end @@ -57,6 +80,10 @@ module ActionView #:nodoc: end memoize :relative_path + def exempt_from_layout? + @@exempt_from_layout.any? { |exempted| path =~ exempted } + end + def mtime File.mtime(filename) end @@ -94,6 +121,7 @@ module ActionView #:nodoc: def load! @loaded = true + compile({}) freeze end diff --git a/actionpack/test/controller/assert_select_test.rb b/actionpack/test/controller/assert_select_test.rb index ed8c4427c9..99c57c0c91 100644 --- a/actionpack/test/controller/assert_select_test.rb +++ b/actionpack/test/controller/assert_select_test.rb @@ -248,6 +248,14 @@ class AssertSelectTest < ActionController::TestCase end end + def test_assert_select_rjs_for_positioned_insert_should_fail_when_mixing_arguments + render_rjs do |page| + page.insert_html :top, "test1", "<div id=\"1\">foo</div>" + page.insert_html :bottom, "test2", "<div id=\"2\">foo</div>" + end + assert_raises(Assertion) {assert_select_rjs :insert, :top, "test2"} + end + # # Test css_select. # diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index e24bb00bc7..7f8e47ba58 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -121,8 +121,7 @@ class PageCachingTest < ActionController::TestCase [:get, :post, :put, :delete].each do |method| unless method == :get and status == :ok define_method "test_shouldnt_cache_#{method}_with_#{status}_status" do - @request.env['REQUEST_METHOD'] = method.to_s.upcase - process status + send(method, status) assert_response status assert_page_not_cached status, "#{method} with #{status} status shouldn't have been cached" end diff --git a/actionpack/test/controller/cookie_test.rb b/actionpack/test/controller/cookie_test.rb index 4b969519c6..3ddc5768a9 100644 --- a/actionpack/test/controller/cookie_test.rb +++ b/actionpack/test/controller/cookie_test.rb @@ -7,15 +7,15 @@ class CookieTest < Test::Unit::TestCase end def authenticate_for_fourteen_days - cookies["user_name"] = { "value" => "david", "expires" => Time.local(2005, 10, 10) } + cookies["user_name"] = { "value" => "david", "expires" => Time.utc(2005, 10, 10,5) } end def authenticate_for_fourteen_days_with_symbols - cookies[:user_name] = { :value => "david", :expires => Time.local(2005, 10, 10) } + cookies[:user_name] = { :value => "david", :expires => Time.utc(2005, 10, 10,5) } end def set_multiple_cookies - cookies["user_name"] = { "value" => "david", "expires" => Time.local(2005, 10, 10) } + cookies["user_name"] = { "value" => "david", "expires" => Time.utc(2005, 10, 10,5) } cookies["login"] = "XJ-122" end @@ -52,33 +52,33 @@ class CookieTest < Test::Unit::TestCase def test_setting_cookie get :authenticate assert_equal ["user_name=david; path=/"], @response.headers["Set-Cookie"] - assert_equal({"user_name" => ["david"]}, @response.cookies) + assert_equal({"user_name" => "david"}, @response.cookies) end def test_setting_cookie_for_fourteen_days get :authenticate_for_fourteen_days - assert_equal ["user_name=david; path=/; expires=Mon, 10 Oct 2005 05:00:00 GMT"], @response.headers["Set-Cookie"] - assert_equal({"user_name" => ["david"]}, @response.cookies) + assert_equal ["user_name=david; path=/; expires=Mon, 10-Oct-2005 05:00:00 GMT"], @response.headers["Set-Cookie"] + assert_equal({"user_name" => "david"}, @response.cookies) end def test_setting_cookie_for_fourteen_days_with_symbols get :authenticate_for_fourteen_days_with_symbols - assert_equal ["user_name=david; path=/; expires=Mon, 10 Oct 2005 05:00:00 GMT"], @response.headers["Set-Cookie"] - assert_equal({"user_name" => ["david"]}, @response.cookies) + assert_equal ["user_name=david; path=/; expires=Mon, 10-Oct-2005 05:00:00 GMT"], @response.headers["Set-Cookie"] + assert_equal({"user_name" => "david"}, @response.cookies) end def test_setting_cookie_with_http_only get :authenticate_with_http_only assert_equal ["user_name=david; path=/; HttpOnly"], @response.headers["Set-Cookie"] - assert_equal({"user_name" => ["david"]}, @response.cookies) + assert_equal({"user_name" => "david"}, @response.cookies) end def test_multiple_cookies get :set_multiple_cookies assert_equal 2, @response.cookies.size - assert_equal "user_name=david; path=/; expires=Mon, 10 Oct 2005 05:00:00 GMT", @response.headers["Set-Cookie"][0] + assert_equal "user_name=david; path=/; expires=Mon, 10-Oct-2005 05:00:00 GMT", @response.headers["Set-Cookie"][0] assert_equal "login=XJ-122; path=/", @response.headers["Set-Cookie"][1] - assert_equal({"login" => ["XJ-122"], "user_name" => ["david"]}, @response.cookies) + assert_equal({"login" => "XJ-122", "user_name" => "david"}, @response.cookies) end def test_setting_test_cookie @@ -87,12 +87,12 @@ class CookieTest < Test::Unit::TestCase def test_expiring_cookie get :logout - assert_equal ["user_name=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"], @response.headers["Set-Cookie"] - assert_equal({"user_name" => []}, @response.cookies) + assert_equal ["user_name=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT"], @response.headers["Set-Cookie"] + assert_equal({"user_name" => nil}, @response.cookies) end def test_cookiejar_accessor - @request.cookies["user_name"] = CGI::Cookie.new("name" => "user_name", "value" => "david", "expires" => Time.local(2025, 10, 10)) + @request.cookies["user_name"] = "david" @controller.request = @request jar = ActionController::CookieJar.new(@controller) assert_equal "david", jar["user_name"] @@ -100,52 +100,14 @@ class CookieTest < Test::Unit::TestCase end def test_cookiejar_accessor_with_array_value - a = %w{1 2 3} - @request.cookies["pages"] = CGI::Cookie.new("name" => "pages", "value" => a, "expires" => Time.local(2025, 10, 10)) + @request.cookies["pages"] = %w{1 2 3} @controller.request = @request jar = ActionController::CookieJar.new(@controller) - assert_equal a, jar["pages"] + assert_equal %w{1 2 3}, jar["pages"] end def test_delete_cookie_with_path get :delete_cookie_with_path - assert_equal ["user_name=; path=/beaten; expires=Thu, 01 Jan 1970 00:00:00 GMT"], @response.headers["Set-Cookie"] - end - - def test_cookie_to_s_simple_values - assert_equal 'myname=myvalue; path=', CGI::Cookie.new('myname', 'myvalue').to_s - end - - def test_cookie_to_s_hash - cookie_str = CGI::Cookie.new( - 'name' => 'myname', - 'value' => 'myvalue', - 'domain' => 'mydomain', - 'path' => 'mypath', - 'expires' => Time.utc(2007, 10, 20), - 'secure' => true, - 'http_only' => true).to_s - assert_equal 'myname=myvalue; domain=mydomain; path=mypath; expires=Sat, 20 Oct 2007 00:00:00 GMT; secure; HttpOnly', cookie_str - end - - def test_cookie_to_s_hash_default_not_secure_not_http_only - cookie_str = CGI::Cookie.new( - 'name' => 'myname', - 'value' => 'myvalue', - 'domain' => 'mydomain', - 'path' => 'mypath', - 'expires' => Time.utc(2007, 10, 20)) - assert cookie_str !~ /secure/ - assert cookie_str !~ /HttpOnly/ - end - - def test_cookies_should_not_be_split_on_ampersand_values - cookies = CGI::Cookie.parse('return_to=http://rubyonrails.org/search?term=api&scope=all&global=true') - assert_equal({"return_to" => ["http://rubyonrails.org/search?term=api&scope=all&global=true"]}, cookies) - end - - def test_cookies_should_not_be_split_on_values_with_newlines - cookies = CGI::Cookie.new("name" => "val", "value" => "this\nis\na\ntest") - assert cookies.size == 1 + assert_equal ["user_name=; path=/beaten; expires=Thu, 01-Jan-1970 00:00:00 GMT"], @response.headers["Set-Cookie"] end end diff --git a/actionpack/test/controller/dispatcher_test.rb b/actionpack/test/controller/dispatcher_test.rb index fd06b4ea99..da87d26146 100644 --- a/actionpack/test/controller/dispatcher_test.rb +++ b/actionpack/test/controller/dispatcher_test.rb @@ -96,9 +96,7 @@ class DispatcherTest < Test::Unit::TestCase private def dispatch(cache_classes = true) - controller = mock() - controller.stubs(:process).returns([200, {}, 'response']) - ActionController::Routing::Routes.stubs(:recognize).returns(controller) + ActionController::Routing::RouteSet.any_instance.stubs(:call).returns([200, {}, 'response']) Dispatcher.define_dispatcher_callbacks(cache_classes) @dispatcher.call({}) end diff --git a/actionpack/test/controller/http_digest_authentication_test.rb b/actionpack/test/controller/http_digest_authentication_test.rb new file mode 100644 index 0000000000..d5c8636a9e --- /dev/null +++ b/actionpack/test/controller/http_digest_authentication_test.rb @@ -0,0 +1,73 @@ +require 'abstract_unit' + +class HttpDigestAuthenticationTest < Test::Unit::TestCase + include ActionController::HttpAuthentication::Digest + + class DummyController + attr_accessor :headers, :renders, :request, :response + + def initialize + @headers, @renders = {}, [] + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + request.session.session_id = "test_session" + end + + def render(options) + self.renderers << options + end + end + + def setup + @controller = DummyController.new + @credentials = { + :username => "dhh", + :realm => "testrealm@host.com", + :nonce => ActionController::HttpAuthentication::Digest.nonce(@controller.request), + :qop => "auth", + :nc => "00000001", + :cnonce => "0a4f113b", + :opaque => ActionController::HttpAuthentication::Digest.opaque(@controller.request), + :uri => "http://test.host/" + } + @encoded_credentials = ActionController::HttpAuthentication::Digest.encode_credentials("GET", @credentials, "secret") + end + + def test_decode_credentials + set_headers + assert_equal @credentials, decode_credentials(@controller.request) + end + + def test_nonce_format + assert_nothing_thrown do + validate_nonce(@controller.request, nonce(@controller.request)) + end + end + + def test_authenticate_should_raise_for_nil_password + set_headers ActionController::HttpAuthentication::Digest.encode_credentials(:get, @credentials, nil) + assert_raise ActionController::HttpAuthentication::Error do + authenticate(@controller, @credentials[:realm]) { |user| user == "dhh" && "secret" } + end + end + + def test_authenticate_should_raise_for_incorrect_password + set_headers + assert_raise ActionController::HttpAuthentication::Error do + authenticate(@controller, @credentials[:realm]) { |user| user == "dhh" && "bad password" } + end + end + + def test_authenticate_should_not_raise_for_correct_password + set_headers + assert_nothing_thrown do + authenticate(@controller, @credentials[:realm]) { |user| user == "dhh" && "secret" } + end + end + + private + def set_headers(value = @encoded_credentials, name = 'HTTP_AUTHORIZATION', method = "GET") + @controller.request.env[name] = value + @controller.request.env["REQUEST_METHOD"] = method + end +end diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index c28050fe0d..53cebf768e 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -8,7 +8,25 @@ class SessionTest < Test::Unit::TestCase } def setup + @credentials = { + :username => "username", + :realm => "MyApp", + :nonce => ActionController::HttpAuthentication::Digest.nonce("session_id"), + :qop => "auth", + :nc => "00000001", + :cnonce => "0a4f113b", + :opaque => ActionController::HttpAuthentication::Digest.opaque("session_id"), + :uri => "/index" + } + @session = ActionController::Integration::Session.new(StubApp) + @session.nonce = @credentials[:nonce] + @session.opaque = @credentials[:opaque] + @session.realm = @credentials[:realm] + end + + def encoded_credentials(method) + ActionController::HttpAuthentication::Digest.encode_credentials(method, @credentials, "password") end def test_https_bang_works_and_sets_truth_by_default @@ -132,6 +150,76 @@ class SessionTest < Test::Unit::TestCase @session.head(path,params,headers) end + def test_get_with_basic + path = "/index"; params = "blah"; headers = {:location => 'blah'} + expected_headers = headers.merge(:authorization => "Basic dXNlcm5hbWU6cGFzc3dvcmQ=\n") + @session.expects(:process).with(:get,path,params,expected_headers) + @session.get_with_basic(path,params,headers,'username','password') + end + + def test_post_with_basic + path = "/index"; params = "blah"; headers = {:location => 'blah'} + expected_headers = headers.merge(:authorization => "Basic dXNlcm5hbWU6cGFzc3dvcmQ=\n") + @session.expects(:process).with(:post,path,params,expected_headers) + @session.post_with_basic(path,params,headers,'username','password') + end + + def test_put_with_basic + path = "/index"; params = "blah"; headers = {:location => 'blah'} + expected_headers = headers.merge(:authorization => "Basic dXNlcm5hbWU6cGFzc3dvcmQ=\n") + @session.expects(:process).with(:put,path,params,expected_headers) + @session.put_with_basic(path,params,headers,'username','password') + end + + def test_delete_with_basic + path = "/index"; params = "blah"; headers = {:location => 'blah'} + expected_headers = headers.merge(:authorization => "Basic dXNlcm5hbWU6cGFzc3dvcmQ=\n") + @session.expects(:process).with(:delete,path,params,expected_headers) + @session.delete_with_basic(path,params,headers,'username','password') + end + + def test_head_with_basic + path = "/index"; params = "blah"; headers = {:location => 'blah'} + expected_headers = headers.merge(:authorization => "Basic dXNlcm5hbWU6cGFzc3dvcmQ=\n") + @session.expects(:process).with(:head,path,params,expected_headers) + @session.head_with_basic(path,params,headers,'username','password') + end + + def test_get_with_digest + path = "/index"; params = "blah"; headers = {:location => 'blah'} + expected_headers = headers.merge(:authorization => encoded_credentials(:get)) + @session.expects(:process).with(:get,path,params,expected_headers) + @session.get_with_digest(path,params,headers,'username','password') + end + + def test_post_with_digest + path = "/index"; params = "blah"; headers = {:location => 'blah'} + expected_headers = headers.merge(:authorization => encoded_credentials(:post)) + @session.expects(:process).with(:post,path,params,expected_headers) + @session.post_with_digest(path,params,headers,'username','password') + end + + def test_put_with_digest + path = "/index"; params = "blah"; headers = {:location => 'blah'} + expected_headers = headers.merge(:authorization => encoded_credentials(:put)) + @session.expects(:process).with(:put,path,params,expected_headers) + @session.put_with_digest(path,params,headers,'username','password') + end + + def test_delete_with_digest + path = "/index"; params = "blah"; headers = {:location => 'blah'} + expected_headers = headers.merge(:authorization => encoded_credentials(:delete)) + @session.expects(:process).with(:delete,path,params,expected_headers) + @session.delete_with_digest(path,params,headers,'username','password') + end + + def test_head_with_digest + path = "/index"; params = "blah"; headers = {:location => 'blah'} + expected_headers = headers.merge(:authorization => encoded_credentials(:head)) + @session.expects(:process).with(:head,path,params,expected_headers) + @session.head_with_digest(path,params,headers,'username','password') + end + def test_xml_http_request_get path = "/index"; params = "blah"; headers = {:location => 'blah'} headers_after_xhr = headers.merge( diff --git a/actionpack/test/controller/layout_test.rb b/actionpack/test/controller/layout_test.rb index 18c01f755c..c2efe9d00b 100644 --- a/actionpack/test/controller/layout_test.rb +++ b/actionpack/test/controller/layout_test.rb @@ -165,15 +165,17 @@ class LayoutStatusIsRenderedTest < ActionController::TestCase end end -class LayoutSymlinkedTest < LayoutTest - layout "symlinked/symlinked_layout" -end - -class LayoutSymlinkedIsRenderedTest < ActionController::TestCase - def test_symlinked_layout_is_rendered - @controller = LayoutSymlinkedTest.new - get :hello - assert_response 200 - assert_equal "layouts/symlinked/symlinked_layout", @response.layout +unless RUBY_PLATFORM =~ /(:?mswin|mingw|bccwin)/ + class LayoutSymlinkedTest < LayoutTest + layout "symlinked/symlinked_layout" + end + + class LayoutSymlinkedIsRenderedTest < ActionController::TestCase + def test_symlinked_layout_is_rendered + @controller = LayoutSymlinkedTest.new + get :hello + assert_response 200 + assert_equal "layouts/symlinked/symlinked_layout", @response.layout + end end end diff --git a/actionpack/test/controller/rack_test.rb b/actionpack/test/controller/rack_test.rb index 81d103f0f9..31bff4ae6d 100644 --- a/actionpack/test/controller/rack_test.rb +++ b/actionpack/test/controller/rack_test.rb @@ -4,7 +4,7 @@ class BaseRackTest < Test::Unit::TestCase def setup @env = { "HTTP_MAX_FORWARDS" => "10", - "SERVER_NAME" => "glu.ttono.us:8007", + "SERVER_NAME" => "glu.ttono.us", "FCGI_ROLE" => "RESPONDER", "AUTH_TYPE" => "Basic", "HTTP_X_FORWARDED_HOST" => "glu.ttono.us", @@ -43,10 +43,10 @@ class BaseRackTest < Test::Unit::TestCase "REDIRECT_STATUS" => "200", "REQUEST_METHOD" => "GET" } - @request = ActionController::RackRequest.new(@env) + @request = ActionController::Request.new(@env) # some Nokia phone browsers omit the space after the semicolon separator. # some developers have grown accustomed to using comma in cookie values. - @alt_cookie_fmt_request = ActionController::RackRequest.new(@env.merge({"HTTP_COOKIE"=>"_session_id=c84ace847,96670c052c6ceb2451fb0f2;is_admin=yes"})) + @alt_cookie_fmt_request = ActionController::Request.new(@env.merge({"HTTP_COOKIE"=>"_session_id=c84ace847,96670c052c6ceb2451fb0f2;is_admin=yes"})) end def default_test; end @@ -145,7 +145,7 @@ class RackRequestTest < BaseRackTest assert_equal "kevin", @request.remote_user assert_equal :get, @request.request_method assert_equal "/dispatch.fcgi", @request.script_name - assert_equal "glu.ttono.us:8007", @request.server_name + assert_equal "glu.ttono.us", @request.server_name assert_equal 8007, @request.server_port assert_equal "HTTP/1.1", @request.server_protocol assert_equal "lighttpd", @request.server_software @@ -187,29 +187,6 @@ class RackRequestContentTypeTest < BaseRackTest end end -class RackRequestMethodTest < BaseRackTest - def test_get - assert_equal :get, @request.request_method - end - - def test_post - @request.env['REQUEST_METHOD'] = 'POST' - assert_equal :post, @request.request_method - end - - def test_put - set_content_data '_method=put' - - assert_equal :put, @request.request_method - end - - def test_delete - set_content_data '_method=delete' - - assert_equal :delete, @request.request_method - end -end - class RackRequestNeedsRewoundTest < BaseRackTest def test_body_should_be_rewound data = 'foo' @@ -218,7 +195,7 @@ class RackRequestNeedsRewoundTest < BaseRackTest @env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded; charset=utf-8' # Read the request body by parsing params. - request = ActionController::RackRequest.new(@env) + request = ActionController::Request.new(@env) request.request_parameters # Should have rewound the body. diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index 8e08a5a8e9..5fd41d8eec 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -21,6 +21,8 @@ class MockLogger end class TestController < ActionController::Base + protect_from_forgery + class LabellingFormBuilder < ActionView::Helpers::FormBuilder end @@ -79,6 +81,10 @@ class TestController < ActionController::Base render :action => "hello_world" end + def render_action_hello_world_as_string + render "hello_world" + end + def render_action_hello_world_with_symbol render :action => :hello_world end @@ -102,6 +108,12 @@ class TestController < ActionController::Base render :file => path end + def render_file_as_string_with_instance_variables + @secret = 'in the sauce' + path = File.expand_path(File.join(File.dirname(__FILE__), '../fixtures/test/render_file_with_ivar.erb')) + render path + end + def render_file_not_using_full_path @secret = 'in the sauce' render :file => 'test/render_file_with_ivar' @@ -122,6 +134,11 @@ class TestController < ActionController::Base render :file => path, :locals => {:secret => 'in the sauce'} end + def render_file_as_string_with_locals + path = File.expand_path(File.join(File.dirname(__FILE__), '../fixtures/test/render_file_with_locals.erb')) + render path, :locals => {:secret => 'in the sauce'} + end + def accessing_request_in_template render :inline => "Hello: <%= request.host %>" end @@ -180,10 +197,6 @@ class TestController < ActionController::Base render :text => "appended" end - def render_invalid_args - render("test/hello") - end - def render_vanilla_js_hello render :js => "alert('hello')" end @@ -193,6 +206,11 @@ class TestController < ActionController::Base render :template => "test/hello" end + def render_xml_hello_as_string_template + @name = "David" + render "test/hello" + end + def render_xml_with_custom_content_type render :xml => "<blah/>", :content_type => "application/atomsvc+xml" end @@ -282,6 +300,14 @@ class TestController < ActionController::Base render :action => "hello_world", :layout => "standard" end + def layout_test_with_different_layout_and_string_action + render "hello_world", :layout => "standard" + end + + def layout_test_with_different_layout_and_symbol_action + render :hello_world, :layout => "standard" + end + def rendering_without_layout render :action => "hello_world", :layout => false end @@ -323,6 +349,10 @@ class TestController < ActionController::Base render :template => "test/hello_world" end + def render_with_explicit_string_template + render "test/hello_world" + end + def render_with_explicit_template_with_locals render :template => "test/render_file_with_locals", :locals => { :secret => 'area51' } end @@ -645,6 +675,7 @@ class TestController < ActionController::Base "accessing_params_in_template", "accessing_params_in_template_with_layout", "render_with_explicit_template", + "render_with_explicit_string_template", "render_js_with_explicit_template", "render_js_with_explicit_action_template", "delete_with_js", "update_page", "update_page_with_instance_variables" @@ -724,6 +755,12 @@ class RenderTest < ActionController::TestCase assert_template "test/hello_world" end + def test_render_action_hello_world_as_string + get :render_action_hello_world_as_string + assert_equal "Hello world!", @response.body + assert_template "test/hello_world" + end + def test_render_action_with_symbol get :render_action_hello_world_with_symbol assert_template "test/hello_world" @@ -749,6 +786,11 @@ class RenderTest < ActionController::TestCase assert_equal "The secret is in the sauce\n", @response.body end + def test_render_file_as_string_with_instance_variables + get :render_file_as_string_with_instance_variables + assert_equal "The secret is in the sauce\n", @response.body + end + def test_render_file_not_using_full_path get :render_file_not_using_full_path assert_equal "The secret is in the sauce\n", @response.body @@ -764,6 +806,11 @@ class RenderTest < ActionController::TestCase assert_equal "The secret is in the sauce\n", @response.body end + def test_render_file_as_string_with_locals + get :render_file_as_string_with_locals + assert_equal "The secret is in the sauce\n", @response.body + end + def test_render_file_from_template get :render_file_from_template assert_equal "The secret is in the sauce\n", @response.body @@ -829,10 +876,6 @@ class RenderTest < ActionController::TestCase assert_equal 'appended', @response.body end - def test_attempt_to_render_with_invalid_arguments - assert_raises(ActionController::RenderError) { get :render_invalid_args } - end - def test_attempt_to_access_object_method assert_raises(ActionController::UnknownAction, "No action responded to [clone]") { get :clone } end @@ -873,6 +916,12 @@ class RenderTest < ActionController::TestCase assert_equal "application/xml", @response.content_type end + def test_render_xml_as_string_template + get :render_xml_hello_as_string_template + assert_equal "<html>\n <p>Hello David</p>\n<p>This is grand!</p>\n</html>\n", @response.body + assert_equal "application/xml", @response.content_type + end + def test_render_xml_with_default get :greeting assert_equal "<p>This is grand!</p>\n", @response.body @@ -1012,6 +1061,16 @@ class RenderTest < ActionController::TestCase assert_equal "<html>Hello world!</html>", @response.body end + def test_layout_test_with_different_layout_and_string_action + get :layout_test_with_different_layout_and_string_action + assert_equal "<html>Hello world!</html>", @response.body + end + + def test_layout_test_with_different_layout_and_symbol_action + get :layout_test_with_different_layout_and_symbol_action + assert_equal "<html>Hello world!</html>", @response.body + end + def test_rendering_without_layout get :rendering_without_layout assert_equal "Hello world!", @response.body @@ -1058,6 +1117,11 @@ class RenderTest < ActionController::TestCase assert_response :success end + def test_render_with_explicit_string_template + get :render_with_explicit_string_template + assert_equal "<html>Hello world!</html>", @response.body + end + def test_double_render assert_raises(ActionController::DoubleRenderError) { get :double_render } end diff --git a/actionpack/test/controller/request_test.rb b/actionpack/test/controller/request_test.rb index 71da50fab0..349cea268f 100644 --- a/actionpack/test/controller/request_test.rb +++ b/actionpack/test/controller/request_test.rb @@ -303,18 +303,16 @@ class RequestTest < ActiveSupport::TestCase end def test_allow_method_hacking_on_post - self.request_method = :post [:get, :head, :options, :put, :post, :delete].each do |method| - @request.instance_eval { @parameters = { :_method => method.to_s } ; @request_method = nil } + self.request_method = method @request.request_method(true) assert_equal(method == :head ? :get : method, @request.method) end end def test_invalid_method_hacking_on_post_raises_exception - self.request_method = :post - @request.instance_eval { @parameters = { :_method => :random_method } ; @request_method = nil } assert_raises(ActionController::UnknownHttpMethod) do + self.request_method = :_random_method @request.request_method(true) end end @@ -426,95 +424,95 @@ class UrlEncodedRequestParameterParsingTest < ActiveSupport::TestCase def test_query_string assert_equal( { "action" => "create_customer", "full_name" => "David Heinemeier Hansson", "customerId" => "1"}, - ActionController::AbstractRequest.parse_query_parameters(@query_string) + ActionController::RequestParser.parse_query_parameters(@query_string) ) end def test_deep_query_string expected = {'x' => {'y' => {'z' => '10'}}} - assert_equal(expected, ActionController::AbstractRequest.parse_query_parameters('x[y][z]=10')) + assert_equal(expected, ActionController::RequestParser.parse_query_parameters('x[y][z]=10')) end def test_deep_query_string_with_array - assert_equal({'x' => {'y' => {'z' => ['10']}}}, ActionController::AbstractRequest.parse_query_parameters('x[y][z][]=10')) - assert_equal({'x' => {'y' => {'z' => ['10', '5']}}}, ActionController::AbstractRequest.parse_query_parameters('x[y][z][]=10&x[y][z][]=5')) + assert_equal({'x' => {'y' => {'z' => ['10']}}}, ActionController::RequestParser.parse_query_parameters('x[y][z][]=10')) + assert_equal({'x' => {'y' => {'z' => ['10', '5']}}}, ActionController::RequestParser.parse_query_parameters('x[y][z][]=10&x[y][z][]=5')) end def test_deep_query_string_with_array_of_hash - assert_equal({'x' => {'y' => [{'z' => '10'}]}}, ActionController::AbstractRequest.parse_query_parameters('x[y][][z]=10')) - assert_equal({'x' => {'y' => [{'z' => '10', 'w' => '10'}]}}, ActionController::AbstractRequest.parse_query_parameters('x[y][][z]=10&x[y][][w]=10')) + assert_equal({'x' => {'y' => [{'z' => '10'}]}}, ActionController::RequestParser.parse_query_parameters('x[y][][z]=10')) + assert_equal({'x' => {'y' => [{'z' => '10', 'w' => '10'}]}}, ActionController::RequestParser.parse_query_parameters('x[y][][z]=10&x[y][][w]=10')) end def test_deep_query_string_with_array_of_hashes_with_one_pair - assert_equal({'x' => {'y' => [{'z' => '10'}, {'z' => '20'}]}}, ActionController::AbstractRequest.parse_query_parameters('x[y][][z]=10&x[y][][z]=20')) - assert_equal("10", ActionController::AbstractRequest.parse_query_parameters('x[y][][z]=10&x[y][][z]=20')["x"]["y"].first["z"]) - assert_equal("10", ActionController::AbstractRequest.parse_query_parameters('x[y][][z]=10&x[y][][z]=20').with_indifferent_access[:x][:y].first[:z]) + assert_equal({'x' => {'y' => [{'z' => '10'}, {'z' => '20'}]}}, ActionController::RequestParser.parse_query_parameters('x[y][][z]=10&x[y][][z]=20')) + assert_equal("10", ActionController::RequestParser.parse_query_parameters('x[y][][z]=10&x[y][][z]=20')["x"]["y"].first["z"]) + assert_equal("10", ActionController::RequestParser.parse_query_parameters('x[y][][z]=10&x[y][][z]=20').with_indifferent_access[:x][:y].first[:z]) end def test_deep_query_string_with_array_of_hashes_with_multiple_pairs assert_equal( {'x' => {'y' => [{'z' => '10', 'w' => 'a'}, {'z' => '20', 'w' => 'b'}]}}, - ActionController::AbstractRequest.parse_query_parameters('x[y][][z]=10&x[y][][w]=a&x[y][][z]=20&x[y][][w]=b') + ActionController::RequestParser.parse_query_parameters('x[y][][z]=10&x[y][][w]=a&x[y][][z]=20&x[y][][w]=b') ) end def test_query_string_with_nil assert_equal( { "action" => "create_customer", "full_name" => ''}, - ActionController::AbstractRequest.parse_query_parameters(@query_string_with_empty) + ActionController::RequestParser.parse_query_parameters(@query_string_with_empty) ) end def test_query_string_with_array assert_equal( { "action" => "create_customer", "selected" => ["1", "2", "3"]}, - ActionController::AbstractRequest.parse_query_parameters(@query_string_with_array) + ActionController::RequestParser.parse_query_parameters(@query_string_with_array) ) end def test_query_string_with_amps assert_equal( { "action" => "create_customer", "name" => "Don't & Does"}, - ActionController::AbstractRequest.parse_query_parameters(@query_string_with_amps) + ActionController::RequestParser.parse_query_parameters(@query_string_with_amps) ) end def test_query_string_with_many_equal assert_equal( { "action" => "create_customer", "full_name" => "abc=def=ghi"}, - ActionController::AbstractRequest.parse_query_parameters(@query_string_with_many_equal) + ActionController::RequestParser.parse_query_parameters(@query_string_with_many_equal) ) end def test_query_string_without_equal assert_equal( { "action" => nil }, - ActionController::AbstractRequest.parse_query_parameters(@query_string_without_equal) + ActionController::RequestParser.parse_query_parameters(@query_string_without_equal) ) end def test_query_string_with_empty_key assert_equal( { "action" => "create_customer", "full_name" => "David Heinemeier Hansson" }, - ActionController::AbstractRequest.parse_query_parameters(@query_string_with_empty_key) + ActionController::RequestParser.parse_query_parameters(@query_string_with_empty_key) ) end def test_query_string_with_many_ampersands assert_equal( { "action" => "create_customer", "full_name" => "David Heinemeier Hansson"}, - ActionController::AbstractRequest.parse_query_parameters(@query_string_with_many_ampersands) + ActionController::RequestParser.parse_query_parameters(@query_string_with_many_ampersands) ) end def test_unbalanced_query_string_with_array assert_equal( {'location' => ["1", "2"], 'age_group' => ["2"]}, - ActionController::AbstractRequest.parse_query_parameters("location[]=1&location[]=2&age_group[]=2") + ActionController::RequestParser.parse_query_parameters("location[]=1&location[]=2&age_group[]=2") ) assert_equal( {'location' => ["1", "2"], 'age_group' => ["2"]}, - ActionController::AbstractRequest.parse_request_parameters({'location[]' => ["1", "2"], + ActionController::RequestParser.parse_request_parameters({'location[]' => ["1", "2"], 'age_group[]' => ["2"]}) ) end @@ -527,7 +525,7 @@ class UrlEncodedRequestParameterParsingTest < ActiveSupport::TestCase expected = { "note" => { "viewers"=>{"viewer"=>[{ "id"=>"1", "type"=>"User"}, {"type"=>"Group", "id"=>"2"} ]} } } - assert_equal(expected, ActionController::AbstractRequest.parse_request_parameters(query)) + assert_equal(expected, ActionController::RequestParser.parse_request_parameters(query)) end def test_parse_params @@ -566,7 +564,7 @@ class UrlEncodedRequestParameterParsingTest < ActiveSupport::TestCase } } - assert_equal expected_output, ActionController::AbstractRequest.parse_request_parameters(input) + assert_equal expected_output, ActionController::RequestParser.parse_request_parameters(input) end UploadedStringIO = ActionController::UploadedStringIO @@ -621,7 +619,7 @@ class UrlEncodedRequestParameterParsingTest < ActiveSupport::TestCase "text_part" => "abc" } - params = ActionController::AbstractRequest.parse_request_parameters(input) + params = ActionController::RequestParser.parse_request_parameters(input) assert_equal expected_output, params # Lone filenames are preserved. @@ -652,7 +650,7 @@ class UrlEncodedRequestParameterParsingTest < ActiveSupport::TestCase "logo" => File.new(File.dirname(__FILE__) + "/rack_test.rb").path, } - assert_equal expected_output, ActionController::AbstractRequest.parse_request_parameters(input) + assert_equal expected_output, ActionController::RequestParser.parse_request_parameters(input) end def test_parse_params_with_array @@ -660,55 +658,55 @@ class UrlEncodedRequestParameterParsingTest < ActiveSupport::TestCase expected_output = { "selected" => [ "1", "2", "3" ] } - assert_equal expected_output, ActionController::AbstractRequest.parse_request_parameters(input) + assert_equal expected_output, ActionController::RequestParser.parse_request_parameters(input) end def test_parse_params_with_non_alphanumeric_name input = { "a/b[c]" => %w(d) } expected = { "a/b" => { "c" => "d" }} - assert_equal expected, ActionController::AbstractRequest.parse_request_parameters(input) + assert_equal expected, ActionController::RequestParser.parse_request_parameters(input) end def test_parse_params_with_single_brackets_in_middle input = { "a/b[c]d" => %w(e) } expected = { "a/b" => {} } - assert_equal expected, ActionController::AbstractRequest.parse_request_parameters(input) + assert_equal expected, ActionController::RequestParser.parse_request_parameters(input) end def test_parse_params_with_separated_brackets input = { "a/b@[c]d[e]" => %w(f) } expected = { "a/b@" => { }} - assert_equal expected, ActionController::AbstractRequest.parse_request_parameters(input) + assert_equal expected, ActionController::RequestParser.parse_request_parameters(input) end def test_parse_params_with_separated_brackets_and_array input = { "a/b@[c]d[e][]" => %w(f) } expected = { "a/b@" => { }} - assert_equal expected , ActionController::AbstractRequest.parse_request_parameters(input) + assert_equal expected , ActionController::RequestParser.parse_request_parameters(input) end def test_parse_params_with_unmatched_brackets_and_array input = { "a/b@[c][d[e][]" => %w(f) } expected = { "a/b@" => { "c" => { }}} - assert_equal expected, ActionController::AbstractRequest.parse_request_parameters(input) + assert_equal expected, ActionController::RequestParser.parse_request_parameters(input) end def test_parse_params_with_nil_key input = { nil => nil, "test2" => %w(value1) } expected = { "test2" => "value1" } - assert_equal expected, ActionController::AbstractRequest.parse_request_parameters(input) + assert_equal expected, ActionController::RequestParser.parse_request_parameters(input) end def test_parse_params_with_array_prefix_and_hashes input = { "a[][b][c]" => %w(d) } expected = {"a" => [{"b" => {"c" => "d"}}]} - assert_equal expected, ActionController::AbstractRequest.parse_request_parameters(input) + assert_equal expected, ActionController::RequestParser.parse_request_parameters(input) end def test_parse_params_with_complex_nesting input = { "a[][b][c][][d][]" => %w(e) } expected = {"a" => [{"b" => {"c" => [{"d" => ["e"]}]}}]} - assert_equal expected, ActionController::AbstractRequest.parse_request_parameters(input) + assert_equal expected, ActionController::RequestParser.parse_request_parameters(input) end end @@ -770,7 +768,7 @@ class MultipartRequestParameterParsingTest < ActiveSupport::TestCase # Ensures that parse_multipart_form_parameters works with streams that cannot be rewound file = File.open(File.join(FIXTURE_PATH, 'large_text_file'), 'rb') file.expects(:rewind).raises(Errno::ESPIPE) - params = ActionController::AbstractRequest.parse_multipart_form_parameters(file, 'AaB03x', file.stat.size, {}) + params = ActionController::RequestParser.parse_multipart_form_parameters(file, 'AaB03x', file.stat.size, {}) assert_not_equal 0, file.pos # file was not rewound after reading end end @@ -809,7 +807,7 @@ class MultipartRequestParameterParsingTest < ActiveSupport::TestCase private def parse_multipart(name) File.open(File.join(FIXTURE_PATH, name), 'rb') do |file| - params = ActionController::AbstractRequest.parse_multipart_form_parameters(file, 'AaB03x', file.stat.size, {}) + params = ActionController::RequestParser.parse_multipart_form_parameters(file, 'AaB03x', file.stat.size, {}) assert_equal 0, file.pos # file was rewound after reading params end @@ -856,7 +854,7 @@ class XmlParamsParsingTest < ActiveSupport::TestCase env = { 'rack.input' => StringIO.new(body), 'CONTENT_TYPE' => 'application/xml', 'CONTENT_LENGTH' => body.size.to_s } - ActionController::RackRequest.new(env).request_parameters + ActionController::Request.new(env).request_parameters end end @@ -866,7 +864,7 @@ class LegacyXmlParamsParsingTest < XmlParamsParsingTest env = { 'rack.input' => StringIO.new(body), 'HTTP_X_POST_DATA_FORMAT' => 'xml', 'CONTENT_LENGTH' => body.size.to_s } - ActionController::RackRequest.new(env).request_parameters + ActionController::Request.new(env).request_parameters end end @@ -888,6 +886,6 @@ class JsonParamsParsingTest < ActiveSupport::TestCase env = { 'rack.input' => StringIO.new(body), 'CONTENT_TYPE' => content_type, 'CONTENT_LENGTH' => body.size.to_s } - ActionController::RackRequest.new(env).request_parameters + ActionController::Request.new(env).request_parameters end end diff --git a/actionpack/test/controller/rescue_test.rb b/actionpack/test/controller/rescue_test.rb index 63f9827f4a..49aca3a6ee 100644 --- a/actionpack/test/controller/rescue_test.rb +++ b/actionpack/test/controller/rescue_test.rb @@ -367,7 +367,11 @@ class RescueControllerTest < ActionController::TestCase end def test_rescue_dispatcher_exceptions - RescueController.process_with_exception(@request, @response, ActionController::RoutingError.new("Route not found")) + env = @request.env + env["actioncontroller.rescue.request"] = @request + env["actioncontroller.rescue.response"] = @response + + RescueController.call_with_exception(env, ActionController::RoutingError.new("Route not found")) assert_equal "no way", @response.body end diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index d5b6bd6b2a..b981119e1e 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -706,7 +706,7 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do port_string = port == 80 ? '' : ":#{port}" protocol = options.delete(:protocol) || "http" - host = options.delete(:host) || "named.route.test" + host = options.delete(:host) || "test.host" anchor = "##{options.delete(:anchor)}" if options.key?(:anchor) path = routes.generate(options) @@ -715,27 +715,7 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do end def request - @request ||= MockRequest.new(:host => "named.route.test", :method => :get) - end - end - - class MockRequest - attr_accessor :path, :path_parameters, :host, :subdomains, :domain, :method - - def initialize(values={}) - values.each { |key, value| send("#{key}=", value) } - if values[:host] - subdomain, self.domain = values[:host].split(/\./, 2) - self.subdomains = [subdomain] - end - end - - def protocol - "http://" - end - - def host_with_port - (subdomains * '.') + '.' + domain + @request ||= ActionController::TestRequest.new end end @@ -900,7 +880,7 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do def test_basic_named_route rs.add_named_route :home, '', :controller => 'content', :action => 'list' x = setup_for_named_route - assert_equal("http://named.route.test/", + assert_equal("http://test.host/", x.send(:home_url)) end @@ -908,7 +888,7 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do rs.add_named_route :home, '', :controller => 'content', :action => 'list' x = setup_for_named_route ActionController::Base.relative_url_root = "/foo" - assert_equal("http://named.route.test/foo/", + assert_equal("http://test.host/foo/", x.send(:home_url)) assert_equal "/foo/", x.send(:home_path) ActionController::Base.relative_url_root = nil @@ -917,14 +897,14 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do def test_named_route_with_option rs.add_named_route :page, 'page/:title', :controller => 'content', :action => 'show_page' x = setup_for_named_route - assert_equal("http://named.route.test/page/new%20stuff", + assert_equal("http://test.host/page/new%20stuff", x.send(:page_url, :title => 'new stuff')) end def test_named_route_with_default rs.add_named_route :page, 'page/:title', :controller => 'content', :action => 'show_page', :title => 'AboutPage' x = setup_for_named_route - assert_equal("http://named.route.test/page/AboutRails", + assert_equal("http://test.host/page/AboutRails", x.send(:page_url, :title => "AboutRails")) end @@ -932,21 +912,21 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do def test_named_route_with_name_prefix rs.add_named_route :page, 'page', :controller => 'content', :action => 'show_page', :name_prefix => 'my_' x = setup_for_named_route - assert_equal("http://named.route.test/page", + assert_equal("http://test.host/page", x.send(:my_page_url)) end def test_named_route_with_path_prefix rs.add_named_route :page, 'page', :controller => 'content', :action => 'show_page', :path_prefix => 'my' x = setup_for_named_route - assert_equal("http://named.route.test/my/page", + assert_equal("http://test.host/my/page", x.send(:page_url)) end def test_named_route_with_nested_controller rs.add_named_route :users, 'admin/user', :controller => 'admin/user', :action => 'index' x = setup_for_named_route - assert_equal("http://named.route.test/admin/user", + assert_equal("http://test.host/admin/user", x.send(:users_url)) end @@ -985,7 +965,7 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do map.root :controller => "hello" end x = setup_for_named_route - assert_equal("http://named.route.test/", x.send(:root_url)) + assert_equal("http://test.host/", x.send(:root_url)) assert_equal("/", x.send(:root_path)) end @@ -1001,7 +981,7 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do # x.send(:article_url, :title => 'hi') # ) assert_equal( - "http://named.route.test/page/2005/6/10/hi", + "http://test.host/page/2005/6/10/hi", x.send(:article_url, :title => 'hi', :day => 10, :year => 2005, :month => 6) ) end @@ -1202,7 +1182,7 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do assert_equal '/test', rs.generate(:controller => 'post', :action => 'show', :year => nil) x = setup_for_named_route - assert_equal("http://named.route.test/test", + assert_equal("http://test.host/test", x.send(:blog_url)) end @@ -1249,7 +1229,7 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do assert_equal '/', rs.generate(:controller => 'content') x = setup_for_named_route - assert_equal("http://named.route.test/", + assert_equal("http://test.host/", x.send(:home_url)) end @@ -1591,7 +1571,7 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do end def request - @request ||= MockRequest.new(:host => "named.routes.test", :method => :get) + @request ||= ActionController::TestRequest.new end def test_generate_extras @@ -1692,13 +1672,13 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do def test_named_route_url_method controller = setup_named_route_test - assert_equal "http://named.route.test/people/5", controller.send(:show_url, :id => 5) + assert_equal "http://test.host/people/5", controller.send(:show_url, :id => 5) assert_equal "/people/5", controller.send(:show_path, :id => 5) - assert_equal "http://named.route.test/people", controller.send(:index_url) + assert_equal "http://test.host/people", controller.send(:index_url) assert_equal "/people", controller.send(:index_path) - assert_equal "http://named.route.test/admin/users", controller.send(:users_url) + assert_equal "http://test.host/admin/users", controller.send(:users_url) assert_equal '/admin/users', controller.send(:users_path) assert_equal '/admin/users', set.generate(controller.send(:hash_for_users_url), {:controller => 'users', :action => 'index'}) end @@ -1706,28 +1686,28 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do def test_named_route_url_method_with_anchor controller = setup_named_route_test - assert_equal "http://named.route.test/people/5#location", controller.send(:show_url, :id => 5, :anchor => 'location') + assert_equal "http://test.host/people/5#location", controller.send(:show_url, :id => 5, :anchor => 'location') assert_equal "/people/5#location", controller.send(:show_path, :id => 5, :anchor => 'location') - assert_equal "http://named.route.test/people#location", controller.send(:index_url, :anchor => 'location') + assert_equal "http://test.host/people#location", controller.send(:index_url, :anchor => 'location') assert_equal "/people#location", controller.send(:index_path, :anchor => 'location') - assert_equal "http://named.route.test/admin/users#location", controller.send(:users_url, :anchor => 'location') + assert_equal "http://test.host/admin/users#location", controller.send(:users_url, :anchor => 'location') assert_equal '/admin/users#location', controller.send(:users_path, :anchor => 'location') - assert_equal "http://named.route.test/people/go/7/hello/joe/5#location", + assert_equal "http://test.host/people/go/7/hello/joe/5#location", controller.send(:multi_url, 7, "hello", 5, :anchor => 'location') - assert_equal "http://named.route.test/people/go/7/hello/joe/5?baz=bar#location", + assert_equal "http://test.host/people/go/7/hello/joe/5?baz=bar#location", controller.send(:multi_url, 7, "hello", 5, :baz => "bar", :anchor => 'location') - assert_equal "http://named.route.test/people?baz=bar#location", + assert_equal "http://test.host/people?baz=bar#location", controller.send(:index_url, :baz => "bar", :anchor => 'location') end def test_named_route_url_method_with_port controller = setup_named_route_test - assert_equal "http://named.route.test:8080/people/5", controller.send(:show_url, 5, :port=>8080) + assert_equal "http://test.host:8080/people/5", controller.send(:show_url, 5, :port=>8080) end def test_named_route_url_method_with_host @@ -1737,30 +1717,30 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do def test_named_route_url_method_with_protocol controller = setup_named_route_test - assert_equal "https://named.route.test/people/5", controller.send(:show_url, 5, :protocol => "https") + assert_equal "https://test.host/people/5", controller.send(:show_url, 5, :protocol => "https") end def test_named_route_url_method_with_ordered_parameters controller = setup_named_route_test - assert_equal "http://named.route.test/people/go/7/hello/joe/5", + assert_equal "http://test.host/people/go/7/hello/joe/5", controller.send(:multi_url, 7, "hello", 5) end def test_named_route_url_method_with_ordered_parameters_and_hash controller = setup_named_route_test - assert_equal "http://named.route.test/people/go/7/hello/joe/5?baz=bar", + assert_equal "http://test.host/people/go/7/hello/joe/5?baz=bar", controller.send(:multi_url, 7, "hello", 5, :baz => "bar") end def test_named_route_url_method_with_ordered_parameters_and_empty_hash controller = setup_named_route_test - assert_equal "http://named.route.test/people/go/7/hello/joe/5", + assert_equal "http://test.host/people/go/7/hello/joe/5", controller.send(:multi_url, 7, "hello", 5, {}) end def test_named_route_url_method_with_no_positional_arguments controller = setup_named_route_test - assert_equal "http://named.route.test/people?baz=bar", + assert_equal "http://test.host/people?baz=bar", controller.send(:index_url, :baz => "bar") end @@ -1896,49 +1876,54 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do end request.path = "/people" - request.method = :get + request.env["REQUEST_METHOD"] = "GET" assert_nothing_raised { set.recognize(request) } assert_equal("index", request.path_parameters[:action]) + request.recycle! - request.method = :post + request.env["REQUEST_METHOD"] = "POST" assert_nothing_raised { set.recognize(request) } assert_equal("create", request.path_parameters[:action]) + request.recycle! - request.method = :put + request.env["REQUEST_METHOD"] = "PUT" assert_nothing_raised { set.recognize(request) } assert_equal("update", request.path_parameters[:action]) + request.recycle! - begin - request.method = :bacon + assert_raises(ActionController::UnknownHttpMethod) { + request.env["REQUEST_METHOD"] = "BACON" set.recognize(request) - flunk 'Should have raised NotImplemented' - rescue ActionController::NotImplemented => e - assert_equal [:get, :post, :put, :delete], e.allowed_methods - end + } + request.recycle! request.path = "/people/5" - request.method = :get + request.env["REQUEST_METHOD"] = "GET" assert_nothing_raised { set.recognize(request) } assert_equal("show", request.path_parameters[:action]) assert_equal("5", request.path_parameters[:id]) + request.recycle! - request.method = :put + request.env["REQUEST_METHOD"] = "PUT" assert_nothing_raised { set.recognize(request) } assert_equal("update", request.path_parameters[:action]) assert_equal("5", request.path_parameters[:id]) + request.recycle! - request.method = :delete + request.env["REQUEST_METHOD"] = "DELETE" assert_nothing_raised { set.recognize(request) } assert_equal("destroy", request.path_parameters[:action]) assert_equal("5", request.path_parameters[:id]) + request.recycle! begin - request.method = :post + request.env["REQUEST_METHOD"] = "POST" set.recognize(request) flunk 'Should have raised MethodNotAllowed' rescue ActionController::MethodNotAllowed => e assert_equal [:get, :put, :delete], e.allowed_methods end + request.recycle! ensure Object.send(:remove_const, :PeopleController) @@ -1954,13 +1939,13 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do end request.path = "/people" - request.method = :get + request.env["REQUEST_METHOD"] = "GET" assert_nothing_raised { set.recognize(request) } assert_equal("people", request.path_parameters[:controller]) assert_equal("index", request.path_parameters[:action]) request.path = "/" - request.method = :get + request.env["REQUEST_METHOD"] = "GET" assert_nothing_raised { set.recognize(request) } assert_equal("people", request.path_parameters[:controller]) assert_equal("index", request.path_parameters[:action]) @@ -1978,7 +1963,7 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do end request.path = "/articles/2005/11/05/a-very-interesting-article" - request.method = :get + request.env["REQUEST_METHOD"] = "GET" assert_nothing_raised { set.recognize(request) } assert_equal("permalink", request.path_parameters[:action]) assert_equal("2005", request.path_parameters[:year]) @@ -2015,17 +2000,19 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do end request.path = "/people/5" - request.method = :get + request.env["REQUEST_METHOD"] = "GET" assert_nothing_raised { set.recognize(request) } assert_equal("show", request.path_parameters[:action]) assert_equal("5", request.path_parameters[:id]) + request.recycle! - request.method = :put + request.env["REQUEST_METHOD"] = "PUT" assert_nothing_raised { set.recognize(request) } assert_equal("update", request.path_parameters[:action]) + request.recycle! request.path = "/people/5.png" - request.method = :get + request.env["REQUEST_METHOD"] = "GET" assert_nothing_raised { set.recognize(request) } assert_equal("show", request.path_parameters[:action]) assert_equal("5", request.path_parameters[:id]) @@ -2050,7 +2037,7 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do set.draw { |map| map.root :controller => "people" } request.path = "" - request.method = :get + request.env["REQUEST_METHOD"] = "GET" assert_nothing_raised { set.recognize(request) } assert_equal("people", request.path_parameters[:controller]) assert_equal("index", request.path_parameters[:action]) @@ -2070,7 +2057,7 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do end request.path = "/api/inventory" - request.method = :get + request.env["REQUEST_METHOD"] = "GET" assert_nothing_raised { set.recognize(request) } assert_equal("api/products", request.path_parameters[:controller]) assert_equal("inventory", request.path_parameters[:action]) @@ -2090,7 +2077,7 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do end request.path = "/api" - request.method = :get + request.env["REQUEST_METHOD"] = "GET" assert_nothing_raised { set.recognize(request) } assert_equal("api/products", request.path_parameters[:controller]) assert_equal("index", request.path_parameters[:action]) @@ -2110,7 +2097,7 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do end request.path = "/prefix/inventory" - request.method = :get + request.env["REQUEST_METHOD"] = "GET" assert_nothing_raised { set.recognize(request) } assert_equal("api/products", request.path_parameters[:controller]) assert_equal("inventory", request.path_parameters[:action]) @@ -2246,7 +2233,7 @@ uses_mocha 'LegacyRouteSet, Route, RouteSet and RouteLoading' do end request.path = "/projects/1/milestones" - request.method = :get + request.env["REQUEST_METHOD"] = "GET" assert_nothing_raised { set.recognize(request) } assert_equal("milestones", request.path_parameters[:controller]) assert_equal("index", request.path_parameters[:action]) diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb index c4349bfc7f..1b7486ad34 100644 --- a/actionpack/test/controller/send_file_test.rb +++ b/actionpack/test/controller/send_file_test.rb @@ -119,6 +119,31 @@ class SendFileTest < Test::Unit::TestCase assert_equal 'private', h['Cache-Control'] end + def test_send_file_headers_with_mime_lookup_with_symbol + options = { + :length => 1, + :type => :png + } + + @controller.headers = {} + @controller.send(:send_file_headers!, options) + + headers = @controller.headers + + assert_equal 'image/png', headers['Content-Type'] + end + + + def test_send_file_headers_with_bad_symbol + options = { + :length => 1, + :type => :this_type_is_not_registered + } + + @controller.headers = {} + assert_raises(ArgumentError){ @controller.send(:send_file_headers!, options) } + end + %w(file data).each do |method| define_method "test_send_#{method}_status" do @controller.options = { :stream => false, :status => 500 } diff --git a/actionpack/test/controller/session/cookie_store_test.rb b/actionpack/test/controller/session/cookie_store_test.rb index 69aec59dc0..d349c18d1d 100644 --- a/actionpack/test/controller/session/cookie_store_test.rb +++ b/actionpack/test/controller/session/cookie_store_test.rb @@ -25,7 +25,7 @@ class CookieStoreTest < ActionController::IntegrationTest def set_session_value session[:foo] = "bar" - render :text => Marshal.dump(session.to_hash) + render :text => Verifier.generate(session.to_hash) end def get_session_value @@ -94,8 +94,7 @@ class CookieStoreTest < ActionController::IntegrationTest with_test_route_set do get '/set_session_value' assert_response :success - session_payload = Verifier.generate(Marshal.load(response.body)) - assert_equal ["_myapp_session=#{session_payload}; path=/"], + assert_equal ["_myapp_session=#{response.body}; path=/"], headers['Set-Cookie'] end end @@ -148,8 +147,8 @@ class CookieStoreTest < ActionController::IntegrationTest with_test_route_set do get '/set_session_value' assert_response :success - session_payload = Verifier.generate(Marshal.load(response.body)) - assert_equal ["_myapp_session=#{session_payload}; path=/"], + session_payload = response.body + assert_equal ["_myapp_session=#{response.body}; path=/"], headers['Set-Cookie'] get '/call_reset_session' diff --git a/actionpack/test/template/date_helper_i18n_test.rb b/actionpack/test/template/date_helper_i18n_test.rb index dc9616db3b..fac30da128 100644 --- a/actionpack/test/template/date_helper_i18n_test.rb +++ b/actionpack/test/template/date_helper_i18n_test.rb @@ -78,6 +78,8 @@ class DateHelperSelectTagsI18nTests < Test::Unit::TestCase uses_mocha 'date_helper_select_tags_i18n_tests' do def setup + @prompt_defaults = {:year => 'Year', :month => 'Month', :day => 'Day', :hour => 'Hour', :minute => 'Minute', :second => 'Seconds'} + I18n.stubs(:translate).with(:'date.month_names', :locale => 'en').returns Date::MONTHNAMES end @@ -98,6 +100,15 @@ class DateHelperSelectTagsI18nTests < Test::Unit::TestCase select_month(8, :locale => 'en', :use_short_month => true) end + def test_date_or_time_select_translates_prompts + @prompt_defaults.each do |key, prompt| + I18n.expects(:translate).with(('datetime.prompts.' + key.to_s).to_sym, :locale => 'en').returns prompt + end + + I18n.expects(:translate).with(:'date.order', :locale => 'en').returns [:year, :month, :day] + datetime_select('post', 'updated_at', :locale => 'en', :include_seconds => true, :prompt => true) + end + # date_or_time_select def test_date_or_time_select_given_an_order_options_does_not_translate_order diff --git a/actionpack/test/template/date_helper_test.rb b/actionpack/test/template/date_helper_test.rb index 49ba140c23..6ec01b7a8f 100644 --- a/actionpack/test/template/date_helper_test.rb +++ b/actionpack/test/template/date_helper_test.rb @@ -153,6 +153,22 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, select_day(16, {}, :class => 'selector') end + def test_select_day_with_default_prompt + expected = %(<select id="date_day" name="date[day]">\n) + 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" + + assert_dom_equal expected, select_day(16, :prompt => true) + end + + def test_select_day_with_custom_prompt + expected = %(<select id="date_day" name="date[day]">\n) + 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" + + assert_dom_equal expected, select_day(16, :prompt => 'Choose day') + end + def test_select_month expected = %(<select id="date_month" name="date[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) @@ -276,6 +292,22 @@ class DateHelperTest < ActionView::TestCase #assert result.include?('<option value="1">January') end + def test_select_month_with_default_prompt + expected = %(<select id="date_month" name="date[month]">\n) + expected << %(<option value="">Month</option>\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option 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" + + assert_dom_equal expected, select_month(8, :prompt => true) + end + + def test_select_month_with_custom_prompt + expected = %(<select id="date_month" name="date[month]">\n) + expected << %(<option value="">Choose month</option>\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option 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" + + assert_dom_equal expected, select_month(8, :prompt => 'Choose month') + end + def test_select_year expected = %(<select id="date_year" name="date[year]">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) @@ -344,6 +376,22 @@ class DateHelperTest < ActionView::TestCase #assert result.include?('<option value="2003"') end + def test_select_year_with_default_prompt + expected = %(<select id="date_year" name="date[year]">\n) + expected << %(<option value="">Year</option>\n<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + assert_dom_equal expected, select_year(nil, :start_year => 2003, :end_year => 2005, :prompt => true) + end + + def test_select_year_with_custom_prompt + expected = %(<select id="date_year" name="date[year]">\n) + expected << %(<option value="">Choose year</option>\n<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + + assert_dom_equal expected, select_year(nil, :start_year => 2003, :end_year => 2005, :prompt => 'Choose year') + end + def test_select_hour 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) @@ -392,6 +440,22 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector', :accesskey => 'M') end + def test_select_hour_with_default_prompt + 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" + + assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => true) + end + + def test_select_hour_with_custom_prompt + 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" + + assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => 'Choose hour') + end + def test_select_minute 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) @@ -470,6 +534,22 @@ class DateHelperTest < ActionView::TestCase #assert result.include?('<option value="00">00') end + def test_select_minute_with_default_prompt + 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" + + assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => true) + end + + def test_select_minute_with_custom_prompt + 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" + + assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => 'Choose minute') + end + def test_select_second 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) @@ -524,6 +604,22 @@ class DateHelperTest < ActionView::TestCase #assert result.include?('<option value="00">00') end + def test_select_second_with_default_prompt + 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" + + assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => true) + end + + def test_select_second_with_custom_prompt + 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" + + assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => 'Choose seconds') + end + def test_select_date 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) @@ -914,6 +1010,57 @@ class DateHelperTest < ActionView::TestCase assert_nothing_raised { select_datetime(Date.today) } end + def test_select_datetime_with_default_prompt + expected = %(<select id="date_first_year" name="date[first][year]">\n) + expected << %(<option value="">Year</option>\n<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="">Month</option>\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option 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="">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 << %(<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 << %(<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" + + assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :start_year => 2003, :end_year => 2005, + :prefix => "date[first]", :prompt => true) + end + + def test_select_datetime_with_custom_prompt + + expected = %(<select id="date_first_year" name="date[first][year]">\n) + expected << %(<option value="">Choose year</option>\n<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="">Choose month</option>\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option 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="">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 << %(<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 << %(<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" + + assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", + :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year', :hour => 'Choose hour', :minute => 'Choose minute'}) + end + def test_select_time 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) @@ -995,6 +1142,40 @@ class DateHelperTest < ActionView::TestCase assert_nothing_raised { select_time(Date.today) } end + def test_select_time_with_default_prompt + 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 << %(<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 << %(<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" + + assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :include_seconds => true, :prompt => true) + end + + def test_select_time_with_custom_prompt + + 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 << %(<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 << %(<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" + + assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => true, :include_seconds => true, + :prompt => {:hour => 'Choose hour', :minute => 'Choose minute', :second => 'Choose seconds'}) + end + def test_date_select @post = Post.new @post.written_on = Date.new(2004, 6, 15) @@ -1277,6 +1458,46 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, date_select("post", "written_on", { :date_separator => " / " }) end + def test_date_select_with_default_prompt + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} + expected << %{<option value="">Year</option>\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} + expected << "</select>\n" + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} + expected << %{<option value="">Month</option>\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n} + expected << "</select>\n" + + expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} + 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" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n} + + expected << "</select>\n" + + assert_dom_equal expected, date_select("post", "written_on", :prompt => true) + end + + def test_date_select_with_custom_prompt + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n} + expected << %{<option value="">Choose year</option>\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} + expected << "</select>\n" + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n} + expected << %{<option value="">Choose month</option>\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n} + expected << "</select>\n" + + expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} + 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" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n} + + expected << "</select>\n" + + assert_dom_equal expected, date_select("post", "written_on", :prompt => {:year => 'Choose year', :month => 'Choose month', :day => 'Choose day'}) + end + def test_time_select @post = Post.new @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) @@ -1403,6 +1624,48 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, time_select("post", "written_on", { :time_separator => " - ", :include_seconds => true }) end + def test_time_select_with_default_prompt + @post = Post.new + @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n} + expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n} + expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n} + + expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n) + expected << %(<option value="">Hour</option>\n) + 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + expected << " : " + expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n) + expected << %(<option value="">Minute</option>\n) + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, time_select("post", "written_on", :prompt => true) + end + + def test_time_select_with_custom_prompt + @post = Post.new + @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n} + expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n} + expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n} + + expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n) + expected << %(<option value="">Choose hour</option>\n) + 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + expected << " : " + expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n) + expected << %(<option value="">Choose minute</option>\n) + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, time_select("post", "written_on", :prompt => {:hour => 'Choose hour', :minute => 'Choose minute'}) + end + def test_datetime_select @post = Post.new @post.updated_at = Time.local(2004, 6, 15, 16, 35) @@ -1526,6 +1789,64 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, datetime_select("post", "updated_at", { :date_separator => " / ", :datetime_separator => " , ", :time_separator => " - ", :include_seconds => true }) end + def test_datetime_select_with_default_prompt + @post = Post.new + @post.updated_at = Time.local(2004, 6, 15, 16, 35) + + expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} + expected << %{<option value="">Year</option>\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} + expected << "</select>\n" + + expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n} + expected << %{<option value="">Month</option>\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n} + expected << "</select>\n" + + expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n} + 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" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n} + expected << "</select>\n" + + expected << " — " + + expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\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">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" 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} + expected << "</select>\n" + expected << " : " + expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\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">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" selected="selected">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" + + assert_dom_equal expected, datetime_select("post", "updated_at", :prompt => true) + end + + def test_datetime_select_with_custom_prompt + @post = Post.new + @post.updated_at = Time.local(2004, 6, 15, 16, 35) + + expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} + expected << %{<option value="">Choose year</option>\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n} + expected << "</select>\n" + + expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n} + expected << %{<option value="">Choose month</option>\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n} + expected << "</select>\n" + + expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n} + 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" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n} + expected << "</select>\n" + + expected << " — " + + expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\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">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" 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} + expected << "</select>\n" + expected << " : " + expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\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">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" selected="selected">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" + + assert_dom_equal expected, datetime_select("post", "updated_at", :prompt => {:year => 'Choose year', :month => 'Choose month', :day => 'Choose day', :hour => 'Choose hour', :minute => 'Choose minute'}) + end + def test_date_select_with_zero_value_and_no_start_year expected = %(<select id="date_first_year" name="date[first][year]">\n) (Date.today.year-5).upto(Date.today.year+1) { |y| expected << %(<option value="#{y}">#{y}</option>\n) } diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index d057ddfcd0..c750f486f9 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,9 @@ *2.3.0/3.0* +* Added dynamic scopes ala dynamic finders #1648 [Yaroslav Markin] + +* Fixed that ActiveRecord::Base#new_record? should return false (not nil) for existing records #1219 [Yaroslav Markin] + * I18n the word separator for error messages. Introduces the activerecord.errors.format.separator translation key. #1294 [Akira Matsuda] * Add :having as a key to find and the relevant associations. [Emilio Tagua] diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index c428366a04..390c005785 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -51,6 +51,7 @@ module ActiveRecord autoload :Callbacks, 'active_record/callbacks' autoload :Dirty, 'active_record/dirty' autoload :DynamicFinderMatch, 'active_record/dynamic_finder_match' + autoload :DynamicScopeMatch, 'active_record/dynamic_scope_match' autoload :Migration, 'active_record/migration' autoload :Migrator, 'active_record/migration' autoload :NamedScope, 'active_record/named_scope' diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 7b1b2d9ad9..9d0bf3a308 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -43,7 +43,7 @@ module ActiveRecord # loading in a more high-level (application developer-friendly) manner. module ClassMethods protected - + # Eager loads the named associations for the given ActiveRecord record(s). # # In this description, 'association name' shall refer to the name passed @@ -94,8 +94,8 @@ module ActiveRecord raise "parent must be an association name" unless parent.is_a?(String) || parent.is_a?(Symbol) preload_associations(records, parent, preload_options) reflection = reflections[parent] - parents = records.map {|record| record.send(reflection.name)}.flatten - unless parents.empty? || parents.first.nil? + parents = records.map {|record| record.send(reflection.name)}.flatten.compact + unless parents.empty? parents.first.class.preload_associations(parents, child) end end @@ -113,7 +113,7 @@ module ActiveRecord # unnecessarily records.group_by {|record| class_to_reflection[record.class] ||= record.class.reflections[association]}.each do |reflection, records| raise ConfigurationError, "Association named '#{ association }' was not found; perhaps you misspelled it?" unless reflection - + # 'reflection.macro' can return 'belongs_to', 'has_many', etc. Thus, # the following could call 'preload_belongs_to_association', # 'preload_has_many_association', etc. @@ -128,7 +128,7 @@ module ActiveRecord association_proxy.target.push(*[associated_record].flatten) end end - + def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record) parent_records.each do |parent_record| parent_record.send("set_#{reflection_name}_target", associated_record) @@ -183,18 +183,19 @@ module ActiveRecord conditions = "t0.#{reflection.primary_key_name} #{in_or_equals_for_ids(ids)}" conditions << append_conditions(reflection, preload_options) - associated_records = reflection.klass.find(:all, :conditions => [conditions, ids], - :include => options[:include], - :joins => "INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}", - :select => "#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id", - :order => options[:order]) - + associated_records = reflection.klass.with_exclusive_scope do + reflection.klass.find(:all, :conditions => [conditions, ids], + :include => options[:include], + :joins => "INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}", + :select => "#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id", + :order => options[:order]) + end set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'the_parent_record_id') end def preload_has_one_association(records, reflection, preload_options={}) return if records.first.send("loaded_#{reflection.name}?") - id_to_record_map, ids = construct_id_map(records) + id_to_record_map, ids = construct_id_map(records) options = reflection.options records.each {|record| record.send("set_#{reflection.name}_target", nil)} if options[:through] @@ -248,7 +249,7 @@ module ActiveRecord reflection.primary_key_name) end end - + def preload_through_records(records, reflection, through_association) through_reflection = reflections[through_association] through_primary_key = through_reflection.primary_key_name @@ -333,11 +334,13 @@ module ActiveRecord end conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} #{in_or_equals_for_ids(ids)}" conditions << append_conditions(reflection, preload_options) - associated_records = klass.find(:all, :conditions => [conditions, ids], + associated_records = klass.with_exclusive_scope do + klass.find(:all, :conditions => [conditions, ids], :include => options[:include], :select => options[:select], :joins => options[:joins], :order => options[:order]) + end set_association_single_records(id_map, reflection.name, associated_records, primary_key) end end @@ -355,13 +358,15 @@ module ActiveRecord conditions << append_conditions(reflection, preload_options) - reflection.klass.find(:all, + reflection.klass.with_exclusive_scope do + reflection.klass.find(:all, :select => (preload_options[:select] || options[:select] || "#{table_name}.*"), :include => preload_options[:include] || options[:include], :conditions => [conditions, ids], :joins => options[:joins], :group => preload_options[:group] || options[:group], :order => preload_options[:order] || options[:order]) + end end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 07bc50c886..c154a5087c 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1216,11 +1216,11 @@ module ActiveRecord # callbacks will be executed after the association is wiped out. old_method = "destroy_without_habtm_shim_for_#{reflection.name}" class_eval <<-end_eval unless method_defined?(old_method) - alias_method :#{old_method}, :destroy_without_callbacks - def destroy_without_callbacks - #{reflection.name}.clear - #{old_method} - end + alias_method :#{old_method}, :destroy_without_callbacks # alias_method :destroy_without_habtm_shim_for_posts, :destroy_without_callbacks + def destroy_without_callbacks # def destroy_without_callbacks + #{reflection.name}.clear # posts.clear + #{old_method} # destroy_without_habtm_shim_for_posts + end # end end_eval add_association_callbacks(reflection.name, options) @@ -1453,7 +1453,7 @@ module ActiveRecord dependent_conditions << sanitize_sql(reflection.options[:conditions]) if reflection.options[:conditions] dependent_conditions << extra_conditions if extra_conditions dependent_conditions = dependent_conditions.collect {|where| "(#{where})" }.join(" AND ") - + dependent_conditions = dependent_conditions.gsub('@', '\@') case reflection.options[:dependent] when :destroy method_name = "has_many_dependent_destroy_for_#{reflection.name}".to_sym @@ -1463,22 +1463,22 @@ module ActiveRecord before_destroy method_name when :delete_all module_eval %Q{ - before_destroy do |record| - delete_all_has_many_dependencies(record, - "#{reflection.name}", - #{reflection.class_name}, - "#{dependent_conditions}") - end + before_destroy do |record| # before_destroy do |record| + delete_all_has_many_dependencies(record, # delete_all_has_many_dependencies(record, + "#{reflection.name}", # "posts", + #{reflection.class_name}, # Post, + %@#{dependent_conditions}@) # %@...@) # this is a string literal like %(...) + end # end } when :nullify module_eval %Q{ - before_destroy do |record| - nullify_has_many_dependencies(record, - "#{reflection.name}", - #{reflection.class_name}, - "#{reflection.primary_key_name}", - "#{dependent_conditions}") - end + before_destroy do |record| # before_destroy do |record| + nullify_has_many_dependencies(record, # nullify_has_many_dependencies(record, + "#{reflection.name}", # "posts", + #{reflection.class_name}, # Post, + "#{reflection.primary_key_name}", # "user_id", + %@#{dependent_conditions}@) # %@...@) # this is a string literal like %(...) + end # end } else raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, or :nullify (#{reflection.options[:dependent].inspect})" diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index aa6013583d..cca012ed55 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1456,7 +1456,10 @@ module ActiveRecord #:nodoc: def respond_to?(method_id, include_private = false) if match = DynamicFinderMatch.match(method_id) return true if all_attributes_exists?(match.attribute_names) + elsif match = DynamicScopeMatch.match(method_id) + return true if all_attributes_exists?(match.attribute_names) end + super end @@ -1494,11 +1497,16 @@ module ActiveRecord #:nodoc: end if scoped?(:find, :order) - scoped_order = reverse_sql_order(scope(:find, :order)) - scoped_methods.select { |s| s[:find].update(:order => scoped_order) } + scope = scope(:find) + original_scoped_order = scope[:order] + scope[:order] = reverse_sql_order(original_scoped_order) end - find_initial(options.merge({ :order => order })) + begin + find_initial(options.merge({ :order => order })) + ensure + scope[:order] = original_scoped_order if original_scoped_order + end end def reverse_sql_order(order_query) @@ -1804,7 +1812,11 @@ module ActiveRecord #:nodoc: # This also enables you to initialize a record if it is not found, such as find_or_initialize_by_amount(amount) # or find_or_create_by_user_and_password(user, password). # - # Each dynamic finder or initializer/creator is also defined in the class after it is first invoked, so that future + # Also enables dynamic scopes like scoped_by_user_name(user_name) and scoped_by_user_name_and_password(user_name, password) that + # are turned into scoped(:conditions => ["user_name = ?", user_name]) and scoped(:conditions => ["user_name = ? AND password = ?", user_name, password]) + # respectively. + # + # Each dynamic finder, scope or initializer/creator is also defined in the class after it is first invoked, so that future # attempts to use it do not run through method_missing. def method_missing(method_id, *arguments, &block) if match = DynamicFinderMatch.match(method_id) @@ -1813,10 +1825,31 @@ module ActiveRecord #:nodoc: if match.finder? finder = match.finder bang = match.bang? + # def self.find_by_login_and_activated(*args) + # options = args.extract_options! + # attributes = construct_attributes_from_arguments( + # [:login,:activated], + # args + # ) + # finder_options = { :conditions => attributes } + # validate_find_options(options) + # set_readonly_option!(options) + # + # if options[:conditions] + # with_scope(:find => finder_options) do + # find(:first, options) + # end + # else + # find(:first, options.merge(finder_options)) + # end + # end self.class_eval %{ def self.#{method_id}(*args) options = args.extract_options! - attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args) + attributes = construct_attributes_from_arguments( + [:#{attribute_names.join(',:')}], + args + ) finder_options = { :conditions => attributes } validate_find_options(options) set_readonly_option!(options) @@ -1834,6 +1867,31 @@ module ActiveRecord #:nodoc: send(method_id, *arguments) elsif match.instantiator? instantiator = match.instantiator + # def self.find_or_create_by_user_id(*args) + # guard_protected_attributes = false + # + # if args[0].is_a?(Hash) + # guard_protected_attributes = true + # attributes = args[0].with_indifferent_access + # find_attributes = attributes.slice(*[:user_id]) + # else + # find_attributes = attributes = construct_attributes_from_arguments([:user_id], args) + # end + # + # options = { :conditions => find_attributes } + # set_readonly_option!(options) + # + # record = find(:first, options) + # + # if record.nil? + # record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) } + # yield(record) if block_given? + # record.save + # record + # else + # record + # end + # end self.class_eval %{ def self.#{method_id}(*args) guard_protected_attributes = false @@ -1863,6 +1921,22 @@ module ActiveRecord #:nodoc: }, __FILE__, __LINE__ send(method_id, *arguments, &block) end + elsif match = DynamicScopeMatch.match(method_id) + attribute_names = match.attribute_names + super unless all_attributes_exists?(attribute_names) + if match.scope? + self.class_eval %{ + def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args) + options = args.extract_options! # options = args.extract_options! + attributes = construct_attributes_from_arguments( # attributes = construct_attributes_from_arguments( + [:#{attribute_names.join(',:')}], args # [:user_name, :password], args + ) # ) + # + scoped(:conditions => attributes) # scoped(:conditions => attributes) + end # end + }, __FILE__, __LINE__ + send(method_id, *arguments) + end else super end @@ -2401,9 +2475,9 @@ module ActiveRecord #:nodoc: write_attribute(self.class.primary_key, value) end - # Returns true if this object hasn't been saved yet -- that is, a record for the object doesn't exist yet. + # Returns true if this object hasn't been saved yet -- that is, a record for the object doesn't exist yet; otherwise, returns false. def new_record? - defined?(@new_record) && @new_record + @new_record || false end # :call-seq: @@ -3010,7 +3084,7 @@ module ActiveRecord #:nodoc: end Base.class_eval do - extend QueryCache + extend QueryCache::ClassMethods include Validations include Locking::Optimistic, Locking::Pessimistic include AttributeMethods diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index 950bd72101..00c71090f3 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -14,12 +14,12 @@ module ActiveRecord def dirties_query_cache(base, *method_names) method_names.each do |method_name| base.class_eval <<-end_code, __FILE__, __LINE__ - def #{method_name}_with_query_dirty(*args) - clear_query_cache if @query_cache_enabled - #{method_name}_without_query_dirty(*args) - end - - alias_method_chain :#{method_name}, :query_dirty + def #{method_name}_with_query_dirty(*args) # def update_with_query_dirty(*args) + clear_query_cache if @query_cache_enabled # clear_query_cache if @query_cache_enabled + #{method_name}_without_query_dirty(*args) # update_without_query_dirty(*args) + end # end + # + alias_method_chain :#{method_name}, :query_dirty # alias_method_chain :update, :query_dirty end_code end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index fe9cbcf024..273f823e7f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -476,12 +476,12 @@ module ActiveRecord %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type| class_eval <<-EOV - def #{column_type}(*args) - options = args.extract_options! - column_names = args - - column_names.each { |name| column(name, '#{column_type}', options) } - end + def #{column_type}(*args) # def string(*args) + options = args.extract_options! # options = args.extract_options! + column_names = args # column_names = args + # + column_names.each { |name| column(name, '#{column_type}', options) } # column_names.each { |name| column(name, 'string', options) } + end # end EOV end @@ -676,24 +676,24 @@ module ActiveRecord # t.string(:goat, :sheep) %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type| class_eval <<-EOV - def #{column_type}(*args) - options = args.extract_options! - column_names = args - - column_names.each do |name| - column = ColumnDefinition.new(@base, name, '#{column_type}') - if options[:limit] - column.limit = options[:limit] - elsif native['#{column_type}'.to_sym].is_a?(Hash) - column.limit = native['#{column_type}'.to_sym][:limit] - end - column.precision = options[:precision] - column.scale = options[:scale] - column.default = options[:default] - column.null = options[:null] - @base.add_column(@table_name, name, column.sql_type, options) - end - end + def #{column_type}(*args) # def string(*args) + options = args.extract_options! # options = args.extract_options! + column_names = args # column_names = args + # + column_names.each do |name| # column_names.each do |name| + column = ColumnDefinition.new(@base, name, '#{column_type}') # column = ColumnDefinition.new(@base, name, 'string') + if options[:limit] # if options[:limit] + column.limit = options[:limit] # column.limit = options[:limit] + elsif native['#{column_type}'.to_sym].is_a?(Hash) # elsif native['string'.to_sym].is_a?(Hash) + column.limit = native['#{column_type}'.to_sym][:limit] # column.limit = native['string'.to_sym][:limit] + end # end + column.precision = options[:precision] # column.precision = options[:precision] + column.scale = options[:scale] # column.scale = options[:scale] + column.default = options[:default] # column.default = options[:default] + column.null = options[:null] # column.null = options[:null] + @base.add_column(@table_name, name, column.sql_type, options) # @base.add_column(@table_name, name, column.sql_type, options) + end # end + end # end EOV end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 46d4b6c89c..60729c63db 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -13,23 +13,25 @@ module MysqlCompat #:nodoc: # C driver >= 2.7 returns null values in each_hash if Mysql.const_defined?(:VERSION) && (Mysql::VERSION.is_a?(String) || Mysql::VERSION >= 20700) target.class_eval <<-'end_eval' - def all_hashes - rows = [] - each_hash { |row| rows << row } - rows - end + def all_hashes # def all_hashes + rows = [] # rows = [] + each_hash { |row| rows << row } # each_hash { |row| rows << row } + rows # rows + end # end end_eval # adapters before 2.7 don't have a version constant # and don't return null values in each_hash else target.class_eval <<-'end_eval' - def all_hashes - rows = [] - all_fields = fetch_fields.inject({}) { |fields, f| fields[f.name] = nil; fields } - each_hash { |row| rows << all_fields.dup.update(row) } - rows - end + def all_hashes # def all_hashes + rows = [] # rows = [] + all_fields = fetch_fields.inject({}) { |fields, f| # all_fields = fetch_fields.inject({}) { |fields, f| + fields[f.name] = nil; fields # fields[f.name] = nil; fields + } # } + each_hash { |row| rows << all_fields.dup.update(row) } # each_hash { |row| rows << all_fields.dup.update(row) } + rows # rows + end # end end_eval end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 60ec01b95e..6685cb8663 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -950,13 +950,13 @@ module ActiveRecord # should know about this but can't detect it there, so deal with it here. money_precision = (postgresql_version >= 80300) ? 19 : 10 PostgreSQLColumn.module_eval(<<-end_eval) - def extract_precision(sql_type) - if sql_type =~ /^money$/ - #{money_precision} - else - super - end - end + def extract_precision(sql_type) # def extract_precision(sql_type) + if sql_type =~ /^money$/ # if sql_type =~ /^money$/ + #{money_precision} # 19 + else # else + super # super + end # end + end # end end_eval configure_connection diff --git a/activerecord/lib/active_record/dirty.rb b/activerecord/lib/active_record/dirty.rb index a1760875ba..4c899f58e5 100644 --- a/activerecord/lib/active_record/dirty.rb +++ b/activerecord/lib/active_record/dirty.rb @@ -174,7 +174,7 @@ module ActiveRecord alias_attribute_without_dirty(new_name, old_name) DIRTY_SUFFIXES.each do |suffix| module_eval <<-STR, __FILE__, __LINE__+1 - def #{new_name}#{suffix}; self.#{old_name}#{suffix}; end + def #{new_name}#{suffix}; self.#{old_name}#{suffix}; end # def subject_changed?; self.title_changed?; end STR end end diff --git a/activerecord/lib/active_record/dynamic_scope_match.rb b/activerecord/lib/active_record/dynamic_scope_match.rb new file mode 100644 index 0000000000..f796ba669a --- /dev/null +++ b/activerecord/lib/active_record/dynamic_scope_match.rb @@ -0,0 +1,25 @@ +module ActiveRecord + class DynamicScopeMatch + def self.match(method) + ds_match = self.new(method) + ds_match.scope ? ds_match : nil + end + + def initialize(method) + @scope = true + case method.to_s + when /^scoped_by_([_a-zA-Z]\w*)$/ + names = $1 + else + @scope = nil + end + @attribute_names = names && names.split('_and_') + end + + attr_reader :scope, :attribute_names + + def scope? + !@scope.nil? + end + end +end diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index a8af89fcb9..eb92bc2545 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -1,20 +1,32 @@ module ActiveRecord - module QueryCache - # Enable the query cache within the block if Active Record is configured. - def cache(&block) - if ActiveRecord::Base.configurations.blank? - yield - else - connection.cache(&block) + class QueryCache + module ClassMethods + # Enable the query cache within the block if Active Record is configured. + def cache(&block) + if ActiveRecord::Base.configurations.blank? + yield + else + connection.cache(&block) + end end + + # Disable the query cache within the block if Active Record is configured. + def uncached(&block) + if ActiveRecord::Base.configurations.blank? + yield + else + connection.uncached(&block) + end + end + end + + def initialize(app) + @app = app end - # Disable the query cache within the block if Active Record is configured. - def uncached(&block) - if ActiveRecord::Base.configurations.blank? - yield - else - connection.uncached(&block) + def call(env) + ActiveRecord::Base.cache do + @app.call(env) end end end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index a9e0efa6fe..8dbe80a01a 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -23,8 +23,8 @@ module ActiveRecord write_attribute('created_at', t) if respond_to?(:created_at) && created_at.nil? write_attribute('created_on', t) if respond_to?(:created_on) && created_on.nil? - write_attribute('updated_at', t) if respond_to?(:updated_at) - write_attribute('updated_on', t) if respond_to?(:updated_on) + write_attribute('updated_at', t) if respond_to?(:updated_at) && updated_at.nil? + write_attribute('updated_on', t) if respond_to?(:updated_on) && updated_on.nil? end create_without_timestamps end diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 617b3f440f..6a9690ba85 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -203,7 +203,6 @@ module ActiveRecord if attr == "base" full_messages << message else - #key = :"activerecord.att.#{@base.class.name.underscore.to_sym}.#{attr}" attr_name = @base.class.human_attribute_name(attr) full_messages << attr_name + I18n.t('activerecord.errors.format.separator', :default => ' ') + message end diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index 8c9ae8a031..45e74ea024 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -104,6 +104,14 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase authors.first.posts.first.special_comments.first.post.very_special_comment end end + + def test_eager_association_loading_where_first_level_returns_nil + authors = Author.find(:all, :include => {:post_about_thinking => :comments}, :order => 'authors.id DESC') + assert_equal [authors(:mary), authors(:david)], authors + assert_no_queries do + authors[1].post_about_thinking.comments.first + end + end end require 'models/vertex' diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 42063d18a3..afbd9fddf9 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -729,12 +729,12 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal authors(:david), assert_no_queries { posts[0].author} posts = assert_queries(2) do - Post.find(:all, :include => :author, :joins => {:taggings => :tag}, :conditions => "tags.name = 'General'") + Post.find(:all, :include => :author, :joins => {:taggings => :tag}, :conditions => "tags.name = 'General'", :order => 'posts.id') end assert_equal posts(:welcome, :thinking), posts posts = assert_queries(2) do - Post.find(:all, :include => :author, :joins => {:taggings => {:tag => :taggings}}, :conditions => "taggings_tags.super_tag_id=2") + Post.find(:all, :include => :author, :joins => {:taggings => {:tag => :taggings}}, :conditions => "taggings_tags.super_tag_id=2", :order => 'posts.id') end assert_equal posts(:welcome, :thinking), posts @@ -771,4 +771,19 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal author_addresses(:david_address), authors[0].author_address end + def test_preload_belongs_to_uses_exclusive_scope + people = Person.males.find(:all, :include => :primary_contact) + assert_not_equal people.length, 0 + people.each do |person| + assert_no_queries {assert_not_nil person.primary_contact} + assert_equal Person.find(person.id).primary_contact, person.primary_contact + end + end + + def test_preload_has_many_uses_exclusive_scope + people = Person.males.find :all, :include => :agents + people.each do |person| + assert_equal Person.find(person.id).agents, person.agents + end + end end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 816ceb6855..20b9acda44 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -665,6 +665,19 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 1, Client.find_all_by_client_of(firm.id).size end + def test_dependent_association_respects_optional_hash_conditions_on_delete + firm = companies(:odegy) + Client.create(:client_of => firm.id, :name => "BigShot Inc.") + Client.create(:client_of => firm.id, :name => "SmallTime Inc.") + # only one of two clients is included in the association due to the :conditions key + assert_equal 2, Client.find_all_by_client_of(firm.id).size + assert_equal 1, firm.dependent_sanitized_conditional_clients_of_firm.size + firm.destroy + # only the correctly associated client should have been deleted + assert_equal 1, Client.find_all_by_client_of(firm.id).size + end + + def test_creation_respects_hash_condition ms_client = companies(:first_firm).clients_like_ms_with_hash_conditions.build diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 5f54931d00..0f03dae829 100755 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -16,6 +16,7 @@ require 'models/post' require 'models/comment' require 'models/minimalistic' require 'models/warehouse_thing' +require 'models/parrot' require 'rexml/document' class Category < ActiveRecord::Base; end @@ -1197,6 +1198,11 @@ class BasicsTest < ActiveRecord::TestCase assert b_true.value? end + def test_new_record_returns_boolean + assert_equal Topic.new.new_record?, true + assert_equal Topic.find(1).new_record?, false + end + def test_clone topic = Topic.find(1) cloned_topic = nil @@ -2071,6 +2077,15 @@ class BasicsTest < ActiveRecord::TestCase ActiveRecord::Base.logger = original_logger end + def test_create_with_custom_timestamps + custom_datetime = 1.hour.ago.beginning_of_day + + %w(created_at created_on updated_at updated_on).each do |attribute| + parrot = LiveParrot.create(:name => "colombian", attribute => custom_datetime) + assert_equal custom_datetime, parrot[attribute] + end + end + private def with_kcode(kcode) if RUBY_VERSION < '1.9' diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb index 6372b4f6aa..80a06116ad 100644 --- a/activerecord/test/cases/method_scoping_test.rb +++ b/activerecord/test/cases/method_scoping_test.rb @@ -27,6 +27,24 @@ class MethodScopingTest < ActiveRecord::TestCase end end + def test_scoped_find_last + highest_salary = Developer.find(:first, :order => "salary DESC") + + Developer.with_scope(:find => { :order => "salary" }) do + assert_equal highest_salary, Developer.last + end + end + + def test_scoped_find_last_preserves_scope + lowest_salary = Developer.find(:first, :order => "salary ASC") + highest_salary = Developer.find(:first, :order => "salary DESC") + + Developer.with_scope(:find => { :order => "salary" }) do + assert_equal highest_salary, Developer.last + assert_equal lowest_salary, Developer.first + end + end + def test_scoped_find_combines_conditions Developer.with_scope(:find => { :conditions => "salary = 9000" }) do assert_equal developers(:poor_jamis), Developer.find(:first, :conditions => "name = 'Jamis'") diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index 64e899780c..b152f95a15 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -278,3 +278,23 @@ class NamedScopeTest < ActiveRecord::TestCase assert_equal post.comments.size, Post.scoped(:joins => join).scoped(:joins => join, :conditions => "posts.id = #{post.id}").size end end + +class DynamicScopeMatchTest < ActiveRecord::TestCase + def test_scoped_by_no_match + assert_nil ActiveRecord::DynamicScopeMatch.match("not_scoped_at_all") + end + + def test_scoped_by + match = ActiveRecord::DynamicScopeMatch.match("scoped_by_age_and_sex_and_location") + assert_not_nil match + assert match.scope? + assert_equal %w(age sex location), match.attribute_names + end +end + +class DynamicScopeTest < ActiveRecord::TestCase + def test_dynamic_scope + assert_equal Post.scoped_by_author_id(1).find(1), Post.find(1) + assert_equal Post.scoped_by_author_id_and_title(1, "Welcome to the weblog").first, Post.find(:first, :conditions => { :author_id => 1, :title => "Welcome to the weblog"}) + end +end diff --git a/activerecord/test/fixtures/people.yml b/activerecord/test/fixtures/people.yml index d5a69e561d..3babb1fe59 100644 --- a/activerecord/test/fixtures/people.yml +++ b/activerecord/test/fixtures/people.yml @@ -1,6 +1,15 @@ michael: id: 1 first_name: Michael + primary_contact_id: 2 + gender: M david: id: 2 - first_name: David
\ No newline at end of file + first_name: David + primary_contact_id: 3 + gender: M +susan: + id: 3 + first_name: Susan + primary_contact_id: 2 + gender: F
\ No newline at end of file diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index 0e3fafa37c..3b27a9e272 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -80,6 +80,7 @@ class ExclusivelyDependentFirm < Company has_one :account, :foreign_key => "firm_id", :dependent => :delete has_many :dependent_sanitized_conditional_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :delete_all, :conditions => "name = 'BigShot Inc.'" has_many :dependent_conditional_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :delete_all, :conditions => ["name = ?", 'BigShot Inc.'] + has_many :dependent_hash_conditional_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :delete_all, :conditions => {:name => 'BigShot Inc.'} end class Client < Company diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb index 430d0b38f7..ec2f684a6e 100644 --- a/activerecord/test/models/person.rb +++ b/activerecord/test/models/person.rb @@ -7,4 +7,10 @@ class Person < ActiveRecord::Base has_many :jobs, :through => :references has_one :favourite_reference, :class_name => 'Reference', :conditions => ['favourite=?', true] has_many :posts_with_comments_sorted_by_comment_id, :through => :readers, :source => :post, :include => :comments, :order => 'comments.id' + + belongs_to :primary_contact, :class_name => 'Person' + has_many :agents, :class_name => 'Person', :foreign_key => 'primary_contact_id' + + named_scope :males, :conditions => { :gender => 'M' } + named_scope :females, :conditions => { :gender => 'F' } end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index fbacc692b4..8199cb8fc7 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -298,8 +298,10 @@ ActiveRecord::Schema.define do end create_table :people, :force => true do |t| - t.string :first_name, :null => false - t.integer :lock_version, :null => false, :default => 0 + t.string :first_name, :null => false + t.references :primary_contact + t.string :gender, :limit => 1 + t.integer :lock_version, :null => false, :default => 0 end create_table :pets, :primary_key => :pet_id ,:force => true do |t| diff --git a/activeresource/lib/active_resource/http_mock.rb b/activeresource/lib/active_resource/http_mock.rb index 9ed532b48c..0b4549f759 100644 --- a/activeresource/lib/active_resource/http_mock.rb +++ b/activeresource/lib/active_resource/http_mock.rb @@ -54,6 +54,9 @@ module ActiveResource end for method in [ :post, :put, :get, :delete, :head ] + # def post(path, request_headers = {}, body = nil, status = 200, response_headers = {}) + # @responses[Request.new(:post, path, nil, request_headers)] = Response.new(body || "", status, response_headers) + # end module_eval <<-EOE, __FILE__, __LINE__ def #{method}(path, request_headers = {}, body = nil, status = 200, response_headers = {}) @responses[Request.new(:#{method}, path, nil, request_headers)] = Response.new(body || "", status, response_headers) @@ -118,6 +121,11 @@ module ActiveResource end for method in [ :post, :put ] + # def post(path, body, headers) + # request = ActiveResource::Request.new(:post, path, body, headers) + # self.class.requests << request + # self.class.responses[request] || raise(InvalidRequestError.new("No response recorded for #{request}")) + # end module_eval <<-EOE, __FILE__, __LINE__ def #{method}(path, body, headers) request = ActiveResource::Request.new(:#{method}, path, body, headers) diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG index bc29c41d7c..7b9ccc888d 100644 --- a/activesupport/CHANGELOG +++ b/activesupport/CHANGELOG @@ -1,5 +1,9 @@ *2.3.0 [Edge]* +* Updated i18n gem to version 0.1.1 #1635 [Yaroslav Markin] + +* Add :allow_nil option to delegate. #1127 [Sergio Gil] + * Add Benchmark.ms convenience method to benchmark realtime in milliseconds. [Jeremy Kemper] * Updated included memcache-client to the 1.5.0.5 version which includes fixes from fiveruns and 37signals to deal with failover and timeouts #1535 [Joshua Sierles] diff --git a/activesupport/lib/active_support/buffered_logger.rb b/activesupport/lib/active_support/buffered_logger.rb index 445d8edf47..33bcf327f8 100644 --- a/activesupport/lib/active_support/buffered_logger.rb +++ b/activesupport/lib/active_support/buffered_logger.rb @@ -68,13 +68,13 @@ module ActiveSupport for severity in Severity.constants class_eval <<-EOT, __FILE__, __LINE__ - def #{severity.downcase}(message = nil, progname = nil, &block) - add(#{severity}, message, progname, &block) - end - - def #{severity.downcase}? - #{severity} >= @level - end + def #{severity.downcase}(message = nil, progname = nil, &block) # def debug(message = nil, progname = nil, &block) + add(#{severity}, message, progname, &block) # add(DEBUG, message, progname, &block) + end # end + # + def #{severity.downcase}? # def debug? + #{severity} >= @level # DEBUG >= @level + end # end EOT end diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index 5cdcaf5ad1..86e66e0588 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -192,13 +192,8 @@ module ActiveSupport end def should_run_callback?(*args) - if options[:if] - evaluate_method(options[:if], *args) - elsif options[:unless] - !evaluate_method(options[:unless], *args) - else - true - end + [options[:if]].flatten.compact.all? { |a| evaluate_method(a, *args) } && + ![options[:unless]].flatten.compact.any? { |a| evaluate_method(a, *args) } end end @@ -210,20 +205,24 @@ module ActiveSupport def define_callbacks(*callbacks) callbacks.each do |callback| class_eval <<-"end_eval" - def self.#{callback}(*methods, &block) - callbacks = CallbackChain.build(:#{callback}, *methods, &block) - (@#{callback}_callbacks ||= CallbackChain.new).concat callbacks - end - - def self.#{callback}_callback_chain - @#{callback}_callbacks ||= CallbackChain.new - - if superclass.respond_to?(:#{callback}_callback_chain) - CallbackChain.new(superclass.#{callback}_callback_chain + @#{callback}_callbacks) - else - @#{callback}_callbacks - end - end + def self.#{callback}(*methods, &block) # def self.before_save(*methods, &block) + callbacks = CallbackChain.build(:#{callback}, *methods, &block) # callbacks = CallbackChain.build(:before_save, *methods, &block) + @#{callback}_callbacks ||= CallbackChain.new # @before_save_callbacks ||= CallbackChain.new + @#{callback}_callbacks.concat callbacks # @before_save_callbacks.concat callbacks + end # end + # + def self.#{callback}_callback_chain # def self.before_save_callback_chain + @#{callback}_callbacks ||= CallbackChain.new # @before_save_callbacks ||= CallbackChain.new + # + if superclass.respond_to?(:#{callback}_callback_chain) # if superclass.respond_to?(:before_save_callback_chain) + CallbackChain.new( # CallbackChain.new( + superclass.#{callback}_callback_chain + # superclass.before_save_callback_chain + + @#{callback}_callbacks # @before_save_callbacks + ) # ) + else # else + @#{callback}_callbacks # @before_save_callbacks + end # end + end # end end_eval end end diff --git a/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb index 186ca69c05..c795871474 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb @@ -11,17 +11,17 @@ class Class syms.flatten.each do |sym| next if sym.is_a?(Hash) class_eval(<<-EOS, __FILE__, __LINE__) - unless defined? @@#{sym} - @@#{sym} = nil - end - - def self.#{sym} - @@#{sym} - end - - def #{sym} - @@#{sym} - end + unless defined? @@#{sym} # unless defined? @@hair_colors + @@#{sym} = nil # @@hair_colors = nil + end # end + # + def self.#{sym} # def self.hair_colors + @@#{sym} # @@hair_colors + end # end + # + def #{sym} # def hair_colors + @@#{sym} # @@hair_colors + end # end EOS end end @@ -30,19 +30,19 @@ class Class options = syms.extract_options! syms.flatten.each do |sym| class_eval(<<-EOS, __FILE__, __LINE__) - unless defined? @@#{sym} - @@#{sym} = nil - end - - def self.#{sym}=(obj) - @@#{sym} = obj - end - - #{" - def #{sym}=(obj) - @@#{sym} = obj - end - " unless options[:instance_writer] == false } + unless defined? @@#{sym} # unless defined? @@hair_colors + @@#{sym} = nil # @@hair_colors = nil + end # end + # + def self.#{sym}=(obj) # def self.hair_colors=(obj) + @@#{sym} = obj # @@hair_colors = obj + end # end + # + #{" # + def #{sym}=(obj) # def hair_colors=(obj) + @@#{sym} = obj # @@hair_colors = obj + end # end + " unless options[:instance_writer] == false } # # instance writer above is generated unless options[:instance_writer] == false EOS end end diff --git a/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb b/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb index 368317df9b..000ccf4d55 100644 --- a/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb +++ b/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb @@ -9,22 +9,23 @@ class Class class_name_to_stop_searching_on = self.superclass.name.blank? ? "Object" : self.superclass.name names.each do |name| class_eval <<-EOS - def self.#{name} - if defined?(@#{name}) - @#{name} - elsif superclass < #{class_name_to_stop_searching_on} && superclass.respond_to?(:#{name}) - superclass.#{name} - end - end - def #{name} - self.class.#{name} - end - def self.#{name}? - !!#{name} - end - def #{name}? - !!#{name} - end + def self.#{name} # def self.only_reader + if defined?(@#{name}) # if defined?(@only_reader) + @#{name} # @only_reader + elsif superclass < #{class_name_to_stop_searching_on} && # elsif superclass < Object && + superclass.respond_to?(:#{name}) # superclass.respond_to?(:only_reader) + superclass.#{name} # superclass.only_reader + end # end + end # end + def #{name} # def only_reader + self.class.#{name} # self.class.only_reader + end # end + def self.#{name}? # def self.only_reader? + !!#{name} # !!only_reader + end # end + def #{name}? # def only_reader? + !!#{name} # !!only_reader + end # end EOS end end @@ -32,9 +33,9 @@ class Class def superclass_delegating_writer(*names) names.each do |name| class_eval <<-EOS - def self.#{name}=(value) - @#{name} = value - end + def self.#{name}=(value) # def self.only_writer=(value) + @#{name} = value # @only_writer = value + end # end EOS end end diff --git a/activesupport/lib/active_support/core_ext/class/inheritable_attributes.rb b/activesupport/lib/active_support/core_ext/class/inheritable_attributes.rb index e6143a274b..1794afe77c 100644 --- a/activesupport/lib/active_support/core_ext/class/inheritable_attributes.rb +++ b/activesupport/lib/active_support/core_ext/class/inheritable_attributes.rb @@ -11,13 +11,13 @@ class Class # :nodoc: syms.each do |sym| next if sym.is_a?(Hash) class_eval <<-EOS - def self.#{sym} - read_inheritable_attribute(:#{sym}) - end - - def #{sym} - self.class.#{sym} - end + def self.#{sym} # def self.before_add_for_comments + read_inheritable_attribute(:#{sym}) # read_inheritable_attribute(:before_add_for_comments) + end # end + # + def #{sym} # def before_add_for_comments + self.class.#{sym} # self.class.before_add_for_comments + end # end EOS end end @@ -26,15 +26,15 @@ class Class # :nodoc: options = syms.extract_options! syms.each do |sym| class_eval <<-EOS - def self.#{sym}=(obj) - write_inheritable_attribute(:#{sym}, obj) - end - - #{" - def #{sym}=(obj) - self.class.#{sym} = obj - end - " unless options[:instance_writer] == false } + def self.#{sym}=(obj) # def self.color=(obj) + write_inheritable_attribute(:#{sym}, obj) # write_inheritable_attribute(:color, obj) + end # end + # + #{" # + def #{sym}=(obj) # def color=(obj) + self.class.#{sym} = obj # self.class.color = obj + end # end + " unless options[:instance_writer] == false } # # the writer above is generated unless options[:instance_writer] == false EOS end end @@ -43,15 +43,15 @@ class Class # :nodoc: options = syms.extract_options! syms.each do |sym| class_eval <<-EOS - def self.#{sym}=(obj) - write_inheritable_array(:#{sym}, obj) - end - - #{" - def #{sym}=(obj) - self.class.#{sym} = obj - end - " unless options[:instance_writer] == false } + def self.#{sym}=(obj) # def self.levels=(obj) + write_inheritable_array(:#{sym}, obj) # write_inheritable_array(:levels, obj) + end # end + # + #{" # + def #{sym}=(obj) # def levels=(obj) + self.class.#{sym} = obj # self.class.levels = obj + end # end + " unless options[:instance_writer] == false } # # the writer above is generated unless options[:instance_writer] == false EOS end end @@ -60,15 +60,15 @@ class Class # :nodoc: options = syms.extract_options! syms.each do |sym| class_eval <<-EOS - def self.#{sym}=(obj) - write_inheritable_hash(:#{sym}, obj) - end - - #{" - def #{sym}=(obj) - self.class.#{sym} = obj - end - " unless options[:instance_writer] == false } + def self.#{sym}=(obj) # def self.nicknames=(obj) + write_inheritable_hash(:#{sym}, obj) # write_inheritable_hash(:nicknames, obj) + end # end + # + #{" # + def #{sym}=(obj) # def nicknames=(obj) + self.class.#{sym} = obj # self.class.nicknames = obj + end # end + " unless options[:instance_writer] == false } # # the writer above is generated unless options[:instance_writer] == false EOS end end diff --git a/activesupport/lib/active_support/core_ext/hash/slice.rb b/activesupport/lib/active_support/core_ext/hash/slice.rb index 88df49a69f..d845a6d8ca 100644 --- a/activesupport/lib/active_support/core_ext/hash/slice.rb +++ b/activesupport/lib/active_support/core_ext/hash/slice.rb @@ -24,10 +24,17 @@ module ActiveSupport #:nodoc: end # Replaces the hash with only the given keys. + # Returns a hash contained the removed key/value pairs + # {:a => 1, :b => 2, :c => 3, :d => 4}.slice!(:a, :b) # => {:c => 3, :d =>4} def slice!(*keys) - replace(slice(*keys)) + keys = keys.map! { |key| convert_key(key) } if respond_to?(:convert_key) + omit = slice(*self.keys - keys) + hash = slice(*keys) + replace(hash) + omit end end end end end + diff --git a/activesupport/lib/active_support/core_ext/logger.rb b/activesupport/lib/active_support/core_ext/logger.rb index 24fe7294c9..858da7aa07 100644 --- a/activesupport/lib/active_support/core_ext/logger.rb +++ b/activesupport/lib/active_support/core_ext/logger.rb @@ -3,12 +3,12 @@ class Logger def self.define_around_helper(level) module_eval <<-end_eval - def around_#{level}(before_message, after_message, &block) - self.#{level}(before_message) - return_value = block.call(self) - self.#{level}(after_message) - return return_value - end + def around_#{level}(before_message, after_message, &block) # def around_debug(before_message, after_message, &block) + self.#{level}(before_message) # self.debug(before_message) + return_value = block.call(self) # return_value = block.call(self) + self.#{level}(after_message) # self.debug(after_message) + return return_value # return return_value + end # end end_eval end [:debug, :info, :error, :fatal].each {|level| define_around_helper(level) } diff --git a/activesupport/lib/active_support/core_ext/module/aliasing.rb b/activesupport/lib/active_support/core_ext/module/aliasing.rb index e640f64520..10fa520ba1 100644 --- a/activesupport/lib/active_support/core_ext/module/aliasing.rb +++ b/activesupport/lib/active_support/core_ext/module/aliasing.rb @@ -64,9 +64,9 @@ module ActiveSupport # e.title # => "Megastars" def alias_attribute(new_name, old_name) module_eval <<-STR, __FILE__, __LINE__+1 - def #{new_name}; self.#{old_name}; end - def #{new_name}?; self.#{old_name}?; end - def #{new_name}=(v); self.#{old_name} = v; end + def #{new_name}; self.#{old_name}; end # def subject; self.title; end + def #{new_name}?; self.#{old_name}?; end # def subject?; self.title?; end + def #{new_name}=(v); self.#{old_name} = v; end # def subject=(v); self.title = v; end STR end end diff --git a/activesupport/lib/active_support/core_ext/module/attr_accessor_with_default.rb b/activesupport/lib/active_support/core_ext/module/attr_accessor_with_default.rb index 683789d853..4d0198f028 100644 --- a/activesupport/lib/active_support/core_ext/module/attr_accessor_with_default.rb +++ b/activesupport/lib/active_support/core_ext/module/attr_accessor_with_default.rb @@ -22,10 +22,10 @@ class Module raise 'Default value or block required' unless !default.nil? || block define_method(sym, block_given? ? block : Proc.new { default }) module_eval(<<-EVAL, __FILE__, __LINE__) - def #{sym}=(value) - class << self; attr_reader :#{sym} end - @#{sym} = value - end + def #{sym}=(value) # def age=(value) + class << self; attr_reader :#{sym} end # class << self; attr_reader :age end + @#{sym} = value # @age = value + end # end EVAL end end diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb index 51e1c9af90..9402cb8534 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -15,17 +15,17 @@ class Module syms.each do |sym| next if sym.is_a?(Hash) class_eval(<<-EOS, __FILE__, __LINE__) - unless defined? @@#{sym} - @@#{sym} = nil - end - - def self.#{sym} - @@#{sym} - end - - def #{sym} - @@#{sym} - end + unless defined? @@#{sym} # unless defined? @@pagination_options + @@#{sym} = nil # @@pagination_options = nil + end # end + # + def self.#{sym} # def self.pagination_options + @@#{sym} # @@pagination_options + end # end + # + def #{sym} # def pagination_options + @@#{sym} # @@pagination_options + end # end EOS end end @@ -34,19 +34,19 @@ class Module options = syms.extract_options! syms.each do |sym| class_eval(<<-EOS, __FILE__, __LINE__) - unless defined? @@#{sym} - @@#{sym} = nil - end - - def self.#{sym}=(obj) - @@#{sym} = obj - end - - #{" - def #{sym}=(obj) - @@#{sym} = obj - end - " unless options[:instance_writer] == false } + unless defined? @@#{sym} # unless defined? @@pagination_options + @@#{sym} = nil # @@pagination_options = nil + end # end + # + def self.#{sym}=(obj) # def self.pagination_options=(obj) + @@#{sym} = obj # @@pagination_options = obj + end # end + # + #{" # + def #{sym}=(obj) # def pagination_options=(obj) + @@#{sym} = obj # @@pagination_options = obj + end # end + " unless options[:instance_writer] == false } # # instance writer above is generated unless options[:instance_writer] == false EOS end end diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index 2905eebc85..fb4b5f0f3c 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -72,6 +72,30 @@ class Module # invoice.customer_name # => "John Doe" # invoice.customer_address # => "Vimmersvej 13" # + # If the object to which you delegate can be nil, you may want to use the + # :allow_nil option. In that case, it returns nil instead of raising a + # NoMethodError exception: + # + # class Foo + # attr_accessor :bar + # def initialize(bar = nil) + # @bar = bar + # end + # delegate :zoo, :to => :bar + # end + # + # Foo.new.zoo # raises NoMethodError exception (you called nil.zoo) + # + # class Foo + # attr_accessor :bar + # def initialize(bar = nil) + # @bar = bar + # end + # delegate :zoo, :to => :bar, :allow_nil => true + # end + # + # Foo.new.zoo # returns nil + # def delegate(*methods) options = methods.pop unless options.is_a?(Hash) && to = options[:to] @@ -84,11 +108,13 @@ class Module prefix = options[:prefix] && "#{options[:prefix] == true ? to : options[:prefix]}_" + allow_nil = options[:allow_nil] && "#{to} && " + methods.each do |method| module_eval(<<-EOS, "(__DELEGATION__)", 1) - def #{prefix}#{method}(*args, &block) - #{to}.__send__(#{method.inspect}, *args, &block) - end + def #{prefix}#{method}(*args, &block) # def customer_name(*args, &block) + #{allow_nil}#{to}.__send__(#{method.inspect}, *args, &block) # client && client.__send__(:name, *args, &block) + end # end EOS end end diff --git a/activesupport/lib/active_support/core_ext/module/synchronization.rb b/activesupport/lib/active_support/core_ext/module/synchronization.rb index 251606024e..069db3fed0 100644 --- a/activesupport/lib/active_support/core_ext/module/synchronization.rb +++ b/activesupport/lib/active_support/core_ext/module/synchronization.rb @@ -26,11 +26,11 @@ class Module end module_eval(<<-EOS, __FILE__, __LINE__) - def #{aliased_method}_with_synchronization#{punctuation}(*args, &block) - #{with}.synchronize do - #{aliased_method}_without_synchronization#{punctuation}(*args, &block) - end - end + def #{aliased_method}_with_synchronization#{punctuation}(*args, &block) # def expire_with_synchronization(*args, &block) + #{with}.synchronize do # @@lock.synchronize do + #{aliased_method}_without_synchronization#{punctuation}(*args, &block) # expire_without_synchronization(*args, &block) + end # end + end # end EOS alias_method_chain method, :synchronization diff --git a/activesupport/lib/active_support/deprecation.rb b/activesupport/lib/active_support/deprecation.rb index 25b26e9c96..d20151661b 100644 --- a/activesupport/lib/active_support/deprecation.rb +++ b/activesupport/lib/active_support/deprecation.rb @@ -90,10 +90,15 @@ module ActiveSupport method_names.each do |method_name| alias_method_chain(method_name, :deprecation) do |target, punctuation| class_eval(<<-EOS, __FILE__, __LINE__) - def #{target}_with_deprecation#{punctuation}(*args, &block) - ::ActiveSupport::Deprecation.warn(self.class.deprecated_method_warning(:#{method_name}, #{options[method_name].inspect}), caller) - #{target}_without_deprecation#{punctuation}(*args, &block) - end + def #{target}_with_deprecation#{punctuation}(*args, &block) # def generate_secret_with_deprecation(*args, &block) + ::ActiveSupport::Deprecation.warn( # ::ActiveSupport::Deprecation.warn( + self.class.deprecated_method_warning( # self.class.deprecated_method_warning( + :#{method_name}, # :generate_secret, + #{options[method_name].inspect}), # "You should use ActiveSupport::SecureRandom.hex(64)"), + caller # caller + ) # ) + #{target}_without_deprecation#{punctuation}(*args, &block) # generate_secret_without_deprecation(*args, &block) + end # end EOS end end diff --git a/activesupport/lib/active_support/memoizable.rb b/activesupport/lib/active_support/memoizable.rb index 9f2fd3a401..952b4d8063 100644 --- a/activesupport/lib/active_support/memoizable.rb +++ b/activesupport/lib/active_support/memoizable.rb @@ -59,34 +59,36 @@ module ActiveSupport memoized_ivar = ActiveSupport::Memoizable.memoized_ivar_for(symbol) class_eval <<-EOS, __FILE__, __LINE__ - include InstanceMethods - - raise "Already memoized #{symbol}" if method_defined?(:#{original_method}) - alias #{original_method} #{symbol} - - if instance_method(:#{symbol}).arity == 0 - def #{symbol}(reload = false) - if reload || !defined?(#{memoized_ivar}) || #{memoized_ivar}.empty? - #{memoized_ivar} = [#{original_method}.freeze] - end - #{memoized_ivar}[0] - end - else - def #{symbol}(*args) - #{memoized_ivar} ||= {} unless frozen? - reload = args.pop if args.last == true || args.last == :reload - - if defined?(#{memoized_ivar}) && #{memoized_ivar} - if !reload && #{memoized_ivar}.has_key?(args) - #{memoized_ivar}[args] - elsif #{memoized_ivar} - #{memoized_ivar}[args] = #{original_method}(*args).freeze - end - else - #{original_method}(*args) - end - end - end + include InstanceMethods # include InstanceMethods + # + if method_defined?(:#{original_method}) # if method_defined?(:_unmemoized_mime_type) + raise "Already memoized #{symbol}" # raise "Already memoized mime_type" + end # end + alias #{original_method} #{symbol} # alias _unmemoized_mime_type mime_type + # + if instance_method(:#{symbol}).arity == 0 # if instance_method(:mime_type).arity == 0 + def #{symbol}(reload = false) # def mime_type(reload = false) + if reload || !defined?(#{memoized_ivar}) || #{memoized_ivar}.empty? # if reload || !defined?(@_memoized_mime_type) || @_memoized_mime_type.empty? + #{memoized_ivar} = [#{original_method}.freeze] # @_memoized_mime_type = [_unmemoized_mime_type.freeze] + end # end + #{memoized_ivar}[0] # @_memoized_mime_type[0] + end # end + else # else + def #{symbol}(*args) # def mime_type(*args) + #{memoized_ivar} ||= {} unless frozen? # @_memoized_mime_type ||= {} unless frozen? + reload = args.pop if args.last == true || args.last == :reload # reload = args.pop if args.last == true || args.last == :reload + # + if defined?(#{memoized_ivar}) && #{memoized_ivar} # if defined?(@_memoized_mime_type) && @_memoized_mime_type + if !reload && #{memoized_ivar}.has_key?(args) # if !reload && @_memoized_mime_type.has_key?(args) + #{memoized_ivar}[args] # @_memoized_mime_type[args] + elsif #{memoized_ivar} # elsif @_memoized_mime_type + #{memoized_ivar}[args] = #{original_method}(*args).freeze # @_memoized_mime_type[args] = _unmemoized_mime_type(*args).freeze + end # end + else # else + #{original_method}(*args) # _unmemoized_mime_type(*args) + end # end + end # end + end # end EOS end end diff --git a/activesupport/lib/active_support/multibyte/unicode_database.rb b/activesupport/lib/active_support/multibyte/unicode_database.rb index 3b8cf8f9eb..a08f38cdbb 100644 --- a/activesupport/lib/active_support/multibyte/unicode_database.rb +++ b/activesupport/lib/active_support/multibyte/unicode_database.rb @@ -24,10 +24,10 @@ module ActiveSupport #:nodoc: # Lazy load the Unicode database so it's only loaded when it's actually used ATTRIBUTES.each do |attr_name| class_eval(<<-EOS, __FILE__, __LINE__) - def #{attr_name} - load - @#{attr_name} - end + def #{attr_name} # def codepoints + load # load + @#{attr_name} # @codepoints + end # end EOS end diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 9a2d283b30..72ff684fcc 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -234,9 +234,9 @@ module ActiveSupport %w(year mon month day mday wday yday hour min sec to_date).each do |method_name| class_eval <<-EOV - def #{method_name} - time.#{method_name} - end + def #{method_name} # def year + time.#{method_name} # time.year + end # end EOV end diff --git a/activesupport/lib/active_support/vendor.rb b/activesupport/lib/active_support/vendor.rb index 4525bba559..3d7d52ca71 100644 --- a/activesupport/lib/active_support/vendor.rb +++ b/activesupport/lib/active_support/vendor.rb @@ -22,8 +22,8 @@ end # TODO I18n gem has not been released yet # begin -# gem 'i18n', '~> 0.0.1' +# gem 'i18n', '~> 0.1.1' # rescue Gem::LoadError - $:.unshift "#{File.dirname(__FILE__)}/vendor/i18n-0.0.1" + $:.unshift "#{File.dirname(__FILE__)}/vendor/i18n-0.1.1/lib" require 'i18n' # end diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/.gitignore b/activesupport/lib/active_support/vendor/i18n-0.1.1/.gitignore new file mode 100644 index 0000000000..0f41a39f89 --- /dev/null +++ b/activesupport/lib/active_support/vendor/i18n-0.1.1/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +test/rails/fixtures +doc diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/MIT-LICENSE b/activesupport/lib/active_support/vendor/i18n-0.1.1/MIT-LICENSE new file mode 100755 index 0000000000..ed8e9ee66d --- /dev/null +++ b/activesupport/lib/active_support/vendor/i18n-0.1.1/MIT-LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2008 The Ruby I18n team + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/README.textile b/activesupport/lib/active_support/vendor/i18n-0.1.1/README.textile new file mode 100644 index 0000000000..a07fc8426d --- /dev/null +++ b/activesupport/lib/active_support/vendor/i18n-0.1.1/README.textile @@ -0,0 +1,20 @@ +h1. Ruby I18n gem + +I18n and localization solution for Ruby. + +For information please refer to http://rails-i18n.org + +h2. Authors + +* "Matt Aimonetti":http://railsontherun.com +* "Sven Fuchs":http://www.artweb-design.de +* "Joshua Harvey":http://www.workingwithrails.com/person/759-joshua-harvey +* "Saimon Moore":http://saimonmoore.net +* "Stephan Soller":http://www.arkanis-development.de + +h2. License + +MIT License. See the included MIT-LICENCE file. + + + diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/Rakefile b/activesupport/lib/active_support/vendor/i18n-0.1.1/Rakefile new file mode 100644 index 0000000000..2164e13e69 --- /dev/null +++ b/activesupport/lib/active_support/vendor/i18n-0.1.1/Rakefile @@ -0,0 +1,5 @@ +task :default => [:test] + +task :test do + ruby "test/all.rb" +end diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/i18n.gemspec b/activesupport/lib/active_support/vendor/i18n-0.1.1/i18n.gemspec new file mode 100644 index 0000000000..14294606bd --- /dev/null +++ b/activesupport/lib/active_support/vendor/i18n-0.1.1/i18n.gemspec @@ -0,0 +1,27 @@ +Gem::Specification.new do |s| + s.name = "i18n" + s.version = "0.1.1" + s.date = "2008-10-26" + s.summary = "Internationalization support for Ruby" + s.email = "rails-i18n@googlegroups.com" + s.homepage = "http://rails-i18n.org" + s.description = "Add Internationalization support to your Ruby application." + s.has_rdoc = false + s.authors = ['Sven Fuchs', 'Joshua Harvey', 'Matt Aimonetti', 'Stephan Soller', 'Saimon Moore'] + s.files = [ + 'i18n.gemspec', + 'lib/i18n/backend/simple.rb', + 'lib/i18n/exceptions.rb', + 'lib/i18n.rb', + 'MIT-LICENSE', + 'README.textile' + ] + s.test_files = [ + 'test/all.rb', + 'test/i18n_exceptions_test.rb', + 'test/i18n_test.rb', + 'test/locale/en.rb', + 'test/locale/en.yml', + 'test/simple_backend_test.rb' + ] +end diff --git a/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n.rb b/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n.rb index 2ffe3618b5..b5ad094d0e 100755 --- a/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n.rb +++ b/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n.rb @@ -2,39 +2,39 @@ # Sven Fuchs (http://www.artweb-design.de), # Joshua Harvey (http://www.workingwithrails.com/person/759-joshua-harvey), # Saimon Moore (http://saimonmoore.net), -# Stephan Soller (http://www.arkanis-development.de/) +# Stephan Soller (http://www.arkanis-development.de/) # Copyright:: Copyright (c) 2008 The Ruby i18n Team # License:: MIT require 'i18n/backend/simple' require 'i18n/exceptions' -module I18n +module I18n @@backend = nil @@load_path = nil @@default_locale = :'en' @@exception_handler = :default_exception_handler - + class << self # Returns the current backend. Defaults to +Backend::Simple+. def backend @@backend ||= Backend::Simple.new end - + # Sets the current backend. Used to set a custom backend. - def backend=(backend) + def backend=(backend) @@backend = backend end - - # Returns the current default locale. Defaults to 'en' + + # Returns the current default locale. Defaults to :'en' def default_locale - @@default_locale + @@default_locale end - + # Sets the current default locale. Used to set a custom default locale. - def default_locale=(locale) - @@default_locale = locale + def default_locale=(locale) + @@default_locale = locale end - + # Returns the current locale. Defaults to I18n.default_locale. def locale Thread.current[:locale] ||= default_locale @@ -44,12 +44,12 @@ module I18n def locale=(locale) Thread.current[:locale] = locale end - + # Sets the exception handler. def exception_handler=(exception_handler) @@exception_handler = exception_handler end - + # Allow clients to register paths providing translation data sources. The # backend defines acceptable sources. # @@ -74,25 +74,25 @@ module I18n def reload! backend.reload! end - - # Translates, pluralizes and interpolates a given key using a given locale, + + # Translates, pluralizes and interpolates a given key using a given locale, # scope, and default, as well as interpolation values. # # *LOOKUP* # - # Translation data is organized as a nested hash using the upper-level keys - # as namespaces. <em>E.g.</em>, ActionView ships with the translation: + # Translation data is organized as a nested hash using the upper-level keys + # as namespaces. <em>E.g.</em>, ActionView ships with the translation: # <tt>:date => {:formats => {:short => "%b %d"}}</tt>. - # - # Translations can be looked up at any level of this hash using the key argument - # and the scope option. <em>E.g.</em>, in this example <tt>I18n.t :date</tt> + # + # Translations can be looked up at any level of this hash using the key argument + # and the scope option. <em>E.g.</em>, in this example <tt>I18n.t :date</tt> # returns the whole translations hash <tt>{:formats => {:short => "%b %d"}}</tt>. - # - # Key can be either a single key or a dot-separated key (both Strings and Symbols + # + # Key can be either a single key or a dot-separated key (both Strings and Symbols # work). <em>E.g.</em>, the short format can be looked up using both: # I18n.t 'date.formats.short' # I18n.t :'date.formats.short' - # + # # Scope can be either a single key, a dot-separated key or an array of keys # or dot-separated keys. Keys and scopes can be combined freely. So these # examples will all look up the same short date format: @@ -105,9 +105,9 @@ module I18n # # Translations can contain interpolation variables which will be replaced by # values passed to #translate as part of the options hash, with the keys matching - # the interpolation variable names. + # the interpolation variable names. # - # <em>E.g.</em>, with a translation <tt>:foo => "foo {{bar}}"</tt> the option + # <em>E.g.</em>, with a translation <tt>:foo => "foo {{bar}}"</tt> the option # value for the key +bar+ will be interpolated into the translation: # I18n.t :foo, :bar => 'baz' # => 'foo baz' # @@ -116,7 +116,7 @@ module I18n # Translation data can contain pluralized translations. Pluralized translations # are arrays of singluar/plural versions of translations like <tt>['Foo', 'Foos']</tt>. # - # Note that <tt>I18n::Backend::Simple</tt> only supports an algorithm for English + # Note that <tt>I18n::Backend::Simple</tt> only supports an algorithm for English # pluralization rules. Other algorithms can be supported by custom backends. # # This returns the singular version of a pluralized translation: @@ -125,9 +125,9 @@ module I18n # These both return the plural version of a pluralized translation: # I18n.t :foo, :count => 0 # => 'Foos' # I18n.t :foo, :count => 2 # => 'Foos' - # - # The <tt>:count</tt> option can be used both for pluralization and interpolation. - # <em>E.g.</em>, with the translation + # + # The <tt>:count</tt> option can be used both for pluralization and interpolation. + # <em>E.g.</em>, with the translation # <tt>:foo => ['{{count}} foo', '{{count}} foos']</tt>, count will # be interpolated to the pluralized translation: # I18n.t :foo, :count => 1 # => '1 foo' @@ -137,11 +137,11 @@ module I18n # This returns the translation for <tt>:foo</tt> or <tt>default</tt> if no translation was found: # I18n.t :foo, :default => 'default' # - # This returns the translation for <tt>:foo</tt> or the translation for <tt>:bar</tt> if no + # This returns the translation for <tt>:foo</tt> or the translation for <tt>:bar</tt> if no # translation for <tt>:foo</tt> was found: # I18n.t :foo, :default => :bar # - # Returns the translation for <tt>:foo</tt> or the translation for <tt>:bar</tt> + # Returns the translation for <tt>:foo</tt> or the translation for <tt>:bar</tt> # or <tt>default</tt> if no translations for <tt>:foo</tt> and <tt>:bar</tt> were found. # I18n.t :foo, :default => [:bar, 'default'] # @@ -161,9 +161,9 @@ module I18n rescue I18n::ArgumentError => e raise e if options[:raise] send(@@exception_handler, e, locale, key, options) - end + end alias :t :translate - + # Localizes certain objects, such as dates and numbers to local formatting. def localize(object, options = {}) locale = options[:locale] || I18n.locale @@ -171,7 +171,7 @@ module I18n backend.localize(locale, object, format) end alias :l :localize - + protected # Handles exceptions raised in the backend. All exceptions except for # MissingTranslationData exceptions are re-raised. When a MissingTranslationData @@ -181,7 +181,7 @@ module I18n return exception.message if MissingTranslationData === exception raise exception end - + # Merges the given locale, key and scope into a single array of keys. # Splits keys that contain dots into multiple keys. Makes sure all # keys are Symbols. @@ -191,4 +191,4 @@ module I18n keys.flatten.map { |k| k.to_sym } end end -end
\ No newline at end of file +end diff --git a/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n/backend/simple.rb b/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n/backend/simple.rb index bdda55d3fe..d298b3a85a 100644 --- a/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n/backend/simple.rb +++ b/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n/backend/simple.rb @@ -6,21 +6,21 @@ module I18n INTERPOLATION_RESERVED_KEYS = %w(scope default) MATCH = /(\\\\)?\{\{([^\}]+)\}\}/ - # Accepts a list of paths to translation files. Loads translations from + # Accepts a list of paths to translation files. Loads translations from # plain Ruby (*.rb) or YAML files (*.yml). See #load_rb and #load_yml # for details. def load_translations(*filenames) filenames.each { |filename| load_file(filename) } end - - # Stores translations for the given locale in memory. + + # Stores translations for the given locale in memory. # This uses a deep merge for the translations hash, so existing # translations will be overwritten by new ones only at the deepest # level of the hash. def store_translations(locale, data) merge_translations(locale, data) end - + def translate(locale, key, options = {}) raise InvalidLocale.new(locale) if locale.nil? return key.map { |k| translate(locale, k, options) } if key.is_a? Array @@ -41,13 +41,13 @@ module I18n entry = interpolate(locale, entry, values) entry end - - # Acts the same as +strftime+, but returns a localized version of the - # formatted date string. Takes a key from the date/time formats - # translations as a format argument (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>). + + # Acts the same as +strftime+, but returns a localized version of the + # formatted date string. Takes a key from the date/time formats + # translations as a format argument (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>). def localize(locale, object, format = :default) raise ArgumentError, "Object must be a Date, DateTime or Time object. #{object.inspect} given." unless object.respond_to?(:strftime) - + type = object.respond_to?(:sec) ? 'time' : 'date' # TODO only translate these if format is a String? formats = translate(locale, :"#{type}.formats") @@ -57,14 +57,14 @@ module I18n # TODO only translate these if the format string is actually present # TODO check which format strings are present, then bulk translate then, then replace them - format.gsub!(/%a/, translate(locale, :"date.abbr_day_names")[object.wday]) + format.gsub!(/%a/, translate(locale, :"date.abbr_day_names")[object.wday]) format.gsub!(/%A/, translate(locale, :"date.day_names")[object.wday]) format.gsub!(/%b/, translate(locale, :"date.abbr_month_names")[object.mon]) format.gsub!(/%B/, translate(locale, :"date.month_names")[object.mon]) format.gsub!(/%p/, translate(locale, :"time.#{object.hour < 12 ? :am : :pm}")) if object.respond_to? :hour object.strftime(format) end - + def initialized? @initialized ||= false end @@ -79,12 +79,12 @@ module I18n load_translations(*I18n.load_path) @initialized = true end - + def translations @translations ||= {} end - - # Looks up a translation from the translations hash. Returns nil if + + # Looks up a translation from the translations hash. Returns nil if # eiher key is nil, or locale, scope or key do not exist as a key in the # nested translations hash. Splits keys or scopes containing dots # into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as @@ -101,19 +101,19 @@ module I18n end end end - - # Evaluates a default translation. + + # Evaluates a default translation. # If the given default is a String it is used literally. If it is a Symbol # it will be translated with the given options. If it is an Array the first # translation yielded will be returned. - # - # <em>I.e.</em>, <tt>default(locale, [:foo, 'default'])</tt> will return +default+ if + # + # <em>I.e.</em>, <tt>default(locale, [:foo, 'default'])</tt> will return +default+ if # <tt>translate(locale, :foo)</tt> does not yield a result. def default(locale, default, options = {}) case default when String then default when Symbol then translate locale, default, options - when Array then default.each do |obj| + when Array then default.each do |obj| result = default(locale, obj, options.dup) and return result end and nil end @@ -135,10 +135,10 @@ module I18n end # Interpolates values into a given string. - # - # interpolate "file {{file}} opened by \\{{user}}", :file => 'test.txt', :user => 'Mr. X' + # + # interpolate "file {{file}} opened by \\{{user}}", :file => 'test.txt', :user => 'Mr. X' # # => "file test.txt opened by {{user}}" - # + # # Note that you have to double escape the <tt>\\</tt> when you want to escape # the <tt>{{...}}</tt> key in a string (once for the string and once for the # interpolation). @@ -167,8 +167,8 @@ module I18n result.force_encoding(original_encoding) if original_encoding result end - - # Loads a single translations file by delegating to #load_rb or + + # Loads a single translations file by delegating to #load_rb or # #load_yml depending on the file extension and directly merges the # data to the existing translations. Raises I18n::UnknownFileType # for all other file extensions. @@ -178,19 +178,19 @@ module I18n data = send :"load_#{type}", filename # TODO raise a meaningful exception if this does not yield a Hash data.each { |locale, d| merge_translations(locale, d) } end - + # Loads a plain Ruby translations file. eval'ing the file must yield # a Hash containing translation data with locales as toplevel keys. def load_rb(filename) eval(IO.read(filename), binding, filename) end - - # Loads a YAML translations file. The data must have locales as + + # Loads a YAML translations file. The data must have locales as # toplevel keys. def load_yml(filename) YAML::load(IO.read(filename)) end - + # Deep merges the given translations hash with the existing translations # for the given locale def merge_translations(locale, data) @@ -202,7 +202,7 @@ module I18n merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 } translations[locale].merge!(data, &merger) end - + # Return a new hash with all keys and nested keys converted to symbols. def deep_symbolize_keys(hash) hash.inject({}) { |result, (key, value)| diff --git a/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n/exceptions.rb b/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n/exceptions.rb index 0f3eff1071..b5cea7acb4 100644 --- a/activesupport/lib/active_support/vendor/i18n-0.0.1/i18n/exceptions.rb +++ b/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n/exceptions.rb @@ -1,6 +1,6 @@ module I18n class ArgumentError < ::ArgumentError; end - + class InvalidLocale < ArgumentError attr_reader :locale def initialize(locale) @@ -42,7 +42,7 @@ module I18n super "reserved key #{key.inspect} used in #{string.inspect}" end end - + class UnknownFileType < ArgumentError attr_reader :type, :filename def initialize(type, filename) @@ -50,4 +50,4 @@ module I18n super "can not load translations from #{filename}, the file type #{type} is not known" end end -end
\ No newline at end of file +end
\ No newline at end of file diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/all.rb b/activesupport/lib/active_support/vendor/i18n-0.1.1/test/all.rb new file mode 100644 index 0000000000..353712da49 --- /dev/null +++ b/activesupport/lib/active_support/vendor/i18n-0.1.1/test/all.rb @@ -0,0 +1,5 @@ +dir = File.dirname(__FILE__) +require dir + '/i18n_test.rb' +require dir + '/simple_backend_test.rb' +require dir + '/i18n_exceptions_test.rb' +# *require* dir + '/custom_backend_test.rb'
\ No newline at end of file diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/i18n_exceptions_test.rb b/activesupport/lib/active_support/vendor/i18n-0.1.1/test/i18n_exceptions_test.rb new file mode 100644 index 0000000000..dfcba6901f --- /dev/null +++ b/activesupport/lib/active_support/vendor/i18n-0.1.1/test/i18n_exceptions_test.rb @@ -0,0 +1,100 @@ +$:.unshift "lib" + +require 'rubygems' +require 'test/unit' +require 'mocha' +require 'i18n' +require 'active_support' + +class I18nExceptionsTest < Test::Unit::TestCase + def test_invalid_locale_stores_locale + force_invalid_locale + rescue I18n::ArgumentError => e + assert_nil e.locale + end + + def test_invalid_locale_message + force_invalid_locale + rescue I18n::ArgumentError => e + assert_equal 'nil is not a valid locale', e.message + end + + def test_missing_translation_data_stores_locale_key_and_options + force_missing_translation_data + rescue I18n::ArgumentError => e + options = {:scope => :bar} + assert_equal 'de', e.locale + assert_equal :foo, e.key + assert_equal options, e.options + end + + def test_missing_translation_data_message + force_missing_translation_data + rescue I18n::ArgumentError => e + assert_equal 'translation missing: de, bar, foo', e.message + end + + def test_invalid_pluralization_data_stores_entry_and_count + force_invalid_pluralization_data + rescue I18n::ArgumentError => e + assert_equal [:bar], e.entry + assert_equal 1, e.count + end + + def test_invalid_pluralization_data_message + force_invalid_pluralization_data + rescue I18n::ArgumentError => e + assert_equal 'translation data [:bar] can not be used with :count => 1', e.message + end + + def test_missing_interpolation_argument_stores_key_and_string + force_missing_interpolation_argument + rescue I18n::ArgumentError => e + assert_equal 'bar', e.key + assert_equal "{{bar}}", e.string + end + + def test_missing_interpolation_argument_message + force_missing_interpolation_argument + rescue I18n::ArgumentError => e + assert_equal 'interpolation argument bar missing in "{{bar}}"', e.message + end + + def test_reserved_interpolation_key_stores_key_and_string + force_reserved_interpolation_key + rescue I18n::ArgumentError => e + assert_equal 'scope', e.key + assert_equal "{{scope}}", e.string + end + + def test_reserved_interpolation_key_message + force_reserved_interpolation_key + rescue I18n::ArgumentError => e + assert_equal 'reserved key "scope" used in "{{scope}}"', e.message + end + + private + def force_invalid_locale + I18n.backend.translate nil, :foo + end + + def force_missing_translation_data + I18n.backend.store_translations 'de', :bar => nil + I18n.backend.translate 'de', :foo, :scope => :bar + end + + def force_invalid_pluralization_data + I18n.backend.store_translations 'de', :foo => [:bar] + I18n.backend.translate 'de', :foo, :count => 1 + end + + def force_missing_interpolation_argument + I18n.backend.store_translations 'de', :foo => "{{bar}}" + I18n.backend.translate 'de', :foo, :baz => 'baz' + end + + def force_reserved_interpolation_key + I18n.backend.store_translations 'de', :foo => "{{scope}}" + I18n.backend.translate 'de', :foo, :baz => 'baz' + end +end
\ No newline at end of file diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/i18n_test.rb b/activesupport/lib/active_support/vendor/i18n-0.1.1/test/i18n_test.rb new file mode 100644 index 0000000000..bbb35ec809 --- /dev/null +++ b/activesupport/lib/active_support/vendor/i18n-0.1.1/test/i18n_test.rb @@ -0,0 +1,125 @@ +$:.unshift "lib" + +require 'rubygems' +require 'test/unit' +require 'mocha' +require 'i18n' +require 'active_support' + +class I18nTest < Test::Unit::TestCase + def setup + I18n.backend.store_translations :'en', { + :currency => { + :format => { + :separator => '.', + :delimiter => ',', + } + } + } + end + + def test_uses_simple_backend_set_by_default + assert I18n.backend.is_a?(I18n::Backend::Simple) + end + + def test_can_set_backend + assert_nothing_raised{ I18n.backend = self } + assert_equal self, I18n.backend + I18n.backend = I18n::Backend::Simple.new + end + + def test_uses_en_us_as_default_locale_by_default + assert_equal 'en', I18n.default_locale + end + + def test_can_set_default_locale + assert_nothing_raised{ I18n.default_locale = 'de' } + assert_equal 'de', I18n.default_locale + I18n.default_locale = 'en' + end + + def test_uses_default_locale_as_locale_by_default + assert_equal I18n.default_locale, I18n.locale + end + + def test_can_set_locale_to_thread_current + assert_nothing_raised{ I18n.locale = 'de' } + assert_equal 'de', I18n.locale + assert_equal 'de', Thread.current[:locale] + I18n.locale = 'en' + end + + def test_can_set_exception_handler + assert_nothing_raised{ I18n.exception_handler = :custom_exception_handler } + I18n.exception_handler = :default_exception_handler # revert it + end + + def test_uses_custom_exception_handler + I18n.exception_handler = :custom_exception_handler + I18n.expects(:custom_exception_handler) + I18n.translate :bogus + I18n.exception_handler = :default_exception_handler # revert it + end + + def test_delegates_translate_to_backend + I18n.backend.expects(:translate).with 'de', :foo, {} + I18n.translate :foo, :locale => 'de' + end + + def test_delegates_localize_to_backend + I18n.backend.expects(:localize).with 'de', :whatever, :default + I18n.localize :whatever, :locale => 'de' + end + + def test_translate_given_no_locale_uses_i18n_locale + I18n.backend.expects(:translate).with 'en', :foo, {} + I18n.translate :foo + end + + def test_translate_on_nested_symbol_keys_works + assert_equal ".", I18n.t(:'currency.format.separator') + end + + def test_translate_with_nested_string_keys_works + assert_equal ".", I18n.t('currency.format.separator') + end + + def test_translate_with_array_as_scope_works + assert_equal ".", I18n.t(:separator, :scope => ['currency.format']) + end + + def test_translate_with_array_containing_dot_separated_strings_as_scope_works + assert_equal ".", I18n.t(:separator, :scope => ['currency.format']) + end + + def test_translate_with_key_array_and_dot_separated_scope_works + assert_equal [".", ","], I18n.t(%w(separator delimiter), :scope => 'currency.format') + end + + def test_translate_with_dot_separated_key_array_and_scope_works + assert_equal [".", ","], I18n.t(%w(format.separator format.delimiter), :scope => 'currency') + end + + def test_translate_with_options_using_scope_works + I18n.backend.expects(:translate).with('de', :precision, :scope => :"currency.format") + I18n.with_options :locale => 'de', :scope => :'currency.format' do |locale| + locale.t :precision + end + end + + # def test_translate_given_no_args_raises_missing_translation_data + # assert_equal "translation missing: en, no key", I18n.t + # end + + def test_translate_given_a_bogus_key_raises_missing_translation_data + assert_equal "translation missing: en, bogus", I18n.t(:bogus) + end + + def test_localize_nil_raises_argument_error + assert_raises(I18n::ArgumentError) { I18n.l nil } + end + + def test_localize_object_raises_argument_error + assert_raises(I18n::ArgumentError) { I18n.l Object.new } + end +end diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/locale/en.rb b/activesupport/lib/active_support/vendor/i18n-0.1.1/test/locale/en.rb new file mode 100644 index 0000000000..6044ce10d9 --- /dev/null +++ b/activesupport/lib/active_support/vendor/i18n-0.1.1/test/locale/en.rb @@ -0,0 +1 @@ +{:'en-Ruby' => {:foo => {:bar => "baz"}}}
\ No newline at end of file diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/locale/en.yml b/activesupport/lib/active_support/vendor/i18n-0.1.1/test/locale/en.yml new file mode 100644 index 0000000000..0b298c9c0e --- /dev/null +++ b/activesupport/lib/active_support/vendor/i18n-0.1.1/test/locale/en.yml @@ -0,0 +1,3 @@ +en-Yaml: + foo: + bar: baz
\ No newline at end of file diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/simple_backend_test.rb b/activesupport/lib/active_support/vendor/i18n-0.1.1/test/simple_backend_test.rb new file mode 100644 index 0000000000..e181975f38 --- /dev/null +++ b/activesupport/lib/active_support/vendor/i18n-0.1.1/test/simple_backend_test.rb @@ -0,0 +1,502 @@ +# encoding: utf-8 +$:.unshift "lib" + +require 'rubygems' +require 'test/unit' +require 'mocha' +require 'i18n' +require 'time' +require 'yaml' + +module I18nSimpleBackendTestSetup + def setup_backend + # backend_reset_translations! + @backend = I18n::Backend::Simple.new + @backend.store_translations 'en', :foo => {:bar => 'bar', :baz => 'baz'} + @locale_dir = File.dirname(__FILE__) + '/locale' + end + alias :setup :setup_backend + + # def backend_reset_translations! + # I18n::Backend::Simple::ClassMethods.send :class_variable_set, :@@translations, {} + # end + + def backend_get_translations + # I18n::Backend::Simple::ClassMethods.send :class_variable_get, :@@translations + @backend.instance_variable_get :@translations + end + + def add_datetime_translations + @backend.store_translations :'de', { + :date => { + :formats => { + :default => "%d.%m.%Y", + :short => "%d. %b", + :long => "%d. %B %Y", + }, + :day_names => %w(Sonntag Montag Dienstag Mittwoch Donnerstag Freitag Samstag), + :abbr_day_names => %w(So Mo Di Mi Do Fr Sa), + :month_names => %w(Januar Februar März April Mai Juni Juli August September Oktober November Dezember).unshift(nil), + :abbr_month_names => %w(Jan Feb Mar Apr Mai Jun Jul Aug Sep Okt Nov Dez).unshift(nil), + :order => [:day, :month, :year] + }, + :time => { + :formats => { + :default => "%a, %d. %b %Y %H:%M:%S %z", + :short => "%d. %b %H:%M", + :long => "%d. %B %Y %H:%M", + }, + :am => 'am', + :pm => 'pm' + }, + :datetime => { + :distance_in_words => { + :half_a_minute => 'half a minute', + :less_than_x_seconds => { + :one => 'less than 1 second', + :other => 'less than {{count}} seconds' + }, + :x_seconds => { + :one => '1 second', + :other => '{{count}} seconds' + }, + :less_than_x_minutes => { + :one => 'less than a minute', + :other => 'less than {{count}} minutes' + }, + :x_minutes => { + :one => '1 minute', + :other => '{{count}} minutes' + }, + :about_x_hours => { + :one => 'about 1 hour', + :other => 'about {{count}} hours' + }, + :x_days => { + :one => '1 day', + :other => '{{count}} days' + }, + :about_x_months => { + :one => 'about 1 month', + :other => 'about {{count}} months' + }, + :x_months => { + :one => '1 month', + :other => '{{count}} months' + }, + :about_x_years => { + :one => 'about 1 year', + :other => 'about {{count}} year' + }, + :over_x_years => { + :one => 'over 1 year', + :other => 'over {{count}} years' + } + } + } + } + end +end + +class I18nSimpleBackendTranslationsTest < Test::Unit::TestCase + include I18nSimpleBackendTestSetup + + def test_store_translations_adds_translations # no, really :-) + @backend.store_translations :'en', :foo => 'bar' + assert_equal Hash[:'en', {:foo => 'bar'}], backend_get_translations + end + + def test_store_translations_deep_merges_translations + @backend.store_translations :'en', :foo => {:bar => 'bar'} + @backend.store_translations :'en', :foo => {:baz => 'baz'} + assert_equal Hash[:'en', {:foo => {:bar => 'bar', :baz => 'baz'}}], backend_get_translations + end + + def test_store_translations_forces_locale_to_sym + @backend.store_translations 'en', :foo => 'bar' + assert_equal Hash[:'en', {:foo => 'bar'}], backend_get_translations + end + + def test_store_translations_converts_keys_to_symbols + # backend_reset_translations! + @backend.store_translations 'en', 'foo' => {'bar' => 'bar', 'baz' => 'baz'} + assert_equal Hash[:'en', {:foo => {:bar => 'bar', :baz => 'baz'}}], backend_get_translations + end +end + +class I18nSimpleBackendTranslateTest < Test::Unit::TestCase + include I18nSimpleBackendTestSetup + + def test_translate_calls_lookup_with_locale_given + @backend.expects(:lookup).with('de', :bar, [:foo]).returns 'bar' + @backend.translate 'de', :bar, :scope => [:foo] + end + + def test_given_no_keys_it_returns_the_default + assert_equal 'default', @backend.translate('en', nil, :default => 'default') + end + + def test_translate_given_a_symbol_as_a_default_translates_the_symbol + assert_equal 'bar', @backend.translate('en', nil, :scope => [:foo], :default => :bar) + end + + def test_translate_given_an_array_as_default_uses_the_first_match + assert_equal 'bar', @backend.translate('en', :does_not_exist, :scope => [:foo], :default => [:does_not_exist_2, :bar]) + end + + def test_translate_given_an_array_of_inexistent_keys_it_raises_missing_translation_data + assert_raises I18n::MissingTranslationData do + @backend.translate('en', :does_not_exist, :scope => [:foo], :default => [:does_not_exist_2, :does_not_exist_3]) + end + end + + def test_translate_an_array_of_keys_translates_all_of_them + assert_equal %w(bar baz), @backend.translate('en', [:bar, :baz], :scope => [:foo]) + end + + def test_translate_calls_pluralize + @backend.expects(:pluralize).with 'en', 'bar', 1 + @backend.translate 'en', :bar, :scope => [:foo], :count => 1 + end + + def test_translate_calls_interpolate + @backend.expects(:interpolate).with 'en', 'bar', {} + @backend.translate 'en', :bar, :scope => [:foo] + end + + def test_translate_calls_interpolate_including_count_as_a_value + @backend.expects(:interpolate).with 'en', 'bar', {:count => 1} + @backend.translate 'en', :bar, :scope => [:foo], :count => 1 + end + + def test_translate_given_nil_as_a_locale_raises_an_argument_error + assert_raises(I18n::InvalidLocale){ @backend.translate nil, :bar } + end + + def test_translate_with_a_bogus_key_and_no_default_raises_missing_translation_data + assert_raises(I18n::MissingTranslationData){ @backend.translate 'de', :bogus } + end +end + +class I18nSimpleBackendLookupTest < Test::Unit::TestCase + include I18nSimpleBackendTestSetup + + # useful because this way we can use the backend with no key for interpolation/pluralization + def test_lookup_given_nil_as_a_key_returns_nil + assert_nil @backend.send(:lookup, 'en', nil) + end + + def test_lookup_given_nested_keys_looks_up_a_nested_hash_value + assert_equal 'bar', @backend.send(:lookup, 'en', :bar, [:foo]) + end +end + +class I18nSimpleBackendPluralizeTest < Test::Unit::TestCase + include I18nSimpleBackendTestSetup + + def test_pluralize_given_nil_returns_the_given_entry + entry = {:one => 'bar', :other => 'bars'} + assert_equal entry, @backend.send(:pluralize, nil, entry, nil) + end + + def test_pluralize_given_0_returns_zero_string_if_zero_key_given + assert_equal 'zero', @backend.send(:pluralize, nil, {:zero => 'zero', :one => 'bar', :other => 'bars'}, 0) + end + + def test_pluralize_given_0_returns_plural_string_if_no_zero_key_given + assert_equal 'bars', @backend.send(:pluralize, nil, {:one => 'bar', :other => 'bars'}, 0) + end + + def test_pluralize_given_1_returns_singular_string + assert_equal 'bar', @backend.send(:pluralize, nil, {:one => 'bar', :other => 'bars'}, 1) + end + + def test_pluralize_given_2_returns_plural_string + assert_equal 'bars', @backend.send(:pluralize, nil, {:one => 'bar', :other => 'bars'}, 2) + end + + def test_pluralize_given_3_returns_plural_string + assert_equal 'bars', @backend.send(:pluralize, nil, {:one => 'bar', :other => 'bars'}, 3) + end + + def test_interpolate_given_incomplete_pluralization_data_raises_invalid_pluralization_data + assert_raises(I18n::InvalidPluralizationData){ @backend.send(:pluralize, nil, {:one => 'bar'}, 2) } + end + + # def test_interpolate_given_a_string_raises_invalid_pluralization_data + # assert_raises(I18n::InvalidPluralizationData){ @backend.send(:pluralize, nil, 'bar', 2) } + # end + # + # def test_interpolate_given_an_array_raises_invalid_pluralization_data + # assert_raises(I18n::InvalidPluralizationData){ @backend.send(:pluralize, nil, ['bar'], 2) } + # end +end + +class I18nSimpleBackendInterpolateTest < Test::Unit::TestCase + include I18nSimpleBackendTestSetup + + def test_interpolate_given_a_value_hash_interpolates_the_values_to_the_string + assert_equal 'Hi David!', @backend.send(:interpolate, nil, 'Hi {{name}}!', :name => 'David') + end + + def test_interpolate_given_a_value_hash_interpolates_into_unicode_string + assert_equal 'Häi David!', @backend.send(:interpolate, nil, 'Häi {{name}}!', :name => 'David') + end + + def test_interpolate_given_nil_as_a_string_returns_nil + assert_nil @backend.send(:interpolate, nil, nil, :name => 'David') + end + + def test_interpolate_given_an_non_string_as_a_string_returns_nil + assert_equal [], @backend.send(:interpolate, nil, [], :name => 'David') + end + + def test_interpolate_given_a_values_hash_with_nil_values_interpolates_the_string + assert_equal 'Hi !', @backend.send(:interpolate, nil, 'Hi {{name}}!', {:name => nil}) + end + + def test_interpolate_given_an_empty_values_hash_raises_missing_interpolation_argument + assert_raises(I18n::MissingInterpolationArgument) { @backend.send(:interpolate, nil, 'Hi {{name}}!', {}) } + end + + def test_interpolate_given_a_string_containing_a_reserved_key_raises_reserved_interpolation_key + assert_raises(I18n::ReservedInterpolationKey) { @backend.send(:interpolate, nil, '{{default}}', {:default => nil}) } + end +end + +class I18nSimpleBackendLocalizeDateTest < Test::Unit::TestCase + include I18nSimpleBackendTestSetup + + def setup + @backend = I18n::Backend::Simple.new + add_datetime_translations + @date = Date.new 2008, 1, 1 + end + + def test_translate_given_the_short_format_it_uses_it + assert_equal '01. Jan', @backend.localize('de', @date, :short) + end + + def test_translate_given_the_long_format_it_uses_it + assert_equal '01. Januar 2008', @backend.localize('de', @date, :long) + end + + def test_translate_given_the_default_format_it_uses_it + assert_equal '01.01.2008', @backend.localize('de', @date, :default) + end + + def test_translate_given_a_day_name_format_it_returns_a_day_name + assert_equal 'Dienstag', @backend.localize('de', @date, '%A') + end + + def test_translate_given_an_abbr_day_name_format_it_returns_an_abbrevated_day_name + assert_equal 'Di', @backend.localize('de', @date, '%a') + end + + def test_translate_given_a_month_name_format_it_returns_a_month_name + assert_equal 'Januar', @backend.localize('de', @date, '%B') + end + + def test_translate_given_an_abbr_month_name_format_it_returns_an_abbrevated_month_name + assert_equal 'Jan', @backend.localize('de', @date, '%b') + end + + def test_translate_given_no_format_it_does_not_fail + assert_nothing_raised{ @backend.localize 'de', @date } + end + + def test_translate_given_an_unknown_format_it_does_not_fail + assert_nothing_raised{ @backend.localize 'de', @date, '%x' } + end + + def test_localize_nil_raises_argument_error + assert_raises(I18n::ArgumentError) { @backend.localize 'de', nil } + end + + def test_localize_object_raises_argument_error + assert_raises(I18n::ArgumentError) { @backend.localize 'de', Object.new } + end +end + +class I18nSimpleBackendLocalizeDateTimeTest < Test::Unit::TestCase + include I18nSimpleBackendTestSetup + + def setup + @backend = I18n::Backend::Simple.new + add_datetime_translations + @morning = DateTime.new 2008, 1, 1, 6 + @evening = DateTime.new 2008, 1, 1, 18 + end + + def test_translate_given_the_short_format_it_uses_it + assert_equal '01. Jan 06:00', @backend.localize('de', @morning, :short) + end + + def test_translate_given_the_long_format_it_uses_it + assert_equal '01. Januar 2008 06:00', @backend.localize('de', @morning, :long) + end + + def test_translate_given_the_default_format_it_uses_it + assert_equal 'Di, 01. Jan 2008 06:00:00 +0000', @backend.localize('de', @morning, :default) + end + + def test_translate_given_a_day_name_format_it_returns_the_correct_day_name + assert_equal 'Dienstag', @backend.localize('de', @morning, '%A') + end + + def test_translate_given_an_abbr_day_name_format_it_returns_the_correct_abbrevated_day_name + assert_equal 'Di', @backend.localize('de', @morning, '%a') + end + + def test_translate_given_a_month_name_format_it_returns_the_correct_month_name + assert_equal 'Januar', @backend.localize('de', @morning, '%B') + end + + def test_translate_given_an_abbr_month_name_format_it_returns_the_correct_abbrevated_month_name + assert_equal 'Jan', @backend.localize('de', @morning, '%b') + end + + def test_translate_given_a_meridian_indicator_format_it_returns_the_correct_meridian_indicator + assert_equal 'am', @backend.localize('de', @morning, '%p') + assert_equal 'pm', @backend.localize('de', @evening, '%p') + end + + def test_translate_given_no_format_it_does_not_fail + assert_nothing_raised{ @backend.localize 'de', @morning } + end + + def test_translate_given_an_unknown_format_it_does_not_fail + assert_nothing_raised{ @backend.localize 'de', @morning, '%x' } + end +end + +class I18nSimpleBackendLocalizeTimeTest < Test::Unit::TestCase + include I18nSimpleBackendTestSetup + + def setup + @old_timezone, ENV['TZ'] = ENV['TZ'], 'UTC' + @backend = I18n::Backend::Simple.new + add_datetime_translations + @morning = Time.parse '2008-01-01 6:00 UTC' + @evening = Time.parse '2008-01-01 18:00 UTC' + end + + def teardown + @old_timezone ? ENV['TZ'] = @old_timezone : ENV.delete('TZ') + end + + def test_translate_given_the_short_format_it_uses_it + assert_equal '01. Jan 06:00', @backend.localize('de', @morning, :short) + end + + def test_translate_given_the_long_format_it_uses_it + assert_equal '01. Januar 2008 06:00', @backend.localize('de', @morning, :long) + end + + # TODO Seems to break on Windows because ENV['TZ'] is ignored. What's a better way to do this? + # def test_translate_given_the_default_format_it_uses_it + # assert_equal 'Di, 01. Jan 2008 06:00:00 +0000', @backend.localize('de', @morning, :default) + # end + + def test_translate_given_a_day_name_format_it_returns_the_correct_day_name + assert_equal 'Dienstag', @backend.localize('de', @morning, '%A') + end + + def test_translate_given_an_abbr_day_name_format_it_returns_the_correct_abbrevated_day_name + assert_equal 'Di', @backend.localize('de', @morning, '%a') + end + + def test_translate_given_a_month_name_format_it_returns_the_correct_month_name + assert_equal 'Januar', @backend.localize('de', @morning, '%B') + end + + def test_translate_given_an_abbr_month_name_format_it_returns_the_correct_abbrevated_month_name + assert_equal 'Jan', @backend.localize('de', @morning, '%b') + end + + def test_translate_given_a_meridian_indicator_format_it_returns_the_correct_meridian_indicator + assert_equal 'am', @backend.localize('de', @morning, '%p') + assert_equal 'pm', @backend.localize('de', @evening, '%p') + end + + def test_translate_given_no_format_it_does_not_fail + assert_nothing_raised{ @backend.localize 'de', @morning } + end + + def test_translate_given_an_unknown_format_it_does_not_fail + assert_nothing_raised{ @backend.localize 'de', @morning, '%x' } + end +end + +class I18nSimpleBackendHelperMethodsTest < Test::Unit::TestCase + def setup + @backend = I18n::Backend::Simple.new + end + + def test_deep_symbolize_keys_works + result = @backend.send :deep_symbolize_keys, 'foo' => {'bar' => {'baz' => 'bar'}} + expected = {:foo => {:bar => {:baz => 'bar'}}} + assert_equal expected, result + end +end + +class I18nSimpleBackendLoadTranslationsTest < Test::Unit::TestCase + include I18nSimpleBackendTestSetup + + def test_load_translations_with_unknown_file_type_raises_exception + assert_raises(I18n::UnknownFileType) { @backend.load_translations "#{@locale_dir}/en.xml" } + end + + def test_load_translations_with_ruby_file_type_does_not_raise_exception + assert_nothing_raised { @backend.load_translations "#{@locale_dir}/en.rb" } + end + + def test_load_rb_loads_data_from_ruby_file + data = @backend.send :load_rb, "#{@locale_dir}/en.rb" + assert_equal({:'en-Ruby' => {:foo => {:bar => "baz"}}}, data) + end + + def test_load_rb_loads_data_from_yaml_file + data = @backend.send :load_yml, "#{@locale_dir}/en.yml" + assert_equal({'en-Yaml' => {'foo' => {'bar' => 'baz'}}}, data) + end + + def test_load_translations_loads_from_different_file_formats + @backend = I18n::Backend::Simple.new + @backend.load_translations "#{@locale_dir}/en.rb", "#{@locale_dir}/en.yml" + expected = { + :'en-Ruby' => {:foo => {:bar => "baz"}}, + :'en-Yaml' => {:foo => {:bar => "baz"}} + } + assert_equal expected, backend_get_translations + end +end + +class I18nSimpleBackendReloadTranslationsTest < Test::Unit::TestCase + include I18nSimpleBackendTestSetup + + def setup + @backend = I18n::Backend::Simple.new + I18n.load_path = [File.dirname(__FILE__) + '/locale/en.yml'] + assert_nil backend_get_translations + @backend.send :init_translations + end + + def teardown + I18n.load_path = [] + end + + def test_setup + assert_not_nil backend_get_translations + end + + def test_reload_translations_unloads_translations + @backend.reload! + assert_nil backend_get_translations + end + + def test_reload_translations_uninitializes_translations + @backend.reload! + assert_equal @backend.initialized?, false + end +end
\ No newline at end of file diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb index 25b8eecef5..2bc2e1eaf0 100644 --- a/activesupport/test/callbacks_test.rb +++ b/activesupport/test/callbacks_test.rb @@ -53,10 +53,41 @@ class Person < Record end class ConditionalPerson < Record + # proc before_save Proc.new { |r| r.history << [:before_save, :proc] }, :if => Proc.new { |r| true } before_save Proc.new { |r| r.history << "b00m" }, :if => Proc.new { |r| false } before_save Proc.new { |r| r.history << [:before_save, :proc] }, :unless => Proc.new { |r| false } before_save Proc.new { |r| r.history << "b00m" }, :unless => Proc.new { |r| true } + # symbol + before_save Proc.new { |r| r.history << [:before_save, :symbol] }, :if => :yes + before_save Proc.new { |r| r.history << "b00m" }, :if => :no + before_save Proc.new { |r| r.history << [:before_save, :symbol] }, :unless => :no + before_save Proc.new { |r| r.history << "b00m" }, :unless => :yes + # string + before_save Proc.new { |r| r.history << [:before_save, :string] }, :if => 'yes' + before_save Proc.new { |r| r.history << "b00m" }, :if => 'no' + before_save Proc.new { |r| r.history << [:before_save, :string] }, :unless => 'no' + before_save Proc.new { |r| r.history << "b00m" }, :unless => 'yes' + # Array with conditions + before_save Proc.new { |r| r.history << [:before_save, :symbol_array] }, :if => [:yes, :other_yes] + before_save Proc.new { |r| r.history << "b00m" }, :if => [:yes, :no] + before_save Proc.new { |r| r.history << [:before_save, :symbol_array] }, :unless => [:no, :other_no] + before_save Proc.new { |r| r.history << "b00m" }, :unless => [:yes, :no] + # Combined if and unless + before_save Proc.new { |r| r.history << [:before_save, :combined_symbol] }, :if => :yes, :unless => :no + before_save Proc.new { |r| r.history << "b00m" }, :if => :yes, :unless => :yes + # Array with different types of conditions + before_save Proc.new { |r| r.history << [:before_save, :symbol_proc_string_array] }, :if => [:yes, Proc.new { |r| true }, 'yes'] + before_save Proc.new { |r| r.history << "b00m" }, :if => [:yes, Proc.new { |r| true }, 'no'] + # Array with different types of conditions comibned if and unless + before_save Proc.new { |r| r.history << [:before_save, :combined_symbol_proc_string_array] }, + :if => [:yes, Proc.new { |r| true }, 'yes'], :unless => [:no, 'no'] + before_save Proc.new { |r| r.history << "b00m" }, :if => [:yes, Proc.new { |r| true }, 'no'], :unless => [:no, 'no'] + + def yes; true; end + def other_yes; true; end + def no; false; end + def other_no; false; end def save run_callbacks(:before_save) @@ -90,7 +121,16 @@ class ConditionalCallbackTest < Test::Unit::TestCase person.save assert_equal [ [:before_save, :proc], - [:before_save, :proc] + [:before_save, :proc], + [:before_save, :symbol], + [:before_save, :symbol], + [:before_save, :string], + [:before_save, :string], + [:before_save, :symbol_array], + [:before_save, :symbol_array], + [:before_save, :combined_symbol], + [:before_save, :symbol_proc_string_array], + [:before_save, :combined_symbol_proc_string_array] ], person.history end end diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index 63ccb5a7da..b63ab30965 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -287,10 +287,14 @@ class HashExtTest < Test::Unit::TestCase # Should return a new hash with only the given keys. assert_equal expected, original.slice(:a, :b) assert_not_equal expected, original + end + + def test_slice_inplace + original = { :a => 'x', :b => 'y', :c => 10 } + expected = { :c => 10 } # Should replace the hash with only the given keys. assert_equal expected, original.slice!(:a, :b) - assert_equal expected, original end def test_slice_with_an_array_key @@ -300,10 +304,14 @@ class HashExtTest < Test::Unit::TestCase # Should return a new hash with only the given keys when given an array key. assert_equal expected, original.slice([:a, :b], :c) assert_not_equal expected, original + end + + def test_slice_inplace_with_an_array_key + original = { :a => 'x', :b => 'y', :c => 10, [:a, :b] => "an array key" } + expected = { :a => 'x', :b => 'y' } # Should replace the hash with only the given keys when given an array key. assert_equal expected, original.slice!([:a, :b], :c) - assert_equal expected, original end def test_slice_with_splatted_keys @@ -322,11 +330,17 @@ class HashExtTest < Test::Unit::TestCase # Should return a new hash with only the given keys. assert_equal expected, original.slice(*keys), keys.inspect assert_not_equal expected, original + end + end + def test_indifferent_slice_inplace + original = { :a => 'x', :b => 'y', :c => 10 }.with_indifferent_access + expected = { :c => 10 }.with_indifferent_access + + [['a', 'b'], [:a, :b]].each do |keys| # Should replace the hash with only the given keys. copy = original.dup assert_equal expected, copy.slice!(*keys) - assert_equal expected, copy end end diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb index 886f692499..a5d98507ba 100644 --- a/activesupport/test/core_ext/module_test.rb +++ b/activesupport/test/core_ext/module_test.rb @@ -41,6 +41,10 @@ Invoice = Struct.new(:client) do delegate :street, :city, :name, :to => :client, :prefix => :customer end +Project = Struct.new(:description, :person) do + delegate :name, :to => :person, :allow_nil => true +end + class Name delegate :upcase, :to => :@full_name @@ -117,6 +121,29 @@ class ModuleTest < Test::Unit::TestCase end end + def test_delegation_with_allow_nil + rails = Project.new("Rails", Someone.new("David")) + assert_equal rails.name, "David" + end + + def test_delegation_with_allow_nil_and_nil_value + rails = Project.new("Rails") + assert_nil rails.name + end + + def test_delegation_with_allow_nil_and_nil_value_and_prefix + Project.class_eval do + delegate :name, :to => :person, :allow_nil => true, :prefix => true + end + rails = Project.new("Rails") + assert_nil rails.person_name + end + + def test_delegation_without_allow_nil_and_nil_value + david = Someone.new("David") + assert_raises(NoMethodError) { david.street } + end + def test_parent assert_equal Yz::Zy, Yz::Zy::Cd.parent assert_equal Yz, Yz::Zy.parent diff --git a/railties/doc/guides/source/command_line.txt b/railties/doc/guides/source/command_line.txt index 1ad2e75c51..8a887bd001 100644 --- a/railties/doc/guides/source/command_line.txt +++ b/railties/doc/guides/source/command_line.txt @@ -52,7 +52,7 @@ NOTE: This output will seem very familiar when we get to the `generate` command. === server === -Let's try it! The `server` command launches a small web server written in Ruby named WEBrick which was also installed when you installed Rails. You'll use this any time you want to view your work through a web browser. +Let's try it! The `server` command launches a small web server named WEBrick which comes bundled with Ruby. You'll use this any time you want to view your work through a web browser. NOTE: WEBrick isn't your only option for serving Rails. We'll get to that in a later section. [XXX: which section] @@ -99,7 +99,7 @@ Using generators will save you a large amount of time by writing *boilerplate co Let's make our own controller with the controller generator. But what command should we use? Let's ask the generator: -NOTE: All Rails console utilities have help text. For commands that require a lot of input to run correctly, you can try the command without any parameters (like `rails` or `./script/generate`). For others, you can try adding `--help` or `-h` to the end, as in `./script/server --help`. +NOTE: All Rails console utilities have help text. As with most *NIX utilities, you can try adding `--help` or `-h` to the end, for example `./script/server --help`. [source,shell] ------------------------------------------------------ @@ -200,24 +200,47 @@ Examples: creates a Post model with a string title, text body, and published flag. ------------------------------------------------------ -Let's set up a simple model called "HighScore" that will keep track of our highest score on video games we play. Then we'll wire up our controller and view to modify and list our scores. +But instead of generating a model directly (which we'll be doing later), let's set up a scaffold. A *scaffold* in Rails is a full set of model, database migration for that model, controller to manipulate it, views to view and manipulate the data, and a test suite for each of the above. + +Let's set up a simple resource called "HighScore" that will keep track of our highest score on video games we play. [source,shell] ------------------------------------------------------ -$ ./script/generate model HighScore id:integer game:string score:integer - exists app/models/ - exists test/unit/ - exists test/fixtures/ - create app/models/high_score.rb - create test/unit/high_score_test.rb - create test/fixtures/high_scores.yml - create db/migrate - create db/migrate/20081126032945_create_high_scores.rb +$ ./script/generate scaffold HighScore game:string score:integer + exists app/models/ + exists app/controllers/ + exists app/helpers/ + create app/views/high_scores + create app/views/layouts/ + exists test/functional/ + create test/unit/ + create public/stylesheets/ + create app/views/high_scores/index.html.erb + create app/views/high_scores/show.html.erb + create app/views/high_scores/new.html.erb + create app/views/high_scores/edit.html.erb + create app/views/layouts/high_scores.html.erb + create public/stylesheets/scaffold.css + create app/controllers/high_scores_controller.rb + create test/functional/high_scores_controller_test.rb + create app/helpers/high_scores_helper.rb + route map.resources :high_scores +dependency model + exists app/models/ + exists test/unit/ + create test/fixtures/ + create app/models/high_score.rb + create test/unit/high_score_test.rb + create test/fixtures/high_scores.yml + exists db/migrate + create db/migrate/20081217071914_create_high_scores.rb ------------------------------------------------------ -Taking it from the top, we have the *models* directory, where all of your data models live. *test/unit*, where all the unit tests live (gasp! -- unit tests!), fixtures for those tests, a test, the *migrate* directory, where the database-modifying migrations live, and a migration to create the `high_scores` table with the right fields. +Taking it from the top - the generator checks that there exist the directories for models, controllers, helpers, layouts, functional and unit tests, stylesheets, creates the views, controller, model and database migration for HighScore (creating the `high_scores` table and fields), takes care of the route for the *resource*, and new tests for everything. + +The migration requires that we *migrate*, that is, run some Ruby code (living in that `20081217071914_create_high_scores.rb`) to modify the schema of our database. Which database? The sqlite3 database that Rails will create for you when we run the `rake db:migrate` command. We'll talk more about Rake in-depth in a little while. -The migration requires that we *migrate*, that is, run some Ruby code (living in that `20081126032945_create_high_scores.rb`) to modify the schema of our database. Which database? The sqlite3 database that Rails will create for you when we run the `rake db:migrate` command. We'll talk more about Rake in-depth in a little while. +NOTE: Hey. Install the sqlite3-ruby gem while you're at it. `gem install sqlite3-ruby` [source,shell] ------------------------------------------------------ @@ -231,23 +254,87 @@ $ rake db:migrate NOTE: Let's talk about unit tests. Unit tests are code that tests and makes assertions about code. In unit testing, we take a little part of code, say a method of a model, and test its inputs and outputs. Unit tests are your friend. The sooner you make peace with the fact that your quality of life will drastically increase when you unit test your code, the better. Seriously. We'll make one in a moment. -Yo! Let's shove a small table into our greeting controller and view, listing our sweet scores. +Let's see the interface Rails created for us. ./script/server; http://localhost:3000/high_scores -[source,ruby] +We can create new high scores (55,160 on Space Invaders!) + +=== console === +The `console` command lets you interact with your Rails application from the command line. On the underside, `script/console` uses IRB, so if you've ever used it, you'll be right at home. This is useful for testing out quick ideas with code and changing data server-side without touching the website. + +=== dbconsole === +`dbconsole` figures out which database you're using and drops you into whichever command line interface you would use with it (and figures out the command line parameters to give to it, too!). It supports MySQL, PostgreSQL, SQLite and SQLite3. + +=== plugin === +The `plugin` command simplifies plugin management; think a miniature version of the Gem utility. Let's walk through installing a plugin. You can call the sub-command *discover*, which sifts through repositories looking for plugins, or call *source* to add a specific repository of plugins, or you can specify the plugin location directly. + +Let's say you're creating a website for a client who wants a small accounting system. Every event having to do with money must be logged, and must never be deleted. Wouldn't it be great if we could override the behavior of a model to never actually take its record out of the database, but *instead*, just set a field? + +There is such a thing! The plugin we're installing is called "acts_as_paranoid", and it lets models implement a "deleted_at" column that gets set when you call destroy. Later, when calling find, the plugin will tack on a database check to filter out "deleted" things. + +[source,shell] +------------------------------------------------------ +$ ./script/plugin install http://svn.techno-weenie.net/projects/plugins/acts_as_paranoid ++ ./CHANGELOG ++ ./MIT-LICENSE +... +... ------------------------------------------------------ -class GreetingController < ApplicationController - def hello - if request.post? - score = HighScore.new(params[:high_score]) - if score.save - flash[:notice] = "New score posted!" - end - end - - @scores = HighScore.find(:all) - end -end +=== runner === +`runner` runs Ruby code in the context of Rails non-interactively. For instance: + +[source,shell] ------------------------------------------------------ +$ ./script/runner "Model.long_running_method" +------------------------------------------------------ + +=== destroy === +Think of `destroy` as the opposite of `generate`. It'll figure out what generate did, and undo it. Believe you-me, the creation of this tutorial used this command many times! -XXX: Go with scaffolding instead, modifying greeting controller for high scores seems dumb. +[source,shell] +------------------------------------------------------ +$ ./script/generate model Oops + exists app/models/ + exists test/unit/ + exists test/fixtures/ + create app/models/oops.rb + create test/unit/oops_test.rb + create test/fixtures/oops.yml + exists db/migrate + create db/migrate/20081221040817_create_oops.rb +$ ./script/destroy model Oops + notempty db/migrate + notempty db + rm db/migrate/20081221040817_create_oops.rb + rm test/fixtures/oops.yml + rm test/unit/oops_test.rb + rm app/models/oops.rb + notempty test/fixtures + notempty test + notempty test/unit + notempty test + notempty app/models + notempty app +------------------------------------------------------ + +=== about === +Check it: Version numbers for Ruby, RubyGems, Rails, the Rails subcomponents, your application's folder, the current Rails environment name, your app's database adapter, and schema version! `about` is useful when you need to ask help, check if a security patch might affect you, or when you need some stats for an existing Rails installation. + +[source,shell] +------------------------------------------------------ +$ ./script/about +About your application's environment +Ruby version 1.8.6 (i486-linux) +RubyGems version 1.3.1 +Rails version 2.2.0 +Active Record version 2.2.0 +Action Pack version 2.2.0 +Active Resource version 2.2.0 +Action Mailer version 2.2.0 +Active Support version 2.2.0 +Edge Rails revision unknown +Application root /home/commandsapp +Environment development +Database adapter sqlite3 +Database schema version 20081217073400 +------------------------------------------------------
\ No newline at end of file diff --git a/railties/doc/guides/source/finders.txt b/railties/doc/guides/source/finders.txt index d2bd55ada7..88e7c15cb6 100644 --- a/railties/doc/guides/source/finders.txt +++ b/railties/doc/guides/source/finders.txt @@ -1,7 +1,7 @@ Rails Finders ============= -This guide covers the +find+ method defined in +ActiveRecord::Base+, as well as other ways of finding particular instances of your models. By using this guide, you will be able to: +This guide covers the +find+ method defined in ActiveRecord::Base, as well as other ways of finding particular instances of your models. By using this guide, you will be able to: * Find records using a variety of methods and conditions * Specify the order, retrieved attributes, grouping, and other properties of the found records @@ -50,7 +50,7 @@ Active Record will perform queries on the database for you and is compatible wit == IDs, First, Last and All -+ActiveRecord::Base+ has methods defined on it to make interacting with your database and the tables within it much, much easier. For finding records, the key method is +find+. This method allows you to pass arguments into it to perform certain queries on your database without the need of SQL. If you wanted to find the record with the id of 1, you could type +Client.find(1)+ which would execute this query on your database: +ActiveRecord::Base has methods defined on it to make interacting with your database and the tables within it much, much easier. For finding records, the key method is +find+. This method allows you to pass arguments into it to perform certain queries on your database without the need of SQL. If you wanted to find the record with the id of 1, you could type +Client.find(1)+ which would execute this query on your database: [source, sql] ------------------------------------------------------- @@ -74,9 +74,9 @@ SELECT * FROM clients WHERE (clients.id IN (1,2)) created_at: "2008-09-28 13:12:40", updated_at: "2008-09-28 13:12:40">] ------------------------------------------------------- -Note that if you pass in a list of numbers that the result will be returned as an array, not as a single +Client+ object. +Note that if you pass in a list of numbers that the result will be returned as an array, not as a single Client object. -NOTE: If +find(id)+ or +find([id1, id2])+ fails to find any records, it will raise a +RecordNotFound+ exception. +NOTE: If +find(id)+ or +find([id1, id2])+ fails to find any records, it will raise a RecordNotFound exception. If you wanted to find the first Client object you would simply type +Client.first+ and that would find the first client in your clients table: @@ -143,7 +143,7 @@ WARNING: Building your own conditions as pure strings can leave you vulnerable t === Array Conditions === -Now what if that number could vary, say as a parameter from somewhere, or perhaps from the user's level status somewhere? The find then becomes something like +Client.first(:conditions => ["orders_count = ?", params[:orders]])+. Active Record will go through the first element in the conditions value and any additional elements will replace the question marks (?) in the first element. If you want to specify two conditions, you can do it like +Client.first(:conditions => ["orders_count = ? AND locked = ?", params[:orders], false])+. In this example, the first question mark will be replaced with the value in +params[:orders]+ and the second will be replaced with +false+ and this will find the first record in the table that has '2' as its value for the +orders_count+ field and +false+ for its locked field. +Now what if that number could vary, say as a argument from somewhere, or perhaps from the user's level status somewhere? The find then becomes something like +Client.first(:conditions => ["orders_count = ?", params[:orders]])+. Active Record will go through the first element in the conditions value and any additional elements will replace the question marks (?) in the first element. If you want to specify two conditions, you can do it like +Client.first(:conditions => ["orders_count = ? AND locked = ?", params[:orders], false])+. In this example, the first question mark will be replaced with the value in +params[:orders]+ and the second will be replaced with the SQL representation of +false+, which depends on the adapter. The reason for doing code like: @@ -159,7 +159,7 @@ instead of: Client.first(:conditions => "orders_count = #{params[:orders]}") ------------------------------------------------------- -is because of parameter safety. Putting the variable directly into the conditions string will pass the variable to the database *as-is*. This means that it will be an unescaped variable directly from a user who may have malicious intent. If you do this, you put your entire database at risk because once a user finds out he or she can exploit your database they can do just about anything to it. Never ever put your parameters directly inside the conditions string. +is because of argument safety. Putting the variable directly into the conditions string will pass the variable to the database *as-is*. This means that it will be an unescaped variable directly from a user who may have malicious intent. If you do this, you put your entire database at risk because once a user finds out he or she can exploit your database they can do just about anything to it. Never ever put your arguments directly inside the conditions string. TIP: For more information on the dangers of SQL injection, see the link:../security.html#_sql_injection[Ruby on Rails Security Guide]. @@ -240,7 +240,7 @@ This makes for clearer readability if you have a large number of variable condit === Hash Conditions -Rails also allows you to pass in a hash conditions too which can increase the readability of your conditions syntax. With hash conditions, you pass in a hash with keys of the fields you want conditionalised and the values of how you want to conditionalise them: +Rails also allows you to pass in a hash conditions which can increase the readability of your conditions syntax. With hash conditions, you pass in a hash with keys of the fields you want conditionalised and the values of how you want to conditionalise them: [source, ruby] ------------------------------------------------------- @@ -258,27 +258,63 @@ The good thing about this is that we can pass in a range for our fields without [source, ruby] ------------------------------------------------------- -Client.all(:conditions => { :created_at => ((Time.now.midnight - 1.day)..Time.now.midnight}) +Client.all(:conditions => { :created_at => (Time.now.midnight - 1.day)..Time.now.midnight}) ------------------------------------------------------- -This will find all clients created yesterday. This shows the shorter syntax for the examples in <<_array_conditions, Array Conditions>> +This will find all clients created yesterday by using a BETWEEN sql statement: + +[source, sql] +------------------------------------------------------- +SELECT * FROM `clients` WHERE (`clients`.`created_at` BETWEEN '2008-12-21 00:00:00' AND '2008-12-22 00:00:00') +------------------------------------------------------- + +This demonstrates a shorter syntax for the examples in <<_array_conditions, Array Conditions>> You can also join in tables and specify their columns in the hash: [source, ruby] ------------------------------------------------------- -Client.all(:include => "orders", :conditions => { 'orders.created_at; => ((Time.now.midnight - 1.day)..Time.now.midnight}) +Client.all(:include => "orders", :conditions => { 'orders.created_at' => (Time.now.midnight - 1.day)..Time.now.midnight }) ------------------------------------------------------- -This will find all clients who have orders that were created yesterday. +An alternative and cleaner syntax to this is: + +[source, ruby] +------------------------------------------------------- +Client.all(:include => "orders", :conditions => { :orders => { :created_at => (Time.now.midnight - 1.day)..Time.now.midnight } }) +------------------------------------------------------- + +This will find all clients who have orders that were created yesterday, again using a BETWEEN expression. + +If you want to find records using the IN expression you can pass an array to the conditions hash: + +[source, ruby] +------------------------------------------------------- +Client.all(:include => "orders", :conditions => { :orders_count => [1,3,5] } +------------------------------------------------------- + +This code will generate SQL like this: + +[source, sql] +------------------------------------------------------- +SELECT * FROM `clients` WHERE (`clients`.`orders_count` IN (1,2,3)) +------------------------------------------------------- == Ordering -If you're getting a set of records and want to force an order, you can use +Client.all(:order => "created_at")+ which by default will sort the records by ascending order. If you'd like to order it in descending order, just tell it to do that using +Client.all(:order => "created_at desc")+ +If you're getting a set of records and want to order them in ascending order by the +created_at+ field in your table, you can use +Client.all(:order => "created_at")+. If you'd like to order it in descending order, just tell it to do that using +Client.all(:order => "created_at desc")+. The value for this option is passed in as sanitized SQL and allows you to sort via multiple fields: +Client.all(:order => "created_at desc, orders_count asc")+. == Selecting Certain Fields -To select certain fields, you can use the select option like this: +Client.first(:select => "viewable_by, locked")+. This select option does not use an array of fields, but rather requires you to type SQL-like code. The above code will execute +SELECT viewable_by, locked FROM clients LIMIT 0,1+ on your database. +To select certain fields, you can use the select option like this: +Client.first(:select => "viewable_by, locked")+. This select option does not use an array of fields, but rather requires you to type SQL-like code. The above code will execute +SELECT viewable_by, locked FROM clients LIMIT 1+ on your database. + +Be careful because this also means you're initializing a model object with only the fields that you've selected. If you attempt to access a field that is not in the initialized record you'll receive: + +------------------------------------------------------- +ActiveRecord::MissingAttributeError: missing attribute: <attribute> +------------------------------------------------------- + +Where <attribute> is the atrribute you asked for. The +id+ method will not raise the +ActiveRecord::MissingAttributeError+, so just be careful when working with associations because they need the +id+ method to function properly. You can also call SQL functions within the select option. For example, if you would like to only grab a single record per unique value in a certain field by using the +DISTINCT+ function you can do it like this: +Client.all(:select => "DISTINCT(name)")+. @@ -328,16 +364,29 @@ The SQL that would be executed would be something like this: SELECT * FROM orders GROUP BY date(created_at) ------------------------------------------------------- +== Having + +The +:having+ option allows you to specify SQL and acts as a kind of a filter on the group option. +:having+ can only be specified when +:group+ is specified. + +An example of using it would be: + +[source, ruby] +------------------------------------------------------- +Order.all(:group => "date(created_at)", :having => ["created_at > ?", 1.month.ago]) +------------------------------------------------------- + +This will return single order objects for each day, but only for the last month. + == Read Only -Readonly is a find option that you can set in order to make that instance of the record read-only. Any attempt to alter or destroy the record will not succeed, raising an +Active Record::ReadOnlyRecord+ exception. To set this option, specify it like this: ++readonly+ is a +find+ option that you can set in order to make that instance of the record read-only. Any attempt to alter or destroy the record will not succeed, raising an ActiveRecord::ReadOnlyRecord exception. To set this option, specify it like this: [source, ruby] ------------------------------------------------------- Client.first(:readonly => true) ------------------------------------------------------- -If you assign this record to a variable +client+, calling the following code will raise an +ActiveRecord::ReadOnlyRecord+ exception: +If you assign this record to a variable client, calling the following code will raise an ActiveRecord::ReadOnlyRecord exception: [source, ruby] ------------------------------------------------------- @@ -358,13 +407,23 @@ Topic.transaction do end ------------------------------------------------------- +You can also pass SQL to this option to allow different types of locks. For example, MySQL has an expression called LOCK IN SHARE MODE where you can lock a record but still allow other queries to read it. To specify this expression just pass it in as the lock option: + +[source, ruby] +------------------------------------------------------- +Topic.transaction do + t = Topic.find(params[:id], :lock => "LOCK IN SHARE MODE") + t.increment!(:views) +end +------------------------------------------------------- + == Making It All Work Together -You can chain these options together in no particular order as Active Record will write the correct SQL for you. If you specify two instances of the same options inside the find statement Active Record will use the latter. +You can chain these options together in no particular order as Active Record will write the correct SQL for you. If you specify two instances of the same options inside the +find+ method Active Record will use the last one you specified. This is because the options passed to find are a hash and defining the same key twice in a hash will result in the last definition being used. == Eager Loading -Eager loading is loading associated records along with any number of records in as few queries as possible. For example, if you wanted to load all the addresses associated with all the clients in a single query you could use +Client.all(:include => :address)+. If you wanted to include both the address and mailing address for the client you would use +Client.find(:all, :include => [:address, :mailing_address]). Include will first find the client records and then load the associated address records. Running script/server in one window, and executing the code through script/console in another window, the output should look similar to this: +Eager loading is loading associated records along with any number of records in as few queries as possible. For example, if you wanted to load all the addresses associated with all the clients in a single query you could use +Client.all(:include => :address)+. If you wanted to include both the address and mailing address for the client you would use +Client.find(:all, :include => [:address, :mailing_address])+. Include will first find the client records and then load the associated address records. Running script/server in one window, and executing the code through script/console in another window, the output should look similar to this: [source, sql] ------------------------------------------------------- @@ -375,9 +434,10 @@ MailingAddress Load (0.001985) SELECT mailing_addresses.* FROM mailing_addresses WHERE (mailing_addresses.client_id IN (13,14)) ------------------------------------------------------- -The numbers +13+ and +14+ in the above SQL are the ids of the clients gathered from the +Client.all+ query. Rails will then run a query to gather all the addresses and mailing addresses that have a client_id of 13 or 14. Although this is done in 3 queries, this is more efficient than not eager loading because without eager loading it would run a query for every time you called +address+ or +mailing_address+ on one of the objects in the clients array, which may lead to performance issues if you're loading a large number of records at once. +The numbers +13+ and +14+ in the above SQL are the ids of the clients gathered from the +Client.all+ query. Rails will then run a query to gather all the addresses and mailing addresses that have a client_id of 13 or 14. Although this is done in 3 queries, this is more efficient than not eager loading because without eager loading it would run a query for every time you called +address+ or +mailing_address+ on one of the objects in the clients array, which may lead to performance issues if you're loading a large number of records at once and is often called the "N+1 query problem". The problem is that the more queries your server has to execute, the slower it will run. -If you wanted to get all the addresses for a client in the same query you would do +Client.all(:joins => :address)+ and you wanted to find the address and mailing address for that client you would do +Client.all(:joins => [:address, :mailing_address])+. This is more efficient because it does all the SQL in one query, as shown by this example: +If you wanted to get all the addresses for a client in the same query you would do +Client.all(:joins => :address)+. +If you wanted to find the address and mailing address for that client you would do +Client.all(:joins => [:address, :mailing_address])+. This is more efficient because it does all the SQL in one query, as shown by this example: [source, sql] ------------------------------------------------------- @@ -400,44 +460,44 @@ When using eager loading you can specify conditions for the columns of the table [source, ruby] ------------------------------------------------------- Client.first(:include => "orders", :conditions => - ["orders.created_at >= ? AND orders.created_at <= ?", Time.now - 2.weeks, Time.now]) + ["orders.created_at >= ? AND orders.created_at <= ?", 2.weeks.ago, Time.now]) ------------------------------------------------------- == Dynamic finders -For every field (also known as an attribute) you define in your table, Active Record provides a finder method. If you have a field called +name+ on your Client model for example, you get +find_by_name+ and +find_all_by_name+ for free from Active Record. If you have also have a +locked+ field on the client model, you also get +find_by_locked+ and +find_all_by_locked+. +For every field (also known as an attribute) you define in your table, Active Record provides a finder method. If you have a field called +name+ on your Client model for example, you get +find_by_name+ and +find_all_by_name+ for free from Active Record. If you have also have a +locked+ field on the Client model, you also get +find_by_locked+ and +find_all_by_locked+. -You can do +find_last_by_*+ methods too which will find the last record matching your parameter. +You can do +find_last_by_*+ methods too which will find the last record matching your argument. -You can specify an exclamation point (!) on the end of the dynamic finders to get them to raise an +ActiveRecord::RecordNotFound+ error if they do not return any records, like +Client.find_by_name!('Ryan')+ +You can specify an exclamation point (!) on the end of the dynamic finders to get them to raise an ActiveRecord::RecordNotFound error if they do not return any records, like +Client.find_by_name!("Ryan")+ -If you want to find both by name and locked, you can chain these finders together by simply typing +and+ between the fields for example +Client.find_by_name_and_locked('Ryan', true)+. +If you want to find both by name and locked, you can chain these finders together by simply typing +and+ between the fields for example +Client.find_by_name_and_locked("Ryan", true)+. -There's another set of dynamic finders that let you find or create/initialize objects if they aren't find. These work in a similar fashion to the other finders and can be used like +find_or_create_by_name(params[:name])+. Using this will firstly perform a find and then create if the find returns nil. The SQL looks like this for +Client.find_or_create_by_name('Ryan')+: +There's another set of dynamic finders that let you find or create/initialize objects if they aren't found. These work in a similar fashion to the other finders and can be used like +find_or_create_by_name(params[:name])+. Using this will firstly perform a find and then create if the find returns nil. The SQL looks like this for +Client.find_or_create_by_name("Ryan")+: [source,sql] ------------------------------------------------------- SELECT * FROM clients WHERE (clients.name = 'Ryan') LIMIT 1 BEGIN INSERT INTO clients (name, updated_at, created_at, orders_count, locked) - VALUES('Ryan', '2008-09-28 15:39:12', '2008-09-28 15:39:12', '0', '0') + VALUES('Ryan', '2008-09-28 15:39:12', '2008-09-28 15:39:12', 0, '0') COMMIT ------------------------------------------------------- -+find_or_create+'s sibling, +find_or_initialize+, will find an object and if it does not exist will act similar to calling +new+ with the parameters you passed in. For example: ++find_or_create+'s sibling, +find_or_initialize+, will find an object and if it does not exist will act similar to calling +new+ with the arguments you passed in. For example: [source, ruby] ------------------------------------------------------- client = Client.find_or_initialize_by_name('Ryan') ------------------------------------------------------- -will either assign an existing client object with the name 'Ryan' to the client local variable, or initialize new object similar to calling +Client.new(:name => 'Ryan')+. From here, you can modify other fields in client by calling the attribute setters on it: +client.locked = true+ and when you want to write it to the database just call +save+ on it. +will either assign an existing client object with the name 'Ryan' to the client local variable, or initialize a new object similar to calling +Client.new(:name => 'Ryan')+. From here, you can modify other fields in client by calling the attribute setters on it: +client.locked = true+ and when you want to write it to the database just call +save+ on it. == Finding By SQL -If you'd like to use your own SQL to find records a table you can use +find_by_sql+. The +find_by_sql+ method will return an array of objects even if it only returns a single record in it's call to the database. For example you could run this query: +If you'd like to use your own SQL to find records in a table you can use +find_by_sql+. The +find_by_sql+ method will return an array of objects even the underlying query returns just a single record. For example you could run this query: [source, ruby] ------------------------------------------------------- @@ -457,7 +517,7 @@ Client.connection.select_all("SELECT * FROM `clients` WHERE `id` = '1'") == Working with Associations -When you define a has_many association on a model you get the find method and dynamic finders also on that association. This is helpful for finding associated records within the scope of an existing record, for example finding all the orders for a client that have been sent and not received by doing something like +Client.find(params[:id]).orders.find_by_sent_and_received(true, false)+. Having this find method available on associations is extremely helpful when using nested controllers. +When you define a has_many association on a model you get the +find+ method and dynamic finders also on that association. This is helpful for finding associated records within the scope of an existing record, for example finding all the orders for a client that have been sent and not received by doing something like +Client.find(params[:id]).orders.find_by_sent_and_received(true, false)+. Having this find method available on associations is extremely helpful when using nested resources. == Named Scopes @@ -465,7 +525,7 @@ Named scopes are another way to add custom finding behavior to the models in the === Simple Named Scopes -Suppose want to find all clients who are male. You could use this code: +Suppose we want to find all clients who are male. You could use this code: [source, ruby] ------------------------------------------------------- @@ -485,7 +545,7 @@ class Client < ActiveRecord::Base end ------------------------------------------------------- -You can call this new named_scope with +Client.active.all+ and this will do the same query as if we just used +Client.all(:conditions => ["active = ?", true])+. Please be aware that the conditions syntax in named_scope and find is different and the two are not interchangeable. If you want to find the first client within this named scope you could do +Client.active.first+. +You can call this new named_scope with +Client.active.all+ and this will do the same query as if we just used +Client.all(:conditions => ["active = ?", true])+. If you want to find the first client within this named scope you could do +Client.active.first+. === Combining Named Scopes @@ -514,7 +574,7 @@ class Client < ActiveRecord::Base end ------------------------------------------------------- -This looks like a standard named scope that defines a method called recent which gathers all records created any time between now and 2 weeks ago. That's correct for the first time the model is loaded but for any time after that, +2.weeks.ago+ is set to that same value, so you will consistently get records from a certain date until your model is reloaded by something like your application restarting. The way to fix this is to put the code in a lambda block: +This looks like a standard named scope that defines a method called +recent+ which gathers all records created any time between now and 2 weeks ago. That's correct for the first time the model is loaded but for any time after that, +2.weeks.ago+ is set to that same value, so you will consistently get records from a certain date until your model is reloaded by something like your application restarting. The way to fix this is to put the code in a lambda block: [source, ruby] ------------------------------------------------------- @@ -523,11 +583,11 @@ class Client < ActiveRecord::Base end ------------------------------------------------------- -And now every time the recent named scope is called, the code in the lambda block will be parsed, so you'll get actually 2 weeks ago from the code execution, not 2 weeks ago from the time the model was loaded. +And now every time the +recent+ named scope is called, the code in the lambda block will be executed, so you'll get actually 2 weeks ago from the code execution, not 2 weeks ago from the time the model was loaded. === Named Scopes with Multiple Models -In a named scope you can use +:include+ and +:joins+ options just like in find. +In a named scope you can use +:include+ and +:joins+ options just like in +find+. [source, ruby] ------------------------------------------------------- @@ -541,7 +601,7 @@ This method, called as +Client.active_within_2_weeks.all+, will return all clien === Arguments to Named Scopes -If you want to pass a named scope a compulsory argument, just specify it as a block parameter like this: +If you want to pass to a named scope a required arugment, just specify it as a block argument like this: [source, ruby] ------------------------------------------------------- @@ -550,7 +610,7 @@ class Client < ActiveRecord::Base end ------------------------------------------------------- -This will work if you call +Client.recent(2.weeks.ago).all+ but not if you call +Client.recent+. If you want to add an optional argument for this, you have to use the splat operator as the block's parameter. +This will work if you call +Client.recent(2.weeks.ago).all+ but not if you call +Client.recent+. If you want to add an optional argument for this, you have to use prefix the arugment with an *. [source, ruby] ------------------------------------------------------- @@ -587,14 +647,14 @@ Just like named scopes, anonymous scopes can be stacked, either with other anony == Existence of Objects -If you simply want to check for the existence of the object there's a method called +exists?+. This method will query the database using the same query as find, but instead of returning an object or collection of objects it will return either true or false. +If you simply want to check for the existence of the object there's a method called +exists?+. This method will query the database using the same query as +find+, but instead of returning an object or collection of objects it will return either +true+ or false+. [source, ruby] ------------------------------------------------------- Client.exists?(1) ------------------------------------------------------- -The above code will check for the existence of a clients table record with the id of 1 and return true if it exists. +The +exists?+ method also takes multiple ids, but the catch is that it will return true if any one of those records exists. [source, ruby] ------------------------------------------------------- @@ -603,8 +663,6 @@ Client.exists?(1,2,3) Client.exists?([1,2,3]) ------------------------------------------------------- -The +exists?+ method also takes multiple ids, as shown by the above code, but the catch is that it will return true if any one of those records exists. - Further more, +exists+ takes a +conditions+ option much like find: [source, ruby] @@ -627,10 +685,10 @@ Which will execute: [source, sql] ------------------------------------------------------- -SELECT count(*) AS count_all FROM clients WHERE (first_name = 1) +SELECT count(*) AS count_all FROM clients WHERE (first_name = 'Ryan') ------------------------------------------------------- -You can also use +include+ or +joins+ for this to do something a little more complex: +You can also use +:include+ or +:joins+ for this to do something a little more complex: [source, ruby] ------------------------------------------------------- @@ -643,7 +701,7 @@ Which will execute: ------------------------------------------------------- SELECT count(DISTINCT clients.id) AS count_all FROM clients LEFT OUTER JOIN orders ON orders.client_id = client.id WHERE - (clients.first_name = 'name' AND orders.status = 'received') + (clients.first_name = 'Ryan' AND orders.status = 'received') ------------------------------------------------------- This code specifies +clients.first_name+ just in case one of the join tables has a field also called +first_name+ and it uses +orders.status+ because that's the name of our join table. @@ -711,6 +769,12 @@ Thanks to Mike Gunderloy for his tips on creating this guide. http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/16[Lighthouse ticket] +* December 23 2008: Xavier Noria suggestions added! From http://rails.lighthouseapp.com/projects/16213/tickets/16-activerecord-finders#ticket-16-27[this ticket] and http://rails.lighthouseapp.com/projects/16213/tickets/16-activerecord-finders#ticket-16-28[this ticket] and http://rails.lighthouseapp.com/projects/16213/tickets/16-activerecord-finders#ticket-16-29[this ticket] +* December 22 2008: Added section on having. +* December 22 2008: Added description of how to make hash conditions use an IN expression http://rails.loglibrary.com/chats/15279234[mentioned here] +* December 22 2008: Mentioned using SQL as values for the lock option as mentioned in http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/16-activerecord-finders#ticket-16-24[this ticket] +* December 21 2008: Fixed http://rails.lighthouseapp.com/projects/16213/tickets/16-activerecord-finders#ticket-16-22[this ticket] minus two points; the lock SQL syntax and the having option. +* December 21 2008: Added more to the has conditions section. * December 17 2008: Fixed up syntax errors. * December 16 2008: Covered hash conditions that were introduced in Rails 2.2.2. * December 1 2008: Added using an SQL function example to Selecting Certain Fields section as per http://rails.lighthouseapp.com/projects/16213/tickets/36-adding-an-example-for-using-distinct-to-ar-finders[this ticket] diff --git a/railties/doc/guides/source/form_helpers.txt b/railties/doc/guides/source/form_helpers.txt index 88ca74a557..d09ad15a90 100644 --- a/railties/doc/guides/source/form_helpers.txt +++ b/railties/doc/guides/source/form_helpers.txt @@ -247,7 +247,7 @@ A nice thing about `f.text_field` and other helper methods is that they will pre Relying on record identification ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -In the previous chapter we handled the Article model. This model is directly available to users of our application and, following the best practices for developing with Rails, we should declare it *a resource*. +In the previous chapter we handled the Article model. This model is directly available to users of our application, so -- following the best practices for developing with Rails -- we should declare it *a resource*. When dealing with RESTful resources, our calls to `form_for` can get significantly easier if we rely on *record identification*. In short, we can just pass the model instance and have Rails figure out model name and the rest: @@ -291,15 +291,13 @@ Here we have a list of cities where their names are presented to the user, but i The select tag and options ~~~~~~~~~~~~~~~~~~~~~~~~~~ -The most generic helper is `select_tag`, which -- as the name implies -- simply generates the `SELECT` tag that encapsulates the options: +The most generic helper is `select_tag`, which -- as the name implies -- simply generates the `SELECT` tag that encapsulates an options string: ---------------------------------------------------------------------------- <%= select_tag(:city_id, '<option value="1">Lisabon</option>...') %> ---------------------------------------------------------------------------- -This is a start, but it doesn't dynamically create our option tags. We had to pass them in as a string. - -We can generate option tags with the `options_for_select` helper: +This is a start, but it doesn't dynamically create our option tags. We can generate option tags with the `options_for_select` helper: ---------------------------------------------------------------------------- <%= options_for_select([['Lisabon', 1], ['Madrid', 2], ...]) %> @@ -311,9 +309,9 @@ output: ... ---------------------------------------------------------------------------- -For input data we used a nested array where each element has two elements: visible value (name) and internal value (ID). +For input data we used a nested array where each item has two elements: option text (city name) and option value (city id). -Now you can combine `select_tag` and `options_for_select` to achieve the desired, complete markup: +Knowing this, you can combine `select_tag` and `options_for_select` to achieve the desired, complete markup: ---------------------------------------------------------------------------- <%= select_tag(:city_id, options_for_select(...)) %> @@ -333,13 +331,114 @@ output: So whenever Rails sees that the internal value of an option being generated matches this value, it will add the `selected` attribute to that option. -Select boxes for dealing with models -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Select helpers for dealing with models +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Until now we've covered how to make generic select boxes, but in most cases our form controls will be tied to a specific database model. So, to continue from our previous examples, let's assume that we have a "Person" model with a `city_id` attribute. +Consistent with other form helpers, when dealing with models we drop the `"_tag"` suffix from `select_tag` that we used in previous examples: + ---------------------------------------------------------------------------- -... +# controller: +@person = Person.new(:city_id => 2) + +# view: +<%= select(:person, :city_id, [['Lisabon', 1], ['Madrid', 2], ...]) %> +---------------------------------------------------------------------------- + +Notice that the third parameter, the options array, is the same kind of argument we pass to `options_for_select`. One thing that we have as an advantage here is that we don't have to worry about pre-selecting the correct city if the user already has one -- Rails will do this for us by reading from `@person.city_id` attribute. + +As before, if we were to use `select` helper on a form builder scoped to `@person` object, the syntax would be: + +---------------------------------------------------------------------------- +# select on a form builder +<%= f.select(:city_id, ...) %> +---------------------------------------------------------------------------- + +Option tags from a collection of arbitrary objects +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Until now we were generating option tags from nested arrays with the help of `options_for_select` method. Data in our array were raw values: + +---------------------------------------------------------------------------- +<%= options_for_select([['Lisabon', 1], ['Madrid', 2], ...]) %> +---------------------------------------------------------------------------- + +But what if we had a *City* model (perhaps an ActiveRecord one) and we wanted to generate option tags from a collection of those objects? One solution would be to make a nested array by iterating over them: + +---------------------------------------------------------------------------- +<% cities_array = City.find(:all).map { |city| [city.name, city.id] } %> +<%= options_for_select(cities_array) %> +---------------------------------------------------------------------------- + +This is a perfectly valid solution, but Rails provides us with a less verbose alternative: `options_from_collection_for_select`. This helper expects a collection of arbitrary objects and two additional arguments: the names of the methods to read the option *value* and *text* from, respectively: + +---------------------------------------------------------------------------- +<%= options_from_collection_for_select(City.all, :id, :name) %> +---------------------------------------------------------------------------- + +As the name implies, this only generates option tags. A method to go along with it is `collection_select`: + +---------------------------------------------------------------------------- +<%= collection_select(:person, :city_id, City.all, :id, :name) %> ---------------------------------------------------------------------------- -...
\ No newline at end of file +To recap, `options_from_collection_for_select` are to `collection_select` what `options_for_select` are to `select`. + +Time zone and country select +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To leverage time zone support in Rails, we have to ask our users what time zone they are in. Doing so would require generating select options from a list of pre-defined TimeZone objects using `collection_select`, but we can simply use the `time_zone_select` helper that already wraps this: + +---------------------------------------------------------------------------- +<%= time_zone_select(:person, :city_id) %> +---------------------------------------------------------------------------- + +There is also `time_zone_options_for_select` helper for a more manual (therefore more customizable) way of doing this. Read the API documentation to learn about the possible arguments for these two methods. + +When it comes to country select, Rails _used_ to have the built-in helper `country_select` but was extracted to a plugin. +TODO: plugin URL + + +Miscellaneous +------------- + +File upload form +~~~~~~~~~~~~~~~~ + +:multipart - If set to true, the enctype is set to "multipart/form-data". + +Scoping out form controls with `fields_for` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Creates a scope around a specific model object like `form_for`, but doesn’t create the form tags themselves. This makes `fields_for` suitable for specifying additional model objects in the same form: + +Making custom form builders +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can also build forms using a customized FormBuilder class. Subclass FormBuilder and override or define some more helpers, then use your custom builder. For example, let’s say you made a helper to automatically add labels to form inputs. + +---------------------------------------------------------------------------- +<% form_for :person, @person, :url => { :action => "update" }, :builder => LabellingFormBuilder do |f| %> + <%= f.text_field :first_name %> + <%= f.text_field :last_name %> + <%= text_area :person, :biography %> + <%= check_box_tag "person[admin]", @person.company.admin? %> +<% end %> +---------------------------------------------------------------------------- + + +* `form_for` within a namespace + +---------------------------------------------------------------------------- +select_tag(name, option_tags = nil, html_options = { :multiple, :disabled }) + +select(object, method, choices, options = {}, html_options = {}) +options_for_select(container, selected = nil) + +collection_select(object, method, collection, value_method, text_method, options = {}, html_options = {}) +options_from_collection_for_select(collection, value_method, text_method, selected = nil) + +time_zone_options_for_select(selected = nil, priority_zones = nil, model = ::ActiveSupport::TimeZone) +time_zone_select(object, method, priority_zones = nil, options = {}, html_options = {}) +---------------------------------------------------------------------------- diff --git a/railties/lib/commands/dbconsole.rb b/railties/lib/commands/dbconsole.rb index 6ff895aa30..06848d3c91 100644 --- a/railties/lib/commands/dbconsole.rb +++ b/railties/lib/commands/dbconsole.rb @@ -41,7 +41,7 @@ when "mysql" if config['password'] && include_password args << "--password=#{config['password']}" - elsif config['password'] && !config['password'].empty? + elsif config['password'] && !config['password'].to_s.empty? args << "-p" end diff --git a/railties/lib/rails_generator/commands.rb b/railties/lib/rails_generator/commands.rb index cacb3807d6..299044c3d7 100644 --- a/railties/lib/rails_generator/commands.rb +++ b/railties/lib/rails_generator/commands.rb @@ -294,7 +294,7 @@ HELP file(relative_source, relative_destination, template_options) do |file| # Evaluate any assignments in a temporary, throwaway binding. vars = template_options[:assigns] || {} - b = binding + b = template_options[:binding] || binding vars.each { |k,v| eval "#{k} = vars[:#{k}] || vars['#{k}']", b } # Render the source file with the temporary binding. diff --git a/railties/lib/rails_generator/generators/applications/app/template_runner.rb b/railties/lib/rails_generator/generators/applications/app/template_runner.rb index c6113648e6..bb7bd0e6f4 100644 --- a/railties/lib/rails_generator/generators/applications/app/template_runner.rb +++ b/railties/lib/rails_generator/generators/applications/app/template_runner.rb @@ -8,23 +8,24 @@ require 'fileutils' module Rails class TemplateRunner attr_reader :root + attr_writer :logger def initialize(template, root = '') # :nodoc: - @root = File.join(Dir.pwd, root) + @root = File.expand_path(File.directory?(root) ? root : File.join(Dir.pwd, root)) - puts "applying template: #{template}" + log 'applying', "template: #{template}" load_template(template) - puts "#{template} applied." + log 'applied', "#{template}" end def load_template(template) begin code = open(template).read in_root { self.instance_eval(code) } - rescue LoadError - raise "The template [#{template}] could not be loaded." + rescue LoadError, Errno::ENOENT => e + raise "The template [#{template}] could not be loaded. Error: #{e}" end end @@ -41,8 +42,8 @@ module Rails # # file("config/apach.conf", "your apache config") # - def file(filename, data = nil, &block) - puts "creating file #{filename}" + def file(filename, data = nil, log_action = true, &block) + log 'file', filename if log_action dir, file = [File.dirname(filename), File.basename(filename)] inside(dir) do @@ -66,7 +67,7 @@ module Rails # plugin 'restful-authentication', :svn => 'svn://svnhub.com/technoweenie/restful-authentication/trunk' # def plugin(name, options) - puts "installing plugin #{name}" + log 'plugin', name if options[:git] && options[:submodule] in_root do @@ -74,28 +75,36 @@ module Rails end elsif options[:git] || options[:svn] in_root do - `script/plugin install #{options[:svn] || options[:git]}` + run("script/plugin install #{options[:svn] || options[:git]}", false) end else - puts "! no git or svn provided for #{name}. skipping..." + log "! no git or svn provided for #{name}. skipping..." end end # Adds an entry into config/environment.rb for the supplied gem : def gem(name, options = {}) - puts "adding gem #{name}" + log 'gem', name - sentinel = 'Rails::Initializer.run do |config|' gems_code = "config.gem '#{name}'" if options.any? - opts = options.inject([]) {|result, h| result << [":#{h[0]} => '#{h[1]}'"] }.join(", ") + opts = options.inject([]) {|result, h| result << [":#{h[0]} => '#{h[1]}'"] }.sort.join(", ") gems_code << ", #{opts}" end + environment gems_code + end + + # Adds a line inside the Initializer block for config/environment.rb. Used by #gem + def environment(data = nil, &block) + sentinel = 'Rails::Initializer.run do |config|' + + data = block.call if !data && block_given? + in_root do gsub_file 'config/environment.rb', /(#{Regexp.escape(sentinel)})/mi do |match| - "#{match}\n #{gems_code}" + "#{match}\n " << data end end end @@ -111,11 +120,11 @@ module Rails def git(command = {}) in_root do if command.is_a?(Symbol) - puts "running git #{command}" + log 'running', "git #{command}" Git.run(command.to_s) else command.each do |command, options| - puts "running git #{command} #{options}" + log 'running', "git #{command} #{options}" Git.run("#{command} #{options}") end end @@ -135,16 +144,8 @@ module Rails # vendor("foreign.rb", "# Foreign code is fun") # def vendor(filename, data = nil, &block) - puts "vendoring file #{filename}" - inside("vendor") do |folder| - File.open("#{folder}/#{filename}", "w") do |f| - if block_given? - f.write(block.call) - else - f.write(data) - end - end - end + log 'vendoring', filename + file("vendor/#{filename}", data, false, &block) end # Create a new file in the lib/ directory. Code can be specified @@ -158,17 +159,9 @@ module Rails # # lib("foreign.rb", "# Foreign code is fun") # - def lib(filename, data = nil) - puts "add lib file #{filename}" - inside("lib") do |folder| - File.open("#{folder}/#{filename}", "w") do |f| - if block_given? - f.write(block.call) - else - f.write(data) - end - end - end + def lib(filename, data = nil, &block) + log 'lib', filename + file("lib/#{filename}", data, false, &block) end # Create a new Rakefile with the provided code (either in a block or a string). @@ -190,16 +183,8 @@ module Rails # rakefile("seed.rake", "puts 'im plantin ur seedz'") # def rakefile(filename, data = nil, &block) - puts "adding rakefile #{filename}" - inside("lib/tasks") do |folder| - File.open("#{folder}/#{filename}", "w") do |f| - if block_given? - f.write(block.call) - else - f.write(data) - end - end - end + log 'rakefile', filename + file("lib/tasks/#{filename}", data, false, &block) end # Create a new initializer with the provided code (either in a block or a string). @@ -219,16 +204,8 @@ module Rails # initializer("api.rb", "API_KEY = '123456'") # def initializer(filename, data = nil, &block) - puts "adding initializer #{filename}" - inside("config/initializers") do |folder| - File.open("#{folder}/#{filename}", "w") do |f| - if block_given? - f.write(block.call) - else - f.write(data) - end - end - end + log 'initializer', filename + file("config/initializers/#{filename}", data, false, &block) end # Generate something using a generator from Rails or a plugin. @@ -240,10 +217,10 @@ module Rails # generate(:authenticated, "user session") # def generate(what, *args) - puts "generating #{what}" + log 'generating', what argument = args.map(&:to_s).flatten.join(" ") - in_root { `#{root}/script/generate #{what} #{argument}` } + in_root { run("script/generate #{what} #{argument}", false) } end # Executes a command @@ -254,8 +231,8 @@ module Rails # run('ln -s ~/edge rails) # end # - def run(command) - puts "executing #{command} from #{Dir.pwd}" + def run(command, log_action = true) + log 'executing', "#{command} from #{Dir.pwd}" if log_action `#{command}` end @@ -268,10 +245,10 @@ module Rails # rake("gems:install", :sudo => true) # def rake(command, options = {}) - puts "running rake task #{command}" + log 'rake', command env = options[:env] || 'development' sudo = options[:sudo] ? 'sudo ' : '' - in_root { `#{sudo}rake #{command} RAILS_ENV=#{env}` } + in_root { run("#{sudo}rake #{command} RAILS_ENV=#{env}", false) } end # Just run the capify command in root @@ -281,7 +258,8 @@ module Rails # capify! # def capify! - in_root { `capify .` } + log 'capifying' + in_root { run('capify .', false) } end # Add Rails to /vendor/rails @@ -291,8 +269,8 @@ module Rails # freeze! # def freeze!(args = {}) - puts "vendoring rails edge" - in_root { `rake rails:freeze:edge` } + log 'vendor', 'rails edge' + in_root { run('rake rails:freeze:edge', false) } end # Make an entry in Rails routing file conifg/routes.rb @@ -302,6 +280,7 @@ module Rails # route "map.root :controller => :welcome" # def route(routing_code) + log 'route', routing_code sentinel = 'ActionController::Routing::Routes.draw do |map|' in_root do @@ -321,7 +300,7 @@ module Rails # freeze! if ask("Should I freeze the latest Rails?") == "yes" # def ask(string) - puts string + log '', string gets.strip end @@ -368,5 +347,23 @@ module Rails def destination_path(relative_destination) File.join(root, relative_destination) end + + def log(action, message = '') + logger.log(action, message) + end + + def logger + @logger ||= Rails::Generator::Base.logger + end + + def logger + @logger ||= if defined?(Rails::Generator::Base) + Rails::Generator::Base.logger + else + require 'rails_generator/simple_logger' + Rails::Generator::SimpleLogger.new(STDOUT) + end + end + end end
\ No newline at end of file diff --git a/railties/lib/tasks/tmp.rake b/railties/lib/tasks/tmp.rake index b191039d63..fea15058bb 100644 --- a/railties/lib/tasks/tmp.rake +++ b/railties/lib/tasks/tmp.rake @@ -2,7 +2,7 @@ namespace :tmp do desc "Clear session, cache, and socket files from tmp/" task :clear => [ "tmp:sessions:clear", "tmp:cache:clear", "tmp:sockets:clear"] - desc "Creates tmp directories for sessions, cache, and sockets" + desc "Creates tmp directories for sessions, cache, sockets, and pids" task :create do FileUtils.mkdir_p(%w( tmp/sessions tmp/cache tmp/sockets tmp/pids )) end @@ -34,4 +34,4 @@ namespace :tmp do FileUtils.rm(Dir['tmp/pids/[^.]*']) end end -end
\ No newline at end of file +end diff --git a/railties/test/fcgi_dispatcher_test.rb b/railties/test/fcgi_dispatcher_test.rb index cc054c24aa..c469c5dd01 100644 --- a/railties/test/fcgi_dispatcher_test.rb +++ b/railties/test/fcgi_dispatcher_test.rb @@ -1,10 +1,9 @@ require 'abstract_unit' begin +require 'action_controller' require 'fcgi_handler' -module ActionController; module Routing; module Routes; end end end - class RailsFCGIHandlerTest < Test::Unit::TestCase def setup @log = StringIO.new @@ -131,19 +130,11 @@ class RailsFCGIHandlerSignalsTest < Test::Unit::TestCase end end - class ::Dispatcher - class << self - attr_accessor :signal - alias_method :old_dispatch, :dispatch - def dispatch(cgi) - signal ? Process.kill(signal, $$) : old_dispatch - end - end - end - def setup @log = StringIO.new @handler = RailsFCGIHandler.new(@log) + @dispatcher = mock + Dispatcher.stubs(:new).returns(@dispatcher) end def test_interrupted_via_HUP_when_not_in_request @@ -159,19 +150,6 @@ class RailsFCGIHandlerSignalsTest < Test::Unit::TestCase assert_equal :reload, @handler.when_ready end - def test_interrupted_via_HUP_when_in_request - cgi = mock - FCGI.expects(:each_cgi).once.yields(cgi) - Dispatcher.expects(:signal).times(2).returns('HUP') - - @handler.expects(:reload!).once - @handler.expects(:close_connection).never - @handler.expects(:exit).never - - @handler.process! - assert_equal :reload, @handler.when_ready - end - def test_interrupted_via_USR1_when_not_in_request cgi = mock FCGI.expects(:each_cgi).once.yields(cgi) @@ -186,19 +164,6 @@ class RailsFCGIHandlerSignalsTest < Test::Unit::TestCase assert_nil @handler.when_ready end - def test_interrupted_via_USR1_when_in_request - cgi = mock - FCGI.expects(:each_cgi).once.yields(cgi) - Dispatcher.expects(:signal).times(2).returns('USR1') - - @handler.expects(:reload!).never - @handler.expects(:close_connection).with(cgi).once - @handler.expects(:exit).never - - @handler.process! - assert_equal :exit, @handler.when_ready - end - def test_restart_via_USR2_when_in_request cgi = mock FCGI.expects(:each_cgi).once.yields(cgi) @@ -217,7 +182,7 @@ class RailsFCGIHandlerSignalsTest < Test::Unit::TestCase def test_interrupted_via_TERM cgi = mock FCGI.expects(:each_cgi).once.yields(cgi) - Dispatcher.expects(:signal).times(2).returns('TERM') + ::Rack::Handler::FastCGI.expects(:serve).once.returns('TERM') @handler.expects(:reload!).never @handler.expects(:close_connection).never @@ -238,7 +203,7 @@ class RailsFCGIHandlerSignalsTest < Test::Unit::TestCase cgi = mock error = RuntimeError.new('foo') FCGI.expects(:each_cgi).once.yields(cgi) - Dispatcher.expects(:dispatch).once.with(cgi).raises(error) + ::Rack::Handler::FastCGI.expects(:serve).once.raises(error) @handler.expects(:dispatcher_error).with(error, regexp_matches(/^unhandled/)) @handler.process! end @@ -254,7 +219,7 @@ class RailsFCGIHandlerSignalsTest < Test::Unit::TestCase cgi = mock error = SignalException.new('USR2') FCGI.expects(:each_cgi).once.yields(cgi) - Dispatcher.expects(:dispatch).once.with(cgi).raises(error) + ::Rack::Handler::FastCGI.expects(:serve).once.raises(error) @handler.expects(:dispatcher_error).with(error, regexp_matches(/^stopping/)) @handler.process! end @@ -284,7 +249,7 @@ class RailsFCGIHandlerPeriodicGCTest < Test::Unit::TestCase cgi = mock FCGI.expects(:each_cgi).times(10).yields(cgi) - Dispatcher.expects(:dispatch).times(10).with(cgi) + Dispatcher.expects(:new).times(10) @handler.expects(:run_gc!).never 9.times { @handler.process! } diff --git a/railties/test/gem_dependency_test.rb b/railties/test/gem_dependency_test.rb index 30fd899fea..6c1f0961a1 100644 --- a/railties/test/gem_dependency_test.rb +++ b/railties/test/gem_dependency_test.rb @@ -9,33 +9,33 @@ Rails::VendorGemSourceIndex.silence_spec_warnings = true uses_mocha "Plugin Tests" do class GemDependencyTest < Test::Unit::TestCase def setup - @gem = Rails::GemDependency.new "hpricot" - @gem_with_source = Rails::GemDependency.new "hpricot", :source => "http://code.whytheluckystiff.net" - @gem_with_version = Rails::GemDependency.new "hpricot", :version => "= 0.6" - @gem_with_lib = Rails::GemDependency.new "aws-s3", :lib => "aws/s3" - @gem_without_load = Rails::GemDependency.new "hpricot", :lib => false + @gem = Rails::GemDependency.new "xhpricotx" + @gem_with_source = Rails::GemDependency.new "xhpricotx", :source => "http://code.whytheluckystiff.net" + @gem_with_version = Rails::GemDependency.new "xhpricotx", :version => "= 0.6" + @gem_with_lib = Rails::GemDependency.new "xaws-s3x", :lib => "aws/s3" + @gem_without_load = Rails::GemDependency.new "xhpricotx", :lib => false end def test_configuration_adds_gem_dependency config = Rails::Configuration.new - config.gem "aws-s3", :lib => "aws/s3", :version => "0.4.0" - assert_equal [["install", "aws-s3", "--version", '"= 0.4.0"']], config.gems.collect(&:install_command) + config.gem "xaws-s3x", :lib => "aws/s3", :version => "0.4.0" + assert_equal [["install", "xaws-s3x", "--version", '"= 0.4.0"']], config.gems.collect(&:install_command) end def test_gem_creates_install_command - assert_equal %w(install hpricot), @gem.install_command + assert_equal %w(install xhpricotx), @gem.install_command end def test_gem_with_source_creates_install_command - assert_equal %w(install hpricot --source http://code.whytheluckystiff.net), @gem_with_source.install_command + assert_equal %w(install xhpricotx --source http://code.whytheluckystiff.net), @gem_with_source.install_command end def test_gem_with_version_creates_install_command - assert_equal ["install", "hpricot", "--version", '"= 0.6"'], @gem_with_version.install_command + assert_equal ["install", "xhpricotx", "--version", '"= 0.6"'], @gem_with_version.install_command end def test_gem_creates_unpack_command - assert_equal %w(unpack hpricot), @gem.unpack_command + assert_equal %w(unpack xhpricotx), @gem.unpack_command end def test_gem_with_version_unpack_install_command @@ -43,7 +43,7 @@ uses_mocha "Plugin Tests" do mock_spec = mock() mock_spec.stubs(:version).returns('0.6') @gem_with_version.stubs(:specification).returns(mock_spec) - assert_equal ["unpack", "hpricot", "--version", '= 0.6'], @gem_with_version.unpack_command + assert_equal ["unpack", "xhpricotx", "--version", '= 0.6'], @gem_with_version.unpack_command end def test_gem_adds_load_paths diff --git a/railties/test/generators/rails_template_runner_test.rb b/railties/test/generators/rails_template_runner_test.rb new file mode 100644 index 0000000000..fcc020603d --- /dev/null +++ b/railties/test/generators/rails_template_runner_test.rb @@ -0,0 +1,190 @@ +require 'abstract_unit' +require 'generators/generator_test_helper' + +class RailsTemplateRunnerTest < GeneratorTestCase + def setup + Rails::Generator::Base.use_application_sources! + run_generator('app', [RAILS_ROOT]) + # generate empty template + @template_path = File.join(RAILS_ROOT, 'template.rb') + File.open(File.join(@template_path), 'w') {|f| f << '' } + + @git_plugin_uri = 'git://github.com/technoweenie/restful-authentication.git' + @svn_plugin_uri = 'svn://svnhub.com/technoweenie/restful-authentication/trunk' + end + + def teardown + super + rm_rf "#{RAILS_ROOT}/README" + rm_rf "#{RAILS_ROOT}/Rakefile" + rm_rf "#{RAILS_ROOT}/doc" + rm_rf "#{RAILS_ROOT}/lib" + rm_rf "#{RAILS_ROOT}/log" + rm_rf "#{RAILS_ROOT}/script" + rm_rf "#{RAILS_ROOT}/vendor" + rm_rf "#{RAILS_ROOT}/tmp" + rm_rf "#{RAILS_ROOT}/Capfile" + rm_rf @template_path + end + + def test_initialize_should_load_template + Rails::TemplateRunner.any_instance.expects(:load_template).with(@template_path) + silence_generator do + Rails::TemplateRunner.new(@template_path, RAILS_ROOT) + end + end + + def test_initialize_should_raise_error_on_missing_template_file + assert_raise(RuntimeError) do + silence_generator do + Rails::TemplateRunner.new('non/existent/path/to/template.rb', RAILS_ROOT) + end + end + end + + def test_file_should_write_data_to_file_path + run_template_method(:file, 'lib/test_file.rb', 'heres test data') + assert_generated_file_with_data 'lib/test_file.rb', 'heres test data' + end + + def test_file_should_write_block_contents_to_file_path + run_template_method(:file, 'lib/test_file.rb') { 'heres block data' } + assert_generated_file_with_data 'lib/test_file.rb', 'heres block data' + end + + def test_plugin_with_git_option_should_run_plugin_install + expects_run_with_command("script/plugin install #{@git_plugin_uri}") + run_template_method(:plugin, 'restful-authentication', :git => @git_plugin_uri) + end + + def test_plugin_with_svn_option_should_run_plugin_install + expects_run_with_command("script/plugin install #{@svn_plugin_uri}") + run_template_method(:plugin, 'restful-authentication', :svn => @svn_plugin_uri) + end + + def test_plugin_with_git_option_and_submodule_should_use_git_scm + Rails::Git.expects(:run).with("submodule add #{@git_plugin_uri} vendor/plugins/rest_auth") + run_template_method(:plugin, 'rest_auth', :git => @git_plugin_uri, :submodule => true) + end + + def test_plugin_with_no_options_should_skip_method + Rails::TemplateRunner.any_instance.expects(:run).never + run_template_method(:plugin, 'rest_auth', {}) + end + + def test_gem_should_put_gem_dependency_in_enviroment + run_template_method(:gem, 'will-paginate') + assert_rails_initializer_includes("config.gem 'will-paginate'") + end + + def test_gem_with_options_should_include_options_in_gem_dependency_in_environment + run_template_method(:gem, 'mislav-will-paginate', :lib => 'will-paginate', :source => 'http://gems.github.com') + assert_rails_initializer_includes("config.gem 'mislav-will-paginate', :lib => 'will-paginate', :source => 'http://gems.github.com'") + end + + def test_environment_should_include_data_in_environment_initializer_block + load_paths = 'config.load_paths += %w["#{RAILS_ROOT}/app/extras"]' + run_template_method(:environment, load_paths) + assert_rails_initializer_includes(load_paths) + end + + def test_environment_with_block_should_include_block_contents_in_environment_initializer_block + run_template_method(:environment) do + '# This wont be added' + '# This will be added' + end + assert_rails_initializer_includes('# This will be added') + end + + def test_git_with_symbol_should_run_command_using_git_scm + Rails::Git.expects(:run).once.with('init') + run_template_method(:git, :init) + end + + def test_git_with_hash_should_run_each_command_using_git_scm + Rails::Git.expects(:run).times(2) + run_template_method(:git, {:init => '', :add => '.'}) + end + + def test_vendor_should_write_data_to_file_in_vendor + run_template_method(:vendor, 'vendor_file.rb', '# vendor data') + assert_generated_file_with_data('vendor/vendor_file.rb', '# vendor data') + end + + def test_lib_should_write_data_to_file_in_lib + run_template_method(:lib, 'my_library.rb', 'class MyLibrary') + assert_generated_file_with_data('lib/my_library.rb', 'class MyLibrary') + end + + def test_rakefile_should_write_date_to_file_in_lib_tasks + run_template_method(:rakefile, 'myapp.rake', 'task :run => [:environment]') + assert_generated_file_with_data('lib/tasks/myapp.rake', 'task :run => [:environment]') + end + + def test_initializer_should_write_date_to_file_in_config_initializers + run_template_method(:initializer, 'constants.rb', 'MY_CONSTANT = 42') + assert_generated_file_with_data('config/initializers/constants.rb', 'MY_CONSTANT = 42') + end + + def test_generate_should_run_script_generate_with_argument_and_options + expects_run_with_command('script/generate model MyModel') + run_template_method(:generate, 'model', 'MyModel') + end + + def test_rake_should_run_rake_command_with_development_env + expects_run_with_command('rake log:clear RAILS_ENV=development') + run_template_method(:rake, 'log:clear') + end + + def test_rake_with_env_option_should_run_rake_command_in_env + expects_run_with_command('rake log:clear RAILS_ENV=production') + run_template_method(:rake, 'log:clear', :env => 'production') + end + + def test_rake_with_sudo_option_should_run_rake_command_with_sudo + expects_run_with_command('sudo rake log:clear RAILS_ENV=development') + run_template_method(:rake, 'log:clear', :sudo => true) + end + + def test_capify_should_run_the_capify_command + expects_run_with_command('capify .') + run_template_method(:capify!) + end + + def test_freeze_should_freeze_rails_edge + expects_run_with_command('rake rails:freeze:edge') + run_template_method(:freeze!) + end + + def test_route_should_add_data_to_the_routes_block_in_config_routes + route_command = "map.route '/login', :controller => 'sessions', :action => 'new'" + run_template_method(:route, route_command) + assert_generated_file_with_data 'config/routes.rb', route_command + end + + protected + def run_template_method(method_name, *args, &block) + silence_generator do + @template_runner = Rails::TemplateRunner.new(@template_path, RAILS_ROOT) + @template_runner.send(method_name, *args, &block) + end + end + + def expects_run_with_command(command) + Rails::TemplateRunner.any_instance.stubs(:run).once.with(command, false) + end + + def assert_rails_initializer_includes(data, message = nil) + message ||= "Rails::Initializer should include #{data}" + assert_generated_file 'config/environment.rb' do |body| + assert_match(/#{Regexp.escape("Rails::Initializer.run do |config|")}.+#{Regexp.escape(data)}.+end/m, body, message) + end + end + + def assert_generated_file_with_data(file, data, message = nil) + message ||= "#{file} should include '#{data}'" + assert_generated_file(file) do |file| + assert_match(/#{Regexp.escape(data)}/,file, message) + end + end +end
\ No newline at end of file |