views:

24

answers:

1

I have a big, flat table:

id
product_id
attribute1
attribute2
attribute3
attribute4

Here is how I want users to get to products:

See a list of unique values for attribute1.
Clicking one of those gets you a list of unique values for attribute2.
Clicking one of those gets you a list of unique values for attribute3.
Clicking one of those gets you a list of unique values for attribute4.
Clicking one of those shows you the relevant products.

I have been coding Rails for about 4 years now. I just can't unthink my current approach to this problem.

I have major writer's block. Seems like such an easy problem. But I either code it with 4 different "step" methods in my controller, or I try to write one "search" method that attempts to divine the last level you selected, and all the previous values that you selected.

Both are major YUCK and I keep deleting my work.

What is the most elegant way to do this?

A: 

Here is a solution that may be an option. Just off the top of my head and not tested (so there is probably a bit more elegant solution). You could use chained scopes in your model:

class Product < ActiveRecord::Base
  scope :with_capacity, lambda { |*args| args.first.nil? ? nil : where(:capacity=>args.first) }
  scope :with_weight, lambda { |*args| args.first.nil? ? nil : where(:weight=>args.first) }
  scope :with_color, lambda { |*args| args.first.nil? ? nil : where(:color=>args.first) }
  scope :with_manufacturer, lambda { |*args| args.first.nil? ? nil : where(:manufacturer=>args.first) }

  self.available_attributes(products,attribute)
    products.collect{|product| product.send(attribute)}.uniq
  end

end

The code above will give you a scope for each attribute. If you pass a parameter to the scope, then it will give you the products with that attribute value. If the argument is nil, then the scope will return the full set (I think ;-). You could keep track of the attributes they are drilling down in in the session with 2 variables (page_attribute and page_attribute_value) in your controller. Then you call the entire chain to get your list of products (if you want to use them on the page). Next you can get the attribute values by passing in the set of products and the attribute name to Product.available_attributes. Note that this method (Product.available_attributes) is a total hack and would be inefficient for a large set of data, so you may want to make this another scope and use :select=>"DISTINCT(your_attribute)" or something more database efficient instead of iterating thru the full set of products as I did in the hack method.

class ProductsController < ApplicationController
  def show
    session[params[:page_attribute].to_sym] = params[:page_attribute_value]
    @products = Product.all.with_capacity(session[:capacity]).with_weight(session[:weight]).with_color(session[:color]).with_manufacturer(session[:manufacturer])
    @attr_values = Product.available_attributes(@products,params[:page_attribute])
  end
end

Again, I want to warn you that I did not test this code, so its totally possible that some of the syntax is incorrect, but hopefully this will give you a starting point. Holla if you have any questions about my (psuedo) code.

cowboycoded
That is awesome. I read Ryan Daigle's scope doc on edge rails right before I read your solution. I really appreciate your insight, thanks for the tips!
HuckyDucky
No problemo...I use something similar for a comparison shopping site to filter products based on attributes. One note.. the session variables might introduce some problems. In my app, the users can clear the attribute values by clicking [X], but your design is a little different and if you user's start over the drill-down process, you will want to figure out a way to remove the values from the sesssion vars. This is kinda tricky, so it may make more sense to pass the values as hidden params or something.
cowboycoded