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 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/)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | <br /> class Search<br /> attr_reader :options</p> <p> def initialize(model, options)<br /> @model = model<br /> @options = options || {}<br /> end</p> <p> def name<br /> options[:name]<br /> end</p> <p> def address<br /> options[:address]<br /> end</p> <p> def event_date_after<br /> date_from_options(:event_date_after)<br /> end</p> <p> def event_date_before<br /> date_from_options(:event_date_before)<br /> end</p> <p> def has_name?<br /> name.present?<br /> end</p> <p> def has_address?<br /> address.present?<br /> end</p> <p> def conditions<br /> conditions = []<br /> parameters = []</p> <p> return nil if options.empty?</p> <p> if has_name?<br /> conditions << "#{@model.table_name}.name LIKE ?"<br /> parameters << "%#{name}%"<br /> end</p> <p> if has_address?<br /> conditions << "#{@model.table_name}.address LIKE ?"<br /> parameters << "%#{address}%"<br /> end</p> <p> if event_date_after<br /> conditions << "#{@model.table_name}.start_at >= ?"<br /> parameters << event_date_after.to_time<br /> end</p> <p> if event_date_before<br /> conditions << "#{@model.table_name}.end_at <= ?"<br /> parameters << event_date_before.to_time.end_of_day<br /> end</p> <p> unless conditions.empty?<br /> [conditions.join(" AND "), *parameters]<br /> else<br /> nil<br /> end<br /> end</p> <p> private</p> <p> def date_from_options(which)<br /> part = Proc.new { |n| options["#{which}(#{n}i)"] }<br /> y, m, d = part[1], part[2], part[3]<br /> y = Date.today.year if y.blank?<br /> Date.new(y.to_i, m.to_i, d.to_i)<br /> rescue ArgumentError => e<br /> return nil<br /> end</p> <p>end<br /> |
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ì:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <br /> class EventsController < ApplicationController</p> <p> def index<br /> @events = []<br /> @search = Search.new(Event, params[:search])<br /> if is_search?<br /> @events = Event.search(@search, :page => params[:page])<br /> else<br /> @events = Event.paginate(:page => params[:page])<br /> end<br /> end</p> <p> private</p> <p> def is_search?<br /> @search.conditions<br /> end</p> <p>end<br /> |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <br /> class Event < ActiveRecord::Base</p> <p> def self.search(search, args = {})<br /> self.build_search_hash search, args<br /> self.paginate(:all, @search_hash)<br /> end</p> <p> private</p> <p> def self.build_search_hash(search, args = {})<br /> @search_hash = {:conditions => search.conditions,<br /> :page => args[:page],<br /> :per_page => args[:per_page],<br /> :order => 'events.created_at'}<br /> end<br /> end<br /> |
Rimane a questo punto da vedere come modellare la form di ricerca nel nostro template erb (utilizzando tra l’altro formtastic>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <br /> <% semantic_form_for :search, @search, :html => { :method => :get } do |form| %><br /> <% form.inputs do %><br /> <% form.inputs do %><br /> <%= form.input :name, :label => t('search_form.name') %><br /> <%= form.input :address, :label => t('search_form.address') %><br /> <%= form.input :event_date_after,<br /> :as => :date,<br /> :label => t('search_form.event_date_after') %><br /> <%= form.input :event_date_before,<br /> :as => :date,<br /> :label => t('search_form.event_date_before') %><br /> <% end %><br /> <% end %></p> <p> <% form.buttons do %><br /> <%= pretty_positive_button t('search') %><br /> <% end %><br /> <% end %><br /> |
Come si puo’ vedere, il method della form di ricerca è GET, dunque appenderà i parametri all’url e invocherà il metodo index dell’ EventsController.
Tags: rails, ruby, ruby on rails, search

















[...] ORIGINAL POST [...]
nitpicking:
not address.nil? and not address.empty?
better to user
address.present? #returns true unless address is nil, “”, [] or {} (and maybe some other classes have custom implementations as well)
address.blank? # opposite of address.present?
lots of lovely goodness like this in activesupport, worth reading the code for it to find loads of little cool bits like this
Thanks Ale.
I’ve updated the post with your suggestions.
Sometimes I’m too Java oriented
I think it would be good to include validations in your search class. For example,
start date and end date cannot be more than 1 month apart, price has to be positive,
need to choose some option, etc.
Would the Search class need some connection to the Event class? Call it EventSearch, or bind in some other way ?
Stephan
Hi Stephan.
Of course you can add all validations you need. I didn’t add them to keep the code readable and simpler.
In my project, the Search class is a bit more complex, because it covers not only the Event model but also other 2 models (Artist and LiveClub).
They have some attributes with the same name (name and address for example) so I’ve used the same Search class to build the where conditions (If I search for Artists, the event_date_after and event_date_before will be false).
Then I’ve extracted the search form as a partial and included it in the right template. In this way I can call the index method of the proper controller.
That’s why I didn’t call this class EventSearch but simply Search.
Finally note that I’ve binded the model class in the constructor.
[...] This post was mentioned on Twitter by Dev Interface, Stefano Mancini. Stefano Mancini said: How to model a custom search form in Rails http://bit.ly/aOgx0P [...]
[...] This post was mentioned on Twitter by Dev Interface, Stefano Mancini. Stefano Mancini said: How to model a custom search form in Rails http://bit.ly/aOgx0P [...]
Awesome, just tried it and it works perfectly =)
No need of searchlogic anymore !
Sorry, but how can I get it work under rails 3?
Im new to Ruby on Rails. SO please help me, I have followed the below forum to make a REST api for my app. “http://digg.com/newsbar/topnews/How_to_create_a_REST_API_for_Ruby_on_Rails_applications”.
But the ‘map.connect_resource :book’(mentioned in the 3rd page of the doc) causes the following error, when executes ‘rake test:functionals.
Error: undefined local variable or method `map’ for #.
In my app, Im trying to implement RoR with mysql DB with the following table data. Table Name: Object Fields: object_id, Object_name, Object_description etc…
I would like to create REST api object for querying the above database and retrieving the data as api object…