Skip to content
Browse files

Merge

  • Loading branch information...
2 parents d7d9173 + eca11bf commit 5e94d3e3ea06e577d97a32eb4b38b61c4ce8ff8f @dhh dhh committed
Showing with 2,812 additions and 2,441 deletions.
  1. +5 −4 actionmailer/CHANGELOG
  2. +1 −1 actionmailer/actionmailer.gemspec
  3. +1 −1 actionmailer/lib/action_mailer/base.rb
  4. +1,467 −1,466 actionmailer/lib/action_mailer/vendor/text-format-0.6.3/text/format.rb
  5. +9 −9 actionmailer/test/mail_layout_test.rb
  6. +44 −44 actionmailer/test/mail_service_test.rb
  7. +1 −1 actionmailer/test/quoting_test.rb
  8. +2 −2 actionmailer/test/test_helper_test.rb
  9. +2 −2 actionmailer/test/tmail_compat_test.rb
  10. +2 −2 actionpack/lib/action_controller/caching.rb
  11. +8 −6 actionpack/lib/action_controller/caching/fragments.rb
  12. +6 −2 actionpack/lib/action_controller/caching/pages.rb
  13. +4 −3 actionpack/lib/action_controller/metal/logger.rb
  14. +1 −1 actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
  15. +22 −2 actionpack/lib/action_dispatch/middleware/show_exceptions.rb
  16. +1 −0 actionpack/lib/action_view/base.rb
  17. +39 −3 actionpack/lib/action_view/helpers/form_helper.rb
  18. +4 −3 actionpack/lib/action_view/render/partials.rb
  19. +3 −2 actionpack/lib/action_view/render/rendering.rb
  20. +4 −1 actionpack/test/abstract_unit.rb
  21. +3 −4 actionpack/test/activerecord/controller_runtime_test.rb
  22. +8 −8 actionpack/test/controller/caching_test.rb
  23. +2 −1 actionpack/test/controller/logging_test.rb
  24. +23 −0 actionpack/test/dispatch/show_exceptions_test.rb
  25. +1 −0 actionpack/test/lib/controller/fake_models.rb
  26. +40 −0 actionpack/test/template/form_helper_test.rb
  27. +6 −5 activemodel/lib/active_model/errors.rb
  28. +14 −0 activemodel/lib/active_model/lint.rb
  29. +3 −0 activemodel/lib/active_model/locale/en.yml
  30. +2 −1 activemodel/lib/active_model/validator.rb
  31. +2 −0 activemodel/test/cases/lint_test.rb
  32. +7 −15 activemodel/test/cases/validations/i18n_validation_test.rb
  33. +47 −0 activemodel/test/cases/validations/with_validation_test.rb
  34. +6 −0 activemodel/test/cases/validations_test.rb
  35. +5 −0 activerecord/CHANGELOG
  36. +1 −0 activerecord/lib/active_record.rb
  37. +1 −80 activerecord/lib/active_record/associations.rb
  38. +2 −2 activerecord/lib/active_record/associations/association_collection.rb
  39. +1 −1 activerecord/lib/active_record/autosave_association.rb
  40. +13 −21 activerecord/lib/active_record/base.rb
  41. +1 −1 activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
  42. +3 −0 activerecord/lib/active_record/locale/en.yml
  43. +1 −1 activerecord/lib/active_record/named_scope.rb
  44. +1 −1 activerecord/lib/active_record/railtie.rb
  45. +55 −32 activerecord/lib/active_record/relation.rb
  46. +8 −0 activerecord/lib/active_record/relation/query_methods.rb
  47. +85 −0 activerecord/lib/active_record/relation/spawn_methods.rb
  48. +24 −7 activerecord/test/cases/autosave_association_test.rb
  49. +6 −0 activerecord/test/cases/multiple_db_test.rb
  50. +1 −1 activerecord/test/cases/nested_attributes_test.rb
  51. +88 −1 activerecord/test/cases/relations_test.rb
  52. +2 −2 activesupport/lib/active_support/cache.rb
  53. +1 −1 activesupport/lib/active_support/core_ext/integer/multiple.rb
  54. +5 −0 activesupport/test/core_ext/integer_ext_test.rb
  55. +3 −3 activesupport/test/isolation_test.rb
  56. +1 −1 rack
  57. +1 −14 railties/lib/rails/application.rb
  58. +57 −93 railties/lib/rails/generators.rb
  59. +33 −29 railties/lib/rails/generators/base.rb
  60. +11 −3 railties/lib/rails/generators/rails/app/app_generator.rb
  61. +1 −1 railties/lib/rails/generators/rails/app/templates/config/application.rb
  62. +239 −0 railties/lib/rails/generators/test_case.rb
  63. +2 −2 railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb
  64. +0 −3 railties/lib/rails/vendor/thor-0.12.1/lib/thor/version.rb
  65. +5 −7 railties/lib/rails/vendor/{thor-0.12.1 → thor-0.12.3}/CHANGELOG.rdoc
  66. 0 railties/lib/rails/vendor/{thor-0.12.1 → thor-0.12.3}/LICENSE
  67. 0 railties/lib/rails/vendor/{thor-0.12.1 → thor-0.12.3}/README.rdoc
  68. 0 railties/lib/rails/vendor/{thor-0.12.1 → thor-0.12.3}/Thorfile
  69. +41 −43 railties/lib/rails/vendor/{thor-0.12.1 → thor-0.12.3}/lib/thor.rb
  70. +8 −7 railties/lib/rails/vendor/{thor-0.12.1 → thor-0.12.3}/lib/thor/actions.rb
  71. +2 −2 railties/lib/rails/vendor/{thor-0.12.1 → thor-0.12.3}/lib/thor/actions/create_file.rb
  72. 0 railties/lib/rails/vendor/{thor-0.12.1 → thor-0.12.3}/lib/thor/actions/directory.rb
  73. 0 railties/lib/rails/vendor/{thor-0.12.1 → thor-0.12.3}/lib/thor/actions/empty_directory.rb
  74. +4 −4 railties/lib/rails/vendor/{thor-0.12.1 → thor-0.12.3}/lib/thor/actions/file_manipulation.rb
  75. +2 −2 railties/lib/rails/vendor/{thor-0.12.1 → thor-0.12.3}/lib/thor/actions/inject_into_file.rb
  76. +30 −32 railties/lib/rails/vendor/{thor-0.12.1 → thor-0.12.3}/lib/thor/base.rb
  77. +9 −0 railties/lib/rails/vendor/thor-0.12.3/lib/thor/core_ext/file_binary_read.rb
  78. +1 −1 ...es/lib/rails/vendor/{thor-0.12.1 → thor-0.12.3}/lib/thor/core_ext/hash_with_indifferent_access.rb
  79. 0 railties/lib/rails/vendor/{thor-0.12.1 → thor-0.12.3}/lib/thor/core_ext/ordered_hash.rb
  80. 0 railties/lib/rails/vendor/{thor-0.12.1 → thor-0.12.3}/lib/thor/error.rb
  81. +25 −21 railties/lib/rails/vendor/{thor-0.12.1 → thor-0.12.3}/lib/thor/group.rb
  82. 0 railties/lib/rails/vendor/{thor-0.12.1 → thor-0.12.3}/lib/thor/invocation.rb
  83. 0 railties/lib/rails/vendor/{thor-0.12.1 → thor-0.12.3}/lib/thor/parser.rb
  84. 0 railties/lib/rails/vendor/{thor-0.12.1 → thor-0.12.3}/lib/thor/parser/argument.rb
  85. 0 railties/lib/rails/vendor/{thor-0.12.1 → thor-0.12.3}/lib/thor/parser/arguments.rb
  86. +1 −1 railties/lib/rails/vendor/{thor-0.12.1 → thor-0.12.3}/lib/thor/parser/option.rb
  87. 0 railties/lib/rails/vendor/{thor-0.12.1 → thor-0.12.3}/lib/thor/parser/options.rb
  88. 0 railties/lib/rails/vendor/{thor-0.12.1 → thor-0.12.3}/lib/thor/rake_compat.rb
  89. +45 −41 railties/lib/rails/vendor/{thor-0.12.1 → thor-0.12.3}/lib/thor/runner.rb
  90. +1 −1 railties/lib/rails/vendor/{thor-0.12.1 → thor-0.12.3}/lib/thor/shell.rb
  91. +48 −28 railties/lib/rails/vendor/{thor-0.12.1 → thor-0.12.3}/lib/thor/shell/basic.rb
  92. +1 −1 railties/lib/rails/vendor/{thor-0.12.1 → thor-0.12.3}/lib/thor/shell/color.rb
  93. +32 −43 railties/lib/rails/vendor/{thor-0.12.1 → thor-0.12.3}/lib/thor/task.rb
  94. +4 −22 railties/lib/rails/vendor/{thor-0.12.1 → thor-0.12.3}/lib/thor/util.rb
  95. +3 −0 railties/lib/rails/vendor/thor-0.12.3/lib/thor/version.rb
  96. +5 −0 railties/test/application/configuration_test.rb
  97. +3 −9 railties/test/generators/actions_test.rb
  98. +21 −18 railties/test/generators/app_generator_test.rb
  99. +4 −11 railties/test/generators/controller_generator_test.rb
  100. +1 −8 railties/test/generators/generator_generator_test.rb
  101. +11 −81 railties/test/generators/generators_test_helper.rb
  102. +1 −8 railties/test/generators/helper_generator_test.rb
  103. +1 −8 railties/test/generators/integration_test_generator_test.rb
  104. +1 −8 railties/test/generators/mailer_generator_test.rb
  105. +1 −8 railties/test/generators/metal_generator_test.rb
  106. +16 −25 railties/test/generators/migration_generator_test.rb
  107. +4 −11 railties/test/generators/model_generator_test.rb
  108. +4 −4 railties/test/generators/named_base_test.rb
  109. +1 −8 railties/test/generators/observer_generator_test.rb
  110. +1 −8 railties/test/generators/performance_test_generator_test.rb
  111. +1 −8 railties/test/generators/plugin_generator_test.rb
  112. +3 −10 railties/test/generators/resource_generator_test.rb
  113. +10 −17 railties/test/generators/scaffold_controller_generator_test.rb
  114. +9 −19 railties/test/generators/scaffold_generator_test.rb
  115. +0 −9 railties/test/generators/session_migration_generator_test.rb
  116. +1 −10 railties/test/generators/stylesheets_generator_test.rb
  117. +9 −8 railties/test/generators_test.rb
View
9 actionmailer/CHANGELOG
@@ -1,13 +1,14 @@
*Rails 3.0 (pending)*
-* ActionMailer::Base :default_implicit_parts_order now is in the sequence of the order you want, no reversing of ordering takes place. The default order now is text/plain, then text/enriched, then text/html and then any other part that is not one of these three.
+* The Mail::Message class has helped methods for all the field types that return 'common' defaults for the common use case, so to get the subject, mail.subject will give you a string, mail.date will give you a DateTime object, mail.from will give you an array of address specs (mikel@test.lindsaar.net) etc. If you want to access the field object itself, call mail[:field_name] which will return the field object you want, which you can then chain, like mail[:from].formatted
-* Mail does not have "quoted_body", "quoted_subject" etc. All of these are accessed via body.encoded, subject.encoded etc
+* Mail#content_type now returns the content_type field as a string. If you want the mime type of a mail, then you call Mail#mime_type (eg, text/plain), if you want the parameters of the content type field, you call Mail#content_type_parameters which gives you a hash, eg {'format' => 'flowed', 'charset' => 'utf-8'}
-* Every part of a Mail object returns an object, never a string. So Mail.body returns a Mail::Body class object, need to call #encoded or #decoded to get the string you want.
+* ActionMailer::Base :default_implicit_parts_order now is in the sequence of the order you want, no reversing of ordering takes place. The default order now is text/plain, then text/enriched, then text/html and then any other part that is not one of these three.
-* By default, a field will return the #decoded value when you send it :to_s and any object that is a container (like header, body etc) will return #encoded value when you send it :to_s
+* Mail does not have "quoted_body", "quoted_subject" etc. All of these are accessed via body.encoded, subject.encoded etc
+* Every object in a Mail object returns an object, never a string. So Mail.body returns a Mail::Body class object, need to call #encoded or #decoded to get the string you want.
* Mail::Message#set_content_type does not exist, it is simply Mail::Message#content_type
* Every mail message gets a unique message_id unless you specify one, had to change all the tests that check for equality with expected.encoded == actual.encoded to first replace their message_ids with control values
View
2 actionmailer/actionmailer.gemspec
@@ -11,7 +11,7 @@ Gem::Specification.new do |s|
s.homepage = "http://www.rubyonrails.org"
s.add_dependency('actionpack', '= 3.0.pre')
- s.add_dependency('mail', '~> 1.4.3')
+ s.add_dependency('mail', '~> 1.5.0')
s.files = Dir['CHANGELOG', 'README', 'MIT-LICENSE', 'lib/**/*']
s.has_rdoc = true
View
2 actionmailer/lib/action_mailer/base.rb
@@ -500,7 +500,7 @@ def deliver!(mail = @mail)
logger.debug "\n#{mail.encoded}"
end
- ActiveSupport::Notifications.instrument(:deliver_mail, :mail => mail) do
+ ActiveSupport::Notifications.instrument("action_mailer.deliver", :mail => mail) do
begin
self.delivery_method.perform_delivery(mail) if perform_deliveries
rescue Exception => e # Net::SMTP errors or sendmail pipe errors
View
2,933 actionmailer/lib/action_mailer/vendor/text-format-0.6.3/text/format.rb
1,467 additions, 1,466 deletions not shown because the diff is too large. Please use a local Git client to view these changes.
View
18 actionmailer/test/mail_layout_test.rb
@@ -72,12 +72,12 @@ def test_should_pickup_multipart_layout
mail = AutoLayoutMailer.create_multipart(@recipient)
# CHANGED: content_type returns an object
# assert_equal "multipart/alternative", mail.content_type
- assert_equal "multipart/alternative", mail.content_type.string
+ assert_equal "multipart/alternative", mail.mime_type
assert_equal 2, mail.parts.size
# CHANGED: content_type returns an object
# assert_equal 'text/plain', mail.parts.first.content_type
- assert_equal 'text/plain', mail.parts.first.content_type.string
+ assert_equal 'text/plain', mail.parts.first.mime_type
# CHANGED: body returns an object
# assert_equal "text/plain layout - text/plain multipart", mail.parts.first.body
@@ -85,7 +85,7 @@ def test_should_pickup_multipart_layout
# CHANGED: content_type returns an object
# assert_equal 'text/html', mail.parts.last.content_type
- assert_equal 'text/html', mail.parts.last.content_type.string
+ assert_equal 'text/html', mail.parts.last.mime_type
# CHANGED: body returns an object
# assert_equal "Hello from layout text/html multipart", mail.parts.last.body
@@ -96,19 +96,19 @@ def test_should_pickup_multipartmixed_layout
mail = AutoLayoutMailer.create_multipart(@recipient, "multipart/mixed")
# CHANGED: content_type returns an object
# assert_equal "multipart/mixed", mail.content_type
- assert_equal "multipart/mixed", mail.content_type.string
+ assert_equal "multipart/mixed", mail.mime_type
assert_equal 2, mail.parts.size
# CHANGED: content_type returns an object
# assert_equal 'text/plain', mail.parts.first.content_type
- assert_equal 'text/plain', mail.parts.first.content_type.string
+ assert_equal 'text/plain', mail.parts.first.mime_type
# CHANGED: body returns an object
# assert_equal "text/plain layout - text/plain multipart", mail.parts.first.body
assert_equal "text/plain layout - text/plain multipart", mail.parts.first.body.to_s
# CHANGED: content_type returns an object
# assert_equal 'text/html', mail.parts.last.content_type
- assert_equal 'text/html', mail.parts.last.content_type.string
+ assert_equal 'text/html', mail.parts.last.mime_type
# CHANGED: body returns an object
# assert_equal "Hello from layout text/html multipart", mail.parts.last.body
assert_equal "Hello from layout text/html multipart", mail.parts.last.body.to_s
@@ -116,13 +116,13 @@ def test_should_pickup_multipartmixed_layout
def test_should_fix_multipart_layout
mail = AutoLayoutMailer.create_multipart(@recipient, "text/plain")
- assert_equal "multipart/alternative", mail.content_type.string
+ assert_equal "multipart/alternative", mail.mime_type
assert_equal 2, mail.parts.size
- assert_equal 'text/plain', mail.parts.first.content_type.string
+ assert_equal 'text/plain', mail.parts.first.mime_type
assert_equal "text/plain layout - text/plain multipart", mail.parts.first.body.to_s
- assert_equal 'text/html', mail.parts.last.content_type.string
+ assert_equal 'text/html', mail.parts.last.mime_type
assert_equal "Hello from layout text/html multipart", mail.parts.last.body.to_s
end
View
88 actionmailer/test/mail_service_test.rb
@@ -362,13 +362,13 @@ def test_nested_parts
assert_equal 2, created.parts.size
assert_equal 2, created.parts.first.parts.size
- assert_equal "multipart/mixed", created.content_type.string
- assert_equal "multipart/alternative", created.parts[0].content_type.string
+ assert_equal "multipart/mixed", created.mime_type
+ assert_equal "multipart/alternative", created.parts[0].mime_type
assert_equal "bar", created.parts[0].header['foo'].to_s
assert_nil created.parts[0].charset
- assert_equal "text/plain", created.parts[0].parts[0].content_type.string
- assert_equal "text/html", created.parts[0].parts[1].content_type.string
- assert_equal "application/octet-stream", created.parts[1].content_type.string
+ assert_equal "text/plain", created.parts[0].parts[0].mime_type
+ assert_equal "text/html", created.parts[0].parts[1].mime_type
+ assert_equal "application/octet-stream", created.parts[1].mime_type
end
@@ -380,11 +380,11 @@ def test_nested_parts_with_body
assert_equal 1,created.parts.size
assert_equal 2,created.parts.first.parts.size
- assert_equal "multipart/mixed", created.content_type.string
- assert_equal "multipart/alternative", created.parts.first.content_type.string
- assert_equal "text/plain", created.parts.first.parts.first.content_type.string
+ assert_equal "multipart/mixed", created.mime_type
+ assert_equal "multipart/alternative", created.parts.first.mime_type
+ assert_equal "text/plain", created.parts.first.parts.first.mime_type
assert_equal "Nothing to see here.", created.parts.first.parts.first.body.to_s
- assert_equal "text/html", created.parts.first.parts.second.content_type.string
+ assert_equal "text/html", created.parts.first.parts.second.mime_type
assert_equal "<b>test</b> HTML<br/>", created.parts.first.parts.second.body.to_s
end
@@ -468,8 +468,8 @@ def test_custom_templating_extension
assert_nothing_raised { created = TestMailer.create_custom_templating_extension(@recipient) }
assert_not_nil created
assert_equal 2, created.parts.length
- assert_equal 'text/plain', created.parts[0].content_type.string
- assert_equal 'text/html', created.parts[1].content_type.string
+ assert_equal 'text/plain', created.parts[0].mime_type
+ assert_equal 'text/html', created.parts[1].mime_type
end
def test_cancelled_account
@@ -731,8 +731,8 @@ def test_unquote_quoted_printable_subject
The body
EOF
mail = Mail.new(msg)
- assert_equal "testing testing \326\244", mail.subject.to_s
- assert_equal "Subject: =?utf-8?Q?testing_testing_=D6=A4?=\r\n", mail.subject.encoded
+ assert_equal "testing testing \326\244", mail.subject
+ assert_equal "Subject: =?utf-8?Q?testing_testing_=D6=A4?=\r\n", mail[:subject].encoded
end
def test_unquote_7bit_subject
@@ -744,8 +744,8 @@ def test_unquote_7bit_subject
The body
EOF
mail = Mail.new(msg)
- assert_equal "this == working?", mail.subject.to_s
- assert_equal "Subject: this == working?\r\n", mail.subject.encoded
+ assert_equal "this == working?", mail.subject
+ assert_equal "Subject: this == working?\r\n", mail[:subject].encoded
end
def test_unquote_7bit_body
@@ -868,7 +868,7 @@ def test_receive_attachments
mail = Mail.new(fixture)
attachment = mail.attachments.last
assert_equal "smime.p7s", attachment.original_filename
- assert_equal "application/pkcs7-signature", mail.parts.last.content_type.string
+ assert_equal "application/pkcs7-signature", mail.parts.last.mime_type
end
def test_decode_attachment_without_charset
@@ -913,7 +913,7 @@ def test_decode_message_with_incorrect_charset
def test_multipart_with_mime_version
mail = TestMailer.create_multipart_with_mime_version(@recipient)
- assert_equal "1.1", mail.mime_version.version
+ assert_equal "1.1", mail.mime_version
end
def test_multipart_with_utf8_subject
@@ -929,29 +929,29 @@ def test_implicitly_multipart_with_utf8
def test_explicitly_multipart_messages
mail = TestMailer.create_explicitly_multipart_example(@recipient)
assert_equal 3, mail.parts.length
- assert_equal 'multipart/mixed', mail.content_type.string
- assert_equal "text/plain", mail.parts[0].content_type.string
+ assert_equal 'multipart/mixed', mail.mime_type
+ assert_equal "text/plain", mail.parts[0].mime_type
- assert_equal "text/html", mail.parts[1].content_type.string
+ assert_equal "text/html", mail.parts[1].mime_type
assert_equal "iso-8859-1", mail.parts[1].charset
- assert_equal "image/jpeg", mail.parts[2].content_type.string
- assert_equal "attachment", mail.parts[2].content_disposition.disposition_type
- assert_equal "foo.jpg", mail.parts[2].content_disposition.filename
- assert_equal "foo.jpg", mail.parts[2].content_type.filename
+ assert_equal "image/jpeg", mail.parts[2].mime_type
+ assert_equal "attachment", mail.parts[2][:content_disposition].disposition_type
+ assert_equal "foo.jpg", mail.parts[2][:content_disposition].filename
+ assert_equal "foo.jpg", mail.parts[2][:content_type].filename
assert_nil mail.parts[2].charset
end
def test_explicitly_multipart_with_content_type
mail = TestMailer.create_explicitly_multipart_example(@recipient, "multipart/alternative")
assert_equal 3, mail.parts.length
- assert_equal "multipart/alternative", mail.content_type.string
+ assert_equal "multipart/alternative", mail.mime_type
end
def test_explicitly_multipart_with_invalid_content_type
mail = TestMailer.create_explicitly_multipart_example(@recipient, "text/xml")
assert_equal 3, mail.parts.length
- assert_equal 'multipart/mixed', mail.content_type.string
+ assert_equal 'multipart/mixed', mail.mime_type
end
def test_implicitly_multipart_messages
@@ -960,12 +960,12 @@ def test_implicitly_multipart_messages
mail = TestMailer.create_implicitly_multipart_example(@recipient)
assert_equal 3, mail.parts.length
assert_equal "1.0", mail.mime_version.to_s
- assert_equal "multipart/alternative", mail.content_type.string
- assert_equal "text/plain", mail.parts[0].content_type.string
+ assert_equal "multipart/alternative", mail.mime_type
+ assert_equal "text/plain", mail.parts[0].mime_type
assert_equal "utf-8", mail.parts[0].charset
- assert_equal "text/html", mail.parts[1].content_type.string
+ assert_equal "text/html", mail.parts[1].mime_type
assert_equal "utf-8", mail.parts[1].charset
- assert_equal "application/x-yaml", mail.parts[2].content_type.string
+ assert_equal "application/x-yaml", mail.parts[2].mime_type
assert_equal "utf-8", mail.parts[2].charset
end
@@ -974,9 +974,9 @@ def test_implicitly_multipart_messages_with_custom_order
mail = TestMailer.create_implicitly_multipart_example(@recipient, nil, ["application/x-yaml", "text/plain"])
assert_equal 3, mail.parts.length
- assert_equal "application/x-yaml", mail.parts[0].content_type.string
- assert_equal "text/plain", mail.parts[1].content_type.string
- assert_equal "text/html", mail.parts[2].content_type.string
+ assert_equal "application/x-yaml", mail.parts[0].mime_type
+ assert_equal "text/plain", mail.parts[1].mime_type
+ assert_equal "text/html", mail.parts[2].mime_type
end
def test_implicitly_multipart_messages_with_charset
@@ -984,14 +984,14 @@ def test_implicitly_multipart_messages_with_charset
assert_equal "multipart/alternative", mail.header['content-type'].content_type
- assert_equal 'iso-8859-1', mail.parts[0].content_type.parameters[:charset]
- assert_equal 'iso-8859-1', mail.parts[1].content_type.parameters[:charset]
- assert_equal 'iso-8859-1', mail.parts[2].content_type.parameters[:charset]
+ assert_equal 'iso-8859-1', mail.parts[0].content_type_parameters[:charset]
+ assert_equal 'iso-8859-1', mail.parts[1].content_type_parameters[:charset]
+ assert_equal 'iso-8859-1', mail.parts[2].content_type_parameters[:charset]
end
def test_html_mail
mail = TestMailer.create_html_mail(@recipient)
- assert_equal "text/html", mail.content_type.string
+ assert_equal "text/html", mail.mime_type
end
def test_html_mail_with_underscores
@@ -1043,7 +1043,7 @@ def test_recursive_multipart_processing
assert_equal(4, mail.parts.first.parts.length)
assert_equal("This is the first part.", mail.parts.first.parts.first.body.to_s)
assert_equal("test.rb", mail.parts.first.parts.second.filename)
- assert_equal("flowed", mail.parts.first.parts.fourth.content_type.parameters[:format])
+ assert_equal("flowed", mail.parts.first.parts.fourth.content_type_parameters[:format])
assert_equal('smime.p7s', mail.parts.second.filename)
end
@@ -1081,9 +1081,9 @@ def test_headers_with_nonalpha_chars
assert !mail.from_addrs.empty?
assert !mail.cc_addrs.empty?
assert !mail.bcc_addrs.empty?
- assert_match(/:/, mail.from_addrs.to_s)
- assert_match(/:/, mail.cc_addrs.to_s)
- assert_match(/:/, mail.bcc_addrs.to_s)
+ assert_match(/:/, mail[:from].decoded)
+ assert_match(/:/, mail[:cc].decoded)
+ assert_match(/:/, mail[:bcc].decoded)
end
def test_deliver_with_mail_object
@@ -1095,14 +1095,14 @@ def test_deliver_with_mail_object
def test_multipart_with_template_path_with_dots
mail = FunkyPathMailer.create_multipart_with_template_path_with_dots(@recipient)
assert_equal 2, mail.parts.length
- assert "text/plain", mail.parts[1].content_type.string
+ assert "text/plain", mail.parts[1].mime_type
assert "utf-8", mail.parts[1].charset
end
def test_custom_content_type_attributes
mail = TestMailer.create_custom_content_type_attributes
- assert_match %r{format="flowed"}, mail.content_type.encoded
- assert_match %r{charset="utf-8"}, mail.content_type.encoded
+ assert_match %r{format=flowed}, mail.content_type
+ assert_match %r{charset=utf-8}, mail.content_type
end
def test_return_path_with_create
View
2 actionmailer/test/quoting_test.rb
@@ -78,7 +78,7 @@ def test_email_with_partially_quoted_subject
mail = Mail.new(IO.read("#{File.dirname(__FILE__)}/fixtures/raw_email_with_partially_quoted_subject"))
# CHANGED: subject returns an object now
# assert_equal "Re: Test: \"\346\274\242\345\255\227\" mid \"\346\274\242\345\255\227\" tail", mail.subject
- assert_equal "Re: Test: \"\346\274\242\345\255\227\" mid \"\346\274\242\345\255\227\" tail", mail.subject.decoded
+ assert_equal "Re: Test: \"\346\274\242\345\255\227\" mid \"\346\274\242\345\255\227\" tail", mail.subject
end
private
View
4 actionmailer/test/test_helper_test.rb
@@ -19,8 +19,8 @@ def test_setup_sets_right_action_mailer_options
def test_setup_creates_the_expected_mailer
assert @expected.is_a?(Mail::Message)
- assert_equal "1.0", @expected.mime_version.version
- assert_equal "text/plain", @expected.content_type.string
+ assert_equal "1.0", @expected.mime_version
+ assert_equal "text/plain", @expected.mime_type
end
def test_mailer_class_is_correctly_inferred
View
4 actionmailer/test/tmail_compat_test.rb
@@ -8,7 +8,7 @@ def test_set_content_type_raises_deprecation_warning
assert_nothing_raised do
mail.set_content_type "text/plain"
end
- assert_equal mail.content_type.string, "text/plain"
+ assert_equal mail.mime_type, "text/plain"
end
def test_transfer_encoding_raises_deprecation_warning
@@ -17,7 +17,7 @@ def test_transfer_encoding_raises_deprecation_warning
assert_nothing_raised do
mail.transfer_encoding "base64"
end
- assert_equal mail.content_transfer_encoding.value, "base64"
+ assert_equal mail.content_transfer_encoding, "base64"
end
end
View
4 actionpack/lib/action_controller/caching.rb
@@ -62,9 +62,9 @@ def cache_configured?
end
def log_event(name, before, after, instrumenter_id, payload)
- if name.to_s =~ /(read|write|cache|expire|exist)_(fragment|page)\??/
+ if name.to_s =~ /action_controller\.((read|write|expire|exist)_(fragment|page)\??)/
key_or_path = payload[:key] || payload[:path]
- human_name = name.to_s.humanize
+ human_name = $1.humanize
duration = (after - before) * 1000
logger.info("#{human_name} #{key_or_path.inspect} (%.1fms)" % duration)
else
View
14 actionpack/lib/action_controller/caching/fragments.rb
@@ -53,7 +53,7 @@ def write_fragment(key, content, options = nil)
return content unless cache_configured?
key = fragment_cache_key(key)
- ActiveSupport::Notifications.instrument(:write_fragment, :key => key) do
+ instrument_fragment_cache :write_fragment, key do
cache_store.write(key, content, options)
end
content
@@ -64,7 +64,7 @@ def read_fragment(key, options = nil)
return unless cache_configured?
key = fragment_cache_key(key)
- ActiveSupport::Notifications.instrument(:read_fragment, :key => key) do
+ instrument_fragment_cache :read_fragment, key do
cache_store.read(key, options)
end
end
@@ -74,7 +74,7 @@ def fragment_exist?(key, options = nil)
return unless cache_configured?
key = fragment_cache_key(key)
- ActiveSupport::Notifications.instrument(:exist_fragment?, :key => key) do
+ instrument_fragment_cache :exist_fragment?, key do
cache_store.exist?(key, options)
end
end
@@ -101,16 +101,18 @@ def expire_fragment(key, options = nil)
key = fragment_cache_key(key) unless key.is_a?(Regexp)
message = nil
- ActiveSupport::Notifications.instrument(:expire_fragment, :key => key) do
+ instrument_fragment_cache :expire_fragment, key do
if key.is_a?(Regexp)
- message = "Expired fragments matching: #{key.source}"
cache_store.delete_matched(key, options)
else
- message = "Expired fragment: #{key}"
cache_store.delete(key, options)
end
end
end
+
+ def instrument_fragment_cache(name, key)
+ ActiveSupport::Notifications.instrument("action_controller.#{name}", :key => key){ yield }
+ end
end
end
end
View
8 actionpack/lib/action_controller/caching/pages.rb
@@ -64,7 +64,7 @@ def expire_page(path)
return unless perform_caching
path = page_cache_path(path)
- ActiveSupport::Notifications.instrument(:expire_page, :path => path) do
+ instrument_page_cache :expire_page, path do
File.delete(path) if File.exist?(path)
end
end
@@ -75,7 +75,7 @@ def cache_page(content, path)
return unless perform_caching
path = page_cache_path(path)
- ActiveSupport::Notifications.instrument(:cache_page, :path => path) do
+ instrument_page_cache :write_page, path do
FileUtils.makedirs(File.dirname(path))
File.open(path, "wb+") { |f| f.write(content) }
end
@@ -107,6 +107,10 @@ def page_cache_file(path)
def page_cache_path(path)
page_cache_directory + page_cache_file(path)
end
+
+ def instrument_page_cache(name, path)
+ ActiveSupport::Notifications.instrument("action_controller.#{name}", :path => path){ yield }
+ end
end
# Expires the page that was cached with the +options+ as a key. Example:
View
7 actionpack/lib/action_controller/metal/logger.rb
@@ -15,7 +15,8 @@ module Logger
attr_internal :view_runtime
def process_action(action)
- ActiveSupport::Notifications.instrument(:process_action, :controller => self, :action => action) do
+ ActiveSupport::Notifications.instrument("action_controller.process_action",
+ :controller => self, :action => action) do
super
end
end
@@ -50,7 +51,7 @@ module ClassMethods
# This is the hook invoked by ActiveSupport::Notifications.subscribe.
# If you need to log any event, overwrite the method and do it here.
def log_event(name, before, after, instrumenter_id, payload) #:nodoc:
- if name == :process_action
+ if name == "action_controller.process_action"
duration = [(after - before) * 1000, 0.01].max
controller = payload[:controller]
request = controller.request
@@ -66,7 +67,7 @@ def log_event(name, before, after, instrumenter_id, payload) #:nodoc:
message << " [#{request.request_uri rescue "unknown"}]"
logger.info(message)
- elsif name == :render_template
+ elsif name == "action_view.render_template"
# TODO Make render_template logging work if you are using just ActionView
duration = (after - before) * 1000
message = "Rendered #{payload[:identifier]}"
View
2 actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
@@ -179,7 +179,7 @@ def ensure_session_key(key)
'cookie containing the session data. Use ' +
'config.action_controller.session = { :key => ' +
'"_myapp_session", :secret => "some secret phrase" } in ' +
- 'config/environment.rb'
+ 'config/application.rb'
end
end
View
24 actionpack/lib/action_dispatch/middleware/show_exceptions.rb
@@ -1,7 +1,24 @@
require 'active_support/core_ext/exception'
+require 'active_support/notifications'
require 'action_dispatch/http/request'
module ActionDispatch
+ # This middleware rescues any exception returned by the application and renders
+ # nice exception pages if it's being rescued locally.
+ #
+ # Every time an exception is caught, a notification is published, becoming a good API
+ # to deal with exceptions. So, if you want send an e-mail through ActionMailer
+ # everytime this notification is published, you just need to do the following:
+ #
+ # ActiveSupport::Notifications.subscribe "action_dispatch.show_exception" do |name, start, end, instrumentation_id, payload|
+ # ExceptionNotifier.deliver_exception(start, payload)
+ # end
+ #
+ # The payload is a hash which has to pairs:
+ #
+ # * :env - Contains the rack env for the given request;
+ # * :exception - The exception raised;
+ #
class ShowExceptions
LOCALHOST = '127.0.0.1'.freeze
@@ -44,8 +61,11 @@ def initialize(app, consider_all_requests_local = false)
def call(env)
@app.call(env)
rescue Exception => exception
- raise exception if env['action_dispatch.show_exceptions'] == false
- render_exception(env, exception)
+ ActiveSupport::Notifications.instrument 'action_dispatch.show_exception',
+ :env => env, :exception => exception do
+ raise exception if env['action_dispatch.show_exceptions'] == false
+ render_exception(env, exception)
+ end
end
private
View
1 actionpack/lib/action_view/base.rb
@@ -274,6 +274,7 @@ def inspect
end
def initialize(view_paths = [], assigns_for_first_render = {}, controller = nil, formats = nil)#:nodoc:
+ @config = nil
@formats = formats
@assigns = assigns_for_first_render.each { |key, value| instance_variable_set("@#{key}", value) }
@controller = controller
View
42 actionpack/lib/action_view/helpers/form_helper.rb
@@ -504,8 +504,9 @@ def fields_for(record_or_name_or_array, *args, &block)
end
# Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object
- # assigned to the template (identified by +object+). The text of label will default to the attribute name unless you specify
- # it explicitly. Additional options on the label tag can be passed as a hash with +options+. These options will be tagged
+ # assigned to the template (identified by +object+). The text of label will default to the attribute name unless a translation
+ # is found in the current I18n locale (through views.labels.<modelname>.<attribute>) or you specify it explicitly.
+ # Additional options on the label tag can be passed as a hash with +options+. These options will be tagged
# onto the HTML as an HTML element attribute as in the example shown, except for the <tt>:value</tt> option, which is designed to
# target labels for radio_button tags (where the value is used in the ID of the input tag).
#
@@ -513,6 +514,29 @@ def fields_for(record_or_name_or_array, *args, &block)
# label(:post, :title)
# # => <label for="post_title">Title</label>
#
+ # You can localize your labels based on model and attribute names.
+ # For example you can define the following in your locale (e.g. en.yml)
+ #
+ # views:
+ # labels:
+ # post:
+ # body: "Write your entire text here"
+ #
+ # Which then will result in
+ #
+ # label(:post, :body)
+ # # => <label for="post_body">Write your entire text here</label>
+ #
+ # Localization can also be based purely on the translation of the attribute-name like this:
+ #
+ # activemodel:
+ # attribute:
+ # post:
+ # cost: "Total cost"
+ #
+ # label(:post, :cost)
+ # # => <label for="post_cost">Total cost</label>
+ #
# label(:post, :title, "A short title")
# # => <label for="post_title">A short title</label>
#
@@ -751,7 +775,19 @@ def to_label_tag(text = nil, options = {})
add_default_name_and_id_for_value(tag_value, name_and_id)
options.delete("index")
options["for"] ||= name_and_id["id"]
- content = (text.blank? ? nil : text.to_s) || method_name.humanize
+
+ content = if text.blank?
+ I18n.t("views.labels.#{object_name}.#{method_name}", :default => "").presence
+ else
+ text.to_s
+ end
+
+ content ||= if object && object.class.respond_to?(:human_attribute_name)
+ object.class.human_attribute_name(method_name)
+ end
+
+ content ||= method_name.humanize
+
label_tag(name_and_id["id"], content, options)
end
View
7 actionpack/lib/action_view/render/partials.rb
@@ -215,12 +215,13 @@ def render
options = @options
if @collection
- ActiveSupport::Notifications.instrument(:render_collection, :path => @path,
- :count => @collection.size) do
+ ActiveSupport::Notifications.instrument("action_view.render_collection",
+ :path => @path, :count => @collection.size) do
render_collection
end
else
- content = ActiveSupport::Notifications.instrument(:render_partial, :path => @path) do
+ content = ActiveSupport::Notifications.instrument("action_view.render_partial",
+ :path => @path) do
render_partial
end
View
5 actionpack/lib/action_view/render/rendering.rb
@@ -93,7 +93,7 @@ def render_template(options)
def _render_template(template, layout = nil, options = {})
locals = options[:locals] || {}
- content = ActiveSupport::Notifications.instrument(:render_template,
+ content = ActiveSupport::Notifications.instrument("action_view.render_template",
:identifier => template.identifier, :layout => (layout ? layout.identifier : nil)) do
template.render(self, locals)
end
@@ -109,7 +109,8 @@ def _render_template(template, layout = nil, options = {})
end
def _render_layout(layout, locals, &block)
- ActiveSupport::Notifications.instrument(:render_layout, :identifier => layout.identifier) do
+ ActiveSupport::Notifications.instrument("action_view.render_layout",
+ :identifier => layout.identifier) do
layout.render(self, locals){ |*name| _layout_for(*name, &block) }
end
end
View
5 actionpack/test/abstract_unit.rb
@@ -96,7 +96,6 @@ class ActiveSupport::TestCase
end
class MockLogger
- attr_reader :logged
attr_accessor :level
def initialize
@@ -108,6 +107,10 @@ def method_missing(method, *args, &blk)
@logged << args.first
@logged << blk.call if block_given?
end
+
+ def logged
+ @logged.compact.map { |l| l.to_s.strip }
+ end
end
class ActionController::IntegrationTest < ActiveSupport::TestCase
View
7 actionpack/test/activerecord/controller_runtime_test.rb
@@ -23,9 +23,11 @@ def wait
end
def test_log_with_active_record
+ # Wait pending notifications to be published
+ wait
get :show
wait
- assert_match /ActiveRecord runtime/, logs[3]
+ assert_match /ActiveRecord runtime/, @controller.logger.logged[3]
end
private
@@ -33,7 +35,4 @@ def set_logger
@controller.logger = MockLogger.new
end
- def logs
- @logs ||= @controller.logger.logged.compact.map {|l| l.to_s.strip}
- end
end
View
16 actionpack/test/controller/caching_test.rb
@@ -630,17 +630,17 @@ def test_fragment_for
end
def test_fragment_for_logging
- fragment_computed = false
- events = []
- ActiveSupport::Notifications.subscribe { |*args| events << args }
+ # Wait pending notifications to be published
+ ActiveSupport::Notifications.notifier.wait
+ @controller.logger = MockLogger.new
- buffer = 'generated till now -> '
- @controller.fragment_for(buffer, 'expensive') { fragment_computed = true }
+ fragment_computed = false
+ @controller.fragment_for('buffer', 'expensive') { fragment_computed = true }
+ ActiveSupport::Notifications.notifier.wait
assert fragment_computed
- assert_equal 'generated till now -> ', buffer
- ActiveSupport::Notifications.notifier.wait
- assert_equal [:exist_fragment?, :write_fragment], events.map(&:first)
+ assert_match /Exist fragment\? "views\/expensive"/, @controller.logger.logged[0]
+ assert_match /Write fragment "views\/expensive"/, @controller.logger.logged[1]
end
end
View
3 actionpack/test/controller/logging_test.rb
@@ -19,6 +19,7 @@ class LoggingTest < ActionController::TestCase
def setup
super
+ wait # Wait pending notifications to be published
set_logger
end
@@ -75,6 +76,6 @@ def set_logger
end
def logs
- @logs ||= @controller.logger.logged.compact.map {|l| l.to_s.strip}
+ @logs ||= @controller.logger.logged
end
end
View
23 actionpack/test/dispatch/show_exceptions_test.rb
@@ -104,4 +104,27 @@ class ShowExceptionsTest < ActionController::IntegrationTest
assert_response 405
assert_match /ActionController::MethodNotAllowed/, body
end
+
+ test "publishes notifications" do
+ # Wait pending notifications to be published
+ ActiveSupport::Notifications.notifier.wait
+
+ @app, event = ProductionApp, nil
+ self.remote_addr = '127.0.0.1'
+
+ ActiveSupport::Notifications.subscribe('action_dispatch.show_exception') do |*args|
+ event = args
+ end
+
+ get "/"
+ assert_response 500
+ assert_match /puke/, body
+
+ ActiveSupport::Notifications.notifier.wait
+
+ assert_equal 'action_dispatch.show_exception', event.first
+ assert_kind_of Hash, event.last[:env]
+ assert_equal 'GET', event.last[:env]["REQUEST_METHOD"]
+ assert_kind_of RuntimeError, event.last[:exception]
+ end
end
View
1 actionpack/test/lib/controller/fake_models.rb
@@ -54,6 +54,7 @@ class Store < Question
class Post < Struct.new(:title, :author_name, :body, :secret, :written_on, :cost)
extend ActiveModel::Naming
include ActiveModel::Conversion
+ extend ActiveModel::Translation
alias_method :secret?, :secret
View
40 actionpack/test/template/form_helper_test.rb
@@ -6,6 +6,25 @@ class FormHelperTest < ActionView::TestCase
def setup
super
+
+ # Create "label" locale for testing I18n label helpers
+ I18n.backend.store_translations 'label', {
+ :activemodel => {
+ :attributes => {
+ :post => {
+ :cost => "Total cost"
+ }
+ }
+ },
+ :views => {
+ :labels => {
+ :post => {
+ :body => "Write entire text here"
+ }
+ }
+ }
+ }
+
@post = Post.new
@comment = Comment.new
def @post.errors()
@@ -51,6 +70,27 @@ def test_label_with_symbols
assert_dom_equal('<label for="post_secret">Secret?</label>', label(:post, :secret?))
end
+ def test_label_with_locales_strings
+ old_locale, I18n.locale = I18n.locale, :label
+ assert_dom_equal('<label for="post_body">Write entire text here</label>', label("post", "body"))
+ ensure
+ I18n.locale = old_locale
+ end
+
+ def test_label_with_human_attribute_name
+ old_locale, I18n.locale = I18n.locale, :label
+ assert_dom_equal('<label for="post_cost">Total cost</label>', label(:post, :cost))
+ ensure
+ I18n.locale = old_locale
+ end
+
+ def test_label_with_locales_symbols
+ old_locale, I18n.locale = I18n.locale, :label
+ assert_dom_equal('<label for="post_body">Write entire text here</label>', label(:post, :body))
+ ensure
+ I18n.locale = old_locale
+ end
+
def test_label_with_for_attribute_as_symbol
assert_dom_equal('<label for="my_for">Title</label>', label(:post, :title, nil, :for => "my_for"))
end
View
11 activemodel/lib/active_model/errors.rb
@@ -97,18 +97,19 @@ def full_messages
full_messages = []
each do |attribute, messages|
- messages = Array.wrap(messages)
+ messages = Array(messages)
next if messages.empty?
if attribute == :base
messages.each {|m| full_messages << m }
else
- attr_name = @base.class.human_attribute_name(attribute)
- options = { :default => ' ', :scope => @base.class.i18n_scope }
- prefix = attr_name + I18n.t(:"errors.format.separator", options)
+ attr_name = attribute.to_s.gsub('.', '_').humanize
+ attr_name = @base.class.human_attribute_name(attribute, :default => attr_name)
+ options = { :default => "{{attribute}} {{message}}", :attribute => attr_name,
+ :scope => @base.class.i18n_scope }
messages.each do |m|
- full_messages << "#{prefix}#{m}"
+ full_messages << I18n.t(:"errors.format", options.merge(:message => m))
end
end
end
View
14 activemodel/lib/active_model/lint.rb
@@ -41,6 +41,20 @@ def test_destroyed?
assert_boolean model.destroyed?, "destroyed?"
end
+ # naming
+ # ------
+ #
+ # Model.model_name must returns a string with some convenience methods as
+ # :human and :partial_path. Check ActiveModel::Naming for more information.
+ #
+ def test_model_naming
+ assert model.class.respond_to?(:model_name), "The model should respond to model_name"
+ model_name = model.class.model_name
+ assert_kind_of String, model_name
+ assert_kind_of String, model_name.human
+ assert_kind_of String, model_name.partial_path
+ end
+
# errors
# ------
#
View
3 activemodel/lib/active_model/locale/en.yml
@@ -1,6 +1,9 @@
en:
activemodel:
errors:
+ # model.errors.full_messages format.
+ format: "{{attribute}} {{message}}"
+
# The values :model, :attribute and :value are always available for interpolation
# The value :count is available when applicable. Can be used for pluralization.
messages:
View
3 activemodel/lib/active_model/validator.rb
@@ -72,7 +72,8 @@ class EachValidator < Validator
attr_reader :attributes
def initialize(options)
- @attributes = options.delete(:attributes)
+ @attributes = Array(options.delete(:attributes))
+ raise ":attributes cannot be blank" if @attributes.empty?
super
check_validity!
end
View
2 activemodel/test/cases/lint_test.rb
@@ -4,6 +4,8 @@ class LintTest < ActiveModel::TestCase
include ActiveModel::Lint::Tests
class CompliantModel
+ extend ActiveModel::Naming
+
def to_model
self
end
View
22 activemodel/test/cases/validations/i18n_validation_test.rb
@@ -21,20 +21,6 @@ def teardown
I18n.backend = @old_backend
end
- def test_percent_s_interpolation_syntax_in_error_messages_was_deprecated
- assert_not_deprecated do
- default = "%s interpolation syntax was deprecated"
- assert_equal default, I18n.t(:does_not_exist, :default => default, :value => 'this')
- end
- end
-
- def test_percent_d_interpolation_syntax_in_error_messages_was_deprecated
- assert_not_deprecated do
- default = "%d interpolation syntaxes are deprecated"
- assert_equal default, I18n.t(:does_not_exist, :default => default, :count => 2)
- end
- end
-
def test_errors_add_on_empty_generates_message
@person.errors.expects(:generate_message).with(:title, :empty, {:default => nil})
@person.errors.add_on_empty :title
@@ -57,10 +43,16 @@ def test_errors_add_on_blank_generates_message_with_custom_default_message
def test_errors_full_messages_translates_human_attribute_name_for_model_attributes
@person.errors.add('name', 'empty')
- I18n.expects(:translate).with(:"person.name", :default => ['Name'], :scope => [:activemodel, :attributes], :count => 1).returns('Name')
+ I18n.expects(:translate).with(:"person.name", :default => ['Name', 'Name'], :scope => [:activemodel, :attributes], :count => 1).returns('Name')
@person.errors.full_messages
end
+ def test_errors_full_messages_uses_format
+ I18n.backend.store_translations('en', :activemodel => {:errors => {:format => "Field {{attribute}} {{message}}"}})
+ @person.errors.add('name', 'empty')
+ assert_equal ["Field Name empty"], @person.errors.full_messages
+ end
+
# ActiveRecord::Validations
# validates_confirmation_of w/ mocha
def test_validates_confirmation_of_generates_message
View
47 activemodel/test/cases/validations/with_validation_test.rb
@@ -39,6 +39,18 @@ def validate(record)
end
end
+ class ValidatorPerEachAttribute < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ record.errors[attribute] << "Value is #{value}"
+ end
+ end
+
+ class ValidatorCheckValidity < ActiveModel::EachValidator
+ def check_validity!
+ raise "boom!"
+ end
+ end
+
test "vaidation with class that adds errors" do
Topic.validates_with(ValidatorThatAddsErrors)
topic = Topic.new
@@ -116,4 +128,39 @@ def validate(record)
assert topic.errors[:base].include?(ERROR_MESSAGE)
end
+ test "validates_with each validator" do
+ Topic.validates_with(ValidatorPerEachAttribute, :attributes => [:title, :content])
+ topic = Topic.new :title => "Title", :content => "Content"
+ assert !topic.valid?
+ assert_equal ["Value is Title"], topic.errors[:title]
+ assert_equal ["Value is Content"], topic.errors[:content]
+ end
+
+ test "each validator checks validity" do
+ assert_raise RuntimeError do
+ Topic.validates_with(ValidatorCheckValidity, :attributes => [:title])
+ end
+ end
+
+ test "each validator expects attributes to be given" do
+ assert_raise RuntimeError do
+ Topic.validates_with(ValidatorPerEachAttribute)
+ end
+ end
+
+ test "each validator skip nil values if :allow_nil is set to true" do
+ Topic.validates_with(ValidatorPerEachAttribute, :attributes => [:title, :content], :allow_nil => true)
+ topic = Topic.new :content => ""
+ assert !topic.valid?
+ assert topic.errors[:title].empty?
+ assert_equal ["Value is "], topic.errors[:content]
+ end
+
+ test "each validator skip blank values if :allow_blank is set to true" do
+ Topic.validates_with(ValidatorPerEachAttribute, :attributes => [:title, :content], :allow_blank => true)
+ topic = Topic.new :content => ""
+ assert topic.valid?
+ assert topic.errors[:title].empty?
+ assert topic.errors[:content].empty?
+ end
end
View
6 activemodel/test/cases/validations_test.rb
@@ -71,6 +71,12 @@ def test_multiple_errors_per_attr_iteration_with_full_error_composition
assert_equal 2, r.errors.count
end
+ def test_errors_on_nested_attributes_expands_name
+ t = Topic.new
+ t.errors["replies.name"] << "can't be blank"
+ assert_equal ["Replies name can't be blank"], t.errors.full_messages
+ end
+
def test_errors_on_base
r = Reply.new
r.content = "Mismatch"
View
5 activerecord/CHANGELOG
@@ -2,6 +2,11 @@
* Changed ActiveRecord::Base.store_full_sti_class to be true by default reflecting the previously announced Rails 3 default [DHH]
+* Add Relation#except. [Pratik Naik]
+
+ one_red_item = Item.where(:colour => 'red').limit(1)
+ all_items = one_red_item.except(:where, :limit)
+
* Add Relation#delete_all. [Pratik Naik]
Item.where(:colour => 'red').delete_all
View
1 activerecord/lib/active_record.rb
@@ -55,6 +55,7 @@ module ActiveRecord
autoload :FinderMethods
autoload :CalculationMethods
autoload :PredicateBuilder
+ autoload :SpawnMethods
end
autoload :Base
View
81 activerecord/lib/active_record/associations.rb
@@ -1465,8 +1465,7 @@ def add_touch_callbacks(reflection, touch_attribute)
after_destroy(method_name)
end
- def find_with_associations(options = {}, join_dependency = nil)
- join_dependency ||= JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins])
+ def find_with_associations(options, join_dependency)
rows = select_all_rows(options, join_dependency)
join_dependency.instantiate(rows)
rescue ThrowResult
@@ -1770,84 +1769,6 @@ def construct_finder_sql_for_association_limiting(options, join_dependency)
relation.to_sql
end
- def tables_in_string(string)
- return [] if string.blank?
- string.scan(/([a-zA-Z_][\.\w]+).?\./).flatten
- end
-
- def tables_in_hash(hash)
- return [] if hash.blank?
- tables = hash.map do |key, value|
- if value.is_a?(Hash)
- key.to_s
- else
- tables_in_string(key) if key.is_a?(String)
- end
- end
- tables.flatten.compact
- end
-
- def conditions_tables(options)
- # look in both sets of conditions
- conditions = [scope(:find, :conditions), options[:conditions]].inject([]) do |all, cond|
- case cond
- when nil then all
- when Array then all << tables_in_string(cond.first)
- when Hash then all << tables_in_hash(cond)
- else all << tables_in_string(cond)
- end
- end
- conditions.flatten
- end
-
- def order_tables(options)
- order = [options[:order], scope(:find, :order) ].join(", ")
- return [] unless order && order.is_a?(String)
- tables_in_string(order)
- end
-
- def selects_tables(options)
- select = options[:select]
- return [] unless select && select.is_a?(String)
- tables_in_string(select)
- end
-
- def joined_tables(options)
- scope = scope(:find)
- joins = options[:joins]
- merged_joins = scope && scope[:joins] && joins ? merge_joins(scope[:joins], joins) : (joins || scope && scope[:joins])
- [table_name] + case merged_joins
- when Symbol, Hash, Array
- if array_of_strings?(merged_joins)
- tables_in_string(merged_joins.join(' '))
- else
- join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_joins, nil)
- join_dependency.join_associations.collect {|join_association| [join_association.aliased_join_table_name, join_association.aliased_table_name]}.flatten.compact
- end
- else
- tables_in_string(merged_joins)
- end
- end
-
- # Checks if the conditions reference a table other than the current model table
- def include_eager_conditions?(options, tables = nil, joined_tables = nil)
- ((tables || conditions_tables(options)) - (joined_tables || joined_tables(options))).any?
- end
-
- # Checks if the query order references a table other than the current model's table.
- def include_eager_order?(options, tables = nil, joined_tables = nil)
- ((tables || order_tables(options)) - (joined_tables || joined_tables(options))).any?
- end
-
- def include_eager_select?(options, joined_tables = nil)
- (selects_tables(options) - (joined_tables || joined_tables(options))).any?
- end
-
- def references_eager_loaded_tables?(options)
- joined_tables = joined_tables(options)
- include_eager_order?(options, nil, joined_tables) || include_eager_conditions?(options, nil, joined_tables) || include_eager_select?(options, joined_tables)
- end
-
def using_limitable_reflections?(reflections)
reflections.reject { |r| [ :belongs_to, :has_one ].include?(r.macro) }.length.zero?
end
View
4 activerecord/lib/active_record/associations/association_collection.rb
@@ -21,7 +21,7 @@ def initialize(owner, reflection)
construct_sql
end
- delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :from, :lock, :readonly, :having, :to => :scoped
+ delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :to => :scoped
def select(select = nil, &block)
if block_given?
@@ -58,7 +58,7 @@ def find(*args)
find_scope = construct_scope[:find].slice(:conditions, :order)
with_scope(:find => find_scope) do
- relation = @reflection.klass.send(:construct_finder_arel_with_includes, options)
+ relation = @reflection.klass.send(:construct_finder_arel, options)
case args.first
when :first, :last, :all
View
2 activerecord/lib/active_record/autosave_association.rb
@@ -267,7 +267,7 @@ def association_valid?(reflection, association)
unless valid = association.valid?
if reflection.options[:autosave]
association.errors.each do |attribute, message|
- attribute = "#{reflection.name}_#{attribute}"
+ attribute = "#{reflection.name}.#{attribute}"
errors[attribute] << message if errors[attribute].empty?
end
else
View
34 activerecord/lib/active_record/base.rb
@@ -645,7 +645,7 @@ def find(*args)
options = args.extract_options!
set_readonly_option!(options)
- relation = construct_finder_arel_with_includes(options)
+ relation = construct_finder_arel(options)
case args.first
when :first, :last, :all
@@ -655,7 +655,7 @@ def find(*args)
end
end
- delegate :select, :group, :order, :limit, :joins, :where, :preload, :eager_load, :from, :lock, :readonly, :having, :to => :scoped
+ delegate :select, :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :to => :scoped
# A convenience wrapper for <tt>find(:first, *args)</tt>. You can pass in all the
# same arguments to this method as you can to <tt>find(:first)</tt>.
@@ -1510,11 +1510,17 @@ def active_relation
end
def active_relation_table(table_name_alias = nil)
- Arel::Table.new(table_name, :as => table_name_alias)
+ Arel::Table.new(table_name, :as => table_name_alias, :engine => active_relation_engine)
end
def active_relation_engine
- @active_relation_engine ||= Arel::Sql::Engine.new(self)
+ @active_relation_engine ||= begin
+ if self == ActiveRecord::Base
+ Arel::Table.engine
+ else
+ connection_handler.connection_pools[name] ? Arel::Sql::Engine.new(self) : superclass.active_relation_engine
+ end
+ end
end
private
@@ -1579,7 +1585,8 @@ def construct_finder_arel(options = {}, scope = scope(:find))
order(construct_order(options[:order], scope)).
limit(construct_limit(options[:limit], scope)).
offset(construct_offset(options[:offset], scope)).
- from(options[:from])
+ from(options[:from]).
+ includes( merge_includes(scope && scope[:include], options[:include]))
lock = (scope && scope[:lock]) || options[:lock]
relation = relation.lock if lock.present?
@@ -1589,21 +1596,6 @@ def construct_finder_arel(options = {}, scope = scope(:find))
relation
end
- def construct_finder_arel_with_includes(options = {})
- relation = construct_finder_arel(options)
- include_associations = merge_includes(scope(:find, :include), options[:include])
-
- if include_associations.any?
- if references_eager_loaded_tables?(options)
- relation = relation.eager_load(include_associations)
- else
- relation = relation.preload(include_associations)
- end
- end
-
- relation
- end
-
def construct_join(joins, scope)
merged_joins = scope && scope[:joins] && joins ? merge_joins(scope[:joins], joins) : (joins || scope && scope[:joins])
case merged_joins
@@ -1722,7 +1714,7 @@ def method_missing(method_id, *arguments, &block)
super unless all_attributes_exists?(attribute_names)
if match.finder?
options = arguments.extract_options!
- relation = options.any? ? construct_finder_arel_with_includes(options) : scoped
+ relation = options.any? ? construct_finder_arel(options) : scoped
relation.send :find_by_attributes, match, attribute_names, *arguments
elsif match.instantiator?
scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block
View
2 activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -201,7 +201,7 @@ def log_info(sql, name, ms)
protected
def log(sql, name)
result = nil
- ActiveSupport::Notifications.instrument(:sql, :sql => sql, :name => name) do
+ ActiveSupport::Notifications.instrument("active_record.sql", :sql => sql, :name => name) do
@runtime += Benchmark.ms { result = yield }
end
result
View
3 activerecord/lib/active_record/locale/en.yml
@@ -1,6 +1,9 @@
en:
activerecord:
errors:
+ # model.errors.full_messages format.
+ format: "{{attribute}} {{message}}"
+
# The values :model, :attribute and :value are always available for interpolation
# The value :count is available when applicable. Can be used for pluralization.
messages:
View
2 activerecord/lib/active_record/named_scope.rb
@@ -29,7 +29,7 @@ def scoped(options = {}, &block)
unless scoped?(:find)
finder_needs_type_condition? ? active_relation.where(type_condition) : active_relation.spawn
else
- construct_finder_arel_with_includes
+ construct_finder_arel
end
end
end
View
2 activerecord/lib/active_record/railtie.rb
@@ -62,7 +62,7 @@ class Railtie < Rails::Railtie
initializer "active_record.notifications" do
require 'active_support/notifications'
- ActiveSupport::Notifications.subscribe("sql") do |name, before, after, instrumenter_id, payload|
+ ActiveSupport::Notifications.subscribe("active_record.sql") do |name, before, after, instrumenter_id, payload|
ActiveRecord::Base.connection.log_info(payload[:sql], payload[:name], (after - before) * 1000)
end
end
View
87 activerecord/lib/active_record/relation.rb
@@ -1,36 +1,32 @@
module ActiveRecord
class Relation
- include QueryMethods, FinderMethods, CalculationMethods
+ include QueryMethods, FinderMethods, CalculationMethods, SpawnMethods
- delegate :to_sql, :to => :relation
delegate :length, :collect, :map, :each, :all?, :to => :to_a
- attr_reader :relation, :klass, :preload_associations, :eager_load_associations
- attr_writer :readonly, :preload_associations, :eager_load_associations, :table
+ attr_reader :relation, :klass
+ attr_writer :readonly, :table
+ attr_accessor :preload_associations, :eager_load_associations, :includes_associations, :create_with_attributes
def initialize(klass, relation)
@klass, @relation = klass, relation
@preload_associations = []
@eager_load_associations = []
+ @includes_associations = []
@loaded, @readonly = false
end
- def merge(r)
- raise ArgumentError, "Cannot merge a #{r.klass.name} relation with #{@klass.name} relation" if r.klass != @klass
+ def new(*args, &block)
+ with_create_scope { @klass.new(*args, &block) }
+ end
- joins(r.relation.joins(r.relation)).
- group(r.send(:group_clauses).join(', ')).
- order(r.send(:order_clauses).join(', ')).
- where(r.send(:where_clause)).
- limit(r.taken).
- offset(r.skipped).
- select(r.send(:select_clauses).join(', ')).
- eager_load(r.eager_load_associations).
- preload(r.preload_associations).
- from(r.send(:sources).present? ? r.send(:from_clauses) : nil)
+ def create(*args, &block)
+ with_create_scope { @klass.create(*args, &block) }
end
- alias :& :merge
+ def create!(*args, &block)
+ with_create_scope { @klass.create!(*args, &block) }
+ end
def respond_to?(method, include_private = false)
return true if @relation.respond_to?(method, include_private) || Array.method_defined?(method)
@@ -47,19 +43,21 @@ def respond_to?(method, include_private = false)
def to_a
return @records if loaded?
- @records = if @eager_load_associations.any?
+ find_with_associations = @eager_load_associations.any? || references_eager_loaded_tables?
+
+ @records = if find_with_associations
begin
@klass.send(:find_with_associations, {
:select => @relation.send(:select_clauses).join(', '),
:joins => @relation.joins(relation),
:group => @relation.send(:group_clauses).join(', '),
- :order => @relation.send(:order_clauses).join(', '),
+ :order => order_clause,
:conditions => where_clause,
:limit => @relation.taken,
:offset => @relation.skipped,
:from => (@relation.send(:from_clauses) if @relation.send(:sources).present?)
},
- ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, @eager_load_associations, nil))
+ ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, @eager_load_associations + @includes_associations, nil))
rescue ThrowResult
[]
end
@@ -67,7 +65,10 @@ def to_a
@klass.find_by_sql(@relation.to_sql)
end
- @preload_associations.each {|associations| @klass.send(:preload_associations, @records, associations) }
+ preload = @preload_associations
+ preload += @includes_associations unless find_with_associations
+ preload.each {|associations| @klass.send(:preload_associations, @records, associations) }
+
@records.each { |record| record.readonly! } if @readonly
@loaded = true
@@ -123,28 +124,23 @@ def reload
end
def reset
- @first = @last = nil
+ @first = @last = @to_sql = @order_clause = @scope_for_create = nil
@records = []
self
end
- def spawn(relation = @relation)
- relation = self.class.new(@klass, relation)
- relation.readonly = @readonly
- relation.preload_associations = @preload_associations
- relation.eager_load_associations = @eager_load_associations
- relation.table = table
- relation
- end
-
def table
- @table ||= Arel::Table.new(@klass.table_name, Arel::Sql::Engine.new(@klass))
+ @table ||= Arel::Table.new(@klass.table_name, :engine => @klass.active_relation_engine)
end
def primary_key
@primary_key ||= table[@klass.primary_key]
end
+ def to_sql
+ @to_sql ||= @relation.to_sql
+ end
+
protected
def method_missing(method, *args, &block)
@@ -166,9 +162,36 @@ def method_missing(method, *args, &block)
end
end
+ def with_create_scope
+ @klass.send(:with_scope, :create => scope_for_create) { yield }
+ end
+
+ def scope_for_create
+ @scope_for_create ||= begin
+ @create_with_attributes || wheres.inject({}) do |hash, where|
+ hash[where.operand1.name] = where.operand2.value if where.is_a?(Arel::Predicates::Equality)
+ hash
+ end
+ end
+ end
+
def where_clause(join_string = " AND ")
@relation.send(:where_clauses).join(join_string)
end
+ def order_clause
+ @order_clause ||= @relation.send(:order_clauses).join(', ')
+ end
+
+ def references_eager_loaded_tables?
+ joined_tables = (tables_in_string(@relation.joins(relation)) + [table.name, table.table_alias]).compact.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
+ end
+
end
end
View
8 activerecord/lib/active_record/relation/query_methods.rb
@@ -5,6 +5,10 @@ def preload(*associations)
spawn.tap {|r| r.preload_associations += Array.wrap(associations) }
end
+ def includes(*associations)
+ spawn.tap {|r| r.includes_associations += Array.wrap(associations) }
+ end
+
def eager_load(*associations)
spawn.tap {|r| r.eager_load_associations += Array.wrap(associations) }
end
@@ -13,6 +17,10 @@ def readonly(status = true)
spawn.tap {|r| r.readonly = status }
end
+ def create_with(attributes = {})
+ spawn.tap {|r| r.create_with_attributes = attributes }
+ end
+
def select(selects)
if selects.present?
relation = spawn(@relation.project(selects))
View
85 activerecord/lib/active_record/relation/spawn_methods.rb
@@ -0,0 +1,85 @@
+module ActiveRecord
+ module SpawnMethods
+ def spawn(relation = @relation)
+ relation = Relation.new(@klass, relation)
+ relation.readonly = @readonly
+ relation.preload_associations = @preload_associations
+ relation.eager_load_associations = @eager_load_associations
+ relation.includes_associations = @includes_associations
+ relation.create_with_attributes = @create_with_attributes
+ relation.table = table
+ relation
+ end
+
+ def merge(r)
+ raise ArgumentError, "Cannot merge a #{r.klass.name} relation with #{@klass.name} relation" if r.klass != @klass
+
+ merged_relation = spawn(table).eager_load(r.eager_load_associations).preload(r.preload_associations).includes(r.includes_associations)
+ merged_relation.readonly = r.readonly
+
+ [self.relation, r.relation].each do |arel|
+ merged_relation = merged_relation.
+ joins(arel.joins(arel)).
+ group(arel.groupings).
+ limit(arel.taken).
+ offset(arel.skipped).
+ select(arel.send(:select_clauses)).
+ from(arel.sources).
+ having(arel.havings).
+ lock(arel.locked)
+ end
+
+ relation_order = r.send(:order_clause)
+ merged_order = relation_order.present? ? relation_order : order_clause
+ merged_relation = merged_relation.order(merged_order)
+
+ merged_relation.create_with_attributes = @create_with_attributes
+
+ if @create_with_attributes && r.create_with_attributes
+ merged_relation.create_with_attributes = @create_with_attributes.merge(r.create_with_attributes)
+ else
+ merged_relation.create_with_attributes = r.create_with_attributes || @create_with_attributes
+ end
+
+ merged_wheres = @relation.wheres
+
+ r.wheres.each do |w|
+ if w.is_a?(Arel::Predicates::Equality)
+ merged_wheres = merged_wheres.reject {|p| p.is_a?(Arel::Predicates::Equality) && p.operand1.name == w.operand1.name }
+ end
+
+ merged_wheres << w
+ end
+
+ merged_relation.where(*merged_wheres)
+ end
+
+ alias :& :merge
+
+ def except(*skips)
+ result = Relation.new(@klass, table)
+ result.table = table
+
+ [:eager_load, :preload, :includes].each do |load_method|
+ result = result.send(load_method, send(:"#{load_method}_associations"))
+ end
+
+ result.readonly = self.readonly unless skips.include?(:readonly)
+ result.create_with_attributes = @create_with_attributes unless skips.include?(:create_with)
+
+ result = result.joins(@relation.joins(@relation)) unless skips.include?(:joins)
+ result = result.group(@relation.groupings) unless skips.include?(:group)
+ result = result.limit(@relation.taken) unless skips.include?(:limit)
+ result = result.offset(@relation.skipped) unless skips.include?(:offset)
+ result = result.select(@relation.send(:select_clauses)) unless skips.include?(:select)
+ result = result.from(@relation.sources) unless skips.include?(:from)
+ result = result.order(order_clause) unless skips.include?(:order)
+ result = result.where(*@relation.wheres) unless skips.include?(:where)
+ result = result.having(*@relation.havings) unless skips.include?(:having)
+ result = result.lock(@relation.locked) unless skips.include?(:lock)
+
+ result
+ end
+
+ end
+end
View
31 activerecord/test/cases/autosave_association_test.rb
@@ -786,14 +786,14 @@ def test_should_automatically_save_bang_the_associated_model
def test_should_automatically_validate_the_associated_model
@pirate.ship.name = ''
assert @pirate.invalid?
- assert @pirate.errors[:ship_name].any?
+ assert @pirate.errors[:"ship.name"].any?
end
def test_should_merge_errors_on_the_associated_models_onto_the_parent_even_if_it_is_not_valid
@pirate.ship.name = nil
@pirate.catchphrase = nil
assert @pirate.invalid?
- assert @pirate.errors[:ship_name].any?
+ assert @pirate.errors[:"ship.name"].any?
assert @pirate.errors[:catchphrase].any?
end
@@ -886,7 +886,7 @@ def test_should_automatically_save_bang_the_associated_model
def test_should_automatically_validate_the_associated_model
@ship.pirate.catchphrase = ''
assert @ship.invalid?
- assert @ship.errors[:pirate_catchphrase].any?
+ assert @ship.errors[:"pirate.catchphrase"].any?
end
def test_should_merge_errors_on_the_associated_model_onto_the_parent_even_if_it_is_not_valid
@@ -894,7 +894,7 @@ def test_should_merge_errors_on_the_associated_model_onto_the_parent_even_if_it_
@ship.pirate.catchphrase = nil
assert @ship.invalid?
assert @ship.errors[:name].any?
- assert @ship.errors[:pirate_catchphrase].any?