When we last left off we were just about to jump into Renderable#render
. Thatâ??s where we are going next:
# (line 193 action_pack/action_view/renderable.rb)
def render(view, local_assigns = {})
compile(local_assigns)
view.with_template self do
view.send(:_evaluate_assigns_and_ivars)
view.send(:_set_controller_content_type, mime_type) if respond_to?(:mime_type)
view.send(method_name(local_assigns), local_assigns) do |*names|
ivar = :@_proc_for_layout
if !view.instance_variable_defined?(:"@content_for_#{names.first}") && view.instance_variable_defined?(ivar) && (proc = view.instance_variable_get(ivar))
view.capture(*names, &proc)
elsif view.instance_variable_defined?(ivar = :"@content_for_#{names.first || :layout}")
view.instance_variable_get(ivar)
end
end
end
end
Alright, this is where we get real deep into Rails. This particular method does the vast majority of the rendering work in Rails. Letâ??s see whatâ??s going on:
compile(local_assigns)
This is the first line and it sets up a series of methods that we are going to run through. compile
is a private method in Renderable
:
# (line 193 action_pack/action_view/renderable.rb)
def compile(local_assigns)
render_symbol = method_name(local_assigns)
if !Base::CompiledTemplates.method_defined?(render_symbol) || recompile?
compile!(render_symbol, local_assigns)
end
end
which starts out creating a method symbol for this renderable object by calling another private method aptly named method_name
. This is the definition for method_name
:
# (line 45 action_pack/action_view/renderable.rb)
def method_name(local_assigns)
if local_assigns && local_assigns.any?
method_name = method_name_without_locals.dup
method_name << "_locals_#{local_assigns.keys.map { |k| k.to_s }.sort.join('_')}"
else
method_name = method_name_without_locals
end
method_name.to_sym
end
Here are the params for our example:
local_assigns => {}
Since our local_assigns
is empty, we end up executing the else part of the conditional which sets method_name
to the result method_name_without_locals
which is yet another private method in Renderable
. Here it is:
# (line 22 action_pack/action_view/renderable.rb)
def method_name_without_locals
['_run', extension, method_segment].compact.join('_')
end
This method gives us unique method name to use for the definition of the compiled rendered template. You may have seen something like the following in your Rails development logs:
Account Columns (251.9ms) SHOW FIELDS FROM `accounts`
AccountSettings Columns (4.2ms) SHOW FIELDS FROM `account_settings`
Account Load (0.5ms) SELECT * FROM `accounts` WHERE (`accounts`.`id` = 47715)
app/controllers/application.rb:144:in `current_user'
app/views/layouts/bare.rhtml:21:in `_run_rhtml_app47views47layouts47bare46rhtml'
app/controllers/administration_controller.rb:10:in `index'
vendor/plugins/mongrel_proctitle/lib/mongrel_proctitle.rb:114:in `process_client'
vendor/plugins/mongrel_proctitle/lib/mongrel_proctitle.rb:33:in `request'
vendor/plugins/mongrel_proctitle/lib/mongrel_proctitle.rb:112:in `process_client'
vendor/plugins/mongrel_proctitle/lib/mongrel_proctitle.rb:102:in `run'
You see that line:
app/views/layouts/bare.rhtml:21:in `_run_rhtml_app47views47layouts47bare46rhtml'
the _run_rhtml_app47views47layouts47bare46rhtml
is actually the name of the method that is given to your template. This is done because Rails executes your template, just like any piece of Ruby code. To do that, it needs a method name, and that is what we are doing in the method_name_without_locals
method, creating a unique method name for this template we are about to run. method_segment
is a method that is included in from ActionView::Template
and extension
is an attribute in ActionView::Template
. Here is method_segment
and extension
:
# (line 22 action_pack/action_view/template.rb)
def method_segment
relative_path.to_s.gsub(/([^a-zA-Z0-9_])/) { $1.ord }
end
extension => "erb"
which calls another method in Template
called relative_path
displayed here:
# (line 172 action_pack/action_view/template.rb)
def relative_path
path = File.expand_path(filename)
path.sub!(/^#{Regexp.escape(File.expand_path(RAILS_ROOT))}\//, '') if defined?(RAILS_ROOT)
path
end
First, we get the full pathname to the current template and then we run sub
on the path to remove anything that may look like the full expanded path of RAILS_ROOT
if RAILS_ROOT
is defined. The templateâ??s filename is stored in the attribute filename
. Here are the params:
path => app/views/message_mailer/contact_form.erb
filename => /example_app/app/views/message_mailer/contact_form.erb
As you can see, the main purpose for this method is to give us the relative path to the mailer. Now that we got our path, letâ??s see what method_segment
does to it:
relative_path.to_s.gsub(/([^a-zA-Z0-9_])/) { $1.ord }
We call gsub
on the path, using the less familiar block usage. This particular usage passes each matched string to the block for processing. In this case, we want to match any character that isnt alphanumeric or an â??_â?. We take each of these matches (which are the individual characters that matched our expression) and call ord
on them which returns their ASCII number. So in our case, method_segment
would end up returning app47views47message_mailer47contact_form46erb
.
Good, now we have our method_segment and we know what our extension is, so letâ??s see what we get from method_name_without_locals
for our example:
_run_erb_app47views47message_mailer47contact_form46erb
Whew, now weâ??re back to Renderable#method_name
with our method name and we return back from that to compile
. Letâ??s refresh our memories real quick:
# (line 193 action_pack/action_view/renderable.rb)
def compile(local_assigns)
render_symbol = method_name(local_assigns)
if !Base::CompiledTemplates.method_defined?(render_symbol) || recompile?
compile!(render_symbol, local_assigns)
end
end
and we get back for render_symbol
:
:_run_erb_app47views47message_mailer47contact_form46erb
The next section is where the super-crazy-secret-sauce magic happens, but first we have to do some checks. First, we check to see if our render_symbol
is defined as a method in the module ActionView::Base::CompiledTemplates
. Letâ??s take a gander at CompiledTemplates:
# (line 193 action_pack/action_view/renderable.rb)
module CompiledTemplates #:nodoc:
# holds compiled template code
end
What theâ?¦. Where is the code?
Well, remember that we just created a new method name for our template. Why did we do that? Because Rails needs to execute the template as with everything else. So we have to define the method somehow so that it can be executed, right? Exactly. That is what CompiledTemplates
is for. Itâ??s the placeholder module where we will keep these method definitions. Basically, itâ??s a nice container to hold these dynamically created methods so they arent spread out all over ObjectSpace
. Plus, it makes clearing the methods cache pretty simple, just undef every method in CompiledTemplates
. So first, we check to see if there is a method defined for this particular template. Then we check the private method recompile?
, which is so simple, that I think it may be a placeholder for something in the future. Here is recompile?
:
# (line 92 action_pack/action_view/renderable.rb)
def recompile?
false
end
So basically, we are just checking the first part of our condition for the method definition. In our case, we dont have a method defined as such and we go on to compile!
, which is a private method in Renderable
and looks like this:
# (line 66 action_pack/action_view/renderable.rb)
def compile!(render_symbol, local_assigns)
locals_code = local_assigns.keys.map { |key| "#{key} = local_assigns[:#{key}];" }.join
source = <<-end_src
def #{render_symbol}(local_assigns)
old_output_buffer = output_buffer;#{locals_code};#{compiled_source}
ensure
self.output_buffer = old_output_buffer
end
end_src
begin
ActionView::Base::CompiledTemplates.module_eval(source, filename, 0)
rescue Errno::ENOENT => e
raise e # Missing template file, re-raise for Base to rescue
rescue Exception => e # errors from template code
if logger = defined?(ActionController) && Base.logger
logger.debug "ERROR: compiling #{render_symbol} RAISED #{e}"
logger.debug "Function body: #{source}"
logger.debug "Backtrace: #{e.backtrace.join("\n")}"
end
raise ActionView::TemplateError.new(self, {}, e)
end
end
Here, we start by creating the string that will assign the local variables to their proper values and storing it in the variable locals_code
. Then we move on to creating the source code that we want dynamically created and inserted into CompiledTemplates
. This code uses the method compiled_source
which uses another method named handler
. handler
is a wrapper method for handler_class_for_extension
which is a class method belonging to Template
. This method is defined in TemplateHandlers
and extended by Template
. All of these methods are illustrated below:
# (line 66 action_pack/action_view/renderable.rb)
def handler
Template.handler_class_for_extension(extension)
end
def compiled_source
handler.call(self)
end
# (action_pack/action_view/template_handlers_.rb)
module ActionView #:nodoc:
module TemplateHandlers #:nodoc:
...
def self.extended(base)
base.register_default_template_handler :erb, TemplateHandlers::ERB
base.register_template_handler :rjs, TemplateHandlers::RJS
base.register_template_handler :builder, TemplateHandlers::Builder
base.register_template_handler :rhtml, TemplateHandlers::ERB
base.register_template_handler :rxml, TemplateHandlers::Builder
end
...
def registered_template_handler(extension)
extension && ``template_handlers[extension.to_sym]
end
...
def handler_class_for_extension(extension)
registered_template_handler(extension) || ``default_template_handlers
end
end
end
When TemplateHandlers
is extended it registers several extension handlers. You are probably familiar with all of them, so we wont go into what each extension is for. handler_class_for_extension
returns the result of registered_template_handler
or @default_template_handlers
. registered_template_handler
returns the proper handler (TemplateHandlers::ERB, TemplateHandlers::RJS or TemplateHandlers::MyCustomTemplateHandler) from the class variable @template_handlers
which holds the defaults plus any newly registered templates.
So we get the proper handler back (in our case ActionView::TemplateHandlers::ERB
) and we execute the call
method with our ReloadableTemplate
as the parameter. call
is a class method in the module TemplateHandlers::Compilable
and included into ERB
. It looks like this:
# (line 4 action_pack/action_view/template_handlers_.rb)
module Compilable
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def call(template)
new.compile(template)
end
end
def compile(template)
raise "Need to implement #{self.class.name}#compile(template)"
end
end
When we include this module we extend the base class with the module ClassMethods
which contains the call
method. This method creates a new instance of the base class and calls the compile
instance method with the template. The compile
in this module raises an error to let you know that it is an abstract method and needs to be defined by the base class, in our case ERB
. Letâ??s see what ERB
looks like:
# (line 4 action_pack/action_view/template_handlers/erb.rb)
class ERB < TemplateHandler
include Compilable
cattr_accessor :erb_trim_mode
self.erb_trim_mode = '-'
...
def compile(template)
src = ::ERB.new("<% __in_erb_template=true %>#{template.source}", nil, erb_trim_mode, '@output_buffer').src
RUBY_VERSION >= '1.9' ? src.sub(/\A#coding:.*\n/, '') : src
end
end
compile
is pretty simple. We create a new instance of ERB parser and pass it the template source and the variable that we want to build the output on (@output_buffer
) and returning the src
attribute which holds the ruby code generated by the ERB parser. @output_buffer
is the name of an attribute inside of ActionView::Base
that is used as the output buffer (of course) which will get flushed when we do the final rendering (towards the end of the deliver method). In the case of an AM view, the rendering is pretty simple, but when we get to full controller views with partials we need a place to concat all the different views together which is what we use @output_buffer
for. The next line checks for Ruby 1.9 and does some cleaning of the result before returning it. Here is what src
outputs for our example:
src => "@output_buffer = ''; __in_erb_template=true ; @output_buffer.concat \"Hi,\\n\"\n@output_buffer.concat \"\\n\"\n@output_buffer.concat \"I am contacting you with this message:\\n\"\n@output_buffer.concat \"\\n\"\n@output_buffer.concat(( `message ).to_s); @output_buffer.concat \"\\n\"\n@output_buffer.concat \"\\n\"\n@output_buffer.concat \"\\n\"\n@output_buffer.concat \"\\n\"\n@output_buffer.concat \"Thanks\\n\"\n@output_buffer.concat \"\\n\"\n@output_buffer.concat \"Jake\"; @output_buffer"
This is one long string that represents the compiled version of the ruby code for our mailer view. Now that we have the compiled_source, letâ??s go back compile!
:
# (line 66 action_pack/action_view/renderable.rb)
def compile!(render_symbol, local_assigns)
locals_code = local_assigns.keys.map { |key| "#{key} = local_assigns[:#{key}];" }.join
source = <<-end_src
def #{render_symbol}(local_assigns)
old_output_buffer = output_buffer;#{locals_code};#{compiled_source}
ensure
self.output_buffer = old_output_buffer
end
end_src
...
end
So now we know what all the pieces mean, letâ??s see what our example becomes:
def _run_erb_app47views47message_mailer47contact_form46erb(local_assigns)
old_output_buffer = output_buffer;;@output_buffer = ''; __in_erb_template=true ; @output_buffer.concat "Hi,\n"
@output_buffer.concat "\n"
@output_buffer.concat "I am contacting you with this message:\n"
@output_buffer.concat "\n"
@output_buffer.concat(( `message ).to_s); @output_buffer.concat "\n"
@output_buffer.concat "\n"
@output_buffer.concat "\n"
@output_buffer.concat "\n"@output_buffer.concat "Thanks\n"
@output_buffer.concat "\n"
@output_buffer.concat "Jake"; @output_buffer
ensure
self.output_buffer = old_output_buffer
end
Alright, now we got the code we want evalâ??ed, letâ??s keep going through compile!
# (line 66 action_pack/action_view/renderable.rb)
def compile!(render_symbol, local_assigns)
...
begin
ActionView::Base::CompiledTemplates.module_eval(source, filename, 0)
rescue Errno::ENOENT => e
raise e # Missing template file, re-raise for Base to rescue
rescue Exception => e # errors from template code
if logger = defined?(ActionController) && Base.logger
logger.debug "ERROR: compiling #{render_symbol} RAISED #{e}"
logger.debug "Function body: #{source}"
logger.debug "Backtrace: #{e.backtrace.join("\n")}"
end
raise ActionView::TemplateError.new(self, {}, e)
end
end
Now we open up CompiledTemplates
and perform a module_eval
with the source we just made and the filename of this template (/example_app/app/views/message_mailer/contact_form.erb
) and 0
. The filename and the number 0 are used in error reporting during module_eval
. Letâ??s say something went wrong with the module_eval, in our case, the error would look something like this:
/example_app/app/views/message_mailer/contact_form.erb:0:in `module_eval': the real problem goes here
Thatâ??s it for this week. Next week we continue with our dynamically compiled method and see what Rails does with it.
When we left off last, we were just about to get into rendering the message. Letâ??s recap that code:
(line 553 action_mailer/base.rb)
def render_message(method_name, body)
if method_name.respond_to?(:content_type)
@current_template_content_type = method_name.content_type
end
render :file => method_name, :body => body
ensure
@current_template_content_type = nil
end
When we call render_message
in create!
, we call it with @template
and @body
, letâ??s see what those get set to inside of render_message
for our example:
@template = method_name => "contact_form"
@body = body => {:message=>"Send this message"}
At the beginning of render_message @body
is set to the hash of instance variables that we set in our contact_form
method in our mailer. Once we are finished processing, that hash will be gone and @body
will be replaced with the full rendered message.
Next, we see that we want to set the @current_template_content_type
instance variable to the proper content type. This is mainly used for multipart so that if render is passed one of those fancy ActionView::ReloadableTemplate
objects it renders it with the proper parser. Since we did not specify a content_type the method_name â??contact_formâ? is passed and we get to skip over that part and go straight to the meat:
render :file => method_name, :body => body
This is not the same render from ActionController
or ActionView
, but it does use the AV template render methods. Letâ??s take a look at the definition:
(line 562 action_mailer/base.rb)
def render(opts)
body = opts.delete(:body)
if opts[:file] && (opts[:file] !~ /\// && !opts[:file].respond_to?(:render))
opts[:file] = "#{mailer_name}/#{opts[:file]}"
end
begin
old_template, @template = @template, initialize_template_class(body)
layout = respond_to?(:pick_layout, true) ? pick_layout(opts) : false
@template.render(opts.merge(:layout => layout))
ensure
@template = old_template
end
end
and the params with our example:
opts => {
:file => "contact_form",
:body => {:message=>"Send this message"}
}
The first thing we do is set the variable body
to opts[:body]
and delete it from the options hash (this is all done in one stroke by the call delete
which returns the value that was deleted). Next, we test to see if opts[:file]
does not match â/â? and does not respond to render then we set opts[:file]
to the same thing but prepended with the mailer_name, in our case âmessage_mailerâ?.
Now we are getting to the real meat. First, we need to set some quick variables before we render. Here are the 2 lines we will be examining next:
old_template, @template = @template, initialize_template_class(body)
layout = respond_to?(:pick_layout, true) ? pick_layout(opts) : false
In the first line we are swapping out the current @template
variable with the result of initialize_template_class(body)
and storing the old value in old_template
. Here is the definition of initialize_template_class
:
(line 603 action_mailer/base.rb)
def initialize_template_class(assigns)
template = ActionView::Base.new(self.class.view_paths, assigns, self)
template.template_format = default_template_format
template
end
and assigns
gets set to:
body => {:message=>"Send this message"}
The first thing we see is that we create a var named template
that is a new instantiation of ActionView::Base
with the view_paths
, assigns
, and the model
(MessageMailer in our example) passed to it. Letâ??s dig in and cross over to ActionView::Base
and look at initialize
:
(line 221 action_pack/action_view/base.rb)
def initialize(view_paths = [], assigns_for_first_render = {}, controller = nil)#:nodoc:
@assigns = assigns_for_first_render
@assigns_added = nil
@controller = controller
@helpers = ProxyModule.new(self)
self.view_paths = view_paths
@_first_render = nil
@_current_render = nil
end
and the params with our example:
view_paths => ["/example_app/app/views"]
assigns_for_first_render => {:message=>"Send this message"}
controller => #<MessageMailer:0x34a579c
@action_name="contact_form",
@subject="Im contacting you",
@content_type="text/plain",
@implicit_parts_order=["text/html", "text/enriched", "text/plain"],
@from="jake@somewhere.com",
@sent_on=Sun Nov 08 11:34:17 -0600 2009,
@headers={},
@default_template_name="contact_form",
@mime_version="1.0",
@body={:message=>"Send this message"},
@mailer_name="message_mailer",
@recipients="someemail@somedomain.com",
@parts=[],
@template="contact_form",
@charset="utf-8">
This allows us to wrap the mailers view around the ActionView
templating system and reuse the rendering module from ActionView
in AM. This also stores our body variables for use inside of our view. Now that we have a template, we need to initialize the template_format
with our mailerâ??s default template format.
template.template_format = default_template_format
Letâ??s open up the definition for default_template_format
(line 577 action_mailer/base.rb)
def default_template_format
if @current_template_content_type
Mime::Type.lookup(@current_template_content_type).to_sym
else
:html
end
end
This method checks the @current_template_content_type
ivar and returns the proper mime-type. This is used as a switch by the renderer. If you dont explicitly set the content type AM defaults to HTML and your message is rendered in HTML mode. Since we didnt define a content_type
in our example @current_template_content_type
is nil and :html will be returned for the default_template_format
.
After that we return the created template instance and that gets us through the initialize_template_class
method.
So now that we have set the @template
ivar in:
old_template, @template = @template, initialize_template_class(body)
layout = respond_to?(:pick_layout, true) ? pick_layout(opts) : false
We move down to setting the layout for the mailer. In Rails you have the ability to use layouts just like in regular views. In our case, if we wanted to use a layout, we would have to add a file named message_mailer.html.erb
in app/views/layouts
. The other option is specifying a layout name in our mailer model (just like we would in a controller). So we could define MessageMailer as:
class MessageMailer < ActionMailer::Base
layout "my_email_layout"
def contact_form(msg_from, message)
...
end
end
In that case, we would need a file named my_email_layout.html.erb
in our layouts directory. Since we include ActionController::Layout
in AM::Base our model will respond to pick_layout
. This is the method that will determine the layout needed for the renderer. Letâ??s dive into pick_layout
, which is defined in ActionController::Layout
# (line 229 action_pack/action_controller/layout.rb)
def pick_layout(options)
if options.has_key?(:layout)
case layout = options.delete(:layout)
when FalseClass
nil
when NilClass, TrueClass
active_layout if action_has_layout? && candidate_for_layout?(:template => default_template_name)
else
active_layout(layout, :html_fallback => true)
end
else
active_layout if action_has_layout? && candidate_for_layout?(options)
end
end
and the params for our example:
options => {:file=>"message_mailer/contact_form"}
The first part of pick_layout
checks for the key :layout
in options and since we didnt define it in our example we will skip over it and straight to the else part of the conditional:
active_layout if action_has_layout? && candidate_for_layout?(options)
Here, we call the method active_layout
if action_has_layout? returns true and candidate_for_layout?(options) returns true. action_has_layout?
is a simple method to check if this action or mailer method has a layout and is basically making sure that your :except and :only actions render properly. Letâ??s take a look at action_has_layout? with our example
# (line 244 action_pack/action_controller/layout.rb)
def action_has_layout?
if conditions = self.class.layout_conditions
case
when only = conditions[:only]
only.include?(action_name)
when except = conditions[:except]
!except.include?(action_name)
else
true
end
else
true
end
end
and conditions
would be:
conditions => nil
Since we are not setting a layout for our message the method self.class.layout_conditions
returns nil. layout_conditions
is a class method on AM that reads the attribute accessor hash layout_conditions
. These conditions are written when you make the call:
layout "my_template" # or layout "my_template", :except => "some_message"
This writes all your conditions to a attribute hash for easy reading and subclassing. Since our example returns nil for layout_conditions
we end up at the outer else statement and return true out of this method and on to candidate_for_layout?(options)
. This is a base method is written in layout.rb and overridden in AM::base.rb:
# (line 585 action_mailer/base.rb)
def candidate_for_layout?(options)
!self.view_paths.find_template(default_template_name, default_template_format).exempt_from_layout?
rescue ActionView::MissingTemplate
return true
end
and our paramaters and extra variables/methods :
options => {:file=>"message_mailer/contact_form"}
default_template_name => "contact_form"
default_template_format => :html
In this method, we are checking to see if there is a template with the above params. Since we dont have any layouts Rails throws an ActionView::MissingTemplate
exception which we catch and just return true from it. This sets up the final part of pick_layout
which is active_layout
. Here is the definition for that:
# (line 201 action_pack/action_controller/layout.rb)
def active_layout(passed_layout = nil, options = {})
layout = passed_layout || default_layout
return layout if layout.respond_to?(:render)
active_layout = case layout
when Symbol then __send__(layout)
when Proc then layout.call(self)
else layout
end
find_layout(active_layout, default_template_format, options[:html_fallback]) if active_layout
end
In our case, we dont pass anything to active_layout so the first line will get set to default_layout
, which is a private method that looks like:
# (line 220 action_pack/action_controller/layout.rb)
def default_layout #:nodoc:
layout = self.class.read_inheritable_attribute(:layout)
return layout unless self.class.read_inheritable_attribute(:auto_layout)
find_layout(layout, default_template_format)
rescue ActionView::MissingTemplate
nil
end
Since we didnt set a layout, the first line will return nil
and the second line will return that value because auto_layout
will also be nil. When we get back to active_layout
, the variable layout
is set to nil
. The variable active_layout
gets set to nil
as well since we are layout-less. Now we get to find_layout
which doesnt get called in our example because active_layout
is nil
. Thatâs it, we are now ready to do the main rendering. Letâs look at the code again:
(line 562 action_mailer/base.rb)
def render(opts)
...
begin
old_template, @template = @template, initialize_template_class(body)
layout = respond_to?(:pick_layout, true) ? pick_layout(opts) : false
@template.render(opts.merge(:layout => layout))
ensure
@template = old_template
end
end
and the params and current variables with our example:
opts => {
:file => "contact_form",
:body => {:message=>"Send this message"}
}
old_template => "contact_form"
@template => #<ActionView::Base:0x3496c10
@template_format=:html,
@assigns={:message=>"Send this message"},
@helpers=#<ActionView::Base::ProxyModule:0x3496b84>,
@controller=#<MessageMailer:0x34a5814
@action_name="contact_form",
@subject="Im contacting you",
@content_type="text/plain",
@implicit_parts_order=["text/html", "text/enriched", "text/plain"],
@from="jake@somewhere.com",
@sent_on=Sun Nov 08 18:15:42 -0600 2009,
@headers={},
@default_template_name="contact_form",
@mime_version="1.0",
@body={:message=>"Send this message"},
@mailer_name="message_mailer",
@recipients="someemail@somedomain.com",
@parts=[],
@template=#<ActionView::Base:0x3496c10 ...>,
@charset="utf-8">,
@_current_render=nil,
@assigns_added=nil,
@_first_render=nil,
@view_paths=["/example_app/app/views"]>
So now we see that layout
ends up being nil in our example which we then pass on to @template.render
along with our original options. Now weâ??ll jump into render
:
# (line 201 action_view/base.rb)
def render(options = {}, local_assigns = {}, &block) #:nodoc:
local_assigns ||= {}
case options
when Hash
options = options.reverse_merge(:locals => {})
if options[:layout]
_render_with_layout(options, local_assigns, &block)
elsif options[:file]
template = self.view_paths.find_template(options[:file], template_format)
template.render_template(self, options[:locals])
elsif options[:partial]
render_partial(options)
elsif options[:inline]
InlineTemplate.new(options[:inline], options[:type]).render(self, options[:locals])
elsif options[:text]
options[:text]
end
when :update
update_page(&block)
else
render_partial(:partial => options, :locals => local_assigns)
end
end
and our passed in params:
options => {:file=>"message_mailer/contact_form", :layout=>nil}
local_assigns => {}
Since options
is a Hash, we jump right in and first merge in :locals => {}
with options
. options[:layout]
returns nil so we skip that statement and go into the options[:file]
conditional
template = self.view_paths.find_template(options[:file], template_format)
template.render_template(self, options[:locals])
Here, we search out the template from the AM models view_paths
with our file and the temlpate_format
, which in our case is :html
. view_paths
is an instance method in ActionView::Base
that accesses the attribute @view_paths
which holds an instance of @ ActionView::PathSet@. This is a helper class that wraps an array with some pretty handy methods, one of which is find_template
. This helper class stores each of the view_paths as a ActionView::ReloadableTemplate::ReloadablePath
object. This allows us to do other cooler things which we go over later. In our case, view_paths
returns:
self.view_paths => ["/example_app/app/views"]
Letâ??s dive into find_template
:
# (line 201 action_pack/action_view/paths.rb)
def find_template(original_template_path, format = nil, html_fallback = true)
return original_template_path if original_template_path.respond_to?(:render)
template_path = original_template_path.sub(/^\//, '')
each do |load_path|
if format && (template = load_path["#{template_path}.#{I18n.locale}.#{format}"])
return template
elsif format && (template = load_path["#{template_path}.#{format}"])
return template
elsif template = load_path["#{template_path}.#{I18n.locale}"]
return template
elsif template = load_path[template_path]
return template
# Try to find html version if the format is javascript
elsif format == :js && html_fallback && template = load_path["#{template_path}.#{I18n.locale}.html"]
return template
elsif format == :js && html_fallback && template = load_path["#{template_path}.html"]
return template
end
end
return Template.new(original_template_path, original_template_path =~ /\A\// ? "" : ".") if File.file?(original_template_path)
raise MissingTemplate.new(self, original_template_path, format)
end
and our params:
original_template_path => "message_mailer/contact_form"
format => :html
The first line just returns back if we have a full fledged ActionView model that responds to render. Since we dont, we keep going and see that the next line makes sure that our original_template_path
isnt an absolute path by removing the initial â??/â? if it is there and storing the final result in template_path
. Next, we iterate over ourselves and do some checking (remember, PathSet is a subclass of Array, so we can call each
and iterate over the contents internally). In our example, we end up executing the line:
elsif template = load_path[template_path]
return template
This is part of the fanciness of ActionView::ReloadableTemplate::ReloadablePath
. One of the methods implemented is []
which works like a hash and pulls out the proper ReloadableTemplate
from the paths array, in our case itâ??s
template => #<ActionView::ReloadableTemplate:0x34a2ad8
@base_path="message_mailer",
@template_path="message_mailer/contact_form.erb",
@_memoized_path=["message_mailer/contact_form.erb"],
@name="contact_form",
@previously_last_modified=Sat Nov 07 10:32:50 -0600 2009,
@_memoized_method_segment=["app47views47message_mailer47contact_form46erb"],
@format=nil,
@filename="/example_app/app/views/message_mailer/contact_form.erb",
@_memoized_relative_path=["app/views/message_mailer/contact_form.erb"],
@extension="erb",
@locale=nil,
@_memoized_path_without_extension=["message_mailer/contact_form"],
@_memoized_method_name_without_locals=["_run_erb_app47views47message_mailer47contact_form46erb"],
@load_path="/example_app/app/views">
So we found the proper view and we are now done with @find_template@. Back to @render@:
# (line 201 action_pack/action_view/base.rb)
def render(options = {}, local_assigns = {}, &block) #:nodoc:
local_assigns ||= {}
case options
when Hash
...
elsif options[:file]
template = self.view_paths.find_template(options[:file], template_format)
template.render_template(self, options[:locals])
elsif options[:partial]
...
end
when :update
...
end
end
The next line renders the template with any locals (in our case there are none). Before we dive into render_template
âs definition, letâs take a look at the view we created for our example message:
(message_mailer/contact_form.erb)
Hi,
I am contacting you with this message:
<%= @message %>
Thanks
Jake
A ridiculously simple mailer view for our equally ridiculously simple mailer message. Back to render_template
# (line 193 action_pack/action_view/template.rb)
def render_template(view, local_assigns = {})
render(view, local_assigns)
rescue Exception => e
raise e unless filename
if TemplateError === e
e.sub_template_of(self)
raise e
else
raise TemplateError.new(self, view.assigns, e)
end
end
and our current params:
view => #<ActionView::Base:0x3496c10
@template_format=:html,
@assigns={:message=>"Send this message"},
@helpers=#<ActionView::Base::ProxyModule:0x3496b84>,
@controller=#<MessageMailer:0x34a5814
@action_name="contact_form",
@subject="Im contacting you",
@content_type="text/plain",
@implicit_parts_order=["text/html", "text/enriched", "text/plain"],
@from="jake@somewhere.com",
@sent_on=Mon Nov 09 20:09:58 -0600 2009,
@headers={},
@default_template_name="contact_form",
@mime_version="1.0",
@body={:message=>"Send this message"},
@mailer_name="message_mailer",
@recipients="someemail@somedomain.com",
@parts=[],
@template=#<ActionView::Base:0x3496c10 ...>,
@charset="utf-8">,
@_current_render=nil,
@assigns_added=nil,
@_first_render=nil,
@view_paths=["/example_app/app/views"]>
local_assigns => {}
As we can see, render_template
is a relatively simple method that delegates most of itâs work to Renderable#render
. Thatâs where we are going next:
# (line 193 action_pack/action_view/renderable.rb)
def render(view, local_assigns = {})
compile(local_assigns)
view.with_template self do
view.send(:_evaluate_assigns_and_ivars)
view.send(:_set_controller_content_type, mime_type) if respond_to?(:mime_type)
view.send(method_name(local_assigns), local_assigns) do |*names|
ivar = :@_proc_for_layout
if !view.instance_variable_defined?(:"@content_for_#{names.first}") && view.instance_variable_defined?(ivar) && (proc = view.instance_variable_get(ivar))
view.capture(*names, &proc)
elsif view.instance_variable_defined?(ivar = :"@content_for_#{names.first || :layout}")
view.instance_variable_get(ivar)
end
end
end
end
Whew, thatâs it for this week. Next week, we get super deep into rendering and see how Rails performs all of itâs rendering duties.
Welcome to the first in a series of articles that will delve deep into several commonly used methods of Rails. The first method we will look at is Actionmailer#deliver. Now, I am going to assume that you know what AM is and how to use. You’ve generated mailer models before and set up views and called these methods via something like: MyMailer.deliver_some_message(insert, your, params, here)
. What we are going to to look at is what happens when you make the deliver call.
We will be skipping what happens around rails (the ruby core goings-ons) and all the method stacking that goes on in ruby and focus on the rails part in this series. So let’s get started. (We will be using Rails 2.3.2 as the guide for the code)
What’s happening now?
When deliver_some_message
is executed, the first method that is fired is AR’s class method method_missing
. Here’s the method that gets executed:
(line 391 action_mailer/base.rb)
class << self
...
def method_missing(method_symbol, *parameters) #:nodoc:
if match = matches_dynamic_method?(method_symbol)
case match[1]
when 'create' then new(match[2], *parameters).mail
when 'deliver' then new(match[2], *parameters).deliver!
when 'new' then nil
else super
end
else
super
end
end
...
end
So when you call deliver_some_message(params)
, ruby passes all that info (method name included) to the method_missing method. I have created a simple Mailer for this series to help us see what’s going on throughout this entire chain of methods and to see each of the params for these methods.
Here is the quick mailer I created:
class MessageMailer < ActionMailer::Base
def contact_form(msg_from, message)
subject 'Im contacting you'
recipients "someemail@somedomain.com"
from msg_from
sent_on Time.now
body :message => message
end
end
So when I call
MessageMailer.deliver_contact_form("jake@somewhere.com", "Send this message")
the params end up being distributed like so:
method_symbol => :deliver_contact_form
parameters => ["jake@somewhere.com", "Send this message"]
Now that we know what’s being passed, let’s see what happens to it. The first line of the method is
if match = matches_dynamic_method?(method_symbol)
matches_dynamic_method?
is a private method in AM and is defined as:
(line 441 action_mailer/base.rb)
private
def matches_dynamic_method?(method_name) #:nodoc:
method_name = method_name.to_s
/^(create|deliver)_([_a-z]\w*)/.match(method_name) || /^(new)$/.match(method_name)
end
This is pretty simple, we are doing a scan across the method name to match it against create_ or deliver_ and then capturing the rest of the method name or we are looking strictly for new at the beginning of our method name and with nothing else attached. The reason this is done is to disable AM’s initialize capabilities for outside referencing and only offer create
and deliver
. On line 254 in AR base.rb we see that we have privatize the method new with:
private_class_method :new #:nodoc:
So if someone tries to be clever and call MessageMailer.new
, it will hit the method_missing
(since the base new method has now been made private) and end up returning nil.
The match method returns a Match
object that contains the captures. With our example the match variable would look like:
match.captures => ["deliver", "contact_form"]
match[1] => deliver
match[2] => contact_form
So now we got a match and we can continue thru the case statement. Since match[1]
in our example returns deliver
, we execute the line
when 'deliver' then new(match[2], *parameters).deliver!
Here, we call the private new
method to initialize a new AR instance and we pass it the second part of our capture and all the params that came to method_missing originally. After initializing, the method deliver!
is then called (which we will talk about after we talk about what happens during initialization).
Let’s go ahead and see what new
looks like (or more precisely, initialize
, since you dont create a new
method, you create the initialize
method which new calls). AR’s initialize
method looks like this:
(line 452 action_mailer/base.rb)
def initialize(method_name=nil, *parameters) #:nodoc:
create!(method_name, *parameters) if method_name
end
This initialize
is quite simple, it calls create!
if method_name
is present. In our case it will be since we passed “contact_form” to it. So let’s go to create!
which is also right below it in the source:
(line 458 action_mailer/base.rb)
def create!(method_name, *parameters) #:nodoc:
initialize_defaults(method_name)
__send__(method_name, *parameters)
unless String === @body
if @parts.empty?
Dir.glob("#{template_path}/#{@template}.*").each do |path|
template = template_root["#{mailer_name}/#{File.basename(path)}"]
next unless template && template.multipart?
@parts << Part.new(
:content_type => template.content_type,
:disposition => "inline",
:charset => charset,
:body => render_message(template, @body)
)
end
unless @parts.empty?
@content_type = "multipart/alternative" if @content_type !~ /^multipart/
@parts = sort_parts(@parts, @implicit_parts_order)
end
end
template_exists = @parts.empty?
template_exists ||= template_root["#{mailer_name}/#{@template}"]
@body = render_message(@template, @body) if template_exists
if !@parts.empty? && String === @body
@parts.unshift Part.new(:charset => charset, :body => @body)
@body = nil
end
end
@mime_version ||= "1.0" if !@parts.empty?
@mail = create_mail
end
Whoa! Don’t worry, it’s not that bad. Let’s take it line by line. Note: I have removed the comments from the above snippet because they wouldnt format in textile properly.
First, let’s figure out what we have in the method:
method_name => "contact_form"
parameters => ["jake@somewhere.com", "Send this message"]
Cool, now we know what we have going in, so let’s step through it. First we have: initialize_defaults(method_name)
which is a private method in AM
which looks like:
(line 537 action_mailer/base.rb)
def initialize_defaults(method_name)
@charset ||= @@default_charset.dup
@content_type ||= @@default_content_type.dup
@implicit_parts_order ||= @@default_implicit_parts_order.dup
@template ||= method_name
@default_template_name = @action_name = @template
@mailer_name ||= self.class.name.underscore
@parts ||= []
@headers ||= {}
@body ||= {}
@mime_version = @@default_mime_version.dup if @@default_mime_version
end
This method initializes several of the attribute accessors, or in this case advanced attribute accessors, with defaults and some class attribute accessors. The advanced attribute accessors are just like regular attribute accessors except they add one extra ability which you have seen several times in your use of ActionMailer models. Take a look at our simple mailer again:
class MessageMailer < ActionMailer::Base
def contact_form(msg_from, message)
subject 'Im contacting you'
...
end
end
Do you see it? Notice, how we can make the call:
subject 'Im contacting you'
This is not a normal attribute accessor call. Normally, you can only call
obj.some_attribute
or
obj.some_attribute = "some stuff"
The advanced attribute accessor gives you a third option of setting the attribute via a regular method call like:
obj.some_attribute("some stuff")
or, more elegantly:
obj.some_attribute "some stuff"
instead of the regular ol’
obj.some_attribute = "some stuff"
Let’s see how AM accomplishes this. Take a look at action_mailer/adv_attr_accessor.rb
:
module ActionMailer
module AdvAttrAccessor #:nodoc:
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods #:nodoc:
def adv_attr_accessor(*names)
names.each do |name|
ivar = "@#{name}"
define_method("#{name}=") do |value|
instance_variable_set(ivar, value)
end
define_method(name) do |*parameters|
raise ArgumentError, "expected 0 or 1 parameters" unless parameters.length <= 1
if parameters.empty?
if instance_variable_names.include?(ivar)
instance_variable_get(ivar)
end
else
instance_variable_set(ivar, parameters.first)
end
end
end
end
end
end
end
The main part of all of that is the adv_attr_accessor(*names)
definition. What Rails is doing here is going over each name (or attribute) you want to create and then doing the following:
- Dynamically creating an instance variable name.
- Dynamically creating a
method=
method (or setter method) for the attribute. - Dynamically creating a
method
method (or getter method) for the attribute, but (and this is where the magic is) adding the ability to have 0 or 1 parameters (instead of the normal 0 parameters that Ruby normally provides). If the parameters to the method is 0 then it just gets the instance variable (attribute) as normal, if it is 1, then it sets the instance variable to the parameter (like the normalmethod=
method does).
That’s how we get the sugar coated ability to write obj.some_attribute "some stuff"
for our AM methods.
Ok, now back to initialize_defaults
. So we see the attributes getting initialized, let’s see how our example would initialize:
method_name => contact_form
charset => "utf-8"
content_type => "text/plain"
implicit_parts_order => ["text/html", "text/enriched", "text/plain"]
template.inspect => "contact_form"
default_template_name => "contact_form"
action_name => contact_form
mailer_name => "message_mailer"
parts => []
headers => {}
body => {}
mime_version => "1.0"
Cool, now that we got that all initialized, let’s keep going. Back to create!:
(line 458 action_mailer/base.rb)
def create!(method_name, *parameters) #:nodoc:
initialize_defaults(method_name)
__send__(method_name, *parameters)
...
end
The next line calls __send__
with the method_name and parameters that we passed in originally. The reason __send__
is called is just in case you override the normal send method. This is a nice lesson for you newcomers to Rails. DONT OVERRIDE CORE RUBY METHODS! Sorry for the yelling.
But, since we cant predict everything, more specifically, we cant predict what new rails developers will do with AM, we use the __send__
method the make sure we dynamically call method_name
with the parameters and not an overridden send method in your customized AM class.
In our example, we have the following arguments to send:
method_name => "contact_form"
parameters => ["jake@somewhere.com", "Send this message"]
So Ruby sends the message “contact_form” to our instantiated AM model and since we defined contact_form
in our mailer model Ruby will execute this method now. Let’s look at our mailer again:
class MessageMailer < ActionMailer::Base
def contact_form(msg_from, message)
subject 'Im contacting you'
recipients "someemail@somedomain.com"
from msg_from
sent_on Time.now
body :message => message
end
end
Now that we have seen how the adv_attr_accessors
work we can see that all that our contact_form
method (or any AM deliver method for that matter) is doing is setting all the proper attributes. Your view is just an HTML or plain text version of your email with some fancy ERB parsing to help make it dynamic.
Let’s keep going. We have just set several attributes using the dynamic __send__
method to call our contact_form method in our AM model. Now we are back to the create!
method again:
(line 458 action_mailer/base.rb)
def create!(method_name, *parameters) #:nodoc:
initialize_defaults(method_name)
__send__(method_name, *parameters)
unless String === @body
if @parts.empty?
Dir.glob("#{template_path}/#{@template}.*").each do |path|
template = template_root["#{mailer_name}/#{File.basename(path)}"]
next unless template && template.multipart?
@parts << Part.new(
:content_type => template.content_type,
:disposition => "inline",
:charset => charset,
:body => render_message(template, @body)
)
end
unless @parts.empty?
@content_type = "multipart/alternative" if @content_type !~ /^multipart/
@parts = sort_parts(@parts, @implicit_parts_order)
end
end
...
end
...
end
The next thing we do is check to see whether our body
attribute is a String. If not, we are going to do some processing. Let’s take a look and see what attributes we have set with our example:
@parts => nil
@template => "contact_form"
Now we know that (in our example) parts is empty and we are going to enter into that conditional statement. Now we are going to find all template views that match our template “contact_form”. template_root
is a class method on AM that returns the view_path root (usually apps/views/). This method is copied from ActionView::Base
and the root is set there.
Next, we will set the variable template
to be the value of template_root
when the key is the full path (with the mailer_name
, in this case “message_mailer”) to the erb or rhtml file. What we get back, is an instance of ActionView::ReloadableTemplate
wrapped around our view file. Let’s see that with our example:
template =>
#<ActionView::ReloadableTemplate:0x34a4a68
@base_path="message_mailer",
@template_path="message_mailer/contact_form.erb",
@_memoized_path=["message_mailer/contact_form.erb"],
@name="contact_form",
@filename="/example_app/app/views/message_mailer/contact_form.erb",
...
@load_path="/example_app/app/views">
Next, we see that we skip over any further processing unless we have a multipart message or have multiple views for our mailer method (thereby making it a multipart message). That takes us out of the loop (for our example) and down to
unless @parts.empty?
@content_type = "multipart/alternative" if @content_type !~ /^multipart/
@parts = sort_parts(@parts, @implicit_parts_order)
end
In our example, @parts
is empty so we are pretty much done with this area. Let’s take a look and see what happens next:
def create!(method_name, *parameters) #:nodoc: ... unless String === @body if @parts.empty? ... end template_exists = @parts.empty? template_exists ||= template_root["#{mailer_name}/#{@template}"] @body = render_message(@template, @body) if template_exists ... end end
Basically, what we are doing here is making sure we have either an implied template or dynamically generated template. This is mainly for multipart messages. In our example, since there are still no parts, template_exist gets set to true. Since it is already true, the next line gets skipped and we go directly to rendering the message into our protected attribute @body
. Let’s see what render_message looks like:
(line 553 action_mailer/base.rb)
def render_message(method_name, body)
if method_name.respond_to?(:content_type)
@current_template_content_type = method_name.content_type
end
render :file => method_name, :body => body
ensure
@current_template_content_type = nil
end
When we call render_message
in create!
, we call it with @template
and @body
, let’s see what those get set to inside of render_message
for our example:
@template = method_name => "contact_form"
@body = body => {:message=>"Send this message"}
At the beginning of render_message @body
is set to the hash of instance variables that we set in our contact_form
method in our mailer. Once we are finished processing, that hash will be gone and @body
will be replaced with the full rendered message.
That’s it for now. Stay tuned for next week for Part II where we will pick back up and continue our trek through ActionMailer#deliver and get real deep into rendering.
Super InPlace Controls has gone through some upgrades.
We’ve Moved
First and foremost, we have moved the code from google to github.
To install run
script/plugin install git://github.com/flvorful/super_inplace_controls.git
Rails 2.3 Support
Tested with Rails 2.3 and 2.1
Better Jquery Support
We have updated the code to work better with jQuery. Now, in_place_date_selector, uses the jQuery datepicker method if available. JRails is required for jquery support.
Better Validation
Validations were a problem with SIPC, but no longer. Now, if you dont have an error div, SIPC will skip over the rendering of the error box and just add the class “fieldWithError” to the proper input tag.
Overall cleanup
Cleaned up the code in general and made it less brittle.
Check out the demos and the docs at our Open Source Site and stay tuned for some new plugins we’ve been working on.
—jake