diff options
77 files changed, 721 insertions, 646 deletions
@@ -29,10 +29,10 @@ gem "text-format", "~> 1.0.0" # AR if mri || RUBY_ENGINE == "rbx" - gem "sqlite3-ruby", "= 1.3.0.beta.2", :require => 'sqlite3' + gem "sqlite3-ruby", "~> 1.3.0", :require => 'sqlite3' group :db do - gem "pg", ">= 0.9.0" + # gem "pg", ">= 0.9.0" gem "mysql", ">= 2.8.1" end elsif RUBY_ENGINE == "jruby" @@ -46,6 +46,7 @@ end # AP gem "RedCloth", ">= 4.2.2" +gem "bluecloth", ">= 2.0.7" group :documentation do gem 'rdoc', '2.1' diff --git a/RAILS_VERSION b/RAILS_VERSION index d989233c8c..c3eddb1401 100644 --- a/RAILS_VERSION +++ b/RAILS_VERSION @@ -1 +1 @@ -3.0.0.beta3 +3.0.0.beta4 diff --git a/actionmailer/CHANGELOG b/actionmailer/CHANGELOG index 7a17ff1df9..6e2c441e53 100644 --- a/actionmailer/CHANGELOG +++ b/actionmailer/CHANGELOG @@ -1,4 +1,4 @@ -*Rails 3.0.0 [beta 4/release candidate] (unreleased)* +*Rails 3.0.0 [beta 4] (June 8th, 2010)* * Changed encoding behaviour of mail, so updated tests in actionmailer and bumped mail version to 2.2.1 [ML] diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec index 4706b63b79..2b7c21b3f2 100644 --- a/actionmailer/actionmailer.gemspec +++ b/actionmailer/actionmailer.gemspec @@ -20,5 +20,5 @@ Gem::Specification.new do |s| s.has_rdoc = true s.add_dependency('actionpack', version) - s.add_dependency('mail', '~> 2.2.1') + s.add_dependency('mail', '~> 2.2.3') end diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 3a82979d35..7da033b6af 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -36,6 +36,9 @@ module ActionMailer #:nodoc: # * <tt>attachments[]=</tt> - Allows you to add attachments to your email in an intuitive # manner; <tt>attachments['filename.png'] = File.read('path/to/filename.png')</tt> # + # * <tt>attachments.inline[]=</tt> - Allows you to add an inline attachment to your email + # in the same manner as <tt>attachments[]=</tt> + # # * <tt>headers[]=</tt> - Allows you to specify any header field in your email such # as <tt>headers['X-No-Spam'] = 'True'</tt>. Note, while most fields (like <tt>To:</tt> # <tt>From:</tt> can only appear once in an email header, other fields like <tt>X-Anything</tt> @@ -173,7 +176,7 @@ module ActionMailer #:nodoc: # # class ApplicationMailer < ActionMailer::Base # def welcome(recipient) - # attachments['free_book.pdf'] = { :data => File.read('path/to/file.pdf') } + # attachments['free_book.pdf'] = File.read('path/to/file.pdf') # mail(:to => recipient, :subject => "New account information") # end # end @@ -184,6 +187,34 @@ module ActionMailer #:nodoc: # and the second being a <tt>application/pdf</tt> with a Base64 encoded copy of the file.pdf book # with the filename +free_book.pdf+. # + # = Inline Attachments + # + # You can also specify that a file should be displayed inline with other HTML. For example a + # corporate logo or a photo or the like. + # + # To do this is simple, in the Mailer: + # + # class ApplicationMailer < ActionMailer::Base + # def welcome(recipient) + # attachments.inline['photo.png'] = File.read('path/to/photo.png') + # mail(:to => recipient, :subject => "Here is what we look like") + # end + # end + # + # And then to reference the image in the view, you create a <tt>welcome.html.erb</tt> file and + # make a call to +image_tag+ passing in the attachment you want to display and then call + # +url+ on the attachment to get the relative content id path for the image source: + # + # <h1>Please Don't Cringe</h1> + # + # <%= image_tag attachments['photo.png'].url -%> + # + # As we are using ActionView's +image_tag+ method, you can pass in any other options you want: + # + # <h1>Please Don't Cringe</h1> + # + # <%= image_tag attachments['photo.png'].url, :alt => 'Our Photo', :class => 'photo' -%> + # # = Observing and Intercepting Mails # # Action Mailer provides hooks into the Mail observer and interceptor methods. These allow you to @@ -612,7 +643,11 @@ module ActionMailer #:nodoc: when user_content_type.present? user_content_type when m.has_attachments? - ["multipart", "mixed", params] + if m.attachments.detect { |a| a.inline? } + ["multipart", "related", params] + else + ["multipart", "mixed", params] + end when m.multipart? ["multipart", "alternative", params] else diff --git a/actionmailer/lib/action_mailer/mail_helper.rb b/actionmailer/lib/action_mailer/mail_helper.rb index aab6e12387..b708881edf 100644 --- a/actionmailer/lib/action_mailer/mail_helper.rb +++ b/actionmailer/lib/action_mailer/mail_helper.rb @@ -32,5 +32,10 @@ module ActionMailer def message @_message end + + # Access the message attachments list. + def attachments + @_message.attachments + end end end diff --git a/actionmailer/lib/action_mailer/version.rb b/actionmailer/lib/action_mailer/version.rb index e66172c8d3..8c19082f61 100644 --- a/actionmailer/lib/action_mailer/version.rb +++ b/actionmailer/lib/action_mailer/version.rb @@ -3,7 +3,7 @@ module ActionMailer MAJOR = 3 MINOR = 0 TINY = 0 - BUILD = "beta3" + BUILD = "beta4" STRING = [MAJOR, MINOR, TINY, BUILD].join('.') end diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index 54bf3de6af..b9226196fd 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -33,6 +33,11 @@ class BaseTest < ActiveSupport::TestCase mail(hash) end + def inline_attachment + attachments.inline['logo.png'] = "\312\213\254\232" + mail + end + def attachment_with_content(hash = {}) attachments['invoice.pdf'] = 'This is test File content' mail(hash) @@ -264,6 +269,18 @@ class BaseTest < ActiveSupport::TestCase assert_equal("application/pdf", email.parts[1].mime_type) assert_equal("VGhpcyBpcyB0ZXN0IEZpbGUgY29udGVudA==\r\n", email.parts[1].body.encoded) end + + test "can embed an inline attachment" do + email = BaseMailer.inline_attachment + # Need to call #encoded to force the JIT sort on parts + email.encoded + assert_equal(2, email.parts.length) + assert_equal("multipart/related", email.mime_type) + assert_equal("multipart/alternative", email.parts[0].mime_type) + assert_equal("text/plain", email.parts[0].parts[0].mime_type) + assert_equal("text/html", email.parts[0].parts[1].mime_type) + assert_equal("logo.png", email.parts[1].filename) + end # Defaults values test "uses default charset from class" do diff --git a/actionmailer/test/fixtures/base_mailer/inline_attachment.html.erb b/actionmailer/test/fixtures/base_mailer/inline_attachment.html.erb new file mode 100644 index 0000000000..e200878127 --- /dev/null +++ b/actionmailer/test/fixtures/base_mailer/inline_attachment.html.erb @@ -0,0 +1,5 @@ +<h1>Inline Image</h1> + +<%= image_tag attachments['logo.png'].url %> + +<p>This is an image that is inline</p>
\ No newline at end of file diff --git a/actionmailer/test/fixtures/base_mailer/inline_attachment.text.erb b/actionmailer/test/fixtures/base_mailer/inline_attachment.text.erb new file mode 100644 index 0000000000..e161d244d2 --- /dev/null +++ b/actionmailer/test/fixtures/base_mailer/inline_attachment.text.erb @@ -0,0 +1,4 @@ +Inline Image + +No image for you + diff --git a/actionmailer/test/old_base/mail_service_test.rb b/actionmailer/test/old_base/mail_service_test.rb index 7054e8052a..e8e8fcedc9 100644 --- a/actionmailer/test/old_base/mail_service_test.rb +++ b/actionmailer/test/old_base/mail_service_test.rb @@ -674,7 +674,7 @@ 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 @@ -863,7 +863,7 @@ EOF def test_multipart_with_utf8_subject mail = TestMailer.multipart_with_utf8_subject(@recipient) - regex = Regexp.escape('Subject: =?UTF-8?Q?Foo_=C3=A1=C3=AB=C3=B4_=C3=AE=C3=BC?=') + 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 áëô îü" assert_match(string, mail.subject) @@ -871,7 +871,7 @@ EOF def test_implicitly_multipart_with_utf8 mail = TestMailer.implicitly_multipart_with_utf8 - regex = Regexp.escape('Subject: =?UTF-8?Q?Foo_=C3=A1=C3=AB=C3=B4_=C3=AE=C3=BC?=') + 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 áëô îü" assert_match(string, mail.subject) diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index faa0d674dc..c3609958bc 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,4 +1,14 @@ -*Rails 3.0.0 [beta 4/release candidate] (unreleased)* +*Rails 3.0.0 [beta 4] (June 8th, 2010)* + +* Add shallow routes back to the new router [Diego Carrion] + + resources :posts do + shallow do + resources :comments + end + end + + You can now use comment_path for /comments/1 instead of post_comment_path for /posts/1/comments/1. * Remove middleware laziness [José Valim] diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index 10c7ca9021..c465035ca1 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -16,5 +16,13 @@ module ActionController raise "In order to use #url_for, you must include the helpers of a particular " \ "router. For instance, `include Rails.application.routes.url_helpers" end + + module ClassMethods + def action_methods + @action_methods ||= begin + super - _router.named_routes.helper_names + end + end + end end end
\ No newline at end of file diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb index 602411ed37..064ff3724d 100644 --- a/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb +++ b/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb @@ -23,6 +23,7 @@ module HTML #:nodoc: # Create a new Tokenizer for the given text. def initialize(text) + text.encode! if text.encoding_aware? @scanner = StringScanner.new(text) @position = 0 @line = 0 diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index f9e81a02d3..8a2d8cd077 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -6,7 +6,7 @@ module ActionDispatch # This middleware rescues any exception returned by the application and renders # nice exception pages if it's being rescued locally. class ShowExceptions - LOCALHOST = ['127.0.0.1', '::1'].freeze + LOCALHOST = [/^127\.0\.0\.\d{1,3}$/, "::1", /^0:0:0:0:0:0:0:1(%.*)?$/].freeze RESCUES_TEMPLATE_PATH = File.join(File.dirname(__FILE__), 'templates') @@ -114,7 +114,7 @@ module ActionDispatch # True if the request came from localhost, 127.0.0.1. def local_request?(request) - LOCALHOST.any?{ |local_ip| request.remote_addr == local_ip && request.remote_ip == local_ip } + LOCALHOST.any? { |local_ip| local_ip === request.remote_addr && local_ip === request.remote_ip } end def status_code(exception) diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index ae4417b56c..e91a72cbe5 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -350,6 +350,10 @@ module ActionDispatch scope(:constraints => constraints) { yield } end + def shallow + scope(:shallow => true) { yield } + end + def defaults(defaults = {}) scope(:defaults => defaults) { yield } end @@ -374,12 +378,21 @@ module ActionDispatch @scope_options ||= private_methods.grep(/^merge_(.+)_scope$/) { $1.to_sym } end + def merge_shallow_scope(parent, child) + parent or child + end + def merge_path_scope(parent, child) - Mapper.normalize_path("#{parent}/#{child}") + parent_path = (@scope[:shallow] and child.eql?(':id')) ? parent.split('/').last : parent + Mapper.normalize_path "#{parent_path}/#{child}" end def merge_name_prefix_scope(parent, child) - parent ? "#{parent}_#{child}" : child + if @scope[:shallow] + child + else + parent ? "#{parent}_#{child}" : child + end end def merge_module_scope(parent, child) @@ -463,6 +476,10 @@ module ActionDispatch name.to_s.singularize end + def member_prefix + ':id' + end + def member_name singular end @@ -509,11 +526,19 @@ module ActionDispatch end end + def nested_prefix + id_segment + end + def nested_options options = { :name_prefix => member_name } options["#{singular}_id".to_sym] = id_constraint if id_constraint? options end + + def shallow? + options[:shallow] + end end class SingletonResource < Resource #:nodoc: @@ -532,9 +557,21 @@ module ActionDispatch end end + def member_prefix + '' + end + def member_name name end + + def nested_prefix + '' + end + + def nested_options + { :name_prefix => member_name } + end end def initialize(*args) #:nodoc: @@ -544,6 +581,7 @@ module ActionDispatch def resource(*resources, &block) options = resources.extract_options! + options = (@scope[:options] || {}).merge(options) if apply_common_behavior_for(:resource, resources, options, &block) return self @@ -554,17 +592,17 @@ module ActionDispatch scope(:path => resource.path, :controller => resource.controller) do with_scope_level(:resource, resource) do - scope(:name_prefix => resource.name.to_s, :as => "") do - yield if block_given? - end + yield if block_given? - scope(resource.options) do - get :show if resource.actions.include?(:show) - post :create if resource.actions.include?(:create) - put :update if resource.actions.include?(:update) - delete :destroy if resource.actions.include?(:destroy) - get :new, :as => resource.name if resource.actions.include?(:new) - get :edit, :as => resource.name if resource.actions.include?(:edit) + with_scope_level(:member) do + scope(resource.options) do + get :show if resource.actions.include?(:show) + post :create if resource.actions.include?(:create) + put :update if resource.actions.include?(:update) + delete :destroy if resource.actions.include?(:destroy) + get :new, :as => resource.name if resource.actions.include?(:new) + get :edit, :as => resource.name if resource.actions.include?(:edit) + end end end end @@ -574,6 +612,7 @@ module ActionDispatch def resources(*resources, &block) options = resources.extract_options! + options = (@scope[:options] || {}).merge(options) if apply_common_behavior_for(:resources, resources, options, &block) return self @@ -581,8 +620,12 @@ module ActionDispatch resource = Resource.new(resources.pop, options) - scope(:path => resource.path, :controller => resource.controller) do + scope(:path => resource.path, :controller => resource.controller, :shallow => resource.shallow?) do with_scope_level(:resources, resource) do + if @scope[:shallow] && @scope[:name_prefix] + @scope[:path] = "/#{@scope[:name_prefix].pluralize}/:#{@scope[:name_prefix]}_id/#{resource.path}" + end + yield if block_given? with_scope_level(:collection) do @@ -596,6 +639,8 @@ module ActionDispatch with_scope_level(:member) do scope(':id') do scope(resource.options) do + @scope[:name_prefix] = nil if @scope[:shallow] + get :show if resource.actions.include?(:show) put :update if resource.actions.include?(:update) delete :destroy if resource.actions.include?(:destroy) @@ -622,31 +667,36 @@ module ActionDispatch end def member - unless [:resources, :resource].include?(@scope[:scope_level]) - raise ArgumentError, "You can't use member action outside resources and resource scope." + unless resource_scope? + raise ArgumentError, "can't use member outside resource(s) scope" end - case @scope[:scope_level] - when :resources - with_scope_level(:member) do - scope(':id', :name_prefix => parent_resource.member_name, :as => "") do - yield - end + with_scope_level(:member) do + scope(parent_resource.member_prefix, :name_prefix => parent_resource.member_name, :as => "") do + yield end - when :resource - with_scope_level(:member) do + end + end + + def new + unless resource_scope? + raise ArgumentError, "can't use new outside resource(s) scope" + end + + with_scope_level(:new) do + scope(new_scope_prefix, :name_prefix => parent_resource.member_name, :as => "") do yield end end end def nested - unless @scope[:scope_level] == :resources - raise ArgumentError, "can't use nested outside resources scope" + unless resource_scope? + raise ArgumentError, "can't use nested outside resource(s) scope" end with_scope_level(:nested) do - scope(parent_resource.id_segment, parent_resource.nested_options) do + scope(parent_resource.nested_prefix, parent_resource.nested_options) do yield end end @@ -678,7 +728,7 @@ module ActionDispatch @scope[:path] = old_path end else - with_exclusive_name_prefix(action) do + with_exclusive_name_prefix(action_name_prefix(action, options)) do return match("#{action_path(action, path_names)}(.:format)", options.reverse_merge(:to => action)) end end @@ -691,10 +741,16 @@ module ActionDispatch return collection { match(*args) } when :member return member { match(*args) } + when :new + return new { match(*args) } + end + + if @scope[:scope_level] == :resource + return member { match(*args) } end - if @scope[:scope_level] == :resources - raise ArgumentError, "can't define route directly in resources scope" + if resource_scope? + raise ArgumentError, "can't define route directly in resource(s) scope" end super @@ -716,6 +772,10 @@ module ActionDispatch path_names[name.to_sym] || name.to_s end + def action_name_prefix(action, options = {}) + (options[:on] == :new || @scope[:scope_level] == :new) ? "#{action}_new" : action + end + def apply_common_behavior_for(method, resources, options, &block) if resources.length > 1 resources.each { |r| send(method, r, options, &block) } @@ -729,7 +789,7 @@ module ActionDispatch return true end - if @scope[:scope_level] == :resources + if resource_scope? nested do send(method, resources.pop, options, &block) end @@ -739,6 +799,14 @@ module ActionDispatch false end + def new_scope_prefix + @scope[:path_names][:new] || 'new' + end + + def resource_scope? + [:resource, :resources].include?(@scope[:scope_level]) + end + def with_exclusive_name_prefix(prefix) begin old_name_prefix = @scope[:name_prefix] diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 750912b251..57a73dde75 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -68,6 +68,10 @@ module ActionDispatch clear! end + def helper_names + self.module.instance_methods.map(&:to_s) + end + def clear! @routes = {} @helpers = [] @@ -176,7 +180,6 @@ module ActionDispatch url_for(options) end - protected :#{selector} END_EVAL helpers << selector end diff --git a/actionpack/lib/action_dispatch/testing/assertions/selector.rb b/actionpack/lib/action_dispatch/testing/assertions/selector.rb index 9deabf5b3c..0e82b41590 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/selector.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/selector.rb @@ -267,14 +267,12 @@ module ActionDispatch if match_with = equals[:text] matches.delete_if do |match| text = "" - text.force_encoding(match_with.encoding) if text.respond_to?(:force_encoding) stack = match.children.reverse while node = stack.pop if node.tag? stack.concat node.children.reverse else content = node.content - content.force_encoding(match_with.encoding) if content.respond_to?(:force_encoding) text << content end end diff --git a/actionpack/lib/action_pack/version.rb b/actionpack/lib/action_pack/version.rb index 8f0c5d939f..532d060c06 100644 --- a/actionpack/lib/action_pack/version.rb +++ b/actionpack/lib/action_pack/version.rb @@ -3,7 +3,7 @@ module ActionPack MAJOR = 3 MINOR = 0 TINY = 0 - BUILD = "beta3" + BUILD = "beta4" STRING = [MAJOR, MINOR, TINY, BUILD].join('.') end diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb index 626cc7d3b0..25426a5547 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb @@ -620,7 +620,10 @@ module ActionView options.symbolize_keys! src = options[:src] = path_to_image(source) - options[:alt] = options.fetch(:alt){ File.basename(src, '.*').capitalize } + + unless src =~ /^cid:/ + options[:alt] = options.fetch(:alt){ File.basename(src, '.*').capitalize } + end if size = options.delete(:size) options[:width], options[:height] = size.split("x") if size =~ %r{^\d+x\d+$} @@ -754,7 +757,7 @@ module ActionView end def is_uri?(path) - path =~ %r{^[-a-z]+://} + path =~ %r{^[-a-z]+://|^cid:} end # Pick an asset host for this source. Returns +nil+ if no host is set, diff --git a/actionpack/lib/action_view/helpers/cache_helper.rb b/actionpack/lib/action_view/helpers/cache_helper.rb index a904af56bb..8251ed18f4 100644 --- a/actionpack/lib/action_view/helpers/cache_helper.rb +++ b/actionpack/lib/action_view/helpers/cache_helper.rb @@ -32,27 +32,27 @@ module ActionView # <i>Topics listed alphabetically</i> # <% end %> def cache(name = {}, options = nil, &block) - safe_concat fragment_for(name, options, &block) + if controller.perform_caching + safe_concat(fragment_for(name, options, &block)) + else + yield + end + nil end private # TODO: Create an object that has caching read/write on it def fragment_for(name = {}, options = nil, &block) #:nodoc: - if controller.perform_caching - if controller.fragment_exist?(name, options) - controller.read_fragment(name, options) - else - # VIEW TODO: Make #capture usable outside of ERB - # This dance is needed because Builder can't use capture - pos = output_buffer.length - yield - fragment = output_buffer.slice!(pos..-1) - controller.write_fragment(name, fragment, options) - end + if controller.fragment_exist?(name, options) + controller.read_fragment(name, options) else - ret = yield - ActiveSupport::SafeBuffer.new(ret) if ret.is_a?(String) + # VIEW TODO: Make #capture usable outside of ERB + # This dance is needed because Builder can't use capture + pos = output_buffer.length + yield + fragment = output_buffer.slice!(pos..-1) + controller.write_fragment(name, fragment, options) end end end diff --git a/actionpack/lib/action_view/helpers/tag_helper.rb b/actionpack/lib/action_view/helpers/tag_helper.rb index c09d01eeee..66277f79fe 100644 --- a/actionpack/lib/action_view/helpers/tag_helper.rb +++ b/actionpack/lib/action_view/helpers/tag_helper.rb @@ -110,7 +110,7 @@ module ActionView def content_tag_string(name, content, options, escape = true) tag_options = tag_options(options, escape) if options - "<#{name}#{tag_options}>#{ERB::Util.h(content)}</#{name}>".html_safe + "<#{name}#{tag_options}>#{escape ? ERB::Util.h(content) : content}</#{name}>".html_safe end def tag_options(options, escape = true) diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb index bfad9f8d31..8f63845d49 100644 --- a/actionpack/lib/action_view/helpers/text_helper.rb +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -74,6 +74,7 @@ module ActionView options.reverse_merge!(:length => 30) + text = sanitize(text) unless text.html_safe? || options[:safe] text.truncate(options.delete(:length), options) if text end @@ -105,6 +106,7 @@ module ActionView end options.reverse_merge!(:highlighter => '<strong class="highlight">\1</strong>') + text = sanitize(text) unless text.html_safe? || options[:safe] if text.blank? || phrases.blank? text else @@ -244,13 +246,14 @@ module ActionView # def textilize(text, *options) options ||= [:hard_breaks] + text = sanitize(text) unless text.html_safe? || options.delete(:safe) if text.blank? "" else textilized = RedCloth.new(text, options) textilized.to_html - end + end.html_safe end # Returns the text with all the Textile codes turned into HTML tags, @@ -271,8 +274,8 @@ module ActionView # # textilize_without_paragraph("Visit the Rails website "here":http://www.rubyonrails.org/.) # # => "Visit the Rails website <a href="http://www.rubyonrails.org/">here</a>." - def textilize_without_paragraph(text) - textiled = textilize(text) + def textilize_without_paragraph(text, *options) + textiled = textilize(text, *options) if textiled[0..2] == "<p>" then textiled = textiled[3..-1] end if textiled[-4..-1] == "</p>" then textiled = textiled[0..-5] end return textiled @@ -295,8 +298,9 @@ module ActionView # # markdown('') # # => '<p><img src="http://rubyonrails.com/images/rails.png" alt="The ROR logo" title="Ruby on Rails" /></p>' - def markdown(text) - text.blank? ? "" : BlueCloth.new(text).to_html + def markdown(text, *options) + text = sanitize(text) unless text.html_safe? || options.delete(:safe) + (text.blank? ? "" : BlueCloth.new(text).to_html).html_safe end # Returns +text+ transformed into HTML using simple formatting rules. @@ -320,14 +324,15 @@ module ActionView # # simple_format("Look ma! A class!", :class => 'description') # # => "<p class='description'>Look ma! A class!</p>" - def simple_format(text, html_options={}) + def simple_format(text, html_options={}, options={}) + text = '' if text.nil? start_tag = tag('p', html_options, true) - text = h(text) + text = sanitize(text) unless text.html_safe? || options[:safe] text.gsub!(/\r\n?/, "\n") # \r\n and \r -> \n text.gsub!(/\n\n+/, "</p>\n\n#{start_tag}") # 2+ newline -> paragraph text.gsub!(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br text.insert 0, start_tag - text.safe_concat("</p>") + text.html_safe.safe_concat("</p>") end # Turns all URLs and e-mail addresses into clickable links. The <tt>:link</tt> option @@ -368,7 +373,7 @@ module ActionView # # => "Welcome to my new blog at <a href=\"http://www.myblog.com/\" target=\"_blank\">http://www.myblog.com</a>. # Please e-mail me at <a href=\"mailto:me@email.com\">me@email.com</a>." def auto_link(text, *args, &block)#link = :all, html = {}, &block) - return '' if text.blank? + return ''.html_safe if text.blank? options = args.size == 2 ? {} : args.extract_options! # this is necessary because the old auto_link API has a Hash as its last parameter unless args.empty? @@ -378,9 +383,9 @@ module ActionView options.reverse_merge!(:link => :all, :html => {}) case options[:link].to_sym - when :all then auto_link_email_addresses(auto_link_urls(text, options[:html], &block), options[:html], &block) + when :all then auto_link_email_addresses(auto_link_urls(text, options[:html], options, &block), options[:html], &block) when :email_addresses then auto_link_email_addresses(text, options[:html], &block) - when :urls then auto_link_urls(text, options[:html], &block) + when :urls then auto_link_urls(text, options[:html], options, &block) end end @@ -544,7 +549,7 @@ module ActionView # Turns all urls into clickable links. If a block is given, each url # is yielded and the result is used as the link text. - def auto_link_urls(text, html_options = {}) + def auto_link_urls(text, html_options = {}, options = {}) link_attributes = html_options.stringify_keys text.gsub(AUTO_LINK_RE) do scheme, href = $1, $& @@ -566,21 +571,22 @@ module ActionView link_text = block_given?? yield(href) : href href = 'http://' + href unless scheme - content_tag(:a, link_text, link_attributes.merge('href' => href)) + punctuation.reverse.join('') + content_tag(:a, link_text, link_attributes.merge('href' => href), !(options[:safe] || text.html_safe?)) + punctuation.reverse.join('') end - end + end.html_safe end # Turns all email addresses into clickable links. If a block is given, # each email is yielded and the result is used as the link text. - def auto_link_email_addresses(text, html_options = {}) + def auto_link_email_addresses(text, html_options = {}, options = {}) text.gsub(AUTO_EMAIL_RE) do text = $& if auto_linked?($`, $') - text + text.html_safe else display_text = (block_given?) ? yield(text) : text + display_text = sanitize(display_text) unless options[:safe] mail_to text, display_text, html_options end end diff --git a/actionpack/lib/action_view/lookup_context.rb b/actionpack/lib/action_view/lookup_context.rb index 3aaa5e401c..823226cb9c 100644 --- a/actionpack/lib/action_view/lookup_context.rb +++ b/actionpack/lib/action_view/lookup_context.rb @@ -188,7 +188,7 @@ module ActionView begin yield ensure - _set_detail(:formats, formats) + _set_detail(:formats, old_formats) end end end diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index 4431eb2e2a..c161bea945 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -644,21 +644,6 @@ class FragmentCachingTest < ActionController::TestCase assert_equal 'will not expire ;-)', @store.read('views/primalgrasp') end - def test_fragment_for_with_disabled_caching - @controller.perform_caching = false - - @store.write('views/expensive', 'fragment content') - fragment_computed = false - - view_context = @controller.view_context - - buffer = 'generated till now -> '.html_safe - buffer << view_context.send(:fragment_for, 'expensive') { fragment_computed = true } - - assert fragment_computed - assert_equal 'generated till now -> ', buffer - end - def test_fragment_for @store.write('views/expensive', 'fragment content') fragment_computed = false diff --git a/actionpack/test/controller/new_base/render_rjs_test.rb b/actionpack/test/controller/new_base/render_rjs_test.rb index b602b9f8e9..74bf865b54 100644 --- a/actionpack/test/controller/new_base/render_rjs_test.rb +++ b/actionpack/test/controller/new_base/render_rjs_test.rb @@ -2,7 +2,10 @@ require 'abstract_unit' module RenderRjs class BasicController < ActionController::Base + layout "application", :only => :index_respond_to + self.view_paths = [ActionView::FixtureResolver.new( + "layouts/application.html.erb" => "", "render_rjs/basic/index.js.rjs" => "page[:customer].replace_html render(:partial => 'customer')", "render_rjs/basic/index_html.js.rjs" => "page[:customer].replace_html :partial => 'customer'", "render_rjs/basic/index_no_js.js.erb" => "<%= render(:partial => 'developer') %>", diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index ffa4f50b00..a294535e88 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -34,6 +34,33 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + resources :users do + shallow do + resources :photos do + resources :types do + member do + post :preview + end + collection do + delete :erase + end + end + end + end + end + + shallow do + resources :teams do + resources :players + end + + resources :countries do + resources :cities do + resources :places + end + end + end + match 'account/logout' => redirect("/logout"), :as => :logout_redirect match 'account/login', :to => redirect("/login") @@ -69,8 +96,17 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end scope 'pt', :name_prefix => 'pt' do - resources :projects, :path_names => { :edit => 'editar' }, :path => 'projetos' - resource :admin, :path_names => { :new => 'novo' }, :path => 'administrador' + resources :projects, :path_names => { :edit => 'editar', :new => 'novo' }, :path => 'projetos' do + post :preview, :on => :new + end + resource :admin, :path_names => { :new => 'novo' }, :path => 'administrador' do + post :preview, :on => :new + end + resources :products, :path_names => { :new => 'novo' } do + new do + post :preview + end + end end resources :projects, :controller => :project do @@ -119,6 +155,10 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end resources :replies do + new do + post :preview + end + member do put :answer, :to => :mark_as_answer delete :answer, :to => :unmark_as_answer @@ -189,10 +229,13 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest root :to => 'projects#index' end - resources :products, :constraints => { :id => /\d{4}/ } do - root :to => "products#root" - get :favorite, :on => :collection - resources :images + scope :only => [:index, :show] do + resources :products, :constraints => { :id => /\d{4}/ } do + root :to => "products#root" + get :favorite, :on => :collection + resources :images + end + resource :account end resource :dashboard, :constraints => { :ip => /192\.168\.1\.\d{1,3}/ } @@ -207,6 +250,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end match "whatever/:controller(/:action(/:id))" + + resource :profile do + get :settings + + new do + post :preview + end + end end end @@ -728,6 +779,18 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + def test_shallow_routes + with_test_routes do + assert_equal '/photos/4', photo_path(4) + assert_equal '/types/10/edit', edit_type_path(10) + assert_equal '/types/5/preview', preview_type_path(5) + assert_equal '/photos/2/types', photo_types_path(2) + assert_equal '/cities/1/places', url_for(:controller => :places, :action => :index, :city_id => 1, :only_path => true) + assert_equal '/teams/new', url_for(:controller => :teams, :action => :new, :only_path => true) + assert_equal '/photos/11/types/erase', url_for(:controller => :types, :action => :erase, :photo_id => 11, :only_path => true) + end + end + def test_update_project_person with_test_routes do get '/projects/1/people/2/update' @@ -1038,6 +1101,54 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + def test_resource_new_actions + with_test_routes do + assert_equal '/replies/new/preview', preview_new_reply_path + assert_equal '/pt/projetos/novo/preview', preview_new_pt_project_path + assert_equal '/pt/administrador/novo/preview', preview_new_pt_admin_path + assert_equal '/pt/products/novo/preview', preview_new_pt_product_path + assert_equal '/profile/new/preview', preview_new_profile_path + + post '/replies/new/preview' + assert_equal 'replies#preview', @response.body + + post '/pt/projetos/novo/preview' + assert_equal 'projects#preview', @response.body + + post '/pt/administrador/novo/preview' + assert_equal 'admins#preview', @response.body + + post '/pt/products/novo/preview' + assert_equal 'products#preview', @response.body + + post '/profile/new/preview' + assert_equal 'profiles#preview', @response.body + end + end + + def test_resource_merges_options_from_scope + with_test_routes do + assert_raise(NameError) { new_account_path } + + get '/account/new' + assert_equal 404, status + end + end + + def test_resources_merges_options_from_scope + with_test_routes do + assert_raise(NoMethodError) { edit_product_path('1') } + + get '/products/1/edit' + assert_equal 404, status + + assert_raise(NoMethodError) { edit_product_image_path('1', '2') } + + post '/products/1/images/2/edit' + assert_equal 404, status + end + end + private def with_test_routes yield diff --git a/actionpack/test/dispatch/show_exceptions_test.rb b/actionpack/test/dispatch/show_exceptions_test.rb index b447b0715c..4966527f4d 100644 --- a/actionpack/test/dispatch/show_exceptions_test.rb +++ b/actionpack/test/dispatch/show_exceptions_test.rb @@ -53,7 +53,7 @@ class ShowExceptionsTest < ActionController::IntegrationTest test "rescue locally from a local request" do @app = ProductionApp - ['127.0.0.1', '::1'].each do |ip_address| + ['127.0.0.1', '127.0.0.127', '::1', '0:0:0:0:0:0:0:1', '0:0:0:0:0:0:0:1%0'].each do |ip_address| self.remote_addr = ip_address get "/", {}, {'action_dispatch.show_exceptions' => true} diff --git a/actionpack/test/template/asset_tag_helper_test.rb b/actionpack/test/template/asset_tag_helper_test.rb index b6a6f52876..633641514e 100644 --- a/actionpack/test/template/asset_tag_helper_test.rb +++ b/actionpack/test/template/asset_tag_helper_test.rb @@ -404,6 +404,15 @@ class AssetTagHelperTest < ActionView::TestCase assert_equal %(<img alt="Rails" src="#{expected_path}" />), image_tag("rails.png") end + def test_image_tag_interpreting_email_cid_correctly + # An inline image has no need for an alt tag to be automatically generated from the cid: + assert_equal '<img src="cid:thi%25%25sis@acontentid" />', image_tag("cid:thi%25%25sis@acontentid") + end + + def test_image_tag_interpreting_email_adding_optional_alt_tag + assert_equal '<img alt="Image" src="cid:thi%25%25sis@acontentid" />', image_tag("cid:thi%25%25sis@acontentid", :alt => "Image") + end + def test_timebased_asset_id_with_relative_url_root @controller.config.relative_url_root = "/collaboration/hieraki" expected_time = File.stat(File.expand_path(File.dirname(__FILE__) + "/../fixtures/public/images/rails.png")).mtime.to_i.to_s diff --git a/actionpack/test/template/tag_helper_test.rb b/actionpack/test/template/tag_helper_test.rb index 256d9bdcde..ec5fe3d1d7 100644 --- a/actionpack/test/template/tag_helper_test.rb +++ b/actionpack/test/template/tag_helper_test.rb @@ -39,6 +39,8 @@ class TagHelperTest < ActionView::TestCase content_tag("a", "Create", :href => "create") assert_equal "<p><script>evil_js</script></p>", content_tag(:p, '<script>evil_js</script>') + assert_equal "<p><script>evil_js</script></p>", + content_tag(:p, '<script>evil_js</script>', nil, false) end def test_content_tag_with_block_in_erb diff --git a/actionpack/test/template/text_helper_test.rb b/actionpack/test/template/text_helper_test.rb index bb808b77a5..64f1d46413 100644 --- a/actionpack/test/template/text_helper_test.rb +++ b/actionpack/test/template/text_helper_test.rb @@ -7,6 +7,12 @@ rescue LoadError $stderr.puts "Skipping textilize tests. `gem install RedCloth` to enable." end +begin + require 'bluecloth' +rescue LoadError + $stderr.puts "Skipping markdown tests. 'gem install bluecloth' to enable." +end + class TextHelperTest < ActionView::TestCase tests ActionView::Helpers::TextHelper include TestingSandbox @@ -45,19 +51,42 @@ class TextHelperTest < ActionView::TestCase assert simple_format("<b> test with html tags </b>").html_safe? end - def test_simple_format_should_escape_unsafe_input - assert_equal "<p><b> test with unsafe string </b></p>", simple_format("<b> test with unsafe string </b>") + def test_simple_format_should_sanitize_unsafe_input + assert_equal "<p><b> test with unsafe string </b></p>", simple_format("<b> test with unsafe string </b><script>code!</script>") + end + + def test_simple_format_should_not_sanitize_input_if_safe_option + assert_equal "<p><b> test with unsafe string </b><script>code!</script></p>", simple_format("<b> test with unsafe string </b><script>code!</script>", {}, :safe => true) end - def test_simple_format_should_not_escape_safe_input + def test_simple_format_should_not_sanitize_safe_input assert_equal "<p><b> test with safe string </b></p>", simple_format("<b> test with safe string </b>".html_safe) end + def test_truncate_should_be_html_safe + assert truncate("Hello World!", :length => 12).html_safe? + end + def test_truncate assert_equal "Hello World!", truncate("Hello World!", :length => 12) assert_equal "Hello Wor...", truncate("Hello World!!", :length => 12) end + def test_truncate_should_sanitize_unsafe_input + assert_equal "Hello World!", truncate("Hello <script>code!</script>World!", :length => 12) + assert_equal "Hello Wor...", truncate("Hello <script>code!</script>World!!", :length => 12) + end + + def test_truncate_should_not_sanitize_input_if_safe_option + assert_equal "Hello <sc...", truncate("Hello <script>code!</script>World!", :length => 12, :safe => true) + assert_equal "Hello <sc...", truncate("Hello <script>code!</script>World!!", :length => 12, :safe => true) + end + + def test_truncate_should_not_sanitize_safe_input + assert_equal "Hello <sc...", truncate("Hello <script>code!</script>World!".html_safe, :length => 12) + assert_equal "Hello <sc...", truncate("Hello <script>code!</script>World!!".html_safe, :length => 12) + end + def test_truncate_should_use_default_length_of_30 str = "This is a string that will go longer then the default truncate length of 30" assert_equal str[0...27] + "...", truncate(str) @@ -93,7 +122,11 @@ class TextHelperTest < ActionView::TestCase end end - def test_highlighter + def test_highlight_should_be_html_safe + assert highlight("This is a beautiful morning", "beautiful").html_safe? + end + + def test_highlight assert_equal( "This is a <strong class=\"highlight\">beautiful</strong> morning", highlight("This is a beautiful morning", "beautiful") @@ -117,6 +150,27 @@ class TextHelperTest < ActionView::TestCase assert_equal ' ', highlight(' ', 'blank text is returned verbatim') end + def test_highlight_should_sanitize_unsafe_input + assert_equal( + "This is a <strong class=\"highlight\">beautiful</strong> morning", + highlight("This is a beautiful morning<script>code!</script>", "beautiful") + ) + end + + def test_highlight_should_not_sanitize_input_if_safe_option + assert_equal( + "This is a <strong class=\"highlight\">beautiful</strong> morning<script>code!</script>", + highlight("This is a beautiful morning<script>code!</script>", "beautiful", :safe => true) + ) + end + + def test_highlight_should_not_sanitize_safe_input + assert_equal( + "This is a <strong class=\"highlight\">beautiful</strong> morning<script>code!</script>", + highlight("This is a beautiful morning<script>code!</script>".html_safe, "beautiful") + ) + end + def test_highlight_with_regexp assert_equal( "This is a <strong class=\"highlight\">beautiful!</strong> morning", @@ -163,7 +217,7 @@ class TextHelperTest < ActionView::TestCase highlight("<p class=\"beautiful\">This is a beautiful morning, but also a beautiful day</p>", "beautiful") ) assert_equal( - "<p>This is a <strong class=\"highlight\">beautiful</strong> <a href=\"http://example.com/beautiful\#top?what=beautiful%20morning&when=now+then\">morning</a>, but also a <strong class=\"highlight\">beautiful</strong> day</p>", + "<p>This is a <strong class=\"highlight\">beautiful</strong> <a href=\"http://example.com/beautiful\#top?what=beautiful%20morning&when=now+then\">morning</a>, but also a <strong class=\"highlight\">beautiful</strong> day</p>", highlight("<p>This is a beautiful <a href=\"http://example.com/beautiful\#top?what=beautiful%20morning&when=now+then\">morning</a>, but also a beautiful day</p>", "beautiful") ) end @@ -286,7 +340,17 @@ class TextHelperTest < ActionView::TestCase %{<a href="#{CGI::escapeHTML href}">#{CGI::escapeHTML link_text}</a>} end - def test_auto_linking + def test_auto_link_should_be_html_safe + email_raw = 'santiago@wyeworks.com' + link_raw = 'http://www.rubyonrails.org' + + assert auto_link(nil).html_safe? + assert auto_link('').html_safe? + assert auto_link("#{link_raw} #{link_raw} #{link_raw}").html_safe? + assert auto_link("hello #{email_raw}").html_safe? + end + + def test_auto_link email_raw = 'david@loudthinking.com' email_result = %{<a href="mailto:#{email_raw}">#{email_raw}</a>} link_raw = 'http://www.rubyonrails.com' @@ -378,6 +442,21 @@ class TextHelperTest < ActionView::TestCase assert_equal %(<p>#{link10_result} Link</p>), auto_link("<p>#{link10_raw} Link</p>") end + def test_auto_link_should_sanitize_unsafe_input + link_raw = %{http://www.rubyonrails.com?id=1&num=2} + assert_equal %{<a href="http://www.rubyonrails.com?id=1&num=2">http://www.rubyonrails.com?id=1&num=2</a>}, auto_link(link_raw) + end + + def test_auto_link_should_sanitize_unsafe_input + link_raw = %{http://www.rubyonrails.com?id=1&num=2} + assert_equal %{<a href="http://www.rubyonrails.com?id=1&num=2">http://www.rubyonrails.com?id=1&num=2</a>}, auto_link(link_raw, :safe => true) + end + + def test_auto_link_should_not_sanitize_safe_input + link_raw = %{http://www.rubyonrails.com?id=1&num=2} + assert_equal %{<a href="http://www.rubyonrails.com?id=1&num=2">http://www.rubyonrails.com?id=1&num=2</a>}, auto_link(link_raw.html_safe) + end + def test_auto_link_other_protocols ftp_raw = 'ftp://example.com/file.txt' assert_equal %(Download #{generate_result(ftp_raw)}), auto_link("Download #{ftp_raw}") @@ -587,7 +666,12 @@ class TextHelperTest < ActionView::TestCase assert_equal(%w{Specialized Fuji Giant}, @cycles) end + # TODO test textilize_without_paragraph and markdown if defined? RedCloth + def test_textilize_should_be_html_safe + assert textilize("*This is Textile!* Rejoice!").html_safe? + end + def test_textilize assert_equal("<p><strong>This is Textile!</strong> Rejoice!</p>", textilize("*This is Textile!* Rejoice!")) end @@ -600,8 +684,82 @@ class TextHelperTest < ActionView::TestCase assert_equal("<p>This is worded <strong>strongly</strong></p>", textilize("This is worded <strong>strongly</strong>", :filter_html)) end + def test_textilize_should_sanitize_unsafe_input + assert_equal("<p>This is worded <strong>strongly</strong></p>", textilize("This is worded <strong>strongly</strong><script>code!</script>")) + end + + def test_textilize_should_not_sanitize_input_if_safe_option + assert_equal("<p>This is worded <strong>strongly</strong><script>code!</script></p>", textilize("This is worded <strong>strongly</strong><script>code!</script>", :safe)) + end + + def test_textilize_should_not_sanitize_safe_input + assert_equal("<p>This is worded <strong>strongly</strong><script>code!</script></p>", textilize("This is worded <strong>strongly</strong><script>code!</script>".html_safe)) + end + def test_textilize_with_hard_breaks assert_equal("<p>This is one scary world.<br />\n True.</p>", textilize("This is one scary world.\n True.")) end + + def test_textilize_without_paragraph_should_be_html_safe + textilize_without_paragraph("*This is Textile!* Rejoice!").html_safe? + end + + def test_textilize_without_paragraph + assert_equal("<strong>This is Textile!</strong> Rejoice!", textilize_without_paragraph("*This is Textile!* Rejoice!")) + end + + def test_textilize_without_paragraph_with_blank + assert_equal("", textilize_without_paragraph("")) + end + + def test_textilize_without_paragraph_with_options + assert_equal("This is worded <strong>strongly</strong>", textilize_without_paragraph("This is worded <strong>strongly</strong>", :filter_html)) + end + + def test_textilize_without_paragraph_should_sanitize_unsafe_input + assert_equal("This is worded <strong>strongly</strong>", textilize_without_paragraph("This is worded <strong>strongly</strong><script>code!</script>")) + end + + def test_textilize_without_paragraph_should_not_sanitize_input_if_safe_option + assert_equal("This is worded <strong>strongly</strong><script>code!</script>", textilize_without_paragraph("This is worded <strong>strongly</strong><script>code!</script>", :safe)) + end + + def test_textilize_without_paragraph_should_not_sanitize_safe_input + assert_equal("This is worded <strong>strongly</strong><script>code!</script>", textilize_without_paragraph("This is worded <strong>strongly</strong><script>code!</script>".html_safe)) + end + + def test_textilize_without_paragraph_with_hard_breaks + assert_equal("This is one scary world.<br />\n True.", textilize_without_paragraph("This is one scary world.\n True.")) + end + end + + if defined? BlueCloth + def test_markdown_should_be_html_safe + assert markdown("We are using __Markdown__ now!").html_safe? + end + + def test_markdown + assert_equal("<p>We are using <strong>Markdown</strong> now!</p>", markdown("We are using __Markdown__ now!")) + end + + def test_markdown_with_blank + assert_equal("", markdown("")) + end + + def test_markdown_should_sanitize_unsafe_input + assert_equal("<p>This is worded <strong>strongly</strong></p>", markdown("This is worded <strong>strongly</strong><script>code!</script>")) + end + + def test_markdown_should_not_sanitize_input_if_safe_option + assert_equal("<p>This is worded <strong>strongly</strong><script>code!</script></p>", markdown("This is worded <strong>strongly</strong><script>code!</script>", :safe)) + end + + def test_markdown_should_not_sanitize_safe_input + assert_equal("<p>This is worded <strong>strongly</strong><script>code!</script></p>", markdown("This is worded <strong>strongly</strong><script>code!</script>".html_safe)) + end + + def test_markdown_with_hard_breaks + assert_equal("<p>This is one scary world.</p>\n\n<p>True.</p>", markdown("This is one scary world.\n\nTrue.")) + end end end diff --git a/activemodel/CHANGELOG b/activemodel/CHANGELOG index 43cf67d2b7..a5e7f300d9 100644 --- a/activemodel/CHANGELOG +++ b/activemodel/CHANGELOG @@ -1,4 +1,4 @@ -*Rails 3.0.0 [beta 4/release candidate] (unreleased)* +*Rails 3.0.0 [beta 4] (June 8th, 2010)* * JSON supports a custom root option: to_json(:root => 'custom') #4515 [Jatinder Singh] diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index ffdfbfcaaf..90305978c4 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -1,5 +1,5 @@ require 'active_support/json' -require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/core_ext/class/attribute' module ActiveModel module Serializers @@ -10,7 +10,8 @@ module ActiveModel included do extend ActiveModel::Naming - cattr_accessor :include_root_in_json, :instance_writer => true + class_attribute :include_root_in_json + self.include_root_in_json = true end # Returns a JSON string representing the model. Some configuration is @@ -92,7 +93,9 @@ module ActiveModel end def from_json(json) - self.attributes = ActiveSupport::JSON.decode(json) + hash = ActiveSupport::JSON.decode(json) + hash = hash.values.first if include_root_in_json + self.attributes = hash self end end diff --git a/activemodel/lib/active_model/version.rb b/activemodel/lib/active_model/version.rb index aac47d5f81..c36fc8b1f7 100644 --- a/activemodel/lib/active_model/version.rb +++ b/activemodel/lib/active_model/version.rb @@ -3,7 +3,7 @@ module ActiveModel MAJOR = 3 MINOR = 0 TINY = 0 - BUILD = "beta3" + BUILD = "beta4" STRING = [MAJOR, MINOR, TINY, BUILD].join('.') end diff --git a/activemodel/test/cases/serializeration/json_serialization_test.rb b/activemodel/test/cases/serializeration/json_serialization_test.rb index 7e89815c96..04b50e5bb8 100644 --- a/activemodel/test/cases/serializeration/json_serialization_test.rb +++ b/activemodel/test/cases/serializeration/json_serialization_test.rb @@ -22,35 +22,41 @@ class JsonSerializationTest < ActiveModel::TestCase end test "should include root in json" do + json = @contact.to_json + + assert_match %r{^\{"contact":\{}, json + assert_match %r{"name":"Konata Izumi"}, json + assert_match %r{"age":16}, json + assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})) + assert_match %r{"awesome":true}, json + assert_match %r{"preferences":\{"shows":"anime"\}}, json + end + + test "should not include root in json" do begin - Contact.include_root_in_json = true + Contact.include_root_in_json = false json = @contact.to_json - assert_match %r{^\{"contact":\{}, json + assert_no_match %r{^\{"contact":\{}, json assert_match %r{"name":"Konata Izumi"}, json assert_match %r{"age":16}, json assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})) assert_match %r{"awesome":true}, json assert_match %r{"preferences":\{"shows":"anime"\}}, json ensure - Contact.include_root_in_json = false + Contact.include_root_in_json = true end end test "should include custom root in json" do - begin - Contact.include_root_in_json = true - json = @contact.to_json(:root => 'json_contact') + json = @contact.to_json(:root => 'json_contact') - assert_match %r{^\{"json_contact":\{}, json - assert_match %r{"name":"Konata Izumi"}, json - assert_match %r{"age":16}, json - assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})) - assert_match %r{"awesome":true}, json - assert_match %r{"preferences":\{"shows":"anime"\}}, json - ensure - Contact.include_root_in_json = false - end + assert_match %r{^\{"json_contact":\{}, json + assert_match %r{"name":"Konata Izumi"}, json + assert_match %r{"age":16}, json + assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})) + assert_match %r{"awesome":true}, json + assert_match %r{"preferences":\{"shows":"anime"\}}, json end test "should encode all encodable attributes" do diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index d8f68d7c28..348248e849 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,4 +1,4 @@ -*Rails 3.0.0 [beta 4/release candidate] (unreleased)* +*Rails 3.0.0 [beta 4] (June 8th, 2010)* * Add index length support for MySQL. #1852 [Emili Parreno, Pratik Naik] @@ -12,8 +12,6 @@ * find_or_create_by_attr(value, ...) works when attr is protected. #4457 [Santiago Pastorino, Marc-André Lafortune] -* New callbacks: after_commit and after_rollback. Do expensive operations like image thumbnailing after_commit instead of after_save. #2991 [Brian Durand] - * Serialized attributes are not converted to YAML if they are any of the formats that can be serialized to XML (like Hash, Array and Strings). [José Valim] * Destroy uses optimistic locking. If lock_version on the record you're destroying doesn't match lock_version in the database, a StaleObjectError is raised. #1966 [Curtis Hawthorne] diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 44c8fb83e1..5aea992801 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -23,6 +23,6 @@ Gem::Specification.new do |s| s.add_dependency('activesupport', version) s.add_dependency('activemodel', version) - s.add_dependency('arel', '~> 0.3.3') + s.add_dependency('arel', '~> 0.4.0') s.add_dependency('tzinfo', '~> 0.3.16') end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 284ae6695b..c1e16d08cb 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1756,7 +1756,8 @@ module ActiveRecord end def count_aliases_from_table_joins(name) - quoted_name = join_base.active_record.connection.quote_table_name(name.downcase) + # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase + quoted_name = join_base.active_record.connection.quote_table_name(name.downcase).downcase join_sql = join_base.table_joins.to_s.downcase join_sql.blank? ? 0 : # Table names @@ -1902,7 +1903,7 @@ module ActiveRecord end def ==(other) - other.is_a?(JoinBase) && + other.class == self.class && other.active_record == active_record && other.table_joins == table_joins end @@ -1973,7 +1974,7 @@ module ActiveRecord end def ==(other) - other.is_a?(JoinAssociation) && + other.class == self.class && other.reflection == reflection && other.parent == parent end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index b9fb452eee..0c87e052c4 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -122,8 +122,6 @@ module ActiveRecord requires_new = options[:requires_new] || !last_transaction_joinable transaction_open = false - @_current_transaction_records ||= [] - begin if block_given? if requires_new || open_transactions == 0 @@ -134,7 +132,6 @@ module ActiveRecord end increment_open_transactions transaction_open = true - @_current_transaction_records.push([]) end yield end @@ -144,10 +141,8 @@ module ActiveRecord decrement_open_transactions if open_transactions == 0 rollback_db_transaction - rollback_transaction_records(true) else rollback_to_savepoint - rollback_transaction_records(false) end end raise unless database_transaction_rollback.is_a?(ActiveRecord::Rollback) @@ -162,35 +157,20 @@ module ActiveRecord begin if open_transactions == 0 commit_db_transaction - commit_transaction_records else release_savepoint - save_point_records = @_current_transaction_records.pop - unless save_point_records.blank? - @_current_transaction_records.push([]) if @_current_transaction_records.empty? - @_current_transaction_records.last.concat(save_point_records) - end end rescue Exception => database_transaction_rollback if open_transactions == 0 rollback_db_transaction - rollback_transaction_records(true) else rollback_to_savepoint - rollback_transaction_records(false) end raise end end end - # Register a record with the current transaction so that its after_commit and after_rollback callbacks - # can be called. - def add_transaction_record(record) - last_batch = @_current_transaction_records.last - last_batch << record if last_batch - end - # Begins the transaction (and turns off auto-committing). def begin_db_transaction() end @@ -288,42 +268,6 @@ module ActiveRecord limit.to_i end end - - # Send a rollback message to all records after they have been rolled back. If rollback - # is false, only rollback records since the last save point. - def rollback_transaction_records(rollback) #:nodoc - if rollback - records = @_current_transaction_records.flatten - @_current_transaction_records.clear - else - records = @_current_transaction_records.pop - end - - unless records.blank? - records.uniq.each do |record| - begin - record.rolledback!(rollback) - rescue Exception => e - record.logger.error(e) if record.respond_to?(:logger) - end - end - end - end - - # Send a commit message to all records after they have been committed. - def commit_transaction_records #:nodoc - records = @_current_transaction_records.flatten - @_current_transaction_records.clear - unless records.blank? - records.uniq.each do |record| - begin - record.committed! - rescue Exception => e - record.logger.error(e) if record.respond_to?(:logger) - end - end - end - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index e8a45bb3c6..deb62e3802 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -34,6 +34,10 @@ module ActiveRecord end def binary_to_string(value) + if value.respond_to?(:force_encoding) && value.encoding != Encoding::ASCII_8BIT + value = value.force_encoding(Encoding::ASCII_8BIT) + end + value.gsub(/%00|%25/n) do |b| case b when "%00" then "\0" diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 4e62187449..99c914d7fc 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -356,13 +356,16 @@ module ActiveRecord end def references_eager_loaded_tables? - joined_tables = (tables_in_string(arel.joins(arel)) + [table.name, table.table_alias]).compact.uniq + # always convert table names to downcase as in Oracle quoted table names are in uppercase + joined_tables = (tables_in_string(arel.joins(arel)) + [table.name, table.table_alias]).compact.map(&:downcase).uniq (tables_in_string(to_sql) - joined_tables).any? end def tables_in_string(string) return [] if string.blank? - string.scan(/([a-zA-Z_][\.\w]+).?\./).flatten.uniq + # always convert table names to downcase as in Oracle quoted table names are in uppercase + # ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries + string.scan(/([a-zA-Z_][\.\w]+).?\./).flatten.map(&:downcase).uniq - ['raw_sql_'] end end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 5a8e2ce880..3f2c1911e7 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -8,11 +8,6 @@ module ActiveRecord class TransactionError < ActiveRecordError # :nodoc: end - included do - define_model_callbacks :commit, :commit_on_update, :commit_on_create, :commit_on_destroy, :only => :after - define_model_callbacks :rollback, :rollback_on_update, :rollback_on_create, :rollback_on_destroy - end - # Transactions are protective blocks where SQL statements are only permanent # if they can all succeed as one atomic action. The classic example is a # transfer between two accounts where you can only have a deposit if the @@ -163,21 +158,6 @@ module ActiveRecord # http://dev.mysql.com/doc/refman/5.0/en/savepoints.html # for more information about savepoints. # - # === Callbacks - # - # There are two types of callbacks associated with committing and rolling back transactions: - # +after_commit+ and +after_rollback+. - # - # +after_commit+ callbacks are called on every record saved or destroyed within a - # transaction immediately after the transaction is committed. +after_rollback+ callbacks - # are called on every record saved or destroyed within a transaction immediately after the - # transaction or savepoint is rolled back. - # - # These callbacks are useful for interacting with other systems since you will be guaranteed - # that the callback is only executed when the database is in a permanent state. For example, - # +after_commit+ is a good spot to put in a hook to clearing a cache since clearing it from - # within a transaction could trigger the cache to be regenerated before the database is updated. - # # === Caveats # # If you're on MySQL, then do not use DDL operations in nested transactions @@ -225,50 +205,19 @@ module ActiveRecord # Reset id and @new_record if the transaction rolls back. def rollback_active_record_state! - remember_transaction_record_state + id_present = has_attribute?(self.class.primary_key) + previous_id = id + previous_new_record = new_record? yield rescue Exception - restore_transaction_record_state - raise - ensure - clear_transaction_record_state - end - - # Call the after_commit callbacks - def committed! #:nodoc: - if transaction_record_state(:new_record) - _run_commit_on_create_callbacks - elsif transaction_record_state(:destroyed) - _run_commit_on_destroy_callbacks + @new_record = previous_new_record + if id_present + self.id = previous_id else - _run_commit_on_update_callbacks - end - _run_commit_callbacks - ensure - clear_transaction_record_state - end - - # Call the after rollback callbacks. The restore_state argument indicates if the record - # state should be rolled back to the beginning or just to the last savepoint. - def rolledback!(force_restore_state = false) #:nodoc: - if transaction_record_state(:new_record) - _run_rollback_on_create_callbacks - elsif transaction_record_state(:destroyed) - _run_rollback_on_destroy_callbacks - else - _run_rollback_on_update_callbacks - end - _run_rollback_callbacks - ensure - restore_transaction_record_state(force_restore_state) - end - - # Add the record to the current transaction so that the :after_rollback and :after_commit callbacks - # can be called. - def add_to_transaction - if self.class.connection.add_transaction_record(self) - remember_transaction_record_state + @attributes.delete(self.class.primary_key) + @attributes_cache.delete(self.class.primary_key) end + raise end # Executes +method+ within a transaction and captures its return value as a @@ -280,59 +229,10 @@ module ActiveRecord def with_transaction_returning_status status = nil self.class.transaction do - add_to_transaction status = yield raise ActiveRecord::Rollback unless status end status end - - protected - - # Save the new record state and id of a record so it can be restored later if a transaction fails. - def remember_transaction_record_state #:nodoc - @_start_transaction_state ||= {} - unless @_start_transaction_state.include?(:new_record) - @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key) - @_start_transaction_state[:new_record] = @new_record - end - unless @_start_transaction_state.include?(:destroyed) - @_start_transaction_state[:destroyed] = @new_record - end - @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 - end - - # Clear the new record state and id of a record. - def clear_transaction_record_state #:nodoc - if defined?(@_start_transaction_state) - @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 - remove_instance_variable(:@_start_transaction_state) if @_start_transaction_state[:level] < 1 - end - end - - # Restore the new record state and id of a record that was previously saved by a call to save_record_state. - def restore_transaction_record_state(force = false) #:nodoc - if defined?(@_start_transaction_state) - @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 - if @_start_transaction_state[:level] < 1 - restore_state = remove_instance_variable(:@_start_transaction_state) - if restore_state - @new_record = restore_state[:new_record] - @destroyed = restore_state[:destroyed] - if restore_state[:id] - self.id = restore_state[:id] - else - @attributes.delete(self.class.primary_key) - @attributes_cache.delete(self.class.primary_key) - end - end - end - end - end - - # Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed. - def transaction_record_state(state) #:nodoc - @_start_transaction_state[state] if defined?(@_start_transaction_state) - end end end diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb index bf6b995a37..d18fed0131 100644 --- a/activerecord/lib/active_record/version.rb +++ b/activerecord/lib/active_record/version.rb @@ -3,7 +3,7 @@ module ActiveRecord MAJOR = 3 MINOR = 0 TINY = 0 - BUILD = "beta3" + BUILD = "beta4" STRING = [MAJOR, MINOR, TINY, BUILD].join('.') end diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 9b28766405..fc08c2178a 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -145,22 +145,13 @@ class AdapterTest < ActiveRecord::TestCase def test_add_limit_offset_should_sanitize_sql_injection_for_limit_without_comas sql_inject = "1 select * from schema" - assert_equal " LIMIT 1", @connection.add_limit_offset!("", :limit => sql_inject) - if current_adapter?(:MysqlAdapter) - assert_equal " LIMIT 7, 1", @connection.add_limit_offset!("", :limit => sql_inject, :offset => 7) - else - assert_equal " LIMIT 1 OFFSET 7", @connection.add_limit_offset!("", :limit => sql_inject, :offset => 7) - end + assert_no_match(/schema/, @connection.add_limit_offset!("", :limit=>sql_inject)) + assert_no_match(/schema/, @connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7)) end def test_add_limit_offset_should_sanitize_sql_injection_for_limit_with_comas sql_inject = "1, 7 procedure help()" - if current_adapter?(:MysqlAdapter) - assert_equal " LIMIT 1,7", @connection.add_limit_offset!("", :limit => sql_inject) - assert_equal " LIMIT 7, 1", @connection.add_limit_offset!("", :limit => '1 ; DROP TABLE USERS', :offset => 7) - else - assert_equal " LIMIT 1,7", @connection.add_limit_offset!("", :limit => sql_inject) - assert_equal " LIMIT 1,7 OFFSET 7", @connection.add_limit_offset!("", :limit => sql_inject, :offset => 7) - end + assert_no_match(/procedure/, @connection.add_limit_offset!("", :limit=>sql_inject)) + assert_no_match(/procedure/, @connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7)) end end diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 9258c987ef..fb1e6e7e70 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -37,6 +37,10 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase if current_adapter?(:MysqlAdapter) assert_no_match(/`firm_with_primary_keys_companies`\.`id`/, sql) assert_match(/`firm_with_primary_keys_companies`\.`name`/, sql) + elsif current_adapter?(:OracleAdapter) + # on Oracle aliases are truncated to 30 characters and are quoted in uppercase + assert_no_match(/"firm_with_primary_keys_compani"\."id"/i, sql) + assert_match(/"firm_with_primary_keys_compani"\."name"/i, sql) else assert_no_match(/"firm_with_primary_keys_companies"\."id"/, sql) assert_match(/"firm_with_primary_keys_companies"\."name"/, sql) diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index fe558f9d3b..7a4a33d177 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -7,9 +7,10 @@ require 'models/categorization' require 'models/company' require 'models/topic' require 'models/reply' +require 'models/person' class CascadedEagerLoadingTest < ActiveRecord::TestCase - fixtures :authors, :mixins, :companies, :posts, :topics, :accounts, :comments, :categorizations + fixtures :authors, :mixins, :companies, :posts, :topics, :accounts, :comments, :categorizations, :people def test_eager_association_loading_with_cascaded_two_levels authors = Author.find(:all, :include=>{:posts=>:comments}, :order=>"authors.id") @@ -38,6 +39,13 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase assert_equal 9, assert_no_queries { authors[0].comments.size } end + def test_eager_association_loading_grafts_stashed_associations_to_correct_parent + assert_nothing_raised do + Person.eager_load(:primary_contact => :primary_contact).where('primary_contacts_people_2.first_name = ?', 'Susan').all + end + assert_equal people(:michael), Person.eager_load(:primary_contact => :primary_contact).where('primary_contacts_people_2.first_name = ?', 'Susan').first + end + def test_eager_association_loading_with_cascaded_two_levels_with_two_has_many_associations authors = Author.find(:all, :include=>{:posts=>[:comments, :categorizations]}, :order=>"authors.id") assert_equal 2, authors.size diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 8e5bc56008..45c7498013 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -511,7 +511,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_deleting_updates_counter_cache - topic = Topic.first + topic = Topic.first(:order => "id ASC") assert_equal topic.replies.to_a.size, topic.replies_count topic.replies.delete(topic.replies.first) diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 1d883f7ea8..36c572b5e7 100755 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -430,14 +430,14 @@ class BasicsTest < ActiveRecord::TestCase end def test_preserving_date_objects - if current_adapter?(:SybaseAdapter, :OracleAdapter) + if current_adapter?(:SybaseAdapter) # Sybase ctlib does not (yet?) support the date type; use datetime instead. - # Oracle treats all dates/times as Time. assert_kind_of( Time, Topic.find(1).last_read, "The last_read attribute should be of the Time class" ) else + # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb) assert_kind_of( Date, Topic.find(1).last_read, "The last_read attribute should be of the Date class" @@ -2125,10 +2125,11 @@ class BasicsTest < ActiveRecord::TestCase assert_equal "integer", xml.elements["//parent-id"].attributes['type'] assert_equal "true", xml.elements["//parent-id"].attributes['nil'] - if current_adapter?(:SybaseAdapter, :OracleAdapter) + if current_adapter?(:SybaseAdapter) assert_equal last_read_in_current_timezone, xml.elements["//last-read"].text assert_equal "datetime" , xml.elements["//last-read"].attributes['type'] else + # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb) assert_equal "2004-04-15", xml.elements["//last-read"].text assert_equal "date" , xml.elements["//last-read"].attributes['type'] end diff --git a/activerecord/test/cases/date_time_test.rb b/activerecord/test/cases/date_time_test.rb index 36e1caa0b6..a8b4b7a096 100644 --- a/activerecord/test/cases/date_time_test.rb +++ b/activerecord/test/cases/date_time_test.rb @@ -5,7 +5,9 @@ require 'models/task' class DateTimeTest < ActiveRecord::TestCase def test_saves_both_date_and_time time_values = [1807, 2, 10, 15, 30, 45] - now = DateTime.civil(*time_values) + # create DateTime value with local time zone offset + local_offset = Rational(Time.local_time(*time_values).utc_offset, 86400) + now = DateTime.civil(*(time_values + [local_offset])) task = Task.new task.starting = now diff --git a/activerecord/test/cases/json_serialization_test.rb b/activerecord/test/cases/json_serialization_test.rb index a3145d2c04..c275557da8 100644 --- a/activerecord/test/cases/json_serialization_test.rb +++ b/activerecord/test/cases/json_serialization_test.rb @@ -8,7 +8,7 @@ require 'models/comment' class JsonSerializationTest < ActiveRecord::TestCase class NamespacedContact < Contact - column :name, :string + column :name, :string end def setup @@ -23,16 +23,12 @@ class JsonSerializationTest < ActiveRecord::TestCase end def test_should_demodulize_root_in_json - NamespacedContact.include_root_in_json = true @contact = NamespacedContact.new :name => 'whatever' json = @contact.to_json assert_match %r{^\{"namespaced_contact":\{}, json - ensure - NamespacedContact.include_root_in_json = false end def test_should_include_root_in_json - Contact.include_root_in_json = true json = @contact.to_json assert_match %r{^\{"contact":\{}, json @@ -41,8 +37,6 @@ class JsonSerializationTest < ActiveRecord::TestCase assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})) assert_match %r{"awesome":true}, json assert_match %r{"preferences":\{"shows":"anime"\}}, json - ensure - Contact.include_root_in_json = false end def test_should_encode_all_encodable_attributes @@ -170,15 +164,19 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase end def test_should_allow_only_option_for_list_of_authors + ActiveRecord::Base.include_root_in_json = false authors = [@david, @mary] - assert_equal %([{"name":"David"},{"name":"Mary"}]), ActiveSupport::JSON.encode(authors, :only => :name) + ensure + ActiveRecord::Base.include_root_in_json = true end def test_should_allow_except_option_for_list_of_authors + ActiveRecord::Base.include_root_in_json = false authors = [@david, @mary] - assert_equal %([{"id":1},{"id":2}]), ActiveSupport::JSON.encode(authors, :except => [:name, :author_address_id, :author_address_extra_id]) + ensure + ActiveRecord::Base.include_root_in_json = true end def test_should_allow_includes_for_list_of_authors @@ -201,7 +199,6 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase 1 => @david, 2 => @mary } - - assert_equal %({"1":{"name":"David"}}), ActiveSupport::JSON.encode(authors_hash, :only => [1, :name]) + assert_equal %({"1":{"author":{"name":"David"}}}), ActiveSupport::JSON.encode(authors_hash, :only => [1, :name]) end end diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index 6ba84fa57b..1f544b4211 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -27,7 +27,7 @@ class LogSubscriberTest < ActiveSupport::TestCase wait assert_equal 1, @logger.logged(:debug).size assert_match(/Developer Load/, @logger.logged(:debug).last) - assert_match(/SELECT .*?FROM .?developers.?/, @logger.logged(:debug).last) + assert_match(/SELECT .*?FROM .?developers.?/i, @logger.logged(:debug).last) end def test_cached_queries @@ -38,6 +38,6 @@ class LogSubscriberTest < ActiveSupport::TestCase wait assert_equal 2, @logger.logged(:debug).size assert_match(/CACHE/, @logger.logged(:debug).last) - assert_match(/SELECT .*?FROM .?developers.?/, @logger.logged(:debug).last) + assert_match(/SELECT .*?FROM .?developers.?/i, @logger.logged(:debug).last) end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index b5fa258f7b..1edec66c25 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -30,13 +30,14 @@ if ActiveRecord::Base.connection.supports_migrations? conn = ActiveRecord::Base.connection conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name) if conn.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name) - ActiveRecord::Base.table_name_prefix = 'foo_' - ActiveRecord::Base.table_name_suffix = '_bar' + # Use shorter prefix and suffix as in Oracle database identifier cannot be larger than 30 characters + ActiveRecord::Base.table_name_prefix = 'p_' + ActiveRecord::Base.table_name_suffix = '_s' conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name) if conn.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name) conn.initialize_schema_migrations_table - assert_equal "foo_unique_schema_migrations_bar", conn.indexes(ActiveRecord::Migrator.schema_migrations_table_name)[0][:name] + assert_equal "p_unique_schema_migrations_s", conn.indexes(ActiveRecord::Migrator.schema_migrations_table_name)[0][:name] ensure ActiveRecord::Base.table_name_prefix = "" ActiveRecord::Base.table_name_suffix = "" @@ -83,13 +84,17 @@ if ActiveRecord::Base.connection.supports_migrations? # Orcl nds shrt indx nms. Sybs 2. # OpenBase does not have named indexes. You must specify a single column name - unless current_adapter?(:OracleAdapter, :SybaseAdapter, :OpenBaseAdapter) + unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"]) } assert_nothing_raised { Person.connection.remove_index("people", :column => ["last_name", "first_name"]) } - assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"]) } - assert_nothing_raised { Person.connection.remove_index("people", :name => "index_people_on_last_name_and_first_name") } - assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"]) } - assert_nothing_raised { Person.connection.remove_index("people", "last_name_and_first_name") } + # Oracle adapter cannot have specified index name larger than 30 characters + # Oracle adapter is shortening index name when just column list is given + unless current_adapter?(:OracleAdapter) + assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"]) } + assert_nothing_raised { Person.connection.remove_index("people", :name => "index_people_on_last_name_and_first_name") } + assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"]) } + assert_nothing_raised { Person.connection.remove_index("people", "last_name_and_first_name") } + end assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"]) } assert_nothing_raised { Person.connection.remove_index("people", ["last_name", "first_name"]) } assert_nothing_raised { Person.connection.add_index("people", ["last_name"], :length => 10) } @@ -736,13 +741,7 @@ if ActiveRecord::Base.connection.supports_migrations? table.column :hat_size, :integer table.column :hat_style, :string, :limit => 100 end - # Oracle index names should be 30 or less characters - if current_adapter?(:OracleAdapter) - ActiveRecord::Base.connection.add_index "hats", ["hat_style", "hat_size"], :unique => true, - :name => 'index_hats_on_hat_style_size' - else - ActiveRecord::Base.connection.add_index "hats", ["hat_style", "hat_size"], :unique => true - end + ActiveRecord::Base.connection.add_index "hats", ["hat_style", "hat_size"], :unique => true assert_nothing_raised { Person.connection.remove_column("hats", "hat_size") } ensure @@ -1375,8 +1374,8 @@ if ActiveRecord::Base.connection.supports_migrations? return unless current_adapter? :OracleAdapter # table name is 29 chars, the standard sequence name will - # be 33 chars and fail - assert_raise(ActiveRecord::StatementInvalid) do + # be 33 chars and should be shortened + assert_nothing_raised do begin Person.connection.create_table :table_with_name_thats_just_ok do |t| t.column :foo, :string, :null => false diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 4097c5119e..43519db976 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -114,7 +114,7 @@ class RelationTest < ActiveRecord::TestCase def test_select_with_block even_ids = Developer.scoped.select {|d| d.id % 2 == 0 }.map(&:id) - assert_equal [2, 4, 6, 8, 10], even_ids + assert_equal [2, 4, 6, 8, 10], even_ids.sort end def test_finding_with_hash_conditions_on_joined_table diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb deleted file mode 100644 index ebc16653cb..0000000000 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ /dev/null @@ -1,240 +0,0 @@ -require "cases/helper" -require 'models/topic' -require 'models/reply' - -class TransactionCallbacksTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false - fixtures :topics - - class TopicWithCallbacks < ActiveRecord::Base - set_table_name :topics - - after_commit{|record| record.send(:do_after_commit, nil)} - after_commit(:on => :create){|record| record.send(:do_after_commit, :create)} - after_commit(:on => :update){|record| record.send(:do_after_commit, :update)} - after_commit(:on => :destroy){|record| record.send(:do_after_commit, :destroy)} - after_rollback{|record| record.send(:do_after_rollback, nil)} - after_rollback(:on => :create){|record| record.send(:do_after_rollback, :create)} - after_rollback(:on => :update){|record| record.send(:do_after_rollback, :update)} - after_rollback(:on => :destroy){|record| record.send(:do_after_rollback, :destroy)} - - def history - @history ||= [] - end - - def after_commit_block(on = nil, &block) - @after_commit ||= {} - @after_commit[on] ||= [] - @after_commit[on] << block - end - - def after_rollback_block(on = nil, &block) - @after_rollback ||= {} - @after_rollback[on] ||= [] - @after_rollback[on] << block - end - - def do_after_commit(on) - blocks = @after_commit[on] if defined?(@after_commit) - blocks.each{|b| b.call(self)} if blocks - end - - def do_after_rollback(on) - blocks = @after_rollback[on] if defined?(@after_rollback) - blocks.each{|b| b.call(self)} if blocks - end - end - - def setup - @first, @second = TopicWithCallbacks.find(1, 3).sort_by { |t| t.id } - end - - def test_call_after_commit_after_transaction_commits - @first.after_commit_block{|r| r.history << :after_commit} - @first.after_rollback_block{|r| r.history << :after_rollback} - - @first.save! - assert_equal [:after_commit], @first.history - end - - def test_only_call_after_commit_on_update_after_transaction_commits_for_existing_record - @first.after_commit_block(:create){|r| r.history << :commit_on_create} - @first.after_commit_block(:update){|r| r.history << :commit_on_update} - @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} - @first.after_rollback_block(:create){|r| r.history << :rollback_on_create} - @first.after_rollback_block(:update){|r| r.history << :rollback_on_update} - @first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy} - - @first.save! - assert_equal [:commit_on_update], @first.history - end - - def test_only_call_after_commit_on_destroy_after_transaction_commits_for_destroyed_record - @first.after_commit_block(:create){|r| r.history << :commit_on_create} - @first.after_commit_block(:update){|r| r.history << :commit_on_update} - @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} - @first.after_rollback_block(:create){|r| r.history << :rollback_on_create} - @first.after_rollback_block(:update){|r| r.history << :rollback_on_update} - @first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy} - - @first.destroy - assert_equal [:commit_on_destroy], @first.history - end - - def test_only_call_after_commit_on_create_after_transaction_commits_for_new_record - @new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today) - @new_record.after_commit_block(:create){|r| r.history << :commit_on_create} - @new_record.after_commit_block(:update){|r| r.history << :commit_on_update} - @new_record.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} - @new_record.after_rollback_block(:create){|r| r.history << :rollback_on_create} - @new_record.after_rollback_block(:update){|r| r.history << :rollback_on_update} - @new_record.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy} - - @new_record.save! - assert_equal [:commit_on_create], @new_record.history - end - - def test_call_after_rollback_after_transaction_rollsback - @first.after_commit_block{|r| r.history << :after_commit} - @first.after_rollback_block{|r| r.history << :after_rollback} - - Topic.transaction do - @first.save! - raise ActiveRecord::Rollback - end - - assert_equal [:after_rollback], @first.history - end - - def test_only_call_after_rollback_on_update_after_transaction_rollsback_for_existing_record - @first.after_commit_block(:create){|r| r.history << :commit_on_create} - @first.after_commit_block(:update){|r| r.history << :commit_on_update} - @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} - @first.after_rollback_block(:create){|r| r.history << :rollback_on_create} - @first.after_rollback_block(:update){|r| r.history << :rollback_on_update} - @first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy} - - Topic.transaction do - @first.save! - raise ActiveRecord::Rollback - end - - assert_equal [:rollback_on_update], @first.history - end - - def test_only_call_after_rollback_on_destroy_after_transaction_rollsback_for_destroyed_record - @first.after_commit_block(:create){|r| r.history << :commit_on_create} - @first.after_commit_block(:update){|r| r.history << :commit_on_update} - @first.after_commit_block(:destroy){|r| r.history << :commit_on_update} - @first.after_rollback_block(:create){|r| r.history << :rollback_on_create} - @first.after_rollback_block(:update){|r| r.history << :rollback_on_update} - @first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy} - - Topic.transaction do - @first.destroy - raise ActiveRecord::Rollback - end - - assert_equal [:rollback_on_destroy], @first.history - end - - def test_only_call_after_rollback_on_create_after_transaction_rollsback_for_new_record - @new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today) - @new_record.after_commit_block(:create){|r| r.history << :commit_on_create} - @new_record.after_commit_block(:update){|r| r.history << :commit_on_update} - @new_record.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} - @new_record.after_rollback_block(:create){|r| r.history << :rollback_on_create} - @new_record.after_rollback_block(:update){|r| r.history << :rollback_on_update} - @new_record.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy} - - Topic.transaction do - @new_record.save! - raise ActiveRecord::Rollback - end - - assert_equal [:rollback_on_create], @new_record.history - end - - def test_call_after_rollback_when_commit_fails - @first.connection.class.send(:alias_method, :real_method_commit_db_transaction, :commit_db_transaction) - begin - @first.connection.class.class_eval do - def commit_db_transaction; raise "boom!"; end - end - - @first.after_commit_block{|r| r.history << :after_commit} - @first.after_rollback_block{|r| r.history << :after_rollback} - - assert !@first.save rescue nil - assert_equal [:after_rollback], @first.history - ensure - @first.connection.class.send(:remove_method, :commit_db_transaction) - @first.connection.class.send(:alias_method, :commit_db_transaction, :real_method_commit_db_transaction) - end - end - - def test_only_call_after_rollback_on_records_rolled_back_to_a_savepoint - def @first.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end - def @first.commits(i=0); @commits ||= 0; @commits += i if i; end - @first.after_rollback_block{|r| r.rollbacks(1)} - @first.after_commit_block{|r| r.commits(1)} - - def @second.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end - def @second.commits(i=0); @commits ||= 0; @commits += i if i; end - @second.after_rollback_block{|r| r.rollbacks(1)} - @second.after_commit_block{|r| r.commits(1)} - - Topic.transaction do - @first.save! - Topic.transaction(:requires_new => true) do - @second.save! - raise ActiveRecord::Rollback - end - end - - assert_equal 1, @first.commits - assert_equal 0, @first.rollbacks - assert_equal 0, @second.commits - assert_equal 1, @second.rollbacks - end - - def test_only_call_after_rollback_on_records_rolled_back_to_a_savepoint_when_release_savepoint_fails - def @first.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end - def @first.commits(i=0); @commits ||= 0; @commits += i if i; end - - @first.after_rollback_block{|r| r.rollbacks(1)} - @first.after_commit_block{|r| r.commits(1)} - - Topic.transaction do - @first.save - Topic.transaction(:requires_new => true) do - @first.save! - raise ActiveRecord::Rollback - end - Topic.transaction(:requires_new => true) do - @first.save! - raise ActiveRecord::Rollback - end - end - - assert_equal 1, @first.commits - assert_equal 2, @first.rollbacks - end - - def test_after_transaction_callbacks_should_not_raise_errors - def @first.last_after_transaction_error=(e); @last_transaction_error = e; end - def @first.last_after_transaction_error; @last_transaction_error; end - @first.after_commit_block{|r| r.last_after_transaction_error = :commit; raise "fail!";} - @first.after_rollback_block{|r| r.last_after_transaction_error = :rollback; raise "fail!";} - - @first.save! - assert_equal :commit, @first.last_after_transaction_error - - Topic.transaction do - @first.save! - raise ActiveRecord::Rollback - end - - assert_equal :rollback, @first.last_after_transaction_error - end -end diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 958a4e4f94..00f3b527d7 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -320,33 +320,6 @@ class TransactionTest < ActiveRecord::TestCase end end - def test_restore_active_record_state_for_all_records_in_a_transaction - topic_1 = Topic.new(:title => 'test_1') - topic_2 = Topic.new(:title => 'test_2') - Topic.transaction do - assert topic_1.save - assert topic_2.save - @first.save - @second.destroy - assert_equal false, topic_1.new_record? - assert_not_nil topic_1.id - assert_equal false, topic_2.new_record? - assert_not_nil topic_2.id - assert_equal false, @first.new_record? - assert_not_nil @first.id - assert_equal true, @second.destroyed? - raise ActiveRecord::Rollback - end - - assert_equal true, topic_1.new_record? - assert_nil topic_1.id - assert_equal true, topic_2.new_record? - assert_nil topic_2.id - assert_equal false, @first.new_record? - assert_not_nil @first.id - assert_equal false, @second.destroyed? - end - if current_adapter?(:PostgreSQLAdapter) && defined?(PGconn::PQTRANS_IDLE) def test_outside_transaction_works assert Topic.connection.outside_transaction? @@ -409,12 +382,6 @@ class TransactionTest < ActiveRecord::TestCase end private - def define_callback_method(callback_method) - define_method(callback_method) do - self.history << [callback_method, :method] - end - end - def add_exception_raising_after_save_callback_to_topic Topic.class_eval <<-eoruby, __FILE__, __LINE__ + 1 remove_method(:after_save_for_transaction) diff --git a/activerecord/test/connections/native_oracle/connection.rb b/activerecord/test/connections/native_oracle/connection.rb index c8183dc0fb..bb4040058f 100644 --- a/activerecord/test/connections/native_oracle/connection.rb +++ b/activerecord/test/connections/native_oracle/connection.rb @@ -53,7 +53,7 @@ Course.establish_connection 'arunit2' # for assert_queries test helper ActiveRecord::Base.connection.class.class_eval do - IGNORED_SELECT_SQL = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^\s*select .* from all_tab_columns/im] + IGNORED_SELECT_SQL = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from ((all|user)_tab_columns|(all|user)_triggers|(all|user)_constraints)/im] def select_with_query_record(sql, name = nil, return_column_names = false) $queries_executed ||= [] diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index 91fc7c9416..617f01b47d 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -43,6 +43,12 @@ class Topic < ActiveRecord::Base before_create :default_written_on before_destroy :destroy_children + # Explicitly define as :date column so that returned Oracle DATE values would be typecasted to Date and not Time. + # Some tests depend on assumption that this attribute will have Date values. + if current_adapter?(:OracleEnhancedAdapter) + set_date_columns :last_read + end + def parent Topic.find(parent_id) end diff --git a/activeresource/CHANGELOG b/activeresource/CHANGELOG index bd97b2d549..2b2092b9fe 100644 --- a/activeresource/CHANGELOG +++ b/activeresource/CHANGELOG @@ -1,4 +1,4 @@ -*Rails 3.0.0 [beta 4/release candidate] (unreleased)* +*Rails 3.0.0 [beta 4] (June 8th, 2010)* * JSON: set Base.include_root_in_json = true to include a root value in the JSON: {"post": {"title": ...}}. Mirrors the Active Record option. [Santiago Pastorino] diff --git a/activeresource/lib/active_resource/version.rb b/activeresource/lib/active_resource/version.rb index 6b9b8d3a9d..198e77a3d1 100644 --- a/activeresource/lib/active_resource/version.rb +++ b/activeresource/lib/active_resource/version.rb @@ -3,7 +3,7 @@ module ActiveResource MAJOR = 3 MINOR = 0 TINY = 0 - BUILD = "beta3" + BUILD = "beta4" STRING = [MAJOR, MINOR, TINY, BUILD].join('.') end diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG index 0e24cc138a..6566d3fa06 100644 --- a/activesupport/CHANGELOG +++ b/activesupport/CHANGELOG @@ -1,4 +1,4 @@ -*Rails 3.0.0 [beta 4/release candidate] (unreleased)* +*Rails 3.0.0 [beta 4] (June 8th, 2010)* * Extracted String#truncate from TextHelper#truncate [DHH] diff --git a/activesupport/lib/active_support/core_ext/date_time/conversions.rb b/activesupport/lib/active_support/core_ext/date_time/conversions.rb index 24168c7825..c3f0acce25 100644 --- a/activesupport/lib/active_support/core_ext/date_time/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date_time/conversions.rb @@ -60,7 +60,7 @@ class DateTime # Converts self to a Ruby Date object; time portion is discarded def to_date ::Date.new(year, month, day) - end unless method_defined?(:to_date) + end unless instance_methods(false).include?(:to_date) # Attempts to convert self to a Ruby Time object; returns self if out of range of Ruby Time class # If self has an offset other than 0, self will just be returned unaltered, since there's no clean way to map it to a Time @@ -71,7 +71,7 @@ class DateTime # To be able to keep Times, Dates and DateTimes interchangeable on conversions def to_datetime self - end unless method_defined?(:to_datetime) + end unless instance_methods(false).include?(:to_datetime) def self.civil_from_format(utc_or_local, year, month=1, day=1, hour=0, min=0, sec=0) offset = utc_or_local.to_sym == :local ? local_offset : 0 @@ -81,7 +81,7 @@ class DateTime # Converts datetime to an appropriate format for use in XML def xmlschema strftime("%Y-%m-%dT%H:%M:%S%Z") - end unless method_defined?(:xmlschema) + end unless instance_methods(false).include?(:xmlschema) # Converts self to a floating-point number of seconds since the Unix epoch def to_f diff --git a/activesupport/lib/active_support/core_ext/file.rb b/activesupport/lib/active_support/core_ext/file.rb index dc24afbe7f..a763447566 100644 --- a/activesupport/lib/active_support/core_ext/file.rb +++ b/activesupport/lib/active_support/core_ext/file.rb @@ -1 +1,2 @@ require 'active_support/core_ext/file/atomic' +require 'active_support/core_ext/file/path' diff --git a/activesupport/lib/active_support/core_ext/load_error.rb b/activesupport/lib/active_support/core_ext/load_error.rb index 615ebe9588..8bdfa0c5bc 100644 --- a/activesupport/lib/active_support/core_ext/load_error.rb +++ b/activesupport/lib/active_support/core_ext/load_error.rb @@ -3,6 +3,7 @@ class LoadError /^no such file to load -- (.+)$/i, /^Missing \w+ (?:file\s*)?([^\s]+.rb)$/i, /^Missing API definition file in (.+)$/i, + /^cannot load such file -- (.+)$/i, ] def path diff --git a/activesupport/lib/active_support/core_ext/string.rb b/activesupport/lib/active_support/core_ext/string.rb index 411ea0f016..d8d1f9436e 100644 --- a/activesupport/lib/active_support/core_ext/string.rb +++ b/activesupport/lib/active_support/core_ext/string.rb @@ -9,3 +9,4 @@ require 'active_support/core_ext/string/behavior' require 'active_support/core_ext/string/interpolation' require 'active_support/core_ext/string/output_safety' require 'active_support/core_ext/string/exclude' +require 'active_support/core_ext/string/encoding'
\ No newline at end of file diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index 429b65bf15..1eb6e8748b 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -45,6 +45,8 @@ module ActiveSupport #:nodoc: @wrapped_string = string @wrapped_string.force_encoding(Encoding::UTF_8) unless @wrapped_string.frozen? end + + undef <=> else def initialize(string) #:nodoc: @wrapped_string = string diff --git a/activesupport/lib/active_support/version.rb b/activesupport/lib/active_support/version.rb index 538a8b87c6..52612c27cb 100644 --- a/activesupport/lib/active_support/version.rb +++ b/activesupport/lib/active_support/version.rb @@ -3,7 +3,7 @@ module ActiveSupport MAJOR = 3 MINOR = 0 TINY = 0 - BUILD = "beta3" + BUILD = "beta4" STRING = [MAJOR, MINOR, TINY, BUILD].join('.') end diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb index ea6ebca3ce..dd84860a91 100644 --- a/activesupport/test/abstract_unit.rb +++ b/activesupport/test/abstract_unit.rb @@ -18,7 +18,7 @@ ENV['NO_RELOAD'] = '1' require 'active_support' # Include shims until we get off 1.8.6 -require 'active_support/ruby/shim' +require 'active_support/ruby/shim' if RUBY_VERSION < '1.8.7' def uses_memcached(test_name) require 'memcache' diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb index eba8170cda..19d7935211 100644 --- a/activesupport/test/core_ext/date_time_ext_test.rb +++ b/activesupport/test/core_ext/date_time_ext_test.rb @@ -26,11 +26,11 @@ class DateTimeExtCalculationsTest < Test::Unit::TestCase end def test_to_date - assert_equal Date.new(2005, 2, 21), DateTime.new(2005, 2, 21).to_date + assert_equal Date.new(2005, 2, 21), DateTime.new(2005, 2, 21, 14, 30, 0).to_date end def test_to_datetime - assert_equal DateTime.new(2005, 2, 21), DateTime.new(2005, 2, 21).to_datetime + assert_equal DateTime.new(2005, 2, 21, 14, 30, 0), DateTime.new(2005, 2, 21, 14, 30, 0).to_datetime end def test_to_time diff --git a/railties/CHANGELOG b/railties/CHANGELOG index fe3ab3cdcf..61924745c8 100644 --- a/railties/CHANGELOG +++ b/railties/CHANGELOG @@ -1,4 +1,4 @@ -*Rails 3.0.0 [beta 4/release candidate] (unreleased)* +*Rails 3.0.0 [beta 4] (June 8th, 2010)* * Version bump * Removed Rails Metal [YK & JV]. diff --git a/railties/guides/source/3_0_release_notes.textile b/railties/guides/source/3_0_release_notes.textile index 44ada7f5a4..41ea5d5822 100644 --- a/railties/guides/source/3_0_release_notes.textile +++ b/railties/guides/source/3_0_release_notes.textile @@ -565,6 +565,7 @@ Action Mailer has been given a new API with TMail being replaced out with the ne * All mailers are now in <tt>app/mailers</tt> by default. * Can now send email using new API with three methods: +attachments+, +headers+ and +mail+. +* ActionMailer now has native support for inline attachments using the <tt>attachments.inline</tt> method. * Action Mailer emailing methods now return <tt>Mail::Message</tt> objects, which can then be sent the +deliver+ message to send itself. * All delivery methods are now abstracted out to the Mail gem. * The mail delivery method can accept a hash of all valid mail header fields with their value pair. diff --git a/railties/guides/source/action_mailer_basics.textile b/railties/guides/source/action_mailer_basics.textile index a2a748c749..8eb48e2751 100644 --- a/railties/guides/source/action_mailer_basics.textile +++ b/railties/guides/source/action_mailer_basics.textile @@ -211,6 +211,37 @@ attachments['filename.jpg'] = {:mime_type => 'application/x-gzip', NOTE: If you specify an encoding, Mail will assume that your content is already encoded and not try to Base64 encode it. +h5. Making Inline Attachments + +Inline attachments are now possible in ActionMailer. While previously in the pre 3.0 version of Rails, you could do inline attachments, it involved a lot of hacking and determination to pull it off. + +ActionMailer now makes inline attachments as trivial as they should be. + +* Firstly, to tell Mail to turn an attachment into an inline attachment, you just call <tt>#inline</tt> on the attachments method within your Mailer: + +<ruby> +def welcome + attachments.inline['image.jpg'] = File.read('/path/to/image.jpg') +end +</ruby> + +* Then in your view, you can just reference <tt>attachments[]</tt> as a hash and specify which attachment you want to show, calling +url+ on it and then passing the result into the <tt>image_tag</tt> method: + +<erb> +<p>Hello there, this is our image</p> + +<%= image_tag attachments['image.jpg'].url %> +</erb> + +* As this is a standard call to +image_tag+ you can pass in an options hash after the attachment url as you could for any other image: + +<erb> +<p>Hello there, this is our image</p> + +<%= image_tag attachments['image.jpg'].url, :alt => 'My Photo', + :class => 'photos' %> +</erb> + h4. Mailer Views Mailer views are located in the +app/views/name_of_mailer_class+ directory. The specific mailer view is known to the class because it's name is the same as the mailer method. So for example, in our example from above, our mailer view for the +welcome_email+ method will be in +app/views/user_mailer/welcome_email.html.erb+ for the HTML version and +welcome_email.text.erb+ for the plain text version. diff --git a/railties/lib/rails/configuration.rb b/railties/lib/rails/configuration.rb index ee0fca6592..0becb780de 100644 --- a/railties/lib/rails/configuration.rb +++ b/railties/lib/rails/configuration.rb @@ -28,6 +28,10 @@ module Rails @operations << [:use, args, block] end + def delete(*args, &block) + @operations << [:delete, args, block] + end + def merge_into(other) @operations.each do |operation, args, block| other.send(operation, *args, &block) diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index af92757053..f4990bfb4c 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -178,6 +178,7 @@ module Rails "#{orm}:migration", "#{orm}:model", "#{orm}:observer", + "#{orm}:session_migration", "#{test}:controller", "#{test}:helper", "#{test}:integration", @@ -186,8 +187,11 @@ module Rails "#{test}:observer", "#{test}:scaffold", "#{test}:view", + "#{test}:performance", + "#{test}:plugin", "#{template}:controller", - "#{template}:scaffold" + "#{template}:scaffold", + "#{template}:mailer" ] end end diff --git a/railties/lib/rails/version.rb b/railties/lib/rails/version.rb index c10876134a..34d96fd0f3 100644 --- a/railties/lib/rails/version.rb +++ b/railties/lib/rails/version.rb @@ -3,7 +3,7 @@ module Rails MAJOR = 3 MINOR = 0 TINY = 0 - BUILD = "beta3" + BUILD = "beta4" STRING = [MAJOR, MINOR, TINY, BUILD].join('.') end diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb index bab17d8af5..aa75fed793 100644 --- a/railties/test/application/middleware_test.rb +++ b/railties/test/application/middleware_test.rb @@ -57,6 +57,12 @@ module ApplicationTests assert !middleware.include?("ActionDispatch::Static") end + test "can delete a middleware from the stack" do + add_to_config "config.middleware.delete ActionDispatch::Static" + boot! + assert !middleware.include?("ActionDispatch::Static") + end + test "removes show exceptions if action_dispatch.show_exceptions is disabled" do add_to_config "config.action_dispatch.show_exceptions = false" boot! diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 3975a39ab1..8bd9dc9f39 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -70,9 +70,7 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_name_collision_raises_an_error - reserved_words = %w[generate g console c server s dbconsole db - application destroy benchmarker profiler - plugin runner test] + reserved_words = %w[application destroy plugin runner test] reserved_words.each do |reserved| content = capture(:stderr){ run_generator [File.join(destination_root, reserved)] } assert_equal "Invalid application name #{reserved}. Please give a name which does not match one of the reserved rails words.\n", content diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 6f4c5d77f3..b46ac0efaf 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -232,7 +232,7 @@ Module.new do require_environment = "-r #{environment}" end - `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/bin/rails #{tmp_path('app_template')}` + `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/bin/rails new #{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" |