diff options
author | Pratik Naik <pratiknaik@gmail.com> | 2008-08-14 16:31:14 +0100 |
---|---|---|
committer | Pratik Naik <pratiknaik@gmail.com> | 2008-08-14 16:31:14 +0100 |
commit | 2ebe8d275efa53af967b09ad66dab68acc1aed98 (patch) | |
tree | b0b3e42c277b213788b55558aef3579f4e242300 | |
parent | 73ef94e9675ef6db85f18f1e3c70bf6ddfc1260a (diff) | |
parent | 8cb14ee1203c9ed380c4b192e8730757a52d43cb (diff) | |
download | rails-2ebe8d275efa53af967b09ad66dab68acc1aed98.tar.gz rails-2ebe8d275efa53af967b09ad66dab68acc1aed98.tar.bz2 rails-2ebe8d275efa53af967b09ad66dab68acc1aed98.zip |
Merge commit 'mainstream/master'
Conflicts:
actionpack/lib/action_controller/request.rb
actionpack/lib/action_controller/resources.rb
78 files changed, 1561 insertions, 889 deletions
diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index fa29ae2446..72c94529b5 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -216,7 +216,7 @@ module ActionMailer #:nodoc: # * <tt>:domain</tt> - If you need to specify a HELO domain, you can do it here. # * <tt>:user_name</tt> - If your mail server requires authentication, set the username in this setting. # * <tt>:password</tt> - If your mail server requires authentication, set the password in this setting. - # * <tt>:authentication</tt> - If your mail server requires authentication, you need to specify the authentication type here. + # * <tt>:authentication</tt> - If your mail server requires authentication, you need to specify the authentication type here. # This is a symbol and one of <tt>:plain</tt>, <tt>:login</tt>, <tt>:cram_md5</tt>. # # * <tt>sendmail_settings</tt> - Allows you to override options for the <tt>:sendmail</tt> delivery method. @@ -233,10 +233,10 @@ module ActionMailer #:nodoc: # * <tt>deliveries</tt> - Keeps an array of all the emails sent out through the Action Mailer with <tt>delivery_method :test</tt>. Most useful # for unit and functional testing. # - # * <tt>default_charset</tt> - The default charset used for the body and to encode the subject. Defaults to UTF-8. You can also + # * <tt>default_charset</tt> - The default charset used for the body and to encode the subject. Defaults to UTF-8. You can also # pick a different charset from inside a method with +charset+. # * <tt>default_content_type</tt> - The default content type used for the main part of the message. Defaults to "text/plain". You - # can also pick a different content type from inside a method with +content_type+. + # can also pick a different content type from inside a method with +content_type+. # * <tt>default_mime_version</tt> - The default mime version used for the message. Defaults to <tt>1.0</tt>. You # can also pick a different value from inside a method with +mime_version+. # * <tt>default_implicit_parts_order</tt> - When a message is built implicitly (i.e. multiple parts are assembled from templates @@ -253,9 +253,6 @@ module ActionMailer #:nodoc: class_inheritable_accessor :view_paths cattr_accessor :logger - cattr_accessor :template_extensions - @@template_extensions = ['erb', 'builder', 'rhtml', 'rxml'] - @@smtp_settings = { :address => "localhost", :port => 25, @@ -414,15 +411,10 @@ module ActionMailer #:nodoc: new.deliver!(mail) end - # Register a template extension so mailer templates written in a - # templating language other than rhtml or rxml are supported. - # To use this, include in your template-language plugin's init - # code or on a per-application basis, this can be invoked from - # <tt>config/environment.rb</tt>: - # - # ActionMailer::Base.register_template_extension('haml') def register_template_extension(extension) - template_extensions << extension + ActiveSupport::Deprecation.warn( + "ActionMailer::Base.register_template_extension has been deprecated." + + "Use ActionView::Base.register_template_extension instead", caller) end def template_root @@ -455,16 +447,18 @@ module ActionMailer #:nodoc: # "the_template_file.text.html.erb", etc.). Only do this if parts # have not already been specified manually. if @parts.empty? - templates = Dir.glob("#{template_path}/#{@template}.*") - templates.each do |path| - basename = File.basename(path) - template_regex = Regexp.new("^([^\\\.]+)\\\.([^\\\.]+\\\.[^\\\.]+)\\\.(" + template_extensions.join('|') + ")$") - next unless md = template_regex.match(basename) - template_name = basename - content_type = md.captures[1].gsub('.', '/') - @parts << Part.new(:content_type => content_type, - :disposition => "inline", :charset => charset, - :body => render_message(template_name, @body)) + Dir.glob("#{template_path}/#{@template}.*").each do |path| + template = template_root["#{mailer_name}/#{File.basename(path)}"] + + # Skip unless template has a multipart format + next unless template.multipart? + + @parts << Part.new( + :content_type => template.content_type, + :disposition => "inline", + :charset => charset, + :body => render_message(template, @body) + ) end unless @parts.empty? @content_type = "multipart/alternative" @@ -477,7 +471,7 @@ module ActionMailer #:nodoc: # normal template exists (or if there were no implicit parts) we render # it. template_exists = @parts.empty? - template_exists ||= Dir.glob("#{template_path}/#{@template}.*").any? { |i| File.basename(i).split(".").length == 2 } + template_exists ||= template_root["#{mailer_name}/#{@template}"] @body = render_message(@template, @body) if template_exists # Finally, if there are other message parts and a textual body exists, @@ -538,7 +532,7 @@ module ActionMailer #:nodoc: def render(opts) body = opts.delete(:body) - if opts[:file] && opts[:file] !~ /\// + if opts[:file] && (opts[:file] !~ /\// && !opts[:file].respond_to?(:render)) opts[:file] = "#{mailer_name}/#{opts[:file]}" end initialize_template_class(body).render(opts) diff --git a/actionmailer/test/fixtures/test_mailer/signed_up.erb b/actionmailer/test/fixtures/test_mailer/signed_up.html.erb index a85d5fa442..a85d5fa442 100644 --- a/actionmailer/test/fixtures/test_mailer/signed_up.erb +++ b/actionmailer/test/fixtures/test_mailer/signed_up.html.erb diff --git a/actionmailer/test/mail_service_test.rb b/actionmailer/test/mail_service_test.rb index e5ecb0e254..882b07d675 100644 --- a/actionmailer/test/mail_service_test.rb +++ b/actionmailer/test/mail_service_test.rb @@ -219,7 +219,7 @@ class TestMailer < ActionMailer::Base end attachment :content_type => "application/octet-stream",:filename => "test.txt", :body => "test abcdefghijklmnopqstuvwxyz" end - + def nested_multipart_with_body(recipient) recipients recipient subject "nested multipart with body" @@ -321,7 +321,7 @@ class ActionMailerTest < Test::Unit::TestCase assert_nothing_raised { created = TestMailer.create_nested_multipart(@recipient)} assert_equal 2,created.parts.size assert_equal 2,created.parts.first.parts.size - + assert_equal "multipart/mixed", created.content_type assert_equal "multipart/alternative", created.parts.first.content_type assert_equal "bar", created.parts.first.header['foo'].to_s @@ -366,7 +366,7 @@ class ActionMailerTest < Test::Unit::TestCase assert_not_nil ActionMailer::Base.deliveries.first assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded end - + def test_custom_template expected = new_mail expected.to = @recipient @@ -382,7 +382,6 @@ class ActionMailerTest < Test::Unit::TestCase end def test_custom_templating_extension - # # N.b., custom_templating_extension.text.plain.haml is expected to be in fixtures/test_mailer directory expected = new_mail expected.to = @recipient @@ -390,18 +389,10 @@ class ActionMailerTest < Test::Unit::TestCase expected.body = "Hello there, \n\nMr. #{@recipient}" expected.from = "system@loudthinking.com" expected.date = Time.local(2004, 12, 12) - + # Stub the render method so no alternative renderers need be present. ActionView::Base.any_instance.stubs(:render).returns("Hello there, \n\nMr. #{@recipient}") - - # If the template is not registered, there should be no parts. - created = nil - assert_nothing_raised { created = TestMailer.create_custom_templating_extension(@recipient) } - assert_not_nil created - assert_equal 0, created.parts.length - - ActionMailer::Base.register_template_extension('haml') - + # Now that the template is registered, there should be one part. The text/plain part. created = nil assert_nothing_raised { created = TestMailer.create_custom_templating_extension(@recipient) } @@ -428,7 +419,7 @@ class ActionMailerTest < Test::Unit::TestCase assert_not_nil ActionMailer::Base.deliveries.first assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded end - + def test_cc_bcc expected = new_mail expected.to = @recipient @@ -550,7 +541,7 @@ class ActionMailerTest < Test::Unit::TestCase TestMailer.deliver_signed_up(@recipient) assert_equal 1, ActionMailer::Base.deliveries.size end - + def test_doesnt_raise_errors_when_raise_delivery_errors_is_false ActionMailer::Base.raise_delivery_errors = false TestMailer.any_instance.expects(:perform_delivery_test).raises(Exception) @@ -670,7 +661,7 @@ EOF assert_not_nil ActionMailer::Base.deliveries.first assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded end - + def test_utf8_body_is_not_quoted @recipient = "Foo áëô îü <extended@example.net>" expected = new_mail "utf-8" @@ -760,7 +751,7 @@ EOF mail = TestMailer.create_multipart_with_mime_version(@recipient) assert_equal "1.1", mail.mime_version end - + def test_multipart_with_utf8_subject mail = TestMailer.create_multipart_with_utf8_subject(@recipient) assert_match(/\nSubject: =\?utf-8\?Q\?Foo_.*?\?=/, mail.encoded) @@ -825,7 +816,7 @@ EOF mail = TestMailer.create_implicitly_multipart_example(@recipient, 'iso-8859-1') assert_equal "multipart/alternative", mail.header['content-type'].body - + assert_equal 'iso-8859-1', mail.parts[0].sub_header("content-type", "charset") assert_equal 'iso-8859-1', mail.parts[1].sub_header("content-type", "charset") assert_equal 'iso-8859-1', mail.parts[2].sub_header("content-type", "charset") @@ -852,7 +843,7 @@ EOF assert_equal "line #1\nline #2\nline #3\nline #4\n\n", mail.parts[0].body assert_equal "<p>line #1</p>\n<p>line #2</p>\n<p>line #3</p>\n<p>line #4</p>\n\n", mail.parts[1].body end - + def test_headers_removed_on_smtp_delivery ActionMailer::Base.delivery_method = :smtp TestMailer.deliver_cc_bcc(@recipient) diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index 177c6a354e..6f65d4003d 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -7,8 +7,14 @@ * Update Prototype to 1.6.0.2 #599 [Patrick Joyce] * Conditional GET utility methods. [Jeremy Kemper] - * etag!([:admin, post, current_user]) sets the ETag response header and returns head(:not_modified) if it matches the If-None-Match request header. - * last_modified!(post.updated_at) sets Last-Modified and returns head(:not_modified) if it's no later than If-Modified-Since. + response.last_modified = @post.updated_at + response.etag = [:admin, @post, current_user] + + if request.fresh?(response) + head :not_modified + else + # render ... + end * All 2xx requests are considered successful [Josh Peek] diff --git a/actionpack/lib/action_controller/assertions/response_assertions.rb b/actionpack/lib/action_controller/assertions/response_assertions.rb index 765225ae24..e2e8bbdc71 100644 --- a/actionpack/lib/action_controller/assertions/response_assertions.rb +++ b/actionpack/lib/action_controller/assertions/response_assertions.rb @@ -87,11 +87,11 @@ module ActionController # def assert_template(expected = nil, message=nil) clean_backtrace do - rendered = @response.rendered_template + rendered = @response.rendered_template.to_s msg = build_message(message, "expecting <?> but rendering with <?>", expected, rendered) assert_block(msg) do if expected.nil? - @response.rendered_template.nil? + @response.rendered_template.blank? else rendered.to_s.match(expected) end diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 5689a9825e..0fdbcbd26f 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -428,11 +428,7 @@ module ActionController #:nodoc: # By default, all methods defined in ActionController::Base and included modules are hidden. # More methods can be hidden using <tt>hide_actions</tt>. def hidden_actions - unless read_inheritable_attribute(:hidden_actions) - write_inheritable_attribute(:hidden_actions, ActionController::Base.public_instance_methods.map { |m| m.to_s }) - end - - read_inheritable_attribute(:hidden_actions) + read_inheritable_attribute(:hidden_actions) || write_inheritable_attribute(:hidden_actions, []) end # Hide each of the given methods from being callable as actions. @@ -1199,7 +1195,7 @@ module ActionController #:nodoc: end def perform_action - if self.class.action_methods.include?(action_name) + if action_methods.include?(action_name) send(action_name) default_render unless performed? elsif respond_to? :method_missing @@ -1208,7 +1204,7 @@ module ActionController #:nodoc: elsif template_exists? && template_public? default_render else - raise UnknownAction, "No action responded to #{action_name}. Actions: #{action_methods.to_a.sort.to_sentence}", caller + raise UnknownAction, "No action responded to #{action_name}. Actions: #{action_methods.sort.to_sentence}", caller end end @@ -1234,7 +1230,15 @@ module ActionController #:nodoc: end def self.action_methods - @action_methods ||= Set.new(public_instance_methods.map { |m| m.to_s }) - hidden_actions + @action_methods ||= + # All public instance methods of this class, including ancestors + public_instance_methods(true).map { |m| m.to_s }.to_set - + # Except for public instance methods of Base and its ancestors + Base.public_instance_methods(true).map { |m| m.to_s } + + # Be sure to include shadowed public instance methods of this class + public_instance_methods(false).map { |m| m.to_s } - + # And always exclude explicitly hidden actions + hidden_actions end def add_variables_to_assigns diff --git a/actionpack/lib/action_controller/cgi_process.rb b/actionpack/lib/action_controller/cgi_process.rb index 8bc5e4c3a7..0ca27b30db 100644 --- a/actionpack/lib/action_controller/cgi_process.rb +++ b/actionpack/lib/action_controller/cgi_process.rb @@ -43,7 +43,7 @@ module ActionController #:nodoc: :session_path => "/", # available to all paths in app :session_key => "_session_id", :cookie_only => true - } unless const_defined?(:DEFAULT_SESSION_OPTIONS) + } def initialize(cgi, session_options = {}) @cgi = cgi @@ -61,53 +61,14 @@ module ActionController #:nodoc: end 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 - @cgi.stdinput - end - end - - def query_parameters - @query_parameters ||= self.class.parse_query_parameters(query_string) - end - - def request_parameters - @request_parameters ||= parse_formatted_request_parameters + def body_stream #:nodoc: + @cgi.stdinput end def cookies @cgi.cookies.freeze end - def host_with_port_without_standard_port_handling - if forwarded = env["HTTP_X_FORWARDED_HOST"] - forwarded.split(/,\s?/).last - elsif http_host = env['HTTP_HOST'] - http_host - elsif server_name = env['SERVER_NAME'] - server_name - else - "#{env['SERVER_ADDR']}:#{env['SERVER_PORT']}" - end - end - - def host - host_with_port_without_standard_port_handling.sub(/:\d+$/, '') - end - - def port - if host_with_port_without_standard_port_handling =~ /:(\d+)$/ - $1.to_i - else - standard_port - end - end - def session unless defined?(@session) if @session_options == false diff --git a/actionpack/lib/action_controller/filters.rb b/actionpack/lib/action_controller/filters.rb index 10dc0cc45b..1d7236f18a 100644 --- a/actionpack/lib/action_controller/filters.rb +++ b/actionpack/lib/action_controller/filters.rb @@ -109,16 +109,17 @@ module ActionController #:nodoc: update_options! options end + # override these to return true in appropriate subclass def before? - self.class == BeforeFilter + false end def after? - self.class == AfterFilter + false end def around? - self.class == AroundFilter + false end # Make sets of strings from :only/:except options @@ -170,6 +171,10 @@ module ActionController #:nodoc: :around end + def around? + true + end + def call(controller, &block) if should_run_callback?(controller) method = filter_responds_to_before_and_after? ? around_proc : self.method @@ -212,6 +217,10 @@ module ActionController #:nodoc: :before end + def before? + true + end + def call(controller, &block) super if controller.send!(:performed?) @@ -224,6 +233,10 @@ module ActionController #:nodoc: def type :after end + + def after? + true + end end # Filters enable controllers to run shared pre- and post-processing code for its actions. These filters can be used to do diff --git a/actionpack/lib/action_controller/headers.rb b/actionpack/lib/action_controller/headers.rb index 7239438c49..139669c66f 100644 --- a/actionpack/lib/action_controller/headers.rb +++ b/actionpack/lib/action_controller/headers.rb @@ -1,31 +1,33 @@ +require 'active_support/memoizable' + module ActionController module Http class Headers < ::Hash - - def initialize(constructor = {}) - if constructor.is_a?(Hash) + extend ActiveSupport::Memoizable + + def initialize(*args) + if args.size == 1 && args[0].is_a?(Hash) super() - update(constructor) + update(args[0]) else - super(constructor) + super end end - + def [](header_name) if include?(header_name) - super + super else - super(normalize_header(header_name)) + super(env_name(header_name)) end end - - + private - # Takes an HTTP header name and returns it in the - # format - def normalize_header(header_name) + # Converts a HTTP header name to an environment variable name. + def env_name(header_name) "HTTP_#{header_name.upcase.gsub(/-/, '_')}" end + memoize :env_name end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/rack_process.rb b/actionpack/lib/action_controller/rack_process.rb index 7e0a6b091e..dcbcf8bc1d 100644 --- a/actionpack/lib/action_controller/rack_process.rb +++ b/actionpack/lib/action_controller/rack_process.rb @@ -3,7 +3,7 @@ require 'action_controller/session/cookie_store' module ActionController #:nodoc: class RackRequest < AbstractRequest #:nodoc: - attr_accessor :env, :session_options + attr_accessor :session_options attr_reader :cgi class SessionFixationAttempt < StandardError #:nodoc: @@ -15,7 +15,7 @@ module ActionController #:nodoc: :session_path => "/", # available to all paths in app :session_key => "_session_id", :cookie_only => true - } unless const_defined?(:DEFAULT_SESSION_OPTIONS) + } def initialize(env, session_options = DEFAULT_SESSION_OPTIONS) @session_options = session_options @@ -30,35 +30,21 @@ module ActionController #:nodoc: SERVER_NAME SERVER_PROTOCOL HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING - HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM HTTP_HOST + 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 - # 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'] - StringIO.new(raw_post) - else - @env['rack.input'] - end + def body_stream #:nodoc: + @env['rack.input'] end def key?(key) @env.key?(key) end - def query_parameters - @query_parameters ||= self.class.parse_query_parameters(query_string) - end - - def request_parameters - @request_parameters ||= parse_formatted_request_parameters - end - def cookies return {} unless @env["HTTP_COOKIE"] @@ -70,34 +56,6 @@ module ActionController #:nodoc: @env["rack.request.cookie_hash"] end - def host_with_port_without_standard_port_handling - if forwarded = @env["HTTP_X_FORWARDED_HOST"] - forwarded.split(/,\s?/).last - elsif http_host = @env['HTTP_HOST'] - http_host - elsif server_name = @env['SERVER_NAME'] - server_name - else - "#{env['SERVER_ADDR']}:#{env['SERVER_PORT']}" - end - end - - def host - host_with_port_without_standard_port_handling.sub(/:\d+$/, '') - end - - def port - if host_with_port_without_standard_port_handling =~ /:(\d+)$/ - $1.to_i - else - standard_port - end - end - - def remote_addr - @env['REMOTE_ADDR'] - end - def server_port @env['SERVER_PORT'].to_i end diff --git a/actionpack/lib/action_controller/request.rb b/actionpack/lib/action_controller/request.rb index 60ff75fe2c..185518761d 100644 --- a/actionpack/lib/action_controller/request.rb +++ b/actionpack/lib/action_controller/request.rb @@ -2,35 +2,35 @@ require 'tempfile' require 'stringio' require 'strscan' -module ActionController - # HTTP methods which are accepted by default. - ACCEPTED_HTTP_METHODS = Set.new(%w( get head put post delete options )) +require 'active_support/memoizable' +module ActionController # CgiRequest and TestRequest provide concrete implementations. class AbstractRequest + extend ActiveSupport::Memoizable + def self.relative_url_root=(*args) ActiveSupport::Deprecation.warn( "ActionController::AbstractRequest.relative_url_root= has been renamed." + "You can now set it with config.action_controller.relative_url_root=", caller) end - # The hash of CGI-like environment variables for this request, such as - # - # { 'SERVER_PROTOCOL' => 'HTTP/1.1', 'HTTP_ACCEPT_LANGUAGE' => 'en-us', ... } + 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 # 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 - @request_method ||= begin - method = ((@env['REQUEST_METHOD'] == 'POST' && !parameters[:_method].blank?) ? parameters[:_method].to_s : @env['REQUEST_METHOD']).downcase - if ACCEPTED_HTTP_METHODS.include?(method) - method.to_sym - else - raise UnknownHttpMethod, "#{method}, accepted HTTP methods are #{ACCEPTED_HTTP_METHODS.to_a.to_sentence}" - end - end + 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 # The HTTP request \method as a lowercase symbol, such as <tt>:get</tt>. # Note, HEAD is returned as <tt>:get</tt> since the two are functionally @@ -69,34 +69,60 @@ module ActionController # # request.headers["Content-Type"] # => "text/plain" def headers - @headers ||= ActionController::Http::Headers.new(@env) + ActionController::Http::Headers.new(@env) end + memoize :headers # Returns the content length of the request as an integer. def content_length - @content_length ||= env['CONTENT_LENGTH'].to_i + @env['CONTENT_LENGTH'].to_i end + memoize :content_length # The MIME type of the HTTP request, such as Mime::XML. # # For backward compatibility, the post \format is extracted from the # X-Post-Data-Format HTTP header if present. def content_type - @content_type ||= Mime::Type.lookup(content_type_without_parameters) + Mime::Type.lookup(content_type_without_parameters) end + memoize :content_type # Returns the accepted MIME type for the request. def accepts - @accepts ||= - begin - header = @env['HTTP_ACCEPT'].to_s.strip + header = @env['HTTP_ACCEPT'].to_s.strip - if header.empty? - [content_type, Mime::ALL].compact - else - Mime::Type.parse(header) - end - end + if header.empty? + [content_type, Mime::ALL].compact + else + Mime::Type.parse(header) + end + end + memoize :accepts + + def if_modified_since + if since = env['HTTP_IF_MODIFIED_SINCE'] + Time.rfc2822(since) + end + end + memoize :if_modified_since + + def if_none_match + env['HTTP_IF_NONE_MATCH'] + end + + def not_modified?(modified_at) + if_modified_since && modified_at && if_modified_since >= modified_at + end + + def etag_matches?(etag) + if_none_match && if_none_match == etag + end + + # Check response freshness (Last-Modified and ETag) against request + # If-Modified-Since and If-None-Match conditions. + def fresh?(response) + not_modified?(response.last_modified) || etag_matches?(response.etag) end # Returns the Mime type for the \format used in the request. @@ -105,7 +131,7 @@ module ActionController # GET /posts/5.xhtml | request.format => Mime::HTML # GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first depending on the value of <tt>ActionController::Base.use_accept_header</tt> def format - @format ||= begin + @format ||= if parameters[:format] Mime::Type.lookup_by_extension(parameters[:format]) elsif ActionController::Base.use_accept_header @@ -115,7 +141,6 @@ module ActionController else Mime::Type.lookup_by_extension("html") end - end end @@ -203,22 +228,26 @@ EOM @env['REMOTE_ADDR'] end + memoize :remote_ip # Returns the lowercase name of the HTTP server software. def server_software (@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil end + memoize :server_software # Returns the complete URL used for this request. def url protocol + host_with_port + request_uri end + memoize :url # Returns 'https://' if this is an SSL request and 'http://' otherwise. def protocol ssl? ? 'https://' : 'http://' end + memoize :protocol # Is this an SSL request? def ssl? @@ -226,19 +255,36 @@ EOM end # Returns the \host for this request, such as "example.com". + def raw_host_with_port + if forwarded = env["HTTP_X_FORWARDED_HOST"] + forwarded.split(/,\s?/).last + else + env['HTTP_HOST'] || env['SERVER_NAME'] || "#{env['SERVER_ADDR']}:#{env['SERVER_PORT']}" + end + end + + # Returns the host for this request, such as example.com. def host + raw_host_with_port.sub(/:\d+$/, '') end + memoize :host # Returns a \host:\port string for this request, such as "example.com" or # "example.com:8080". def host_with_port - @host_with_port ||= host + port_string + "#{host}#{port_string}" end + memoize :host_with_port # Returns the port number of this request as an integer. def port - @port_as_int ||= @env['SERVER_PORT'].to_i + if raw_host_with_port =~ /:(\d+)$/ + $1.to_i + else + standard_port + end end + memoize :port # Returns the standard \port number for this request's protocol. def standard_port @@ -251,7 +297,7 @@ EOM # Returns a \port suffix like ":8080" if the \port number of this request # is not the default HTTP \port 80 or HTTPS \port 443. def port_string - (port == standard_port) ? '' : ":#{port}" + port == standard_port ? '' : ":#{port}" end # Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify @@ -280,6 +326,7 @@ EOM @env['QUERY_STRING'] || '' end end + memoize :query_string # Returns the request URI, accounting for server idiosyncrasies. # WEBrick includes the full URL. IIS leaves REQUEST_URI blank. @@ -289,21 +336,23 @@ EOM (%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri else # Construct IIS missing REQUEST_URI from SCRIPT_NAME and PATH_INFO. - script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$}) - uri = @env['PATH_INFO'] - uri = uri.sub(/#{script_filename}\//, '') unless script_filename.nil? - unless (env_qs = @env['QUERY_STRING']).nil? || env_qs.empty? - uri << '?' << env_qs + uri = @env['PATH_INFO'].to_s + + if script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$}) + uri = uri.sub(/#{script_filename}\//, '') end - if uri.nil? + env_qs = @env['QUERY_STRING'].to_s + uri += "?#{env_qs}" unless env_qs.empty? + + if uri.blank? @env.delete('REQUEST_URI') - uri else @env['REQUEST_URI'] = uri end end end + memoize :request_uri # Returns the interpreted \path to requested resource after all the installation # directory of this application was taken into account. @@ -314,6 +363,7 @@ EOM path.sub!(%r/^#{ActionController::Base.relative_url_root}/, '') path || '' end + memoize :path # Read the request \body. This is useful for web services that need to # work with raw requests directly. @@ -350,19 +400,41 @@ EOM @path_parameters ||= {} 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 + end - #-- - # Must be implemented in the concrete request - #++ + def remote_addr + @env['REMOTE_ADDR'] + end - # The request \body as an IO input stream. - def body + def referrer + @env['HTTP_REFERER'] end + alias referer referrer + - def query_parameters #:nodoc: + def query_parameters + @query_parameters ||= self.class.parse_query_parameters(query_string) end - def request_parameters #:nodoc: + def request_parameters + @request_parameters ||= parse_formatted_request_parameters + end + + + #-- + # Must be implemented in the concrete request + #++ + + def body_stream #:nodoc: end def cookies #:nodoc: @@ -389,8 +461,9 @@ EOM # The raw content type string with its parameters stripped off. def content_type_without_parameters - @content_type_without_parameters ||= self.class.extract_content_type_without_parameters(content_type_with_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 diff --git a/actionpack/lib/action_controller/resources.rb b/actionpack/lib/action_controller/resources.rb index 77b329b70e..becf6b0b63 100644 --- a/actionpack/lib/action_controller/resources.rb +++ b/actionpack/lib/action_controller/resources.rb @@ -481,8 +481,7 @@ module ActionController resource.collection_methods.each do |method, actions| actions.each do |action| action_options = action_options_for(action, resource, method) - map.named_route("#{action}_#{resource.name_prefix}#{resource.plural}", "#{resource.path}#{resource.action_separator}#{action}", action_options) - map.named_route("formatted_#{action}_#{resource.name_prefix}#{resource.plural}", "#{resource.path}#{resource.action_separator}#{action}.:format", action_options) + map_named_routes(map, "#{action}_#{resource.name_prefix}#{resource.plural}", "#{resource.path}#{resource.action_separator}#{action}", action_options) end end end @@ -495,18 +494,15 @@ module ActionController index_route_name << "_index" end - map.named_route(index_route_name, resource.path, index_action_options) - map.named_route("formatted_#{index_route_name}", "#{resource.path}.:format", index_action_options) + map_named_routes(map, index_route_name, resource.path, index_action_options) create_action_options = action_options_for("create", resource) - map.connect(resource.path, create_action_options) - map.connect("#{resource.path}.:format", create_action_options) + map_unnamed_routes(map, resource.path, create_action_options) end def map_default_singleton_actions(map, resource) create_action_options = action_options_for("create", resource) - map.connect(resource.path, create_action_options) - map.connect("#{resource.path}.:format", create_action_options) + map_unnamed_routes(map, resource.path, create_action_options) end def map_new_actions(map, resource) @@ -514,11 +510,9 @@ module ActionController actions.each do |action| action_options = action_options_for(action, resource, method) if action == :new - map.named_route("new_#{resource.name_prefix}#{resource.singular}", resource.new_path, action_options) - map.named_route("formatted_new_#{resource.name_prefix}#{resource.singular}", "#{resource.new_path}.:format", action_options) + map_named_routes(map, "new_#{resource.name_prefix}#{resource.singular}", resource.new_path, action_options) else - map.named_route("#{action}_new_#{resource.name_prefix}#{resource.singular}", "#{resource.new_path}#{resource.action_separator}#{action}", action_options) - map.named_route("formatted_#{action}_new_#{resource.name_prefix}#{resource.singular}", "#{resource.new_path}#{resource.action_separator}#{action}.:format", action_options) + map_named_routes(map, "#{action}_new_#{resource.name_prefix}#{resource.singular}", "#{resource.new_path}#{resource.action_separator}#{action}", action_options) end end end @@ -532,22 +526,28 @@ module ActionController action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash) action_path ||= Base.resources_path_names[action] || action - map.named_route("#{action}_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}#{resource.action_separator}#{action_path}", action_options) - map.named_route("formatted_#{action}_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}#{resource.action_separator}#{action_path}.:format",action_options) + map_named_routes(map, "#{action}_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}#{resource.action_separator}#{action_path}", action_options) end end show_action_options = action_options_for("show", resource) - map.named_route("#{resource.name_prefix}#{resource.singular}", resource.member_path, show_action_options) - map.named_route("formatted_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}.:format", show_action_options) + map_named_routes(map, "#{resource.name_prefix}#{resource.singular}", resource.member_path, show_action_options) update_action_options = action_options_for("update", resource) - map.connect(resource.member_path, update_action_options) - map.connect("#{resource.member_path}.:format", update_action_options) + map_unnamed_routes(map, resource.member_path, update_action_options) destroy_action_options = action_options_for("destroy", resource) - map.connect(resource.member_path, destroy_action_options) - map.connect("#{resource.member_path}.:format", destroy_action_options) + map_unnamed_routes(map, resource.member_path, destroy_action_options) + end + + def map_unnamed_routes(map, path_without_format, options) + map.connect(path_without_format, options) + map.connect("#{path_without_format}.:format", options) + end + + def map_named_routes(map, name, path_without_format, options) + map.named_route(name, path_without_format, options) + map.named_route("formatted_#{name}", "#{path_without_format}.:format", options) end def add_conditions_for(conditions, method) @@ -574,4 +574,4 @@ end class ActionController::Routing::RouteSet::Mapper include ActionController::Resources -end +end
\ No newline at end of file diff --git a/actionpack/lib/action_controller/response.rb b/actionpack/lib/action_controller/response.rb index da352b6993..a85fad0d39 100644 --- a/actionpack/lib/action_controller/response.rb +++ b/actionpack/lib/action_controller/response.rb @@ -37,12 +37,20 @@ module ActionController # :nodoc: attr_accessor :body # The headers of the response, as a Hash. It maps header names to header values. attr_accessor :headers - attr_accessor :session, :cookies, :assigns, :template, :redirected_to, :redirected_to_method_params, :layout + attr_accessor :session, :cookies, :assigns, :template, :layout + attr_accessor :redirected_to, :redirected_to_method_params def initialize @body, @headers, @session, @assigns = "", DEFAULT_HEADERS.merge("cookie" => []), [], [] end + def status; headers['Status'] end + def status=(status) headers['Status'] = status end + + def location; headers['Location'] end + def location=(url) headers['Location'] = url end + + # Sets the HTTP response's content MIME type. For example, in the controller # you could write this: # @@ -70,35 +78,29 @@ module ActionController # :nodoc: charset.blank? ? nil : charset.strip.split("=")[1] end - def redirect(to_url, response_status) - self.headers["Status"] = response_status - self.headers["Location"] = to_url + def last_modified + Time.rfc2822(headers['Last-Modified']) + end - self.body = "<html><body>You are being <a href=\"#{to_url}\">redirected</a>.</body></html>" + def last_modified=(utc_time) + headers['Last-Modified'] = utc_time.httpdate end - def prepare! - handle_conditional_get! - convert_content_type! - set_content_length! + def etag; headers['ETag'] end + def etag=(etag) + headers['ETag'] = %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(etag))}") end - # Sets the Last-Modified response header. Returns whether it's older than - # the If-Modified-Since request header. - def last_modified!(utc_time) - headers['Last-Modified'] ||= utc_time.httpdate - if request && since = request.headers['HTTP_IF_MODIFIED_SINCE'] - utc_time <= Time.rfc2822(since) - end + def redirect(url, status) + self.status = status + self.location = url + self.body = "<html><body>You are being <a href=\"#{url}\">redirected</a>.</body></html>" end - # Sets the ETag response header. Returns whether it matches the - # If-None-Match request header. - def etag!(tag) - headers['ETag'] ||= %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(tag))}") - if request && request.headers['HTTP_IF_NONE_MATCH'] == headers['ETag'] - true - end + def prepare! + handle_conditional_get! + convert_content_type! + set_content_length! end private @@ -106,15 +108,15 @@ module ActionController # :nodoc: if nonempty_ok_response? set_conditional_cache_control! - if etag!(body) - headers['Status'] = '304 Not Modified' + self.etag ||= body + if request && request.etag_matches?(etag) + self.status = '304 Not Modified' self.body = '' end end end def nonempty_ok_response? - status = headers['Status'] ok = !status || status[0..2] == '200' ok && body.is_a?(String) && !body.empty? end diff --git a/actionpack/lib/action_controller/routing/segments.rb b/actionpack/lib/action_controller/routing/segments.rb index 75784c3b78..9d4b740a44 100644 --- a/actionpack/lib/action_controller/routing/segments.rb +++ b/actionpack/lib/action_controller/routing/segments.rb @@ -2,7 +2,8 @@ module ActionController module Routing class Segment #:nodoc: RESERVED_PCHAR = ':@&=+$,;' - UNSAFE_PCHAR = Regexp.new("[^#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}]", false, 'N').freeze + SAFE_PCHAR = "#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}" + UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false, 'N').freeze # TODO: Convert :is_optional accessor to read only attr_accessor :is_optional diff --git a/actionpack/lib/action_controller/session/cookie_store.rb b/actionpack/lib/action_controller/session/cookie_store.rb index b477c1f7da..5bf7503f04 100644 --- a/actionpack/lib/action_controller/session/cookie_store.rb +++ b/actionpack/lib/action_controller/session/cookie_store.rb @@ -129,7 +129,7 @@ class CGI::Session::CookieStore private # Marshal a session hash into safe cookie data. Include an integrity hash. def marshal(session) - data = ActiveSupport::Base64.encode64(Marshal.dump(session)).chop + data = ActiveSupport::Base64.encode64s(Marshal.dump(session)) "#{data}--#{generate_digest(data)}" end diff --git a/actionpack/lib/action_controller/test_process.rb b/actionpack/lib/action_controller/test_process.rb index 66675aaa13..0c705207e3 100644 --- a/actionpack/lib/action_controller/test_process.rb +++ b/actionpack/lib/action_controller/test_process.rb @@ -23,7 +23,7 @@ module ActionController #:nodoc: class TestRequest < AbstractRequest #:nodoc: attr_accessor :cookies, :session_options - attr_accessor :query_parameters, :request_parameters, :path, :session, :env + attr_accessor :query_parameters, :request_parameters, :path, :session attr_accessor :host, :user_agent def initialize(query_parameters = nil, request_parameters = nil, session = nil) @@ -42,7 +42,7 @@ module ActionController #:nodoc: end # Wraps raw_post in a StringIO. - def body + def body_stream #:nodoc: StringIO.new(raw_post) end @@ -54,7 +54,7 @@ module ActionController #:nodoc: def port=(number) @env["SERVER_PORT"] = number.to_i - @port_as_int = nil + port(true) end def action=(action_name) @@ -68,6 +68,8 @@ module ActionController #:nodoc: @env["REQUEST_URI"] = value @request_uri = nil @path = nil + request_uri(true) + path(true) end def request_uri=(uri) @@ -77,21 +79,26 @@ module ActionController #:nodoc: def accept=(mime_types) @env["HTTP_ACCEPT"] = Array(mime_types).collect { |mime_types| mime_types.to_s }.join(",") + accepts(true) end - def remote_addr=(addr) - @env['REMOTE_ADDR'] = addr + def if_modified_since=(last_modified) + @env["HTTP_IF_MODIFIED_SINCE"] = last_modified end - def remote_addr - @env['REMOTE_ADDR'] + def if_none_match=(etag) + @env["HTTP_IF_NONE_MATCH"] = etag end - def request_uri + def remote_addr=(addr) + @env['REMOTE_ADDR'] = addr + end + + def request_uri(*args) @request_uri || super end - def path + def path(*args) @path || super end @@ -113,17 +120,13 @@ module ActionController #:nodoc: end end @parameters = nil # reset TestRequest#parameters to use the new path_parameters - end - + end + def recycle! self.request_parameters = {} self.query_parameters = {} self.path_parameters = {} - @request_method, @accepts, @content_type = nil, nil, nil - end - - def referer - @env["HTTP_REFERER"] + unmemoize_all end private @@ -448,10 +451,13 @@ module ActionController #:nodoc: end def method_missing(selector, *args) - return @controller.send!(selector, *args) if ActionController::Routing::Routes.named_routes.helpers.include?(selector) - return super + if ActionController::Routing::Routes.named_routes.helpers.include?(selector) + @controller.send(selector, *args) + else + super + end end - + # Shortcut for <tt>ActionController::TestUploadedFile.new(Test::Unit::TestCase.fixture_path + path, type)</tt>: # # post :change_avatar, :avatar => fixture_file_upload('/files/spongebob.png', 'image/png') diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index bdcb1dc246..ad59d92086 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -300,6 +300,8 @@ module ActionView #:nodoc: # # => 'users/legacy.rhtml' # 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] @@ -343,7 +345,8 @@ module ActionView #:nodoc: ActiveSupport::Deprecation.warn("use_full_path option has been deprecated and has no affect.", caller) end - if defined?(ActionMailer) && defined?(ActionMailer::Base) && controller.is_a?(ActionMailer::Base) && !template_path.include?("/") + if defined?(ActionMailer) && defined?(ActionMailer::Base) && controller.is_a?(ActionMailer::Base) && + template_path.is_a?(String) && !template_path.include?("/") raise ActionViewError, <<-END_ERROR Due to changes in ActionMailer, you need to provide the mailer_name along with the template name. diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb index 769eada120..c2b4f51c9c 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb @@ -463,7 +463,7 @@ module ActionView end private - COMPUTED_PUBLIC_PATHS = ActiveSupport::Cache::MemoryStore.new.silence!.threadsafe! + COMPUTED_PUBLIC_PATHS = ActiveSupport::Cache::MemoryStore.new.silence! # Add the the extension +ext+ if not present. Return full URLs otherwise untouched. # Prefix with <tt>/dir/</tt> if lacking a leading +/+. Account for relative URL @@ -618,6 +618,11 @@ module ActionView def write_asset_file_contents(joined_asset_path, asset_paths) FileUtils.mkdir_p(File.dirname(joined_asset_path)) File.open(joined_asset_path, "w+") { |cache| cache.write(join_asset_file_contents(asset_paths)) } + + # Set mtime to the latest of the combined files to allow for + # consistent ETag without a shared filesystem. + mt = asset_paths.map { |p| File.mtime(File.join(ASSETS_DIR, p)) }.max + File.utime(mt, mt, joined_asset_path) end def collect_asset_files(*path) diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index c7a1d40ff2..953a2a9f86 100644 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -13,9 +13,6 @@ module ActionView # the select_month method would use simply "date" (which can be overwritten using <tt>:prefix</tt>) instead of # "date[month]". module DateHelper - include ActionView::Helpers::TagHelper - DEFAULT_PREFIX = 'date' unless const_defined?('DEFAULT_PREFIX') - # Reports the approximate distance in time between two Time or Date objects or integers as seconds. # Set <tt>include_seconds</tt> to true if you want more detailed approximations when distance < 1 min, 29 secs # Distances are reported based on the following table: @@ -52,7 +49,7 @@ module ActionView # distance_of_time_in_words(from_time, from_time - 45.seconds, true) # => less than a minute # distance_of_time_in_words(from_time, 76.seconds.from_now) # => 1 minute # distance_of_time_in_words(from_time, from_time + 1.year + 3.days) # => about 1 year - # distance_of_time_in_words(from_time, from_time + 4.years + 15.days + 30.minutes + 5.seconds) # => over 4 years + # distance_of_time_in_words(from_time, from_time + 4.years + 9.days + 30.minutes + 5.seconds) # => over 4 years # # to_time = Time.now + 6.years + 19.days # distance_of_time_in_words(from_time, to_time, true) # => over 6 years @@ -109,19 +106,36 @@ module ActionView alias_method :distance_of_time_in_words_to_now, :time_ago_in_words # Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based - # attribute (identified by +method+) on an object assigned to the template (identified by +object+). It's - # possible to tailor the selects through the +options+ hash, which accepts all the keys that each of the - # individual select builders do (like <tt>:use_month_numbers</tt> for select_month) as well as a range of discard - # options. The discard options are <tt>:discard_year</tt>, <tt>:discard_month</tt> and <tt>:discard_day</tt>. Set - # to true, they'll drop the respective select. Discarding the month select will also automatically discard the - # day select. It's also possible to explicitly set the order of the tags using the <tt>:order</tt> option with an - # array of symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. Symbols may be omitted - # and the respective select is not included. - # - # Pass the <tt>:default</tt> option to set the default date. Use a Time object or a Hash of <tt>:year</tt>, - # <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>, <tt>:minute</tt>, and <tt>:second</tt>. - # - # Passing <tt>:disabled => true</tt> as part of the +options+ will make elements inaccessible for change. + # attribute (identified by +method+) on an object assigned to the template (identified by +object+). You can + # the output in the +options+ hash. + # + # ==== Options + # * <tt>:use_month_numbers</tt> - Set to true if you want to use month numbers rather than month names (e.g. + # "2" instead of "February"). + # * <tt>:use_short_month</tt> - Set to true if you want to use the abbreviated month name instead of the full + # name (e.g. "Feb" instead of "February"). + # * <tt>:add_month_number</tt> - Set to true if you want to show both, the month's number and name (e.g. + # "2 - February" instead of "February"). + # * <tt>:use_month_names</tt> - Set to an array with 12 month names if you want to customize month names. + # Note: You can also use Rails' new i18n functionality for this. + # * <tt>:date_separator</tt> - Specifies a string to separate the date fields. Default is "" (i.e. nothing). + # * <tt>:start_year</tt> - Set the start year for the year select. Default is <tt>Time.now.year - 5</tt>. + # * <tt>:end_year</tt> - Set the end year for the year select. Default is <tt>Time.now.year + 5</tt>. + # * <tt>:discard_day</tt> - Set to true if you don't want to show a day select. This includes the day + # as a hidden field instead of showing a select field. Also note that this implicitly sets the day to be the + # first of the given month in order to not create invalid dates like 31 February. + # * <tt>:discard_month</tt> - Set to true if you don't want to show a month select. This includes the month + # as a hidden field instead of showing a select field. Also note that this implicitly sets :discard_day to true. + # * <tt>:discard_year</tt> - Set to true if you don't want to show a year select. This includes the year + # as a hidden field instead of showing a select field. + # * <tt>:order</tt> - Set to an array containing <tt>:day</tt>, <tt>:month</tt> and <tt>:year</tt> do + # customize the order in which the select fields are shown. If you leave out any of the symbols, the respective + # select will not be shown (like when you set <tt>:discard_xxx => true</tt>. Defaults to the order defined in + # the respective locale (e.g. [:year, :month, :day] in the en-US locale that ships with Rails). + # * <tt>:include_blank</tt> - Include a blank option in every select field so it's possible to set empty + # 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. # # If anything is passed in the +html_options+ hash it will be applied to every select tag in the set. # @@ -165,9 +179,9 @@ module ActionView InstanceTag.new(object_name, method, self, options.delete(:object)).to_date_select_tag(options, html_options) end - # Returns a set of select tags (one for hour, minute and optionally second) pre-selected for accessing a specified - # time-based attribute (identified by +method+) on an object assigned to the template (identified by +object+). - # You can include the seconds with <tt>:include_seconds</tt>. + # Returns a set of select tags (one for hour, minute and optionally second) pre-selected for accessing a + # specified time-based attribute (identified by +method+) on an object assigned to the template (identified by + # +object+). You can include the seconds with <tt>:include_seconds</tt>. # # This method will also generate 3 input hidden tags, for the actual year, month and day unless the option # <tt>:ignore_date</tt> is set to +true+. @@ -178,7 +192,8 @@ module ActionView # # Creates a time select tag that, when POSTed, will be stored in the post variable in the sunrise attribute # time_select("post", "sunrise") # - # # Creates a time select tag that, when POSTed, will be stored in the order variable in the submitted attribute + # # Creates a time select tag that, when POSTed, will be stored in the order variable in the submitted + # # attribute # time_select("order", "submitted") # # # Creates a time select tag that, when POSTed, will be stored in the mail variable in the sent_at attribute @@ -210,7 +225,8 @@ module ActionView # If anything is passed in the html_options hash it will be applied to every select tag in the set. # # ==== Examples - # # Generates a datetime select that, when POSTed, will be stored in the post variable in the written_on attribute + # # Generates a datetime select that, when POSTed, will be stored in the post variable in the written_on + # # attribute # datetime_select("post", "written_on") # # # Generates a datetime select with a year select that starts at 1995 that, when POSTed, will be stored in the @@ -230,12 +246,12 @@ module ActionView InstanceTag.new(object_name, method, self, options.delete(:object)).to_datetime_select_tag(options, html_options) end - # Returns a set of html select-tags (one for year, month, day, hour, and minute) pre-selected with the +datetime+. - # It's also possible to explicitly set the order of the tags using the <tt>:order</tt> option with an array of - # symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not supply a Symbol, - # it will be appended onto the <tt>:order</tt> passed in. You can also add <tt>:date_separator</tt>, - # <tt>:datetime_separator</tt> and <tt>:time_separator</tt> keys to the +options+ to control visual display of - # the elements. + # Returns a set of html select-tags (one for year, month, day, hour, and minute) pre-selected with the + # +datetime+. It's also possible to explicitly set the order of the tags using the <tt>:order</tt> option with + # an array of symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not + # supply a Symbol, it will be appended onto the <tt>:order</tt> passed in. You can also add + # <tt>:date_separator</tt>, <tt>:datetime_separator</tt> and <tt>:time_separator</tt> keys to the +options+ to + # control visual display of the elements. # # If anything is passed in the html_options hash it will be applied to every select tag in the set. # @@ -270,14 +286,13 @@ module ActionView # select_datetime(my_date_time, :prefix => 'payday') # def select_datetime(datetime = Time.current, options = {}, html_options = {}) - separator = options[:datetime_separator] || '' - select_date(datetime, options, html_options) + separator + select_time(datetime, options, html_options) + DateTimeSelector.new(datetime, options, html_options).select_datetime end # Returns a set of html select-tags (one for year, month, and day) pre-selected with the +date+. # It's possible to explicitly set the order of the tags using the <tt>:order</tt> option with an array of - # symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not supply a Symbol, it - # will be appended onto the <tt>:order</tt> passed in. + # symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not supply a Symbol, + # it will be appended onto the <tt>:order</tt> passed in. # # If anything is passed in the html_options hash it will be applied to every select tag in the set. # @@ -307,12 +322,7 @@ module ActionView # select_date(my_date, :prefix => 'payday') # def select_date(date = Date.current, options = {}, html_options = {}) - options.reverse_merge!(:order => [], :date_separator => '') - [:year, :month, :day].each { |o| options[:order].push(o) unless options[:order].include?(o) } - - options[:order].inject([]) { |s, o| - s << self.send("select_#{o}", date, options, html_options) - }.join(options[:date_separator]) + DateTimeSelector.new(date, options, html_options).select_date end # Returns a set of html select-tags (one for hour and minute) @@ -343,9 +353,7 @@ module ActionView # select_time(my_time, :time_separator => ':', :include_seconds => true) # def select_time(datetime = Time.current, options = {}, html_options = {}) - separator = options[:time_separator] || '' - select_hour(datetime, options, html_options) + separator + select_minute(datetime, options, html_options) + - (options[:include_seconds] ? separator + select_second(datetime, options, html_options) : '') + DateTimeSelector.new(datetime, options, html_options).select_time end # Returns a select tag with options for each of the seconds 0 through 59 with the current second selected. @@ -366,15 +374,12 @@ module ActionView # select_second(my_time, :field_name => 'interval') # def select_second(datetime, options = {}, html_options = {}) - val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.sec) : '' - options[:use_hidden] ? - (options[:include_seconds] ? _date_hidden_html(options[:field_name] || 'second', val, options) : '') : - _date_select_html(options[:field_name] || 'second', _date_build_options(val), options, html_options) + DateTimeSelector.new(datetime, options, html_options).select_second end # Returns a select tag with options for each of the minutes 0 through 59 with the current minute selected. - # Also can return a select tag with options by <tt>minute_step</tt> from 0 through 59 with the 00 minute selected - # The <tt>minute</tt> can also be substituted for a minute number. + # Also can return a select tag with options by <tt>minute_step</tt> from 0 through 59 with the 00 minute + # selected. The <tt>minute</tt> can also be substituted for a minute number. # Override the field name using the <tt>:field_name</tt> option, 'minute' by default. # # ==== Examples @@ -391,11 +396,7 @@ module ActionView # select_minute(my_time, :field_name => 'stride') # def select_minute(datetime, options = {}, html_options = {}) - val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.min) : '' - options[:use_hidden] ? - _date_hidden_html(options[:field_name] || 'minute', val, options) : - _date_select_html(options[:field_name] || 'minute', - _date_build_options(val, :step => options[:minute_step]), options, html_options) + DateTimeSelector.new(datetime, options, html_options).select_minute end # Returns a select tag with options for each of the hours 0 through 23 with the current hour selected. @@ -416,9 +417,7 @@ module ActionView # select_minute(my_time, :field_name => 'stride') # def select_hour(datetime, options = {}, html_options = {}) - val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.hour) : '' - options[:use_hidden] ? _date_hidden_html(options[:field_name] || 'hour', val, options) : - _date_select_html(options[:field_name] || 'hour', _date_build_options(val, :end => 23), options, html_options) + DateTimeSelector.new(datetime, options, html_options).select_hour end # Returns a select tag with options for each of the days 1 through 31 with the current day selected. @@ -439,11 +438,7 @@ module ActionView # select_day(my_time, :field_name => 'due') # def select_day(date, options = {}, html_options = {}) - val = date ? (date.kind_of?(Fixnum) ? date : date.day) : '' - options[:use_hidden] ? _date_hidden_html(options[:field_name] || 'day', val, options) : - _date_select_html(options[:field_name] || 'day', - _date_build_options(val, :start => 1, :end => 31, :leading_zeros => false), - options, html_options) + DateTimeSelector.new(date, options, html_options).select_day end # Returns a select tag with options for each of the months January through December with the current month @@ -481,36 +476,7 @@ module ActionView # select_month(Date.today, :use_month_names => %w(Januar Februar Marts ...)) # def select_month(date, options = {}, html_options = {}) - locale = options[:locale] - - val = date ? (date.kind_of?(Fixnum) ? date : date.month) : '' - if options[:use_hidden] - _date_hidden_html(options[:field_name] || 'month', val, options) - else - month_options = [] - month_names = options[:use_month_names] || begin - key = options[:use_short_month] ? :'date.abbr_month_names' : :'date.month_names' - I18n.translate key, :locale => locale - end - month_names.unshift(nil) if month_names.size < 13 - - 1.upto(12) do |month_number| - month_name = if options[:use_month_numbers] - month_number - elsif options[:add_month_numbers] - month_number.to_s + ' - ' + month_names[month_number] - else - month_names[month_number] - end - - month_options << ((val == month_number) ? - content_tag(:option, month_name, :value => month_number, :selected => "selected") : - content_tag(:option, month_name, :value => month_number) - ) - month_options << "\n" - end - _date_select_html(options[:field_name] || 'month', month_options.join, options, html_options) - end + DateTimeSelector.new(date, options, html_options).select_month end # Returns a select tag with options for each of the five years on each side of the current, which is selected. @@ -537,158 +503,369 @@ module ActionView # select_year(2006, :start_year => 2000, :end_year => 2010) # def select_year(date, options = {}, html_options = {}) - if !date || date == 0 + DateTimeSelector.new(date, options, html_options).select_year + end + end + + class DateTimeSelector #:nodoc: + extend ActiveSupport::Memoizable + include ActionView::Helpers::TagHelper + + DEFAULT_PREFIX = 'date'.freeze unless const_defined?('DEFAULT_PREFIX') + POSITION = { + :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 + }.freeze unless const_defined?('POSITION') + + def initialize(datetime, options = {}, html_options = {}) + @options = options.dup + @html_options = html_options.dup + @datetime = datetime + end + + def select_datetime + # TODO: Remove tag conditional + # Ideally we could just join select_date and select_date for the tag case + if @options[:tag] && @options[:ignore_date] + select_time + elsif @options[:tag] + order = date_order.dup + order -= [:hour, :minute, :second] + + @options[:discard_year] ||= true unless order.include?(:year) + @options[:discard_month] ||= true unless order.include?(:month) + @options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day) + @options[:discard_minute] ||= true if @options[:discard_hour] + @options[:discard_second] ||= true unless @options[:include_seconds] && !@options[:discard_minute] + + # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are + # valid (otherwise it could be 31 and february wouldn't be a valid date) + if @options[:discard_day] && !@options[:discard_month] + @datetime = @datetime.change(:day => 1) + end + + [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) } + order += [:hour, :minute, :second] unless @options[:discard_hour] + + build_selects_from_types(order) + else + "#{select_date}#{@options[:datetime_separator]}#{select_time}" + end + end + + def select_date + order = date_order.dup + + # TODO: Remove tag conditional + if @options[:tag] + @options[:discard_hour] = true + @options[:discard_minute] = true + @options[:discard_second] = true + + @options[:discard_year] ||= true unless order.include?(:year) + @options[:discard_month] ||= true unless order.include?(:month) + @options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day) + + # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are + # valid (otherwise it could be 31 and february wouldn't be a valid date) + if @options[:discard_day] && !@options[:discard_month] + @datetime = @datetime.change(:day => 1) + end + end + + [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) } + + build_selects_from_types(order) + end + + def select_time + order = [] + + # TODO: Remove tag conditional + if @options[:tag] + @options[:discard_month] = true + @options[:discard_year] = true + @options[:discard_day] = true + @options[:discard_second] ||= true unless @options[:include_seconds] + + order += [:year, :month, :day] unless @options[:ignore_date] + end + + order += [:hour, :minute] + order << :second if @options[:include_seconds] + + build_selects_from_types(order) + end + + def select_second + if @options[:use_hidden] || @options[:discard_second] + build_hidden(:second, sec) if @options[:include_seconds] + else + build_options_and_select(:second, sec) + end + end + + def select_minute + if @options[:use_hidden] || @options[:discard_minute] + build_hidden(:minute, min) + else + build_options_and_select(:minute, min, :step => @options[:minute_step]) + end + end + + def select_hour + if @options[:use_hidden] || @options[:discard_hour] + build_hidden(:hour, hour) + else + build_options_and_select(:hour, hour, :end => 23) + end + end + + def select_day + if @options[:use_hidden] || @options[:discard_day] + build_hidden(:day, day) + else + build_options_and_select(:day, day, :start => 1, :end => 31, :leading_zeros => false) + end + end + + def select_month + if @options[:use_hidden] || @options[:discard_month] + build_hidden(:month, month) + else + month_options = [] + 1.upto(12) do |month_number| + options = { :value => month_number } + options[:selected] = "selected" if month == month_number + month_options << content_tag(:option, month_name(month_number), options) + "\n" + end + build_select(:month, month_options.join) + end + end + + def select_year + if !@datetime || @datetime == 0 val = '' middle_year = Date.today.year - elsif date.kind_of?(Fixnum) - val = middle_year = date else - val = middle_year = date.year + val = middle_year = year end - if options[:use_hidden] - _date_hidden_html(options[:field_name] || 'year', val, options) + if @options[:use_hidden] || @options[:discard_year] + build_hidden(:year, val) else - options[:start_year] ||= middle_year - 5 - options[:end_year] ||= middle_year + 5 - step = options[:start_year] < options[:end_year] ? 1 : -1 - - _date_select_html(options[:field_name] || 'year', - _date_build_options(val, - :start => options[:start_year], - :end => options[:end_year], - :step => step, - :leading_zeros => false - ), options, html_options) + options = {} + options[:start] = @options[:start_year] || middle_year - 5 + options[:end] = @options[:end_year] || middle_year + 5 + options[:step] = options[:start] < options[:end] ? 1 : -1 + options[:leading_zeros] = false + + build_options_and_select(:year, val, options) end end private - def _date_build_options(selected, options={}) - options.reverse_merge!(:start => 0, :end => 59, :step => 1, :leading_zeros => true) + %w( sec min hour day month year ).each do |method| + define_method(method) do + @datetime.kind_of?(Fixnum) ? @datetime : @datetime.send(method) if @datetime + end + end + + # Returns translated month names, but also ensures that a custom month + # name array has a leading nil element + def month_names + month_names = @options[:use_month_names] || translated_month_names + month_names.unshift(nil) if month_names.size < 13 + month_names + end + memoize :month_names + + # Returns translated month names + # => [nil, "January", "February", "March", + # "April", "May", "June", "July", + # "August", "September", "October", + # "November", "December"] + # + # If :use_short_month option is set + # => [nil, "Jan", "Feb", "Mar", "Apr", "May", "Jun", + # "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + def translated_month_names + begin + key = @options[:use_short_month] ? :'date.abbr_month_names' : :'date.month_names' + I18n.translate(key, :locale => @options[:locale]) + end + end + + # Lookup month name for number + # month_name(1) => "January" + # + # If :use_month_numbers option is passed + # month_name(1) => 1 + # + # If :add_month_numbers option is passed + # month_name(1) => "1 - January" + def month_name(number) + if @options[:use_month_numbers] + number + elsif @options[:add_month_numbers] + "#{number} - #{month_names[number]}" + else + month_names[number] + end + end + + def date_order + @options[:order] || translated_date_order + end + memoize :date_order + + def translated_date_order + begin + I18n.translate(:'date.order', :locale => @options[:locale]) || [] + end + end + + # Build full select tag from date type and options + def build_options_and_select(type, selected, options = {}) + build_select(type, build_options(selected, options)) + end + + # Build select option html from date value and options + # build_options(15, :start => 1, :end => 31) + # => "<option value="1">1</option> + # <option value=\"2\">2</option> + # <option value=\"3\">3</option>..." + def build_options(selected, options = {}) + start = options.delete(:start) || 0 + stop = options.delete(:end) || 59 + step = options.delete(:step) || 1 + leading_zeros = options.delete(:leading_zeros).nil? ? true : false select_options = [] - (options[:start] || 0).step((options[:end] || 59), options[:step] || 1) do |i| - value = options[:leading_zeros] ? sprintf("%02d", i) : i + start.step(stop, step) do |i| + value = leading_zeros ? sprintf("%02d", i) : i tag_options = { :value => value } tag_options[:selected] = "selected" if selected == i - select_options << content_tag(:option, value, tag_options) end select_options.join("\n") + "\n" end - def _date_select_html(type, html_options, options, select_tag_options = {}) - _date_name_and_id_from_options(options, type) - select_options = {:id => options[:id], :name => options[:name]} - select_options.merge!(:disabled => 'disabled') if options[:disabled] - select_options.merge!(select_tag_options) unless select_tag_options.empty? + # Builds select tag from date type and html select options + # build_select(:month, "<option value="1">January</option>...") + # => "<select id="post_written_on_2i" name="post[written_on(2i)]"> + # <option value="1">January</option>... + # </select>" + def build_select(type, select_options_as_html) + select_options = { + :id => input_id_from_type(type), + :name => input_name_from_type(type) + }.merge(@html_options) + select_options.merge!(:disabled => 'disabled') if @options[:disabled] + select_html = "\n" - select_html << content_tag(:option, '', :value => '') + "\n" if options[:include_blank] - select_html << html_options.to_s + select_html << content_tag(:option, '', :value => '') + "\n" if @options[:include_blank] + select_html << select_options_as_html.to_s + content_tag(:select, select_html, select_options) + "\n" end - def _date_hidden_html(type, value, options) - _date_name_and_id_from_options(options, type) - hidden_html = tag(:input, :type => "hidden", :id => options[:id], :name => options[:name], :value => value) + "\n" + # 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" />" + def build_hidden(type, value) + tag(:input, { + :type => "hidden", + :id => input_id_from_type(type), + :name => input_name_from_type(type), + :value => value + }) + "\n" end - def _date_name_and_id_from_options(options, type) - options[:name] = (options[:prefix] || DEFAULT_PREFIX) + (options[:discard_type] ? '' : "[#{type}]") - options[:id] = options[:name].gsub(/([\[\(])|(\]\[)/, '_').gsub(/[\]\)]/, '') + # Returns the name attribute for the input tag + # => post[written_on(1i)] + def input_name_from_type(type) + prefix = @options[:prefix] || ActionView::Helpers::DateTimeSelector::DEFAULT_PREFIX + prefix += "[#{@options[:index]}]" if @options[:index] + + field_name = @options[:field_name] || type + if @options[:include_position] + field_name += "(#{ActionView::Helpers::DateTimeSelector::POSITION[type]}i)" + end + + @options[:discard_type] ? prefix : "#{prefix}[#{field_name}]" + end + + # Returns the id attribute for the input tag + # => "post_written_on_1i" + def input_id_from_type(type) + input_name_from_type(type).gsub(/([\[\(])|(\]\[)/, '_').gsub(/[\]\)]/, '') + end + + # Given an ordering of datetime components, create the selection html + # and join them with their appropriate seperators + def build_selects_from_types(order) + select = '' + order.reverse.each do |type| + separator = separator(type) unless type == order.first # don't add on last field + select.insert(0, separator.to_s + send("select_#{type}").to_s) + end + select + end + + # Returns the separator for a given datetime component + def separator(type) + case type + when :month, :day + @options[:date_separator] + when :hour + (@options[:discard_year] && @options[:discard_day]) ? "" : @options[:datetime_separator] + when :minute + @options[:time_separator] + when :second + @options[:include_seconds] ? @options[:time_separator] : "" + end end end class InstanceTag #:nodoc: - include DateHelper - def to_date_select_tag(options = {}, html_options = {}) - date_or_time_select(options.merge(:discard_hour => true), html_options) + datetime_selector(options, html_options).select_date end def to_time_select_tag(options = {}, html_options = {}) - date_or_time_select(options.merge(:discard_year => true, :discard_month => true), html_options) + datetime_selector(options, html_options).select_time end def to_datetime_select_tag(options = {}, html_options = {}) - date_or_time_select(options, html_options) + datetime_selector(options, html_options).select_datetime end private - def date_or_time_select(options, html_options = {}) - locale = options[:locale] - - defaults = { :discard_type => true } - options = defaults.merge(options) - datetime = value(object) - datetime ||= default_time_from_options(options[:default]) unless options[:include_blank] - - position = { :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 } - - order = options[:order] ||= I18n.translate(:'date.order', :locale => locale) - - # Discard explicit and implicit by not being included in the :order - discard = {} - discard[:year] = true if options[:discard_year] or !order.include?(:year) - discard[:month] = true if options[:discard_month] or !order.include?(:month) - discard[:day] = true if options[:discard_day] or discard[:month] or !order.include?(:day) - discard[:hour] = true if options[:discard_hour] - discard[:minute] = true if options[:discard_minute] or discard[:hour] - discard[:second] = true unless options[:include_seconds] && !discard[:minute] - - # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are valid - # (otherwise it could be 31 and february wouldn't be a valid date) - if datetime && discard[:day] && !discard[:month] - datetime = datetime.change(:day => 1) - end - - # Maintain valid dates by including hidden fields for discarded elements - [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) } - - # Ensure proper ordering of :hour, :minute and :second - [:hour, :minute, :second].each { |o| order.delete(o); order.push(o) } - - date_or_time_select = '' - order.reverse.each do |param| - # Send hidden fields for discarded elements once output has started - # This ensures AR can reconstruct valid dates using ParseDate - next if discard[param] && (date_or_time_select.empty? || options[:ignore_date]) - - date_or_time_select.insert(0, - self.send("select_#{param}", - datetime, - options_with_prefix(position[param], options.merge(:use_hidden => discard[param])), - html_options)) - date_or_time_select.insert(0, - case param - when :hour then (discard[:year] && discard[:day] ? "" : " — ") - when :minute then " : " - when :second then options[:include_seconds] ? " : " : "" - else "" - end) - end - - date_or_time_select + def datetime_selector(options, html_options) + datetime = value(object) || default_datetime(options) + + options = options.dup + options[:field_name] = @method_name + options[:include_position] = true + options[:prefix] ||= @object_name + options[:index] ||= @auto_index + options[:datetime_separator] ||= ' — ' + options[:time_separator] ||= ' : ' + + DateTimeSelector.new(datetime, options.merge(:tag => true), html_options) end - def options_with_prefix(position, options) - prefix = "#{@object_name}" - if options[:index] - prefix << "[#{options[:index]}]" - elsif @auto_index - prefix << "[#{@auto_index}]" - end - options.merge(:prefix => "#{prefix}[#{@method_name}(#{position}i)]") - end + def default_datetime(options) + return if options[:include_blank] - def default_time_from_options(default) - case default + case options[:default] when nil Time.current when Date, Time - default + options[:default] else + default = options[:default].dup + # Rename :minute and :second to :min and :sec default[:min] ||= default[:minute] default[:sec] ||= default[:second] @@ -699,8 +876,11 @@ module ActionView default[key] ||= time.send(key) end - Time.utc_time(default[:year], default[:month], default[:day], default[:hour], default[:min], default[:sec]) - end + Time.utc_time( + default[:year], default[:month], default[:day], + default[:hour], default[:min], default[:sec] + ) + end end end diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index 8c1dea2186..77f19b36a6 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -71,9 +71,9 @@ module ActionView def number_to_currency(number, options = {}) options.symbolize_keys! - defaults, currency = I18n.translate([:'number.format', :'number.currency.format'], - :locale => options[:locale]) || [{},{}] - defaults = defaults.merge(currency) + defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {} + currency = I18n.translate(:'number.currency.format', :locale => options[:locale], :raise => true) rescue {} + defaults = defaults.merge(currency) precision = options[:precision] || defaults[:precision] unit = options[:unit] || defaults[:unit] @@ -109,9 +109,9 @@ module ActionView def number_to_percentage(number, options = {}) options.symbolize_keys! - defaults, percentage = I18n.translate([:'number.format', :'number.percentage.format'], - :locale => options[:locale]) || [{},{}] - defaults = defaults.merge(percentage) + defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {} + percentage = I18n.translate(:'number.percentage.format', :locale => options[:locale], :raise => true) rescue {} + defaults = defaults.merge(percentage) precision = options[:precision] || defaults[:precision] separator = options[:separator] || defaults[:separator] @@ -151,7 +151,7 @@ module ActionView options = args.extract_options! options.symbolize_keys! - defaults = I18n.translate(:'number.format', :locale => options[:locale]) || {} + defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {} unless args.empty? ActiveSupport::Deprecation.warn('number_with_delimiter takes an option hash ' + @@ -195,9 +195,10 @@ module ActionView options = args.extract_options! options.symbolize_keys! - defaults, precision_defaults = I18n.translate([:'number.format', :'number.precision.format'], - :locale => options[:locale]) || [{},{}] - defaults = defaults.merge(precision_defaults) + defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {} + precision_defaults = I18n.translate(:'number.precision.format', :locale => options[:locale], + :raise => true) rescue {} + defaults = defaults.merge(precision_defaults) unless args.empty? ActiveSupport::Deprecation.warn('number_with_precision takes an option hash ' + @@ -209,12 +210,14 @@ module ActionView separator ||= (options[:separator] || defaults[:separator]) delimiter ||= (options[:delimiter] || defaults[:delimiter]) - rounded_number = (Float(number) * (10 ** precision)).round.to_f / 10 ** precision - number_with_delimiter("%01.#{precision}f" % rounded_number, - :separator => separator, - :delimiter => delimiter) - rescue - number + begin + rounded_number = (Float(number) * (10 ** precision)).round.to_f / 10 ** precision + number_with_delimiter("%01.#{precision}f" % rounded_number, + :separator => separator, + :delimiter => delimiter) + rescue + number + end end STORAGE_UNITS = %w( Bytes KB MB GB TB ).freeze @@ -251,8 +254,8 @@ module ActionView options = args.extract_options! options.symbolize_keys! - defaults, human = I18n.translate([:'number.format', :'number.human.format'], - :locale => options[:locale]) || [{},{}] + defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {} + human = I18n.translate(:'number.human.format', :locale => options[:locale], :raise => true) rescue {} defaults = defaults.merge(human) unless args.empty? @@ -272,13 +275,16 @@ module ActionView number /= 1024 ** exponent unit = STORAGE_UNITS[exponent] - number_with_precision(number, - :precision => precision, - :separator => separator, - :delimiter => delimiter - ).sub(/(\d)(#{Regexp.escape(separator)}[1-9]*)?0+\z/, '\1') + " #{unit}" - rescue - number + begin + escaped_separator = Regexp.escape(separator) + number_with_precision(number, + :precision => precision, + :separator => separator, + :delimiter => delimiter + ).sub(/(\d)(#{escaped_separator}[1-9]*)?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, '') + " #{unit}" + rescue + number + end end end end diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb index 022edf23c8..f9096d0029 100644 --- a/actionpack/lib/action_view/helpers/text_helper.rb +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -558,7 +558,7 @@ module ActionView [-\w]+ # subdomain or domain (?:\.[-\w]+)* # remaining subdomains or domain (?::\d+)? # port - (?:/(?:(?:[~\w\+@%=\(\)-]|(?:[,.;:'][^\s$])))*)* # path + (?:/(?:[~\w\+@%=\(\)-]|(?:[,.;:'][^\s$]))*)* # path (?:\?[\w\+@%&=.;-]+)? # query string (?:\#[\w\-]*)? # trailing anchor ) diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb index f31502d99d..7ba42a3b72 100644 --- a/actionpack/lib/action_view/helpers/url_helper.rb +++ b/actionpack/lib/action_view/helpers/url_helper.rb @@ -442,7 +442,7 @@ module ActionView # # => <a href="mailto:me@domain.com">me@domain.com</a> # # mail_to "me@domain.com", "My email", :encode => "javascript" - # # => <script type="text/javascript">eval(unescape('%64%6f%63...%6d%65%6e'))</script> + # # => <script type="text/javascript">eval(decodeURIComponent('%64%6f%63...%27%29%3b'))</script> # # mail_to "me@domain.com", "My email", :encode => "hex" # # => <a href="mailto:%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d">My email</a> @@ -476,7 +476,7 @@ module ActionView "document.write('#{content_tag("a", name || email_address_obfuscated, html_options.merge({ "href" => "mailto:"+email_address+extras }))}');".each_byte do |c| string << sprintf("%%%x", c) end - "<script type=\"#{Mime::JS}\">eval(unescape('#{string}'))</script>" + "<script type=\"#{Mime::JS}\">eval(decodeURIComponent('#{string}'))</script>" elsif encode == "hex" email_address_encoded = '' email_address_obfuscated.each_byte do |c| diff --git a/actionpack/lib/action_view/partials.rb b/actionpack/lib/action_view/partials.rb index eb74d4a4c7..894b88534c 100644 --- a/actionpack/lib/action_view/partials.rb +++ b/actionpack/lib/action_view/partials.rb @@ -146,7 +146,7 @@ module ActionView def find_partial_path(partial_path) if partial_path.include?('/') - "#{File.dirname(partial_path)}/_#{File.basename(partial_path)}" + File.join(File.dirname(partial_path), "_#{File.basename(partial_path)}") elsif respond_to?(:controller) "#{controller.class.controller_path}/_#{partial_path}" else diff --git a/actionpack/lib/action_view/renderable.rb b/actionpack/lib/action_view/renderable.rb index 5fe1ca86f3..89ac500717 100644 --- a/actionpack/lib/action_view/renderable.rb +++ b/actionpack/lib/action_view/renderable.rb @@ -31,10 +31,10 @@ module ActionView view.send(:evaluate_assigns) view.send(:set_controller_content_type, mime_type) if respond_to?(:mime_type) - view.send(:execute, method(local_assigns), local_assigns) + view.send(:execute, method_name(local_assigns), local_assigns) end - def method(local_assigns) + 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('_')}" end @@ -44,7 +44,7 @@ module ActionView private # Compile and evaluate the template's code (if necessary) def compile(local_assigns) - render_symbol = method(local_assigns) + render_symbol = method_name(local_assigns) @@mutex.synchronize do if recompile?(render_symbol) diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index b281ff61f2..5dc6708431 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -22,6 +22,14 @@ module ActionView #:nodoc: end memoize :format_and_extension + def multipart? + format && format.include?('.') + end + + def content_type + format.gsub('.', '/') + end + def mime_type Mime::Type.lookup_by_extension(format) if format end @@ -84,7 +92,7 @@ module ActionView #:nodoc: # [base_path, name, format, extension] def split(file) if m = file.match(/^(.*\/)?([^\.]+)\.?(\w+)?\.?(\w+)?\.?(\w+)?$/) - if m[5] # Mulipart formats + if m[5] # Multipart formats [m[1], m[2], "#{m[3]}.#{m[4]}", m[5]] elsif m[4] # Single format [m[1], m[2], m[3], m[4]] diff --git a/actionpack/test/controller/assert_select_test.rb b/actionpack/test/controller/assert_select_test.rb index 5af579f3e3..1531e7c21a 100644 --- a/actionpack/test/controller/assert_select_test.rb +++ b/actionpack/test/controller/assert_select_test.rb @@ -17,6 +17,8 @@ unless defined?(ActionMailer) end end +ActionMailer::Base.template_root = FIXTURE_LOAD_PATH + class AssertSelectTest < Test::Unit::TestCase class AssertSelectController < ActionController::Base def response_with=(content) @@ -69,11 +71,10 @@ class AssertSelectTest < Test::Unit::TestCase ActionMailer::Base.deliveries = [] end - def teardown ActionMailer::Base.deliveries.clear end - + def assert_failure(message, &block) e = assert_raises(AssertionFailedError, &block) assert_match(message, e.message) if Regexp === message @@ -91,7 +92,6 @@ class AssertSelectTest < Test::Unit::TestCase assert_failure(/Expected at least 1 element matching \"p\", found 0/) { assert_select "p" } end - def test_equality_true_false render_html %Q{<div id="1"></div><div id="2"></div>} assert_nothing_raised { assert_select "div" } @@ -102,7 +102,6 @@ class AssertSelectTest < Test::Unit::TestCase assert_nothing_raised { assert_select "p", false } end - def test_equality_string_and_regexp render_html %Q{<div id="1">foo</div><div id="2">foo</div>} assert_nothing_raised { assert_select "div", "foo" } @@ -116,7 +115,6 @@ class AssertSelectTest < Test::Unit::TestCase assert_raises(AssertionFailedError) { assert_select "p", :text=>/foobar/ } end - def test_equality_of_html render_html %Q{<p>\n<em>"This is <strong>not</strong> a big problem,"</em> he said.\n</p>} text = "\"This is not a big problem,\" he said." @@ -135,7 +133,6 @@ class AssertSelectTest < Test::Unit::TestCase assert_raises(AssertionFailedError) { assert_select "pre", :html=>text } end - def test_counts render_html %Q{<div id="1">foo</div><div id="2">foo</div>} assert_nothing_raised { assert_select "div", 2 } @@ -166,7 +163,6 @@ class AssertSelectTest < Test::Unit::TestCase end end - def test_substitution_values render_html %Q{<div id="1">foo</div><div id="2">foo</div>} assert_select "div#?", /\d+/ do |elements| @@ -181,7 +177,6 @@ class AssertSelectTest < Test::Unit::TestCase end end - def test_nested_assert_select render_html %Q{<div id="1">foo</div><div id="2">foo</div>} assert_select "div" do |elements| @@ -200,7 +195,7 @@ class AssertSelectTest < Test::Unit::TestCase assert_select "#3", false end end - + assert_failure(/Expected at least 1 element matching \"#4\", found 0\./) do assert_select "div" do assert_select "#4" @@ -208,7 +203,6 @@ class AssertSelectTest < Test::Unit::TestCase end end - def test_assert_select_text_match render_html %Q{<div id="1"><span>foo</span></div><div id="2"><span>bar</span></div>} assert_select "div" do @@ -225,7 +219,6 @@ class AssertSelectTest < Test::Unit::TestCase end end - # With single result. def test_assert_select_from_rjs_with_single_result render_rjs do |page| @@ -255,19 +248,16 @@ class AssertSelectTest < Test::Unit::TestCase end end - # # Test css_select. # - def test_css_select render_html %Q{<div id="1"></div><div id="2"></div>} assert 2, css_select("div").size assert 0, css_select("p").size end - def test_nested_css_select render_html %Q{<div id="1">foo</div><div id="2">foo</div>} assert_select "div#?", /\d+/ do |elements| @@ -286,7 +276,6 @@ class AssertSelectTest < Test::Unit::TestCase end end - # With one result. def test_css_select_from_rjs_with_single_result render_rjs do |page| @@ -309,12 +298,10 @@ class AssertSelectTest < Test::Unit::TestCase assert_equal 1, css_select("#2").size end - # # Test assert_select_rjs. # - # Test that we can pick up all statements in the result. def test_assert_select_rjs_picks_up_all_statements render_rjs do |page| @@ -381,7 +368,6 @@ class AssertSelectTest < Test::Unit::TestCase assert_raises(AssertionFailedError) { assert_select_rjs "test4" } end - def test_assert_select_rjs_for_replace render_rjs do |page| page.replace "test1", "<div id=\"1\">foo</div>" @@ -479,7 +465,7 @@ class AssertSelectTest < Test::Unit::TestCase end end end - + # Simple hide def test_assert_select_rjs_for_hide render_rjs do |page| @@ -500,7 +486,7 @@ class AssertSelectTest < Test::Unit::TestCase end end end - + # Simple toggle def test_assert_select_rjs_for_toggle render_rjs do |page| @@ -521,7 +507,7 @@ class AssertSelectTest < Test::Unit::TestCase end end end - + # Non-positioned insert. def test_assert_select_rjs_for_nonpositioned_insert render_rjs do |page| @@ -568,7 +554,7 @@ class AssertSelectTest < Test::Unit::TestCase assert_select "div", 4 end end - + # Simple selection from a single result. def test_nested_assert_select_rjs_with_single_result render_rjs do |page| @@ -600,7 +586,6 @@ class AssertSelectTest < Test::Unit::TestCase end end - def test_feed_item_encoded render_xml <<-EOF <rss version="2.0"> @@ -654,7 +639,6 @@ EOF end end - # # Test assert_select_email # @@ -670,7 +654,6 @@ EOF end end - protected def render_html(html) @controller.response_with = html diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index 47a0fcf99d..b6cdd116e5 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -109,7 +109,7 @@ class PageCachingTest < Test::Unit::TestCase uses_mocha("should_cache_ok_at_custom_path") do def test_should_cache_ok_at_custom_path - @request.expects(:path).returns("/index.html") + @request.stubs(:path).returns("/index.html") get :ok assert_response :ok assert File.exist?("#{FILE_STORE_PATH}/index.html") diff --git a/actionpack/test/controller/cgi_test.rb b/actionpack/test/controller/cgi_test.rb index 8ca70f8595..813171857a 100644 --- a/actionpack/test/controller/cgi_test.rb +++ b/actionpack/test/controller/cgi_test.rb @@ -75,7 +75,7 @@ class CgiRequestTest < BaseCgiTest assert_equal "rubyonrails.org:8080", @request.host_with_port @request_hash['HTTP_X_FORWARDED_HOST'] = "www.firsthost.org, www.secondhost.org" - assert_equal "www.secondhost.org", @request.host + assert_equal "www.secondhost.org", @request.host(true) end def test_http_host_with_default_port_overrides_server_port diff --git a/actionpack/test/controller/content_type_test.rb b/actionpack/test/controller/content_type_test.rb index d457d13aef..e1bc46bb56 100644 --- a/actionpack/test/controller/content_type_test.rb +++ b/actionpack/test/controller/content_type_test.rb @@ -128,23 +128,23 @@ class AcceptBasedContentTypeTest < ActionController::TestCase def test_render_default_content_types_for_respond_to - @request.env["HTTP_ACCEPT"] = Mime::HTML.to_s + @request.accept = Mime::HTML.to_s get :render_default_content_types_for_respond_to assert_equal Mime::HTML, @response.content_type - @request.env["HTTP_ACCEPT"] = Mime::JS.to_s + @request.accept = Mime::JS.to_s get :render_default_content_types_for_respond_to assert_equal Mime::JS, @response.content_type end def test_render_default_content_types_for_respond_to_with_template - @request.env["HTTP_ACCEPT"] = Mime::XML.to_s + @request.accept = Mime::XML.to_s get :render_default_content_types_for_respond_to assert_equal Mime::XML, @response.content_type end def test_render_default_content_types_for_respond_to_with_overwrite - @request.env["HTTP_ACCEPT"] = Mime::RSS.to_s + @request.accept = Mime::RSS.to_s get :render_default_content_types_for_respond_to assert_equal Mime::XML, @response.content_type end diff --git a/actionpack/test/controller/mime_responds_test.rb b/actionpack/test/controller/mime_responds_test.rb index 1701431858..0d508eb8df 100644 --- a/actionpack/test/controller/mime_responds_test.rb +++ b/actionpack/test/controller/mime_responds_test.rb @@ -177,7 +177,7 @@ class MimeControllerTest < Test::Unit::TestCase end def test_html - @request.env["HTTP_ACCEPT"] = "text/html" + @request.accept = "text/html" get :js_or_html assert_equal 'HTML', @response.body @@ -189,7 +189,7 @@ class MimeControllerTest < Test::Unit::TestCase end def test_all - @request.env["HTTP_ACCEPT"] = "*/*" + @request.accept = "*/*" get :js_or_html assert_equal 'HTML', @response.body # js is not part of all @@ -201,13 +201,13 @@ class MimeControllerTest < Test::Unit::TestCase end def test_xml - @request.env["HTTP_ACCEPT"] = "application/xml" + @request.accept = "application/xml" get :html_xml_or_rss assert_equal 'XML', @response.body end def test_js_or_html - @request.env["HTTP_ACCEPT"] = "text/javascript, text/html" + @request.accept = "text/javascript, text/html" get :js_or_html assert_equal 'JS', @response.body @@ -232,7 +232,7 @@ class MimeControllerTest < Test::Unit::TestCase 'JSON' => %w(application/json text/x-json) }.each do |body, content_types| content_types.each do |content_type| - @request.env['HTTP_ACCEPT'] = content_type + @request.accept = content_type get :json_or_yaml assert_equal body, @response.body end @@ -240,7 +240,7 @@ class MimeControllerTest < Test::Unit::TestCase end def test_js_or_anything - @request.env["HTTP_ACCEPT"] = "text/javascript, */*" + @request.accept = "text/javascript, */*" get :js_or_html assert_equal 'JS', @response.body @@ -252,34 +252,34 @@ class MimeControllerTest < Test::Unit::TestCase end def test_using_defaults - @request.env["HTTP_ACCEPT"] = "*/*" + @request.accept = "*/*" get :using_defaults assert_equal "text/html", @response.content_type assert_equal 'Hello world!', @response.body - @request.env["HTTP_ACCEPT"] = "text/javascript" + @request.accept = "text/javascript" get :using_defaults assert_equal "text/javascript", @response.content_type assert_equal '$("body").visualEffect("highlight");', @response.body - @request.env["HTTP_ACCEPT"] = "application/xml" + @request.accept = "application/xml" get :using_defaults assert_equal "application/xml", @response.content_type assert_equal "<p>Hello world!</p>\n", @response.body end def test_using_defaults_with_type_list - @request.env["HTTP_ACCEPT"] = "*/*" + @request.accept = "*/*" get :using_defaults_with_type_list assert_equal "text/html", @response.content_type assert_equal 'Hello world!', @response.body - @request.env["HTTP_ACCEPT"] = "text/javascript" + @request.accept = "text/javascript" get :using_defaults_with_type_list assert_equal "text/javascript", @response.content_type assert_equal '$("body").visualEffect("highlight");', @response.body - @request.env["HTTP_ACCEPT"] = "application/xml" + @request.accept = "application/xml" get :using_defaults_with_type_list assert_equal "application/xml", @response.content_type assert_equal "<p>Hello world!</p>\n", @response.body @@ -298,55 +298,55 @@ class MimeControllerTest < Test::Unit::TestCase end def test_synonyms - @request.env["HTTP_ACCEPT"] = "application/javascript" + @request.accept = "application/javascript" get :js_or_html assert_equal 'JS', @response.body - @request.env["HTTP_ACCEPT"] = "application/x-xml" + @request.accept = "application/x-xml" get :html_xml_or_rss assert_equal "XML", @response.body end def test_custom_types - @request.env["HTTP_ACCEPT"] = "application/crazy-xml" + @request.accept = "application/crazy-xml" get :custom_type_handling assert_equal "application/crazy-xml", @response.content_type assert_equal 'Crazy XML', @response.body - @request.env["HTTP_ACCEPT"] = "text/html" + @request.accept = "text/html" get :custom_type_handling assert_equal "text/html", @response.content_type assert_equal 'HTML', @response.body end def test_xhtml_alias - @request.env["HTTP_ACCEPT"] = "application/xhtml+xml,application/xml" + @request.accept = "application/xhtml+xml,application/xml" get :html_or_xml assert_equal 'HTML', @response.body end def test_firefox_simulation - @request.env["HTTP_ACCEPT"] = "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5" + @request.accept = "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5" get :html_or_xml assert_equal 'HTML', @response.body end def test_handle_any - @request.env["HTTP_ACCEPT"] = "*/*" + @request.accept = "*/*" get :handle_any assert_equal 'HTML', @response.body - @request.env["HTTP_ACCEPT"] = "text/javascript" + @request.accept = "text/javascript" get :handle_any assert_equal 'Either JS or XML', @response.body - @request.env["HTTP_ACCEPT"] = "text/xml" + @request.accept = "text/xml" get :handle_any assert_equal 'Either JS or XML', @response.body end def test_handle_any_any - @request.env["HTTP_ACCEPT"] = "*/*" + @request.accept = "*/*" get :handle_any_any assert_equal 'HTML', @response.body end @@ -357,31 +357,31 @@ class MimeControllerTest < Test::Unit::TestCase end def test_handle_any_any_explicit_html - @request.env["HTTP_ACCEPT"] = "text/html" + @request.accept = "text/html" get :handle_any_any assert_equal 'HTML', @response.body end def test_handle_any_any_javascript - @request.env["HTTP_ACCEPT"] = "text/javascript" + @request.accept = "text/javascript" get :handle_any_any assert_equal 'Whatever you ask for, I got it', @response.body end def test_handle_any_any_xml - @request.env["HTTP_ACCEPT"] = "text/xml" + @request.accept = "text/xml" get :handle_any_any assert_equal 'Whatever you ask for, I got it', @response.body end def test_rjs_type_skips_layout - @request.env["HTTP_ACCEPT"] = "text/javascript" + @request.accept = "text/javascript" get :all_types_with_layout assert_equal 'RJS for all_types_with_layout', @response.body end def test_html_type_with_layout - @request.env["HTTP_ACCEPT"] = "text/html" + @request.accept = "text/html" get :all_types_with_layout assert_equal '<html><div id="html">HTML for all_types_with_layout</div></html>', @response.body end @@ -460,7 +460,7 @@ class MimeControllerTest < Test::Unit::TestCase end def test_format_with_custom_response_type_and_request_headers - @request.env["HTTP_ACCEPT"] = "text/iphone" + @request.accept = "text/iphone" get :iphone_with_html_response_type assert_equal '<html><div id="iphone">Hello iPhone future from iPhone!</div></html>', @response.body assert_equal "text/html", @response.content_type @@ -470,7 +470,7 @@ class MimeControllerTest < Test::Unit::TestCase get :iphone_with_html_response_type_without_layout assert_equal '<html><div id="html_missing">Hello future from Firefox!</div></html>', @response.body - @request.env["HTTP_ACCEPT"] = "text/iphone" + @request.accept = "text/iphone" assert_raises(ActionView::MissingTemplate) { get :iphone_with_html_response_type_without_layout } end end @@ -522,7 +522,7 @@ class MimeControllerLayoutsTest < Test::Unit::TestCase get :index assert_equal '<html><div id="html">Hello Firefox</div></html>', @response.body - @request.env["HTTP_ACCEPT"] = "text/iphone" + @request.accept = "text/iphone" get :index assert_equal 'Hello iPhone', @response.body end @@ -533,7 +533,7 @@ class MimeControllerLayoutsTest < Test::Unit::TestCase get :index assert_equal 'Super Firefox', @response.body - @request.env["HTTP_ACCEPT"] = "text/iphone" + @request.accept = "text/iphone" get :index assert_equal '<html><div id="super_iphone">Super iPhone</div></html>', @response.body end diff --git a/actionpack/test/controller/new_render_test.rb b/actionpack/test/controller/new_render_test.rb index d2a3a2b0b0..be99350cd2 100644 --- a/actionpack/test/controller/new_render_test.rb +++ b/actionpack/test/controller/new_render_test.rb @@ -136,6 +136,10 @@ class NewRenderTestController < ActionController::Base render :partial => "partial_only", :layout => true end + def partial_with_counter + render :partial => "counter", :locals => { :counter_counter => 5 } + end + def partial_with_locals render :partial => "customer", :locals => { :customer => Customer.new("david") } end @@ -741,6 +745,11 @@ EOS assert_equal "<title>Talking to the layout</title>\nAction was here!", @response.body end + def test_partial_with_counter + get :partial_with_counter + assert_equal "5", @response.body + end + def test_partials_list get :partials_list assert_equal "goodbyeHello: davidHello: marygoodbye\n", @response.body diff --git a/actionpack/test/controller/rack_test.rb b/actionpack/test/controller/rack_test.rb index ab8bbc3bf9..d1650de1fc 100644 --- a/actionpack/test/controller/rack_test.rb +++ b/actionpack/test/controller/rack_test.rb @@ -64,58 +64,61 @@ end class RackRequestTest < BaseRackTest def test_proxy_request - assert_equal 'glu.ttono.us', @request.host_with_port + assert_equal 'glu.ttono.us', @request.host_with_port(true) end def test_http_host @env.delete "HTTP_X_FORWARDED_HOST" @env['HTTP_HOST'] = "rubyonrails.org:8080" - assert_equal "rubyonrails.org:8080", @request.host_with_port + assert_equal "rubyonrails.org", @request.host(true) + assert_equal "rubyonrails.org:8080", @request.host_with_port(true) @env['HTTP_X_FORWARDED_HOST'] = "www.firsthost.org, www.secondhost.org" - assert_equal "www.secondhost.org", @request.host + assert_equal "www.secondhost.org", @request.host(true) end def test_http_host_with_default_port_overrides_server_port @env.delete "HTTP_X_FORWARDED_HOST" @env['HTTP_HOST'] = "rubyonrails.org" - assert_equal "rubyonrails.org", @request.host_with_port + assert_equal "rubyonrails.org", @request.host_with_port(true) end def test_host_with_port_defaults_to_server_name_if_no_host_headers @env.delete "HTTP_X_FORWARDED_HOST" @env.delete "HTTP_HOST" - assert_equal "glu.ttono.us:8007", @request.host_with_port + assert_equal "glu.ttono.us:8007", @request.host_with_port(true) end def test_host_with_port_falls_back_to_server_addr_if_necessary @env.delete "HTTP_X_FORWARDED_HOST" @env.delete "HTTP_HOST" @env.delete "SERVER_NAME" - assert_equal "207.7.108.53:8007", @request.host_with_port + assert_equal "207.7.108.53", @request.host(true) + assert_equal 8007, @request.port(true) + assert_equal "207.7.108.53:8007", @request.host_with_port(true) end def test_host_with_port_if_http_standard_port_is_specified @env['HTTP_X_FORWARDED_HOST'] = "glu.ttono.us:80" - assert_equal "glu.ttono.us", @request.host_with_port + assert_equal "glu.ttono.us", @request.host_with_port(true) end def test_host_with_port_if_https_standard_port_is_specified @env['HTTP_X_FORWARDED_PROTO'] = "https" @env['HTTP_X_FORWARDED_HOST'] = "glu.ttono.us:443" - assert_equal "glu.ttono.us", @request.host_with_port + assert_equal "glu.ttono.us", @request.host_with_port(true) end def test_host_if_ipv6_reference @env.delete "HTTP_X_FORWARDED_HOST" @env['HTTP_HOST'] = "[2001:1234:5678:9abc:def0::dead:beef]" - assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", @request.host + assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", @request.host(true) end def test_host_if_ipv6_reference_with_port @env.delete "HTTP_X_FORWARDED_HOST" @env['HTTP_HOST'] = "[2001:1234:5678:9abc:def0::dead:beef]:8008" - assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", @request.host + assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", @request.host(true) end def test_cgi_environment_variables diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index 76832f5713..1b9b12acc6 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -15,9 +15,14 @@ class TestController < ActionController::Base end def conditional_hello - etag! [:foo, 123] - last_modified! Time.now.utc.beginning_of_day - render :action => 'hello_world' unless performed? + response.last_modified = Time.now.utc.beginning_of_day + response.etag = [:foo, 123] + + if request.fresh?(response) + head :not_modified + else + render :action => 'hello_world' + end end def render_hello_world @@ -428,7 +433,7 @@ class RenderTest < Test::Unit::TestCase end def test_should_render_formatted_html_erb_template_with_faulty_accepts_header - @request.env["HTTP_ACCEPT"] = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, appliction/x-shockwave-flash, */*" + @request.accept = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, appliction/x-shockwave-flash, */*" get :formatted_xml_erb assert_equal '<test>passed formatted html erb</test>', @response.body end @@ -490,16 +495,16 @@ class EtagRenderTest < Test::Unit::TestCase end def test_render_against_etag_request_should_304_when_match - @request.headers["HTTP_IF_NONE_MATCH"] = etag_for("hello david") + @request.if_none_match = etag_for("hello david") get :render_hello_world_from_variable - assert_equal "304 Not Modified", @response.headers['Status'] + assert_equal "304 Not Modified", @response.status assert @response.body.empty? end def test_render_against_etag_request_should_200_when_no_match - @request.headers["HTTP_IF_NONE_MATCH"] = etag_for("hello somewhere else") + @request.if_none_match = etag_for("hello somewhere else") get :render_hello_world_from_variable - assert_equal "200 OK", @response.headers['Status'] + assert_equal "200 OK", @response.status assert !@response.body.empty? end @@ -508,13 +513,13 @@ class EtagRenderTest < Test::Unit::TestCase expected_etag = etag_for('hello david') assert_equal expected_etag, @response.headers['ETag'] - @request.headers["HTTP_IF_NONE_MATCH"] = expected_etag + @request.if_none_match = expected_etag get :render_hello_world_from_variable - assert_equal "304 Not Modified", @response.headers['Status'] + assert_equal "304 Not Modified", @response.status - @request.headers["HTTP_IF_NONE_MATCH"] = "\"diftag\"" + @request.if_none_match = "\"diftag\"" get :render_hello_world_from_variable - assert_equal "200 OK", @response.headers['Status'] + assert_equal "200 OK", @response.status end def render_with_404_shouldnt_have_etag @@ -557,17 +562,17 @@ class LastModifiedRenderTest < Test::Unit::TestCase end def test_request_not_modified - @request.headers["HTTP_IF_MODIFIED_SINCE"] = @last_modified + @request.if_modified_since = @last_modified get :conditional_hello - assert_equal "304 Not Modified", @response.headers['Status'] + assert_equal "304 Not Modified", @response.status assert @response.body.blank?, @response.body assert_equal @last_modified, @response.headers['Last-Modified'] end def test_request_modified - @request.headers["HTTP_IF_MODIFIED_SINCE"] = 'Thu, 16 Jul 2008 00:00:00 GMT' + @request.if_modified_since = 'Thu, 16 Jul 2008 00:00:00 GMT' get :conditional_hello - assert_equal "200 OK", @response.headers['Status'] + assert_equal "200 OK", @response.status assert !@response.body.blank? assert_equal @last_modified, @response.headers['Last-Modified'] end diff --git a/actionpack/test/controller/request_test.rb b/actionpack/test/controller/request_test.rb index 7db5264840..226c1ac018 100644 --- a/actionpack/test/controller/request_test.rb +++ b/actionpack/test/controller/request_test.rb @@ -15,57 +15,57 @@ class RequestTest < Test::Unit::TestCase assert_equal '0.0.0.0', @request.remote_ip @request.remote_addr = '1.2.3.4' - assert_equal '1.2.3.4', @request.remote_ip + assert_equal '1.2.3.4', @request.remote_ip(true) @request.env['HTTP_CLIENT_IP'] = '2.3.4.5' - assert_equal '1.2.3.4', @request.remote_ip + assert_equal '1.2.3.4', @request.remote_ip(true) @request.remote_addr = '192.168.0.1' - assert_equal '2.3.4.5', @request.remote_ip + assert_equal '2.3.4.5', @request.remote_ip(true) @request.env.delete 'HTTP_CLIENT_IP' @request.remote_addr = '1.2.3.4' @request.env['HTTP_X_FORWARDED_FOR'] = '3.4.5.6' - assert_equal '1.2.3.4', @request.remote_ip + assert_equal '1.2.3.4', @request.remote_ip(true) @request.remote_addr = '127.0.0.1' @request.env['HTTP_X_FORWARDED_FOR'] = '3.4.5.6' - assert_equal '3.4.5.6', @request.remote_ip + assert_equal '3.4.5.6', @request.remote_ip(true) @request.env['HTTP_X_FORWARDED_FOR'] = 'unknown,3.4.5.6' - assert_equal '3.4.5.6', @request.remote_ip + assert_equal '3.4.5.6', @request.remote_ip(true) @request.env['HTTP_X_FORWARDED_FOR'] = '172.16.0.1,3.4.5.6' - assert_equal '3.4.5.6', @request.remote_ip + assert_equal '3.4.5.6', @request.remote_ip(true) @request.env['HTTP_X_FORWARDED_FOR'] = '192.168.0.1,3.4.5.6' - assert_equal '3.4.5.6', @request.remote_ip + assert_equal '3.4.5.6', @request.remote_ip(true) @request.env['HTTP_X_FORWARDED_FOR'] = '10.0.0.1,3.4.5.6' - assert_equal '3.4.5.6', @request.remote_ip + assert_equal '3.4.5.6', @request.remote_ip(true) @request.env['HTTP_X_FORWARDED_FOR'] = '10.0.0.1, 10.0.0.1, 3.4.5.6' - assert_equal '3.4.5.6', @request.remote_ip + assert_equal '3.4.5.6', @request.remote_ip(true) @request.env['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,3.4.5.6' - assert_equal '3.4.5.6', @request.remote_ip + assert_equal '3.4.5.6', @request.remote_ip(true) @request.env['HTTP_X_FORWARDED_FOR'] = 'unknown,192.168.0.1' - assert_equal 'unknown', @request.remote_ip + assert_equal 'unknown', @request.remote_ip(true) @request.env['HTTP_X_FORWARDED_FOR'] = '9.9.9.9, 3.4.5.6, 10.0.0.1, 172.31.4.4' - assert_equal '3.4.5.6', @request.remote_ip + assert_equal '3.4.5.6', @request.remote_ip(true) @request.env['HTTP_CLIENT_IP'] = '8.8.8.8' e = assert_raises(ActionController::ActionControllerError) { - @request.remote_ip + @request.remote_ip(true) } assert_match /IP spoofing attack/, e.message assert_match /HTTP_X_FORWARDED_FOR="9.9.9.9, 3.4.5.6, 10.0.0.1, 172.31.4.4"/, e.message assert_match /HTTP_CLIENT_IP="8.8.8.8"/, e.message @request.env['HTTP_X_FORWARDED_FOR'] = '8.8.8.8, 9.9.9.9' - assert_equal '8.8.8.8', @request.remote_ip + assert_equal '8.8.8.8', @request.remote_ip(true) @request.env.delete 'HTTP_CLIENT_IP' @request.env.delete 'HTTP_X_FORWARDED_FOR' @@ -168,58 +168,58 @@ class RequestTest < Test::Unit::TestCase ActionController::Base.relative_url_root = nil # The following tests are for when REQUEST_URI is not supplied (as in IIS) - @request.set_REQUEST_URI nil @request.env['PATH_INFO'] = "/path/of/some/uri?mapped=1" @request.env['SCRIPT_NAME'] = nil #"/path/dispatch.rb" + @request.set_REQUEST_URI nil assert_equal "/path/of/some/uri?mapped=1", @request.request_uri assert_equal "/path/of/some/uri", @request.path ActionController::Base.relative_url_root = '/path' - @request.set_REQUEST_URI nil @request.env['PATH_INFO'] = "/path/of/some/uri?mapped=1" @request.env['SCRIPT_NAME'] = "/path/dispatch.rb" - assert_equal "/path/of/some/uri?mapped=1", @request.request_uri - assert_equal "/of/some/uri", @request.path + @request.set_REQUEST_URI nil + assert_equal "/path/of/some/uri?mapped=1", @request.request_uri(true) + assert_equal "/of/some/uri", @request.path(true) ActionController::Base.relative_url_root = nil - @request.set_REQUEST_URI nil @request.env['PATH_INFO'] = "/path/of/some/uri" @request.env['SCRIPT_NAME'] = nil + @request.set_REQUEST_URI nil assert_equal "/path/of/some/uri", @request.request_uri assert_equal "/path/of/some/uri", @request.path - @request.set_REQUEST_URI nil @request.env['PATH_INFO'] = "/" + @request.set_REQUEST_URI nil assert_equal "/", @request.request_uri assert_equal "/", @request.path - @request.set_REQUEST_URI nil @request.env['PATH_INFO'] = "/?m=b" + @request.set_REQUEST_URI nil assert_equal "/?m=b", @request.request_uri assert_equal "/", @request.path - @request.set_REQUEST_URI nil @request.env['PATH_INFO'] = "/" @request.env['SCRIPT_NAME'] = "/dispatch.cgi" + @request.set_REQUEST_URI nil assert_equal "/", @request.request_uri assert_equal "/", @request.path ActionController::Base.relative_url_root = '/hieraki' - @request.set_REQUEST_URI nil @request.env['PATH_INFO'] = "/hieraki/" @request.env['SCRIPT_NAME'] = "/hieraki/dispatch.cgi" + @request.set_REQUEST_URI nil assert_equal "/hieraki/", @request.request_uri assert_equal "/", @request.path ActionController::Base.relative_url_root = nil @request.set_REQUEST_URI '/hieraki/dispatch.cgi' ActionController::Base.relative_url_root = '/hieraki' - assert_equal "/dispatch.cgi", @request.path + assert_equal "/dispatch.cgi", @request.path(true) ActionController::Base.relative_url_root = nil @request.set_REQUEST_URI '/hieraki/dispatch.cgi' ActionController::Base.relative_url_root = '/foo' - assert_equal "/hieraki/dispatch.cgi", @request.path + assert_equal "/hieraki/dispatch.cgi", @request.path(true) ActionController::Base.relative_url_root = nil # This test ensures that Rails uses REQUEST_URI over PATH_INFO @@ -227,8 +227,8 @@ class RequestTest < Test::Unit::TestCase @request.env['REQUEST_URI'] = "/some/path" @request.env['PATH_INFO'] = "/another/path" @request.env['SCRIPT_NAME'] = "/dispatch.cgi" - assert_equal "/some/path", @request.request_uri - assert_equal "/some/path", @request.path + assert_equal "/some/path", @request.request_uri(true) + assert_equal "/some/path", @request.path(true) end def test_host_with_default_port @@ -244,13 +244,13 @@ class RequestTest < Test::Unit::TestCase end def test_server_software - assert_equal nil, @request.server_software + assert_equal nil, @request.server_software(true) @request.env['SERVER_SOFTWARE'] = 'Apache3.422' - assert_equal 'apache', @request.server_software + assert_equal 'apache', @request.server_software(true) @request.env['SERVER_SOFTWARE'] = 'lighttpd(1.1.4)' - assert_equal 'lighttpd', @request.server_software + assert_equal 'lighttpd', @request.server_software(true) end def test_xml_http_request @@ -280,44 +280,44 @@ class RequestTest < Test::Unit::TestCase def test_symbolized_request_methods [:get, :post, :put, :delete].each do |method| - set_request_method_to method + self.request_method = method assert_equal method, @request.method end end def test_invalid_http_method_raises_exception - set_request_method_to :random_method assert_raises(ActionController::UnknownHttpMethod) do - @request.method + self.request_method = :random_method end end def test_allow_method_hacking_on_post - set_request_method_to :post + self.request_method = :post [:get, :head, :options, :put, :post, :delete].each do |method| - @request.instance_eval { @parameters = { :_method => method } ; @request_method = nil } + @request.instance_eval { @parameters = { :_method => method.to_s } ; @request_method = nil } + @request.request_method(true) assert_equal(method == :head ? :get : method, @request.method) end end def test_invalid_method_hacking_on_post_raises_exception - set_request_method_to :post + self.request_method = :post @request.instance_eval { @parameters = { :_method => :random_method } ; @request_method = nil } assert_raises(ActionController::UnknownHttpMethod) do - @request.method + @request.request_method(true) end end def test_restrict_method_hacking @request.instance_eval { @parameters = { :_method => 'put' } } [:get, :put, :delete].each do |method| - set_request_method_to method + self.request_method = method assert_equal method, @request.method end end - def test_head_masquarading_as_get - set_request_method_to :head + def test_head_masquerading_as_get + self.request_method = :head assert_equal :get, @request.method assert @request.get? assert @request.head? @@ -339,9 +339,16 @@ class RequestTest < Test::Unit::TestCase end def test_nil_format - @request.instance_eval { @parameters = { :format => nil } } + ActionController::Base.use_accept_header, old = + false, ActionController::Base.use_accept_header + + @request.instance_eval { @parameters = {} } @request.env["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest" + assert @request.xhr? assert_equal Mime::JS, @request.format + + ensure + ActionController::Base.use_accept_header = old end def test_content_type @@ -384,9 +391,9 @@ class RequestTest < Test::Unit::TestCase end protected - def set_request_method_to(method) + def request_method=(method) @request.env['REQUEST_METHOD'] = method.to_s.upcase - @request.instance_eval { @request_method = nil } + @request.request_method(true) end end diff --git a/actionpack/test/fixtures/_top_level_partial.html.erb b/actionpack/test/fixtures/_top_level_partial.html.erb new file mode 100644 index 0000000000..0b1c2e46e0 --- /dev/null +++ b/actionpack/test/fixtures/_top_level_partial.html.erb @@ -0,0 +1 @@ +top level partial html
\ No newline at end of file diff --git a/actionpack/test/fixtures/_top_level_partial_only.erb b/actionpack/test/fixtures/_top_level_partial_only.erb new file mode 100644 index 0000000000..44f25b61d0 --- /dev/null +++ b/actionpack/test/fixtures/_top_level_partial_only.erb @@ -0,0 +1 @@ +top level partial
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_counter.html.erb b/actionpack/test/fixtures/test/_counter.html.erb new file mode 100644 index 0000000000..fd245bfc70 --- /dev/null +++ b/actionpack/test/fixtures/test/_counter.html.erb @@ -0,0 +1 @@ +<%= counter_counter %>
\ No newline at end of file diff --git a/actionpack/test/template/asset_tag_helper_test.rb b/actionpack/test/template/asset_tag_helper_test.rb index 8410e82c3c..7e40a55dc5 100644 --- a/actionpack/test/template/asset_tag_helper_test.rb +++ b/actionpack/test/template/asset_tag_helper_test.rb @@ -425,7 +425,8 @@ class AssetTagHelperTest < ActionView::TestCase stylesheet_link_tag(:all, :cache => true) ) - assert File.exist?(File.join(ActionView::Helpers::AssetTagHelper::STYLESHEETS_DIR, 'all.css')) + expected = Dir["#{ActionView::Helpers::AssetTagHelper::STYLESHEETS_DIR}/*.css"].map { |p| File.mtime(p) }.max + assert_equal expected, File.mtime(File.join(ActionView::Helpers::AssetTagHelper::STYLESHEETS_DIR, 'all.css')) assert_dom_equal( %(<link href="http://a0.example.com/stylesheets/money.css" media="screen" rel="stylesheet" type="text/css" />), diff --git a/actionpack/test/template/date_helper_i18n_test.rb b/actionpack/test/template/date_helper_i18n_test.rb index aca3593921..2b40074498 100644 --- a/actionpack/test/template/date_helper_i18n_test.rb +++ b/actionpack/test/template/date_helper_i18n_test.rb @@ -3,22 +3,22 @@ require 'abstract_unit' class DateHelperDistanceOfTimeInWordsI18nTests < Test::Unit::TestCase include ActionView::Helpers::DateHelper attr_reader :request - + def setup @from = Time.mktime(2004, 6, 6, 21, 45, 0) end - + uses_mocha 'date_helper_distance_of_time_in_words_i18n_test' do # distance_of_time_in_words def test_distance_of_time_in_words_calls_i18n { # with include_seconds - [2.seconds, true] => [:'less_than_x_seconds', 5], - [9.seconds, true] => [:'less_than_x_seconds', 10], - [19.seconds, true] => [:'less_than_x_seconds', 20], - [30.seconds, true] => [:'half_a_minute', nil], - [59.seconds, true] => [:'less_than_x_minutes', 1], - [60.seconds, true] => [:'x_minutes', 1], + [2.seconds, true] => [:'less_than_x_seconds', 5], + [9.seconds, true] => [:'less_than_x_seconds', 10], + [19.seconds, true] => [:'less_than_x_seconds', 20], + [30.seconds, true] => [:'half_a_minute', nil], + [59.seconds, true] => [:'less_than_x_minutes', 1], + [60.seconds, true] => [:'x_minutes', 1], # without include_seconds [29.seconds, false] => [:'less_than_x_minutes', 1], @@ -38,7 +38,7 @@ class DateHelperDistanceOfTimeInWordsI18nTests < Test::Unit::TestCase def assert_distance_of_time_in_words_translates_key(passed, expected) diff, include_seconds = *passed - key, count = *expected + key, count = *expected to = @from + diff options = {:locale => 'en-US', :scope => :'datetime.distance_in_words'} @@ -49,11 +49,11 @@ class DateHelperDistanceOfTimeInWordsI18nTests < Test::Unit::TestCase end end end - + class DateHelperSelectTagsI18nTests < Test::Unit::TestCase include ActionView::Helpers::DateHelper attr_reader :request - + uses_mocha 'date_helper_select_tags_i18n_tests' do def setup I18n.stubs(:translate).with(:'date.month_names', :locale => 'en-US').returns Date::MONTHNAMES diff --git a/actionpack/test/template/date_helper_test.rb b/actionpack/test/template/date_helper_test.rb index d8c07e731b..1a645bccc6 100644 --- a/actionpack/test/template/date_helper_test.rb +++ b/actionpack/test/template/date_helper_test.rb @@ -557,11 +557,8 @@ class DateHelperTest < ActionView::TestCase end def test_select_date_with_incomplete_order - expected = %(<select id="date_first_day" name="date[first][day]">\n) - expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) - expected << "</select>\n" - - expected << %(<select id="date_first_year" name="date[first][year]">\n) + # NOTE: modified this test because of minimal API change + expected = %(<select id="date_first_year" name="date[first][year]">\n) expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" @@ -569,6 +566,10 @@ class DateHelperTest < ActionView::TestCase expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n) expected << "</select>\n" + expected << %(<select id="date_first_day" name="date[first][day]">\n) + expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n) + expected << "</select>\n" + assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", :order => [:day]) end @@ -909,6 +910,10 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), { :datetime_separator => "—", :date_separator => "/", :time_separator => ":", :start_year => 2003, :end_year => 2005, :prefix => "date[first]"}, :class => 'selector') end + def test_select_datetime_should_work_with_date + assert_nothing_raised { select_datetime(Date.today) } + 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) @@ -986,31 +991,8 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), {:include_seconds => false}, :class => 'selector') end - uses_mocha 'TestDatetimeAndTimeSelectUseTimeCurrentAsDefault' do - def test_select_datetime_uses_time_current_as_default - time = stub(:year => 2004, :month => 6, :day => 15, :hour => 16, :min => 35, :sec => 0) - Time.expects(:current).returns time - expects(:select_date).with(time, anything, anything).returns('') - expects(:select_time).with(time, anything, anything).returns('') - select_datetime - end - - def test_select_time_uses_time_current_as_default - time = stub(:year => 2004, :month => 6, :day => 15, :hour => 16, :min => 35, :sec => 0) - Time.expects(:current).returns time - expects(:select_hour).with(time, anything, anything).returns('') - expects(:select_minute).with(time, anything, anything).returns('') - select_time - end - - def test_select_date_uses_date_current_as_default - date = stub(:year => 2004, :month => 6, :day => 15) - Date.expects(:current).returns date - expects(:select_year).with(date, anything, anything).returns('') - expects(:select_month).with(date, anything, anything).returns('') - expects(:select_day).with(date, anything, anything).returns('') - select_date - end + def test_select_time_should_work_with_date + assert_nothing_raised { select_time(Date.today) } end def test_date_select @@ -1231,6 +1213,30 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end + def test_date_select_with_separator + @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="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 << " / " + + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\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" 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 << " / " + + expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n} + expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" 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", { :date_separator => " / " }) + end + def test_time_select @post = Post.new @post.written_on = Time.local(2004, 6, 15, 15, 16, 35) @@ -1330,6 +1336,33 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end + def test_time_select_with_separator + @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) + 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) + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + expected << " - " + + expected << %(<select id="post_written_on_6i" name="post[written_on(6i)]">\n) + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 35}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, time_select("post", "written_on", { :time_separator => " - ", :include_seconds => true }) + end + def test_datetime_select @post = Post.new @post.updated_at = Time.local(2004, 6, 15, 16, 35) @@ -1412,6 +1445,47 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end + def test_datetime_select_with_separators + @post = Post.new + @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35) + + expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n} + expected << %{<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 << " / " + + expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\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" 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 << " / " + + expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n} + expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" 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) + 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_updated_at_5i" name="post[updated_at(5i)]">\n) + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + expected << " - " + + expected << %(<select id="post_updated_at_6i" name="post[updated_at(6i)]">\n) + 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 35}>#{sprintf("%02d", i)}</option>\n) } + expected << "</select>\n" + + assert_dom_equal expected, datetime_select("post", "updated_at", { :date_separator => " / ", :datetime_separator => " , ", :time_separator => " - ", :include_seconds => true }) + 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) } @@ -1814,26 +1888,151 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, datetime_select("post", "updated_at", {}, :class => 'selector') end - uses_mocha 'TestInstanceTagDefaultTimeFromOptions' do - def test_instance_tag_default_time_from_options_uses_time_current_as_default_when_hash_passed_as_arg - dummy_instance_tag = ActionView::Helpers::InstanceTag.new(1,2,3) - Time.expects(:current).returns Time.now - dummy_instance_tag.send!(:default_time_from_options, :hour => 2) - end - - def test_instance_tag_default_time_from_options_respects_hash_arg_settings_when_time_falls_in_system_local_dst_spring_gap - with_env_tz('US/Central') do - dummy_instance_tag = ActionView::Helpers::InstanceTag.new(1,2,3) - Time.stubs(:now).returns Time.local(2006, 4, 2, 1) - assert_equal 2, dummy_instance_tag.send!(:default_time_from_options, :hour => 2).hour - end - end - - def test_instance_tag_default_time_from_options_handles_far_future_date - dummy_instance_tag = ActionView::Helpers::InstanceTag.new(1,2,3) - time = dummy_instance_tag.send!(:default_time_from_options, :year => 2050, :month => 2, :day => 10, :hour => 15, :min => 30, :sec => 45) - assert_equal 2050, time.year - end + def test_date_select_should_not_change_passed_options_hash + @post = Post.new + @post.updated_at = Time.local(2008, 7, 16, 23, 30) + + options = { + :order => [ :year, :month, :day ], + :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 }, + :discard_type => false, + :include_blank => false, + :ignore_date => false, + :include_seconds => true + } + date_select(@post, :updated_at, options) + + # note: the literal hash is intentional to show that the actual options hash isn't modified + # don't change this! + assert_equal({ + :order => [ :year, :month, :day ], + :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 }, + :discard_type => false, + :include_blank => false, + :ignore_date => false, + :include_seconds => true + }, options) + end + + def test_datetime_select_should_not_change_passed_options_hash + @post = Post.new + @post.updated_at = Time.local(2008, 7, 16, 23, 30) + + options = { + :order => [ :year, :month, :day ], + :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 }, + :discard_type => false, + :include_blank => false, + :ignore_date => false, + :include_seconds => true + } + datetime_select(@post, :updated_at, options) + + # note: the literal hash is intentional to show that the actual options hash isn't modified + # don't change this! + assert_equal({ + :order => [ :year, :month, :day ], + :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 }, + :discard_type => false, + :include_blank => false, + :ignore_date => false, + :include_seconds => true + }, options) + end + + def test_time_select_should_not_change_passed_options_hash + @post = Post.new + @post.updated_at = Time.local(2008, 7, 16, 23, 30) + + options = { + :order => [ :year, :month, :day ], + :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 }, + :discard_type => false, + :include_blank => false, + :ignore_date => false, + :include_seconds => true + } + time_select(@post, :updated_at, options) + + # note: the literal hash is intentional to show that the actual options hash isn't modified + # don't change this! + assert_equal({ + :order => [ :year, :month, :day ], + :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 }, + :discard_type => false, + :include_blank => false, + :ignore_date => false, + :include_seconds => true + }, options) + end + + def test_select_date_should_not_change_passed_options_hash + options = { + :order => [ :year, :month, :day ], + :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 }, + :discard_type => false, + :include_blank => false, + :ignore_date => false, + :include_seconds => true + } + select_date(Date.today, options) + + # note: the literal hash is intentional to show that the actual options hash isn't modified + # don't change this! + assert_equal({ + :order => [ :year, :month, :day ], + :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 }, + :discard_type => false, + :include_blank => false, + :ignore_date => false, + :include_seconds => true + }, options) + end + + def test_select_datetime_should_not_change_passed_options_hash + options = { + :order => [ :year, :month, :day ], + :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 }, + :discard_type => false, + :include_blank => false, + :ignore_date => false, + :include_seconds => true + } + select_datetime(Time.now, options) + + # note: the literal hash is intentional to show that the actual options hash isn't modified + # don't change this! + assert_equal({ + :order => [ :year, :month, :day ], + :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 }, + :discard_type => false, + :include_blank => false, + :ignore_date => false, + :include_seconds => true + }, options) + end + + def test_select_time_should_not_change_passed_options_hash + options = { + :order => [ :year, :month, :day ], + :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 }, + :discard_type => false, + :include_blank => false, + :ignore_date => false, + :include_seconds => true + } + select_time(Time.now, options) + + # note: the literal hash is intentional to show that the actual options hash isn't modified + # don't change this! + assert_equal({ + :order => [ :year, :month, :day ], + :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 }, + :discard_type => false, + :include_blank => false, + :ignore_date => false, + :include_seconds => true + }, options) end protected diff --git a/actionpack/test/template/number_helper_i18n_test.rb b/actionpack/test/template/number_helper_i18n_test.rb index ce0da398cc..2ee7f43a65 100644 --- a/actionpack/test/template/number_helper_i18n_test.rb +++ b/actionpack/test/template/number_helper_i18n_test.rb @@ -18,35 +18,35 @@ class NumberHelperI18nTests < Test::Unit::TestCase end def test_number_to_currency_translates_currency_formats - I18n.expects(:translate).with( - [:'number.format', :'number.currency.format'], :locale => 'en-US' - ).returns([@number_defaults, @currency_defaults]) + I18n.expects(:translate).with(:'number.format', :locale => 'en-US', :raise => true).returns(@number_defaults) + I18n.expects(:translate).with(:'number.currency.format', :locale => 'en-US', + :raise => true).returns(@currency_defaults) number_to_currency(1, :locale => 'en-US') end def test_number_with_precision_translates_number_formats - I18n.expects(:translate).with( - [:'number.format', :'number.precision.format'], :locale => 'en-US' - ).returns([@number_defaults, @precision_defaults]) + I18n.expects(:translate).with(:'number.format', :locale => 'en-US', :raise => true).returns(@number_defaults) + I18n.expects(:translate).with(:'number.precision.format', :locale => 'en-US', + :raise => true).returns(@precision_defaults) number_with_precision(1, :locale => 'en-US') end def test_number_with_delimiter_translates_number_formats - I18n.expects(:translate).with(:'number.format', :locale => 'en-US').returns(@number_defaults) + I18n.expects(:translate).with(:'number.format', :locale => 'en-US', :raise => true).returns(@number_defaults) number_with_delimiter(1, :locale => 'en-US') end def test_number_to_percentage_translates_number_formats - I18n.expects(:translate).with( - [:'number.format', :'number.percentage.format'], :locale => 'en-US' - ).returns([@number_defaults, @percentage_defaults]) + I18n.expects(:translate).with(:'number.format', :locale => 'en-US', :raise => true).returns(@number_defaults) + I18n.expects(:translate).with(:'number.percentage.format', :locale => 'en-US', + :raise => true).returns(@percentage_defaults) number_to_percentage(1, :locale => 'en-US') end def test_number_to_human_size_translates_human_formats - I18n.expects(:translate).with( - [:'number.format', :'number.human.format'], :locale => 'en-US' - ).returns([@number_defaults, @human_defaults]) + I18n.expects(:translate).with(:'number.format', :locale => 'en-US', :raise => true).returns(@number_defaults) + I18n.expects(:translate).with(:'number.human.format', :locale => 'en-US', + :raise => true).returns(@human_defaults) # can't be called with 1 because this directly returns without calling I18n.translate number_to_human_size(1025, :locale => 'en-US') end diff --git a/actionpack/test/template/render_test.rb b/actionpack/test/template/render_test.rb index b1af043099..31cfdce531 100644 --- a/actionpack/test/template/render_test.rb +++ b/actionpack/test/template/render_test.rb @@ -19,6 +19,10 @@ class ViewRenderTest < Test::Unit::TestCase assert_equal "Hello world!", @view.render("test/hello_world") end + def test_render_file_at_top_level + assert_equal 'Elastica', @view.render('/shared') + end + def test_render_file_with_full_path template_path = File.join(File.dirname(__FILE__), '../fixtures/test/hello_world.erb') assert_equal "Hello world!", @view.render(:file => template_path) @@ -47,6 +51,24 @@ class ViewRenderTest < Test::Unit::TestCase assert_equal "only partial", @view.render(:partial => "test/partial_only") end + def test_render_partial_with_format + assert_equal 'partial html', @view.render(:partial => 'test/partial') + end + + def test_render_partial_at_top_level + # file fixtures/_top_level_partial_only.erb (not fixtures/test) + assert_equal 'top level partial', @view.render(:partial => '/top_level_partial_only') + end + + def test_render_partial_with_format_at_top_level + # file fixtures/_top_level_partial.html.erb (not fixtures/test, with format extension) + assert_equal 'top level partial html', @view.render(:partial => '/top_level_partial') + end + + def test_render_partial_with_locals + assert_equal "5", @view.render(:partial => "test/counter", :locals => { :counter_counter => 5 }) + end + def test_render_partial_with_errors assert_raise(ActionView::TemplateError) { @view.render(:partial => "test/raise") } end @@ -54,14 +76,14 @@ class ViewRenderTest < Test::Unit::TestCase def test_render_partial_collection assert_equal "Hello: davidHello: mary", @view.render(:partial => "test/customer", :collection => [ Customer.new("david"), Customer.new("mary") ]) end - + def test_render_partial_collection_as - assert_equal "david david davidmary mary mary", + assert_equal "david david davidmary mary mary", @view.render(:partial => "test/customer_with_var", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => :customer) end - + def test_render_partial_collection_without_as - assert_equal "local_inspector,local_inspector_counter,object", + assert_equal "local_inspector,local_inspector_counter,object", @view.render(:partial => "test/local_inspector", :collection => [ Customer.new("mary") ]) end diff --git a/actionpack/test/template/url_helper_test.rb b/actionpack/test/template/url_helper_test.rb index 867503fb29..3065d33c1b 100644 --- a/actionpack/test/template/url_helper_test.rb +++ b/actionpack/test/template/url_helper_test.rb @@ -277,7 +277,11 @@ class UrlHelperTest < ActionView::TestCase end def test_mail_to_with_javascript - assert_dom_equal "<script type=\"text/javascript\">eval(unescape('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%22%3e%4d%79%20%65%6d%61%69%6c%3c%2f%61%3e%27%29%3b'))</script>", mail_to("me@domain.com", "My email", :encode => "javascript") + assert_dom_equal "<script type=\"text/javascript\">eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%22%3e%4d%79%20%65%6d%61%69%6c%3c%2f%61%3e%27%29%3b'))</script>", mail_to("me@domain.com", "My email", :encode => "javascript") + end + + def test_mail_to_with_javascript_unicode + assert_dom_equal "<script type=\"text/javascript\">eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%75%6e%69%63%6f%64%65%40%65%78%61%6d%70%6c%65%2e%63%6f%6d%22%3e%c3%ba%6e%69%63%6f%64%65%3c%2f%61%3e%27%29%3b'))</script>", mail_to("unicode@example.com", "únicode", :encode => "javascript") end def test_mail_with_options @@ -301,8 +305,8 @@ class UrlHelperTest < ActionView::TestCase assert_dom_equal "<a href=\"mailto:%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d\">me(at)domain.com</a>", mail_to("me@domain.com", nil, :encode => "hex", :replace_at => "(at)") assert_dom_equal "<a href=\"mailto:%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d\">My email</a>", mail_to("me@domain.com", "My email", :encode => "hex", :replace_at => "(at)") assert_dom_equal "<a href=\"mailto:%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d\">me(at)domain(dot)com</a>", mail_to("me@domain.com", nil, :encode => "hex", :replace_at => "(at)", :replace_dot => "(dot)") - assert_dom_equal "<script type=\"text/javascript\">eval(unescape('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%22%3e%4d%79%20%65%6d%61%69%6c%3c%2f%61%3e%27%29%3b'))</script>", mail_to("me@domain.com", "My email", :encode => "javascript", :replace_at => "(at)", :replace_dot => "(dot)") - assert_dom_equal "<script type=\"text/javascript\">eval(unescape('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%22%3e%6d%65%28%61%74%29%64%6f%6d%61%69%6e%28%64%6f%74%29%63%6f%6d%3c%2f%61%3e%27%29%3b'))</script>", mail_to("me@domain.com", nil, :encode => "javascript", :replace_at => "(at)", :replace_dot => "(dot)") + assert_dom_equal "<script type=\"text/javascript\">eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%22%3e%4d%79%20%65%6d%61%69%6c%3c%2f%61%3e%27%29%3b'))</script>", mail_to("me@domain.com", "My email", :encode => "javascript", :replace_at => "(at)", :replace_dot => "(dot)") + assert_dom_equal "<script type=\"text/javascript\">eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%22%3e%6d%65%28%61%74%29%64%6f%6d%61%69%6e%28%64%6f%74%29%63%6f%6d%3c%2f%61%3e%27%29%3b'))</script>", mail_to("me@domain.com", nil, :encode => "javascript", :replace_at => "(at)", :replace_dot => "(dot)") end def protect_against_forgery? diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index a28be9eed1..9061037b39 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -344,7 +344,7 @@ module ActiveRecord callback(:before_add, record) yield(record) if block_given? @target ||= [] unless loaded? - @target << record + @target << record unless @reflection.options[:uniq] && @target.include?(record) callback(:after_add, record) record end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index cf7ae97452..8e40b331d9 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -2599,7 +2599,7 @@ module ActiveRecord #:nodoc: removed_attributes = attributes.keys - safe_attributes.keys if removed_attributes.any? - logger.debug "WARNING: Can't mass-assign these protected attributes: #{removed_attributes.join(', ')}" + log_protected_attribute_removal(removed_attributes) end safe_attributes @@ -2614,6 +2614,10 @@ module ActiveRecord #:nodoc: end end + def log_protected_attribute_removal(*attributes) + logger.debug "WARNING: Can't mass-assign these protected attributes: #{attributes.join(', ')}" + end + # The primary key and inheritance column can never be set by mass-assignment for security reasons. def attributes_protected_by_default default = [ self.class.primary_key, self.class.inheritance_column ] @@ -2627,8 +2631,15 @@ module ActiveRecord #:nodoc: quoted = {} connection = self.class.connection attribute_names.each do |name| - if column = column_for_attribute(name) - quoted[name] = connection.quote(read_attribute(name), column) unless !include_primary_key && column.primary + if (column = column_for_attribute(name)) && (include_primary_key || !column.primary) + value = read_attribute(name) + + # We need explicit to_yaml because quote() does not properly convert Time/Date fields to YAML. + if value && self.class.serialized_attributes.has_key?(name) && (value.acts_like?(:date) || value.acts_like?(:time)) + value = value.to_yaml + end + + quoted[name] = connection.quote(value, column) end end include_readonly_attributes ? quoted : remove_readonly_attributes(quoted) diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb index 34ffc9a5e5..e765b46cc2 100644 --- a/activerecord/lib/active_record/calculations.rb +++ b/activerecord/lib/active_record/calculations.rb @@ -211,7 +211,7 @@ module ActiveRecord sql << " ORDER BY #{options[:order]} " if options[:order] add_limit!(sql, options, scope) - sql << ')' if use_workaround + sql << ') AS #{aggregate_alias}_subquery' if use_workaround sql end diff --git a/activerecord/lib/active_record/dirty.rb b/activerecord/lib/active_record/dirty.rb index 4ce0356457..63bf8c8f5b 100644 --- a/activerecord/lib/active_record/dirty.rb +++ b/activerecord/lib/active_record/dirty.rb @@ -134,7 +134,9 @@ module ActiveRecord def update_with_dirty if partial_updates? - update_without_dirty(changed) + # Serialized attributes should always be written in case they've been + # changed in place. + update_without_dirty(changed | self.class.serialized_attributes.keys) else update_without_dirty end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 731a350854..fd77f27b77 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -349,6 +349,27 @@ module ActiveRecord end end + # MigrationProxy is used to defer loading of the actual migration classes + # until they are needed + class MigrationProxy + + attr_accessor :name, :version, :filename + + delegate :migrate, :announce, :write, :to=>:migration + + private + + def migration + @migration ||= load_migration + end + + def load_migration + load(filename) + name.constantize + end + + end + class Migrator#:nodoc: class << self def migrate(migrations_path, target_version = nil) @@ -437,7 +458,7 @@ module ActiveRecord runnable.pop if down? && !target.nil? runnable.each do |migration| - Base.logger.info "Migrating to #{migration} (#{migration.version})" + Base.logger.info "Migrating to #{migration.name} (#{migration.version})" # On our way up, we skip migrating the ones we've already migrated # On our way down, we skip reverting the ones we've never migrated @@ -470,11 +491,10 @@ module ActiveRecord raise DuplicateMigrationNameError.new(name.camelize) end - load(file) - - klasses << returning(name.camelize.constantize) do |klass| - class << klass; attr_accessor :version end - klass.version = version + klasses << returning(MigrationProxy.new) do |migration| + migration.name = name.camelize + migration.version = version + migration.filename = file end end diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb index 7f274543b6..eb887ee550 100644 --- a/activerecord/lib/active_record/named_scope.rb +++ b/activerecord/lib/active_record/named_scope.rb @@ -103,7 +103,7 @@ module ActiveRecord attr_reader :proxy_scope, :proxy_options [].methods.each do |m| - unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|find|count|sum|average|maximum|minimum|paginate|first|last|empty?|any?)/ + unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|find|count|sum|average|maximum|minimum|paginate|first|last|empty?|any?|respond_to?)/ delegate m, :to => :proxy_found end end @@ -140,6 +140,10 @@ module ActiveRecord @found ? @found.empty? : count.zero? end + def respond_to?(method) + super || @proxy_scope.respond_to?(method) + end + def any? if block_given? proxy_found.any? { |*block_args| yield(*block_args) } diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index 3631be76a0..1f8a1090eb 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -9,7 +9,7 @@ require 'models/topic' require 'models/reply' class CascadedEagerLoadingTest < ActiveRecord::TestCase - fixtures :authors, :mixins, :companies, :posts, :topics + fixtures :authors, :mixins, :companies, :posts, :topics, :accounts, :comments, :categorizations def test_eager_association_loading_with_cascaded_two_levels authors = Author.find(:all, :include=>{:posts=>:comments}, :order=>"authors.id") diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index f65ada550b..58506574f8 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -21,7 +21,7 @@ class EagerAssociationTest < ActiveRecord::TestCase fixtures :posts, :comments, :authors, :categories, :categories_posts, :companies, :accounts, :tags, :taggings, :people, :readers, :owners, :pets, :author_favorites, :jobs, :references, :subscribers, :subscriptions, :books, - :developers, :projects + :developers, :projects, :developers_projects def test_loading_with_one_association posts = Post.find(:all, :include => :comments) diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index b29df68d22..f71b122ff0 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -70,7 +70,7 @@ end class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects, - :parrots, :pirates, :treasures, :price_estimates + :parrots, :pirates, :treasures, :price_estimates, :tags, :taggings def test_has_and_belongs_to_many david = Developer.find(1) @@ -299,6 +299,17 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 3, projects(:active_record, :reload).developers.size end + def test_uniq_option_prevents_duplicate_push + project = projects(:active_record) + project.developers << developers(:jamis) + project.developers << developers(:david) + assert_equal 3, project.developers.size + + project.developers << developers(:david) + project.developers << developers(:jamis) + assert_equal 3, project.developers.size + end + def test_deleting david = Developer.find(1) active_record = Project.find(1) diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 47e4b3527d..b806e885e1 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -14,7 +14,7 @@ require 'models/reader' class HasManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :categories, :companies, :developers, :projects, :developers_projects, :topics, :authors, :comments, :author_addresses, - :people, :posts + :people, :posts, :readers def setup Client.destroyed_client_ids.clear diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index e6d1b5ddfd..36d30ade5e 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -76,7 +76,7 @@ class TopicWithProtectedContentAndAccessibleAuthorName < ActiveRecord::Base end class BasicsTest < ActiveRecord::TestCase - fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations + fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories def test_table_exists assert !NonExistentTable.table_exists? @@ -1361,6 +1361,12 @@ class BasicsTest < ActiveRecord::TestCase assert_equal(myobj, topic.content) end + def test_serialized_time_attribute + myobj = Time.local(2008,1,1,1,0) + topic = Topic.create("content" => myobj).reload + assert_equal(myobj, topic.content) + end + def test_nil_serialized_attribute_with_class_constraint myobj = MyObject.new('value1', 'value2') topic = Topic.new diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index e5e022050d..feb47a15a8 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -191,6 +191,18 @@ class DirtyTest < ActiveRecord::TestCase assert !pirate.changed? end + def test_save_should_store_serialized_attributes_even_with_partial_updates + with_partial_updates(Topic) do + topic = Topic.create!(:content => {:a => "a"}) + topic.content[:b] = "b" + #assert topic.changed? # Known bug, will fail + topic.save! + assert_equal "b", topic.content[:b] + topic.reload + assert_equal "b", topic.content[:b] + end + end + private def with_partial_updates(klass, on = true) old = klass.partial_updates? diff --git a/activerecord/test/cases/lifecycle_test.rb b/activerecord/test/cases/lifecycle_test.rb index ab005c6b00..54fb3d8c39 100644 --- a/activerecord/test/cases/lifecycle_test.rb +++ b/activerecord/test/cases/lifecycle_test.rb @@ -74,7 +74,7 @@ class MultiObserver < ActiveRecord::Observer end class LifecycleTest < ActiveRecord::TestCase - fixtures :topics, :developers + fixtures :topics, :developers, :minimalistics def test_before_destroy original_count = Topic.count diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb index d6b3e341df..ee66ac948d 100644 --- a/activerecord/test/cases/method_scoping_test.rb +++ b/activerecord/test/cases/method_scoping_test.rb @@ -6,7 +6,7 @@ require 'models/post' require 'models/category' class MethodScopingTest < ActiveRecord::TestCase - fixtures :developers, :projects, :comments, :posts + fixtures :developers, :projects, :comments, :posts, :developers_projects def test_set_conditions Developer.with_scope(:find => { :conditions => 'just a test...' }) do diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 7ecf755ef8..920f719995 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -922,6 +922,26 @@ if ActiveRecord::Base.connection.supports_migrations? migrations[0].name == 'innocent_jointable' end + def test_only_loads_pending_migrations + # migrate up to 1 + ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", 1) + + # now unload the migrations that have been defined + PeopleHaveLastNames.unloadable + ActiveSupport::Dependencies.remove_unloadable_constants! + + ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", nil) + + assert !defined? PeopleHaveLastNames + + %w(WeNeedReminders, InnocentJointable).each do |migration| + assert defined? migration + end + + ensure + load(MIGRATIONS_ROOT + "/valid/1_people_have_last_names.rb") + end + def test_migrator_interleaved_migrations ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/interleaved/pass_1") diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index e21ffbbdba..bd6ec23853 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -45,6 +45,12 @@ class NamedScopeTest < ActiveRecord::TestCase assert_equal Topic.average(:replies_count), Topic.base.average(:replies_count) end + def test_scope_should_respond_to_own_methods_and_methods_of_the_proxy + assert Topic.approved.respond_to?(:proxy_found) + assert Topic.approved.respond_to?(:count) + assert Topic.approved.respond_to?(:length) + end + def test_subclasses_inherit_scopes assert Topic.scopes.include?(:base) @@ -186,9 +192,10 @@ class NamedScopeTest < ActiveRecord::TestCase def test_any_should_not_load_results topics = Topic.base - assert_queries(1) do - topics.expects(:empty?).returns(true) - assert !topics.any? + assert_queries(2) do + topics.any? # use count query + topics.collect # force load + topics.any? # use loaded (no query) end end diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index dc9eeec281..eae2104531 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -58,7 +58,7 @@ end uses_mocha 'QueryCacheExpiryTest' do class QueryCacheExpiryTest < ActiveRecord::TestCase - fixtures :tasks + fixtures :tasks, :posts, :categories, :categories_posts def test_find Task.connection.expects(:clear_query_cache).times(1) diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 5a064f8bea..95eae3a77e 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -39,10 +39,6 @@ module ActiveSupport class Store cattr_accessor :logger - def threadsafe! - extend ThreadSafety - end - def silence! @silence = true self @@ -115,20 +111,6 @@ module ActiveSupport logger.debug("Cache #{operation}: #{key}#{options ? " (#{options.inspect})" : ""}") if logger && !@silence && !@logger_off end end - - module ThreadSafety #:nodoc: - def self.extended(object) #:nodoc: - object.instance_variable_set(:@mutex, Mutex.new) - end - - %w(read write delete delete_matched exist? increment decrement).each do |method| - module_eval <<-EOS, __FILE__, __LINE__ - def #{method}(*args) - @mutex.synchronize { super } - end - EOS - end - end end end diff --git a/activesupport/lib/active_support/cache/compressed_mem_cache_store.rb b/activesupport/lib/active_support/cache/compressed_mem_cache_store.rb index 9470ac9f66..0bff6cf9ad 100644 --- a/activesupport/lib/active_support/cache/compressed_mem_cache_store.rb +++ b/activesupport/lib/active_support/cache/compressed_mem_cache_store.rb @@ -1,14 +1,14 @@ module ActiveSupport module Cache class CompressedMemCacheStore < MemCacheStore - def read(name, options = {}) - if value = super(name, options.merge(:raw => true)) + def read(name, options = nil) + if value = super(name, (options || {}).merge(:raw => true)) Marshal.load(ActiveSupport::Gzip.decompress(value)) end end - def write(name, value, options = {}) - super(name, ActiveSupport::Gzip.compress(Marshal.dump(value)), options.merge(:raw => true)) + def write(name, value, options = nil) + super(name, ActiveSupport::Gzip.compress(Marshal.dump(value)), (options || {}).merge(:raw => true)) end end end diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index 5b771b1da0..437679cc05 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -9,13 +9,13 @@ module ActiveSupport def read(name, options = nil) super - File.open(real_file_path(name), 'rb') { |f| f.read } rescue nil + File.open(real_file_path(name), 'rb') { |f| Marshal.load(f) } rescue nil end def write(name, value, options = nil) super ensure_cache_path(File.dirname(real_file_path(name))) - File.open(real_file_path(name), "wb+") { |f| f.write(value) } + File.atomic_write(real_file_path(name), cache_path) { |f| Marshal.dump(value, f) } rescue => e RAILS_DEFAULT_LOGGER.error "Couldn't create cache directory: #{name} (#{e.message})" if RAILS_DEFAULT_LOGGER end diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb index 6f114273e4..a44f877414 100644 --- a/activesupport/lib/active_support/cache/memory_store.rb +++ b/activesupport/lib/active_support/cache/memory_store.rb @@ -3,6 +3,13 @@ module ActiveSupport class MemoryStore < Store def initialize @data = {} + @mutex = Mutex.new + end + + def fetch(key, options = {}) + @mutex.synchronize do + super + end end def read(name, options = nil) @@ -16,23 +23,32 @@ module ActiveSupport end def delete(name, options = nil) - super @data.delete(name) end def delete_matched(matcher, options = nil) - super @data.delete_if { |k,v| k =~ matcher } end def exist?(name,options = nil) - super @data.has_key?(name) end + def increment(key, amount = 1) + @mutex.synchronize do + super + end + end + + def decrement(key, amount = 1) + @mutex.synchronize do + super + end + end + def clear @data.clear end end end -end
\ No newline at end of file +end diff --git a/activesupport/lib/active_support/core_ext/file.rb b/activesupport/lib/active_support/core_ext/file.rb index 45d93b220f..e03f8ac44e 100644 --- a/activesupport/lib/active_support/core_ext/file.rb +++ b/activesupport/lib/active_support/core_ext/file.rb @@ -1,21 +1,5 @@ -require 'tempfile' +require 'active_support/core_ext/file/atomic' -# Write to a file atomically. Useful for situations where you don't -# want other processes or threads to see half-written files. -# -# File.atomic_write("important.file") do |file| -# file.write("hello") -# end -# -# If your temp directory is not on the same filesystem as the file you're -# trying to write, you can provide a different temporary directory. -# -# File.atomic_write("/data/something.important", "/data/tmp") do |f| -# file.write("hello") -# end -def File.atomic_write(file_name, temp_dir = Dir.tmpdir) - temp_file = Tempfile.new(File.basename(file_name), temp_dir) - yield temp_file - temp_file.close - File.rename(temp_file.path, file_name) -end
\ No newline at end of file +class File #:nodoc: + extend ActiveSupport::CoreExtensions::File::Atomic +end diff --git a/activesupport/lib/active_support/core_ext/file/atomic.rb b/activesupport/lib/active_support/core_ext/file/atomic.rb new file mode 100644 index 0000000000..4d3cf5423f --- /dev/null +++ b/activesupport/lib/active_support/core_ext/file/atomic.rb @@ -0,0 +1,46 @@ +require 'tempfile' + +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module File #:nodoc: + module Atomic + # Write to a file atomically. Useful for situations where you don't + # want other processes or threads to see half-written files. + # + # File.atomic_write("important.file") do |file| + # file.write("hello") + # end + # + # If your temp directory is not on the same filesystem as the file you're + # trying to write, you can provide a different temporary directory. + # + # File.atomic_write("/data/something.important", "/data/tmp") do |f| + # file.write("hello") + # end + def atomic_write(file_name, temp_dir = Dir.tmpdir) + temp_file = Tempfile.new(basename(file_name), temp_dir) + yield temp_file + temp_file.close + + begin + # Get original file permissions + old_stat = stat(file_name) + rescue Errno::ENOENT + # No old permissions, write a temp file to determine the defaults + check_name = ".permissions_check.#{Thread.current.object_id}.#{Process.pid}.#{rand(1000000)}" + new(check_name, "w") + old_stat = stat(check_name) + unlink(check_name) + end + + # Overwrite original file with temp file + rename(temp_file.path, file_name) + + # Set correct permissions on new file + chown(old_stat.uid, old_stat.gid, file_name) + chmod(old_stat.mode, file_name) + end + end + end + end +end diff --git a/activesupport/lib/active_support/inflector.rb b/activesupport/lib/active_support/inflector.rb index 6651569d33..c2738b39fc 100644 --- a/activesupport/lib/active_support/inflector.rb +++ b/activesupport/lib/active_support/inflector.rb @@ -291,11 +291,14 @@ module ActiveSupport # NameError is raised when the name is not in CamelCase or the constant is # unknown. def constantize(camel_cased_word) - unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ camel_cased_word - raise NameError, "#{camel_cased_word.inspect} is not a valid constant name!" - end + names = camel_cased_word.split('::') + names.shift if names.empty? || names.first.empty? - Object.module_eval("::#{$1}", __FILE__, __LINE__) + constant = Object + names.each do |name| + constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name) + end + constant end # Turns a number into an ordinal string used to denote the position in an @@ -326,4 +329,4 @@ require 'active_support/inflections' require 'active_support/core_ext/string/inflections' unless String.included_modules.include?(ActiveSupport::CoreExtensions::String::Inflections) String.send :include, ActiveSupport::CoreExtensions::String::Inflections -end
\ No newline at end of file +end diff --git a/activesupport/lib/active_support/memoizable.rb b/activesupport/lib/active_support/memoizable.rb index 23dd96e4df..6506238ac0 100644 --- a/activesupport/lib/active_support/memoizable.rb +++ b/activesupport/lib/active_support/memoizable.rb @@ -10,18 +10,37 @@ module ActiveSupport end def freeze_with_memoizable - methods.each do |method| - __send__($1) if method.to_s =~ /^_unmemoized_(.*)/ - end unless frozen? - + memoize_all unless frozen? freeze_without_memoizable end + + def memoize_all + methods.each do |m| + if m.to_s =~ /^_unmemoized_(.*)/ + if method(m).arity == 0 + __send__($1) + else + ivar = :"@_memoized_#{$1}" + instance_variable_set(ivar, {}) + end + end + end + end + + def unmemoize_all + methods.each do |m| + if m.to_s =~ /^_unmemoized_(.*)/ + ivar = :"@_memoized_#{$1}" + instance_variable_get(ivar).clear if instance_variable_defined?(ivar) + end + end + end end def memoize(*symbols) symbols.each do |symbol| - original_method = "_unmemoized_#{symbol}" - memoized_ivar = "@_memoized_#{symbol}" + original_method = :"_unmemoized_#{symbol}" + memoized_ivar = :"@_memoized_#{symbol.to_s.sub(/\?\Z/, '_query').sub(/!\Z/, '_bang')}" class_eval <<-EOS, __FILE__, __LINE__ include Freezable @@ -29,14 +48,27 @@ module ActiveSupport raise "Already memoized #{symbol}" if method_defined?(:#{original_method}) alias #{original_method} #{symbol} - def #{symbol}(*args) - #{memoized_ivar} ||= {} - reload = args.pop if args.last == true || args.last == :reload + 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 !reload && #{memoized_ivar} && #{memoized_ivar}.has_key?(args) - #{memoized_ivar}[args] - else - #{memoized_ivar}[args] = #{original_method}(*args).freeze + if #{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 EOS diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index 0af4251962..c5f7fb7fdd 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -71,69 +71,29 @@ uses_mocha 'high-level cache store tests' do end end -class ThreadSafetyCacheStoreTest < Test::Unit::TestCase +class FileStoreTest < Test::Unit::TestCase def setup - @cache = ActiveSupport::Cache.lookup_store(:memory_store).threadsafe! - @cache.write('foo', 'bar') - - # No way to have mocha proxy to the original method - @mutex = @cache.instance_variable_get(:@mutex) - @mutex.instance_eval %( - def calls; @calls; end - def synchronize - @calls ||= 0 - @calls += 1 - yield - end - ) + @cache = ActiveSupport::Cache.lookup_store(:file_store, Dir.pwd) end - def test_read_is_synchronized + def test_should_read_and_write_strings + @cache.write('foo', 'bar') assert_equal 'bar', @cache.read('foo') - assert_equal 1, @mutex.calls - end - - def test_write_is_synchronized - @cache.write('foo', 'baz') - assert_equal 'baz', @cache.read('foo') - assert_equal 2, @mutex.calls + ensure + File.delete("foo.cache") end - def test_delete_is_synchronized - assert_equal 'bar', @cache.read('foo') - @cache.delete('foo') - assert_equal nil, @cache.read('foo') - assert_equal 3, @mutex.calls + def test_should_read_and_write_hash + @cache.write('foo', {:a => "b"}) + assert_equal({:a => "b"}, @cache.read('foo')) + ensure + File.delete("foo.cache") end - def test_delete_matched_is_synchronized - assert_equal 'bar', @cache.read('foo') - @cache.delete_matched(/foo/) + def test_should_read_and_write_nil + @cache.write('foo', nil) assert_equal nil, @cache.read('foo') - assert_equal 3, @mutex.calls - end - - def test_fetch_is_synchronized - assert_equal 'bar', @cache.fetch('foo') { 'baz' } - assert_equal 'fu', @cache.fetch('bar') { 'fu' } - assert_equal 3, @mutex.calls - end - - def test_exist_is_synchronized - assert @cache.exist?('foo') - assert !@cache.exist?('bar') - assert_equal 2, @mutex.calls - end - - def test_increment_is_synchronized - @cache.write('foo_count', 1) - assert_equal 2, @cache.increment('foo_count') - assert_equal 4, @mutex.calls - end - - def test_decrement_is_synchronized - @cache.write('foo_count', 1) - assert_equal 0, @cache.decrement('foo_count') - assert_equal 4, @mutex.calls + ensure + File.delete("foo.cache") end end diff --git a/activesupport/test/core_ext/file_test.rb b/activesupport/test/core_ext/file_test.rb index 5efe357e9f..eedc6b592b 100644 --- a/activesupport/test/core_ext/file_test.rb +++ b/activesupport/test/core_ext/file_test.rb @@ -1,9 +1,8 @@ require 'abstract_unit' class AtomicWriteTest < Test::Unit::TestCase - def test_atomic_write_without_errors - contents = "Atomic Text" + contents = "Atomic Text" File.atomic_write(file_name, Dir.pwd) do |file| file.write(contents) assert !File.exist?(file_name) @@ -13,7 +12,7 @@ class AtomicWriteTest < Test::Unit::TestCase ensure File.unlink(file_name) rescue nil end - + def test_atomic_write_doesnt_write_when_block_raises File.atomic_write(file_name) do |file| file.write("testing") @@ -22,8 +21,47 @@ class AtomicWriteTest < Test::Unit::TestCase rescue assert !File.exist?(file_name) end - - def file_name - "atomic.file" + + def test_atomic_write_preserves_file_permissions + contents = "Atomic Text" + File.open(file_name, "w", 0755) do |file| + file.write(contents) + assert File.exist?(file_name) + end + assert File.exist?(file_name) + assert_equal 0100755, file_mode + assert_equal contents, File.read(file_name) + + File.atomic_write(file_name, Dir.pwd) do |file| + file.write(contents) + assert File.exist?(file_name) + end + assert File.exist?(file_name) + assert_equal 0100755, file_mode + assert_equal contents, File.read(file_name) + ensure + File.unlink(file_name) rescue nil + end + + def test_atomic_write_preserves_default_file_permissions + contents = "Atomic Text" + File.atomic_write(file_name, Dir.pwd) do |file| + file.write(contents) + assert !File.exist?(file_name) + end + assert File.exist?(file_name) + assert_equal 0100666 ^ File.umask, file_mode + assert_equal contents, File.read(file_name) + ensure + File.unlink(file_name) rescue nil end + + private + def file_name + "atomic.file" + end + + def file_mode + File.stat(file_name).mode + end end diff --git a/activesupport/test/memoizable_test.rb b/activesupport/test/memoizable_test.rb index cd84dcda53..135d56f14a 100644 --- a/activesupport/test/memoizable_test.rb +++ b/activesupport/test/memoizable_test.rb @@ -16,6 +16,16 @@ uses_mocha 'Memoizable' do "Josh" end + def name? + true + end + memoize :name? + + def update(name) + "Joshua" + end + memoize :update + def age @age_calls += 1 nil @@ -88,6 +98,10 @@ uses_mocha 'Memoizable' do assert_equal 1, @person.name_calls end + def test_memoization_with_punctuation + assert_equal true, @person.name? + end + def test_memoization_with_nil_value assert_equal nil, @person.age assert_equal 1, @person.age_calls @@ -96,6 +110,11 @@ uses_mocha 'Memoizable' do assert_equal 1, @person.age_calls end + def test_memorized_results_are_immutable + assert_equal "Josh", @person.name + assert_raise(ActiveSupport::FrozenObjectError) { @person.name.gsub!("Josh", "Gosh") } + end + def test_reloadable counter = @calculator.counter assert_equal 1, @calculator.counter @@ -105,6 +124,21 @@ uses_mocha 'Memoizable' do assert_equal 3, @calculator.counter end + def test_unmemoize_all + assert_equal 1, @calculator.counter + + assert @calculator.instance_variable_get(:@_memoized_counter).any? + @calculator.unmemoize_all + assert @calculator.instance_variable_get(:@_memoized_counter).empty? + + assert_equal 2, @calculator.counter + end + + def test_memoize_all + @calculator.memoize_all + assert @calculator.instance_variable_defined?(:@_memoized_counter) + end + def test_memoization_cache_is_different_for_each_instance assert_equal 1, @calculator.counter assert_equal 2, @calculator.counter(:reload) @@ -114,6 +148,7 @@ uses_mocha 'Memoizable' do def test_memoized_is_not_affected_by_freeze @person.freeze assert_equal "Josh", @person.name + assert_equal "Joshua", @person.update("Joshua") end def test_memoization_with_args diff --git a/railties/CHANGELOG b/railties/CHANGELOG index 6df7c568dc..3a276d5aad 100644 --- a/railties/CHANGELOG +++ b/railties/CHANGELOG @@ -1,5 +1,7 @@ *Edge* +* Added config.threadsafe! to toggle allow concurrency settings and disable the dependency loader [Josh Peek] + * Turn cache_classes on by default [Josh Peek] * Added configurable eager load paths. Defaults to app/models, app/controllers, and app/helpers [Josh Peek] diff --git a/railties/environments/production.rb b/railties/environments/production.rb index e915e8be73..ec5b7bc865 100644 --- a/railties/environments/production.rb +++ b/railties/environments/production.rb @@ -4,6 +4,9 @@ # Code is not reloaded between requests config.cache_classes = true +# Enable threaded mode +# config.threadsafe! + # Use a different logger for distributed setups # config.logger = SyslogLogger.new diff --git a/railties/lib/commands/runner.rb b/railties/lib/commands/runner.rb index 926bc26344..14159c3893 100644 --- a/railties/lib/commands/runner.rb +++ b/railties/lib/commands/runner.rb @@ -42,7 +42,7 @@ if code_or_file.nil? $stderr.puts "Run '#{$0} -h' for help." exit 1 elsif File.exist?(code_or_file) - eval(File.read(code_or_file)) + eval(File.read(code_or_file), nil, code_or_file) else eval(code_or_file) end diff --git a/railties/lib/initializer.rb b/railties/lib/initializer.rb index a2d08e2938..6576cd368b 100644 --- a/railties/lib/initializer.rb +++ b/railties/lib/initializer.rb @@ -340,9 +340,11 @@ Run `rake gems:install` to install the missing gems. end def load_view_paths - ActionView::PathSet::Path.eager_load_templates! if configuration.cache_classes - ActionMailer::Base.template_root.load if configuration.frameworks.include?(:action_mailer) - ActionController::Base.view_paths.load if configuration.frameworks.include?(:action_controller) + if configuration.frameworks.include?(:action_view) + ActionView::PathSet::Path.eager_load_templates! if configuration.cache_classes + ActionController::Base.view_paths.load if configuration.frameworks.include?(:action_controller) + ActionMailer::Base.template_root.load if configuration.frameworks.include?(:action_mailer) + end end # Eager load application classes @@ -440,9 +442,11 @@ Run `rake gems:install` to install the missing gems. # paths have already been set, it is not changed, otherwise it is # set to use Configuration#view_path. def initialize_framework_views - view_path = ActionView::PathSet::Path.new(configuration.view_path, false) - ActionMailer::Base.template_root ||= view_path if configuration.frameworks.include?(:action_mailer) - ActionController::Base.view_paths = view_path if configuration.frameworks.include?(:action_controller) && ActionController::Base.view_paths.empty? + if configuration.frameworks.include?(:action_view) + view_path = ActionView::PathSet::Path.new(configuration.view_path, false) + ActionMailer::Base.template_root ||= view_path if configuration.frameworks.include?(:action_mailer) + ActionController::Base.view_paths = view_path if configuration.frameworks.include?(:action_controller) && ActionController::Base.view_paths.empty? + end end # If Action Controller is not one of the loaded frameworks (Configuration#frameworks) @@ -688,13 +692,17 @@ Run `rake gems:install` to install the missing gems. # You can add gems with the #gem method. attr_accessor :gems - # Adds a single Gem dependency to the rails application. + # Adds a single Gem dependency to the rails application. By default, it will require + # the library with the same name as the gem. Use :lib to specify a different name. # # # gem 'aws-s3', '>= 0.4.0' # # require 'aws/s3' # config.gem 'aws-s3', :lib => 'aws/s3', :version => '>= 0.4.0', \ # :source => "http://code.whytheluckystiff.net" # + # To require a library be installed, but not attempt to load it, pass :lib => false + # + # config.gem 'qrp', :version => '0.4.1', :lib => false def gem(name, options = {}) @gems << Rails::GemDependency.new(name, options) end @@ -764,6 +772,17 @@ Run `rake gems:install` to install the missing gems. ::RAILS_ROOT.replace @root_path end + # Enable threaded mode. Allows concurrent requests to controller actions and + # multiple database connections. Also disables automatic dependency loading + # after boot + def threadsafe! + self.cache_classes = true + self.dependency_loading = false + self.active_record.allow_concurrency = true + self.action_controller.allow_concurrency = true + self + end + # Loads and returns the contents of the #database_configuration_file. The # contents of the file are processed via ERB before being sent through # YAML::load. diff --git a/railties/lib/rails/gem_dependency.rb b/railties/lib/rails/gem_dependency.rb index f8d97840c1..471e03fa5f 100644 --- a/railties/lib/rails/gem_dependency.rb +++ b/railties/lib/rails/gem_dependency.rb @@ -58,7 +58,7 @@ module Rails def load return if @loaded || @load_paths_added == false - require(@lib || @name) + require(@lib || @name) unless @lib == false @loaded = true rescue LoadError puts $!.to_s diff --git a/railties/lib/tasks/databases.rake b/railties/lib/tasks/databases.rake index 5ec712a02d..21c81b3fb5 100644 --- a/railties/lib/tasks/databases.rake +++ b/railties/lib/tasks/databases.rake @@ -182,11 +182,11 @@ namespace :db do end namespace :fixtures do - desc "Load fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z." + desc "Load fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." task :load => :environment do require 'active_record/fixtures' ActiveRecord::Base.establish_connection(Rails.env) - base_dir = File.join(Rails.root, 'test', 'fixtures') + base_dir = ENV['FIXTURES_PATH'] ? File.join(Rails.root, ENV['FIXTURES_PATH']) : File.join(Rails.root, 'test', 'fixtures') fixtures_dir = ENV['FIXTURES_DIR'] ? File.join(base_dir, ENV['FIXTURES_DIR']) : base_dir (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/).map {|f| File.join(fixtures_dir, f) } : Dir.glob(File.join(fixtures_dir, '*.{yml,csv}'))).each do |fixture_file| @@ -194,7 +194,7 @@ namespace :db do end end - desc "Search for a fixture given a LABEL or ID." + desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." task :identify => :environment do require "active_record/fixtures" @@ -203,7 +203,8 @@ namespace :db do puts %Q(The fixture ID for "#{label}" is #{Fixtures.identify(label)}.) if label - Dir["#{RAILS_ROOT}/test/fixtures/**/*.yml"].each do |file| + base_dir = ENV['FIXTURES_PATH'] ? File.join(Rails.root, ENV['FIXTURES_PATH']) : File.join(Rails.root, 'test', 'fixtures') + Dir["#{base_dir}/**/*.yml"].each do |file| if data = YAML::load(ERB.new(IO.read(file)).result) data.keys.each do |key| key_id = Fixtures.identify(key) diff --git a/railties/test/gem_dependency_test.rb b/railties/test/gem_dependency_test.rb index b5946aa7b8..964ca50992 100644 --- a/railties/test/gem_dependency_test.rb +++ b/railties/test/gem_dependency_test.rb @@ -11,6 +11,7 @@ uses_mocha "Plugin Tests" do @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 end def test_configuration_adds_gem_dependency @@ -62,5 +63,13 @@ uses_mocha "Plugin Tests" do @gem_with_lib.add_load_paths @gem_with_lib.load end + + def test_gem_without_lib_loading + @gem_without_load.expects(:gem).with(@gem_without_load.name) + @gem_without_load.expects(:require).with(@gem_without_load.lib).never + @gem_without_load.add_load_paths + @gem_without_load.load + end + end end |