Sun, 2008 Oct 05

Minesweeper

Posted in Computing at 19:35 by jmorgan

I’m playing Minesweeper a lot these days. I know, classic Windows game. The one the haven’t yet messed up with an “Internet version”. Anyway, it’s a fun game, doesn’t take too long per game, although in sum, I can wile away many hours.

Complaint: I’m a big fan of resolution increases. I can fit more stuff on the screen, and it’s increasingly clear. The problem is that things keep getting physically smaller, which is particularly noticeable with the little cheesy games which are sensitive to click location. Say, Minesweeper. Also, websites (and no, I don’t particularly care IE7’s weird resizing thing. It ends up looking fuzzy. Of course, I avoid IE anyway).

So, what about per application/window resolution? Is this technically feasible? Obviously, it could be a pain for the user–“Hey, why is the menu bar in Word so much bigger”–but it could be very nice as well. I have my mouse reactions tuned to my normal state of work–bookkeeping and programming. I want quick movement not fine interactions, which doesn’t work so well in super-mini-looking Minesweeper.

General purpose typically means multiple purpose. But resolution settings tend to hope for one-size-fits-all. Of course, there’s probably many other solutions.

Fri, 2008 Oct 03

Wait...

Posted in Money at 14:01 by jmorgan

Why the hallibut is the DOW down?!

Fri, 2008 Sep 19

My Sweet Lord

Posted in General at 21:30 by jmorgan

Are you familiar with George Harrison’s song, “My Sweet Lord”? It’s this worship song filled with so much longing and desire to just be closer, just be nearer. The subject is not Jesus, but Krishna. Ignoring that (I think) there are people who see Jesus and Krishna as one, every time I hear this song, I’m left wondering why I’ve never heard a song sung to Yahweh and/or Christ with that kind of heartfelt longing to be closer.

Fri, 2008 Sep 12

Wistle Part 5: Multi-Site Views

Posted in Wistle at 09:00 by jmorgan

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.

Fri, 2008 Sep 05

Wistle Part 4: Multi-Site Subversion Models

Posted in Wistle at 09:00 by jmorgan

Multi Site models

Ah, point #4, multiple sites hosted on one Wistle instance. I'm not going to create additional "library" functionality to support this. After all, this is getting pretty application-specific. But, I am going to take advantage of the existing Wistle library.

The key point here is going to be a Site model, that: a) Articles belong to; and b) takes over storing per-site configuration. In essence, it will replace Wistle::Config. The key is that "site-wide" configuration will subsitute for model-wide configuration. So, let's start with the Site model.

class Site
            include DataMapper::Resource
          
            has n, :articles
          
            property :id, Integer, :serial => true
            property :name, String, :unique => true, :nullable => false
            property :domain_regex, String
          
            # Subversion
            property :contents_uri, Text
            property :contents_revision, Integer, :default => 0
            property :username, String
            property :password, String
            property :property_prefix, String, :default => "ws:"
            property :extension, String, :default => "txt"
          
            # Content Filters
            property :article_filter, String
            property :comment_filter, String
          
            # Timestamps
            property :created_at, DateTime
            property :updated_at, DateTime
          end
          

Here's the properties (and has n, :articles). Note that the properties under the "Subversion" heading are pretty close to the instance variables of Svn::Config. Also, notice contents_uri and contents_revision. These match with Config's uri and revision. Why the prefix? Because I want to able to use a different uri (possibly in another repo) for views and public files. But that is for the next section. I could set up username, etc this same way; I won't for now, because I have no use for doing so. If I were, however, I would probably create yet another model, called "Config" or something that belongs to a Site, with a role property. Like I said, it's not needed for now, so I won't bother.

The contents_* fields could create a problem though, because Wistle::SvnSync expects different names. A simple solution is some (not-quite) aliasing:

class Site
            def uri
              @contents_uri
            end
          
            def revision
              @contents_revision
            end
          
            def revision=(rev)
              attribute_set(:contents_revision, rev)
            end
          
            def body_property
              :body
            end
          end
          

The revision= is also used by Wistle::SvnSync, and body_property is another configuration option that SvnSync expects. With body_property, there's only one option, at least so long as I only use the one model (Article). So, body_property always returns :body. I'll show how all this hooks into SvnSync in a moment. Before that, though, a bit about the :domain_regex property.

Wistle is not designed to be user-friendly in the traditional sense, except when the user is defined as me. For example, adding Sites, deleting Comments, etc. must, at this point, be done through a console. That's great by me, but for someone without programming experience, Wistle would probably not be a good choice. Another example is the domain regex property. It's used by Site.by_domain (below) to find a site based on a domain. Except, as it's name implies, domain_regex is a regular expression. Great for me, might be less attractive to others.

class Site
            class << self
              # Find a Site by domain regex, prefer longest match.
              def by_domain(val)
                possible = []
          
                # Find matching Sites
                Site.all.each do |s|
                  r = Regexp.new(s.domain_regex.to_s, true)
                  m = r.match(val)
                  if m
                    possible << [s, m[0].length] 
                  end
                end
          
                      # Sort for longest match.
                possible.sort!{ |a, b| b[1] <=> a[1] }
                possible[0] ? possible[0][0] : nil
              end
            end
          end
          

I no longer need to include Wistle::Svn in the Article model, but I do need to add in the properties that Wistle::Svn took care of.

class Article
              # Subversion-specific properties
              property :path, String
              property :svn_created_at, DateTime
              property :svn_updated_at, DateTime
              property :svn_created_rev, String
              property :svn_updated_rev, String
              property :svn_created_by, String
              property :svn_updated_by, String
          end
          

I also update how Filters works to deal with the *_filter properties. To utilize these properties, in Article and Comment, I change the :filter option of the body property to set :default => :site . This tells the Filters::Resource module to use the Site model to determine default filters. In Comment, I also add a method #site, because Filters may try to call this method.

class Comment
            def site
              @article.site
            end
          end
          

Now, you may have noticed a few weird methods that didn't do much in SvnSync, partically get and new_record. Here's where they come in. To use SvnSync with the new Site model (instead of the Wistle::Model Model), a few things have to change. First, Site doesn't have a config method, pointing to a Wistle::Config object. It does, however, respond to the the same methods as a Config object. Second, when creating or getting the content, we need to scope by Site. What to do? Inherit Wistle::SvnSync and override a few key methods.

class SiteSync < Wistle::SvnSync
            def initialize(model_row)
              @model_row = model_row
              @model = Article
              @config = model_row
            end
          
            # Get an Article by site and path.
            def get(path)
              Article.first(:site_id => @model_row.id, :path => short_path(path))
            end
          
            def new_record
              @model.new(:site_id => @model_row.id)
            end
          end
          

Awesome-sauce.

Now, just hook in Site to SiteSync and all the ugly work is done!

class Site
            def sync
              SiteSync.new(self).run
            end
          
            class << self
              def sync_all
                Site.all.each do |site|
                  site.sync if site.contents_uri
                end
              end
            end
          end
          

The controllers need a few updates to filter by Site (and the application view needs one for the list of recent articles, but I'm ignoring views). Application needs updates first:

class Application < Merb::Controller
            before :sync_articles
            before :choose_site
          
            protected
          
            def sync_articles
              Site.sync_all
            end
          
            def choose_site
              @site = Site.by_domain(request.host)
            end
          end
          

I change the syncarticles method to use Site.sync_all. Then, I add a choosesite before filter to assign @site, using Site.by_domain (request.host is the full host name including any port number).

One other bit I want to do that might as well fall in this section is folders as categories. My approach here is definately I reflection of my personal organizations styles; in addition, the code is probably not a good solution.

Anyway, I want each top-level folder under the articles directory to represent a category; I want to be able to add additional subfolders without them creating additional categories. I also prefer to use only one category per article, with additional "categorization" through tags (which I will not be implementing in this already way too long article).

To do so, I need to add a category property, which I'll update with a before :save hook

class Article
            property :category, String
            before :save, :update_category
          
            def update_category
              if attribute_dirty?(:category) || @category.nil?
                attribute_set(:category, @path.split('/')[0]) if @path
              end
            end
          end
          

I then add two new methods to Site, one to get a list of categories, the second to find published articles by category.

class Site
            def categories 
              repository.adapter.query('SELECT category FROM articles WHERE site_id = ? group by category order by category', self.id)
            end
          
            def published_by_category(category = nil, options = {})
              conditions = "datetime(published_at) <= datetime('now') "
              if category
                conditions << "and path like '#{category}/%' "
              end
              Article.all(options.merge(
                    :conditions => [conditions + "and site_id = ?", self.id],
                    :order => [:published_at.desc]))
            end
          end
          

Now is also a nice time for some routing updates, both to take advantage of categories, and for "permalink" paths for the Articles. I'm taking advantage of Merb's support for regular expressions in routes:

Merb::Router.prepare do |r|
            r.resources :articles do | article |
              article.resources :comments
            end
          
            r.match('/').to(:controller => 'articles', :action =>'index')
          
            r.match(%r[/categories/(.*)]).to(
               :controller => 'articles', :action => 'index', :category => '[1]')
          
            r.match(%r[/(.*)]).to(
               :controller => 'articles', :action => 'show', :path => '[1]')
          end
          

The articles resource remains to support comments, although it is probably not needed.

The last match is the "permalink" one, so that there's not "articles" or other prefixes in permalinks; doing this obviously depends on the particular application.

And the Articles controller gets a couple of updates to take advantage of these routes:

class Articles < Application
            # provides :xml, :yaml, :js
          
            def index
              @articles = @site.published_by_category(params[:category])
              display @articles
            end
          
            def show
              if params[:path]
                @article = Article.first(:path => params[:path], :site_id => @site.id)
              else
                @article = Article.first(:id => params[:id], :site_id => @site.id)
              end
          
              raise NotFound unless @article
              display @article
            end
          end
          

Revision 73

And, next, the views...