Come modellare una form di ricerca usando Rails

Spesso si ha la necessità di creare una maschera di ricerca per filtrare le righe di una tabella corrispondenti ad uno specifico model.

"SearchLogic":http://github.com/binarylogic/searchlogic puo' essere una valida soluzione ma magari si vuole puntare su una alternativa più personalizzabile.

La soluzione che propongo è di utilizzare una classe Search.rb in grado di collezionare i parametri di ricerca e di creare le "where conditions" da applicare alla nostra find.

Supponiamo di voler filtrare i record di un model Event.rb con i seguenti attributi:

* name, string
* address, string
* start_at, datetime
* end_at, datetime

La maschera di ricerca proporrà dunque due campi di testo per _name_ e _address_ ed eventualmente due datepicker per start_at ed end_at.

Si potrà decidere di mettere le condizioni in AND piuttosto che in OR o definire intervalli di tempo sulla base della valorizzazione di uno o di entrambi i campi data.

Vediamo ora come modellare la nostra classe di supporto Search.rb (da creare ad esempio in app/models/)


class Search
attr_reader :options

def initialize(model, options)
@model = model
@options = options || {}
end

def name
options[:name]
end

def address
options[:address]
end

def event_date_after
date_from_options(:event_date_after)
end

def event_date_before
date_from_options(:event_date_before)
end

def has_name?
name.present?
end

def has_address?
address.present?
end

def conditions
conditions = []
parameters = []

return nil if options.empty?

if has_name?
conditions << "#{@model.table_name}.name LIKE ?"
parameters << "%#{name}%"
end

if has_address?
conditions << "#{@model.table_name}.address LIKE ?"
parameters << "%#{address}%"
end

if event_date_after
conditions << "#{@model.table_name}.start_at >= ?"
parameters << event_date_after.to_time
end

if event_date_before
conditions << "#{@model.table_name}.end_at <= ?"
parameters << event_date_before.to_time.end_of_day
end

unless conditions.empty?
[conditions.join(" AND "), *parameters]
else
nil
end
end

private

def date_from_options(which)
part = Proc.new { |n| options["#{which}(#{n}i)"] }
y, m, d = part[1], part[2], part[3]
y = Date.today.year if y.blank?
Date.new(y.to_i, m.to_i, d.to_i)
rescue ArgumentError => e
return nil
end

end

Strutturiamo ora il controller che si occuperà di applicare i parametri di ricerca agli eventi.

Nello specifico, vorrei fare in modo che la ricerca rispondesse alla action _index_ di _EventsController_.

A questo punto, un'url senza parametri (del tipo http://localhost:3000/events) eseguirà una Event.find(:all); una richiesta con parametri (del tipo http://localhost:3000/events?name=rock+contest) applicherà la ricerca.
Sarà dunque necessario che la nostra maschera di ricerca risponda al metodo GET e non POST. Vedremo poi in dettaglio questo aspetto.

Il codice del controller apparirà così:


class EventsController < ApplicationController

def index
@events = []
@search = Search.new(Event, params[:search])
if is_search?
@events = Event.search(@search, :page => params[:page])
else
@events = Event.paginate(:page => params[:page])
end
end

private

def is_search?
@search.conditions
end

end

Come si può notare, nel caso di ricerca, verrà chiamato il metodo di classe _search_.
Vediamo come è stato definito all'interno del model Event.rb


class Event < ActiveRecord::Base

def self.search(search, args = {})
self.build_search_hash search, args
self.paginate(:all, @search_hash)
end

private

def self.build_search_hash(search, args = {})
@search_hash = {:conditions => search.conditions,
:page => args[:page],
:per_page => args[:per_page],
:order => 'events.created_at'}
end
end

Rimane a questo punto da vedere come modellare la form di ricerca nel nostro template erb (utilizzando tra l'altro "formtastic":http://github.com/justinfrench/formtastic).


<% semantic_form_for :search, @search, :html => { :method => :get } do |form| %>
<% form.inputs do %>
<% form.inputs do %>
<%= form.input :name, :label => t('search_form.name') %>
<%= form.input :address, :label => t('search_form.address') %>
<%= form.input :event_date_after,
:as => :date,
:label => t('search_form.event_date_after') %>
<%= form.input :event_date_before,
:as => :date,
:label => t('search_form.event_date_before') %>
<% end %>
<% end %>

<% form.buttons do %>
<%= pretty_positive_button t('search') %>
<% end %>
<% end %>

Come si puo' vedere, il _method_ della form di ricerca è GET, dunque appenderà i parametri all'url e invocherà il metodo _index_ dell' _EventsController_.