Introducing has_browser: Parameterized browse interfaces for your AR models. 8

Posted by james
on Monday, May 19

Pretty soon after starting to use has_finder, I needed to write a browse interface. That is, given a set of parameters, return all the models that match. With has_finder, it is possible to compartmentalize those parameters in to small chunks, each with names that make sense in your model's domain. You can then chain them together to get their combined scope. The only problem iswas calling them when you aren't aware, in advance, of which finders need to be called.

Having had this problem a few times, and being sick of solving it over and over again, I decided to extract a general solution from a project I'm working on.

has_browser

It's a simple plugin, with a simple syntax. Using the canonical blog example, let's imagine we want to create a browse interface to posts. We'd want the user to be able to browse by category, author, or tags, but not to be able to access any of the other finders on the Post model for obvious security reasons. To set up has_browser, we'd do something like this:

has_browser :category, :tags, :author

Then, assuming the has_finders are already written, the posts can be browsed as follows:

Post.browse(:category => 'Testing', :tags => 'activerecord', :author => 'james')

In that example, each of the finders requires an argument; has_browser also supports finders that don't. As long as the argumentless finder is present in the browse hash, it will be called:

has_browser :category, :tags, :author, :without_args => [:order_by_date, :order_by_number_of_comments]

Post.browse(:category => 'Testing', :tags => 'activerecord', :author => 'james', :order_by_number_of_comments => 'true')

Browse can also be called from association_proxies. For a multi-blog platform, we could easily restrict browsing of posts to the current blog:

@blog.posts.browse(:category => 'Testing', :tags => 'activerecord', :author => 'james', :order_by_number_of_comments => 'true')

Since has_browser returns the same proxy as has_finder, it is possible to further restrict the results of a browse by chaining finders after the browse call. With our blog, for example, we'd probably want to restrict browsing to published posts.

@blog.posts.browse(:category => 'Testing', :tags => 'activerecord', :author => 'james', :order_by_number_of_comments => 'true').published

Note: It is not possible to chain finders before the browse call.

Finally, like has_finder, has_browser is compatible with will_paginate out of the box.

Get It

Releases of has_browser will be available as a gem, which you can freeze in to your rails app (init.rb included):

$ sudo gem install has_browser

Development, of course, is at github.

Plugins I've Known and Loved #2: has_finder 10

Posted by james
on Monday, February 25

Plugins I've Known and Loved started out as a would-be series of presentations at Montreal on Rails. I've been bad about getting to the second presentation, so I thought I'd continue PLIKaL here. No, I'm not going to talk about any of my plugins. Instead, over the next week or two, I'd like to tell you about some wonderful plugins that I only recently discovered (though, none of them are particularly new). Today — a plugin that belongs in everybody's toolbox: has_finder.

The basic idea of this school of plugins is creating reusable scopes for your models. For instance, if you had a blogging application with a post model, you might want to query for published posts. To that end, you might write something like this in your controller:

Post.find(:all, :conditions => {:published => true})

Hopefully, though, you're familiar with the skinny controller, fat model best practice, so you'd write it like this:

class Post < ActiveRecord::Base
  def self.published
    find(:all, :conditions => {:published => true})
  end
end

That's a lot better, but far from perfect. If you were to write several such methods, for example, you would not be able to combine them easily. So, looking for all published posts written by James would require writing a second method; not very DRY. Of course, ideally, you'd use an association proxy to accomplish that goal anyhow. That would work with our hand-written finder, but we'd lose all the benefits of the association_proxy like nested finds (...published.find(:all, :order => 'created_at DESC')), and we certainly wouldn't be able to chain two of them together (...published.order_by_recent). has_finder solves those problems, and more.

I Can Has Finder?

In order to reproduce the finder we wrote earlier, you'd write the following:

has_finder :published, :conditions => {:published => true}

Should you want to find all published blog posts by James, you could then make use of association proxy:

User.find_by_name('James').published.find(:all, :order => 'created_at DESC')

I have come across a good number of associations used for similar purposes, but defined on the associated model. For instance (omitting irrelevant details):

has_many :published_posts, :conditions => {:published => true}

Having discovered the wonders of has_finder, I now consider this to be an anti-pattern. The has_finder method is definitely DRYer. Far more importantly, however, the definition of what makes a post published (in our case, :published => true) belongs in the post model. The same rule applies to ordering. Following this best practice ensures there's only one point of change for refactoring, and that your models and their tests tell a more complete story about the data they represent.

Finally, has_finder supports parameterization of finders. You can wrap your conditions hash in a lambda that accepts an arbitrary number of arguments. Continuing with our blog post example, you might wish to query your posts table for all posts this week, this month, or this year, depending on the situation. To keep DRY, you could define your finder as follows:

has_finder :since, lambda { |date| {:conditions => ['created_at > ?', date]} }

Using it like this:

Post.since(1.week.ago)

To put it all together, let's query for all James's published posts in the last week:

User.find_by_name('James').posts.published.since(1.week.ago)

It reads just like a sentence!

Oh yeah, and all of this is compatible out of the box with will_paginate. I heard a rumor that it's going to be sucked down in to rails, too. Really, how could you not check it out (get it here)?


Clicky Web Analytics