Blog

Ruby Metaprogramming is Awesome

by Jim Gay

If you’ve come to read about how wrong I am about metaprogramming, don’t worry, I’m sure I’ll follow-up with a post about how bad it is to do metaprogramming and how it causes you real, physical pain.

If, however, you’ve come to find out about how awesome metaprogramming is, then you can tell by the title of this article that you are in the right place! Now that’s authoritative!

The best part about this article is that it is for you! That’s right, newbie. I’m not going in-depth, I’m merely going to discuss how a dash of metaprogramming solved an annoying problem.

I do a lot of work with Radiant. And Radiant manages pages well, but there are plenty of extensions that allow you to do other things. You can, for example, add the ability to upload files and whatnot. Or you can add the ability to edit stylesheets and javascripts.

That’s pretty cool. Radiant does a good job of managing pages in the pages table. But stylesheets and javascripts are not pages. Some of the samples that Radiant bundles have stylesheets as regular pages in the page tree and are tied to an almost blank layout that sets a particular content-type like text/css. Yuck. So Chris Parrish, the author of SNS, added text_assets to handle other types of content like stylesheets and javascripts.

One of his reasons for creating this extension is the weirdness of having a stylesheet in your page tree: “No more confused users wonder what those fancy pages are.” Saaaweet! Thanks, Chris.

All is well until you find out that the SNS extension doesn’t let you use your typical Radius tags within javascripts or stylesheets like:

#something { background: url('< r:assets:url title="my_image" />'); }

Nooooooo! Why, God, why!?!

Well, with SNS, you’re dealing with a TextAsset and not a Page. But if you’re familiar with Radiant, you know that page tags aren’t only used on pages, they’re used on snippets and layouts too. So what’s the deal with that?

All of the radius tags that you use in Radiant are evaluated in the context of a single page. Pages, snippets, and layouts are all used in reference to rendering a Page. But SNS renders a TextAsset and as a result, it doesn’t have any of the fancy tags added. Afterall, it’s class TextAsset < ActiveRecord::Base and not class TextAsset < Page.

You read that right: text assets are not pages. Bummer, dude.

Well, what if you could include the same modules as are included in the Page model? Then you could use those tags in your stylesheets.

Fire up your console because you’re about to see how to do it. In a rails console (for a Radiant instance), try this:

Page.included_modules

And you’ll get back a bunch that don’t matter for you. Let’s simplify that:

>> Page.included_modules.select{|m| m.to_s =~ /Tags$/}
=> [TextileTags, SmartyPantsTags, MarkdownTags, StandardTags]

Awesome. All we need to do is include all of those modules into TextAsset and we’re done! Almost.

Since we’re dealing with the SNS extension, you’ll also see that it includes Sns::PageTags which adds tags to output stylesheets and javascripts. Those, we don’t need. So we can filter them out and include them into TextAsset:

TextAsset.class_eval {
  Page.included_modules.select{|m| m.to_s =~ /Tags$/}.reject{|m| m == Sns::PageTags }.each do |mod|
    include mod
  end
}

Bingo! Now we’re in business. Except for the fact that it doesn’t work.

So we need to update the context in which these tags are evaluated. Since they are all written expecting to be included in a Page model, the global variables for the tags need to have knowledge of a page.

TextAssetContext.class_eval {
  def initialize(text_asset)
    super()
    globals.page = text_asset # This is the important line that adds the hook for all existing tags for pages.
    globals.text_asset = text_asset
    text_asset.tags.each do |name|
      define_tag(name) { |tag_binding| text_asset.render_tag(name, tag_binding) }
    end
  end
}

There you have it. All it took was some class_eval and some looping over included_modules.

page tags in stylesheets And now you’ll have happy users. Because even though Chris solved the problem of weird stylesheet pages in the page tree, the solution introduced a problem where editors of the site expect the radius tags to simply work everywhere. Now they do.

The above code assumes that the only valuable tags are those whose modules have “Tags” at the end of their names. That’s a reasonable expectation, but we can go even further by inspecting the included modules of the included modules to see if Radiant::Taggable is there:

TextAsset.class_eval {
  Page.included_modules.reject{|m| m == Sns::PageTags }.each do |mod|
    if mod.included_modules.any? {|inc| inc == Radiant::Taggable }
      include mod
    end
  end
}

So go ahead and install the page attachments, or paperclipped, or whatever extension with sns and just do script/extension install sns_page_hook, or better yet: gem install radiant-sns_page_hook-extension. Please let me know if you find any problems with it.

Permalink… Comments: 2

Radiant 0.9 Official Release

by Jim Gay

Finally, after a long, long wait: 0.9 is out.

It’s my first non prerelease gem to push out for Radiant but I’m proud of all the work that I and many, many others did to make it happen. John Long put the UI updates together with the help of a good group. Keith Bingman managed all that went into internationalization and the many contributors that made it happen. William Ross added features such as pagination in the standard Radius tags, and pagination in the admin screens. Josh French gave us the ability to load extensions from gems. John Muhl, our newest core team member increased the general quality in testing for bugs and improving the overall quality of the application.

There are many contributors, I’m just listing some from the top of my head. Thank you to everyone of the contributors, be you bug reporters, committers, or just users who discuss your needs on the mailing list.

While this release is an exciting improvement, I’m even more excited about the future of the project. More to come…

Permalink… Comments: 0

Rack Cache on Heroku with Memcached

by Jim Gay

The convenience of deploying application on Heroku is attractive especially with their add-ons and the free options that they provide, in particular Memcache.

If you’re working with an application which needs to manage it’s cache with Rack::Cache, you’ll want to have fast responses for your metastore. The meta information about your content is probably the most important part of your cache since it’s checked by many clients for the status of the content: is it fresh or stale? See more about How Web Caches Work.

Typically you might setup Rack::Cache like this:

config.middleware.use Rack::Cache, :metastore => 'file:tmp/cache/meta', :entitystore => 'file:tmp/cache/entity'

5MB of Memcache is a decent place to start for free and it’s integrated into your application without any effort on your part. So on Heroku you can use Memcache as your metastore like this:

$cache = Memcache.new
config.middleware.use Rack::Cache, :metastore => $cache, :entitystore => 'file:tmp/cache/entity'

That’s simple enough, and it’s just as easy if you’re deploying Radiant:

$cache = Memcache.new
config.middleware.use Radiant::Cache, :metastore => $cache

If you want to look at an example of a simple app that does this, there’s an easy to understand sample application on github. Enjoy your speedy metastore.

Permalink… Comments: 7

Radiant Page#find_by_url

by Jim Gay

On a recent project a client asked about overriding Page#find_by_url and when that actually occurs. I think the answer should be explained for everyone working with it.

20 second summary

This is an in-depth look at the method that gathers pages within Radiant. In short, if you want to do special page finding, create a subclass of Page and write your own find_by_url method to adjust the way Radiant behaves. Every page will respond to this method and return appropriate pages according to the requested url. In the admin interface, you can select your special page type to make that page behave as you have specified.

Simple finding

find_by_url is defined both as a class method and an instance method. Let’s look at the class method Page.find_by_url from the Page model:

class << self
  def find_by_url(url, live = true)
    root = find_by_parent_id(nil)
    raise MissingRootPageError unless root
    root.find_by_url(url, live)
  end
  # ...
end

First, it looks for the root page, which is considered the page with no parent_id. If no root page is found it raises a MissingRootPageError exception; otherwise, it calls the instance method find_by_url on the root page.

This class method takes 2 arguments: the url (really the path matched in the routes from request) to be found, and a live flag which defaults to true (more about that later).

Finding the first page

The find_by_url instance method is a bit more complex. Let’s take a look:

def find_by_url(url, live = true, clean = true)
  return nil if virtual?
  url = clean_url(url) if clean
  my_url = self.url
  if (my_url == url) && (not live or published?)
    self
  elsif (url =~ /^\#{Regexp.quote(my_url)}([^\\/]*)/)
    slug_child = children.find_by_slug($1)
    if slug_child
      found = slug_child.find_by_url(url, live, clean)
      return found if found
    end
    children.each do |child|
      found = child.find_by_url(url, live, clean)
      return found if found
    end
    file_not_found_types = ([FileNotFoundPage] + FileNotFoundPage.descendants)
    file_not_found_names = file_not_found_types.collect { |x| x.name }
    condition = (['class_name = ?'] * file_not_found_names.length).join(' or ')
    condition = \"status_id = \#{Status[:published].id} and (\#{condition})\" if live
    children.find(:first, :conditions => [condition] + file_not_found_names)
  end
end

Wow. There’s a lot going on there and there’s room for some refactoring, but for now let’s just walk through it.

First, nil will be returned if the page is virtual?. A page, by default, is not virtual. This is stored in the database in a boolean field, but you may override this in any subclass of Page that you create. For now, let’s assume that your page isn’t and won’t be virtual and we’ll get back to what it means.

Next, we clean the url if the clean flag is set to true (which it is by default). clean_url simply ensures that the url being checked is properly formatted and that any doubling of slashes is fixed. So this right//here//// becomes this /right/here/.

The next step shows us why we clean the url. A local variable is setup to compare against the page’s url.

my_url = self.url
if (my_url == url) #...

What is a page’s url? It’s calculated by the page’s slug and the slugs of it’s ancestors. In short, if your current page’s slug is ‘here’ and it’s parent page is ‘right’, and that page’s parent is the home page (with a slug of ‘/’) then your current page’s url is ‘/right/here/’.

So we check to see that to see if it is the same as the url in the request. But also, in this comparison, we check to see if the live flag is set and is false or if the page is published?.

This live flag is a bit strange in appearance:

my_url = self.url
if (my_url == url) && (not live or published?)

By default, this not live returns false (since live is true by default and we reverse it with not) so it moves on to published?. You might set live to false in other situations, but for now we’ll just go with this.

A page is published? if it’s status (as stored in the database) is the ‘Published’ Status.

So if the incoming url matches the current page’s url (which at the first pass is the root or home page), then we return with the current page:

my_url = self.url
if (my_url == url) && (not live or published?)
  self

Finding deeper pages

If it isn’t true that the incoming url and the current page’s url are equal, then we move on to the next step:

my_url = self.url
if (my_url == url) && (not live or published?)
  self
elsif (url =~ /^#{Regexp.quote(my_url)}([^\/]*)/)

Here it matches the incoming url against a Regexp of the current page’s url. When it starts, we’re matching the root page which has a url of ‘/’. If that’s the incoming url, it would have been caught in the original if block, but we ended up at the elsif. The Regexp that’s used matches the next slug in the incoming url. So if the incoming url is ‘/right/here/’ then it will match the slug ‘right’.

From that match, we find the current page’s children by their slug (remembering that the current page is the root, with a slug of ‘/’):

elsif (url =~ /^#{Regexp.quote(my_url)}([^\/]*)/)
  slug_child = children.find_by_slug($1)

If it finds that ‘slug_child’, then we call find_by_url on that page to loop down the tree to find the final page that we want (which would be the page that responds to the url ‘/right/here’ or in this simple case, the page with a slug of ‘here’). If it finds the page, then it returns the found page:

  slug_child = children.find_by_slug($1)
  if slug_child
    found = slug_child.find_by_url(url, live, clean)
    return found if found
  end

In that if slug_child block, the slug_child.find_by_url acts as a loop. Because every page responds to this method and will do exactly what is happening here for the root page, each page will search it’s children for a slug matching the slug from the incoming url and any found page will likewise call find_by_url to search it’s children as well.

There is some room here for some optimization in the way we do a lookup for a page, but for now it works and we can get to the refactoring another time.

When no slug is found: customizing the finder

If the slug_child is not found (and no child matches that slug) then this if slug_child block is never hit and we move to the next step. This is where the magic happens for subclasses of Page:

  children.each do |child|
    found = child.find_by_url(url, live, clean)
    return found if found
  end

It asks each child of the current page if it responds to find_by_url and returns any found page.

So even if none of the pages are found by the slug, we still ask the children if they respond to find_by_url. Why would we do this?

The answer lies in one of the included extensions: Archive.

The ArchivePage is a subclass of page which provides it’s own find_by_url method. The ArchivePage#find_by_url will check the incoming url for it’s details and if it meets certain requirements (namely that there is a standard date format in the url such as ‘articles/2010/06/22’) then it will find the appropriate page type such as ArchiveDayIndexPage, ArchiveMonthIndexPage or ArchiveYearIndexPage and return the proper page. If none of those are found it just calls super and calls the original Page#find_by_url.

This can act as your router for your custom page types. If you want to return a particular page type, such as a ProductsPage and your url is ‘/products/1234’ then you can create a ProductPage which has it’s own find_by_url method and would find your ProductDetailsPage to display a standard view of all of your products based upon the slug ‘1234’ which I’d assume would be a product id, but could be anything you want.

Handling 404

Lastly, if none of this finds any pages to return, Radiant has a FileNotFoundPage page which allows you to easily create your own 404 error message for content that isn’t found. You can subclass a FileNotFoundPage page to provide your own behavior there too. But when searching for a match to an incoming url, Radiant will find deeply nested 404 pages. So you can create a FileNotFoundPage as a child of your root page, but you can also create a FileNotFoundPage as a child of your ProductsPage to return an appropriate message to someone looking for ‘/products/not-a-valid-url’.

Here’s the code for that last step:

  file_not_found_types = ([FileNotFoundPage] + FileNotFoundPage.descendants)
  file_not_found_names = file_not_found_types.collect { |x| x.name }
  condition = (['class_name = ?'] * file_not_found_names.length).join(' or ')
  condition = "status_id = #{Status[:published].id} and (#{condition})" if live
  children.find(:first, :conditions => [condition] + file_not_found_names)

The live flag comes into play here again and optionally allows you to find pages that are not published. By default live is true, so in this instance we only check for a FileNotFoundPage that is published.

Radiant has a ‘development’ mode which would find unpublished pages, but that’s a subject for another discussion.

I hope this gives you a good understanding of how Radiant finds its content, and how you can easily bend it to behave differently by creating a subclass of Page and writing your own find_by_url method. If I’ve left anything out or if you want me to cover some other aspect, let me know in the comments.

Permalink… Comments: 2

Combining show_for and stonewall

by Jim Gay

I’d been doing some thinking about simplifying the display of records in Rails applications and fortunately came across show_for.

show_for is a formtastic-like approach to displaying details about database records. It helps you DRY up your views and can even reflect on associations with code like this:

<% show_for @user do |u| %>
  <%= u.attribute :name %>
  <%= u.attribute :nickname, :in => :profile %>
  <%= u.attribute :confirmed? %>
  <%= u.attribute :created_at, :format => :short %>
  <%= u.attribute :last_sign_in_at, :if_blank => "User did not access yet",
                  :wrapper_html => { :id => "sign_in_timestamp" } %>

  <% u.attribute :photo do %>
    <%= image_tag(@user.photo_url) %>
  <% end %>

  <%= u.association :company %>
  <%= u.association :tags, :to_sentence => true %>
<% end %>

But since I’m working on an application which uses stonewall (which allows you to limit the display of a record’s details), I needed the 2 of them to work together.

stonewall allows you to guard the display of your data from your models:

stonewall do |s|
  s.varies_on :aasm_state

  s.guard_attribute :secret_cultivation_details

  # ... role details here
end

It provides a allowed? method which you can use to ask an object if the current user is allowed to see the content.

So I quickly put together show_for_stonewall which will limit the display of certain fields if stonewall prevents it. It was as simple as using alias_method_chain to check that an attribute is not allowed

!allowed?(@object, current_user, attribute_name)

You can edit your show_for translation yaml and add a redacted key for the content to display if a field is not allowed for the current user.

Permalink… Comments: 0

Adventures in Javascript

by Jim Gay

I wrote a simple Javascript library for myself in 2006 called simpleDOM.

At the time, there was a problem with using innerHTML in Internet Explorer to write content to a page and then select it when traversing the DOM or submit a form with the added content. Not only that, but it’s expensive to process and manipulate DOM objects in the browser while they are displayed so this library gave me an easy way to create document fragments and manipulate those to then be injected in the right place.

Over time, I got into working with other libraries and problems like this were solved for me, so I never did much more with it.

I’ve still got it working in one production site but haven’t looked at it in a long time. I thought I’d put it out there on github for posterity.

Permalink… Comments: 0

1999 - 2010 © Saturn Flyer LLC 1901 N. Moore St. Suite 206 Arlington, VA 22209

Call Jim Gay at 571 403 0338