Half-Penny For Your Thoughts

rounded down to the nearest cent



Categories


Recent Articles




Wistle

Wistle Part 5: Multi-Site Views

Multi Site views and public files

Parts 1 2 3 4

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:

  1. Decide how to organize the per-site files.
  2. Figure out how to get those files updates.
  3. 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.

Revision 79

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.


0 Comments

Add a comment