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: , , ,


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.

About DevInterface

We are an information and communication technology agency. Our mission is to provide web application development, design services and communication strategies. We specialize in building web applications with modern and efficient frameworks.

Related Post

10 Responses to “Come modellare una form di ricerca usando Rails”

  1. Ale scrive:

    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

  2. Stefano scrive:

    Thanks Ale.

    I’ve updated the post with your suggestions.

    Sometimes I’m too Java oriented :-)

  3. Stephan Wehner scrive:

    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

  4. Stefano scrive:

    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.

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

  6. Steve scrive:

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

  7. Boris Rorsvort scrive:

    Awesome, just tried it and it works perfectly =)
    No need of searchlogic anymore !

  8. Globolus scrive:

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

  9. Ratheesh R scrive:

    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…

Leave a Reply

Insert code beetween <code lang="ruby"> and </code>

Copyright 2012 DevInterface s.n.c.

DevInterface Blog is proudly powered by WordPress