Fri, 2008 Sep 12
Wistle Part 5: Multi-Site Views
Multi Site views and public files
Finally, I want to be able to create the views, and do so using haml, erb, etc, and store them in Subversion, and have different views (and that means different stylesheets, etc) for each site. That involves three major actions:
- Decide how to organize the per-site files.
- Figure out how to get those files updates.
- Tell Merb where to find the files.
My organization goes like this
/app
/sites
/SITENAME
/views
(possibly /helpers here in the future).
/public
/sites
/SITENAME
The file updates is more tricky. An easy option would be to use svn:externals. This could be a hassle, though, if the Wistle app is hosting a lot of sites.
Instead, I'm going to update SiteSync to also update the views and public files. This will be done by deleting the current directory (when there has been an update) and exporting the most recent files. First thing, a few more properties in Site:
class Site
property :views_uri, Text
property :views_revision, Integer, :default => 0
property :public_uri, Text
property :public_revision, Integer, :default => 0
# A URI based off of contents_uri to use as the base for building URI's
# for public and views
def base_uri
ary = contents_uri.split("/")
ary.pop if ary[-1].blank?
ary.pop
ary.join("/") + "/"
end
def views_uri
@views_uri || (base_uri + "app/views")
end
def public_uri
@public_uri || (base_uri + "public")
end
end
The additional methods give me some default URIs based on the contents_uri. This is based on my preferred organization.
SiteSync is where the big updates happen. Basically, I add some methods to check if there are updates to the views or public files. If so, the current are deleted and an export is done. This means that the files could be inaccessible for a few seconds (depending on connection speed and repository size). I'm also not sure if/when reboots would be required in a production environment.
class SiteSync
def run
super
export_views
export_public
end
def export_views
export("views", File.join(Merb::root, "app", "sites", @model_row.name, "views"))
end
def export_public
export("public", File.join(Merb::root, "public", "sites", @model_row.name))
end
def export(name, export_path)
export_path = File.expand_path(export_path)
uri = @model_row.__send__("#{name}_uri")
rev = @model_row.__send__("#{name}_revision")
connect(uri)
return false if @repos.latest_revnum <= rev
updated_rev = @repos.stat(uri[(@repos.repos_root.length)..-1], @repos.latest_revnum).created_rev
return false if updated_rev <= rev
FileUtils.mkdir_p(export_path)
FileUtils.rm_rf(export_path)
@ctx.export(uri, export_path)
@model_row.update_attributes("#{name}_revision" => @repos.latest_revnum)
true
end
end
Method #export is the workhorse here, and the bulk is checking if we really need to do any work and that the path is ready for the export. The actual @ctx.export line is anticlimatic.
This does require some updates in Wistle::SvnSync because we may be accessing multiple repositories within on instance. In short, #connect and #context both need to accept a uri option rather than relying on @config.uri. Probably some refactoring is in order (move all connection work to another class, for example).
Telling Merb where to find the files
This turns out to be surprisingly easy, so long as the "correct" helper methods are used. On that note, I'll look first at the public files. This requires two override methods in GlobalHelpers.
module Merb
module GlobalHelpers
def image_tag(img, opts ={})
opts[:path] ||= "/sites/#{@site.name}/images/"
super(img, opts)
end
def asset_path(asset_type, filename, local_path = false)
path = super(asset_type, filename, local_path)
"/sites/#{@site.name}#{path}"
end
end
end
image_tag generates a :path option to the site-specific image directory, unless :path has been set manually. It then calls super to let the original method do the real work.
asset_path is similar but, well, backwards. This is called by js_include_tag and css_include_tag to generate the appropriate path. I call super to let the parent method again do the real work. Then I prepend its result with the site-specific public path.
Another option is if I were using Lighttpd or Apache or something similar to serve public files, I could use the web server's url rewriting capabilities.
The approach I take for the views is fun. In Application, I add this little jewel:
class Application < Merb::Controller
before :update_template_roots
after :revert_template_roots
def update_template_roots
self.class._template_roots = [
["#{Merb.root}/app/views", :_template_location],
["#{Merb.root}/app/sites/#{@site.name}/views", :_template_location]
]
end
def revert_template_roots
self.class._template_roots = [
["#{Merb.root}/app/views", :_template_location]
]
end
end
Is that an ugly hack or what? Surely there's a better way than back and forth modifying a class variable. Please? Well, there probably is, but I don't know Merb's internals well enough.
The key is the class method (I believe representing a class variable), _template_roots . If I understand it all correctly, this is used by render to determine possible base paths and what method to use with that path. So, with each request to render, I tack on the current site's view path as a possible root, call super, then revert to the default. Why this back and forth? Because one request could be directly followed by a request for a different Site.
I half expect to be beaten in my sleep for that one. But it works.
Conclusion
Of course, this is just the starting point, but it's met my goals, and I hope it's illustrated both some basics of Merb and DataMapper as well as how these can be used to interact with data that is not stored in an relational database. After all, great frameworks and libraries can really free us to focus on the important bits, but they can also make it difficult to see all the possibilities.
After that cheesy statement, here's a few pieces I'd like to expand Wistle with in the future:
- Tags (now supported)
- Search
- Date links (i.e. /2008 gets all articles from 2008)
- RSS/Atom
- A sync action (for use by, e.g. subversion hooks; now supported)
- Pagination
- Per-site Helpers (maybe; I've debated whether there's any likely value in this)
- Better support for STI (I've played with this a bit)
Finally, it's worth mentioning that my intent with these articles is illustrative and/or tutorial, rather than to start a "project". That is, I hope this helps people who are writing their own blog or similar application. However, should you decide to use Wistle, that's great, and I'd be happy to receive bug reports, feature requests, etc. Whether I will do anything with them probably depends on the day.
