提交 5e94d3e3 编写于 作者: D David Heinemeier Hansson

Merge

*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
......
......@@ -11,7 +11,7 @@
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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......@@ -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
......
......@@ -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
......@@ -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:
......
......@@ -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]}"
......
......@@ -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
......
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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......@@ -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
......
......@@ -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
......@@ -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
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
# ------
#
......
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:
......
......@@ -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
......
......@@ -4,6 +4,8 @@ class LintTest < ActiveModel::TestCase
include ActiveModel::Lint::Tests
class CompliantModel
extend ActiveModel::Naming
def to_model
self
end
......
......@@ -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
......
......@@ -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
......@@ -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"
......
......@@ -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
......
......@@ -55,6 +55,7 @@ module ActiveRecord
autoload :FinderMethods
autoload :CalculationMethods
autoload :PredicateBuilder
autoload :SpawnMethods
end
autoload :Base
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
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:
......
......@@ -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
......
......@@ -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
......
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
......@@ -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))
......
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
......@@ -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?
assert @ship.errors[:"pirate.catchphrase"].any?
end
def test_should_still_allow_to_bypass_validations_on_the_associated_model
......@@ -961,7 +961,7 @@ def test_should_automatically_validate_the_associated_models
@pirate.send(@association_name).each { |child| child.name = '' }
assert !@pirate.valid?
assert_equal ["can't be blank"], @pirate.errors["#{@association_name}_name"]
assert_equal ["can't be blank"], @pirate.errors["#{@association_name}.name"]
assert @pirate.errors[@association_name].empty?
end
......@@ -969,16 +969,33 @@ def test_should_not_use_default_invalid_error_on_associated_models
@pirate.send(@association_name).build(:name => '')
assert !@pirate.valid?
assert_equal ["can't be blank"], @pirate.errors["#{@association_name}_name"]
assert_equal ["can't be blank"], @pirate.errors["#{@association_name}.name"]
assert @pirate.errors[@association_name].empty?
end
def test_should_default_invalid_error_from_i18n
I18n.backend.store_translations(:en, :activerecord => { :errors => { :models =>
{ @association_name.to_s.singularize.to_sym => { :blank => "cannot be blank" } }
}})
@pirate.send(@association_name).build(:name => '')
assert !@pirate.valid?
assert_equal ["cannot be blank"], @pirate.errors["#{@association_name}.name"]
assert_equal ["#{@association_name.to_s.titleize} name cannot be blank"], @pirate.errors.full_messages
assert @pirate.errors[@association_name].empty?
ensure
I18n.backend.store_translations(:en, :activerecord => { :errors => { :models =>
{ @association_name.to_s.singularize.to_sym => nil }
}})
end
def test_should_merge_errors_on_the_associated_models_onto_the_parent_even_if_it_is_not_valid
@pirate.send(@association_name).each { |child| child.name = '' }
@pirate.catchphrase = nil
assert !@pirate.valid?
assert_equal ["can't be blank"], @pirate.errors["#{@association_name}_name"]
assert_equal ["can't be blank"], @pirate.errors["#{@association_name}.name"]
assert @pirate.errors[:catchphrase].any?
end
......
require "cases/helper"
require 'models/entrant'
require 'models/bird'
# So we can test whether Course.connection survives a reload.
require_dependency 'models/course'
......@@ -82,4 +83,9 @@ def test_transactions_across_databases
assert_equal "Ruby Development", Course.find(1).name
assert_equal "Ruby Developer", Entrant.find(1).name
end
def test_arel_table_engines
assert_not_equal Entrant.active_relation_engine, Course.active_relation_engine
assert_equal Entrant.active_relation_engine, Bird.active_relation_engine
end
end
......@@ -592,7 +592,7 @@ def test_validate_presence_of_parent__fails_without_inverse_of
assert_no_difference ['Man.count', 'Interest.count'] do
man = Man.create(:name => 'John',
:interests_attributes => [{:topic=>'Cars'}, {:topic=>'Sports'}])
assert !man.errors[:interests_man].empty?
assert !man.errors[:"interests.man"].empty?
end
end
# restore :inverse_of
......
......@@ -10,6 +10,7 @@
require 'models/entrant'
require 'models/developer'
require 'models/company'
require 'models/bird'
class RelationTest < ActiveRecord::TestCase
fixtures :authors, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :posts, :comments,
......@@ -177,7 +178,7 @@ def test_eager_association_loading_of_stis_with_multiple_references
end
end
def test_find_with_included_associations
def test_find_with_preloaded_associations
assert_queries(2) do
posts = Post.preload(:comments)
assert posts.first.comments.first
......@@ -205,6 +206,29 @@ def test_find_with_included_associations
end
end
def test_find_with_included_associations
assert_queries(2) do
posts = Post.includes(:comments)
assert posts.first.comments.first
end
assert_queries(2) do
posts = Post.scoped.includes(:comments)
assert posts.first.comments.first
end
assert_queries(2) do
posts = Post.includes(:author)
assert posts.first.author
end
assert_queries(3) do
posts = Post.includes(:author, :comments).to_a
assert posts.first.author
assert posts.first.comments.first
end
end
def test_default_scope_with_conditions_string
assert_equal Developer.find_all_by_name('David').map(&:id).sort, DeveloperCalledDavid.scoped.map(&:id).sort
assert_equal nil, DeveloperCalledDavid.create!.name
......@@ -383,6 +407,11 @@ def test_relation_merging_with_eager_load
end
end
def test_relation_merging_with_locks
devs = Developer.lock.where("salary >= 80000").order("id DESC") & Developer.limit(2)
assert devs.locked.present?
end
def test_relation_merging_with_preload
[Post.scoped & Post.preload(:author), Post.preload(:author) & Post.scoped].each do |posts|
assert_queries(2) { assert posts.first.author }
......@@ -477,4 +506,62 @@ def test_many_with_limits
assert posts.many?
assert ! posts.limit(1).many?
end
def test_build
posts = Post.scoped
post = posts.new
assert_kind_of Post, post
end
def test_scoped_build
posts = Post.where(:title => 'You told a lie')
post = posts.new
assert_kind_of Post, post
assert_equal 'You told a lie', post.title
end
def test_create
birds = Bird.scoped
sparrow = birds.create
assert_kind_of Bird, sparrow
assert sparrow.new_record?
hen = birds.where(:name => 'hen').create
assert ! hen.new_record?
assert_equal 'hen', hen.name
end
def test_create_bang
birds = Bird.scoped
assert_raises(ActiveRecord::RecordInvalid) { birds.create! }
hen = birds.where(:name => 'hen').create!
assert_kind_of Bird, hen
assert ! hen.new_record?
assert_equal 'hen', hen.name
end
def test_explicit_create_scope
hens = Bird.where(:name => 'hen')
assert_equal 'hen', hens.new.name
hens = hens.create_with(:name => 'cock')
assert_equal 'cock', hens.new.name
end
def test_except
relation = Post.where(:author_id => 1).order('id ASC').limit(1)
assert_equal [posts(:welcome)], relation.all
author_posts = relation.except(:order, :limit)
assert_equal Post.where(:author_id => 1).all, author_posts.all
all_posts = relation.except(:where, :order, :limit)
assert_equal Post.all, all_posts.all
end
end
......@@ -247,13 +247,13 @@ def expires_in(options)
expires_in || 0
end
def instrument(operation, key, options, &block)
def instrument(operation, key, options)
log(operation, key, options)
if self.class.instrument
payload = { :key => key }
payload.merge!(options) if options.is_a?(Hash)
ActiveSupport::Notifications.instrument(:"cache_#{operation}", payload, &block)
ActiveSupport::Notifications.instrument("active_support.cache_#{operation}", payload){ yield }
else
yield
end
......
class Integer
# Check whether the integer is evenly divisible by the argument.
def multiple_of?(number)
self % number == 0
number != 0 ? self % number == 0 : zero?
end
end
......@@ -5,6 +5,11 @@ class IntegerExtTest < Test::Unit::TestCase
def test_multiple_of
[ -7, 0, 7, 14 ].each { |i| assert i.multiple_of?(7) }
[ -7, 7, 14 ].each { |i| assert ! i.multiple_of?(6) }
# test the 0 edge case
assert 0.multiple_of?(0)
assert !5.multiple_of?(0)
# test with a prime
assert !22953686867719691230002707821868552601124472329079.multiple_of?(2)
assert !22953686867719691230002707821868552601124472329079.multiple_of?(3)
......
require 'abstract_unit'
require 'rbconfig'
# if defined?(MiniTest) || defined?(Test::Unit::TestResultFailureSupport)
# $stderr.puts "Isolation tests can test test-unit 1 only"
if defined?(MiniTest) || defined?(Test::Unit::TestResultFailureSupport)
$stderr.puts "Isolation tests can test test-unit 1 only"
if ENV['CHILD']
elsif ENV['CHILD']
class ChildIsolationTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation
......
Subproject commit c6805fb93da30e0056b38e0fa6015c3d1bca5876
Subproject commit 1ffa95c55394c862798727ac8b203ecedda8842a
......@@ -8,7 +8,7 @@ class Application
class << self
attr_writer :config
alias configure class_eval
delegate :initialize!, :load_tasks, :to => :instance
delegate :initialize!, :load_tasks, :root, :to => :instance
private :new
def instance
......@@ -267,18 +267,5 @@ def call(env)
ActiveSupport::Dependencies.unhook!
end
end
# For each framework, search for instrument file with Notifications hooks.
#
initializer :load_notifications_hooks do
frameworks = [ :active_record, :action_controller, :action_view,
:action_mailer, :active_resource ]
frameworks.each do |framework|
begin
require "#{framework}/notifications"
rescue LoadError => e
end
end
end
end
end
......@@ -10,7 +10,7 @@
require 'active_support/core_ext/string/inflections'
# TODO: Do not always push on vendored thor
$LOAD_PATH.unshift("#{File.dirname(__FILE__)}/vendor/thor-0.12.1/lib")
$LOAD_PATH.unshift("#{File.dirname(__FILE__)}/vendor/thor-0.12.3/lib")
require 'rails/generators/base'
require 'rails/generators/named_base'
......@@ -117,11 +117,15 @@ def self.fallbacks
end
# Remove the color from output.
#
def self.no_color!
Thor::Base.shell = Thor::Shell::Basic
end
# Track all generators subclasses.
def self.subclasses
@subclasses ||= []
end
# Generators load paths used on lookup. The lookup happens as:
#
# 1) lib generators
......@@ -147,18 +151,10 @@ def self.load_paths
end
load_paths # Cache load paths. Needed to avoid __FILE__ pointing to wrong paths.
# Rails finds namespaces exactly as thor, with three conveniences:
#
# 1) If your generator name ends with generator, as WebratGenerator, it sets
# its namespace to "webrat", so it can be invoked as "webrat" and not
# "webrat_generator";
# Rails finds namespaces similar to thor, it only adds one rule:
#
# 2) If your generator has a generators namespace, as Rails::Generators::WebratGenerator,
# the namespace is set to "rails:generators:webrat", but Rails allows it
# to be invoked simply as "rails:webrat". The "generators" is added
# automatically when doing the lookup;
#
# 3) Rails looks in load paths and loads the generator just before it's going to be used.
# Generators names must end with "_generator.rb". This is required because Rails
# looks in load paths and loads the generator just before it's going to be used.
#
# ==== Examples
#
......@@ -166,113 +162,81 @@ def self.load_paths
#
# Will search for the following generators:
#
# "rails:generators:webrat", "webrat:generators:integration", "webrat"
#
# On the other hand, if "rails:webrat" is given, it will search for:
# "rails:webrat", "webrat:integration", "webrat"
#
# "rails:generators:webrat", "rails:webrat"
#
# Notice that the "generators" namespace is handled automatically by Rails,
# so you don't need to type it when you want to invoke a generator in specific.
# Notice that "rails:generators:webrat" could be loaded as well, what
# Rails looks for is the first and last parts of the namespace.
#
def self.find_by_namespace(name, base=nil, context=nil) #:nodoc:
name, attempts = name.to_s, [ ]
case name.count(':')
when 1
base, name = name.split(':')
return find_by_namespace(name, base)
when 0
attempts += generator_names(base, name) if base
attempts += generator_names(name, context) if context
end
attempts << name
attempts += generator_names(name, name) unless name.include?(?:)
attempts.uniq!
unloaded = attempts - namespaces
lookup(unloaded)
# Mount regexps to lookup
regexps = []
regexps << /^#{base}:[\w:]*#{name}$/ if base
regexps << /^#{name}:[\w:]*#{context}$/ if context
regexps << /^[(#{name}):]+$/
regexps.uniq!
# Check if generator happens to be loaded
checked = subclasses.dup
klass = find_by_regexps(regexps, checked)
return klass if klass
# Try to require other generators by looking in load_paths
lookup(name, context)
unchecked = subclasses - checked
klass = find_by_regexps(regexps, unchecked)
return klass if klass
# Invoke fallbacks
invoke_fallbacks_for(name, base) || invoke_fallbacks_for(context, name)
end
attempts.each do |namespace|
klass = Thor::Util.find_by_namespace(namespace)
return klass if klass
# Tries to find a generator which the namespace match the regexp.
def self.find_by_regexps(regexps, klasses)
klasses.find do |klass|
namespace = klass.namespace
regexps.find { |r| namespace =~ r }
end
invoke_fallbacks_for(name, base) || invoke_fallbacks_for(context, name)
end
# Receives a namespace, arguments and the behavior to invoke the generator.
# It's used as the default entry point for generate, destroy and update
# commands.
#
def self.invoke(namespace, args=ARGV, config={})
if klass = find_by_namespace(namespace, "rails")
names = namespace.to_s.split(':')
if klass = find_by_namespace(names.pop, names.shift || "rails")
args << "--help" if klass.arguments.any? { |a| a.required? } && args.empty?
klass.start args, config
klass.start(args, config)
else
puts "Could not find generator #{namespace}."
end
end
# Show help message with available generators.
#
def self.help
rails = Rails::Generators.builtin.map do |group, name|
name if group == "rails"
end
rails.compact!
rails.sort!
puts "Please select a generator."
puts "Builtin: #{rails.join(', ')}."
# Load paths and remove builtin
paths, others = load_paths.dup, []
paths.pop
paths.each do |path|
tail = [ "*", "*", "*_generator.rb" ]
until tail.empty?
others += Dir[File.join(path, *tail)].collect do |file|
name = file.split('/')[-tail.size, 2]
name.last.sub!(/_generator\.rb$/, '')
name.uniq!
name.join(':')
end
tail.shift
end
end
builtin = Rails::Generators.builtin.each { |n| n.sub!(/^rails:/, '') }
builtin.sort!
lookup("*")
others = subclasses.map{ |k| k.namespace.gsub(':generators:', ':') }
others -= Rails::Generators.builtin
others.sort!
puts "Please select a generator."
puts "Builtin: #{builtin.join(', ')}."
puts "Others: #{others.join(', ')}." unless others.empty?
end
protected
# Return all defined namespaces.
#
def self.namespaces #:nodoc:
Thor::Base.subclasses.map { |klass| klass.namespace }
end
# Keep builtin generators in an Array[Array[group, name]].
#
# Keep builtin generators in an Array.
def self.builtin #:nodoc:
Dir[File.dirname(__FILE__) + '/generators/*/*'].collect do |file|
file.split('/')[-2, 2]
file.split('/')[-2, 2].join(':')
end
end
# By default, Rails strips the generator namespace to make invocations
# easier. This method generaters the both possibilities names.
def self.generator_names(first, second) #:nodoc:
[ "#{first}:generators:#{second}", "#{first}:#{second}" ]
end
# Try callbacks for the given base.
#
# Try fallbacks for the given base.
def self.invoke_fallbacks_for(name, base) #:nodoc:
return nil unless base && fallbacks[base.to_sym]
invoked_fallbacks = []
......@@ -290,10 +254,10 @@ def self.invoke_fallbacks_for(name, base) #:nodoc:
# Receives namespaces in an array and tries to find matching generators
# in the load path.
#
def self.lookup(attempts) #:nodoc:
attempts = attempts.map { |a| "#{a.split(":").last}_generator" }.uniq
attempts = "{#{attempts.join(',')}}.rb"
def self.lookup(*attempts) #:nodoc:
attempts.compact!
attempts.uniq!
attempts = "{#{attempts.join(',')}}_generator.rb"
self.load_paths.each do |path|
Dir[File.join(path, '**', attempts)].each do |file|
......
......@@ -76,17 +76,18 @@ def self.namespace(name=nil)
#
# The controller generator will then try to invoke the following generators:
#
# "rails:generators:test_unit", "test_unit:generators:controller", "test_unit"
# "rails:test_unit", "test_unit:controller", "test_unit"
#
# In this case, the "test_unit:generators:controller" is available and is
# invoked. This allows any test framework to hook into Rails as long as it
# provides any of the hooks above.
# Notice that "rails:generators:test_unit" could be loaded as well, what
# Rails looks for is the first and last parts of the namespace. This is what
# allows any test framework to hook into Rails as long as it provides any
# of the hooks above.
#
# ==== Options
#
# This lookup can be customized with two options: :base and :as. The first
# is the root module value and in the example above defaults to "rails".
# The later defaults to the generator name, without the "Generator" ending.
# The first and last part used to find the generator to be invoked are
# guessed based on class invokes hook_for, as noticed in the example above.
# This can be customized with two options: :base and :as.
#
# Let's suppose you are creating a generator that needs to invoke the
# controller generator from test unit. Your first attempt is:
......@@ -97,7 +98,7 @@ def self.namespace(name=nil)
#
# The lookup in this case for test_unit as input is:
#
# "test_unit:generators:awesome", "test_unit"
# "test_unit:awesome", "test_unit"
#
# Which is not the desired the lookup. You can change it by providing the
# :as option:
......@@ -108,18 +109,18 @@ def self.namespace(name=nil)
#
# And now it will lookup at:
#
# "test_unit:generators:awesome", "test_unit"
# "test_unit:controller", "test_unit"
#
# Similarly, if you want it to also lookup in the rails namespace, you just
# need to provide the :base value:
#
# class AwesomeGenerator < Rails::Generators::Base
# hook_for :test_framework, :base => :rails, :as => :controller
# hook_for :test_framework, :in => :rails, :as => :controller
# end
#
# And the lookup is exactly the same as previously:
#
# "rails:generators:test_unit", "test_unit:generators:controller", "test_unit"
# "rails:test_unit", "test_unit:controller", "test_unit"
#
# ==== Switches
#
......@@ -151,11 +152,11 @@ def self.namespace(name=nil)
# ==== Custom invocations
#
# You can also supply a block to hook_for to customize how the hook is
# going to be invoked. The block receives two parameters, an instance
# going to be invoked. The block receives two arguments, an instance
# of the current class and the klass to be invoked.
#
# For example, in the resource generator, the controller should be invoked
# with a pluralized class name. By default, it is invoked with the same
# with a pluralized class name. But by default it is invoked with the same
# name as the resource generator, which is singular. To change this, we
# can give a block to customize how the controller can be invoked.
#
......@@ -178,11 +179,11 @@ def self.hook_for(*names, &block)
end
unless class_options.key?(name)
class_option name, defaults.merge!(options)
class_option(name, defaults.merge!(options))
end
hooks[name] = [ in_base, as_hook ]
invoke_from_option name, options, &block
invoke_from_option(name, options, &block)
end
end
......@@ -193,7 +194,7 @@ def self.hook_for(*names, &block)
# remove_hook_for :orm
#
def self.remove_hook_for(*names)
remove_invocation *names
remove_invocation(*names)
names.each do |name|
hooks.delete(name)
......@@ -219,12 +220,16 @@ def self.inherited(base) #:nodoc:
# and can point to wrong directions when inside an specified directory.
base.source_root
if base.name && base.name !~ /Base$/ && base.base_name && base.generator_name && defined?(Rails.root) && Rails.root
path = File.expand_path(File.join(Rails.root, 'lib', 'templates'))
if base.name.include?('::')
base.source_paths << File.join(path, base.base_name, base.generator_name)
else
base.source_paths << File.join(path, base.generator_name)
if base.name && base.name !~ /Base$/
Rails::Generators.subclasses << base
if defined?(Rails.root) && Rails.root
path = File.expand_path(File.join(Rails.root, 'lib', 'templates'))
if base.name.include?('::')
base.source_paths << File.join(path, base.base_name, base.generator_name)
else
base.source_paths << File.join(path, base.generator_name)
end
end
end
end
......@@ -267,7 +272,7 @@ def class_collisions(*class_names) #:nodoc:
# parameters.
#
def invoked?(args)
args.last.is_a?(Hash) && args.last.key?(:invocations)
args.last.is_a?(Hash) && (args.last.key?(:invocations) || args.last.key?(:destination_root))
end
# Use Rails default banner.
......@@ -290,12 +295,10 @@ def self.base_name
# Rails::Generators::MetalGenerator will return "metal" as generator name.
#
def self.generator_name
if name
@generator_name ||= begin
if klass_name = name.to_s.split('::').last
klass_name.sub!(/Generator$/, '')
klass_name.underscore
end
@generator_name ||= begin
if generator = name.to_s.split('::').last
generator.sub!(/Generator$/, '')
generator.underscore
end
end
end
......@@ -339,6 +342,7 @@ def self.hooks #:nodoc:
#
def self.prepare_for_invocation(name, value) #:nodoc:
if value && constants = self.hooks[name]
value = name if TrueClass === value
Rails::Generators.find_by_namespace(value, *constants)
else
super
......
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册