Come modellare una form di ricerca usando Rails

By | 24 maggio 2010

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.

Category: Svliluppo Ruby Tag:, , ,

About Stefano

Stefano Mancini is a co-founder of DevInterface.

After graduating in Computer Science, he first specialized in Java/J2EE development by participating in several international projects in the pharmaceutical and banking ambits.

Enthusiast of agile development, like SCRUM for project management and eXtreme Programming for code writing, he then moved to dynamic languages like Ruby and Python.

10 thoughts on “Come modellare una form di ricerca usando Rails

  1. Pingback: How to model a custom search form in Rails « XP on Rails

  2. Ale

    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

  3. Stefano Post author

    Thanks Ale.

    I’ve updated the post with your suggestions.

    Sometimes I’m too Java oriented :-)

  4. Stephan Wehner

    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

  5. Stefano Post author

    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.

  6. Pingback: Tweets that mention How to model a custom search form in Rails « DevInterface Blog -- Topsy.com

  7. Steve

    [...] 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 [...]

  8. Globolus

    Sorry, but how can I get it work under rails 3?

  9. Ratheesh R

    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…

Comments are closed.