diff options
84 files changed, 857 insertions, 560 deletions
diff --git a/actionmailer/CHANGELOG b/actionmailer/CHANGELOG index 605b18d7e9..383ad2105d 100644 --- a/actionmailer/CHANGELOG +++ b/actionmailer/CHANGELOG @@ -1,3 +1,10 @@ +*Rails 3.0.0 [beta 3] (April 13th, 2010)* + +* Removed all quoting.rb type files from ActionMailer and put Mail 2.2.0 in instead [ML] + +* Lot of updates to various test cases that now work better with the new Mail and so have different expectations + + *Rails 3.0.0 [beta 2] (April 1st, 2010)* * Added interceptors and observers from Mail [ML] @@ -5,6 +12,16 @@ ActionMailer::Base.register_interceptor calls Mail.register_interceptor ActionMailer::Base.register_observer calls Mail.register_observer +* Mail::Part now no longer has nil as a default charset, it is always set to something, and defaults to UTF-8 + +* Added explict setting of charset in set_fields! method to make sure Mail has the user defined default + +* Removed quoting.rb and refactored for Mail to take responsibility of all quoting and auto encoding requirements for the header. + +* Fixed several tests which had incorrect encoding. + +* Changed all utf-8 to UTF-8 for consistency + * Whole new API added with tests. See base.rb for full details. Old API is deprecated. diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec index 410df0e106..01a886c5ee 100644 --- a/actionmailer/actionmailer.gemspec +++ b/actionmailer/actionmailer.gemspec @@ -20,6 +20,6 @@ Gem::Specification.new do |s| s.has_rdoc = true s.add_dependency('actionpack', version) - s.add_dependency('mail', '~> 2.1.5.3') + s.add_dependency('mail', '~> 2.2.0') s.add_dependency('text-format', '~> 1.0.0') end diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb index 43d73e71b9..46168d9a4a 100644 --- a/actionmailer/lib/action_mailer.rb +++ b/actionmailer/lib/action_mailer.rb @@ -46,7 +46,6 @@ module ActionMailer autoload :DeprecatedApi autoload :MailHelper autoload :OldApi - autoload :Quoting autoload :TestCase autoload :TestHelper end diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index af7255ae4f..d827ccdf2b 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -207,7 +207,7 @@ module ActionMailer #:nodoc: # scores instead of hyphens, so <tt>Content-Transfer-Encoding:</tt> # becomes <tt>:content_transfer_encoding</tt>. The defaults set by Action Mailer are: # * <tt>:mime_version => "1.0"</tt> - # * <tt>:charset => "utf-8",</tt> + # * <tt>:charset => "UTF-8",</tt> # * <tt>:content_type => "text/plain",</tt> # * <tt>:parts_order => [ "text/plain", "text/enriched", "text/html" ]</tt> # @@ -264,7 +264,7 @@ module ActionMailer #:nodoc: # (i.e. multiple parts are assembled from templates which specify the content type in their # filenames) this variable controls how the parts are ordered. class Base < AbstractController::Base - include DeliveryMethods, Quoting + include DeliveryMethods abstract! include AbstractController::Logger @@ -286,7 +286,7 @@ module ActionMailer #:nodoc: class_attribute :default_params self.default_params = { :mime_version => "1.0", - :charset => "utf-8", + :charset => "UTF-8", :content_type => "text/plain", :parts_order => [ "text/plain", "text/enriched", "text/html" ] }.freeze @@ -375,6 +375,11 @@ module ActionMailer #:nodoc: process(method_name, *args) if method_name end + def process(*args) #:nodoc: + lookup_context.skip_default_locale! + super + end + # Allows you to pass random and unusual headers to the new +Mail::Message+ object # which will add them to itself. # @@ -525,19 +530,25 @@ module ActionMailer #:nodoc: content_type = headers[:content_type] parts_order = headers[:parts_order] - # Merge defaults from class + # Handle defaults headers = headers.reverse_merge(self.class.default) - charset = headers.delete(:charset) - - # Quote fields headers[:subject] ||= default_i18n_subject - quote_fields!(headers, charset) + + # Apply charset at the beginning so all fields are properly quoted + m.charset = charset = headers[:charset] + + # Set configure delivery behavior + wrap_delivery_behavior!(headers.delete(:delivery_method)) + + # Assign all headers except parts_order, content_type and body + assignable = headers.except(:parts_order, :content_type, :body, :template_name, :template_path) + assignable.each { |k, v| m[k] = v } # Render the templates and blocks responses, explicit_order = collect_responses_and_parts_order(headers, &block) - create_parts_from_responses(m, responses, charset) + create_parts_from_responses(m, responses) - # Finally setup content type and parts order + # Setup content type, reapply charset and handle parts order m.content_type = set_content_type(m, content_type, headers[:content_type]) m.charset = charset @@ -547,12 +558,6 @@ module ActionMailer #:nodoc: m.body.sort_parts! end - # Set configure delivery behavior - wrap_delivery_behavior!(headers.delete(:delivery_method)) - - # Remove any missing configuration header and assign all others - headers.except!(:parts_order, :content_type) - headers.each { |k, v| m[k] = v } m end @@ -577,17 +582,6 @@ module ActionMailer #:nodoc: I18n.t(:subject, :scope => [:actionmailer, mailer_scope, action_name], :default => action_name.humanize) end - # TODO: Move this into Mail - def quote_fields!(headers, charset) #:nodoc: - m = @_message - m.subject ||= quote_if_necessary(headers.delete(:subject), charset) if headers[:subject] - m.to ||= quote_address_if_necessary(headers.delete(:to), charset) if headers[:to] - m.from ||= quote_address_if_necessary(headers.delete(:from), charset) if headers[:from] - m.cc ||= quote_address_if_necessary(headers.delete(:cc), charset) if headers[:cc] - m.bcc ||= quote_address_if_necessary(headers.delete(:bcc), charset) if headers[:bcc] - m.reply_to ||= quote_address_if_necessary(headers.delete(:reply_to), charset) if headers[:reply_to] - end - def collect_responses_and_parts_order(headers) #:nodoc: responses, parts_order = [], nil @@ -630,16 +624,16 @@ module ActionMailer #:nodoc: end end - def create_parts_from_responses(m, responses, charset) #:nodoc: + def create_parts_from_responses(m, responses) #:nodoc: if responses.size == 1 && !m.has_attachments? responses[0].each { |k,v| m[k] = v } elsif responses.size > 1 && m.has_attachments? container = Mail::Part.new container.content_type = "multipart/alternative" - responses.each { |r| insert_part(container, r, charset) } + responses.each { |r| insert_part(container, r, m.charset) } m.add_part(container) else - responses.each { |r| insert_part(m, r, charset) } + responses.each { |r| insert_part(m, r, m.charset) } end end diff --git a/actionmailer/lib/action_mailer/old_api.rb b/actionmailer/lib/action_mailer/old_api.rb index 7c59a8ae50..b2111209c7 100644 --- a/actionmailer/lib/action_mailer/old_api.rb +++ b/actionmailer/lib/action_mailer/old_api.rb @@ -143,12 +143,12 @@ module ActionMailer { :content_type => content_type, :content_disposition => content_disposition }.merge(params) end - + def create_mail m = @_message - quote_fields!({:subject => subject, :to => recipients, :from => from, - :bcc => bcc, :cc => cc, :reply_to => reply_to}, charset) + set_fields!({:subject => subject, :to => recipients, :from => from, + :bcc => bcc, :cc => cc, :reply_to => reply_to}, charset) m.mime_version = mime_version unless mime_version.nil? m.date = sent_on.to_time rescue sent_on if sent_on @@ -230,6 +230,17 @@ module ActionMailer ) end + def set_fields!(headers, charset) #:nodoc: + m = @_message + m.charset = charset + m.subject ||= headers.delete(:subject) if headers[:subject] + m.to ||= headers.delete(:to) if headers[:to] + m.from ||= headers.delete(:from) if headers[:from] + m.cc ||= headers.delete(:cc) if headers[:cc] + m.bcc ||= headers.delete(:bcc) if headers[:bcc] + m.reply_to ||= headers.delete(:reply_to) if headers[:reply_to] + end + def split_content_type(ct) ct.to_s.split("/") end diff --git a/actionmailer/lib/action_mailer/quoting.rb b/actionmailer/lib/action_mailer/quoting.rb deleted file mode 100644 index 2b55c0f0ab..0000000000 --- a/actionmailer/lib/action_mailer/quoting.rb +++ /dev/null @@ -1,64 +0,0 @@ -module ActionMailer - module Quoting #:nodoc: - # TODO extract this into Mail itself. - # - # - # Convert the given text into quoted printable format, with an instruction - # that the text be eventually interpreted in the given charset. - def quoted_printable(text, charset) - text = text.gsub( /[^a-z ]/i ) { quoted_printable_encode($&) }. - gsub( / /, "_" ) - "=?#{charset}?Q?#{text}?=" - end - - # Convert the given character to quoted printable format, taking into - # account multi-byte characters (if executing with $KCODE="u", for instance) - def quoted_printable_encode(character) - result = "" - character.each_byte { |b| result << "=%02X" % b } - result - end - - # A quick-and-dirty regexp for determining whether a string contains any - # characters that need escaping. - if !defined?(CHARS_NEEDING_QUOTING) - CHARS_NEEDING_QUOTING = Regexp.new('[\000-\011\013\014\016-\037\177-\377]', nil, 'n') - end - - # Quote the given text if it contains any "illegal" characters - def quote_if_necessary(text, charset) - text = text.dup.force_encoding(Encoding::ASCII_8BIT) if text.respond_to?(:force_encoding) - - (text =~ CHARS_NEEDING_QUOTING) ? - quoted_printable(text, charset) : - text - end - - # Quote any of the given strings if they contain any "illegal" characters - def quote_any_if_necessary(charset, *args) - args.map { |v| quote_if_necessary(v, charset) } - end - - # Quote the given address if it needs to be. The address may be a - # regular email address, or it can be a phrase followed by an address in - # brackets. The phrase is the only part that will be quoted, and only if - # it needs to be. This allows extended characters to be used in the - # "to", "from", "cc", "bcc" and "reply-to" headers. - def quote_address_if_necessary(address, charset) - if Array === address - address.map { |a| quote_address_if_necessary(a, charset) }.join(", ") - elsif address =~ /^(\S.*)\s+(<.*>)$/ - address = $2 - phrase = quote_if_necessary($1.gsub(/^['"](.*)['"]$/, '\1'), charset) - "\"#{phrase}\" #{address}" - else - address - end - end - - # Quote any of the given addresses, if they need to be. - def quote_any_address_if_necessary(charset, *args) - args.map { |v| quote_address_if_necessary(v, charset) } - end - end -end diff --git a/actionmailer/lib/action_mailer/test_case.rb b/actionmailer/lib/action_mailer/test_case.rb index 7c4033a125..d4874c6dbf 100644 --- a/actionmailer/lib/action_mailer/test_case.rb +++ b/actionmailer/lib/action_mailer/test_case.rb @@ -8,7 +8,7 @@ module ActionMailer end class TestCase < ActiveSupport::TestCase - include Quoting, TestHelper + include TestHelper setup :initialize_test_deliveries setup :set_expected_mail @@ -48,11 +48,11 @@ module ActionMailer private def charset - "utf-8" + "UTF-8" end def encode(subject) - quoted_printable(subject, charset) + Mail::Encodings.q_value_encode(subject, charset) end def read_fixture(action) diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index baeee542be..8e69073009 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -510,28 +510,28 @@ class BaseTest < ActiveSupport::TestCase # Rendering test "you can specify a different template for implicit render" do - mail = BaseMailer.implicit_different_template('implicit_multipart') + mail = BaseMailer.implicit_different_template('implicit_multipart').deliver assert_equal("HTML Implicit Multipart", mail.html_part.body.decoded) assert_equal("TEXT Implicit Multipart", mail.text_part.body.decoded) end test "you can specify a different template for explicit render" do - mail = BaseMailer.explicit_different_template('explicit_multipart_templates') + mail = BaseMailer.explicit_different_template('explicit_multipart_templates').deliver assert_equal("HTML Explicit Multipart Templates", mail.html_part.body.decoded) assert_equal("TEXT Explicit Multipart Templates", mail.text_part.body.decoded) end test "you can specify a different layout" do - mail = BaseMailer.different_layout('different_layout') + mail = BaseMailer.different_layout('different_layout').deliver assert_equal("HTML -- HTML", mail.html_part.body.decoded) assert_equal("PLAIN -- PLAIN", mail.text_part.body.decoded) end test "you can specify the template path for implicit lookup" do - mail = BaseMailer.welcome_from_another_path('another.path/base_mailer') + mail = BaseMailer.welcome_from_another_path('another.path/base_mailer').deliver assert_equal("Welcome from another path", mail.body.encoded) - mail = BaseMailer.welcome_from_another_path(['unknown/invalid', 'another.path/base_mailer']) + mail = BaseMailer.welcome_from_another_path(['unknown/invalid', 'another.path/base_mailer']).deliver assert_equal("Welcome from another path", mail.body.encoded) end diff --git a/actionmailer/test/old_base/mail_service_test.rb b/actionmailer/test/old_base/mail_service_test.rb index 9eb067554f..db2db59cc7 100644 --- a/actionmailer/test/old_base/mail_service_test.rb +++ b/actionmailer/test/old_base/mail_service_test.rb @@ -105,7 +105,7 @@ class TestMailer < ActionMailer::Base sent_on Time.local(2004, 12, 12) cc "Foo áëô îü <extended@example.net>" bcc "Foo áëô îü <extended@example.net>" - charset "utf-8" + charset "UTF-8" body "åœö blah" end @@ -131,7 +131,7 @@ class TestMailer < ActionMailer::Base recipients recipient subject "Foo áëô îü" from "test@example.com" - charset "utf-8" + charset "UTF-8" part "text/plain" do |p| p.body = "blah" @@ -316,18 +316,15 @@ class TestMailer < ActionMailer::Base end class ActionMailerTest < Test::Unit::TestCase - include ActionMailer::Quoting - def encode( text, charset="utf-8" ) - quoted_printable( text, charset ) + def encode( text, charset="UTF-8" ) + Mail::Encodings.q_value_encode( text, charset ) end - def new_mail( charset="utf-8" ) + def new_mail( charset="UTF-8" ) mail = Mail.new + mail.charset = charset mail.mime_version = "1.0" - if charset - mail.content_type ["text", "plain", { "charset" => charset }] - end mail end @@ -358,7 +355,7 @@ class ActionMailerTest < Test::Unit::TestCase assert_equal "multipart/mixed", created.mime_type assert_equal "multipart/alternative", created.parts[0].mime_type assert_equal "bar", created.parts[0].header['foo'].to_s - assert_nil created.parts[0].charset + assert_not_nil created.parts[0].charset assert_equal "text/plain", created.parts[0].parts[0].mime_type assert_equal "text/html", created.parts[0].parts[1].mime_type assert_equal "application/octet-stream", created.parts[1].mime_type @@ -570,7 +567,6 @@ class ActionMailerTest < Test::Unit::TestCase def test_iso_charset TestMailer.delivery_method = :test - expected = new_mail( "iso-8859-1" ) expected.to = @recipient expected.subject = encode "testing isø charsets", "iso-8859-1" @@ -671,14 +667,14 @@ class ActionMailerTest < Test::Unit::TestCase def test_unquote_quoted_printable_subject msg = <<EOF From: me@example.com -Subject: =?utf-8?Q?testing_testing_=D6=A4?= +Subject: =?UTF-8?Q?testing_testing_=D6=A4?= Content-Type: text/plain; charset=iso-8859-1 The body EOF mail = Mail.new(msg) assert_equal "testing testing \326\244", mail.subject - assert_equal "Subject: =?utf-8?Q?testing_testing_=D6=A4?=\r\n", mail[:subject].encoded + assert_equal "Subject: testing testing =?UTF-8?Q?_=D6=A4=?=\r\n", mail[:subject].encoded end def test_unquote_7bit_subject @@ -719,7 +715,7 @@ The=3Dbody EOF mail = Mail.new(msg) assert_equal "The=body", mail.body.to_s.strip - assert_equal "The=3Dbody", mail.body.encoded.strip + assert_equal "The=3Dbody=", mail.body.encoded.strip end def test_unquote_base64_body @@ -740,12 +736,12 @@ EOF @recipient = "Grytøyr <test@localhost>" expected = new_mail "iso-8859-1" - expected.to = quote_address_if_necessary @recipient, "iso-8859-1" + expected.to = @recipient expected.subject = "testing extended headers" expected.body = "Nothing to see here." - expected.from = quote_address_if_necessary "Grytøyr <stian1@example.net>", "iso-8859-1" - expected.cc = quote_address_if_necessary "Grytøyr <stian2@example.net>", "iso-8859-1" - expected.bcc = quote_address_if_necessary "Grytøyr <stian3@example.net>", "iso-8859-1" + expected.from = "Grytøyr <stian1@example.net>" + expected.cc = "Grytøyr <stian2@example.net>" + expected.bcc = "Grytøyr <stian3@example.net>" expected.date = Time.local 2004, 12, 12 created = nil @@ -774,13 +770,13 @@ EOF def test_utf8_body_is_not_quoted @recipient = "Foo áëô îü <extended@example.net>" - expected = new_mail "utf-8" - expected.to = quote_address_if_necessary @recipient, "utf-8" - expected.subject = "testing utf-8 body" + expected = new_mail "UTF-8" + expected.to = @recipient + expected.subject = "testing UTF-8 body" expected.body = "åœö blah" - expected.from = quote_address_if_necessary @recipient, "utf-8" - expected.cc = quote_address_if_necessary @recipient, "utf-8" - expected.bcc = quote_address_if_necessary @recipient, "utf-8" + expected.from = @recipient + expected.cc = @recipient + expected.bcc = @recipient expected.date = Time.local 2004, 12, 12 created = TestMailer.utf8_body @recipient @@ -789,18 +785,21 @@ EOF def test_multiple_utf8_recipients @recipient = ["\"Foo áëô îü\" <extended@example.net>", "\"Example Recipient\" <me@example.com>"] - expected = new_mail "utf-8" - expected.to = quote_address_if_necessary @recipient, "utf-8" - expected.subject = "testing utf-8 body" + expected = new_mail "UTF-8" + expected.to = @recipient + expected.subject = "testing UTF-8 body" expected.body = "åœö blah" - expected.from = quote_address_if_necessary @recipient.first, "utf-8" - expected.cc = quote_address_if_necessary @recipient, "utf-8" - expected.bcc = quote_address_if_necessary @recipient, "utf-8" + expected.from = @recipient.first + expected.cc = @recipient + expected.bcc = @recipient expected.date = Time.local 2004, 12, 12 created = TestMailer.utf8_body @recipient - assert_match(/\nFrom: =\?utf-8\?Q\?Foo_.*?\?= <extended@example.net>\r/, created.encoded) - assert_match(/\nTo: =\?utf-8\?Q\?Foo_.*?\?= <extended@example.net>, \r\n\tExample Recipient <me/, created.encoded) + from_regexp = Regexp.escape('From: Foo =?UTF-8?B?w6HDq8O0?= =?UTF-8?B?IMOuw7w=?=') + assert_match(/#{from_regexp}/m, created.encoded) + + to_regexp = Regexp.escape("To: =?UTF-8?B?Rm9vIMOhw6vDtCDDrsO8?= <extended@example.net>") + assert_match(/#{to_regexp}/m, created.encoded) end def test_receive_decodes_base64_encoded_mail @@ -864,12 +863,20 @@ EOF def test_multipart_with_utf8_subject mail = TestMailer.multipart_with_utf8_subject(@recipient) - assert_match(/\nSubject: =\?utf-8\?Q\?Foo_.*?\?=/, mail.encoded) + regex = Regexp.escape('Subject: Foo =?UTF-8?Q?=C3=A1=C3=AB=C3=B4=?= =?UTF-8?Q?_=C3=AE=C3=BC=?=') + assert_match(/#{regex}/, mail.encoded) + string = "Foo áëô îü" + string.force_encoding('UTF-8') if string.respond_to?(:force_encoding) + assert_match(string, mail.subject) end def test_implicitly_multipart_with_utf8 mail = TestMailer.implicitly_multipart_with_utf8 - assert_match(/\nSubject: =\?utf-8\?Q\?Foo_.*?\?=/, mail.encoded) + regex = Regexp.escape('Subject: Foo =?UTF-8?Q?=C3=A1=C3=AB=C3=B4=?= =?UTF-8?Q?_=C3=AE=C3=BC=?=') + assert_match(/#{regex}/, mail.encoded) + string = "Foo áëô îü" + string.force_encoding('UTF-8') if string.respond_to?(:force_encoding) + assert_match(string, mail.subject) end def test_explicitly_multipart_messages @@ -909,11 +916,11 @@ EOF assert_equal "1.0", mail.mime_version.to_s assert_equal "multipart/alternative", mail.mime_type assert_equal "text/plain", mail.parts[0].mime_type - assert_equal "utf-8", mail.parts[0].charset + assert_equal "UTF-8", mail.parts[0].charset assert_equal "text/html", mail.parts[1].mime_type - assert_equal "utf-8", mail.parts[1].charset + assert_equal "UTF-8", mail.parts[1].charset assert_equal "application/x-yaml", mail.parts[2].mime_type - assert_equal "utf-8", mail.parts[2].charset + assert_equal "UTF-8", mail.parts[2].charset end def test_implicitly_multipart_messages_with_custom_order @@ -1044,13 +1051,13 @@ EOF mail = FunkyPathMailer.multipart_with_template_path_with_dots(@recipient) assert_equal 2, mail.parts.length assert "text/plain", mail.parts[1].mime_type - assert "utf-8", mail.parts[1].charset + assert "UTF-8", mail.parts[1].charset end def test_custom_content_type_attributes mail = TestMailer.custom_content_type_attributes assert_match %r{format=flowed}, mail.content_type - assert_match %r{charset=utf-8}, mail.content_type + assert_match %r{charset=UTF-8}, mail.content_type end def test_return_path_with_create diff --git a/actionmailer/test/old_base/url_test.rb b/actionmailer/test/old_base/url_test.rb index 17b383cc2a..6895fb230e 100644 --- a/actionmailer/test/old_base/url_test.rb +++ b/actionmailer/test/old_base/url_test.rb @@ -29,13 +29,12 @@ class UrlTestMailer < ActionMailer::Base end class ActionMailerUrlTest < Test::Unit::TestCase - include ActionMailer::Quoting - def encode( text, charset="utf-8" ) + def encode( text, charset="UTF-8" ) quoted_printable( text, charset ) end - def new_mail( charset="utf-8" ) + def new_mail( charset="UTF-8" ) mail = Mail.new mail.mime_version = "1.0" if charset diff --git a/actionmailer/test/quoting_test.rb b/actionmailer/test/quoting_test.rb deleted file mode 100644 index 7640f4b086..0000000000 --- a/actionmailer/test/quoting_test.rb +++ /dev/null @@ -1,106 +0,0 @@ -# encoding: utf-8 -require 'abstract_unit' -require 'tempfile' - -class QuotingTest < Test::Unit::TestCase - # Move some tests from TMAIL here - def test_unquote_quoted_printable - a ="=?ISO-8859-1?Q?[166417]_Bekr=E6ftelse_fra_Rejsefeber?=" - b = Mail::Encodings.unquote_and_convert_to(a, 'utf-8') - assert_equal "[166417] Bekr\303\246ftelse fra Rejsefeber", b - end - - def test_unquote_base64 - a ="=?ISO-8859-1?B?WzE2NjQxN10gQmVrcuZmdGVsc2UgZnJhIFJlanNlZmViZXI=?=" - b = Mail::Encodings.unquote_and_convert_to(a, 'utf-8') - assert_equal "[166417] Bekr\303\246ftelse fra Rejsefeber", b - end - - def test_unquote_without_charset - a ="[166417]_Bekr=E6ftelse_fra_Rejsefeber" - b = Mail::Encodings.unquote_and_convert_to(a, 'utf-8') - assert_equal "[166417]_Bekr=E6ftelse_fra_Rejsefeber", b - end - - def test_unqoute_multiple - a ="=?utf-8?q?Re=3A_=5B12=5D_=23137=3A_Inkonsistente_verwendung_von_=22Hin?==?utf-8?b?enVmw7xnZW4i?=" - b = Mail::Encodings.unquote_and_convert_to(a, 'utf-8') - assert_equal "Re: [12] #137: Inkonsistente verwendung von \"Hinzuf\303\274gen\"", b - end - - def test_unqoute_in_the_middle - a ="Re: Photos =?ISO-8859-1?Q?Brosch=FCre_Rand?=" - b = Mail::Encodings.unquote_and_convert_to(a, 'utf-8') - assert_equal "Re: Photos Brosch\303\274re Rand", b - end - - def test_unqoute_iso - a ="=?ISO-8859-1?Q?Brosch=FCre_Rand?=" - b = Mail::Encodings.unquote_and_convert_to(a, 'iso-8859-1') - expected = "Brosch\374re Rand" - expected.force_encoding 'iso-8859-1' if expected.respond_to?(:force_encoding) - assert_equal expected, b - end - - def test_quote_multibyte_chars - original = "\303\246 \303\270 and \303\245" - original.force_encoding('ASCII-8BIT') if original.respond_to?(:force_encoding) - - result = execute_in_sandbox(<<-CODE) - $:.unshift(File.dirname(__FILE__) + "/../lib/") - if RUBY_VERSION < '1.9' - $KCODE = 'u' - end - require 'action_mailer/quoting' - include ActionMailer::Quoting - quoted_printable(#{original.inspect}, "UTF-8") - CODE - - unquoted = Mail::Encodings.unquote_and_convert_to(result, nil) - - unquoted.force_encoding(Encoding::ASCII_8BIT) if unquoted.respond_to?(:force_encoding) - original.force_encoding(Encoding::ASCII_8BIT) if original.respond_to?(:force_encoding) - - assert_equal unquoted, original - end - - - # test an email that has been created using \r\n newlines, instead of - # \n newlines. - def test_email_quoted_with_0d0a - mail = Mail.new(IO.read("#{File.dirname(__FILE__)}/fixtures/raw_email_quoted_with_0d0a")) - # CHANGED: subject returns an object now - # assert_match %r{Elapsed time}, mail.body - assert_match %r{Elapsed time}, mail.body.to_s - end - - def test_email_with_partially_quoted_subject - mail = Mail.new(IO.read("#{File.dirname(__FILE__)}/fixtures/raw_email_with_partially_quoted_subject")) - # CHANGED: subject returns an object now - # assert_equal "Re: Test: \"\346\274\242\345\255\227\" mid \"\346\274\242\345\255\227\" tail", mail.subject - assert_equal "Re: Test: \"\346\274\242\345\255\227\" mid \"\346\274\242\345\255\227\" tail", mail.subject - end - - private - # This whole thing *could* be much simpler, but I don't think Tempfile, - # popen and others exist on all platforms (like Windows). - def execute_in_sandbox(code) - test_name = "#{File.dirname(__FILE__)}/am-quoting-test.#{$$}.rb" - res_name = "#{File.dirname(__FILE__)}/am-quoting-test.#{$$}.out" - - File.open(test_name, "w+") do |file| - file.write(<<-CODE) - block = Proc.new do - #{code} - end - puts block.call - CODE - end - - system("ruby #{test_name} > #{res_name}") or raise "could not run test in sandbox" - File.read(res_name).chomp - ensure - File.delete(test_name) rescue nil - File.delete(res_name) rescue nil - end -end diff --git a/actionmailer/test/test_helper_test.rb b/actionmailer/test/test_helper_test.rb index 3a38a91c28..440fb868c8 100644 --- a/actionmailer/test/test_helper_test.rb +++ b/actionmailer/test/test_helper_test.rb @@ -34,11 +34,7 @@ class TestHelperMailerTest < ActionMailer::TestCase end def test_charset_is_utf_8 - assert_equal "utf-8", charset - end - - def test_encode - assert_equal "=?utf-8?Q?=0Aasdf=0A?=", encode("\nasdf\n") + assert_equal "UTF-8", charset end def test_assert_emails diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index 708683747b..388169d981 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,4 +1,11 @@ -*Rails 3.0.0 [beta 3] (pending)* +*Rails 3.0.0 [beta 4/release candidate] (unreleased)* + +* Changed translate helper so that it doesn’t mark every translation as safe HTML. Only keys with a "_html" suffix and keys named "html" are considered to be safe HTML. All other translations are left untouched. [Craig Davey] + + +*Rails 3.0.0 [beta 3] (April 13th, 2010)* + +* New option :as added to form_for allows to change the object name. The old <% form_for :client, @post %> becomes <% form_for @post, :as => :client %> [spastorino] * Removed verify method in controllers. [JV] It's now available as a plugin at http://github.com/rails/verification diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index 98c8c5fa67..d2db366140 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -24,8 +24,7 @@ module AbstractController end def locale=(value) - @i18n_config.locale = value - @lookup_context.update_details(:locale => @i18n_config.locale) + @lookup_context.locale = value end end diff --git a/actionpack/lib/action_controller/deprecated/base.rb b/actionpack/lib/action_controller/deprecated/base.rb index 5d9cfb153a..b817bf42bc 100644 --- a/actionpack/lib/action_controller/deprecated/base.rb +++ b/actionpack/lib/action_controller/deprecated/base.rb @@ -148,6 +148,7 @@ module ActionController deprecated_config_writer :session_store deprecated_config_writer :session_options + deprecated_config_accessor :perform_caching deprecated_config_accessor :relative_url_root, "relative_url_root is ineffective. Please stop using it" deprecated_config_accessor :assets_dir deprecated_config_accessor :javascripts_dir @@ -156,4 +157,4 @@ module ActionController delegate :consider_all_requests_local, :consider_all_requests_local=, :allow_concurrency, :allow_concurrency=, :to => :"self.class" end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 7035e360ec..53585740ce 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -55,6 +55,14 @@ module ActionDispatch path = args.first end + if @scope[:module] && options[:to] + if options[:to].to_s.include?("#") + options[:to] = "#{@scope[:module]}/#{options[:to]}" + elsif @scope[:controller].nil? + options[:to] = "#{@scope[:module]}##{options[:to]}" + end + end + path = normalize_path(path) if using_match_shorthand?(path, options) @@ -116,11 +124,16 @@ module ActionDispatch controller, action = to.split('#') { :controller => controller, :action => action } when Symbol - { :action => to.to_s }.merge(default_controller ? { :controller => default_controller } : {}) + { :action => to.to_s } else - default_controller ? { :controller => default_controller } : {} + {} end + defaults[:controller] ||= default_controller + + defaults.delete(:controller) if defaults[:controller].blank? + defaults.delete(:action) if defaults[:action].blank? + if defaults[:controller].blank? && segment_keys.exclude?("controller") raise ArgumentError, "missing :controller" end @@ -167,7 +180,11 @@ module ActionDispatch end def default_controller - @scope[:controller].to_s if @scope[:controller] + if @options[:controller] + @options[:controller].to_s + elsif @scope[:controller] + @scope[:controller].to_s + end end end diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb index 563d9ec319..8731ed0ef3 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb @@ -709,7 +709,7 @@ module ActionView # You can enable or disable the asset tag timestamps cache. # With the cache enabled, the asset tag helper methods will make fewer - # expense file system calls. However this prevents you from modifying + # expensive file system calls. However this prevents you from modifying # any asset files while the server is running. # # ActionView::Helpers::AssetTagHelper.cache_asset_timestamps = false diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index a3453cc47a..d3604925e8 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -8,90 +8,91 @@ require 'active_support/core_ext/object/blank' module ActionView module Helpers - # Form helpers are designed to make working with models much easier - # compared to using just standard HTML elements by providing a set of - # methods for creating forms based on your models. This helper generates - # the HTML for forms, providing a method for each sort of input - # (e.g., text, password, select, and so on). When the form is submitted - # (i.e., when the user hits the submit button or <tt>form.submit</tt> is - # called via JavaScript), the form inputs will be bundled into the - # <tt>params</tt> object and passed back to the controller. + # Form helpers are designed to make working with resources much easier + # compared to using vanilla HTML. # - # There are two types of form helpers: those that specifically work with - # model attributes and those that don't. This helper deals with those that - # work with model attributes; to see an example of form helpers that don't - # work with model attributes, check the ActionView::Helpers::FormTagHelper - # documentation. + # Forms for models are created with +form_for+. That method yields a form + # builder that knows the model the form is about. The form builder is thus + # able to generate default values for input fields that correspond to model + # attributes, and also convenient names, IDs, endpoints, etc. # - # The core method of this helper, form_for, gives you the ability to create - # a form for a model instance; for example, let's say that you have a model - # <tt>Person</tt> and want to create a new instance of it: + # Conventions in the generated field names allow controllers to receive form + # data nicely structured in +params+ with no effort on your side. # - # # Note: a @person variable will have been created in the controller. - # # For example: @person = Person.new - # <%= form_for @person do |f| %> - # <%= f.text_field :first_name %> - # <%= f.text_field :last_name %> - # <%= submit_tag 'Create' %> - # <% end %> + # For example, to create a new person you typically set up a new instance of + # +Person+ in the <tt>PeopleController#new</tt> action, <tt>@person</tt>, and + # pass it to +form_for+: # - # The HTML generated for this would be: + # <%= form_for @person do |f| %> + # <%= f.label :first_name %>: + # <%= f.text_field :first_name %><br /> # - # <form action="/persons/create" method="post"> - # <input id="person_first_name" name="person[first_name]" size="30" type="text" /> - # <input id="person_last_name" name="person[last_name]" size="30" type="text" /> - # <input name="commit" type="submit" value="Create" /> - # </form> + # <%= f.label :last_name %>: + # <%= f.text_field :last_name %><br /> # - # If you are using a partial for your form fields, you can use this shortcut: + # <%= f.submit %> + # <% end %> # - # <%= form_for :person, @person do |form| %> - # <%= render :partial => f %> - # <%= submit_tag 'Create' %> - # <% end %> + # The HTML generated for this would be (modulus formatting): # - # This example will render the <tt>people/_form</tt> partial, setting a - # local variable called <tt>form</tt> which references the yielded - # FormBuilder. The <tt>params</tt> object created when this form is - # submitted would look like: + # <form action="/people" class="new_person" id="new_person" method="post"> + # <div style="margin:0;padding:0;display:inline"> + # <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" /> + # </div> + # <label for="person_first_name">First name</label>: + # <input id="person_first_name" name="person[first_name]" size="30" type="text" /><br /> # - # {"action"=>"create", "controller"=>"persons", "person"=>{"first_name"=>"William", "last_name"=>"Smith"}} + # <label for="person_last_name">Last name</label>: + # <input id="person_last_name" name="person[last_name]" size="30" type="text" /><br /> # - # The params hash has a nested <tt>person</tt> value, which can therefore - # be accessed with <tt>params[:person]</tt> in the controller. If were - # editing/updating an instance (e.g., <tt>Person.find(1)</tt> rather than - # <tt>Person.new</tt> in the controller), the objects attribute values are - # filled into the form (e.g., the <tt>person_first_name</tt> field would - # have that person's first name in it). + # <input id="person_submit" name="commit" type="submit" value="Create Person" /> + # </form> # - # If the object name contains square brackets the id for the object will be - # inserted. For example: + # As you see, the HTML reflects knowledge about the resource in several spots, + # like the path the form should be submitted to, or the names of the input fields. # - # <%= text_field "person[]", "name" %> + # In particular, thanks to the conventions followed in the generated field names, the + # controller gets a nested hash <tt>params[:person]</tt> with the person attributes + # set in the form. That hash is ready to be passed to <tt>Person.create</tt>: # - # ...will generate the following ERb. + # if @person = Person.create(params[:person]) + # # success + # else + # # error handling + # end # - # <input type="text" id="person_<%= @person.id %>_name" name="person[<%= @person.id %>][name]" value="<%= @person.name %>" /> + # Interestingly, the exact same view code in the previous example can be used to edit + # a person. If <tt>@person</tt> is an existing record with name "John Smith" and ID 256, + # the code above as is would yield instead: # - # If the helper is being used to generate a repetitive sequence of similar - # form elements, for example in a partial used by - # <tt>render_collection_of_partials</tt>, the <tt>index</tt> option may - # come in handy. Example: + # <form action="/people/256" class="edit_person" id="edit_person_256" method="post"> + # <div style="margin:0;padding:0;display:inline"> + # <input name="_method" type="hidden" value="put" /> + # <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" /> + # </div> + # <label for="person_first_name">First name</label>: + # <input id="person_first_name" name="person[first_name]" size="30" type="text" value="John" /><br /> # - # <%= text_field "person", "name", "index" => 1 %> + # <label for="person_last_name">Last name</label>: + # <input id="person_last_name" name="person[last_name]" size="30" type="text" value="Smith" /><br /> # - # ...becomes... + # <input id="person_submit" name="commit" type="submit" value="Update Person" /> + # </form> # - # <input type="text" id="person_1_name" name="person[1][name]" value="<%= @person.name %>" /> + # Note that the endpoint, default values, and submit button label are tailored for <tt>@person</tt>. + # That works that way because the involved helpers know whether the resource is a new record or not, + # and generate HTML accordingly. # - # An <tt>index</tt> option may also be passed to <tt>form_for</tt> and - # <tt>fields_for</tt>. This automatically applies the <tt>index</tt> to - # all the nested fields. + # The controller would receive the form data again in <tt>params[:person]</tt>, ready to be + # passed to <tt>Person#update_attributes</tt>: # - # There are also methods for helping to build form tags in - # link:classes/ActionView/Helpers/FormOptionsHelper.html, - # link:classes/ActionView/Helpers/DateHelper.html, and - # link:classes/ActionView/Helpers/ActiveRecordHelper.html + # if @person.update_attributes(params[:person]) + # # success + # else + # # error handling + # end + # + # That's how you tipically work with resources. module FormHelper extend ActiveSupport::Concern @@ -129,9 +130,8 @@ module ActionView # Admin? : <%= f.check_box :admin %><br /> # <% end %> # - # There, the first argument is a symbol or string with the name of the - # object the form is about, and also the name of the instance variable - # the object is stored in. + # There, the argument is a symbol or string with the name of the + # object the form is about. # # The form builder acts as a regular form helper that somehow carries the # model. Thus, the idea is that @@ -142,26 +142,7 @@ module ActionView # # <%= text_field :person, :first_name %> # - # If the instance variable is not <tt>@person</tt> you can pass the actual - # record as the second argument: - # - # <%= form_for :person, person do |f| %> - # ... - # <% end %> - # - # In that case you can think - # - # <%= f.text_field :first_name %> - # - # gets expanded to - # - # <%= text_field :person, :first_name, :object => person %> - # - # You can even display error messages of the wrapped model this way: - # - # <%= f.error_messages %> - # - # In any of its variants, the rightmost argument to +form_for+ is an + # The rightmost argument to +form_for+ is an # optional hash of options: # # * <tt>:url</tt> - The URL the form is submitted to. It takes the same @@ -177,7 +158,7 @@ module ActionView # possible to use both the stand-alone FormHelper methods and methods # from FormTagHelper. For example: # - # <%= form_for :person, @person do |f| %> + # <%= form_for @person do |f| %> # First name: <%= f.text_field :first_name %> # Last name : <%= f.text_field :last_name %> # Biography : <%= text_area :person, :biography %> @@ -203,7 +184,7 @@ module ActionView # # is equivalent to something like: # - # <%= form_for :post, @post, :url => post_path(@post), :html => { :method => :put, :class => "edit_post", :id => "edit_post_45" } do |f| %> + # <%= form_for @post, :as => :post, :url => post_path(@post), :html => { :method => :put, :class => "edit_post", :id => "edit_post_45" } do |f| %> # ... # <% end %> # @@ -213,9 +194,9 @@ module ActionView # ... # <% end %> # - # expands to + # is equivalent to something like: # - # <%= form_for :post, Post.new, :url => posts_path, :html => { :class => "new_post", :id => "new_post" } do |f| %> + # <%= form_for @post, :as => :post, :url => post_path(@post), :html => { :class => "new_post", :id => "new_post" } do |f| %> # ... # <% end %> # @@ -225,6 +206,13 @@ module ActionView # ... # <% end %> # + # If you have an object that needs to be represented as a different + # parameter, like a Client that acts as a Person: + # + # <%= form_for(@post, :as => :client do |f| %> + # ... + # <% end %> + # # And for namespaced routes, like +admin_post_url+: # # <%= form_for([:admin, @post]) do |f| %> @@ -245,13 +233,13 @@ module ActionView # # Example: # - # <%= form_for(:post, @post, :remote => true, :html => { :id => 'create-post', :method => :put }) do |f| %> + # <%= form_for(@post, :remote => true) do |f| %> # ... # <% end %> # # The HTML generated for this would be: # - # <form action='http://www.example.com' id='create-post' method='post' data-remote='true'> + # <form action='http://www.example.com' method='post' data-remote='true'> # <div style='margin:0;padding:0;display:inline'> # <input name='_method' type='hidden' value='put' /> # </div> @@ -265,7 +253,7 @@ module ActionView # custom builder. For example, let's say you made a helper to # automatically add labels to form inputs. # - # <%= form_for :person, @person, :url => { :action => "create" }, :builder => LabellingFormBuilder do |f| %> + # <%= form_for @person, :url => { :action => "create" }, :builder => LabellingFormBuilder do |f| %> # <%= f.text_field :first_name %> # <%= f.text_field :last_name %> # <%= text_area :person, :biography %> @@ -539,10 +527,7 @@ module ActionView end builder = options[:builder] || ActionView::Base.default_form_builder - - with_output_buffer do - yield builder.new(object_name, object, self, options, block) - end + capture(builder.new(object_name, object, self, options, block), &block) end # Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object diff --git a/actionpack/lib/action_view/helpers/translation_helper.rb b/actionpack/lib/action_view/helpers/translation_helper.rb index 457944dbb6..89c1b4a275 100644 --- a/actionpack/lib/action_view/helpers/translation_helper.rb +++ b/actionpack/lib/action_view/helpers/translation_helper.rb @@ -3,17 +3,28 @@ require 'action_view/helpers/tag_helper' module ActionView module Helpers module TranslationHelper - # Delegates to I18n#translate but also performs two additional functions. First, it'll catch MissingTranslationData exceptions + # Delegates to I18n#translate but also performs three additional functions. First, it'll catch MissingTranslationData exceptions # and turn them into inline spans that contains the missing key, such that you can see in a view what is missing where. # # Second, it'll scope the key by the current partial if the key starts with a period. So if you call translate(".foo") from the # people/index.html.erb template, you'll actually be calling I18n.translate("people.index.foo"). This makes it less repetitive # to translate many keys within the same partials and gives you a simple framework for scoping them consistently. If you don't # prepend the key with a period, nothing is converted. + # + # Third, it’ll mark the translation as safe HTML if the key has the suffix "_html" or the last element of the key is the word + # "html". For example, calling translate("footer_html") or translate("footer.html") will return a safe HTML string that won’t + # be escaped by other HTML helper methods. This naming convention helps to identify translations that include HTML tags so that + # you know what kind of output to expect when you call translate in a template. + def translate(key, options = {}) options[:raise] = true translation = I18n.translate(scope_key_by_partial(key), options) - (translation.respond_to?(:join) ? translation.join : translation).html_safe + translation = (translation.respond_to?(:join) ? translation.join : translation) + if html_safe_translation_key? key + translation.html_safe + else + translation + end rescue I18n::MissingTranslationData => e keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope]) content_tag('span', keys.join(', '), :class => 'translation_missing') @@ -27,7 +38,7 @@ module ActionView alias :l :localize private - + def scope_key_by_partial(key) strkey = key.respond_to?(:join) ? key.join : key.to_s if strkey.first == "." @@ -40,6 +51,15 @@ module ActionView key end end + + def html_safe_translation_key?(key) + last_key = if key.is_a? Array + key.last + else + key.to_s.split('.').last + end + (last_key == "html") || (last_key.ends_with? "_html") + end end end end diff --git a/actionpack/lib/action_view/lookup_context.rb b/actionpack/lib/action_view/lookup_context.rb index 6e61d85dcc..7021342b4a 100644 --- a/actionpack/lib/action_view/lookup_context.rb +++ b/actionpack/lib/action_view/lookup_context.rb @@ -58,7 +58,7 @@ module ActionView def initialize(view_paths, details = {}) @details, @details_key = { :handlers => default_handlers }, nil - @frozen_formats = false + @frozen_formats, @skip_default_locale = false, false self.view_paths = view_paths self.update_details(details, true) end @@ -147,7 +147,13 @@ module ActionView super(value) end - # Overload locale to return a symbol instead of array + # Do not use the default locale on template lookup. + def skip_default_locale! + @skip_default_locale = true + self.locale = nil + end + + # Overload locale to return a symbol instead of array. def locale @details[:locale].first end @@ -160,7 +166,8 @@ module ActionView config = I18n.config.respond_to?(:i18n_config) ? I18n.config.i18n_config : I18n.config config.locale = value end - super(_locale_defaults) + + super(@skip_default_locale ? I18n.locale : _locale_defaults) end # Update the details keys by merging the given hash into the current diff --git a/actionpack/test/controller/layout_test.rb b/actionpack/test/controller/layout_test.rb index 4d687c1ec6..e1c1128753 100644 --- a/actionpack/test/controller/layout_test.rb +++ b/actionpack/test/controller/layout_test.rb @@ -209,7 +209,7 @@ class LayoutStatusIsRenderedTest < ActionController::TestCase end end -unless RUBY_PLATFORM =~ /(:?mswin|mingw|bccwin)/ +unless RUBY_PLATFORM =~ /mswin|mingw/ class LayoutSymlinkedTest < LayoutTest layout "symlinked/symlinked_layout" end diff --git a/actionpack/test/controller/new_base/content_type_test.rb b/actionpack/test/controller/new_base/content_type_test.rb index 0ff5552b08..700b71a7f3 100644 --- a/actionpack/test/controller/new_base/content_type_test.rb +++ b/actionpack/test/controller/new_base/content_type_test.rb @@ -23,7 +23,9 @@ module ContentType "content_type/implied/i_am_html_erb.html.erb" => "Hello world!", "content_type/implied/i_am_xml_erb.xml.erb" => "<xml>Hello world!</xml>", "content_type/implied/i_am_html_builder.html.builder" => "xml.p 'Hello'", - "content_type/implied/i_am_xml_builder.xml.builder" => "xml.awesome 'Hello'" + "content_type/implied/i_am_xml_builder.xml.builder" => "xml.awesome 'Hello'", + "content_type/implied/i_am_rjs_in_html.html.erb" => "<%= render 'i_am_rjs_partial' %>", + "content_type/implied/_i_am_rjs_partial.js.rjs" => "" )] def i_am_html_erb() end @@ -91,6 +93,12 @@ module ContentType assert_header "Content-Type", "application/xml; charset=utf-8" end + + test "sets Content-Type as text/html when rendering *.html.erb with a RJS partial" do + get "/content_type/implied/i_am_rjs_in_html" + + assert_header "Content-Type", "text/html; charset=utf-8" + end end class ExplicitCharsetTest < Rack::TestCase diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index d38c48bfd4..8940990712 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -52,6 +52,8 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest match 'global/:action' end + match "/local/:action", :controller => "local" + constraints(:ip => /192\.168\.1\.\d\d\d/) do get 'admin' => "queenbee#index" end @@ -140,7 +142,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest namespace :account do match 'shorthand' - match 'description', :to => "account#description", :as => "description" + match 'description', :to => "description", :as => "description" resource :subscription, :credit, :credit_card root :to => "account#index" @@ -195,6 +197,8 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest resource :me match '/' => 'mes#index' end + + match "whatever/:controller(/:action(/:id))" end end @@ -415,6 +419,13 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + def test_local + with_test_routes do + get '/local/dashboard' + assert_equal 'local#dashboard', @response.body + end + end + def test_projects with_test_routes do get '/projects' @@ -864,7 +875,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest with_test_routes do assert_equal '/account', account_root_path get '/account' - assert_equal 'account#index', @response.body + assert_equal 'account/account#index', @response.body end end @@ -970,6 +981,16 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + def test_url_generator_for_generic_route + with_test_routes do + get 'whatever/foo/bar' + assert_equal 'foo#bar', @response.body + + assert_equal 'http://www.example.com/whatever/foo/bar/1', + url_for(:controller => "foo", :action => "bar", :id => 1) + end + end + private def with_test_routes yield diff --git a/actionpack/test/fixtures/test/array_translation.erb b/actionpack/test/fixtures/test/array_translation.erb index 12c0763313..def3a1a0c1 100644 --- a/actionpack/test/fixtures/test/array_translation.erb +++ b/actionpack/test/fixtures/test/array_translation.erb @@ -1 +1 @@ -<%= t(['foo', 'bar']) %>
\ No newline at end of file +<%= t(['foo', 'bar', 'html']) %>
\ No newline at end of file diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb index 28a13b07be..47ac911540 100644 --- a/actionpack/test/template/form_helper_test.rb +++ b/actionpack/test/template/form_helper_test.rb @@ -1518,6 +1518,11 @@ class FormHelperTest < ActionView::TestCase assert_equal expected, output_buffer end + def test_fields_for_returns_block_result + output = fields_for(Post.new) { |f| "fields" } + assert_equal "fields", output + end + protected def comments_path(post) "/posts/#{post.id}/comments" diff --git a/actionpack/test/template/number_helper_test.rb b/actionpack/test/template/number_helper_test.rb index a21a1a68e4..14e81fc9dc 100644 --- a/actionpack/test/template/number_helper_test.rb +++ b/actionpack/test/template/number_helper_test.rb @@ -188,6 +188,7 @@ class NumberHelperTest < ActionView::TestCase assert_equal '10 KB', number_to_human_size(kilobytes(10.000), :precision => 4) assert_equal '1 TB', number_to_human_size(1234567890123, :precision => 1) assert_equal '500 MB', number_to_human_size(524288000, :precision=>3) + assert_equal '10 MB', number_to_human_size(9961472, :precision=>0) assert_equal '40 KB', number_to_human_size(41010, :precision => 1) assert_equal '40 KB', number_to_human_size(41100, :precision => 2) assert_equal '1.0 KB', number_to_human_size(kilobytes(1.0123), :precision => 2, :strip_insignificant_zeros => false) diff --git a/actionpack/test/template/translation_helper_test.rb b/actionpack/test/template/translation_helper_test.rb index 6782bf06d4..b382b5eb22 100644 --- a/actionpack/test/template/translation_helper_test.rb +++ b/actionpack/test/template/translation_helper_test.rb @@ -25,7 +25,7 @@ class TranslationHelperTest < ActiveSupport::TestCase def test_translation_of_an_array_with_html expected = '<a href="#">foo</a><a href="#">bar</a>' - I18n.expects(:translate).with(["foo", "bar"], :raise => true).returns(['<a href="#">foo</a>', '<a href="#">bar</a>']) + I18n.expects(:translate).with(["foo", "bar", "html"], :raise => true).returns(['<a href="#">foo</a>', '<a href="#">bar</a>']) @view = ActionView::Base.new(ActionController::Base.view_paths, {}) assert_equal expected, @view.render(:file => "test/array_translation") end @@ -47,4 +47,19 @@ class TranslationHelperTest < ActiveSupport::TestCase @view = ActionView::Base.new(ActionController::Base.view_paths, {}) assert_equal "foobar", @view.render(:file => "test/scoped_array_translation") end + + def test_translate_does_not_mark_plain_text_as_safe_html + I18n.expects(:translate).with("hello", :raise => true).returns("Hello World") + assert_equal false, translate("hello").html_safe? + end + + def test_translate_marks_translations_named_html_as_safe_html + I18n.expects(:translate).with("html", :raise => true).returns("<a>Hello World</a>") + assert translate("html").html_safe? + end + + def test_translate_marks_translations_with_a_html_suffix_as_safe_html + I18n.expects(:translate).with("hello_html", :raise => true).returns("<a>Hello World</a>") + assert translate("hello_html").html_safe? + end end diff --git a/activemodel/CHANGELOG b/activemodel/CHANGELOG index 8c458b6091..74aec3bfd3 100644 --- a/activemodel/CHANGELOG +++ b/activemodel/CHANGELOG @@ -1,3 +1,8 @@ +*Rails 3.0.0 [beta 3] (April 13th, 2010)* + +* No changes + + *Rails 3.0.0 [beta 2] (April 1st, 2010)* * #new_record? and #destroyed? were removed from ActiveModel::Lint. Use diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index e0625c3dbb..8ecef6574f 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,4 +1,4 @@ -*Rails 3.0.0 [Beta 2] (pending)* +*Rails 3.0.0 [beta 3] (April 13th, 2010)* * Add Relation extensions. [Pratik Naik] diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 20a8754b7c..d94cc03938 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1,5 +1,6 @@ -require 'active_support/core_ext/module/delegation' +require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/enumerable' +require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/object/blank' module ActiveRecord @@ -1707,9 +1708,9 @@ module ActiveRecord silence_warnings do self.parent.const_set(extension_module_name, Module.new(&block_extension)) end - Array(extensions).push("#{self.parent}::#{extension_module_name}".constantize) + Array.wrap(extensions).push("#{self.parent}::#{extension_module_name}".constantize) else - Array(extensions) + Array.wrap(extensions) end end diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index b808f8c306..0dfd966466 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -1,4 +1,5 @@ require 'set' +require 'active_support/core_ext/array/wrap' module ActiveRecord module Associations @@ -98,7 +99,7 @@ module ActiveRecord if @target.is_a?(Array) @target.to_ary else - Array(@target) + Array.wrap(@target) end end alias_method :to_a, :to_ary @@ -450,6 +451,16 @@ module ActiveRecord records end + def add_record_to_target_with_callbacks(record) + callback(:before_add, record) + yield(record) if block_given? + @target ||= [] unless loaded? + @target << record unless @reflection.options[:uniq] && @target.include?(record) + callback(:after_add, record) + set_inverse_instance(record, @owner) + record + end + private def create_record(attrs) attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash) @@ -474,16 +485,6 @@ module ActiveRecord end end - def add_record_to_target_with_callbacks(record) - callback(:before_add, record) - yield(record) if block_given? - @target ||= [] unless loaded? - @target << record unless @reflection.options[:uniq] && @target.include?(record) - callback(:after_add, record) - set_inverse_instance(record, @owner) - record - end - def remove_records(*records) records = flatten_deeper(records) records.each { |record| raise_on_type_mismatch(record) } diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 4fb1df3ab9..b9d0fe3abe 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/array/wrap' + module ActiveRecord module Associations # This is the root class of all association proxies: @@ -55,7 +57,7 @@ module ActiveRecord @owner, @reflection = owner, reflection @updated = false reflection.check_validity! - Array(reflection.options[:extend]).each { |ext| proxy_extend(ext) } + Array.wrap(reflection.options[:extend]).each { |ext| proxy_extend(ext) } reset end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index d544c48a4c..fd24dcddc3 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -3,6 +3,7 @@ require 'set' require 'active_support/benchmarkable' require 'active_support/dependencies' require 'active_support/time' +require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/class/attribute_accessors' require 'active_support/core_ext/class/delegating_attributes' require 'active_support/core_ext/class/inheritable_attributes' @@ -341,15 +342,15 @@ module ActiveRecord #:nodoc: # # If you are organising your models within modules you can add a prefix to the models within a namespace by defining # a singleton method in the parent module called table_name_prefix which returns your chosen prefix. - cattr_accessor :table_name_prefix, :instance_writer => false - @@table_name_prefix = "" + class_attribute :table_name_prefix, :instance_writer => false + self.table_name_prefix = "" ## # :singleton-method: # Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp", # "people_basecamp"). By default, the suffix is the empty string. - cattr_accessor :table_name_suffix, :instance_writer => false - @@table_name_suffix = "" + class_attribute :table_name_suffix, :instance_writer => false + self.table_name_suffix = "" ## # :singleton-method: @@ -1079,16 +1080,6 @@ module ActiveRecord #:nodoc: end end - # Nest the type name in the same module as this class. - # Bar is "MyApp::Business::Bar" relative to MyApp::Business::Foo - def type_name_with_module(type_name) - if store_full_sti_class - type_name - else - (/^::/ =~ type_name) ? type_name : "#{parent.name}::#{type_name}" - end - end - def construct_finder_arel(options = {}, scope = nil) relation = options.is_a?(Hash) ? unscoped.apply_finder_options(options) : unscoped.merge(options) relation = scope.merge(relation) if scope @@ -1284,11 +1275,14 @@ module ActiveRecord #:nodoc: with_scope(method_scoping, :overwrite, &block) end - def subclasses #:nodoc: + # Returns a list of all subclasses of this class, meaning all descendants. + def subclasses @@subclasses[self] ||= [] - @@subclasses[self] + extra = @@subclasses[self].inject([]) {|list, subclass| list + subclass.subclasses } + @@subclasses[self] + @@subclasses[self].inject([]) {|list, subclass| list + subclass.subclasses } end + public :subclasses + # Sets the default options for the model. The format of the # <tt>options</tt> argument is the same as in find. # @@ -1311,13 +1305,26 @@ module ActiveRecord #:nodoc: # Returns the class type of the record using the current module as a prefix. So descendants of # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass. def compute_type(type_name) - modularized_name = type_name_with_module(type_name) - silence_warnings do - begin - class_eval(modularized_name, __FILE__) - rescue NameError - class_eval(type_name, __FILE__) + if type_name.match(/^::/) + # If the type is prefixed with a scope operator then we assume that + # the type_name is an absolute reference. + type_name.constantize + else + # Build a list of candidates to search for + candidates = [] + name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" } + candidates << type_name + + candidates.each do |candidate| + begin + constant = candidate.constantize + return constant if candidate == constant.to_s + rescue NameError + rescue ArgumentError + end end + + raise NameError, "uninitialized constant #{candidates.first}" end end diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index add5d99ca6..98c14e6eb0 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/array/wrap' + module ActiveRecord # Callbacks are hooks into the lifecycle of an Active Record object that allow you to trigger logic # before or after an alteration of the object state. This can be used to make sure that associated and @@ -250,7 +252,7 @@ module ActiveRecord def before_validation(*args, &block) options = args.last if options.is_a?(Hash) && options[:on] - options[:if] = Array(options[:if]) + options[:if] = Array.wrap(options[:if]) options[:if] << "@_on_validate == :#{options[:on]}" end set_callback(:validation, :before, *args, &block) @@ -259,7 +261,7 @@ module ActiveRecord def after_validation(*args, &block) options = args.extract_options! options[:prepend] = true - options[:if] = Array(options[:if]) + options[:if] = Array.wrap(options[:if]) options[:if] << "!halted && value != false" options[:if] << "@_on_validate == :#{options[:on]}" if options[:on] set_callback(:validation, :after, *(args << options), &block) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index e5d100b51b..3c560903f7 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -32,7 +32,6 @@ module ActiveRecord # Enable the query cache within the block. def cache old, @query_cache_enabled = @query_cache_enabled, true - @query_cache ||= {} yield ensure clear_query_cache @@ -54,7 +53,7 @@ module ActiveRecord # the same SQL query and repeatedly return the same result each time, silently # undermining the randomness you were expecting. def clear_query_cache - @query_cache.clear if @query_cache + @query_cache.clear end def select_all_with_query_cache(*args) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 6d4ab501fa..e8cba1bd41 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/array/wrap' + module ActiveRecord module ConnectionAdapters # :nodoc: module SchemaStatements @@ -267,7 +269,7 @@ module ActiveRecord # generates # CREATE UNIQUE INDEX by_branch_party ON accounts(branch_id, party_id) def add_index(table_name, column_name, options = {}) - column_names = Array(column_name) + column_names = Array.wrap(column_name) index_name = index_name(table_name, :column => column_names) if Hash === options # legacy support, since this param was a string @@ -297,7 +299,7 @@ module ActiveRecord def index_name(table_name, options) #:nodoc: if Hash === options # legacy support if options[:column] - "index_#{table_name}_on_#{Array(options[:column]) * '_and_'}" + "index_#{table_name}_on_#{Array.wrap(options[:column]) * '_and_'}" elsif options[:name] options[:name] else diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 6ffffc8654..578297029b 100755 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -41,6 +41,7 @@ module ActiveRecord @connection, @logger = connection, logger @runtime = 0 @query_cache_enabled = false + @query_cache = {} end # Returns the human-readable name of the adapter. Use mixed case - one diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 68ee88bba4..ceb1adc9e0 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -54,6 +54,12 @@ module ActiveRecord super(name, self.class.extract_value_from_default(default), sql_type, null) end + # :stopdoc: + class << self + attr_accessor :money_precision + end + # :startdoc: + private def extract_limit(sql_type) case sql_type @@ -71,9 +77,11 @@ module ActiveRecord # Extracts the precision from PostgreSQL-specific data types. def extract_precision(sql_type) - # Actual code is defined dynamically in PostgreSQLAdapter.connect - # depending on the server specifics - super + if sql_type == 'money' + self.class.money_precision + else + super + end end # Maps PostgreSQL-specific data types to logical Rails types. @@ -83,18 +91,18 @@ module ActiveRecord when /^(?:real|double precision)$/ :float # Monetary types - when /^money$/ + when 'money' :decimal # Character types when /^(?:character varying|bpchar)(?:\(\d+\))?$/ :string # Binary data types - when /^bytea$/ + when 'bytea' :binary # Date/time types when /^timestamp with(?:out)? time zone$/ :datetime - when /^interval$/ + when 'interval' :string # Geometric types when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/ @@ -106,16 +114,16 @@ module ActiveRecord when /^bit(?: varying)?(?:\(\d+\))?$/ :string # XML type - when /^xml$/ + when 'xml' :xml # Arrays when /^\D+\[\]$/ :string # Object identifier types - when /^oid$/ + when 'oid' :integer # UUID type - when /^uuid$/ + when 'uuid' :string # Small and big integer types when /^(?:small|big)int$/ @@ -383,9 +391,9 @@ module ActiveRecord def quote(value, column = nil) #:nodoc: if value.kind_of?(String) && column && column.type == :binary "#{quoted_string_prefix}'#{escape_bytea(value)}'" - elsif value.kind_of?(String) && column && column.sql_type =~ /^xml$/ + elsif value.kind_of?(String) && column && column.sql_type == 'xml' "xml E'#{quote_string(value)}'" - elsif value.kind_of?(Numeric) && column && column.sql_type =~ /^money$/ + elsif value.kind_of?(Numeric) && column && column.sql_type == 'money' # Not truly string input, so doesn't require (or allow) escape string syntax. "'#{value.to_s}'" elsif value.kind_of?(String) && column && column.sql_type =~ /^bit/ @@ -662,8 +670,30 @@ module ActiveRecord def tables(name = nil) query(<<-SQL, name).map { |row| row[0] } SELECT tablename + FROM pg_tables + WHERE schemaname = ANY (current_schemas(false)) + SQL + end + + def table_exists?(name) + name = name.to_s + schema, table = name.split('.', 2) + + unless table # A table was provided without a schema + table = schema + schema = nil + end + + if name =~ /^"/ # Handle quoted table names + table = name + schema = nil + end + + query(<<-SQL).first[0].to_i > 0 + SELECT COUNT(*) FROM pg_tables - WHERE schemaname = ANY (current_schemas(false)) + WHERE tablename = '#{table.gsub(/(^"|"$)/,'')}' + #{schema ? "AND schemaname = '#{schema}'" : ''} SQL end @@ -925,7 +955,7 @@ module ActiveRecord # Construct a clean list of column names from the ORDER BY clause, removing # any ASC/DESC modifiers order_columns = order_by.split(',').collect { |s| s.split.first } - order_columns.delete_if &:blank? + order_columns.delete_if(&:blank?) order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" } # Return a DISTINCT ON() clause that's distinct on the columns we want but includes @@ -989,17 +1019,8 @@ module ActiveRecord # Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of # PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision # should know about this but can't detect it there, so deal with it here. - money_precision = (postgresql_version >= 80300) ? 19 : 10 - PostgreSQLColumn.module_eval(<<-end_eval) - def extract_precision(sql_type) # def extract_precision(sql_type) - if sql_type =~ /^money$/ # if sql_type =~ /^money$/ - #{money_precision} # 19 - else # else - super # super - end # end - end # end - end_eval - + PostgreSQLColumn.money_precision = + (postgresql_version >= 80300) ? 19 : 10 configure_connection end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index ee2d420194..70a460d41d 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -348,14 +348,24 @@ module ActiveRecord attributes_collection = attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes } end + association = send(association_name) + + existing_records = if association.loaded? + association.to_a + else + attribute_ids = attributes_collection.map {|a| a['id'] || a[:id] }.compact + attribute_ids.present? ? association.all(:conditions => {:id => attribute_ids}) : [] + end + attributes_collection.each do |attributes| attributes = attributes.with_indifferent_access if attributes['id'].blank? unless reject_new_record?(association_name, attributes) - send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS)) + association.build(attributes.except(*UNASSIGNABLE_KEYS)) end - elsif existing_record = send(association_name).detect { |record| record.id.to_s == attributes['id'].to_s } + elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s } + association.send(:add_record_to_target_with_callbacks, existing_record) unless association.loaded? assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) else raise_nested_attributes_record_not_found(association_name, attributes['id']) diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index b5e8b7570a..09332418d5 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1,3 +1,4 @@ +require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/object/blank' module ActiveRecord @@ -9,7 +10,7 @@ module ActiveRecord attr_accessor :"#{query_method}_values" next if [:where, :having].include?(query_method) - class_eval <<-CEVAL + class_eval <<-CEVAL, __FILE__ def #{query_method}(*args, &block) new_relation = clone new_relation.send(:apply_modules, Module.new(&block)) if block_given? @@ -21,12 +22,12 @@ module ActiveRecord end [:where, :having].each do |query_method| - class_eval <<-CEVAL + class_eval <<-CEVAL, __FILE__ def #{query_method}(*args, &block) new_relation = clone new_relation.send(:apply_modules, Module.new(&block)) if block_given? value = build_where(*args) - new_relation.#{query_method}_values += [*value] if value.present? + new_relation.#{query_method}_values += Array.wrap(value) if value.present? new_relation end CEVAL @@ -35,7 +36,7 @@ module ActiveRecord ActiveRecord::Relation::SINGLE_VALUE_METHODS.each do |query_method| attr_accessor :"#{query_method}_value" - class_eval <<-CEVAL + class_eval <<-CEVAL, __FILE__ def #{query_method}(value = true, &block) new_relation = clone new_relation.send(:apply_modules, Module.new(&block)) if block_given? diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index b19920741e..2e85959b1e 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -1,3 +1,4 @@ +require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/hash/conversions' module ActiveRecord #:nodoc: @@ -186,7 +187,7 @@ module ActiveRecord #:nodoc: end def serializable_method_attributes - Array(options[:methods]).inject([]) do |method_attributes, name| + Array.wrap(options[:methods]).inject([]) do |method_attributes, name| method_attributes << MethodAttribute.new(name.to_s, @serializable) if @serializable.respond_to?(name.to_s) method_attributes end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 4806fa0ecc..6283bdd0d6 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/array/wrap' + module ActiveRecord module Validations class UniquenessValidator < ActiveModel::EachValidator @@ -19,7 +21,7 @@ module ActiveRecord relation = table.where(sql, *params) - Array(options[:scope]).each do |scope_item| + Array.wrap(options[:scope]).each do |scope_item| scope_value = record.send(scope_item) relation = relation.where(scope_item => scope_value) end diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb index 3e72fbeca8..539c2517ee 100644 --- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb +++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb @@ -20,6 +20,11 @@ module ActiveRecord template 'model.rb', File.join('app/models', class_path, "#{file_name}.rb") end + def create_module_file + return if class_path.empty? + template 'module.rb', File.join('app/models', "#{class_path.join('/')}.rb") + end + hook_for :test_framework protected diff --git a/activerecord/lib/rails/generators/active_record/model/templates/module.rb b/activerecord/lib/rails/generators/active_record/model/templates/module.rb new file mode 100644 index 0000000000..bb4220f038 --- /dev/null +++ b/activerecord/lib/rails/generators/active_record/model/templates/module.rb @@ -0,0 +1,5 @@ +module <%= class_path.map(&:camelize).join('::') %> + def self.table_name_prefix + '<%= class_path.join('_') %>_' + end +end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 8774ed58aa..1c3655b587 100755 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -579,9 +579,9 @@ class BasicsTest < ActiveRecord::TestCase assert_equal(topics(:second).title, topics.first.title) end - def test_table_name_guesses - classes = [Category, Smarts, CreditCard, CreditCard::PinNumber, CreditCard::PinNumber::CvvCode, CreditCard::SubPinNumber, CreditCard::Brand, MasterCreditCard] + GUESSED_CLASSES = [Category, Smarts, CreditCard, CreditCard::PinNumber, CreditCard::PinNumber::CvvCode, CreditCard::SubPinNumber, CreditCard::Brand, MasterCreditCard] + def test_table_name_guesses assert_equal "topics", Topic.table_name assert_equal "categories", Category.table_name @@ -592,9 +592,13 @@ class BasicsTest < ActiveRecord::TestCase assert_equal "credit_card_pin_numbers", CreditCard::SubPinNumber.table_name assert_equal "categories", CreditCard::Brand.table_name assert_equal "master_credit_cards", MasterCreditCard.table_name + ensure + GUESSED_CLASSES.each(&:reset_table_name) + end + def test_singular_table_name_guesses ActiveRecord::Base.pluralize_table_names = false - classes.each(&:reset_table_name) + GUESSED_CLASSES.each(&:reset_table_name) assert_equal "category", Category.table_name assert_equal "smarts", Smarts.table_name @@ -604,10 +608,12 @@ class BasicsTest < ActiveRecord::TestCase assert_equal "credit_card_pin_number", CreditCard::SubPinNumber.table_name assert_equal "category", CreditCard::Brand.table_name assert_equal "master_credit_card", MasterCreditCard.table_name - + ensure ActiveRecord::Base.pluralize_table_names = true - classes.each(&:reset_table_name) + GUESSED_CLASSES.each(&:reset_table_name) + end + def test_table_name_guesses_with_prefixes_and_suffixes ActiveRecord::Base.table_name_prefix = "test_" Category.reset_table_name assert_equal "test_categories", Category.table_name @@ -620,8 +626,15 @@ class BasicsTest < ActiveRecord::TestCase ActiveRecord::Base.table_name_suffix = "" Category.reset_table_name assert_equal "categories", Category.table_name + ensure + ActiveRecord::Base.table_name_prefix = "" + ActiveRecord::Base.table_name_suffix = "" + GUESSED_CLASSES.each(&:reset_table_name) + end + def test_singular_table_name_guesses_with_prefixes_and_suffixes ActiveRecord::Base.pluralize_table_names = false + ActiveRecord::Base.table_name_prefix = "test_" Category.reset_table_name assert_equal "test_category", Category.table_name @@ -634,9 +647,40 @@ class BasicsTest < ActiveRecord::TestCase ActiveRecord::Base.table_name_suffix = "" Category.reset_table_name assert_equal "category", Category.table_name - + ensure ActiveRecord::Base.pluralize_table_names = true - classes.each(&:reset_table_name) + ActiveRecord::Base.table_name_prefix = "" + ActiveRecord::Base.table_name_suffix = "" + GUESSED_CLASSES.each(&:reset_table_name) + end + + def test_table_name_guesses_with_inherited_prefixes_and_suffixes + GUESSED_CLASSES.each(&:reset_table_name) + + CreditCard.table_name_prefix = "test_" + CreditCard.reset_table_name + Category.reset_table_name + assert_equal "test_credit_cards", CreditCard.table_name + assert_equal "categories", Category.table_name + CreditCard.table_name_suffix = "_test" + CreditCard.reset_table_name + Category.reset_table_name + assert_equal "test_credit_cards_test", CreditCard.table_name + assert_equal "categories", Category.table_name + CreditCard.table_name_prefix = "" + CreditCard.reset_table_name + Category.reset_table_name + assert_equal "credit_cards_test", CreditCard.table_name + assert_equal "categories", Category.table_name + CreditCard.table_name_suffix = "" + CreditCard.reset_table_name + Category.reset_table_name + assert_equal "credit_cards", CreditCard.table_name + assert_equal "categories", Category.table_name + ensure + CreditCard.table_name_prefix = "" + CreditCard.table_name_suffix = "" + GUESSED_CLASSES.each(&:reset_table_name) end def test_destroy_all @@ -2009,6 +2053,10 @@ class BasicsTest < ActiveRecord::TestCase assert !SubStiPost.descends_from_active_record? end + def test_base_subclasses_is_public_method + assert ActiveRecord::Base.public_methods.include?("subclasses") + end + def test_find_on_abstract_base_class_doesnt_use_type_condition old_class = LooseDescendant Object.send :remove_const, :LooseDescendant @@ -2157,14 +2205,6 @@ class BasicsTest < ActiveRecord::TestCase assert xml.include?(%(<arbitrary-element>#{value}</arbitrary-element>)) end - def test_type_name_with_module_should_handle_beginning - ActiveRecord::Base.store_full_sti_class = false - assert_equal 'ActiveRecord::Person', ActiveRecord::Base.send(:type_name_with_module, 'Person') - assert_equal '::Person', ActiveRecord::Base.send(:type_name_with_module, '::Person') - ensure - ActiveRecord::Base.store_full_sti_class = true - end - def test_to_param_should_return_string assert_kind_of String, Client.find(:first).to_param end diff --git a/activerecord/test/cases/datatype_test_postgresql.rb b/activerecord/test/cases/datatype_test_postgresql.rb index 9454b6e059..3c2d9fb7bd 100644 --- a/activerecord/test/cases/datatype_test_postgresql.rb +++ b/activerecord/test/cases/datatype_test_postgresql.rb @@ -97,7 +97,7 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase def test_money_values assert_equal 567.89, @first_money.wealth - assert_equal -567.89, @second_money.wealth + assert_equal(-567.89, @second_money.wealth) end def test_number_values diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb index 7209966bf8..c924c3dfad 100644 --- a/activerecord/test/cases/modules_test.rb +++ b/activerecord/test/cases/modules_test.rb @@ -1,8 +1,9 @@ require "cases/helper" require 'models/company_in_module' +require 'models/shop' class ModulesTest < ActiveRecord::TestCase - fixtures :accounts, :companies, :projects, :developers + fixtures :accounts, :companies, :projects, :developers, :collections, :products, :variants def setup # need to make sure Object::Firm and Object::Client are not defined, @@ -110,4 +111,34 @@ class ModulesTest < ActiveRecord::TestCase ActiveRecord::Base.table_name_prefix = '' classes.each(&:reset_table_name) end + + def test_compute_type_can_infer_class_name_of_sibling_inside_module + old = ActiveRecord::Base.store_full_sti_class + ActiveRecord::Base.store_full_sti_class = true + assert_equal MyApplication::Business::Firm, MyApplication::Business::Client.send(:compute_type, "Firm") + ensure + ActiveRecord::Base.store_full_sti_class = old + end + + def test_nested_models_should_not_raise_exception_when_using_delete_all_dependency_on_association + old = ActiveRecord::Base.store_full_sti_class + ActiveRecord::Base.store_full_sti_class = true + + collection = Shop::Collection.find(:first) + assert !collection.products.empty?, "Collection should have products" + assert_nothing_raised { collection.destroy } + ensure + ActiveRecord::Base.store_full_sti_class = old + end + + def test_nested_models_should_not_raise_exception_when_using_nullify_dependency_on_association + old = ActiveRecord::Base.store_full_sti_class + ActiveRecord::Base.store_full_sti_class = true + + product = Shop::Product.find(:first) + assert !product.variants.empty?, "Product should have variants" + assert_nothing_raised { product.destroy } + ensure + ActiveRecord::Base.store_full_sti_class = old + end end diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 7ca9c416cb..eae8ae7e39 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -453,6 +453,16 @@ module NestedAttributesOnACollectionAssociationTests assert_equal 'Privateers Greed', @pirate.send(@association_name).last.name end + def test_should_not_load_association_when_updating_existing_records + @pirate.reload + @pirate.send(association_setter, [{ :id => @child_1.id, :name => 'Grace OMalley' }]) + assert ! @pirate.send(@association_name).loaded? + + @pirate.save + assert ! @pirate.send(@association_name).loaded? + assert_equal 'Grace OMalley', @child_1.reload.name + end + def test_should_take_a_hash_with_composite_id_keys_and_assign_the_attributes_to_the_associated_models @child_1.stubs(:id).returns('ABC1X') @child_2.stubs(:id).returns('ABC2X') @@ -507,7 +517,7 @@ module NestedAttributesOnACollectionAssociationTests def test_should_ignore_new_associated_records_if_a_reject_if_proc_returns_false @alternate_params[association_getter]['baz'] = {} - assert_no_difference("@pirate.send(@association_name).length") do + assert_no_difference("@pirate.send(@association_name).count") do @pirate.attributes = @alternate_params end end diff --git a/activerecord/test/cases/schema_test_postgresql.rb b/activerecord/test/cases/schema_test_postgresql.rb index a294848fa3..3ed73786a7 100644 --- a/activerecord/test/cases/schema_test_postgresql.rb +++ b/activerecord/test/cases/schema_test_postgresql.rb @@ -52,6 +52,21 @@ class SchemaTest < ActiveRecord::TestCase @connection.execute "DROP SCHEMA #{SCHEMA_NAME} CASCADE" end + def test_table_exists? + [Thing1, Thing2, Thing3, Thing4].each do |klass| + name = klass.table_name + assert @connection.table_exists?(name), "'#{name}' table should exist" + end + end + + def test_table_exists_wrong_schema + assert(!@connection.table_exists?("foo.things"), "table should not exist") + end + + def test_table_exists_quoted_table + assert(@connection.table_exists?('"things.table"'), "table should exist") + end + def test_with_schema_prefixed_table_name assert_nothing_raised do assert_equal COLUMNS, columns("#{SCHEMA_NAME}.#{TABLE_NAME}") diff --git a/activerecord/test/fixtures/collections.yml b/activerecord/test/fixtures/collections.yml new file mode 100644 index 0000000000..ad0fd26554 --- /dev/null +++ b/activerecord/test/fixtures/collections.yml @@ -0,0 +1,3 @@ +collection_1: + id: 1 + name: Collection diff --git a/activerecord/test/fixtures/products.yml b/activerecord/test/fixtures/products.yml new file mode 100644 index 0000000000..8a197fb038 --- /dev/null +++ b/activerecord/test/fixtures/products.yml @@ -0,0 +1,4 @@ +product_1: + id: 1 + collection_id: 1 + name: Product diff --git a/activerecord/test/fixtures/variants.yml b/activerecord/test/fixtures/variants.yml new file mode 100644 index 0000000000..06be30727b --- /dev/null +++ b/activerecord/test/fixtures/variants.yml @@ -0,0 +1,4 @@ +variant_1: + id: 1 + product_id: 1 + name: Variant diff --git a/activerecord/test/models/shop.rb b/activerecord/test/models/shop.rb new file mode 100644 index 0000000000..b232185693 --- /dev/null +++ b/activerecord/test/models/shop.rb @@ -0,0 +1,12 @@ +module Shop + class Collection < ActiveRecord::Base + has_many :products, :dependent => :nullify + end + + class Product < ActiveRecord::Base + has_many :variants, :dependent => :delete_all + end + + class Variant < ActiveRecord::Base + end +end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index bec4291457..7a0cf550e0 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -99,6 +99,10 @@ ActiveRecord::Schema.define do t.string :name end + create_table :collections, :force => true do |t| + t.string :name + end + create_table :colnametests, :force => true do |t| t.integer :references, :null => false end @@ -394,6 +398,11 @@ ActiveRecord::Schema.define do t.integer :price end + create_table :products, :force => true do |t| + t.references :collection + t.string :name + end + create_table :projects, :force => true do |t| t.string :name t.string :type @@ -499,6 +508,11 @@ ActiveRecord::Schema.define do t.column :looter_type, :string end + create_table :variants, :force => true do |t| + t.references :product + t.string :name + end + create_table :vertices, :force => true do |t| t.column :label, :string end diff --git a/activeresource/CHANGELOG b/activeresource/CHANGELOG index 27bf99a8ee..91dccb9671 100644 --- a/activeresource/CHANGELOG +++ b/activeresource/CHANGELOG @@ -1,3 +1,8 @@ +*Rails 3.0.0 [beta 3] (April 13th, 2010)* + +* No changes + + *Rails 3.0.0 [beta 1] (February 4, 2010)* * Add support for errors in JSON format. #1956 [Fabien Jakimowicz] diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG index a5a7a9b904..da370303c6 100644 --- a/activesupport/CHANGELOG +++ b/activesupport/CHANGELOG @@ -1,4 +1,6 @@ -*Rails 3.0.0 [beta 3] (pending)* +*Rails 3.0.0 [beta 3] (April 13th, 2010)* + +* Improve transliteration quality. #4374 [Norman Clarke] * Speed up and add Ruby 1.9 support for ActiveSupport::Multibyte::Chars#tidy_bytes. #4350 [Norman Clarke] diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb index 725acad43a..d2bcd7a778 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/kernel/singleton_class' -require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/module/remove_method' class Class @@ -41,21 +40,31 @@ class Class def class_attribute(*attrs) instance_writer = !attrs.last.is_a?(Hash) || attrs.pop[:instance_writer] - s = singleton_class - attrs.each do |attr| - s.send(:define_method, attr) { } - s.send(:define_method, :"#{attr}?") { !!send(attr) } - s.send(:define_method, :"#{attr}=") do |value| - singleton_class.remove_possible_method(attr) - singleton_class.send(:define_method, attr) { value } - end + attrs.each do |name| + class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def self.#{name}() nil end + def self.#{name}?() !!#{name} end + + def self.#{name}=(val) + singleton_class.class_eval do + remove_possible_method(:#{name}) + define_method(:#{name}) { val } + end + end + + def #{name} + defined?(@#{name}) ? @#{name} : singleton_class.#{name} + end - define_method(attr) { self.singleton_class.send(attr) } - define_method(:"#{attr}?") { !!send(attr) } - define_method(:"#{attr}=") do |value| - singleton_class.remove_possible_method(attr) - singleton_class.send(:define_method, attr) { value } - end if instance_writer + def #{name}? + !!#{name} + end + RUBY + + if instance_writer + body = "def #{name}=(value) @#{name} = value end" + class_eval body, __FILE__, __LINE__ - 1 + end end end end diff --git a/activesupport/lib/active_support/core_ext/proc.rb b/activesupport/lib/active_support/core_ext/proc.rb index 71b413a88a..94bb5fb0cb 100644 --- a/activesupport/lib/active_support/core_ext/proc.rb +++ b/activesupport/lib/active_support/core_ext/proc.rb @@ -1,4 +1,4 @@ -require "active_support/core_ext/object" +require "active_support/core_ext/kernel/singleton_class" class Proc #:nodoc: def bind(object) diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index c7df62e915..b3dc5b2f3a 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -36,11 +36,13 @@ module ActiveSupport # "ActiveRecord".underscore # => "active_record" # "ActiveRecord::Errors".underscore # => active_record/errors def underscore(camel_cased_word) - camel_cased_word.to_s.gsub(/::/, '/'). - gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). - gsub(/([a-z\d])([A-Z])/,'\1_\2'). - tr("-", "_"). - downcase + word = camel_cased_word.to_s.dup + word.gsub!(/::/, '/') + word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2') + word.gsub!(/([a-z\d])([A-Z])/,'\1_\2') + word.tr!("-", "_") + word.downcase! + word end # Replaces underscores with dashes in the string. diff --git a/activesupport/lib/active_support/inflector/transliterate.rb b/activesupport/lib/active_support/inflector/transliterate.rb index ca591abc7d..9c99dcfb01 100644 --- a/activesupport/lib/active_support/inflector/transliterate.rb +++ b/activesupport/lib/active_support/inflector/transliterate.rb @@ -1,32 +1,47 @@ # encoding: utf-8 -require 'iconv' -require 'kconv' require 'active_support/core_ext/string/multibyte' module ActiveSupport module Inflector extend self - - # Replaces accented characters with their ascii equivalents. - def transliterate(string) - Iconv.iconv('ascii//ignore//translit', 'utf-8', string).to_s - end - if RUBY_VERSION >= '1.9' - undef_method :transliterate - def transliterate(string) - proxy = ActiveSupport::Multibyte.proxy_class.new(string) - proxy.normalize(:kd).gsub(/[^\x00-\x7F]+/, '') - end + # UTF-8 byte => ASCII approximate UTF-8 byte(s) + ASCII_APPROXIMATIONS = { + 198 => [65, 69], # Æ => AE + 208 => 68, # Ð => D + 216 => 79, # Ø => O + 222 => [84, 104], # Þ => Þ + 223 => [115, 115], # ß => ss + 230 => [97, 101], # æ => ae + 240 => 100, # ð => d + 248 => 111, # ø => o + 254 => [116, 104], # þ => th + 272 => 68, # Đ => D + 273 => 100, # đ => đ + 294 => 72, # Ħ => H + 295 => 104, # ħ => h + 305 => 105, # ı => i + 306 => [73, 74], # IJ =>IJ + 307 => [105, 106], # ij => ij + 312 => 107, # ĸ => k + 319 => 76, # Ŀ => L + 320 => 108, # ŀ => l + 321 => 76, # Ł => L + 322 => 108, # ł => l + 329 => 110, # ʼn => n + 330 => [78, 71], # Ŋ => NG + 331 => [110, 103], # ŋ => ng + 338 => [79, 69], # Œ => OE + 339 => [111, 101], # œ => oe + 358 => 84, # Ŧ => T + 359 => 116 # ŧ => t + } - # The iconv transliteration code doesn't function correctly - # on some platforms, but it's very fast where it does function. - elsif "foo" != (Inflector.transliterate("föö") rescue nil) - undef_method :transliterate - def transliterate(string) - string.mb_chars.normalize(:kd). # Decompose accented characters - gsub(/[^\x00-\x7F]+/, '') # Remove anything non-ASCII entirely (e.g. diacritics). - end + # Replaces accented characters with an ASCII approximation, or deletes it if none exsits. + def transliterate(string) + ActiveSupport::Multibyte::Chars.new(string).tidy_bytes.normalize(:d).unpack("U*").map do |char| + ASCII_APPROXIMATIONS[char] || (char if char < 128) + end.compact.flatten.pack("U*") end # Replaces special characters in a string so that it may be used as part of a 'pretty' URL. @@ -45,8 +60,6 @@ module ActiveSupport # <%= link_to(@person.name, person_path(@person)) %> # # => <a href="/person/1-donald-e-knuth">Donald E. Knuth</a> def parameterize(string, sep = '-') - # remove malformed utf8 characters - string = string.toutf8 unless string.is_utf8? # replace accented chars with their ascii equivalents parameterized_string = transliterate(string) # Turn unwanted chars into the separator @@ -59,6 +72,6 @@ module ActiveSupport parameterized_string.gsub!(/^#{re_sep}|#{re_sep}$/i, '') end parameterized_string.downcase - end + end end end diff --git a/activesupport/test/inflector_test_cases.rb b/activesupport/test/inflector_test_cases.rb index 29f87ac302..59515dad32 100644 --- a/activesupport/test/inflector_test_cases.rb +++ b/activesupport/test/inflector_test_cases.rb @@ -188,7 +188,10 @@ module InflectorTestCases StringToParameterizedAndNormalized = { "Malmö" => "malmo", "Garçons" => "garcons", - "Ops\331" => "ops" + "Ops\331" => "opsu", + "Ærøskøbing" => "aeroskobing", + "Aßlar" => "asslar", + "Japanese: 日本語" => "japanese" } UnderscoreToHuman = { diff --git a/activesupport/test/transliterate_test.rb b/activesupport/test/transliterate_test.rb new file mode 100644 index 0000000000..d689b6be73 --- /dev/null +++ b/activesupport/test/transliterate_test.rb @@ -0,0 +1,50 @@ +# encoding: utf-8 +require 'abstract_unit' +require 'active_support/inflector/transliterate' + +class TransliterateTest < Test::Unit::TestCase + + APPROXIMATIONS = { + "À"=>"A", "Á"=>"A", "Â"=>"A", "Ã"=>"A", "Ä"=>"A", "Å"=>"A", "Æ"=>"AE", + "Ç"=>"C", "È"=>"E", "É"=>"E", "Ê"=>"E", "Ë"=>"E", "Ì"=>"I", "Í"=>"I", + "Î"=>"I", "Ï"=>"I", "Ð"=>"D", "Ñ"=>"N", "Ò"=>"O", "Ó"=>"O", "Ô"=>"O", + "Õ"=>"O", "Ö"=>"O", "Ø"=>"O", "Ù"=>"U", "Ú"=>"U", "Û"=>"U", "Ü"=>"U", + "Ý"=>"Y", "Þ"=>"Th", "ß"=>"ss", "à"=>"a", "á"=>"a", "â"=>"a", "ã"=>"a", + "ä"=>"a", "å"=>"a", "æ"=>"ae", "ç"=>"c", "è"=>"e", "é"=>"e", "ê"=>"e", + "ë"=>"e", "ì"=>"i", "í"=>"i", "î"=>"i", "ï"=>"i", "ð"=>"d", "ñ"=>"n", + "ò"=>"o", "ó"=>"o", "ô"=>"o", "õ"=>"o", "ö"=>"o", "ø"=>"o", "ù"=>"u", + "ú"=>"u", "û"=>"u", "ü"=>"u", "ý"=>"y", "þ"=>"th", "ÿ"=>"y", "Ā"=>"A", + "ā"=>"a", "Ă"=>"A", "ă"=>"a", "Ą"=>"A", "ą"=>"a", "Ć"=>"C", "ć"=>"c", + "Ĉ"=>"C", "ĉ"=>"c", "Ċ"=>"C", "ċ"=>"c", "Č"=>"C", "č"=>"c", "Ď"=>"D", + "ď"=>"d", "Đ"=>"D", "đ"=>"d", "Ē"=>"E", "ē"=>"e", "Ĕ"=>"E", "ĕ"=>"e", + "Ė"=>"E", "ė"=>"e", "Ę"=>"E", "ę"=>"e", "Ě"=>"E", "ě"=>"e", "Ĝ"=>"G", + "ĝ"=>"g", "Ğ"=>"G", "ğ"=>"g", "Ġ"=>"G", "ġ"=>"g", "Ģ"=>"G", "ģ"=>"g", + "Ĥ"=>"H", "ĥ"=>"h", "Ħ"=>"H", "ħ"=>"h", "Ĩ"=>"I", "ĩ"=>"i", "Ī"=>"I", + "ī"=>"i", "Ĭ"=>"I", "ĭ"=>"i", "Į"=>"I", "į"=>"i", "İ"=>"I", "ı"=>"i", + "IJ"=>"IJ", "ij"=>"ij", "Ĵ"=>"J", "ĵ"=>"j", "Ķ"=>"K", "ķ"=>"k", "ĸ"=>"k", + "Ĺ"=>"L", "ĺ"=>"l", "Ļ"=>"L", "ļ"=>"l", "Ľ"=>"L", "ľ"=>"l", "Ŀ"=>"L", + "ŀ"=>"l", "Ł"=>"L", "ł"=>"l", "Ń"=>"N", "ń"=>"n", "Ņ"=>"N", "ņ"=>"n", + "Ň"=>"N", "ň"=>"n", "ʼn"=>"n", "Ŋ"=>"NG", "ŋ"=>"ng", "Ō"=>"O", "ō"=>"o", + "Ŏ"=>"O", "ŏ"=>"o", "Ő"=>"O", "ő"=>"o", "Œ"=>"OE", "œ"=>"oe", "Ŕ"=>"R", + "ŕ"=>"r", "Ŗ"=>"R", "ŗ"=>"r", "Ř"=>"R", "ř"=>"r", "Ś"=>"S", "ś"=>"s", + "Ŝ"=>"S", "ŝ"=>"s", "Ş"=>"S", "ş"=>"s", "Š"=>"S", "š"=>"s", "Ţ"=>"T", + "ţ"=>"t", "Ť"=>"T", "ť"=>"t", "Ŧ"=>"T", "ŧ"=>"t", "Ũ"=>"U", "ũ"=>"u", + "Ū"=>"U", "ū"=>"u", "Ŭ"=>"U", "ŭ"=>"u", "Ů"=>"U", "ů"=>"u", "Ű"=>"U", + "ű"=>"u", "Ų"=>"U", "ų"=>"u", "Ŵ"=>"W", "ŵ"=>"w", "Ŷ"=>"Y", "ŷ"=>"y", + "Ÿ"=>"Y", "Ź"=>"Z", "ź"=>"z", "Ż"=>"Z", "ż"=>"z", "Ž"=>"Z", "ž"=>"z" + } + + def test_transliterate_should_not_change_ascii_chars + (0..127).each do |byte| + char = [byte].pack("U") + assert_equal char, ActiveSupport::Inflector.transliterate(char) + end + end + + def test_should_convert_accented_chars_to_approximate_ascii_chars + APPROXIMATIONS.each do |given, expected| + assert_equal expected, ActiveSupport::Inflector.transliterate(given) + end + end + +end diff --git a/bin/rails b/bin/rails new file mode 100755 index 0000000000..4a74726e9b --- /dev/null +++ b/bin/rails @@ -0,0 +1,7 @@ +begin + require "rails/cli" +rescue LoadError + railties_path = File.expand_path('../../railties/lib', __FILE__) + $:.unshift(railties_path) + require "rails/cli" +end diff --git a/ci/site_config.rb b/ci/site_config.rb index 47b7e92120..ac7f658237 100644 --- a/ci/site_config.rb +++ b/ci/site_config.rb @@ -69,4 +69,4 @@ BuildReaper.number_of_builds_to_keep = 100 # any files that you'd like to override in cruise, keep in ~/.cruise, and copy over when this file is loaded like this site_css = CRUISE_DATA_ROOT + "/site.css" -FileUtils.cp site_css, RAILS_ROOT + "/public/stylesheets/site.css" if File.exists? site_css +FileUtils.cp site_css, Rails.root + "/public/stylesheets/site.css" if File.exists? site_css diff --git a/rails.gemspec b/rails.gemspec index 9c3204242f..3b1dfe9456 100644 --- a/rails.gemspec +++ b/rails.gemspec @@ -14,9 +14,10 @@ Gem::Specification.new do |s| s.email = 'david@loudthinking.com' s.homepage = 'http://www.rubyonrails.org' s.rubyforge_project = 'rails' - - s.files = [] - s.require_path = [] + + s.bindir = 'bin' + s.executables = ['rails'] + s.default_executable = 'rails' s.add_dependency('activesupport', version) s.add_dependency('actionpack', version) @@ -24,5 +25,5 @@ Gem::Specification.new do |s| s.add_dependency('activeresource', version) s.add_dependency('actionmailer', version) s.add_dependency('railties', version) - s.add_dependency('bundler', '>= 0.9.14') + s.add_dependency('bundler', '>= 0.9.19') end diff --git a/railties/CHANGELOG b/railties/CHANGELOG index 82684e4614..a326e29d27 100644 --- a/railties/CHANGELOG +++ b/railties/CHANGELOG @@ -1,5 +1,8 @@ +*Rails 3.0.0 [beta 3] (April 13th, 2010)* + * Renamed config.cookie_secret to config.secret_token and pass it as env key. [JV] + *Rails 3.0.0 [beta 2] (April 1st, 2010)* * Session store configuration has changed [YK & CL] diff --git a/railties/guides/source/initialization.textile b/railties/guides/source/initialization.textile index fea0185c4c..7df4f8f719 100644 --- a/railties/guides/source/initialization.textile +++ b/railties/guides/source/initialization.textile @@ -2642,7 +2642,7 @@ The method +find_with_root_flag+ is defined on +Rails::Engine+ (the superclass o root = File.exist?("#{root_path}/#{flag}") ? root_path : default raise "Could not find root path for #{self}" unless root - RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? + RUBY_PLATFORM =~ /mswin|mingw/ ? Pathname.new(root).expand_path : Pathname.new(root).realpath end </ruby> diff --git a/railties/guides/source/routing.textile b/railties/guides/source/routing.textile index 32b1e30502..7ac5bc8d3a 100644 --- a/railties/guides/source/routing.textile +++ b/railties/guides/source/routing.textile @@ -609,10 +609,10 @@ resources :photos, :path_names => { :new => 'make', :edit => 'change' } This would cause the routing to recognize URLs such as -<pre> +<plain> /photos/make /photos/1/change -</pre> +</plain> NOTE: The actual action names aren't changed by this option. The two URLs shown would still route to the new and edit actions. diff --git a/railties/bin/rails b/railties/lib/rails/cli.rb index 173f122445..173f122445 100755..100644 --- a/railties/bin/rails +++ b/railties/lib/rails/cli.rb diff --git a/railties/lib/rails/commands/runner.rb b/railties/lib/rails/commands/runner.rb index 1570b9ab0d..5634ee0f69 100644 --- a/railties/lib/rails/commands/runner.rb +++ b/railties/lib/rails/commands/runner.rb @@ -18,7 +18,7 @@ ARGV.clone.options do |opts| opts.on("-h", "--help", "Show this help message.") { $stderr.puts opts; exit } - if RUBY_PLATFORM !~ /mswin/ + if RUBY_PLATFORM !~ /mswin|mingw/ opts.separator "" opts.separator "You can also use runner as a shebang line for your scripts like this:" opts.separator "-------------------------------------------------------------" diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index c284840a38..0f33b40a13 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -119,7 +119,7 @@ module Rails root = File.exist?("#{root_path}/#{flag}") ? root_path : default raise "Could not find root path for #{self}" unless root - RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? + RUBY_PLATFORM =~ /mswin|mingw/ ? Pathname.new(root).expand_path : Pathname.new(root).realpath end end diff --git a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb index 0615a34a85..9b83207b3f 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb @@ -1,13 +1,14 @@ <%%= form_for(@<%= singular_name %>) do |f| %> <%% if @<%= singular_name %>.errors.any? %> - <div id="errorExplanation"> - <h2><%%= pluralize(@<%= singular_name %>.errors.count, "error") %> prohibited this <%= singular_name %> from being saved:</h2> - <ul> - <%% @<%= singular_name %>.errors.full_messages.each do |msg| %> - <li><%%= msg %></li> - <%% end %> - </ul> - </div> + <div id="error_explanation"> + <h2><%%= pluralize(@<%= singular_name %>.errors.count, "error") %> prohibited this <%= singular_name %> from being saved:</h2> + + <ul> + <%% @<%= singular_name %>.errors.full_messages.each do |msg| %> + <li><%%= msg %></li> + <%% end %> + </ul> + </div> <%% end %> <% for attribute in attributes -%> diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index bb2a080286..6818fafbe9 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -265,7 +265,7 @@ module Rails::Generators "/opt/local/var/run/mysql4/mysqld.sock", # mac + darwinports + mysql4 "/opt/local/var/run/mysql5/mysqld.sock", # mac + darwinports + mysql5 "/opt/lampp/var/mysql/mysql.sock" # xampp for linux - ].find { |f| File.exist?(f) } unless RUBY_PLATFORM =~ /(:?mswin|mingw)/ + ].find { |f| File.exist?(f) } unless RUBY_PLATFORM =~ /mswin|mingw/ end def empty_directory_with_gitkeep(destination, config = {}) diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index 0dd10f3f2d..f751c4519d 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -2,8 +2,10 @@ source 'http://rubygems.org' <%- if options.dev? -%> gem 'rails', :path => '<%= Rails::Generators::RAILS_DEV_PATH %>' +gem 'arel', :git => 'git://github.com/rails/arel.git' <%- elsif options.edge? -%> gem 'rails', :git => 'git://github.com/rails/rails.git' +gem 'arel', :git => 'git://github.com/rails/arel.git' <%- else -%> gem 'rails', '<%= Rails::VERSION::STRING %>' diff --git a/railties/lib/rails/generators/rails/app/templates/config/boot.rb b/railties/lib/rails/generators/rails/app/templates/config/boot.rb index 3971a07012..62a8ccc273 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/boot.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/boot.rb @@ -1,14 +1,9 @@ -# Use locked gems if present. -begin - require File.expand_path('../../.bundle/environment', __FILE__) +require 'rubygems' -rescue LoadError - # Otherwise, use RubyGems. - require 'rubygems' - - # And set up the gems listed in the Gemfile. - if File.exist?(File.expand_path('../../Gemfile', __FILE__)) - require 'bundler' - Bundler.setup - end -end +# Set up gems listed in the Gemfile. +gemfile = File.expand_path('../../Gemfile', __FILE__) +if File.exist?(gemfile) + ENV['BUNDLE_GEMFILE'] = gemfile + require 'bundler' + Bundler.setup +end
\ No newline at end of file diff --git a/railties/lib/rails/generators/rails/model/USAGE b/railties/lib/rails/generators/rails/model/USAGE index d97e9ac103..db98a2dd1b 100644 --- a/railties/lib/rails/generators/rails/model/USAGE +++ b/railties/lib/rails/generators/rails/model/USAGE @@ -15,6 +15,10 @@ Description: Finally, if --parent option is given, it's used as superclass of the created model. This allows you create Single Table Inheritance models. + If you pass a namespaced model name (e.g. admin/account or Admin::Account) + then the generator will create a module with a table_name_prefix method + to prefix the model's table name with the module name (e.g. admin_account) + Examples: `rails generate model account` @@ -28,3 +32,14 @@ Examples: `rails generate model post title:string body:text published:boolean` Creates a Post model with a string title, text body, and published flag. + + `rails generate model admin/account` + + For ActiveRecord and TestUnit it creates: + + Module: app/models/admin.rb + Model: app/models/admin/account.rb + Test: test/unit/admin/account_test.rb + Fixtures: test/fixtures/admin_accounts.yml + Migration: db/migrate/XXX_add_admin_accounts.rb + diff --git a/railties/lib/rails/generators/rails/stylesheets/templates/scaffold.css b/railties/lib/rails/generators/rails/stylesheets/templates/scaffold.css index f3f46d8b98..9f2056a702 100644 --- a/railties/lib/rails/generators/rails/stylesheets/templates/scaffold.css +++ b/railties/lib/rails/generators/rails/stylesheets/templates/scaffold.css @@ -34,7 +34,7 @@ div.field, div.actions { display: table; } -#errorExplanation { +#error_explanation { width: 450px; border: 2px solid red; padding: 7px; @@ -43,7 +43,7 @@ div.field, div.actions { background-color: #f0f0f0; } -#errorExplanation h2 { +#error_explanation h2 { text-align: left; font-weight: bold; padding: 5px 5px 5px 15px; @@ -54,7 +54,7 @@ div.field, div.actions { color: #fff; } -#errorExplanation ul li { +#error_explanation ul li { font-size: 12px; list-style: square; } diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake index 23b8f92abd..83f25506cb 100644 --- a/railties/lib/rails/test_unit/testing.rake +++ b/railties/lib/rails/test_unit/testing.rake @@ -30,7 +30,7 @@ end module Kernel def silence_stderr old_stderr = STDERR.dup - STDERR.reopen(RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'NUL:' : '/dev/null') + STDERR.reopen(RUBY_PLATFORM =~ /mswin|mingw/ ? 'NUL:' : '/dev/null') STDERR.sync = true yield ensure diff --git a/railties/railties.gemspec b/railties/railties.gemspec index aea07efe96..b9278c0399 100644 --- a/railties/railties.gemspec +++ b/railties/railties.gemspec @@ -15,9 +15,6 @@ Gem::Specification.new do |s| s.files = Dir['CHANGELOG', 'README', 'bin/**/*', 'guides/**/*', 'lib/**/{*,.[a-z]*}'] s.require_path = 'lib' - s.bindir = 'bin' - s.executables = ['rails'] - s.default_executable = 'rails' s.rdoc_options << '--exclude' << '.' s.has_rdoc = false diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 8bf0f09d6b..0f3bc1a46a 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -285,5 +285,41 @@ module ApplicationTests get "/" assert last_response.body =~ /csrf\-param/ end + + test "config.action_controller.perform_caching = true" do + make_basic_app do |app| + app.config.action_controller.perform_caching = true + end + + class ::OmgController < ActionController::Base + caches_action :index + def index + render :text => rand(1000) + end + end + + get "/" + res = last_response.body + get "/" + assert_equal res, last_response.body # value should be unchanged + end + + test "config.action_controller.perform_caching = false" do + make_basic_app do |app| + app.config.action_controller.perform_caching = false + end + + class ::OmgController < ActionController::Base + caches_action :index + def index + render :text => rand(1000) + end + end + + get "/" + res = last_response.body + get "/" + assert_not_equal res, last_response.body + end end end diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb index f5cfd8eeca..ef415a4fed 100644 --- a/railties/test/generators/model_generator_test.rb +++ b/railties/test/generators/model_generator_test.rb @@ -27,6 +27,14 @@ class ModelGeneratorTest < Rails::Generators::TestCase assert_file "app/models/account.rb", /class Account < Admin::Account/ end + def test_model_with_namespace + run_generator ["admin/account"] + assert_file "app/models/admin.rb", /module Admin/ + assert_file "app/models/admin.rb", /def self\.table_name_prefix/ + assert_file "app/models/admin.rb", /'admin_'/ + assert_file "app/models/admin/account.rb", /class Admin::Account < ActiveRecord::Base/ + end + def test_migration run_generator assert_migration "db/migrate/create_accounts.rb", /class CreateAccounts < ActiveRecord::Migration/ diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index e6896a1629..f0c64b92ba 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -213,7 +213,7 @@ Module.new do require_environment = "-r #{environment}" end - `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/railties/bin/rails #{tmp_path('app_template')}` + `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/bin/rails #{tmp_path('app_template')}` File.open("#{tmp_path}/app_template/config/boot.rb", 'w') do |f| if require_environment f.puts "Dir.chdir('#{File.dirname(environment)}') do" diff --git a/railties/test/railties/shared_tests.rb b/railties/test/railties/shared_tests.rb index 83d25be5db..20328d402d 100644 --- a/railties/test/railties/shared_tests.rb +++ b/railties/test/railties/shared_tests.rb @@ -264,7 +264,7 @@ YAML Rails.application.routes.draw do namespace :admin do namespace :foo do - match "bar", :to => "admin/foo/bar#index" + match "bar", :to => "bar#index" end end end diff --git a/release.rb b/release.rb new file mode 100644 index 0000000000..2076515f0e --- /dev/null +++ b/release.rb @@ -0,0 +1,11 @@ +version = ARGV.pop + +%w( activesupport activemodel activerecord activeresource actionpack actionmailer railties ).each do |framework| + puts "Building and pushing #{framework}..." + `cd #{framework} && gem build #{framework}.gemspec && gem push #{framework}-#{version}.gem && rm #{framework}-#{version}.gem` +end + +puts "Building and pushing Rails..." +`gem build rails.gemspec` +`gem push rails-#{version}.gem` +`rm rails-#{version}.gem`
\ No newline at end of file |