Users Need to be Able to Find Items Easily

One feature of the application that Ken highlights quickly is the time it takes to search through the lists of companies and people in the application. He asks Rory to build in a search function.

Use the Index View as the Core of the Search View

When Rory starts to write a new search view, he soon realizes that he is repeating the same code that he has in his index view; that is a list of records. The difference is that in the index view all records are being shown, whereas in the search view, a subset of all the records is being shown. If he renders the index view, but passes it the result of the search call, the coding will be greatly simplified.

If the index view is rendered when processing the search action, the index view needs to contain the search form; that is the form where the users submit their search criteria. This is often a good thing, rather than being a problem. That is, users are presented with a search option each time they access an index view. They can start querying and sampling the index list without having to move to a separate view. However, to achieve this, the search form needs to be small and simple.

It is a good idea to provide immediate user access to a quick and simple search tool. Often, this is the only search option required. However, if more advanced options are required, we can always add a second advanced search action.

Note

If you would prefer not to have a search box in your list view, an alternative approach would be to extract from the list view, the code that displays the list of contacts, and put that code into a partial. You could then use the same partial for both the search and list views.

Search The First Attempt

Rory's first attempt at a search function is not satisfactory and inherently insecure. However, it makes a good starting point for describing how to add search functionality. it also demonstrate some problems that can occur.

To create a search function, Rory initially adds a search form by altering the start of views/companies/index.rhtml to:

<h1><%= @page_title %></h1>
<p><%= form_tag({:controller => 'companies',
:action => 'search'},
{:method => :get}) %>
<label for="term">Find company with name:</label>
<%= text_field_tag "term" -%>
<%= submit_tag 'Go' %>
</form></p><br />

He then adds the following method to the companies' controller:

def search
name = params[:term]
@paginator, @companies = paginate :company,
:per_page => 10,
:order => 'name',
:conditions => "name = '#{name}'"
@page_title = "List of companies called #{name}"
render :action => 'index'
end

He uses @page_title to create a page title that is specific to the search. This is useful when navigating back and forth through the browser history, as each search will have a different title and therefore will be more easily identified.

When Rory enters a company name into the search textbox and clicks on the Go button, the entry for that company is listed and he can navigate to the show or edit view from there. However, as he starts trying a few options he discovers a problem. He enters a company name that includes an apostrophe and an error is generated. When he investigates this further he finds there is something missing from his current code.

Do Not Trust User Input

The problem with Rory's code is that it relies on the user entering a correctly formatted search term, and not entering potentially damaging code. Consider these two examples:

  • The user enters a name such as "O'Connell and Son". The SQL WHERE clause generated from this entry will be "WHERE name = 'O'Connell and Son' ". This will cause a SQL error, as the apostrophe in the company name will pair with the first apostrophe within the conditions statement. The WHERE clause in the SQL statement will effectively become "WHERE name = 'O' ". The second part "Connell and Son' " will appear to the database parser as an additional code statement and as it does not match any valid SQL statement, an error is generated.
  • The user injects SQL into their query. For example, they could enter a term "Bob'; Delete from users; Select * from companies where name = 'Bob" which would result in a where clause: "WHERE name = 'Bob'; Delete from users; Select * from companies where name = 'Bob' ".
  • This would parse as three SQL commands:
    • The ActiveRecord generated SQL query ending with name = 'Bob'
    • Delete from users
    • SELECT * FROM companies WHERE name = 'Bob'
  • The ActiveRecord SQL query and the final SELECT statement will do no harm, but the DELETE statement will delete all records from the table users.

Note

There is a convention that SQL code is written with all reserved words in upper case, and lower case is used for program specific identifiers. However, as the example of SQL injection demonstrates, code that does not conform to this convention will still be processed and executed. The convention is useful as most developers find that it makes SQL code easier to read. Therefore I would recommend that you use that convention, but you do not have to.

Fortunately there is a Rails construct that will deal with both issues. Rails will run a number of processes on an entry including removing any potentially harmful code, if we pass :conditions an array formatted as follows:

  • Element 1: The string containing the text you want inserted into the SQL WHERE statement, with any variables replaced with a question mark. So "name = #{name}" becomes "name = ?".
  • Element 2: The variable you wish to substitute into Element 1 at the question mark location.

Therefore, to use this technique in Rory's code, he would change the conditions statement. So the controller code became:

def search
name = params[:term]
@paginator, @companies = paginate :company,
:per_page => 10,
:order => 'name',
:conditions => ["name = ?", name]
@page_title = "List of companies called #{name}"
render :action => 'index'
end

Besides dealing with errors in a user's input, this technique also handles different data types. For example, rather than having to reformat dates into the default SQL format (YYYY-MM-DD) before using them in a query, Rails will do that for us. So to restrict the returned records to only those created on a given date, we would use:

:conditions => ["created_on = ?", date]

We can also have a number of variables within each statement. In this case, Element 2 will either need to be replaced with a list of the substitutes, or with an array containing the variables that need to be substituted into the first element. Either way, the set of substitutes must be in the order they are to be substituted into the first element. For example, if we wanted to only return companies created in the last ten days, whose names were "Bloggins", we could use either:

:conditions => ["name = ? and created_at > ?", name, 10.days.ago]

...or create an array:

condition_elements = [name, 10.days.ago]

...and use that in the :conditions statement:

:conditions => ["name = ? and created_at > ?", condition_elements]

Handle Nothing

There are two conditions that need to be handled. There are no companies in the database, and/or no companies are returned by the current search.

Between the end of the search form and the start of the table displaying the list of companies, Rory can add a conditional statement. If there is no data returned from the query, the statement will return a message to the user instead of an empty table.

<% unless @companies.empty? -%>
<h2> No companies match your criteria </h2>
<% else -%>

...and to the end of the code, add an end to close the if statement as follows:

<% end -%>

He could also add code to differentiate between there being nothing to show because a search has failed, or because there is no data in the database.

Users Need to be Able to Search Without Knowing Exactly What They Are Looking for

With Rory's code as it is, the user needs to know the name of the company they are searching for to get a successful result to their search. For example, if a company name was "Biggum", and a user searched for "Bigum" the company would not be located with this search. As it will be common for users not to know the exact name or spelling of a company name, we need to provide them with another way to search. There are two approaches to dealing with this problem:

  • Provide a search tool that is less specific. That is, a tool that will let a user search for part of the company name and return a list of all matches.
  • Provide a list of matching results, as the search criteria is entered, from which the user can select the correct match.

The second approach is a more complicated process. In the first, all that is required is a small change to the way the database is queried. Whereas in the second approach, a system has to be put in place to continually monitor a user's input and update a list showing matching companies as new characters are added to the search string. However, in Rails, the second approach is actually simpler to create, because a number of helpers have been created that automate the process. The automated process relies on AJAX which deserves a section on its own. So rather than introducing it here, I will initially describe the first approach, and then use the second approach as an introduction to using AJAX with Rails.

A Less Specific Search

SQL provides a way of constructing a conditional WHERE statement so that instead of looking for an exact match, the system returns any rows where the field being queried simply contains the string being searched for. This only works with character string fields, but can be a very useful technique to use with text searches.

When we use a Class.find method in Rails, ActiveRecord builds a SQL statement from our code input, and uses that to query the database. As we have already seen, ActiveRecord uses the contents of the conditions element to construct the WHERE clause of the SQL query. So when searching for companies called "Biggum" Rory's code:

:conditions => ["name = ?", name]

...would be translated to:

WHERE name = 'Biggum'

The WHERE clause has the format:

WHERE field_name = search_string

The alternative approach replaces the equals operator (=) with the operator LIKE. If LIKE is used on its own, it simply replaces the equals operator when comparing string character fields. However, when combined with the % operator, it becomes more flexible as the % becomes a wild-card entry. So:

  • LIKE 'be'—matches field entries that consist only of the character string 'be'
  • LIKE 'be%'—matches field entries that start with the character string 'be'
  • LIKE '%be'—matches field entries that end with the character string 'be'
  • LIKE '%be%'—matches field entries that contain the character string 'be'

So if Rory changes his code to use the format:

WHERE field_name LIKE '%search_string%',

...users entering new search stings will only need to know that the company name contains a certain sequence of characters. It then becomes a fairly simple task for most users to find a sequence of characters that will return a search result that contains the company name they are looking for.

To achieve this, Rory could simply replace the conditions entry with:

:conditions => "name LIKE '%#{name}%'"

However, as discussed above, by so doing Rory would expose his application to problems caused by users' incorrectly formatted entries. Instead a better approach is to use:

:conditions => ["name LIKE ?", "%#{name}%"]

Note that the % operators have to be added to the right-hand-side of the statement, otherwise ActiveRecord will insert a set of apostrophes inside the % operators.

As we may want to use LIKE in many places within our application, the best DRY practice is to add a method to prepare text for use in a LIKE statement. Rory adds the following to application.rb:

protected
# Adds apostrophe to text for SQL LIKE statement.
# prep_for_like('search') > '%search%'
# prep_for_like('search', 'start') > 'search%'
# prep_for_like('search', 'match') > 'search'
def prep_for_like(text, placement='contain')
case placement
when 'match'
text
when 'start'
"#{text}%"
when 'end'
"%#{text}"
else
"%#{text}%"
end
end

Rory could then update the conditions statement to:

:conditions => ["name LIKE ?", prep_for_like(name)]

Case Insensitive Searches

With MySQL, the WHERE statements are case insensitive in the default configuration. That is the statement WHERE field = 'HELLO' will find all of these variations: Hello; HELLO; hello; or even heLLo. However, many databases are case sensitive. Even with MySQL, an administrator can turn off case insensitivity.

If you find your database is case sensitive, you will need to adjust your search code so that users do not have to match the case that a string is stored in within the database. The easiest way to do this is to force both the query string and the text being compared, into the same case— either upper or lower. In fact, it is a good practice to use this technique for all search queries in case your application is moved to a database that is case sensitive.

In SQL the function LCASE converts a string to lower case. Therefore, the following variation of the conditions statement forces SQL to do a case insensitive comparison.

:conditions => ["LCASE(name) like LCASE(?)", prep_for_like(name)]
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.18.101