At Trike, we have an app that uses the same code (and instance!) for several clients. The clients need the same kind of app, the differences are with the styling and (sometimes) the content.

Our first solution

Applying different styles is dead easy! Showing different text (or whatever) is easy as well and we came up with a workable solution (put this in application_helper.rb):


  def render_for_client(partial, options = {})
    return "" unless
      File.exist?(File.join(RAILS_ROOT, "app", "views", current_client.name, "_#{partial}.html.erb"))

    render "#{current_client.name}/#{partial}", options
  end

This worked quite well for a number of reasons:

  1. We identified the parts of the app that are client-specific
  2. We didn't have to worry about partials that are only used with one client (return "")
  3. We didn't have to touch the rails internals

Nice, but not good enough

In short: It wasn't perfect. In particular, it happened that we rendered a 'regular' (shared) partial when we shouldn't. It was just too easy to call the default render method...

In fact - the more we thought about it - we realised that it should be the other way around: Rendering a client specific template should be the default!

So we went ahead and discussed ways to overwrite / monkeypatch the ActionView::Base#render method. After a quick spike, this turned out to become extremely tricky, very fast - mainly because ActionView::Base#render is used in almost all controllers and mailers. Actually, it handles all kinds of situations: from rendering partials and templates to handling layouts.

Rendering partials - Our Way

To solve our problem, we had to dig deeper rails internals. And after a while we found a method that we could adapt to solve our problem: ActionView::Base#render_partial. So without further talk - this is the module that we ended up including in ActionView::Base:


  module RenderSafetyNet
    def self.included(base)
      base.class_eval do
        include InstanceMethods
        alias_method_chain :render_partial, :client
      end
    end

    module InstanceMethods
      def render_partial_with_client(options = {})
        if options[:shared_template] || (options[:locals] && options[:locals].delete(:shared_template))
          render_partial_without_client(options)
        else
          partial_path = options[:partial]

          if (String === partial_path || Symbol === partial_path) && !options.has_key?(:collection)
            begin
              options[:partial] = prefix_partial_path(partial_path)
              render_partial_without_client(options)
            rescue ActionView::MissingTemplate => e
              message = e.message + <<-MSG

  We were looking for a client specific template.
  If you really wanted to render a shared template, please specify the option :shared_template => true
              MSG
              raise e.exception(message)
            end
          else
            render_partial_without_client(options)
          end
        end
      end

      def prefix_partial_path(path)
        "#{client_identifier}/#{path}"
      end
    end
  end

With the new module, we are able to use render for both situations, rendering shared and client specific templates. We also changed what kind of template is called by default. So, if you want to render a client specific template you just use render the way it was intended:

  render "about"

This will then map to the [client_name]/about template. On the other hand, if you want to render a regular file, you are forced to specify an extra option:


  render "login", :shared_template => true

Conclusion

If you look at it from a coders perspective, we ended up with a lot more code - with basically the exact same functionality. So why did we do it? Because we have 'tiny little monkey brains' (@mfallshaw)! This code prevents us from making the simple mistake (again) by including client specific, and maybe even confidential, information on the wrong site.

So yes, we have more code than before - but we took responsibility (or debt) away from our brains and made sure that we're doing the right things (in the future).