Make rails render different views for multiple sites
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:
- We identified the parts of the app that are client-specific
- We didn't have to worry about partials that are only used with one client (
return "") - 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).
