def self.filter_price(direction, price)
return nil if direction.blank? or price.blank?
return {:error => 'Invalid argument: direction'} unless ['at most', 'at least'].include? direction
return {:error => 'Invalid argument: price'} unless price.to_s =~ /\A[0-9]+\Z/
case direction
when 'at most' then return {:sql => 'price <= :price', :price => price.to_s}
when 'at least' then return {:sql => 'price >= :price', :price => price.to_s}
else raise 'Unknown error'
end
end
def self.filter_bedrooms(bedrooms)
return nil if bedrooms.blank?
return {:error => 'Invalid argument: bedrooms'} unless bedrooms.to_s =~ /\A[0-9]+\Z/
{:sql => 'bedrooms = :bedrooms', :bedrooms => bedrooms.to_s}
end
#
# ...Snip. There are several more methods like the ones above...
#
def self.filtered_properties(params)
conditions_string = ''
conditions_hash = {} # Stores the conditions for filtering out properties.
condition_errors = [] # Stores errors related to a filter request.
# Process the "price" and "price_direction" params.
filtered = filter_price params[:price_direction], params[:price]
if filtered.blank? # Do nothing.
elsif filtered[:error]
condition_errors << 'Please provide a number for the price. Eg: 1500'
else
conditions_string << filtered[:sql]
conditions_hash[:price] = filtered[:price]
end
# Process the "bedrooms" param.
filtered = filter_bedrooms params[:bedrooms]
if filtered.blank? # Do nothing.
elsif filtered[:error]
condition_errors << 'Please provide a number for the bedrooms. Eg: 2'
else
conditions_string << ' AND ' << filtered[:sql]
conditions_hash[:bedrooms] = filtered[:bedrooms]
end
# Process the "bathrooms" param.
filtered = filter_bathrooms params[:bathrooms]
if filtered.blank? # Do nothing.
puts '> bathrooms is blank'
elsif filtered[:error]
condition_errors << 'Please provide a number for the bathrooms. Eg: 2'
else
conditions_string << ' AND ' << filtered[:sql]
conditions_hash[:bathrooms] = filtered[:bathrooms]
end
# ...snip...
return condition_errors unless condition_errors.empty?
self.find :all, :conditions => [conditions_string, conditions_hash]
end # End #filtered_properties
Refactorings
No refactoring yet !
Adam
October 27, 2008, October 27, 2008 04:06, permalink
I am going to leave the implementation of ItemFilter as an exercise for the reader as it's more involved than I want to take on at this point, and not necessary for the illustration. But I will point you towards ActiveRecord::Validations and ActiveRecord::NamedScope as two modules that you will probably want to utilize in your class.
class PriceFilter < ItemFilter
DIRECTIONS = { 'at most' => '<=', 'at least' => '>=' }
validates_presence_of :price, :direction
validates_format_of :price, :with => /\A[0-9]+\Z/
validates_inclusion_of :direction, :in => DIRECTIONS.keys
def filter
{ :conditions => [ "price #{DIRECTIONS[direction]} ?", price ] }
end
end
class Item < ActiveRecord::Base
def self.filter_properties(params)
params.inject(self) do |klass,(name,options)|
ItemFilter.filter_for(name).new(klass, options).to_scope
end
end
end
Nick
October 30, 2008, October 30, 2008 17:25, permalink
Hey Adam, thanks for the refactoring suggestion. It's a bit over my head at the moment, as I've never played with AR named scopes, and I'm not entirely sure what you meant the ItemFilter class to do.
What I ended up doing was moving #filtered_properties and #filter_* from the Property model into a new model, named PropertyFilter.
PropertyFilter inherits from ActiveRecord::BaseWithoutTable so that I can use AR's convenient validations, and not save data to a database table.
class PropertyFilter < ActiveRecord::BaseWithoutTable
attr_accessible :price_direction, :price # ...many more attributes...
column :price_direction, :string
column :price, :integer
column :bedrooms, :integer
# ...many more column definitions...
valid_price_directions = ['at most', 'at least']
# ...many more valid values defined...
validates_inclusion_of :price_direction,
:in => valid_price_directions,
:allow_blank => true
# ...many more validations...
def bedrooms_constraint
return nil if self.bedrooms.blank?
{:sql => 'bedrooms = :bedrooms', :bedrooms => self.bedrooms.to_s}
end
# ...many more _constraint methods...
def properties
# ...Call each _constraint method, and build the conditions for #find...
return {:properties => Property.find(:all, :bounds => bounds, :conditions => [conditions_sql, condition_values])}
end
end
In my Rails app, the Property class method #filtered_properties accepts an arg containing params from an HTTP request, processes the params, and returns an array of properties matching the params.
I've created separate class methods (#filter_X , where "X" is "price", "bedrooms", etc) for processing each parameter, which #filtered_properties calls. However, #filtered_properties is still 120 lines.
Might you have any suggestions for how to make #filtered_properties and/or #filter_X more DRY and/or shorter?