Extending the Search Application

only for RuBoard - do not distribute or recompile

Although res_search.pl serves to provide a Web-based interface into the contents of our residence table so that users can search it for themselves, we can make a number of modifications to produce a more capable application. Some of the possibilities (the ones we ll actually implement) are as follows:

  • The price selection field needs some improvement. It allows you to specify the top end of the price range you re looking for, but not the bottom. This means that the higher a price range you select, the more listings will be found as matches. That s somewhat counterintuitive, because you d expect that as you look for homes that are more and more expensive, you d find fewer and fewer matches. We could interpret the price points in the other direction ( $100,000 and up rather than Up to $100,000 ), but that still has the problem that you can match a lot of listings at prices you re really not interested in. To deal with this, we ll change the price pop-up menu to present prices as ranges that have both a minimum and a maximum. That changes the way in which we generate the menu, and also the way we interpret its value when users submit searches.

  • We ll add an option for sort order to give the user control over that aspect of the result display. The effect of this is that the ORDERBY clause no longer can be hardwired into the script. Instead, it must be determined based on user preference as expressed in the search form.

  • The application currently can search a number of the columns in the residence table, looking for values that are strings (location), numbers (price, bedrooms, bathrooms), enumeration elements (style), or sets (features). It does not allow the description column to be searched. That column may contain important information, so we ll add the ability to search for keywords in descriptions. Along with this, we ll provide an option to specify whether the description value must contain all the words (if multiple words are entered) or just some of them.

  • The built column indicates when homes were built. We ll make this column searchable so that searches can eliminate residences that are older than a certain age. On the face of it, this is just another pop-up menu in the search form, and as such may not seem to serve any addixtional purpose for our narrative. However, we ll construct the list of possible values for this field not by using a lookup table, but by retrieving the built values directly from the residence table. This means the pop-up menu will display only those values actually present in the current listings. It also means the values shown in the menu may vary somewhat, depending on the listings available from day to day.

  • We ll add image-display capability. If pictures of a residence are available, we ll show them so that the user can see what the home looks like. However, adding pictures has the potential for making search result pages extremely image-heavy and very slow to load, so we ll also change the default display format to a summary table that lists the most important characteristics of each matching listing. Each item in the table will include a link to see the complete listing, including pictures. This way the user gets a brief overview of matching residences via the summary table, but has access to more complete information, should that be desired.

  • To allow for the possibility that the user is searching residence listings on behalf of someone else, we ll allow listings to be sent by email. This enables you to do such things as mail listings for homes that might be suitable for a friend who is relocating to your area. If you re a realtor, you could use this feature to notify customers of homes they might like to take a look at.

  • An issue in the original res_search.pl was that for searches that matched many listings, the result page could become quite long. Changing to a summary format alleviates the problem but does not eliminate it. Therefore, we ll split the result display into several pages if the number of hits goes above a certain threshold.

  • The features column currently contains only four items that the user can select: heated garage, deck, pool, and basement. We ll add several more items to this column and assess what effect this has on the way the application handles the change.

In the previous section, discussion of the development of res_search.pl took place on a stage-by-stage basis, describing in turn how to generate the search form, how to interpret submitted searches, and how to present search results. For the revised version, res_search2.pl, the discussion proceeds by describing how to add each additional capability, and its impact on the various stages of the application. Consequently, I generally won t show entire listings for display_form(), perform_search(), and so forth. Instead I ll show just the changes to these functions that pertain to a given capability. For example, we ll begin by examining the changes needed to modify the price popup menu and how this affects form generation and interpretation, and then go on to add the option for controlling sort order. You ll probably find it helpful to have a copy of res_search2.pl on hand to refer to as you progress through this discussion.

Changing the Price Points

res_search.pl enables the user to choose home listings that satisfy a price constraint by presenting a price pop-up menu that contains items such as this:

 Any  Up to $100,000  Up to $150,000  Up to $250,000  Up to $500,000  Up to $1,000,000 

As noted in the introduction to this section, however, those options are fairly non-specific because the price ranges are bounded only on the bottom end. A different approach is to present ranges bounded on each end, so that we have items with both a minimum and a maximum. This changes the items in the pop-up menu as follows:

 Any  Up to $100,000  $100,001 to $150,000  $150,001 to $250,000  $250,001 to $500,000  $500,001 to $1,000,000  $1,000,001 and up 

It s trickier to generate items such as these from the lookup table, but they enable the user to be more specific. This approach also enables the user to select only homes costing more than a million dollars, something that s not possible under the first approach. We can still use the contents of the res_price table to get the necessary information, but our formatting and display requirements are more complex. That means we ll have to forego using get_lookup_values() and process the table values ourselves. The query we ll use looks like this:

 SELECT value, FORMAT(value,0), value+1, FORMAT(value+1,0) FROM res_price 

The first two columns selected by the query represent the maximum value for a given price range (both without and with commas). The next two columns represent the value that s one greater; this indicates the minimum of the next price range. As we process each row returned by the query, we ll remember this minimum value so we can use it with the next row. To represent pop-up item values that go along with the labels, we ll use strings of the form " min, max " to indicate the endpoints of the range. For the first range, the minimum will be 0 (as in Up to $100,000 ). For the last range, max will be missing, which we ll interpret as no limit (as in $1,000,001 and up ). The code in display_form() that queries the res_price table and generates the list of items looks like this:

 # Generate values and labels for the price range popup.  # First, add "Any" as the first item in the list.  @price_val = ( "Any" );  %price_label = ( "Any" => "Any" );  # Run query to get price range information  $sth = $dbh->prepare (             "SELECT value, FORMAT(value,0), value+1, FORMAT(value+1,0)              FROM res_price");  $sth->execute ();  @prev_row = ();  while (my @row = $sth->fetchrow_array ())  {     if (!@prev_row)         # this is the first row (implicit min = 0)      {         $val = "0,$row[0]";          $label = "Up to \$$row[1]";      }      else                    # not first row: min, max are both known      {         $val = "$prev_row[2],$row[0]";          $label = "\$$prev_row[3] to \$$row[1]";      }      push (@price_val, $val);      $price_label{$val} = $label;      @prev_row = @row;  }  $sth->finish ();  if (@prev_row)              # last row (no explicit max)  {     $val = "$prev_row[2],";      $label = "\$$prev_row[3] and up";      push (@price_val, $val);      $price_label{$val} = $label;  } 

Then we generate the menu with a call to popup_menu() that includes the values and labels that we ve created:

 popup_menu (-name => "price",                      -values => \@price_val,                      -labels => \%price_label) 

The HTML produced by popup_menu() looks like this:

 <select name="price">  <option value="Any">Any</option>  <option value="0,100000">Up to $100,000</option>  <option value="100001,150000">$100,001 to $150,000</option>  <option value="150001,250000">$150,001 to $250,000</option>  <option value="250001,500000">$250,001 to $500,000</option>  <option value="500001,750000">$500,001 to $750,000</option>  <option value="750001,1000000">$750,001 to $1,000,000</option>  <option value="1000001,">$1,000,001 and up</option>  </select> 

Now the price ranges are more specific, and the value attribute for each option is in a format that we can use easily to delimit the search when processing the price parameter in perform_search() :

 $val = param ("price");  if (defined ($val) && $val ne "Any")  {     my ($min, $max) = split (/,/, $val);      if ($max ne "")             # min, max both known      {         push (@condition, "(price >= ? AND price <= ?)");          push (@placeholder, $min, $max);      }      else                        # no explicit max      {         push (@condition, "price >= ?");          push (@placeholder, $min);      }  } 
Giving the User Control over Sort Order

The original version of res_search.pl knows only how to present search results in a single sort order. (It sorts using the location column.) Because of that, the ORDER BY clause was written literally into the query. To afford the user more flexibility, we can add another option to the search form in display_form() for controlling sort order:

 Tr (     td ("Sort results by: "),      td (radio_group (-name => "order_by",                  -values => [ "location", "price", "style" ],                  -labels =>                      {                         "location" => "Location",                         "price" => "Price",                         "style" => "Style",                     })) )

This changes the way we construct the ORDER BY clause in perform_search(); we can no longer write it literally, but must construct it based on the order_by parameter:

 $order_by = "location";             # default sort order  $val = param ("order_by");  if (defined ($val))  {     $order_by = "location" if $$val eq "location";      $order_by = "price" if $$val eq "price";      $order_by = "style" if $$val eq "style";  }  $order_by = "ORDER BY $order_by"; 

This gives us an $order_by variable that we can use in the query string when we re ready to issue the query (which we re not, yet.)

The code that generates the order_by field uses radio buttons to present the sort options. Arguably, it would be more consistent to use a pop-up menu, given that pop-up menus and not radio buttons are used elsewhere in the form. One difference is that for those elements, the options come from lookup tables, and therefore we don t necessarily know how many of them there will be. If there can be a lot of options, radio buttons can take up a lot of space, and a pop-up menu may be a better choice. However, that s not the case with the order_by options. We know how many values there are and that it s a small number. Radio buttons are useful under these conditions because they make all the options visible at a glance. With a pop-up menu, you have to click it to see the options that are available.

Searching for Keywords in Text

This section discusses how to look for substrings in the description column of residence records. It s true that the original res_search.pl already performs string searches (such as when it looks for matches on location values), but queries constructed by that script look only for exact matches on the column, and only for a single value. Adding the ability to look for keywords in the description column involves another kind of text-based search that differs from the previous ones in a couple of ways:

  • We ll be looking for substring matches, not exact matches that is, we ll consider a keyword to match a description if the word appears in the description anywhere.

  • We ll allow the user to specify multiple words. This complicates things a little, because there are two ways to consider a record as matching the words. It can match only if the description contains all the words, or if it contains any of them. (Basically, this boils down to whether we connect tests for the words with AND or with OR when constructing the query.) To let the user to have it either way, the form will provide a field that includes Match all words or Match any word options.

Adding the description-related items to the search form is a matter of adding a text-input field for the keywords, and a field with the match type options. For the latter field, I ll use a set of radio buttons, for the same reason I used buttons to present the sort order field (it has a small fixed number of options). The code to generate these fields goes into display_form() as another row of the table() call that writes the form:

 Tr (     td ("Keywords:"),      td (textfield (-name => "description", -size => 40),          br (),          radio_group (-name => "desc_match_type",                          -values => [ "all", "any" ],                          labels => {                             "all" => "Match all words",                              "any" => "Match any word"                          }))  ) 

To process these new fields for query construction in perform_search(), we must determine which words the user wants to find, and how to connect the tests that look for the words. The condition for finding any given word is based on a substring match, so the operator is LIKE rather than =, and we ll add the % SQL pattern-match character to both ends of the word so that it matches no matter where in the description it occurs. Also, if there are multiple words, we must join the conditions together with AND or OR, depending on the type of match to perform. This means we ll end up with conditions that look like this:

 (description LIKE '%word1%' AND .... AND description LIKE '%wordn%')  (description LIKE '%word1%' OR .... OR description LIKE '%wordn%') 

The parentheses around the entire clause are necessary only for the OR form of the condition (to prevent the query from being misinterpreted if there are other conditions), but it s easiest just to add them in either case. The text-matching condition is generated like this:

 $val = WebDB::trim (param ("description"));  if ($val ne "")                     # any keywords to look for?  {     my @word = split (/\s+/, $val);      $val = lc (param ("desc_match_type"));      my $bool_op = "AND";    # determine boolean connective (default = AND)      $bool_op = "AND" if $val eq "all";      $bool_op = "OR" if $val eq "any";      # create one test per word; join tests with $bool_op;      # enclose entire string in parentheses      push (@condition,              "("              . join (" $bool_op ", ("description LIKE ?") x @word)              . ")");      # convert each word xxx to %xxx% before adding to @placeholder      push (@placeholder, map { "%$_%" }} @word);  } 

Most of this code is fairly straightforward, except perhaps the join and map calls.The second argument to join creates one LIKE test for each word. It uses Perl s x string multiplier operator to create an array of description LIKE ? strings, one string for each element in the @word array . The map call modifies the words before they are added to the @placeholder array; it converts them to patterns by adding % to the ends of each word.

Allowing the description column to be searched may create a performance problem because LIKE matches can be slow. The CREATE TABLE statement for the residence table didn t include any index on the description column, but even if we add one, it wouldn t help. We re looking for word matches anywhere in the description, and MySQL can use an index for LIKE comparisons only if the pattern for the word you re looking for is anchored to the beginning of the column value (as in WHERE description LIKE 'word% ').

The effect of keyword searches on performance may not be noticeable if the user happens to enter constraints for any of the other searchable columns. In that case, MySQL optimizes the query to find matches based on the indexed columns, and then uses the matching records to test the condition on the non-indexed description column. To the extent that the matches on the indexed columns produce a small intermediate result set, the substring matches won t slow a query down much. However, if you have lots of records in the residence table and the user searches only for description keywords, searches may become slow, particularly when many keywords are given. If this becomes an issue, you may want to consider other strategies for performing keyword matches. One possibility is to maintain a separate table that contains a row for each word of each description value. This table must be updated whenever a residence table record is created or its description value is modified, but the table can be indexed and searched using exact string matches. Another possibility is to use MySQL s FULLTEXT index type (if you have MySQL 3.23.23 or later). This involves creating an index on the description column:

 ALTER TABLE residence ADD FULLTEXT (description) 

With the index in place, you can replace the LIKE conditions in the query with conditions that look like this:

 MATCH(description) AGAINST('word') 

MATCH()/AGAINST() returns a relevance ranking value, which will be zero if the word isn t present.

Adding Search-by-Age Capability

Next, we ll incorporate a field in the search form that displays a list of ages so that the user can exclude homes older than a given age. This is obviously something that could be done easily by means of a lookup table analogous to the other lookup tables we ve been using. To illustrate an alternative technique, however, we ll generate this form element a little differently: We ll look up all the unique built values directly from the residence table and use them to construct a pop-up menu.

With regular lookup tables that are separate from your base table, the list of values in the table tends to be fairly stable. (It s easy to modify the lookup table by adding or deleting rows, but typically you don t do so very often.) When you derive the values directly from the contents of a base table that may change fairly often, there s a certain instability that you must be prepared to contend with. In the case of the residence table, the particular year-built values depend on the listings available at the time of any given search. It s possible that there will be no values or a single value when the residence table is new; it s also possible that there will be many values when the table becomes large. To accommodate this flux in the number of possible built values, we can write the application to be adaptive. If there are fewer than two values, it won t present any age pop-up menu at all; and if there are a large number, it will present a scrolling list rather than a pop-up menu.

We also need to consider what kind of values to present as labels to the user. It would be easiest to write the code to present built values directly, but those represent absolute years. People seem to prefer thinking about houses in terms of how old they are rather than the particular year they were built. Fortunately, it s easy to compute an age from the year by subtracting the year from the current year:

 SELECT YEAR(CURRENT_DATE)-built FROM residence 

We can avoid having to convert ages back to years when we process searches by selecting both the year and the age. By using ages for the pop-up item labels, we make the field easier for users to understand, and by using the years for the item values, we make it easier for the script to use them in queries. The values and labels can be obtained like this:

 # Generate a popup for "year built", but display values to users  # as age in years.  ($built_val_ref, $built_label_ref) = WebDB::get_lookup_values (                     $dbh,                      "SELECT DISTINCT                          built, CONCAT(YEAR(CURRENT_DATE) - built,' years')                     FROM residence ORDER BY built"); 

The query uses SELECT DISTINCT rather than just SELECT, because we want only unique values; without DISTINCT, we d get back a value for every row in the residence table! Next, we determine how many values we actually have. If there are fewer than two distinct values, there really isn t any choice to make, so we don t want to produce any age field at all. We ll also try to avoid creating a really long pop-up menu by using a scrolling list if there are more than 20 values:

 if (@{$built_val_ref} <= 1)     # don't bother with a by-age element  {                               # if there's no real choice      $by_age_element = "";  }  else                            # generate either a popup or a list  {     unshift (@{$built_val_ref}, "Any");      $built_label_ref->{Any} = "Any";      if (@{$built_val_ref} <= 20)        # use a popup menu      {         $by_age_element = popup_menu (-name => "built",                                      -values => $built_val_ref,                                      -labels => $built_label_ref);      }      else                            # use a scrolling list      {         $by_age_element = scrolling_list (-name => "built",                                      -values => $built_val_ref,                                      -labels => $built_label_ref);      }  } 

After determining the value of $by_age_element, we can refer to it somewhere in the call to table() that generates the search form. The following code places it after the field for architectural style. If $by_age_element is the empty string, nothing gets printed. Otherwise, we generate a row with the proper caption and either a pop-up menu or a scrolling list containing the allowable values:

 Tr (     td ("Architectural style:"),      td (popup_menu (-name => "style",                  -values => \@style))  ),  # generate the row for the by-age element, if there is one  ($by_age_element eq ""      ? ""      : Tr (td ("Oldest age to consider:"), td ($by_age_element))),  ) 

Processing the built element when the form is submitted is pretty much the same as for most of the other pop-up menus. If there s a nonAny value present, we construct the appropriate test and add it to the @condition array:

 $val = param ("built");  if (defined ($val) && $val ne "Any")  {     push (@condition, "built >= ?");        # value is a minimum      push (@placeholder, $val);  } 

There is one more rather important detail that needs to be taken care of to help the application to run better. At the moment, the residence table has no index on the built column; we should create one in order to make the SELECT query that looks up values more efficient. To do this, use the following ALTER TABLE statement:

 ALTER TABLE residence ADD INDEX (built) 

Wait a minute, you say. The lookup tables we ve been using to generate the other pop-up menus don t have any indexes on them. If we index the built column of the residence table because we re going to use it to look up option values, shouldn t we index the value column of the lookup tables that we re already using? Strictly speaking, I suppose we should. However, those tables are so small that the performance difference will be negligible. Nevertheless, if you want to index the lookup tables, here are the index creation statements to do so:

 ALTER TABLE res_location ADD INDEX (value)  ALTER TABLE res_price ADD INDEX (value)  ALTER TABLE res_bedrooms ADD INDEX (value)  ALTER TABLE res_bathrooms ADD INDEX (value) 

Our by-age field is constructed on-the-fly by reading a table for values. That is similar to the way we construct other lookup-based fields, but differs in that we re not getting the values from a table created specifically to hold element options. Instead, the values come from the base table that we re searching (residence). The implementation is similar either way, but there are some implications you should consider before generating form elements from the base table in your own applications:

  • With a lookup table, the set of options is more consistent and stable. By contrast, when you draw the values from the table that you re searching, the values vary according to the table s current contents. If the table is modified often, the values presented in the form may vary often as well. Consider the difference between approaches in another context. If you have an online store and you re presenting a search form that enables visitors to query your product inventory, you may want to list all the categories for items you sell, not just categories for items you have in stock at the moment. The latter might give visitors the impression you don t carry a very wide variety of merchandise if your inventory happens to be low at the moment. On the other hand, if you re using a search form for an application that produces summaries or reports, you may well want to list only values that are present in the base table. Suppose you provide a form in which to enter parameters for sales reports that are generated from a sales table. If the form has a state field so that you can get reports for a particular state, it makes sense to display the names only of those states in which you have customers that is, those states actually represented in the sales table.

  • The base table will almost always be much larger than a lookup table, so the query to pull values from it generally will be more expensive, even with indexing.

  • Using values from the base table is not a good technique for columns that have a large number of distinct values. For example, presenting a field based on the resident table s price column probably would generate a long list of values as the table becomes large. The use of a few discrete price ranges is a better approach in this case.

If you really want to look up values from your base table, but you don t want to run a potentially expensive query each time you generate the search form, consider using the base table to generate a lookup table. For example, you could create a table for built values like this:

 CREATE TABLE res_built  (     value SMALLINT UNSIGNED NOT NULL  ) 

Then you can load the table using the values present in the residence table:

 INSERT INTO res_built (value) SELECT DISTINCT built FROM residence 

This gives you a small table that can be used efficiently to generate a pop-up menu without searching the residence table. There s a downside, however: The res_built table will exactly reflect the built values present in the residence table at the time that you create it, but will go out of date somewhat as you modify records in the residence table. Because of this, you ll probably want to purge and reload the lookup table periodically. The rate of divergence between the two tables depends on the amount of update activity on the residence table. This rate, together with the amount of divergence you want to tolerate, influences the frequency at which you should reload the res_built table. A reasonable strategy is to reload it once a day for a while, and then assess whether that seems to be suitable. If not, modify the reload frequency up or down. (Or, as they say in cookbooks, season to taste. ) To avoid the possibility of having other applications accessing the res_built table while it s being reloaded, lock the tables while you re using them:

 LOCK TABLE res_built WRITE, residence READ  DELETE FROM res_built  INSERT INTO res_built (value) SELECT DISTINCT built FROM residence  UNLOCK TABLES 

Alternatively, if you have a version of MySQL that supports transactions, and your tables are the proper type for transaction processing, you can reload the res_built table as a multiple-statement transaction. (Transactions are discussed in Chapter 10, E-Commerce Applications. )

Adding Pictures, Changing the Display Format, and Emailing Listings

Up to this point, we ve dealt with residence listings entirely as text-based records. However, for items such as we re dealing with here (homes), their suitability to potential buyers depends heavily on appearance, and search results often are more meaningful to users if descriptive information is accompanied by a picture. It can be a very useful thing for your visitors to be able to see what candidate homes look like. To that end, we ll create a new table res_picture for storing pictures. At the same time, we ll change the display format from one that shows each listing in full to one that displays a summary table. The reason for this is that if we show the entire set of full listings, including pictures, the result page likely will become image laden and quite slow to load. The summary table will display several important characteristics of the listings and make each ID value a hyperlink that the user can select to see a complete listing. Another addition we ll make in this section is the ability to send listings by email. If you search the available listings and come across one you think a friend might be interested in, or if you re a realtor that uses the search application to find listings for clients, it can be useful to be able to send a message containing information about the listing.

To accommodate these changes, I ll make a couple of architectural modifications to the res_search2.pl application, compared to res_search.pl :

  • The query execution and result display code will be moved out of perform_search() and into a separate function, display_summary(). This isn t strictly necessary, but it helps keep perform_search() from getting so large that it becomes difficult to discuss.

  • The code to present complete listings will be moved into another script, res_display.pl, which will handle both the text and pictures, if there are any. res_display.pl also handles the task of mailing listings to people. The most likely time you d think about mailing a listing to someone is when you re looking at the complete information for it that is, when you re viewing a page generated by res_display.pl. Therefore, it makes more sense to add email capability to that script rather than to res_search2.pl.

The first thing we need is a table in which to store any pictures that might be available for residences. We could just include a picture column in the residence table if we knew there never would be more than one picture per home. But using a separate table gives us more flexibility, because it allows a residence record to be associated with as many pictures as you want. The res_picture table shown here holds the necessary information:

 CREATE TABLE res_picture  ( res_id INT NOT NULL, # ID of parent record  INDEX (res_id),      picture_id  INT UNSIGNED NOT NULL AUTO_INCREMENT,   # picture ID      PRIMARY KEY (picture_id),      picture     BLOB NOT NULL,                          # image data      caption     VARCHAR(60),                            # optional caption      mime_type   VARCHAR(20) NOT NULL                    # image MIME type  ) 

res_id indicates which residence record a picture is associated with. This column is indexed so that we can find all pictures for any given residence quickly. However, the index on this column is not unique. (A residence might have multiple pictures, all of which should be assigned the same res_id value.) picture_id assigns a unique value to each picture so that we can refer unambiguously to any res_picture record. picture holds the actual image data; caption, if not NULL, provides a label to display with the picture. mime_type enables us to determine the proper Content-Type: header to use when transferring the image over the network to the client.

Where to Get Sample Pictures

The webdb distribution for the book includes some sample images under the search directory, as well as a script you can use to load them into the res_picture table. Check the README file there for instructions. I won t describe the script; it s very similar to the load_image.pl script developed in Chapter 5, Writing Form-Based Applications, but tailored to the structure of the res_picture table.

Generating the Summary Table

When we move the query execution and result display code into a separate function, the last part of perform_search() no longer does these things. Instead, it just passes query information to display_summary() :

 display_summary ($dbh, $where, $order_by, @placeholder); 

display_summary() determines which columns to retrieve, and then issues the query and generates an HTML table to show the results:

 sub display_summary  { my ($dbh, $where, $order_by, @placeholder) = @_;  my $col_list =  # list of columns to retrieve in SELECT statement      "id, location, CONCAT('\$',FORMAT(price,0)) AS print_price,"      . "style, bedrooms, bathrooms";  my @col_name =  # columns to display in the summary table, in the order                  # they should be displayed (id is not listed here, it's                  # handled specially)  (     "location", "print_price", "style", "bedrooms", "bathrooms"  );  my %label =     # labels for each column of summary table  (     "location"      => "Location",      "print_price"   => "Price",      "style"         => "Style",      "bedrooms"      => "Bedrooms",      "bathrooms"     => "Bathrooms",  );  my ($stmt, $sth);  my $count = 0;  my @row;            # summary table row array  my $str;      $stmt = "SELECT $col_list FROM residence $where $order_by LIMIT 100";      $sth = $dbh->prepare ($stmt);      $sth->execute (@placeholder);      # Column display order is determined from @col_name, except that ID is      # special because it becomes the hyperlink for displaying a full listing.      # Construct table headers      $str = th ("Residence ID");      foreach my $col_name (@col_name)      {         $str .= th (escapeHTML ($label{$col_name}));      }      push (@row, Tr ($str));      # Construct each table row      while (my $ref = $sth->fetchrow_hashref ())      {         # ID column is a hyperlink to display the complete listing          $str = td (a ({-href => "res_display.pl?id=$ref->{id}"}, $ref->{id}));          # Other columns are just static text          foreach my $col_name (@col_name)          {             $str .= td (escapeHTML ($ref->{$col_name}));          }          push (@row, Tr ($str));          ++$count;      }      $sth->finish ();      if ($count == 0)      {         print p ("Sorry, no qualifying listings were found.");      }      else      {         print table (@row);      }  } 

One of the columns we re allowing users to sort on is price. This has a certain implication for the way we construct our query. To sort by price, we need a numeric sort. However, we display prices in the result page using values such as $123,456, which MySQL treats as strings due to the dollar sign and commas. These values don t sort in the same order as numbers. (For example, $99,999 sorts lexically after $100,000 even though it s a smaller number.) To handle this case, the script uses the price column for sorting, but retrieves the formatted price under the name print-price for display.

The display itself is presented in tabular form as a row of labels describing what s in each column of the summary, followed by one row of information per listing. display_summary() turns each residence ID into a hyperlink the user can select to see the complete listing and any pictures there might be.

Displaying or Emailing Individual Listings

The summary table produced by res_search2.pl includes links for displaying full listings. These links invoke another script, res_display.pl, that presents the complete text of a listing and causes itself to be reinvoked to display any pictures associated with the listing. res_display.pl also has a third function, which is to enable the user to email listings.

The initial part of res_display.pl is written to handle either an id or picture parameter and to display text or image data accordingly:

 #! /usr/bin/perl -w  # res_display.pl - display a residence listing or picture  # Interprets URLS of the following forms:  # .../res_display.pl?id=n                show listing n  # .../res_display.pl?picture=n           show picture n  use strict;  use lib qw(/usr/local/apache/lib/perl);  use CGI qw(:standard escapeHTML);  use WebDB;  use WebDB::TableInfo;  use Mail::Sendmail;  my $id = WebDB::trim (param ("id"));  my $picture_id = WebDB::trim (param ("picture"));  my $email = WebDB::trim (param ("email"));  if ($id ne "")  {     display_listing ($id, $email);  }  elsif ($picture_id ne "")  {     display_picture ($picture_id);  }  else  {     error ("No recognizable action was requested. ($id)");  }  exit (0); 

The script also checks for an email parameter and passes it along to display_listing() in case the user requested that an email message be sent. (The preamble includes a use Mail::Sendmail statement to give the script access to email support. See Chapter 5, Writing Form-Based Applications, for more information about the Mail::Sendmail module).

display_listing() is invoked when an id parameter is received indicating which listing to show. This function uses the ID to look up the proper residence table record, displays it, and then checks whether there are any pictures of the residence. If so, for each one it displays the caption (if there is one) and generates an <img> tag referring to the image so that the browser will issue an additional request to fetch the image. The src attribute of each <img> tag is written to cause res_display.pl to invoke itself again, this time with a picture parameter indicating which picture to send to the client browser:

 <img alt="[picture]" src="/books/2/307/1/html/2/res_display.pl?picture=n"> 

The display_listing() function displays the text of the listing in much the same way as the original res_search.pl, except that it produces output only for a single record. It also sends the listing by email if there is an email parameter, and presents a short form at the end of the page (which is where you enter an address in the first place). The way this works is that if you fill in the recipient field and submit the email form, res_display.pl sends the message to the recipient and then redisplays the same listing you were looking at before.

 sub display_listing  { my ($id, $email) = @_;  my ($dbh, $sth, $ref);  my @col_name =  # columns to display, in the order they should be displayed  (     "id", "location", "price", "style", "bedrooms", "bathrooms",      "built", "features", "description"  );  my %label =     # labels for each column  (     "id"            => "Residence ID",      "location"      => "Location",      "price"         => "Asking price",      "style"         => "Architectural style",      "bedrooms"      => "Number of bedrooms",      "bathrooms"     => "Number of bathrooms",      "built"         => "Year built",      "features"      => "Additional features",      "description"   => "Other information"  );  my $pictures = 0;      print header (),              start_html (-title => "Residence Listing", -bgcolor => "white");      # Look up the listing      $dbh = WebDB::connect ();      $sth = $dbh->prepare ("SELECT * FROM residence WHERE id = ?");      $sth->execute ($id);      $ref = $sth->fetchrow_hashref ();      $sth->finish ();      if (!defined ($ref))      {         $dbh->disconnect ();          print p ("Sorry, no residence with ID $id was found"),              end_html ();          return;      }      # Send the listing by email if an address is present.      # Then display the listing in the browser window.  email_text()      # and display_text() both use the same set of values and labels,      # though they format the information differently.      email_text ($email, $ref, \@col_name, \%label) if $email ne "";      display_text ($ref, \@col_name, \%label);      # Display captions and <img> links for any pictures associated      # with the listing      $sth = $dbh->prepare (                 "SELECT picture_id, caption FROM res_picture                  WHERE res_picture.res_id = ?");      $sth->execute ($id);      while (my ($picture_id, $caption) = $sth->fetchrow_array ())      {         # don't need escape() -- we know $picture_id is a number          my $url = url () . "?picture=$picture_id";          print p ("Pictures:") if $pictures == 0;          print p (escapeHTML ($caption)) if $caption;          print img ({-src => $url, -alt => "[picture]"});          ++$pictures;      }      $sth->finish ();      $dbh->disconnect ();      print p ("No pictures of this residence are available") if !$pictures;      # Print a little form allowing the user to email the listing      print hr (), start_form (-action => url ()),          p ("Email this listing to a friend!"),          # include id so next invocation knows which record to redisplay          hidden (-name => "id", -value => $id, -override => 1),          p ("Recipient's email address:"),          # clear the field to prevent accidental duplicate mailings          textfield (-name => "email", -size => 60,                      -value => "", -override => 1),          br (),          submit (-name => "choice", -value => "Mail It"),          end_form ();      print end_html ();  } 

The email_text() and display_text() functions used by display_listing() are very similar. They both format the contents of a residence record. display_text() is the simpler of the two functions. It produces an HTML table for display in the browser window:

 sub display_text  { my ($rec_ref, $col_name_ref, $label_ref) = @_;  my @row; # array to hold display table rows      foreach my $col_name (@{$col_name_ref})      {         push (@row,     # add label and column value to table                  Tr (                     td ($label_ref->{$col_name} . ':"),                      td (escapeHTML ($rec_ref->{$col_name}))                  ));      }      print table ({-border => 1}, @row);  } 

email_text() validates the email address first, and then formats the listing as plain text, attempts to send it, and indicates whether or not the attempt was successful. As part of the message, it also includes the URL for viewing the listing on the Web, in case the recipient wants to see it that way:

 sub email_text  { my ($email, $rec_ref, $col_name_ref, $label_ref) = @_;  my %mail = (    # Hash containing mail message information      From    =>  $email,      To      =>  $email,      Subject =>  "Residential Listing Information For You",      Message =>  ""  );      # Perform some validation of the email address      if (!WebDB::looks_like_email ($email))      {         print p (escapeHTML ("$email is an invalid email address."));          return;      }      # Generate the message, then send it      foreach my $col_name (@{$col_name_ref})      {         $mail{Message} .= "$label_ref->{$col_name}: $rec_ref->{$col_name}\n";      }      $mail{Message} .= "\nYou can also view this listing at:\n"                          . url () . "?id=$rec_ref->{id}\n";      if (sendmail (%mail))      {         print p (escapeHTML ("The listing has been mailed to $email"));      }      else      {         print p (escapeHTML ("An error occurred while attempting to\n"                              . "mail the listing to $email"));      };  } 

Each <img> link produced by display_listing() reinvokes res_display.pl with a picture parameter indicating which res_picture table image to display. The dispatch logic sees this parameter and calls display_picture() to take care of yanking the image out of the table and shoving it over the network:

 sub display_picture  { my $picture_id = shift;  my ($dbh, $sth, $mime_type, $data);      $dbh = WebDB::connect ();      ($mime_type, $data) = $dbh->selectrow_array (                     "SELECT mime_type, picture FROM res_picture                      WHERE picture_id = ?",                      undef, $picture_id);      $dbh->disconnect ();      # did we find a record?      error ("Cannot find picture ID $picture_id") unless defined ($mime_type);      print header (-type => $mime_type, -Content_Length => length ($data)),              $data;      $dbh->disconnect ();  } 
Presenting Multiple-Page Search Results

Earlier in this chapter, we prevented searches from returning a huge stack of records to the client using the simple expedient of adding a LIMIT 100 clause to the query. In this section, we ll tackle the problem a different way, by presenting the search result using multiple pages if the number of hits goes above a certain threshold. There are a couple of issues to consider when you want to accommodate the possibility of large result sets by splitting the result display into multiple pages:

  • We need to present just part of the result set on each page, not the entire thing. This involves adding an appropriate LIMIT clause to the record-selection query.

  • We need to make it possible for the user to navigate to pages that display the other parts of the result set. This involves adding a set of links to each result page.

When presenting the first page for a given result set, the search parameters will have come from the form that the user just submitted. The user arrives at subsequent pages by selecting navigation links. These links have the potential to become somewhat complex, because they must include two kinds of information:

  • The search parameters the user submitted that determine the WHERE clause and the ORDER BY clause. These define the basic result set containing the records the user wants to see, and the order in which they should be presented.

  • The parameters that control which records to display on a given page. I ll call these the page control parameters; they include at least the starting position within the result set and the number of records to display on the page. These two values translate directly into the values that go into the LIMIT clause.

In other words, each navigation link must include sufficient information to allow res_search2.pl to re-create the search and to determine which part of the result set to display. The script itself need not care whether the parameters it receives come from a form submitted by the user or as part of the URL. In either case, we can extract the necessary information by calling param(), which works just as well for URL parameters as for forms.

The particular set of page control parameters you use depends on the navigation style you provide in the result set pages. A simple style presents just next page and previous page links:

 [previous] [next] 

Another style presents these links, but also a set of page numbers as links that can be selected to go directly to individual pages of the result:

 [previous] [1] [2] [3] [4] [5] [next] 

If you want to provide pages that contain only next-page and previous-page links, the only thing you need to know is whether there actually are next and previous pages. It s trivial to determine the latter, based on the current starting position within the result set: If the first record you re displaying on a page isn t the first record in the set, then clearly there is at least one previous page. Determining whether there is a next page is more difficult, because you must know whether any records follow the ones you re displaying on the page. You can do this in two ways:

  • When you retrieve the records for each page, use a LIMIT clause that fetches one more record than you actually intend to display. If you want to present 10 records per page, for example, use LIMIT clauses that fetch 11 records. The queries look like this:

     SELECT ... LIMIT 0, 11  SELECT ... LIMIT 10, 11  SELECT ... LIMIT 20, 11  ... 

    For each page, display the first 10 records that you get back from the query; if you also get an 11th record, you can discard it but its presence tells you there is a next page.

  • Alternatively, before displaying any records at all, execute a preliminary query that uses COUNT(*) to determine the size of the result set. Then as you display each page of the result, you can tell that there is a next page if you haven t yet reached the maximum record number.

If you present only the next-page and previous-page links, the required page control parameters include only the starting record number and the number of records per page. If you want to present the more informative navigation display that includes links to each individual page of the result, you must also know the total number of records so that you can figure out how many pages there are. Therefore, you begin by issuing a COUNT(*) query to determine the result set size.

The navigation style that provides only next-page and previous-page links is simpler to implement, but less informative and less helpful to users. For res_search2.pl, we ll implement the style that includes individual page links. To do this, we ll need all three page control parameters, which I ll denote as follows:

  • start_pos, the starting position within the result set that indicates the first record to display on a page.

  • page_size, the number of records to display per page.

  • max_rec, the total size of the result set. Together with page_size, max_rec determines how many pages there are.[2]

    [2] It s not strictly necessary to pass the total result set size from page to page.You could run the COUNT(*) query for each page. However, it s more efficient to run that query once to get the count, and then pass the value in the navigation links.

The first stage in implementing multiple-page output is to identify the search parameters. This is what the perform_search() function does. It was written originally for the purpose of determining the contents of the WHERE and ORDER BY clauses from the contents of the search form. However, this function does its work by calling param() to get the parameters, and param() also works for parameters passed in URLs. That s very convenient, because it means perform_search() needs no modification at all. It can figure out the WHERE and ORDER BY clauses without regard to whether the user invoked the script by submitting a search form or by selecting a navigation link.

After we know the parameters we need to fetch records for display, we must determine which part of the result set to present. This is where the page control parameters come in. They re needed to construct the proper LIMIT clause that allows the appropriate part of the result to be retrieved and presented in the summary table.

For res_search2.pl, all the modifications to enable multiple-page result set display take place in display_summary(). When you reach this point in the chapter, you ll need to modify the version of res_search2.pl provided in the webdb distribution. Take a look at the source for res_search2.pl ; you ll see some comments describing how to disable the single-page version of display_summary() and enable the multiple-page version.

The revised version of the function isn t completely different (the format in which the summary table is displayed doesn t change, for example), but there are several new sections of code. The new display_summary() is shown in its entirety here, followed by explanations of the new parts:

 sub display_summary  { my ($dbh, $where, $order_by, @placeholder) = @_;  my $col_list =  # list of columns to retrieve in SELECT statement      "id, location, price, CONCAT('\$',FORMAT(price,0)) AS print_price,"      . "style, bedrooms, bathrooms";  my @col_name =  # columns to display in the summary table, in the order                  # they should be displayed (id is not listed here, it's                  # handled specially)  (      "location", "print_price", "style", "bedrooms", "bathrooms" ); my %label = # labels for each column of summary table (    "location"      => "Location",    "print_price"   => "Price",    "style"         => "Style",    "bedrooms"      => "Bedrooms",    "bathrooms"     => "Bathrooms", ); my ($start_pos, $page_size, $max_rec); my @nav_link;       # navigation link array my $limit;          # LIMIT clause my @row;            # summary table row array my ($stmt, $sth); my $str;            # Get the page control parameters.  If they're not present, this is    # the first time we're running this search.  In that case, run a query    # to determine the result set size and initialize the page parameters.    $start_pos = param ("start_pos");    $page_size = param ("page_size");    $max_rec = param ("max_rec");    if (!defined (param ("start_pos")))    {        $start_pos = 0;        $page_size = 5;     # change this to change #hits/page        $stmt = "SELECT COUNT(*) FROM residence $where";        $max_rec = $dbh->selectrow_array ($stmt, undef, @placeholder);        # We don't need to do much if there aren't any records...        if ($max_rec == 0)        {            print p ("Sorry, no qualifying listings were found.");            return;        }        # put values into environment so gen_nav_link() can find them        # (except for start_pos, which isn't constant across links)        param (-name => "page_size", -value => $page_size);        param (-name => "max_rec", -value => $max_rec);    }    # $start_pos = number of initial records to skip    # $page_size = number of records to retrieve        $limit = "LIMIT $start_pos, $page_size";        print p ("$max_rec matching listings were found.");        $stmt = "SELECT $col_list FROM residence $where $order_by $limit";     $sth = $dbh->prepare ($stmt);     $sth->execute (@placeholder);     # Column display order is determined from @col_name, except that ID is     # special because it becomes the hyperlink for displaying a full listing.     # Construct table headers     $str = th ("Residence ID");     foreach my $col_name (@col_name)     {        $str .= th (escapeHTML ($label{$col_name}));     }     push (@row, Tr ($str));     # Construct each table row         while (my $ref = $sth->fetchrow_hashref ())     {        # ID column is a hyperlink to display complete listing         $str = td (a ({-href => "res_display.pl?id=$ref->{id}"}, $ref->{id}));         # Other columns are just static text         foreach my $col_name (@col_name)         {            $str .= td (escapeHTML ($ref->{$col_name}));         }         push (@row, Tr ($str));     }     $sth->finish ();     print table (@row);     # Generate and print navigational links (if there actually are multiple     # pages).  Generate a prev-page link, numbered page links, and a     # next-page link.  prev-page/next-page indicators are just static text     # if not applicable.  Numbered link for current page is static, too.     if ($max_rec > $page_size)     {        if ($start_pos == 0)                    # first page: no predecessor         {            push (@nav_link, "previous");         }         else         {            push (@nav_link, gen_nav_link ("previous", $start_pos-$page_size));         }                for (my $i = 0; $i < $max_rec; $i += $page_size)         {             my $page_no = int ($i / $page_size) + 1;              if ($start_pos == $i)               # this is the current page              {                 push (@nav_link, $page_no);              }              else              {                 push (@nav_link, gen_nav_link ($page_no, $i));              }         }         if ($start_pos+$page_size > $max_rec)   # last page: no successor         {            push (@nav_link, "next");         }         else         {            push (@nav_link, gen_nav_link ("next", $start_pos+$page_size));         }         # Put square brackets around each link/label, then print them         @nav_link = map { "[$_]\n" } @nav_link;         print hr (), @nav_link;      }  } 

The multiple-page version of display_summary() first checks the values of the page control parameters that determine which part of the result set to display and how many navigation links we ll need to generate. These values are the starting position, page size, and size of the result set. When we first begin to run the search (that is, when the script is invoked as a result of the user submitting the search form), none of these parameters will have values, so it s necessary to initialize them. On subsequent pages, we ll have received the parameter values via the URL of the navigation link used to invoke the script, so we can just extract them from the script environment. This means we can tell whether or not to initialize the parameters by checking whether they re already available in the environment:

 $start_pos = param ("start_pos");  $page_size = param ("page_size");  $max_rec = param ("max_rec");  if (!defined (param ("start_pos")))  {     $start_pos = 0;      $page_size = 5;     # change this to change #hits/page      $stmt = "SELECT COUNT(*) FROM residence $where";      $max_rec = $dbh->selectrow_array ($stmt, undef, @placeholder);      # We don't need to do much if there aren't any records...      if ($max_rec == 0)      {         print p ("Sorry, no qualifying listings were found.");          return;      }      # put values into environment so gen_nav_link() can find them      # (except for start_pos, which isn't constant across links)      param (-name => "page_size", -value => $page_size);      param (-name => "max_rec", -value => $max_rec);  } 

If it turns out that the parameters must be initialized, we need to find out the size of the result set by running a preliminary query that uses COUNT(*). This query uses the WHERE clause conditions from the search parameters submitted by the user. However, you don t use any LIMIT clause (you want to count all the records), and you don t need an ORDER BY clause, either. (The query returns only one row; there s no need to sort it.) After the query has been run, we know whether the result set will be empty. If so, we can short circuit the entire display operation by informing the user no records were found.

Assuming the result set isn t empty, the starting position is initialized to 0. (In MySQL, rows are numbered from 0 for the LIMIT clause). The page size is somewhat arbitrary; res_search2.pl uses a value of 5. Notice that after initializing the page parameters, we put their values into the script environment. That s because our link generator (discussed shortly) will expect to find them there; in effect, we re using the parameter space as a clipboard.

Now we re ready to fetch some records and display them. The starting position and the page size determine the appropriate LIMIT clause to add to the query that retrieves the records to be displayed on the page. The summary table format is the same as before, although we add one extra bit of information: the total number of records matched by the search parameters. (Hey, we had to determine the size of the result set, anyway. Why not provide that information to the user?)

Following the summary table, we generate and display the navigation links, but only if they re necessary. If the result set size doesn t exceed the number of records per page, we don t need multiple pages, and consequently no links are displayed. Otherwise, we generate a set of numbered page links preceded by a previous-page link and followed by a next-page link. The code generates static text rather than a link for nonexisting pages and for the numbered link that corresponds to the current page, because there s little point in sending the user to a page that doesn t exist or that is already displayed in the browser window. For example, the previous-page indicator is static text if we re currently on the first page, because in that case there is no previous page:

 if ($start_pos == 0)                    # first page: no predecessor  {     push (@nav_link, "previous");  }  else  {     push (@nav_link, gen_nav_link ("previous", $start_pos-$page_size));  } 

Each link is generated by calling gen_nav_link() to create the proper URL for reinvoking res_search2.pl to display a particular part of the result set. gen_nav_link() takes two arguments: the label to use for the link ( next, previous, or a page number), and the starting record number for the page:

 sub gen_nav_link  { my ($label, $start_pos) = @_;  my @param =     # parameters to extract from environment  (     # page control parameters      "max_rec", "page_size",      # search parameters      "location", "price", "style", "bedrooms", "bathrooms", "features",      "description", "desc_match_type", "order_by"  );  my $url;      # tell the script to continue the search and which record to start with      $url = url () . "?choice=search;start_pos=$start_pos";      # add other page control and search parameters      foreach my $name (@param)      {         my @val = param ($name);          # if a parameter has multiple values, add it multiple times          foreach my $val (@val)          {             $url .= ";$name=" . escape ($val);          }      }      return (a ({-href => $url}, escapeHTML ($label)));  } 

gen_nav_link() encodes into the URL a choice value of search to tell the script to keep searching, and adds the page control parameters that indicate which part of the result set to display and the search parameters needed to reconstruct the search. Most of these values are already present in the script s environment, so they are obtained by calling param(). The only value that is not available this way is the starting position, which varies from page to page and must be passed to gen_nav_link() as an argument. If a search parameter has multiple values (as may be the case for the features parameter), the name is added to the URL several times, once for each value.

The amount of new code needed to implement multiple-page result set display really isn t that extensive. (Compare the sizes of the single-page and multiple-page versions of display_summary() in the source for res_search2.pl. ) However, trying to understand how the parts work together to pass information around from page to page can be confusing, so you really should try out the script at this point and see what kind of URLs the script generates for the navigation links.

Extending the features Column to Include More Items

When we created the residence table, the features column included only four items:

 features SET('heated garage','deck', 'pool','basement') NOT NULL 

There are many other kinds of things that could be listed here that people would be interested in knowing about, and SET columns can hold up to 64 members, so let s take advantage of that by modifying the column to contain some more items. We can do that with an ALTER TABLE statement that lists all the old items and adds several new ones:

 ALTER TABLE residence MODIFY      features SET('heated garage','deck','pool','basement','central air',                  'patio','jacuzzi','fireplace','ethernet','wooded lot',                  'lakefront','pond/stream','near school','near park',                  'near bus line','near shopping','central vacuum') NOT NULL 

This statement modifies the structure of the table that we re searching, so it s reasonable to ask what effect the change might have on res_search2.pl and whether we ll need to make any compensatory revisions. As it turns out, you don t have to make any changes:

  • Changing the number of members in the features column has no effect on the capability of display_form() to generate the check boxes corresponding to each feature, because we used WebDB::TableInfo to get the member list, and that module doesn t depend on the specific contents of the column.

  • Similarly, no changes are necessary to the code in perform_search() that generates a bitmask from the selected features to use in the WHERE clause of the selection query, or in display_summary() to encode the features in the URLs for the navigation links. Like display_form(), those functions are written with no reference to any specific knowledge about the features column other than that it s a SET.

Nevertheless, you might want to make a few changes. One modification that is optional but useful concerns the layout of the feature check boxes in the search form. display_form() generates those boxes by calling checkbox_group() and passing it a linebreak parameter to display the boxes as a vertical list. As you add more features, that list can become fairly long. checkbox_group() has the capability to display check boxes in tabular form with several items per row, so you might want to take advantage of that to generate a more compact feature display.

Another display-related change you might want to make is to sort the items alphabetically. The original items weren t alphabetic, and the ALTER TABLE statement just shown doesn t add the new items in any particular order, either. When there are only four items, the order in which they re displayed doesn t matter so much, but with a larger number, the display can be much easier to read if you sort the items. There are a couple of ways to accomplish this. Either redefine the column so that the items are listed in order, or else sort the list of items after you extract them from the column description and before generating the form. I prefer the latter method, because then you don t have to remember to make sure the members are in order when you issue an ALTER TABLE statement.

To implement these changes, sort the list of features members when you extract it from the table description, like this:

 @features = sort ($tbl_info->members ("features")); 

Then generate the check boxes by calling checkbox_ group( ) with no linebreak parameter but with a cols parameter indicating how many columns to use for a tabular check box display:

 Tr (     td ("Required additional features:"),      td (checkbox_group (-name => "features",                      -values => \@features,                      -cols => 4))  ) 

These modifications make the script a bit more adaptive with respect to future changes you might make to the features column. (If you re wondering whether sorting the items for display will affect the script s capability to properly construct the bitmask for the query in perform_search(), it won t. The bitmask is constructed by looking for matches based on feature name rather than on position within the column description.)

Suggestions for Modification

Allow the user to select a set of listings from the summary table by adding a check box to each line in the table and including a button that sends a request to res_display.pl to show the complete information for multiple listings. This can help eliminate some of the back and forth between search result pages and pages that show full listings. (This modification would require changes both to res_search2.pl and to res_display.pl.)

Modifying the price menu in the search form to use ranges rather than maximum price makes result sets more specific, but each item in the menu specifies both end-points of the range. The application would be more flexible if it allowed the user to specify the top and bottom of the range independently. You could do this with two pop-up menus, although then you might also want to add some kind of check that the minimum the user selects actually is lower than the maximum.

Reimplement multiple-page search result display to use a session for storing search parameters rather than passing tons of information in the navigation links. (You ll need to read Chapter 8, Session Management, for the necessary background information on doing this.)

Add a sender field to the email form presented by res_display.pl. That way you can identify yourself when you mail a listing to someone. As that script is written currently, recipients have no idea who sent the information.

only for RuBoard - do not distribute or recompile


MySQL and Perl for the Web
MySQL and Perl for the Web
ISBN: 0735710546
EAN: 2147483647
Year: 2005
Pages: 77
Authors: Paul DuBois

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net