Tuesday, 19 June 2007

Slice and Dice

I've been playing around with sorting and scoping to neatly and easily sort and filter an index page. I'm finally happy with a generic set of scoping/sorting methods I can quickly apply to a new resource. It uses the autoscope plugin I discussed earlier, as well as the smart column sorting I played with.

Scope up

To start with, drop some scoping into your model object - and add the three required methods below (with suitably appropriate modifications for your purposes).

  auto_scope \
       :active => {:find => {:conditions => ['destroyed_at IS NULL']}},
       :archived => {:find => {:conditions => ['destroyed_at IS NOT NULL']}}

  # Available scopes - shouldn't this be available via autoscope?
  def self.scopes
    [:all, :active, :archived]
  end
  # Defaults (put into the model object)
  def self.default_scope
    :active
  end
  def self.default_sort
    '(destroyed_at IS NULL) DESC, login ASC'
  end

Next, drop these methods into your application.rb

  # Generate a quick-and-dirty description of the chosen sort order (for displaying in the template)
  def sort_desc
    "#{params[:dir] == 'down' ? 'descending' : ''} by #{params[:col_name] || params[:col]}"
  end
  # Generates a SQL order-by snippet based on requested sort criteria (or given default). 
  #
  # Adapted from the following blog post:
  # http://garbageburrito.com/blog/entry/447/rails-super-cool-simple-column-sorting
  def sort_order(model, default)
    orderby = "#{params[:col]} #{params[:dir] == 'down' ? 'DESC' : 'ASC'}"
    return sort_desc, orderby
  end

  # Uses model scoping to generate a scoped subset of the required objects
  # for use in the index view.
  # Returns a sorted list of the object .
  # Assumes existance of three methods on the model: 
  # scopes:: returns an array of acceptable scopes for this model
  # default_scope:: returns the scope to use when none has been selected
  # default_sort:: represents an SQL-appropriate string for this model
  #   representing the default way of sorting this model object
  def scoped_search(model, scope)
    scope = model.default_scope unless model.scopes.include?(scope.to_sym)
    sort_desc, orderby = sort_order(model, model.default_sort)
    # a description of the search/sort to display in the view
    filter_desc = "#{scope.to_s} #{model.name.pluralize.downcase} #{sort_desc}"
    return filter_desc, model.find(:all, :order => orderby) if scope.to_sym == :all
    return filter_desc, model.send(scope.to_sym).find(:all, :order => orderby)
  end

Using it is now easy. Drop something like this into your controller and call on it for all your collection-based actions.

  # applies user-specified filters and sorting to the specified collection
  def filter_users
    scope = params[:user_scope].to_sym if params[:user_scope]
    @filter_desc, @users = scoped_search(User,scope)
  end

If you need to paginate, you can always paginate-collection, or add the code into the scoped_search function.

No comments: