Rendering a Rails View from a Script

The Problem

Sometimes you want to render a template inside of a script. Suppose you need to cache the generated output, say a web page or a spreadsheet, and save it to disk. So how do we do this?

It is of course easy to do inside a controller. Call render or render_to_string. You can use render_to_string inside a Mailer too to generate an attachment. But several things have been set up for this to work, and these methods are not readily available inside a script context. So what are these things?

ActionView::Base

ActionView::Base is is responsible for rendering in rails. Controllers instantiate an ActionView::Base class and pass rendering on to it when their own aciton is finished. ActionView::Base knows how to find templates, compile templates, and return a rendered template. In particular, all helpers are included within its context, so when a template is evaluated the helpers are available to it. Therefore, the easiest way for us to render templates within a script is to set up our own ActionView::Base class.

For the curious, here is the method called by the controller to instantiate the ActionView:

def view_context  
  view_context_class.new(view_renderer, view_assigns, self)
end  

view_context_class returns the ActionView::Base class with any local additions.

And here is the top of ActionView::Base, where it includes all helpers:

class Base  
  include Helpers, ::ERB::Util, Context
  ...

Aside: It is quite interesting to look inside Helpers, where all the default helpers are included. (Go take a look.)

Lets take a look at the ActionView::Base initialize method:

def initialize(context = nil, assigns = {}, controller = nil, formats = nil) #:nodoc:  
  @_config = ActiveSupport::InheritableOptions.new

  if context.is_a?(ActionView::Renderer)
    @view_renderer = context
  else
    lookup_context = context.is_a?(ActionView::LookupContext) ?
      context : ActionView::LookupContext.new(context)
    lookup_context.formats  = formats if formats
    lookup_context.prefixes = controller._prefixes if controller
    @view_renderer = ActionView::Renderer.new(lookup_context)
  end

  assign(assigns)
  assign_controller(controller)
  _prepare_context
end  

We need to set context and assigns. assigns is short for view assigns, or the variables that will be passed to the template before rendering. It is a simple hash.

But what is context? According to the code, it can be either an ActionView::Renderer, an ActionView::LookupContext, or whatever is passed to a new LookupContext. What is the last?

Here is the code in ActionView::LookupContext:

def initialize(view_paths, details = {}, prefixes = [])  

So what is view_paths? It turns out it is a PathSet object, which encapsulates all the top level locations for retrieving templates (and it contains the file resolvers too.) It turns out that we can easily get this from the controller.

We have enough to get started with. We can do a script two ways. One is with rails runner, and one is just with ruby. We'll do the runner first.

Rails runner

Suppose we have a Rails app that serves widgets, and there is a widgets/index.html.erb template. We are going to render this template inside our script.

According to our findings above, we need to set our assign variables, and get our context (or view paths.) We do that as follows:

view_assigns = {widgets: Widget.all}  
av = ActionView::Base.new(ActionController::Base.view_paths, view_assigns)  

That gets us our ActionView::Base. Now we need to include any helpers required by our template:

av.class_eval do  
  # include any needed helpers (for the view)
  include ApplicationHelper
end  

Now, when the template is evaluated, the helper methods will be available to it. Next we simply call render on our ActionView, and do what we want with the results:

# normal render statement
content = av.render template: 'widgets/index.html.erb'  
# do something with content, such as:
File.open("/tmp/with_runner.html","w") {|f| f.puts content }  

This is a normal render statement, so you can pass any regular options to it. That is it! We are done. Here is the whole script:

view_assigns = {widgets: Widget.all}  
av = ActionView::Base.new(ActionController::Base.view_paths, view_assigns)  
av.class_eval do  
  # include any needed helpers (for the view)
  include ApplicationHelper
end  
# normal render statement
content = av.render template: 'widgets/index.html.erb'  
# do something with content, such as:
File.open("/tmp/with_runner.html","w") {|f| f.puts content }  

Without runner

If we are not using Rails runner, our main work is in setting things up. Rails is not doing any magic loading for us, so we need to explicitly require everything. After that, the last part of the script will be identical.

First we need to require abstract_controller, action_controller, and action_view. We are also going to include active_record since that is where our data is:

require 'abstract_controller'  
require 'action_controller'  
require 'action_view'  
require 'active_record'  

We also need to require each helper file:

require './app/helpers/application_helper'  

Then we include model files and connect to the database:

require './app/models/widget'  
ActiveRecord::Base.establish_connection(  
  adapter: 'sqlite3',
  database: 'db/development.sqlite3'
)

Lastly we prepend the base view path, which in rails is usually app/views:

ActionController::Base.prepend_view_path "./app/views/"  

The rest of the script is as before:

require 'abstract_controller'  
require 'action_controller'  
require 'action_view'  
require 'active_record'

# require any helpers
require './app/helpers/application_helper'

# active record only if data is here
require './app/models/widget'  
ActiveRecord::Base.establish_connection(  
  adapter: 'sqlite3',
  database: 'db/development.sqlite3'
)

ActionController::Base.prepend_view_path "./app/views/"

# the script as before
view_assigns = {widgets: Widget.all}  
av = ActionView::Base.new(ActionController::Base.view_paths, view_assigns)  
av.class_eval do  
  # include any needed helpers (for the view)
  include ApplicationHelper
end  
# normal render statement
content = av.render template: 'widgets/index.html.erb'  
# do something with content, such as:
File.open("/tmp/with_runner.html","w") {|f| f.puts content }  

Aside: Why bother with this? If you need it, you can shave off some time without Rails runner since you aren't loading the entire framework.

Conclusion

So all you really need to do to render a template in a script is properly set up an ActionView::Base context. We saw that took the view_paths (or PathSet) object from the controller, and the view_assigns hash for local variables.

Keep in mind this is completely agnostic regarding the rendered format. By using ActionView::Base we get the whole template resolving and rendering process. You can use this to generate spreadsheets, javascript files, or xml.

comments powered by Disqus