About this entry




Rendering Templates with Extensions such as rxml with theme_support: "Template is missing" and "No rhtml, rxml, or delegate template found for ..." Errors

The rails' plugin theme_support is a great way to allow customized stylesheets, javascripts, images, layouts, and views to your app. Unfortunately, as it is currently built now, it has a short-coming: it doesn't allow for specifying routes with extensions! In this article I will propose a correction which provides the plugin with a performance boost as an added plus.

Liked it? !

Index

  1. Everybody on the Same Page
  2. Building a More Robust XML Support
  3. Bug and Other's Take on it
  4. My Proposal
  5. Further Resources
  6. Coming Soon

 

Everybody on the Same Page

In order to show you how to correct theme_support in a Ruby on Rails application, I will illustrate the steps by modifying the theme_support to our basic RESTful social_graph application to which we added theme support. Please give both articles a quick glimpse before continuing with this article, if you haven't done so already.

Building a More Robust XML Support

Recently I bumped into a problem with theme_support. It didn't support routes with extensions, such as rxml. Suppose we want to improve on the xml functionality already provided by RESTful implementation of our social_graph. We know that by calling

http://amontano-laptop:3000/acquaintances/1.xml

It's great, but as any scaffolding generation, it is doomed to be replaced since the bare-bones functionality it offers is almost never enough. For instance, I rather have the id display as an attribute of the tag, than as a subtag inside it. So we add the app\views\acquaintances\show.rxml:

xml.acquaintance(:id => @acquaintance.id) do
  xml.name @acquaintance.name
  xml.description @acquaintance.description
end

How do we call this template from the controller? Our intuition tells us that since it has the same name as the action, we don't have to explicitly specify it, so merely erasing the default .to_xml line should do the trick, right? So our first attempt at the show action within social_graph\app\controllers\acquaintances.rb looks like this:

# GET /acquaintances/1
# GET /acquaintances/1.xml
def show
  @acquaintance = Acquaintance.find(params[:id])
  respond_to do |format|
    format.html # show.rhtml
    format.xml  # show.rxml
  end
end

Bug and Other's Take on it

When called by the browser:

http://amontano-laptop:3000/acquaintances/1.xml

It fails with a "No rhtml, rxml, or delegate template found for ..." message. For this, dorren proposed a fix which worked nicely when everything is bug-free. Our above solution would still not look right because it is adding the xml template within our application layout to it. So we change this to explicit ask it not to use a layout:

# GET /acquaintances/1
# GET /acquaintances/1.xml
def show
  @acquaintance = Acquaintance.find(params[:id])
  respond_to do |format|
    format.html # show.rhtml
    format.xml { render :template => 'acquaintances/show.rxml', :layout => false }
  end
end

Unfortunately it never is, and his solution led me to another another annoying problem. If the template had a syntax error, the browser displays a misleading "No rhtml, rxml, or delegate template found for ..." message.

Why? Because theme_support's render_file method, meant to override the ActionView::Base method, finds the appropriate theme by going down a list of possibilities sorted by priority and invoking the pick_template_extension method until one does not raise an exception and goes ahead and renders that one. Unfortunately, that is the Action View method used to infer an extension when it is not present within the route, but if it is, ActionView simply takes the extension from the route itself and does not call pick_template_extension to get it. So dorren tries to emend the mistake by following the original ActionView::Base render_file method only invoking pick_template_extension when necessary. His proposal is to replace the render_file method within social_graph\vendor\plugins\theme_support\lib\patches\actionview_ex.rb with:

# Overrides the default Base#render_file to allow theme-specific views
def render_file(template_path, use_full_path = true, local_assigns = {})        
  search_path = [
    "../themes/#{controller.current_theme}/views",       # for components
    "../../themes/#{controller.current_theme}/views",    # for normal views
    "../../themes/#{controller.current_theme}",          # for layouts
    "../../../themes/#{controller.current_theme}/views", # for mailer views
    ".",                                                 # fallback
    "..",                                                # Mailer fallback
    "../.."                                              # namespaced Mailer fallback
  ]       
  if use_full_path
    search_path.each do |prefix|
      theme_path = prefix +'/'+ template_path
      begin
        template_path_without_extension, template_extension = path_and_extension(theme_path)
        template_extension = pick_template_extension(theme_path).to_s if !template_extension
        # Prevent .rhtml (or any other template type) if force_liquid == true
        if force_liquid? and
          template_extension.to_s != 'liquid' and 
          prefix != '.'
          raise ThemeError.new("Template '#{template_path}' must be a liquid document")
        end
        local_assigns['active_theme'] = get_current_theme(local_assigns)
        return __render_file(theme_path, use_full_path, local_assigns)
      rescue ActionView::ActionViewError => err
        next
      rescue ThemeError => err
        # Should it raise an exception, or just call 'next' and revert to
        # the default template?
        raise err
      end
    end
    raise ActionViewError, "No rhtml, rxml, or delegate template found for #{template_path} in #{@base_path}"
  else
    __render_file(template_path, use_full_path, local_assigns)
  end
end

But then who can raise the exception if that template is not found? Well, he moves the original render_file call to inside the block preceding the rescue. So now when pick_template_extension is not called, render_file is the one that raises the exception when no template is found displaying the No rhtml, rxml, or delegate template found for ... message. Unfortunately, it also raises an exception when there is a syntax error within a template again thinking that it failed because there was no template and still displays the now misleading No rhtml, rxml, or delegate template found for ... message. That is easy to verify. Simply add some line that fails to app\views\acquaintances\show.rxml like:

@foo.bar
xml.acquaintance(:id => @acquaintance.id) do
  xml.name @acquaintance.name
  xml.description @acquaintance.description
end

And trying

http://amontano-laptop:3000/acquaintances/1.xml

will again display the annoying non-informative No rhtml, rxml, or delegate template found for ... message.

My Proposal

Now here is my proposal. Change the render_file method within social_graph\vendor\plugins\theme_support\lib\patches\actionview_ex.rb to:

# Overrides the default Base#render_file to allow theme-specific views
def render_file(template_path, use_full_path = true, local_assigns = {})
  search_path = [
    "../themes/#{controller.current_theme}/views",       # for components
    "../../themes/#{controller.current_theme}/views",    # for normal views
    "../../themes/#{controller.current_theme}",          # for layouts
    "../../../themes/#{controller.current_theme}/views", # for mailer views
    ".",                                                 # fallback
    ".."                                                 # Mailer fallback
  ]
  if use_full_path
    template_path_without_extension, template_extension = path_and_extension(template_path)
    template_extension = pick_template_extension(template_path).to_s if !template_extension
    local_assigns['active_theme'] = get_current_theme(local_assigns)
    search_path.each do |prefix|
      begin
        # template_extension = pick_template_extension(theme_path)
        if File.exists?(full_template_path("#{prefix}/#{template_path_without_extension}", template_extension))
          # Prevent .rhtml (or any other template type) if force_liquid == true
          raise ThemeError.new("Template '#{template_path}' must be a liquid document") if force_liquid? && template_extension.to_s != 'liquid' && prefix != '.'                  
          return __render_file("#{prefix}/#{template_path}", use_full_path, local_assigns)
        end
      #rescue ActionView::ActionViewError => err
      #   next
      rescue ThemeError => err
        # Should it raise an exception, or just call 'next' and revert to
        # the default template?
        raise err
      end
    end
    raise ActionViewError, "No rhtml, rxml, or delegate template found for #{template_path}"
  else
    __render_file(template_path, use_full_path, local_assigns)
  end
end

No again try

http://amontano-laptop:3000/acquaintances/1.xml

and you will see a more informative Showing app/views/acquaintances/show.rxml where line #1 raised... message.  So fix app\views\acquaintances\show.rxml back to:

xml.acquaintance(:id => @acquaintance.id) do
  xml.name @acquaintance.name
  xml.description @acquaintance.description
end

And it will work fine! My strategy: instead of relying on render_file or pick_template_extension to succeed or fail in order to know if a template is appropriate, I choose a much more direct approach: call pick_template_extension only when needed and use it with full_template_path to check if the template exists and then render it without rescue.

Further Resources

Coming Soon

  • How to implement partials in rxml.

Stay tuned!

Liked it? !

Posted on December 3rd | 2 comments | Filed Under: Ruby on Rails