--- loncom/interface/lonsearchcat.pm	2004/05/05 17:29:06	1.223
+++ loncom/interface/lonsearchcat.pm	2004/07/19 16:38:07	1.231
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # Search Catalog
 #
-# $Id: lonsearchcat.pm,v 1.223 2004/05/05 17:29:06 matthew Exp $
+# $Id: lonsearchcat.pm,v 1.231 2004/07/19 16:38:07 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -75,6 +75,8 @@ use Apache::lonhtmlcommon;
 use Apache::lonlocal;
 use LONCAPA::lonmetadata();
 use HTML::Entities();
+use Parse::RecDescent;
+use Apache::lonnavmaps;
 
 ######################################################################
 ######################################################################
@@ -174,6 +176,7 @@ sub handler {
                    '&launch='.$ENV{'form.launch'}.
                    '&mode='.$ENV{'form.mode'},
               text=>"Course and Catalog Search",
+              target=>'_top',
               bug=>'Searching',});
     } else {
         &Apache::lonhtmlcommon::add_breadcrumb
@@ -182,6 +185,7 @@ sub handler {
                    '&launch='.$ENV{'form.launch'}.
                    '&mode='.$ENV{'form.mode'},
               text=>"Catalog Search",
+              target=>'_top',
               bug=>'Searching',});
     }
     #
@@ -335,6 +339,29 @@ END
         &course_search($r);
     } elsif(($ENV{'form.phase'} eq 'basic_search') ||
             ($ENV{'form.phase'} eq 'adv_search')) {
+        #
+        # We are running a search, try to parse it
+        my ($query,$customquery,$customshow,$libraries) = 
+            (undef,undef,undef,undef);
+        my $pretty_string;
+        if ($ENV{'form.phase'} eq 'basic_search') {
+            ($query,$pretty_string,$libraries) = 
+                &parse_basic_search($r,$closebutton,$hidden_fields);
+            return OK if (! defined($query));
+            &make_persistent({ basicexp => $ENV{'form.basicexp'}},
+                             $persistent_db_file);
+        } else {                      # Advanced search
+            ($query,$customquery,$customshow,$libraries,$pretty_string) 
+                = &parse_advanced_search($r,$closebutton,$hidden_fields);
+            return OK if (! defined($query));
+        }
+        &make_persistent({ query => $query,
+                           customquery => $customquery,
+                           customshow => $customshow,
+                           libraries => $libraries,
+                           pretty_string => $pretty_string },
+                         $persistent_db_file);
+        #
         # Set up table
         if (! defined(&create_results_table())) {
 	    my $errorstring=&Apache::lonmysql::get_error();
@@ -362,29 +389,12 @@ Unable to properly store search informat
 END
             return OK;
         }
-        #
-        # We are running a search
-        my ($query,$customquery,$customshow,$libraries) = 
-            (undef,undef,undef,undef);
-        my $pretty_string;
-        if ($ENV{'form.phase'} eq 'basic_search') {
-            ($query,$pretty_string,$libraries) = 
-                &parse_basic_search($r,$closebutton,$hidden_fields);
-        } else {                      # Advanced search
-            ($query,$customquery,$customshow,$libraries,$pretty_string) 
-                = &parse_advanced_search($r,$closebutton,$hidden_fields);
-            return OK if (! defined($query));
-        }
-        &make_persistent({ query => $query,
-                           customquery => $customquery,
-                           customshow => $customshow,
-                           libraries => $libraries,
-                           pretty_string => $pretty_string },
-                         $persistent_db_file);
         ##
         ## Print out the frames interface
         ##
-        &print_frames_interface($r);
+        if (defined($query)) {
+            &print_frames_interface($r);
+        }
     }
     return OK;
 } 
@@ -454,10 +464,11 @@ sub course_search {
         }
     }
     my $fulltext=$ENV{'form.crsfulltext'};
+    my $discuss=$ENV{'form.crsdiscuss'};
     my @allwords=($search_string,@New_Words);
     $totalfound=0;
     $r->print('<html><head><title>LON-CAPA Course Search</title></head>'.
-	      $bodytag.'<hr /><center><font size="+2" face="arial">'.$pretty_search_string.'</font></center><hr />');
+	      $bodytag.'<hr /><center><font size="+2" face="arial">'.$pretty_search_string.'</font></center><hr /><b>'.&mt('Course content').':</b><br />');
     $r->rflush();
 # ======================================================= Go through the course
     undef %alreadyseen;
@@ -475,8 +486,83 @@ sub course_search {
         untie(%hash);
     }
     unless ($totalfound) {
-	$r->print('<p>'.&mt('No resources found').'.</p>');
+	$r->print('<p>'.&mt('No matches found in resources').'.</p>');
+    }
+
+# Check discussions if requested
+    if ($discuss) {
+        my $totaldiscussions = 0;
+        $r->print('<br /><br /><b>'.&mt('Discussion postings').':</b><br />'); 
+        my $navmap = Apache::lonnavmaps::navmap->new();
+        my @allres=$navmap->retrieveResources();
+        my %discussiontime = &Apache::lonnet::dump('discussiontimes',
+                               $ENV{'course.'.$ENV{'request.course.id'}.'.domain'},
+                               $ENV{'course.'.$ENV{'request.course.id'}.'.num'});
+        foreach my $resource (@allres) {
+            my $result = '';
+            my $applies = 0;
+            my $symb = $resource->symb();
+            my $ressymb = $symb;
+            if ($symb =~ m#(___adm/\w+/\w+)/(\d+)/bulletinboard$#) {
+                $ressymb = 'bulletin___'.$2.$1.'/'.$2.'/bulletinboard';
+                unless ($ressymb =~ m#bulletin___\d+___adm/wrapper#) {
+                    $ressymb=~s#(bulletin___\d+___)#$1adm/wrapper/#;
+                }
+            }
+            if (defined($discussiontime{$ressymb})) { 
+                my %contrib = &Apache::lonnet::restore($ressymb,$ENV{'request.course.id'},
+                     $ENV{'course.'.$ENV{'request.course.id'}.'.domain'},
+                     $ENV{'course.'.$ENV{'request.course.id'}.'.num'});
+                if ($contrib{'version'}) {
+                    for (my $id=1;$id<=$contrib{'version'};$id++) {
+                        unless (($contrib{'hidden'}=~/\.$id\./) || ($contrib{'deleted'}=~/\.$id\./)) { 
+                            if ($contrib{$id.':subject'}) {
+                                $result .= $contrib{$id.':subject'};
+                            }
+                            if ($contrib{$id.':message'}) {
+                                $result .= $contrib{$id.':message'};
+                            }
+                            if ($contrib{$id,':attachmenturl'}) {
+                                if ($contrib{$id,':attachmenturl'} =~ m-/([^/]+)$-) {
+                                    $result .= $1;
+                                }
+                            }
+                            $applies = &checkwords($result,$applies,@allwords);
+                        }
+                    }
+                }
+            }
+# Does this discussion apply?
+            if ($applies) {
+                my ($map,$ind,$url)=&Apache::lonnet::decode_symb($ressymb);
+                my $disctype = &mt('resource');
+                if ($url =~ m#/bulletinboard$#) {
+                    if ($url =~m#^adm/wrapper/adm/.*/bulletinboard$#) {
+                        $url =~s#^adm/wrapper##;
+                    }
+                    $disctype = &mt('bulletin board');
+                } else {
+                    $url = '/res/'.$url;
+                }
+                if ($url =~ /\?/) {
+                    $url .= '&symb=';
+                } else {
+                    $url .= '?symb=';
+                }
+                $url .= &Apache::lonnet::escape($resource->symb());
+                my $title = $resource->compTitle();
+                $r->print('<br /><a href="'.$url.'" target="cat">'.
+                     ($title?$title:$url).'</a>&nbsp;&nbsp;-&nbsp;'.$disctype.'<br />');
+                $totaldiscussions++;
+            } else {
+                $r->print(' .');
+            }
+        }
+        unless ($totaldiscussions) {
+            $r->print('<p>'.&mt('No matches found in postings').'.</p>');
+        }
     }
+ 
 # =================================================== Done going through course
     $r->print('</body></html>');
 }
@@ -495,14 +581,8 @@ sub checkonthis {
 	$result.=&Apache::lonnet::ssi_body($url);
     }
     $result=~s/\s+/ /gs;
-    my $applies=0;
-    foreach (@allwords) {
-        if ($_=~/\w/) {
-	   if ($result=~/$_/si) {
-	      $applies++;
-           }
-       }
-    }
+    my $applies = 0;
+    $applies = &checkwords($result,$applies,@allwords);
 # Does this resource apply?
     if ($applies) {
        $r->print('<br />');
@@ -526,6 +606,18 @@ sub checkonthis {
     }
 }
 
+sub checkwords {
+    my ($result,$applies,@allwords) = @_;
+    foreach (@allwords) {
+        if ($_=~/\w/) {
+            if ($result=~/$_/si) {
+                $applies++;
+            }
+        }
+    }
+    return $applies;
+}
+
 sub untiehash {
     if (tied(%hash)) {
         untie(%hash);
@@ -559,9 +651,11 @@ Prints the form for the basic search.  S
 ######################################################################
 sub print_basic_search_form {
     my ($r,$closebutton,$hidden_fields) = @_;
+    my $result = ($ENV{'form.catalogmode'} ne 'groupsearch');
     my $bodytag=&Apache::loncommon::bodytag('Search').
-        &Apache::lonhtmlcommon::breadcrumbs(undef,'Searching','Searching',
-                                            undef,undef,! $ENV{'form.launch'});
+        &Apache::lonhtmlcommon::breadcrumbs(undef,'Searching','Search_Basic',
+                                   undef,undef,
+                                   $ENV{'form.catalogmode'} ne 'groupsearch');
     my $scrout = &search_html_header().$bodytag;
     if (&Apache::lonnet::allowed('bre',$ENV{'request.role.domain'})) {
         # Define interface components
@@ -599,8 +693,11 @@ sub print_basic_search_form {
         }
         $scrout.='<table>'.
             '<tr><td align="center" valign="top">'.
-            &Apache::lonhtmlcommon::textbox('basicexp',
-                                            $ENV{'form.basicexp'},50).'<br />'.
+            &Apache::lonhtmlcommon::textbox
+            ('basicexp',
+             &HTML::Entities::encode($ENV{'form.basicexp'},'<>&"'),50
+             ).
+             '<br />'.
             '<font size="-1">'.&searchhelp().'</font>'.'</td>'.
             '<td><font size="-1">'.
             '<nobr>'.('&nbsp;'x3).$adv_search_link.'</nobr>'.'<br />'.
@@ -609,12 +706,6 @@ sub print_basic_search_form {
             '</font></td>'.
             '</tr>'.$/;
         #
-#        $scrout .= '<tr><td align="center">'.
-#            '<font size="-1">'.
-#            $userelatedwords.('&nbsp;'x3).
-#            $onlysearchdomain.('&nbsp;'x2).$adv_search_link.
-#            '</font>'.
-#            '</td></tr>'.$/;
         $scrout .= '<tr><td align="center" colspan="2">'.
             '<font size="-1">'.
             '<input type="submit" name="basicsubmit" '.
@@ -630,7 +721,8 @@ sub print_basic_search_form {
                                            'header' => 'Course Search',
 	 'note' => 'Enter terms or phrases, then press "Search" below',
 	 'use' => 'use related words',
-	 'full' =>'fulltext search (time consuming)'
+	 'full' =>'fulltext search (time consuming)',
+         'disc' => 'search discussion postings (resources and bulletin boards)',
 					   );
         $scrout.=(<<ENDCOURSESEARCH);
 <form name="loncapa_search" method="post" action="/adm/searchcat">
@@ -655,10 +747,14 @@ ENDCOURSESEARCH
         my $relcheckbox = 
             &Apache::lonhtmlcommon::checkbox('crsrelated',
 				   $ENV{'form.crsrelated'});
+        my $discheckbox = 
+            &Apache::lonhtmlcommon::checkbox('crsdiscuss',
+                                   $ENV{'form.crsrelated'});
         $scrout.=(<<ENDENDCOURSE);
 </td></tr>
 <tr><td>$relcheckbox $lt{'use'}</td><td></td></tr>
 <tr><td>$crscheckbox $lt{'full'}</td><td></td></tr>
+<tr><td>$discheckbox $lt{'disc'}</td><td></td></tr>
 </table><p>
 &nbsp;<input type="submit" name="coursesubmit" value='$lt{'srch'}' />
 </p>
@@ -690,10 +786,9 @@ sub print_advanced_search_form{
     my ($r,$closebutton,$hidden_fields) = @_;
     my $bodytag=&Apache::loncommon::bodytag('Advanced Catalog Search').
         &Apache::lonhtmlcommon::breadcrumbs(undef,'Searching',
-                                            'Searching',
+                                            'Search_Advanced',
                                             undef,undef,
-                                            ! $ENV{'form.launch'});
-
+                                  $ENV{'form.catalogmode'} ne 'groupsearch');
     my %lt=&Apache::lonlocal::texthash('srch' => 'Search',
 				       'reset' => 'Reset',
 				       'help' => 'Help');
@@ -774,7 +869,7 @@ ENDHEADER
     $scrout.='<tr><td align="right" valign="top">'.
 	&titlefield(&mt('Copyright/Distribution')).'</td><td colspan="2">'.
         &Apache::lonmeta::selectbox('copyright',
-                                    '',,
+                                    $ENV{'form.copyright'},
                                     \&Apache::loncommon::copyrightdescription,
                                     ( undef,
                                       &Apache::loncommon::copyrightids)
@@ -782,7 +877,7 @@ ENDHEADER
     $scrout.='<tr><td align="right" valign="top">'.
 	&titlefield(&mt('Language')).'</td><td colspan="2">'.
         &Apache::lonmeta::selectbox('language',
-                                    'notset',,
+                                    $ENV{'form.language'},
                                     \&Apache::loncommon::languagedescription,
                                     ('any',&Apache::loncommon::languageids)
                                     ).'</td></tr>';
@@ -997,7 +1092,7 @@ Outputs: return little blurb on how to e
 ######################################################################
 ######################################################################
 sub searchhelp {
-    return &mt('Enter terms or phrases separated by AND, OR, or NOT');
+    return &mt('Enter words and quoted phrases');
 }
 
 ######################################################################
@@ -1188,13 +1283,13 @@ Parse advanced search form and return th
 sub parse_advanced_search {
     my ($r,$closebutton,$hidden_fields)=@_;
     my @BasicFields = ('title','author','subject','keywords','url','version',
-                       'notes','abstract','extension','owner',
+                       'notes','abstract','extension','owner','authorspace',
 #                       'custommetadata','customshow',
                        'modifyinguser','standards','mime');
     my @StatsFields = &statfields();
     my @EvalFields = &evalfields();
     my $fillflag=0;
-    my $pretty_search_string = "<br />\n";
+    my $pretty_search_string = "";
     # Clean up fields for safety
     for my $field (@BasicFields,
                    'creationdatestart_month','creationdatestart_day',
@@ -1204,7 +1299,6 @@ sub parse_advanced_search {
 		   'lastrevisiondatestart_year','lastrevisiondateend_month',
 		   'lastrevisiondateend_day','lastrevisiondateend_year') {
 	$ENV{'form.'.$field}=~s/[^\w\/\s\(\)\=\-\"\']//g;
-        $ENV{'form.'.$field}=~s/(not\s*$|^\s*(and|or)|)//gi;
     }
     foreach ('mode','form','element') {
 	# is this required?  Hmmm.
@@ -1248,22 +1342,30 @@ sub parse_advanced_search {
     my $font = '<font color="#800000" face="helvetica">';
     # Evaluate logical expression AND/OR/NOT phrase fields.
     foreach my $field (@BasicFields) {
-	if ($ENV{'form.'.$field}) {
-            my $searchphrase = $ENV{'form.'.$field};
-            $pretty_search_string .= $font."$field</font> contains <b>".
-                $searchphrase."</b>";
+	next if (!defined($ENV{'form.'.$field}) || $ENV{'form.'.$field} eq '');
+        my ($error,$SQLQuery) = 
+            &process_phrase_input($ENV{'form.'.$field},
+                                  $ENV{'form.'.$field.'_related'},$field);
+        if (defined($error)) {
+            &output_unparsed_phrase_error($r,$closebutton,'phase=disp_adv',
+                                         $hidden_fields,$field);
+            return;
+        } else {
+            $pretty_search_string .= 
+                $font.$field.'</font>: '.$ENV{'form.'.$field};
             if ($ENV{'form.'.$field.'_related'}) {
-                my @New_Words;
-                ($searchphrase,@New_Words) = &related_version($searchphrase);
-                if (@New_Words) {
-                    $pretty_search_string .= " with related words: ".
-                        "<b>@New_Words</b>.";
+                my @Words = 
+                    &Apache::loncommon::get_related_words
+                    ($ENV{'form.'.$field});
+                if (@Words) {
+                    $pretty_search_string.= ' with related words: '.
+                        join(', ',@Words[0..4]);
                 } else {
-                    $pretty_search_string .= " with no related words.";
+                    $pretty_search_string.= ' with related words.';
                 }
             }
-            $pretty_search_string .= "<br />\n";
-	    push @queries,&build_SQL_query($field,$searchphrase);
+            $pretty_search_string .= '<br />';
+            push (@queries,$SQLQuery);
         }
     }
     #
@@ -1280,7 +1382,8 @@ sub parse_advanced_search {
         }
     }
     if (defined($searchphrase)) {
-        push @queries,&build_SQL_query('mime',$searchphrase);
+        my ($error,$SQLsearch) = &process_phrase_input($searchphrase,0,'mime');
+        push @queries,$SQLsearch;
         $pretty_search_string .=$font.'mime</font> contains <b>'.
             $searchphrase.'</b><br />';
     }
@@ -1312,7 +1415,7 @@ sub parse_advanced_search {
 	push @queries,"(copyright like \"$ENV{'form.copyright'}\")";
         $pretty_search_string.=$font."copyright</font> = ".
             &Apache::loncommon::copyrightdescription($ENV{'form.copyright'}).
-                "<br \>\n";
+                "<br />\n";
     }
     #
     # Statistics
@@ -1400,11 +1503,11 @@ sub parse_advanced_search {
     $pretty_search_string .= $pretty_domains_string."<br />\n";
     #
     if (@queries) {
-	$query="select * from metadata where ".join(" AND ",@queries);
+	$query="SELECT * FROM metadata WHERE (".join(") AND (",@queries).')';
     } elsif ($customquery) {
         $query = '';
     }
-#    &Apache::lonnet::logthis('query = '.$/.$query);
+    # &Apache::lonnet::logthis('query = '.$/.$query);
     return ($query,$customquery,$customshow,$libraries_to_query,
             $pretty_search_string);
 }
@@ -1466,7 +1569,7 @@ sub parse_basic_search {
     #
     # Clean up fields for safety
     for my $field ('basicexp') {
-	$ENV{"form.$field"}=~s/[^\w\s\(\)\-]//g;
+	$ENV{"form.$field"}=~s/[^\w\s\'\"\!\(\)\-]//g;
     }
     foreach ('mode','form','element') {
 	# is this required?  Hmmm.
@@ -1479,39 +1582,220 @@ sub parse_basic_search {
     #
     # Check to see if enough of a query is filled in
     my $search_string = $ENV{'form.basicexp'};
-    $search_string =~ s/(not\s*$|^\s*(and|or)|)//gi;
     if (! &filled($search_string)) {
 	&output_blank_field_error($r,$closebutton,'phase=disp_basic');
 	return OK;
     }
-    my $pretty_search_string = '<b>'.$ENV{'form.basicexp'}.'</b>';
-    if ($ENV{'form.related'}) {
-        my @New_Words;
-        ($search_string,@New_Words) = &related_version($ENV{'form.basicexp'});
-        if (@New_Words) {
-            $pretty_search_string .= " with related words: <b>@New_Words</b>.";
-        } else {
-            $pretty_search_string .= " with no related words.";
-        }
+    my $pretty_search_string=$search_string;
+    my @Queries;
+    my $searchfield = 'concat_ws(" ",'.join(',',
+                                            ('title','author','subject',
+                                             'notes','abstract','keywords')
+                                            ).')';
+    my ($error,$SQLQuery) = &process_phrase_input($search_string,
+                                                    $ENV{'form.related'},
+                                                    $searchfield);
+    if ($error) {
+        &output_unparsed_phrase_error($r,$closebutton,'phase=disp_basic',
+                                      '','basicexp');
+        return;
     }
+    push(@Queries,$SQLQuery);
+    #foreach my $q (@Queries) {
+    #    &Apache::lonnet::logthis('    '.$q);
+    #}
+    my $final_query = 'SELECT * FROM metadata WHERE '.join(" AND ",@Queries);
     #
-    # Build SQL query string based on form page
-    my $query='';
-    my $concatarg=join(',',
-		       ('title', 'author', 'subject', 'notes', 'abstract',
-                        'keywords'));
-    $concatarg='title' if $ENV{'form.titleonly'};
-    $query=&build_SQL_query('concat_ws(" ",'.$concatarg.')',$search_string);
     if (defined($pretty_domains_string) && $pretty_domains_string ne '') {
         $pretty_search_string .= ' '.$pretty_domains_string;
     }
     $pretty_search_string .= "<br />\n";
-    my $final_query = 'SELECT * FROM metadata WHERE '.$query;
+    $pretty_search_string =~ s:^<br /> and ::;
     # &Apache::lonnet::logthis($final_query);
     return ($final_query,$pretty_search_string,
             $libraries_to_query);
 }
 
+
+###############################################################
+###############################################################
+
+my @Phrases;
+
+sub concat {
+    my ($item) = @_;
+    my $results = '';
+    foreach (@$item) {
+        if (ref($_) eq 'ARRAY') {
+            $results .= join(' ',@$_);
+        }
+    }
+    return $results;
+}
+
+sub process_phrase_input {
+    my ($phrase,$related,$field)=@_;
+    #&Apache::lonnet::logthis('phrase = :'.$phrase.':');
+    my $grammar = <<'ENDGRAMMAR';
+    searchphrase:
+        expression /^\Z/ {
+            # &Apache::lonsearchcat::print_item(\@item,0);
+            [@item];
+        }
+    expression:
+        phrase(s)   {
+            [@item];
+        }
+    phrase:
+        orword {
+            [@item];
+        }
+      | andword {
+            [@item];
+        }
+      | minusword {
+            unshift(@::Phrases,$item[1]->[0]);
+            unshift(@::Phrases,$item[1]->[1]);
+            [@item];
+        }
+      | word {
+            unshift(@::Phrases,$item[1]);
+            [@item];
+        } 
+    #
+    orword:
+        word 'OR' phrase {
+            unshift(@::Phrases,'OR');
+            unshift(@::Phrases,$item[1]);
+            [@item];
+        }
+        | word 'or' phrase {
+            unshift(@::Phrases,'OR');
+            unshift(@::Phrases,$item[1]);
+            [@item];
+        }    
+        | minusword 'OR' phrase {
+            unshift(@::Phrases,'OR');
+            unshift(@::Phrases,$item[1]->[0]);
+            unshift(@::Phrases,$item[1]->[1]);
+            [@item];
+        }
+        | minusword 'or' phrase {
+            unshift(@::Phrases,'OR');
+            unshift(@::Phrases,$item[1]->[0]);
+            unshift(@::Phrases,$item[1]->[1]);
+            [@item];
+        }    
+    andword:
+        word phrase {
+            unshift(@::Phrases,'AND');
+            unshift(@::Phrases,$item[1]);
+            [@item];
+        }
+        | minusword phrase {
+            unshift(@::Phrases,'AND');
+            unshift(@::Phrases,$item[1]->[0]);
+            unshift(@::Phrases,$item[1]->[1]);
+            [@item];
+        }
+    #
+    minusword:
+        '-' word {
+            [$item[2],'NOT'];
+        }
+    word:
+        "'" term(s) "'" {
+          &Apache::lonsearchcat::concat(\@item);
+        }
+      | '"' term(s) '"' {
+          &Apache::lonsearchcat::concat(\@item);
+        }
+      | term {
+            $item[1];
+        }
+    term:
+        /[\w\Q:!@#$%^&*()+_=|{}<>,.;\\\/?\E]+/ {
+            $item[1];
+        }
+ENDGRAMMAR
+    #
+    # The end result of parsing the phrase with the grammar is an array
+    # @::Phrases.
+    # $phrase = "gene splicing" or cat -> "gene splicing","OR","cat"
+    # $phrase = "genetic engineering" -dna ->
+    #                      "genetic engineering","AND","NOT","dna"
+    # $phrase = cat or dog -poodle -> "cat","OR","dog","AND","NOT","poodle"
+    undef(@::Phrases);
+    my $p = new Parse::RecDescent($grammar);
+    if (! defined($p->searchphrase($phrase))) {
+        &Apache::lonnet::logthis('lonsearchcat:unable to process:'.$phrase);
+        return 'Unable to process phrase '.$phrase;
+    }
+    #
+    # Go through the phrases and make sense of them.  
+    # Apply modifiers NOT OR and AND to the phrases.
+    my @NewPhrases;
+    while(@::Phrases) {
+        my $phrase = shift(@::Phrases);
+        # &Apache::lonnet::logthis('phrase = '.$phrase);
+        my $phrasedata;
+        if ($phrase =~ /^(NOT|OR|AND)$/) {
+            if ($phrase eq 'OR') {
+                $phrasedata->{'or'}++;
+                if (! @::Phrases) { $phrasedata = undef; last; }
+                $phrase = shift(@::Phrases);
+            } elsif ($phrase eq 'AND') {
+                $phrasedata->{'and'}++;
+                if (! @::Phrases) { $phrasedata = undef; last; }
+                $phrase = shift(@::Phrases);
+            }
+            if ($phrase eq 'NOT') {
+                $phrasedata->{'negate'}++;
+                if (! @::Phrases) { $phrasedata = undef; last; }
+                $phrase = shift(@::Phrases);
+            }
+        }
+        $phrasedata->{'phrase'} = $phrase;
+        if ($related) {
+            my @NewWords;
+            (undef,@NewWords) = &related_version($phrasedata->{'phrase'});
+            $phrasedata->{'related_words'} = \@NewWords;
+        }
+        push(@NewPhrases,$phrasedata);
+    }
+    #
+    # Actually build the sql query from the phrases
+    my $SQLQuery;
+    foreach my $phrase (@NewPhrases) {
+        my $query;
+        if ($phrase->{'negate'}) {
+            $query .= $field.' NOT LIKE "%'.$phrase->{'phrase'}.'%"';
+        } else {
+            $query .= $field.' LIKE "%'.$phrase->{'phrase'}.'%"';
+        }
+        foreach my $related (@{$phrase->{'related_words'}}) {
+            if ($phrase->{'negate'}) {
+                $query .= ' AND '.$field.' NOT LIKE "%'.$related.'%"';
+            } else {
+                $query .= ' OR '.$field.' LIKE "%'.$related.'%"';
+            }
+        }
+        if ($SQLQuery) {
+            if ($phrase->{'or'}) {
+                $SQLQuery .= ' OR ('.$query.')';
+            } else {
+                $SQLQuery .= ' AND ('.$query.')';
+            }
+        } else {
+            $SQLQuery = '('.$query.')';
+        }
+    }
+    #
+    # &Apache::lonnet::logthis("SQLQuery = $SQLQuery");
+    #
+    return undef,$SQLQuery;
+}
+
 ######################################################################
 ######################################################################
 
@@ -1530,45 +1814,15 @@ Note: Using this twice on a string is pr
 ######################################################################
 ######################################################################
 sub related_version {
-    my $search_string = shift;
-    my $result = $search_string;
-    my %New_Words = ();
-    while ($search_string =~ /(\w+)/cg) {
-        my $word = $1;
-        next if (lc($word) =~ /\b(or|and|not)\b/);
-        my @Words = &Apache::loncommon::get_related_words($word);
-        @Words = ($#Words>4? @Words[0..4] : @Words);
-        foreach (@Words) { $New_Words{$_}++;}
-        my $replacement = join " OR ", ($word,@Words);
-        $result =~ s/(\b)$word(\b)/$1($replacement)$2/g;
-    }
-    return $result,sort(keys(%New_Words));
+    my ($word) = @_;
+    return (undef) if (lc($word) =~ /\b(or|and|not)\b/);
+    my @Words = &Apache::loncommon::get_related_words($word);
+    # Only use 4 related words
+    @Words = ($#Words>4? @Words[0..4] : @Words);
+    my $result = join " OR ", ($word,@Words);
+    return $result,sort(@Words);
 }
 
-######################################################################
-######################################################################
-
-=pod 
-
-=item &build_SQL_query() 
-
-Builds a SQL query string from a logical expression with AND/OR keywords
-using Text::Query and &recursive_SQL_query_builder()
-
-=cut
-
-######################################################################
-######################################################################
-sub build_SQL_query {
-    my ($field_name,$logic_statement)=@_;
-    my $q=new Text::Query('abc',
-			  -parse => 'Text::Query::ParseAdvanced',
-			  -build => 'Text::Query::Build');
-    $q->prepare($logic_statement);
-    my $matchexp=${$q}{'matchexp'}; chomp $matchexp;
-    my $sql_query=&recursive_SQL_query_build($field_name,$matchexp);
-    return $sql_query;
-}
 
 ######################################################################
 ######################################################################
@@ -1603,47 +1857,6 @@ sub build_custommetadata_query {
     return $matchexp;
 }
 
-######################################################################
-######################################################################
-
-=pod 
-
-=item &recursive_SQL_query_build() 
-
-Recursively constructs an SQL query.  Takes as input $dkey and $pattern.
-
-=cut
-
-######################################################################
-######################################################################
-sub recursive_SQL_query_build {
-    my ($dkey,$pattern)=@_;
-    my @matches=($pattern=~/(\[[^\]|\[]*\])/g);
-    return $pattern unless @matches;
-    foreach my $match (@matches) {
-        $match=~/\[ (\w+)\s(.*) \]/;
-        my ($key,$value)=($1,$2);
-        my $replacement='';
-        if ($key eq 'literal') {
-            $replacement="($dkey LIKE \"\%$value\%\")";
-        } elsif (lc($key) eq 'not') {
-            $value=~s/LIKE/NOT LIKE/;
-#          $replacement="($dkey not like $value)";
-            $replacement="$value";
-        } elsif ($key eq 'and') {
-            $value=~/(.*[\"|\)]) ([|\(|\^].*)/;
-            $replacement="($1 AND $2)";
-	} elsif ($key eq 'or') {
-            $value=~/(.*[\"|\)]) ([|\(|\^].*)/;
-            $replacement="($1 OR $2)";
-	}
-	substr($pattern,
-               index($pattern,$match),
-               length($match),
-               $replacement);
-    }
-    &recursive_SQL_query_build($dkey,$pattern);
-}
 
 ######################################################################
 ######################################################################
@@ -1836,7 +2049,11 @@ a link to change the search query.
 ######################################################################
 sub print_sort_form {
     my ($r,$pretty_query_string) = @_;
-    my $bodytag=&Apache::loncommon::bodytag(undef,undef,undef,1);
+    my $bodytag=&Apache::loncommon::bodytag(undef,undef,undef,1).
+        &Apache::lonhtmlcommon::breadcrumbs
+        (undef,'Searching','Searching',undef,undef,
+         $ENV{'form.catalogmode'} ne 'groupsearch');
+
     ##
     my %SortableFields=&Apache::lonlocal::texthash( 
          id        => 'Default',
@@ -2102,6 +2319,9 @@ results into MySQL.
 sub run_search {
     my ($r,$query,$customquery,$customshow,$serverlist,$pretty_string) = @_;
     my $bodytag=&Apache::loncommon::bodytag(undef,undef,undef,1);
+    $bodytag.=&Apache::lonhtmlcommon::breadcrumbs
+        (undef,'Searching','Searching',undef,undef,
+         $ENV{'form.catalogmode'} ne 'groupsearch');
     my $connection = $r->connection;
     #
     # Print run_search header
@@ -2113,12 +2333,16 @@ $bodytag
 <form name="statusform" action="" method="post">
 <input type="hidden" name="Queue" value="" />
 END
-    # Check to see if $pretty_string has more than one carriage return.
-    # Assume \n s are following <br /> s and truncate the value.
-    # (there is probably a better way)...
-    my @Lines = split /<br \/>/,$pretty_string;
+    # Remove leading and trailing <br />
+    $pretty_string =~ s:^\s*<br />::i;
+    $pretty_string =~ s:(<br />)*\s*$::im;
+    my @Lines = split("<br />",$pretty_string);
+    # I keep getting blank items at the end of the list, hence the following:
+    while ($Lines[-1] =~ /^\s*$/ && @Lines) {
+        pop(@Lines);
+    }
     if (@Lines > 2) {
-        $pretty_string = join '<br \>',(@Lines[0..2],'....<br />');
+        $pretty_string = join '<br />',(@Lines[0..2],'....<br />');
     }
     $r->print(&mt("Search: [_1]",$pretty_string));
     $r->rflush();
@@ -2211,9 +2435,10 @@ END
         #
         # Loop through the servers we have contacted but do not
         # have results from yet, looking for results.
-        while (my ($server,$status) = each(%Server_status)) {
+        foreach my $server (keys(%Server_status)) {
             last if ($connection->aborted());
             &update_seconds($r);
+            my $status = $Server_status{$server};
             if ($status eq 'con_lost') {
                 delete ($Server_status{$server});
                 next;
@@ -2285,13 +2510,13 @@ END
     # results to get, so let the client know the top frame needs to be
     # loaded from /adm/searchcat
     $r->print("</body></html>");
-    if ($ENV{'form.catalogmode'} ne 'groupsearch') {
+#    if ($ENV{'form.catalogmode'} ne 'groupsearch') {
         $r->print("<script>".
                       "window.location='/adm/searchcat?".
                       "phase=sort&".
                       "persistent_db_id=$ENV{'form.persistent_db_id'}';".
                   "</script>");
-    }
+#    }
     return;
 }
 
@@ -2811,6 +3036,41 @@ ENDFRAMES
 ######################################################################
 ######################################################################
 
+sub has_stat_data {
+    my ($values) = @_;
+    if ( (defined($values->{'count'})      && $values->{'count'}      ne '') ||
+         (defined($values->{'stdno'})      && $values->{'stdno'}      ne '') ||
+         (defined($values->{'disc'})       && $values->{'disc'}       ne '') ||
+         (defined($values->{'avetries'})   && $values->{'avetries'}   ne '') ||
+         (defined($values->{'difficulty'}) && $values->{'difficulty'} ne '')) {
+        return 1;
+    }
+    return 0;
+}
+
+sub statfields {
+    return ('count','stdno','disc','avetries','difficulty');
+}
+
+sub has_eval_data {
+    my ($values) = @_;
+    if ( (defined($values->{'clear'})     && $values->{'clear'}     ne '') ||
+         (defined($values->{'technical'}) && $values->{'technical'} ne '') ||
+         (defined($values->{'correct'})   && $values->{'correct'}   ne '') ||
+         (defined($values->{'helpful'})   && $values->{'helpful'}   ne '') ||
+         (defined($values->{'depth'})     && $values->{'depth'}     ne '')) {
+        return 1;
+    }
+    return 0;
+}
+
+sub evalfields { 
+    return ('clear','technical','correct','helpful','depth');
+}
+
+######################################################################
+######################################################################
+
 =pod 
 
 =item Metadata Viewing Functions
@@ -2858,41 +3118,73 @@ sub detailed_citation_view {
          { name=>'copyrighttag',
            translate => '<b>Copyright/Distribution:</b>&nbsp;[_1]',},
          { name=>'count',
+           format => "%d",
            translate => '<b>Access Count:</b>&nbsp;[_1]',},
          { name=>'stdno',
+           format => "%d",
            translate => '<b>Number of Students:</b>&nbsp;[_1]',},
          { name=>'avetries',
+           format => "%.2f",
            translate => '<b>Average Tries:</b>&nbsp;[_1]',},
          { name=>'disc',
+           format => "%.2f",
            translate => '<b>Degree of Discrimination:</b>&nbsp;[_1]',},
          { name=>'difficulty',
+           format => "%.2f",
            translate => '<b>Degree of Difficulty:</b>&nbsp;[_1]',},
          { name=>'clear',
+           format => "%.2f",
            translate => '<b>Clear:</b>&nbsp;[_1]',},
          { name=>'depth',
+           format => "%.2f",
            translate => '<b>Depth:</b>&nbsp;[_1]',},
          { name=>'helpful',
+           format => "%.2f",
            translate => '<b>Helpful:</b>&nbsp;[_1]',},
          { name=>'correct',
-           translate => '<b>Correcy:</b>&nbsp;[_1]',},
+           format => "%.2f",
+           translate => '<b>Correct:</b>&nbsp;[_1]',},
          { name=>'technical',
+           format => "%.2f",
            translate => '<b>Technical:</b>&nbsp;[_1]',},
+         { name=>'comefrom_list',
+           type => 'list',
+           translate => 'Resources that lead up to this resource in maps',},
+         { name=>'goto_list',
+           type => 'list',
+           translate => 'Resources that follow this resource in maps',},
+         { name=>'sequsage_list',
+           type => 'list',
+           translate => 'Resources using or importing resource',},
          ) {
-        if (exists($values{$field->{'name'}}) &&
-            $values{$field->{'name'}} ne '') {
+        next if (! exists($values{$field->{'name'}}) ||
+                 $values{$field->{'name'}} eq '');
+        if (exists($field->{'type'}) && $field->{'type'} eq 'list') {
+            $result .= '<b>'.&mt($field->{'translate'}).'</b><ul>';
+            foreach my $item (split(',',$values{$field->{'name'}})){
+                $result .= '<li>'.
+                    '<a target="search_preview" '.
+                    'href="/res/'.$item.'">'.$item.'</a></li>';
+            }
+            $result .= '</ul>';
+        } elsif (exists($field->{'format'}) && $field->{'format'} ne ''){
+            $result.= &mt($field->{'translate'},
+                          sprintf($field->{'format'},
+                                  $values{$field->{'name'}}))."<br />\n";
+        } else {
             if ($field->{'special'} eq 'url link') {
                 $result.= 
-                    &mt($field->{'translate'},
-                        '<a href="'.$values{'url'}.'" '.
-                            'target="search_preview">'.
-                        $values{$field->{'name'}}.
-                        '</a>');
+                     &mt($field->{'translate'},
+                         '<a href="'.$values{'url'}.'" '.
+                         'target="search_preview">'.
+                         $values{$field->{'name'}}.
+                         '</a>');
             } else {
                 $result.= &mt($field->{'translate'},
                               $values{$field->{'name'}});
             }
             $result .= "<br />\n";
-            }
+        }
     }
     $result .= "</p>";
     if (exists($values{'extrashow'}) && $values{'extrashow'} ne '') {
@@ -2908,41 +3200,6 @@ sub detailed_citation_view {
 ######################################################################
 ######################################################################
 
-sub has_stat_data {
-    my ($values) = @_;
-    if ( (defined($values->{'count'})      && $values->{'count'}      ne '') ||
-         (defined($values->{'stdno'})      && $values->{'stdno'}      ne '') ||
-         (defined($values->{'disc'})       && $values->{'disc'}       ne '') ||
-         (defined($values->{'avetries'})   && $values->{'avetries'}   ne '') ||
-         (defined($values->{'difficulty'}) && $values->{'difficulty'} ne '')) {
-        return 1;
-    }
-    return 0;
-}
-
-sub statfields {
-    return ('count','stdno','disc','avetries','difficulty');
-}
-
-sub has_eval_data {
-    my ($values) = @_;
-    if ( (defined($values->{'clear'})     && $values->{'clear'}     ne '') ||
-         (defined($values->{'technical'}) && $values->{'technical'} ne '') ||
-         (defined($values->{'correct'})   && $values->{'correct'}   ne '') ||
-         (defined($values->{'helpful'})   && $values->{'helpful'}   ne '') ||
-         (defined($values->{'depth'})     && $values->{'depth'}     ne '')) {
-        return 1;
-    }
-    return 0;
-}
-
-sub evalfields { 
-    return ('clear','technical','correct','helpful','depth');
-}
-
-######################################################################
-######################################################################
-
 =pod 
 
 =item &summary_view() 
@@ -3135,6 +3392,51 @@ sub filled {
 
 =pod 
 
+=item &output_unparsed_phrase_error()
+
+=cut
+
+######################################################################
+######################################################################
+sub output_unparsed_phrase_error {
+    my ($r,$closebutton,$parms,$hidden_fields,$field)=@_;
+    my $errorstring;
+    if ($field eq 'basicexp') {
+        $errorstring = &mt('Unable to understand the search phrase <i>[_1]</i>.  Please modify your search.',$ENV{'form.basicexp'});
+    } else {
+        $errorstring = &mt('Unable to understand the search phrase <b>[_1]</b>:<i>[_2]</i>.',$field,$ENV{'form.'.$field});
+    }
+    my $bodytag = &Apache::loncommon::bodytag('Search');
+    my $heading = &mt('Unparsed Field');
+    my $revise  = &mt('Revise search request');
+    # make query information persistent to allow for subsequent revision
+    $r->print(<<ENDPAGE);
+<html>
+<head>
+<title>The LearningOnline Network with CAPA</title>
+</head>
+$bodytag
+<form method="post" action="/adm/searchcat">
+$hidden_fields
+$closebutton
+<hr />
+<h2>$heading</h2>
+<p>
+$errorstring
+</p>
+<p>
+<a href="/adm/searchcat?$parms&persistent_db_id=$ENV{'form.persistent_db_id'}">$revise</a>
+</p>
+</body>
+</html>
+ENDPAGE
+}
+
+######################################################################
+######################################################################
+
+=pod 
+
 =item &output_blank_field_error()
 
 Output a complete page that indicates the user has not filled in enough
@@ -3152,33 +3454,31 @@ $parms is extra information to include i
 ######################################################################
 sub output_blank_field_error {
     my ($r,$closebutton,$parms,$hidden_fields)=@_;
-    my $bodytag=&Apache::loncommon::bodytag(undef,undef,undef,1);
-    # make query information persistent to allow for subsequent revision
-    $r->print(<<BEGINNING);
+    my $bodytag=&Apache::loncommon::bodytag('Search');
+    my $errormsg = &mt('You did not fill in enough information for the search to be started.  You need to fill in relevant fields on the search page in order for a query to be processed.');
+    my $revise = &mt('Revise Search Request');
+    my $heading = &mt('Unactionable Search Queary');
+    $r->print(<<ENDPAGE);
 <html>
 <head>
 <title>The LearningOnline Network with CAPA</title>
-BEGINNING
-    $r->print(<<RESULTS);
 </head>
 $bodytag
-<img align='right' src='/adm/lonIcons/lonlogos.gif' />
-<h1>Search Catalog</h1>
 <form method="post" action="/adm/searchcat">
 $hidden_fields
-<a href="/adm/searchcat?$parms&persistent_db_id=$ENV{'form.persistent_db_id'}"
->Revise search request</a>&nbsp;
 $closebutton
 <hr />
-<h3>Unactionable search query.</h3>
+<h2>$heading</h2>
 <p>
-You did not fill in enough information for the search to be started.
-You need to fill in relevant fields on the search page in order 
-for a query to be processed.
+$errormsg
+</p>
+<p>
+<a href="/adm/searchcat?$parms&persistent_db_id=$ENV{'form.persistent_db_id'}">$revise</a>&nbsp;
 </p>
 </body>
 </html>
-RESULTS
+ENDPAGE
+    return;
 }
 
 ######################################################################