--- loncom/interface/lonsearchcat.pm 2004/04/27 15:24:09 1.217 +++ loncom/interface/lonsearchcat.pm 2005/01/19 20:22:36 1.230.2.1 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Search Catalog # -# $Id: lonsearchcat.pm,v 1.217 2004/04/27 15:24:09 matthew Exp $ +# $Id: lonsearchcat.pm,v 1.230.2.1 2005/01/19 20:22:36 albertel Exp $ # # Copyright Michigan State University Board of Trustees # @@ -75,6 +75,7 @@ use Apache::lonhtmlcommon; use Apache::lonlocal; use LONCAPA::lonmetadata(); use HTML::Entities(); +use Parse::RecDescent; ###################################################################### ###################################################################### @@ -117,7 +118,7 @@ sub handler { my $loaderror=&Apache::lonnet::overloaderror($r); if ($loaderror) { return $loaderror; } - + # my $closebutton; # button that closes the search window # This button is different for the RAT compared to # normal invocation. @@ -174,6 +175,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,14 +184,17 @@ sub handler { '&launch='.$ENV{'form.launch'}. '&mode='.$ENV{'form.mode'}, text=>"Catalog Search", + target=>'_top', bug=>'Searching',}); } # - if (! &get_persistent_form_data($persistent_db_file)) { - if ($ENV{'form.phase'} =~ /(run_search|results)/) { - &Apache::lonnet::logthis("lonsearchcat:Unable to recover data ". - "from $persistent_db_file"); - $r->print(<print(< LON-CAPA Search Error $bodytag @@ -198,8 +203,11 @@ error and has been logged. Please alert END - return OK; + return OK; + } } + } else { + &clean_up_environment(); } ## ## Clear out old values from groupsearch database @@ -226,24 +234,19 @@ END $hidden_fields = ''."\n"; if (exists($ENV{'form.catalogmode'})) { - $hidden_fields .= ''."\n"; + $hidden_fields .= &hidden_field('catalogmode'); } if (exists($ENV{'form.form'})) { - $hidden_fields .= ''."\n"; + $hidden_fields .= &hidden_field('form'); } if (exists($ENV{'form.element'})) { - $hidden_fields .= ''."\n"; + $hidden_fields .= &hidden_field('element'); } if (exists($ENV{'form.titleelement'})) { - $hidden_fields .= ''."\n"; + $hidden_fields .= &hidden_field('titleelement'); } if (exists($ENV{'form.mode'})) { - $hidden_fields .= ''."\n"; + $hidden_fields .= &hidden_field('mode'); } ## ## Configure dynamic components of interface @@ -286,7 +289,7 @@ END $ENV{'form.phase'} = 'disp_basic' if (! exists($ENV{'form.phase'})); $ENV{'form.show'} = 20 if (! exists($ENV{'form.show'})); # - $ENV{'form.searchmode'} = 'basic'; + $ENV{'form.searchmode'} = 'basic' if (! exists($ENV{'form.searchmode'})); if ($ENV{'form.phase'} eq 'adv_search' || $ENV{'form.phase'} eq 'disp_adv') { $ENV{'form.searchmode'} = 'advanced'; @@ -335,6 +338,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,33 +388,52 @@ 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; } +# +# The mechanism used to store values away and retrieve them does not +# handle the case of missing environment variables being significant. +# +# This routine sets non existant checkbox form elements to ''. +# +sub clean_up_environment { + if ($ENV{'form.phase'} eq 'basic_search') { + if (! exists($ENV{'form.related'})) { + $ENV{'form.related'} = ''; + } + if (! exists($ENV{'form.domains'})) { + $ENV{'form.domains'} = ''; + } + } elsif ($ENV{'form.phase'} eq 'adv_search') { + foreach my $field ('title','keywords','notes', + 'abstract','standards','mime') { + if (! exists($ENV{'form.'.$field.'_related'})) { + $ENV{'form.'.$field.'_related'} = ''; + } + } + } elsif ($ENV{'form.phase'} eq 'course_search') { + if (! exists($ENV{'form.crsrelated'})) { + $ENV{'form.crsrelated'} = ''; + } + } +} + +sub hidden_field { + my ($name,$value) = @_; + if (! defined($value)) { + $value = $ENV{'form.'.$name}; + } + return ''.$/; +} + ###################################################################### ###################################################################### ## @@ -399,10 +444,18 @@ END { # Scope the course search to avoid global variables # # Variables For course search -my %alreadyseen; my %hash; my $totalfound; +sub make_symb { + my ($id)=@_; + my ($mapid,$resid)=split(/\./,$id); + my $map=$hash{'map_id_'.$mapid}; + my $res=$hash{'src_'.$id}; + my $symb=&Apache::lonnet::encode_symb($map,$resid,$res); + return $symb; +} + sub course_search { my $r=shift; my $bodytag=&Apache::loncommon::bodytag('Course Search'); @@ -424,16 +477,18 @@ sub course_search { $bodytag.'
'.$pretty_search_string.'

'); $r->rflush(); # ======================================================= Go through the course - undef %alreadyseen; - %alreadyseen=(); my $c=$r->connection; if (tie(%hash,'GDBM_File',$ENV{'request.course.fn'}.".db", &GDBM_READER(),0640)) { - foreach (keys %hash) { + foreach (sort(keys(%hash))) { if ($c->aborted()) { last; } - if (($_=~/^src\_(.+)$/) && (!$alreadyseen{$hash{$_}})) { - &checkonthis($r,$hash{$_},0,$hash{'title_'.$1},$fulltext, - @allwords); + if (($_=~/^src\_(.+)$/)) { + if ($hash{'randomout_'.$1} & !$ENV{'request.role.adv'}) { + next; + } + my $symb=&make_symb($1); + &checkonthis($r,$1,$hash{$_},0,&Apache::lonnet::gettitle($symb), + $fulltext,$symb,@allwords); } } untie(%hash); @@ -448,15 +503,20 @@ sub course_search { # =============================== This pulls up a resource and its dependencies sub checkonthis { - my ($r,$url,$level,$title,$fulltext,@allwords)=@_; - $alreadyseen{$url}=1; + my ($r,$id,$url,$level,$title,$fulltext,$symb,@allwords)=@_; $r->rflush(); - my $result=&Apache::lonnet::metadata($url,'title').' '. - &Apache::lonnet::metadata($url,'subject').' '. - &Apache::lonnet::metadata($url,'abstract').' '. - &Apache::lonnet::metadata($url,'keywords'); - if (($url) && ($fulltext)) { - $result.=&Apache::lonnet::ssi_body($url); + + my $result=$title.' '; + if ($ENV{'request.role.adv'} || !$hash{'encrypted_'.$id}) { + $result.=&Apache::lonnet::metadata($url,'title').' '. + &Apache::lonnet::metadata($url,'subject').' '. + &Apache::lonnet::metadata($url,'abstract').' '. + &Apache::lonnet::metadata($url,'keywords'); + } + my ($extension)=($url=~/\.(\w+)$/); + if (&Apache::loncommon::fileembstyle($extension) eq 'ssi' && + ($url) && ($fulltext)) { + $result.=&Apache::lonnet::ssi_body($url.'?symb='.&Apache::lonnet::escape($symb)); } $result=~s/\s+/ /gs; my $applies=0; @@ -473,8 +533,15 @@ sub checkonthis { for (my $i=0;$i<=$level*5;$i++) { $r->print(' '); } - $r->print(''. - ($title?$title:$url).'
'); + my $href=$url; + if ($hash{'encrypted_'.$id} && !$ENV{'request.role.adv'}) { + $href=&Apache::lonenc::encrypted($href) + .'?symb='.&Apache::lonenc::encrypted($symb); + } else { + $href.='?symb='.&Apache::lonnet::escape($symb); + } + $r->print(''.($title?$title:$url). + '
'); $totalfound++; } elsif ($fulltext) { $r->print(' .'); @@ -484,8 +551,8 @@ sub checkonthis { my $dependencies= &Apache::lonnet::metadata($url,'dependencies'); foreach (split(/\,/,$dependencies)) { - if (($_=~/^\/res\//) && (!$alreadyseen{$_})) { - &checkonthis($r,$_,$level+1,'',$fulltext,@allwords); + if (($_=~/^\/res\//)) { + &checkonthis($r,$id,$_,$level+1,'',$fulltext,undef,@allwords); } } } @@ -523,21 +590,26 @@ 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 my $userelatedwords= &mt('[_1] use related words', &Apache::lonhtmlcommon::checkbox - ('related',$ENV{'form.related'})); + ('related',$ENV{'form.related'},'related')); my $onlysearchdomain= &mt('[_1] only search domain [_2]', - &Apache::lonhtmlcommon::checkbox - ('domains',$ENV{'form.domains'}), - $r->dir_config('lonDefDomain')); + &Apache::lonhtmlcommon::checkbox('domains', + $ENV{'form.domains'}, + $r->dir_config('lonDefDomain') + ), + $r->dir_config('lonDefDomain') + ); my $adv_search_link = ''. - &Apache::lonhtmlcommon::textbox('basicexp', - $ENV{'form.basicexp'},50).'
'. + &Apache::lonhtmlcommon::textbox + ('basicexp', + &HTML::Entities::encode($ENV{'form.basicexp'},'<>&"'),50 + ). + '
'. ''.&searchhelp().''.''. ''. ''.(' 'x3).$adv_search_link.''.'
'. @@ -570,17 +645,12 @@ sub print_basic_search_form { '
'. ''.$/; # -# $scrout .= ''. -# ''. -# $userelatedwords.(' 'x3). -# $onlysearchdomain.(' 'x2).$adv_search_link. -# ''. -# ''.$/; $scrout .= ''. ''. ''. - (' 'x2).$closebutton.(' 'x2).&viewoptions(). + (' 'x2).$closebutton.(' 'x2). + &viewoptions(). ''. ''.$/; $scrout .= ''.$/.''.''; @@ -650,10 +720,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'); @@ -734,7 +803,7 @@ ENDHEADER $scrout.=''. &titlefield(&mt('Copyright/Distribution')).''. &Apache::lonmeta::selectbox('copyright', - '',, + $ENV{'form.copyright'}, \&Apache::loncommon::copyrightdescription, ( undef, &Apache::loncommon::copyrightids) @@ -742,7 +811,7 @@ ENDHEADER $scrout.=''. &titlefield(&mt('Language')).''. &Apache::lonmeta::selectbox('language', - 'notset',, + $ENV{'form.language'}, \&Apache::loncommon::languagedescription, ('any',&Apache::loncommon::languageids) ).''; @@ -754,10 +823,13 @@ ENDHEADER $scrout .= ' '.&mt('Minimum').''. ''.&mt('Maximum').''."\n"; foreach my $statistic - ({ name=>'stdno', - description=>'Number of Students',}, + ({ name=>'count', + description=>'Network-wide number of accesses (hits)',}, + { name=>'stdno', + description=> + 'Total number of students who have worked on this problem',}, { name => 'avetries', - description=>'Average tries to answer correctly',}, + description=>'Average number of tries till solved',}, { name => 'difficulty', description=>'Degree of difficulty',}, { name => 'disc', @@ -920,7 +992,7 @@ Outputs: text for box with view options ###################################################################### ###################################################################### sub viewoptions { - my $scrout="\n".''; + my $scrout; if (! defined($ENV{'form.viewselect'})) { $ENV{'form.viewselect'}='detailed'; } @@ -954,7 +1026,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'); } ###################################################################### @@ -1145,14 +1217,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 = ('stdno','avetries','difficulty','disc'); - my @EvalFields = ('clear','depth','helpful','correct','technical'); - my @ContextFields = ('courses','dependencies'); + my @StatsFields = &statfields(); + my @EvalFields = &evalfields(); my $fillflag=0; - my $pretty_search_string = "
\n"; + my $pretty_search_string = ""; # Clean up fields for safety for my $field (@BasicFields, 'creationdatestart_month','creationdatestart_day', @@ -1174,11 +1245,20 @@ sub parse_advanced_search { ref($ENV{'form.category'})); # # Check to see if enough information was filled in - for my $field (@BasicFields) { + foreach my $field (@BasicFields) { if (&filled($ENV{'form.'.$field})) { $fillflag++; } } + foreach my $field (@StatsFields,@EvalFields) { + if (&filled($ENV{'form.'.$field.'_max'})) { + $fillflag++; + } + if (&filled($ENV{'form.'.$field.'_min'})) { + $fillflag++; + } + } + for my $field ('lowestgradelevel','highestgradelevel') { if ( $ENV{'form.'.$field} =~ /^\d+$/ && $ENV{'form.'.$field} > 0) { @@ -1196,22 +1276,30 @@ sub parse_advanced_search { my $font = ''; # 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 contains ". - $searchphrase.""; + 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.': '.$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: ". - "@New_Words."; + 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 .= "
\n"; - push @queries,&build_SQL_query($field,$searchphrase); + $pretty_search_string .= '
'; + push (@queries,$SQLQuery); } } # @@ -1228,7 +1316,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 contains '. $searchphrase.'
'; } @@ -1260,7 +1349,7 @@ sub parse_advanced_search { push @queries,"(copyright like \"$ENV{'form.copyright'}\")"; $pretty_search_string.=$font."copyright = ". &Apache::loncommon::copyrightdescription($ENV{'form.copyright'}). - "
\n"; + "
\n"; } # # Statistics @@ -1348,11 +1437,11 @@ sub parse_advanced_search { $pretty_search_string .= $pretty_domains_string."
\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); } @@ -1361,7 +1450,7 @@ sub parse_domain_restrictions { my $libraries_to_query = undef; # $ENV{'form.domains'} can be either a scalar or an array reference. # We need an array. - if (! exists($ENV{'form.domains'})) { + if (! exists($ENV{'form.domains'}) || $ENV{'form.domains'} eq '') { return (undef,''); } my @allowed_domains; @@ -1414,7 +1503,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. @@ -1426,39 +1515,221 @@ sub parse_basic_search { &parse_domain_restrictions(); # # Check to see if enough of a query is filled in - unless (&filled($ENV{'form.basicexp'})) { + my $search_string = $ENV{'form.basicexp'}; + if (! &filled($search_string)) { &output_blank_field_error($r,$closebutton,'phase=disp_basic'); return OK; } - my $pretty_search_string = ''.$ENV{'form.basicexp'}.''; - my $search_string = $ENV{'form.basicexp'}; - 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: @New_Words."; - } 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 .= "
\n"; - my $final_query = 'SELECT * FROM metadata WHERE '.$query; + $pretty_search_string =~ s:^
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; +} + ###################################################################### ###################################################################### @@ -1477,45 +1748,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; -} ###################################################################### ###################################################################### @@ -1550,47 +1791,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); -} ###################################################################### ###################################################################### @@ -1783,7 +1983,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', @@ -2049,6 +2253,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 @@ -2060,12 +2267,16 @@ $bodytag
END - # Check to see if $pretty_string has more than one carriage return. - # Assume \n s are following
s and truncate the value. - # (there is probably a better way)... - my @Lines = split /
/,$pretty_string; + # Remove leading and trailing
+ $pretty_string =~ s:^\s*
::i; + $pretty_string =~ s:(
)*\s*$::im; + my @Lines = split("
",$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 '
',(@Lines[0..2],'....
'); + $pretty_string = join '
',(@Lines[0..2],'....
'); } $r->print(&mt("Search: [_1]",$pretty_string)); $r->rflush(); @@ -2158,9 +2369,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; @@ -2232,13 +2444,13 @@ END # results to get, so let the client know the top frame needs to be # loaded from /adm/searchcat $r->print(""); - if ($ENV{'form.catalogmode'} ne 'groupsearch') { +# if ($ENV{'form.catalogmode'} ne 'groupsearch') { $r->print(""); - } +# } return; } @@ -2259,32 +2471,16 @@ sub prev_next_buttons { my ($current_min,$show,$total,$parms) = @_; return '' if ($show eq 'all'); # No links if you get them all at once. # - # Create links - my $prev_min = $current_min - $show; - $prev_min = 1 if $prev_min < 1; - my $prevlink = - qq{
}; - # - my $next_min = $current_min + $show; - $next_min = $current_min if ($next_min > $total); - my $nextlink = - qq{}; - my $reloadlink = - qq{}; - # - # Determine which parameters to pass - my $String = '[_1]prev[_2]   [_3]reload[_4]   [_5]next[_6]'; - if ($prev_min == $current_min) { - $String =~ s:\[_[12]\]::g; - } - if ($next_min == $current_min) { - $String =~ s:\[_[56]\]::g; - } - my $links = &mt($String, - $prevlink, '', - $reloadlink,'', - $nextlink, ''); - return $links; + # Create buttons + my $buttons = ''; + $buttons .= ' 'x3; + $buttons .= '$total_results); my $min = $ENV{'form.start'}; my $max; if ($ENV{'form.show'} eq 'all') { @@ -2358,14 +2561,19 @@ sub display_results { $max = $total_results if ($max > $total_results); } ## + ## Output form elements + $r->print(&hidden_field('table'). + &hidden_field('phase'). + &hidden_field('persistent_db_id'). + &hidden_field('start') + ); + ## ## Output links (if necessary) for 'prev' and 'next' pages. $r->print - ('
'. - &prev_next_buttons($min,$ENV{'form.show'},$total_results, - "table=".$ENV{'form.table'}. - "&phase=results". - "&persistent_db_id=".$ENV{'form.persistent_db_id'}) - ."
\n" + ('
'. + &prev_next_buttons($min,$ENV{'form.show'},$total_results). + ''. + &viewoptions().'
' ); if ($total_results == 0) { $r->print(''. @@ -2704,7 +2912,7 @@ SCRIPT $result.=< $bodytag - + $importbutton END @@ -2762,6 +2970,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 @@ -2783,26 +3026,108 @@ extra custom metadata to show. ###################################################################### sub detailed_citation_view { my ($prefix,%values) = @_; - my $icon=&Apache::loncommon::icon($values{'url'}); - my $result=<$prefix$values{'title'} -

-$values{'author'}, $values{'owner'}
- -Subject: $values{'subject'}
-Keyword(s): $values{'keywords'}
-Notes: $values{'notes'}
-MIME Type: $values{'mimetag'}
-Language: $values{'language'}
-Copyright/Distribution: $values{'copyrighttag'}
-

-$values{'extrashow'} -

-$values{'shortabstract'} -

-
-END + my $result; + $result .= ''.$prefix. + ''.' '. + ''.$values{'title'}."\n"; + $result .= "

\n"; + $result .= ''.$values{'author'}.','. + ' '.$values{'owner'}.'
'; + foreach my $field + ( + { name=>'url', + translate => 'URL: [_1]', + special => 'url link',}, + { name=>'subject', + translate => 'Subject: [_1]',}, + { name=>'keywords', + translate => 'Keywords: [_1]',}, + { name=>'notes', + translate => 'Notes: [_1]',}, + { name=>'mimetag', + translate => 'MIME Type: [_1]',}, + { name=>'standards', + translate => 'Standards:[_1]',}, + { name=>'copyrighttag', + translate => 'Copyright/Distribution: [_1]',}, + { name=>'count', + format => "%d", + translate => 'Access Count: [_1]',}, + { name=>'stdno', + format => "%d", + translate => 'Number of Students: [_1]',}, + { name=>'avetries', + format => "%.2f", + translate => 'Average Tries: [_1]',}, + { name=>'disc', + format => "%.2f", + translate => 'Degree of Discrimination: [_1]',}, + { name=>'difficulty', + format => "%.2f", + translate => 'Degree of Difficulty: [_1]',}, + { name=>'clear', + format => "%.2f", + translate => 'Clear: [_1]',}, + { name=>'depth', + format => "%.2f", + translate => 'Depth: [_1]',}, + { name=>'helpful', + format => "%.2f", + translate => 'Helpful: [_1]',}, + { name=>'correct', + format => "%.2f", + translate => 'Correct: [_1]',}, + { name=>'technical', + format => "%.2f", + translate => 'Technical: [_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',}, + ) { + next if (! exists($values{$field->{'name'}}) || + $values{$field->{'name'}} eq ''); + if (exists($field->{'type'}) && $field->{'type'} eq 'list') { + $result .= ''.&mt($field->{'translate'}).'

    '; + foreach my $item (split(',',$values{$field->{'name'}})){ + $result .= '
  • '. + ''.$item.'
  • '; + } + $result .= '
'; + } elsif (exists($field->{'format'}) && $field->{'format'} ne ''){ + $result.= &mt($field->{'translate'}, + sprintf($field->{'format'}, + $values{$field->{'name'}}))."
\n"; + } else { + if ($field->{'special'} eq 'url link') { + $result.= + &mt($field->{'translate'}, + ''. + $values{$field->{'name'}}. + ''); + } else { + $result.= &mt($field->{'translate'}, + $values{$field->{'name'}}); + } + $result .= "
\n"; + } + } + $result .= "

"; + if (exists($values{'extrashow'}) && $values{'extrashow'} ne '') { + $result .= '

'.$values{'extrashow'}.'

'; + } + if (exists($values{'shortabstract'}) && $values{'shortabstract'} ne '') { + $result .= '

'.$values{'shortabstract'}.'

'; + } + $result .= '
'."\n"; return $result; } @@ -2820,7 +3145,8 @@ sub summary_view { my ($prefix,%values) = @_; my $icon=&Apache::loncommon::icon($values{'url'}); my $result=<  +$values{'author'}
$values{'title'}
$values{'owner'} -- $values{'lastrevisiondate'}
@@ -2845,12 +3171,11 @@ END ###################################################################### sub compact_view { my ($prefix,%values) = @_; - my $icon=&Apache::loncommon::icon($values{'url'}); - my $result=< -$values{'title'} -$values{'author'}
-END + my $result = + $prefix.' '. + $values{'title'}.''.(' 'x2). + ''.$values{'author'}.'
'; return $result; } @@ -2869,28 +3194,35 @@ END sub fielded_format_view { my ($prefix,%values) = @_; my $icon=&Apache::loncommon::icon($values{'url'}); + my %Translated = &Apache::lonmeta::fieldnames(); my $result=< -URL: $values{'url'} -
-Title: $values{'title'}
-Author(s): $values{'author'}
-Subject: $values{'subject'}
-Keyword(s): $values{'keywords'}
-Notes: $values{'notes'}
-MIME Type: $values{'mimetag'}
-Language: $values{'language'}
-Creation Date: $values{'creationdate'}
-Last Revision Date: $values{'lastrevisiondate'}
-Publisher/Owner: $values{'owner'}
-Copyright/Distribution: $values{'copyrighttag'}
-Repository Location: $values{'hostname'}
-Abstract: $values{'shortabstract'}
-$values{'extrashow'} -

-
+
+
URL:
+
$values{'url'}
END + foreach my $field ('title','author','subject','keywords','notes', + 'mimetag','language','creationdate','lastrevisiondate', + 'owner','copyrighttag','hostname','abstract') { + $result .= (' 'x4).'
'.$Translated{$field}.'
'."\n". + (' 'x8).'
'.$values{$field}.'
'."\n"; + } + if (&has_stat_data(\%values)) { + foreach my $field (&statfields()) { + $result .= (' 'x4).'
'.$Translated{$field}.'
'."\n". + (' 'x8).'
'.$values{$field}.'
'."\n"; + } + } + if (&has_eval_data(\%values)) { + foreach my $field (&evalfields()) { + $result .= (' 'x4).'
'.$Translated{$field}.'
'."\n". + (' 'x8).'
'.$values{$field}.'
'."\n"; + } + } + $result .= "
\n"; + $result .= $values{'extrashow'}; + $result .= '
'."\n"; return $result; } @@ -2909,33 +3241,54 @@ END ###################################################################### sub xml_sgml_view { my ($prefix,%values) = @_; - my $xml = < -$values{'url'} -$values{'title'} -$values{'author'} -$values{'subject'} -$values{'keywords'} -$values{'notes'} - -$values{'mime'} -$values{'mimetag'} - - -$values{'language'} -$values{'languagetag'} - -$values{'creationdate'} -$values{'lastrevisiondate'} -$values{'owner'} - -$values{'copyright'} -$values{'copyrighttag'} - -$values{'hostname'} -$values{'shortabstract'} - -END + my $xml = ''."\n"; + # The usual suspects + foreach my $field ('url','title','author','subject','keywords','notes') { + $xml .= qq{<$field>$values{$field}}."\n"; + } + # + $xml .= "\n"; + foreach my $field ('mime','mimetag') { + $xml .= qq{<$field>$values{$field}}."\n"; + } + $xml .= "\n"; + # + $xml .= "\n"; + foreach my $field ('language','languagetag') { + $xml .= qq{<$field>$values{$field}}."\n"; + } + $xml .= "\n"; + # + foreach my $field ('creationdate','lastrevisiondate','owner') { + $xml .= qq{<$field>$values{$field}}."\n"; + } + # + $xml .= "\n"; + foreach my $field ('copyright','copyrighttag') { + $xml .= qq{<$field>$values{$field}}."\n"; + } + $xml .= "\n"; + $xml .= qq{$values{'hostname'}}. + "\n"; + $xml .= qq{$values{'shortabstract'}}."\n"; + # + if (&has_stat_data(\%values)){ + $xml .= "\n"; + foreach my $field (&statfields()) { + $xml .= qq{<$field>$values{$field}}."\n"; + } + $xml .= "\n"; + } + # + if (&has_eval_data(\%values)) { + $xml .= "\n"; + foreach my $field (&evalfields) { + $xml .= qq{<$field>$values{$field}}."\n"; + } + $xml .= "\n"; + } + # + $xml .= "\n"; $xml = &HTML::Entities::encode($xml,'<>&'); my $result=<[_1]. Please modify your search.',$ENV{'form.basicexp'}); + } else { + $errorstring = &mt('Unable to understand the search phrase [_1]:[_2].',$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(< + +The LearningOnline Network with CAPA + +$bodytag + +$hidden_fields +$closebutton +
+

$heading

+

+$errorstring +

+

+$revise +

+ + +ENDPAGE +} + +###################################################################### +###################################################################### + +=pod + =item &output_blank_field_error() Output a complete page that indicates the user has not filled in enough @@ -2990,33 +3388,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(<print(< The LearningOnline Network with CAPA -BEGINNING - $r->print(< $bodytag - -

Search Catalog

$hidden_fields -Revise search request  $closebutton
-

Unactionable search query.

+

$heading

-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 +

+

+$revise 

-RESULTS +ENDPAGE + return; } ######################################################################