I had worked on a similar problem in one of my previous projects. The requirement was to find a set of volunteers to scribe matching a set of criteria like email, location, stream of study etc. The solution that worked for me is to define fine-grained scopes and writing up my own query builder like this:
class MatchMaker
# Scopes
# Volunteer => [ * - 'q' is mandatory, # - 'q' is optional, ** - 's', 'e' are mandatory ]
# active - activation_state is 'active'
# scribes - type is 'scribe'
# readers - type is 'reader'
# located - located near (Geocoder)
# *by_name - name like 'q'
# *by_email - email like 'q'
# educated - has education and title is not null
# any_stream - has education stream and is not null
# *streams - has education stream in 'q'
# #stream - has education stream like 'q'
# #education - has education and title like 'q'
# *level - education level (title) is 'q'
# *level_lt - education level (title) is < 'q'
# *level_lteq - education level (title) is <= 'q'
# *marks_lt - has education and marks obtained < 'q'
# *marks_lteq - has education and marks obtained <= 'q'
# *marks_gt - has education and marks obtained > 'q'
# *marks_gteq - has education and marks obtained >= 'q'
# *knows - knows language 'q'
# *reads - knows and reads language 'q'
# *writes - knows and writes language 'q'
# *not_engaged_on - doesn't have any volunteering engagements on 'q'
# **not_engaged_between - doesn't have any volunteering engagements betwee 'q' & 'q'
# #skyped - has skype id and is not null
def search(scope, criteria)
scope = scope.constantize.scoped
criteria, singular = singular(criteria)
singular.each do |k|
scope = scope.send(k.to_sym)
end
if criteria.has_key?(:not_engaged_between)
multi = criteria.select { |k, v| k.eql?(:not_engaged_between) }
criteria.delete(:not_engaged_between)
attrs = multi.values.flatten
scope = scope.send(:not_engaged_between, attrs[0], attrs[1])
end
build(criteria).each do |k, v|
scope = scope.send(k.to_sym, v)
end
scope.includes(:account).limit(Configuration.service_requests['limit']).all
end
def build(params)
rejects = ['utf8', 'authenticity_token', 'action']
required = ['by_name', 'by_email', 'by_mobile', 'streams', 'marks_lt', 'marks_lteq', 'marks_gt',
'marks_gteq', 'knows', 'reads', 'writes', 'not_engaged_on', 'located', 'excluding',
'level', 'level_lt', 'level_lteq']
optional = ['stream', 'education']
params.delete_if { |k, v| rejects.include?(k) }
params.delete_if { |k, v| required.include?(k) && v.blank? }
params.each { |k, v| params.delete(k) if optional.include?(k.to_s) && v.blank? }
params
end
def singular(params)
pattrs = params.dup
singular = ['active', 'scribes', 'readers', 'educated', 'any_stream', 'skyped']
original = []
pattrs.each { |k, v| original << k && pattrs.delete(k) if singular.include?(k.to_s) }
[pattrs, original]
end
end
The form would be something like this:
...
<%= f.input :paper ... %>
<%= f.input :writes ... %>
<%= f.input :exam_date ... %>
<%= f.time_select :start_time, { :combined => true, ... } %>
<%= f.time_select :end_time, { :combined => true, ... } %>
<fieldset>
<legend>Education criteria</legend>
<%= f.input :streams, :as => :check_boxes,
:collection => ...,
:input_html => { :title => 'The stream(s) from which the scribe can be taken' } %>
<%= f.input :education, :as => :select,
:collection => ...,
:input_html => { :class => 'input-large', :title => configatron.scribe_request.labels[:education]}, :label => configatron.scribe_request.labels[:education] %>
<%= f.input :marks_lteq, :label => configatron.scribe_request.labels[:marks_lteq],
:wrapper => :append do %>
<%= f.input_field :marks_lteq, :title => "Marks", :class => 'input-mini' %>
<%= content_tag :span, "%", :class => "add-on" ... %>
<% end %>
</fieldset>
...
And finally
# Start building search criteria
criteria = service_request.attributes
...
# do cleanup of criteria
MatchMaker.new.search('<Klass>', criteria)
This has worked for me very well in the past. Hope this would lead you in the right direction in solving the problems you are facing. All the best.