# The LearningOnline Network with CAPA
# Search Catalog
#
# $Id: lonsearchcat.pm,v 1.286 2007/05/02 01:33:49 albertel Exp $
#
# Copyright Michigan State University Board of Trustees
#
# This file is part of the LearningOnline Network with CAPA (LON-CAPA).
#
# LON-CAPA is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# LON-CAPA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with LON-CAPA; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# /home/httpd/html/adm/gpl.txt
#
# http://www.lon-capa.org/
#
###############################################################################
###############################################################################
=pod
=head1 NAME
lonsearchcat - LONCAPA Search Interface
=head1 SYNOPSIS
Search interface to LON-CAPAs digital library
=head1 DESCRIPTION
This module enables searching for a distributed browseable catalog.
This is part of the LearningOnline Network with CAPA project
described at http://www.lon-capa.org.
lonsearchcat presents the user with an interface to search the LON-CAPA
digital library. lonsearchcat also initiates the execution of a search
by sending the search parameters to LON-CAPA servers. The progress of
search (on a server basis) is displayed to the user in a separate window.
=head1 Internals
=over 4
=cut
###############################################################################
###############################################################################
package Apache::lonsearchcat;
use strict;
use Apache::Constants qw(:common :http);
use Apache::lonnet;
use Apache::File();
use CGI qw(:standard);
use Text::Query;
use GDBM_File;
use Apache::loncommon();
use Apache::lonmysql();
use Apache::lonmeta;
use Apache::lonhtmlcommon;
use Apache::lonlocal;
use LONCAPA::lonmetadata();
use HTML::Entities();
use Parse::RecDescent;
use Apache::lonnavmaps;
use Apache::lonindexer();
use LONCAPA;
######################################################################
######################################################################
##
## Global variables
##
######################################################################
######################################################################
my %groupsearch_db; # Database hash used to save values for the
# groupsearch RAT interface.
my %persistent_db; # gdbm hash which holds data which is supposed to
# persist across calls to lonsearchcat.pm
# The different view modes and associated functions
my %Views = ("detailed" => \&detailed_citation_view,
"detailedpreview" => \&detailed_citation_preview,
"summary" => \&summary_view,
"summarypreview" => \&summary_preview,
"fielded" => \&fielded_format_view,
"xml" => \&xml_sgml_view,
"compact" => \&compact_view);
######################################################################
######################################################################
sub handler {
my $r = shift;
# &set_defaults();
#
# set form defaults
#
my $hidden_fields;# Hold all the hidden fields used to keep track
# of the search system state
my $importbutton; # button to take the selected results and go to group
# sorting
my $diropendb; # The full path to the (temporary) search database file.
# This is set and used in &handler() and is also used in
# &output_results().
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.
#
&Apache::loncommon::content_type($r,'text/html');
$r->send_http_header;
return OK if $r->header_only;
##
## Prevent caching of the search interface window. Hopefully this means
## we will get the launch=1 passed in a little more.
&Apache::loncommon::no_cache($r);
##
## Pick up form fields passed in the links.
##
&Apache::loncommon::get_unprocessed_cgi($ENV{'QUERY_STRING'},
['catalogmode','launch','acts','mode','form','element','pause',
'phase','persistent_db_id','table','start','show',
'cleargroupsort','titleelement','area']);
##
## The following is a trick - we wait a few seconds if asked to so
## the daemon running the search can get ahead of the daemon
## printing the results. We only need (theoretically) to do
## this once, so the pause indicator is deleted
##
if (exists($env{'form.pause'})) {
sleep(1);
delete($env{'form.pause'});
}
##
## Initialize global variables
##
my $domain = $r->dir_config('lonDefDomain');
$diropendb= "/home/httpd/perl/tmp/".
"$env{'user.domain'}_$env{'user.name'}_sel_res.db";
#
# set the name of the persistent database
# $env{'form.persistent_db_id'} can only have digits in it.
if (! exists($env{'form.persistent_db_id'}) ||
($env{'form.persistent_db_id'} =~ /\D/) ||
($env{'form.launch'} eq '1')) {
$env{'form.persistent_db_id'} = time;
}
my $persistent_db_file = "/home/httpd/perl/tmp/".
&escape($domain).
'_'.&escape($env{'user.name'}).
'_'.$env{'form.persistent_db_id'}.'_persistent_search.db';
##
&Apache::lonhtmlcommon::clear_breadcrumbs();
my @allowed_searches = ('portfolio');
if (&Apache::lonnet::allowed('bre',$env{'request.role.domain'})) {
push(@allowed_searches,'res');
}
if (exists($env{'request.course.id'}) && $env{'request.course.id'} ne '') {
push(@allowed_searches,'course');
}
my $crumb_text = 'Portfolio Search';
if (@allowed_searches == 3) {
$crumb_text = 'Course, Portfolio and Catalog Search';
} elsif (@allowed_searches ==2) {
if (grep(/^res$/,@allowed_searches)) {
$crumb_text = 'Portfolio and Catalog Search';
} elsif (grep(/^course$/,@allowed_searches)) {
$crumb_text = 'Portfolio and Course Search';
}
}
&Apache::lonhtmlcommon::add_breadcrumb
({href=>'/adm/searchcat?'.
'catalogmode='.$env{'form.catalogmode'}.
'&launch='.$env{'form.launch'}.
'&mode='.$env{'form.mode'},
text=>"$crumb_text",
target=>'_top',
bug=>'Searching',});
#
if ($env{'form.phase'} !~ m/(basic|adv|course)_search/) {
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);
my $msg =
'We were unable to retrieve data describing your search. '.
'This is a serious error and has been logged. '.
'Please alert your LON-CAPA administrator.';
&Apache::loncommon::simple_error_page($r,'Search Error',
$msg);
return OK;
}
}
} else {
&clean_up_environment();
}
##
## Clear out old values from groupsearch database
##
untie %groupsearch_db if (tied(%groupsearch_db));
if (($env{'form.cleargroupsort'} eq '1') ||
(($env{'form.launch'} eq '1') &&
($env{'form.catalogmode'} eq 'import'))) {
if (tie(%groupsearch_db,'GDBM_File',$diropendb,&GDBM_WRCREAT(),0640)) {
&start_fresh_session();
untie %groupsearch_db;
delete($env{'form.cleargroupsort'});
} else {
# This is a stupid error to give to the user.
# It really tells them nothing.
my $msg = 'Unable to tie hash to db file.';
&Apache::loncommon::simple_error_page($r,'Search Error',
$msg);
return OK;
}
}
##
## Configure hidden fields
##
$hidden_fields = ''."\n";
if (exists($env{'form.catalogmode'})) {
$hidden_fields .= &hidden_field('catalogmode');
}
if (exists($env{'form.form'})) {
$hidden_fields .= &hidden_field('form');
}
if (exists($env{'form.element'})) {
$hidden_fields .= &hidden_field('element');
}
if (exists($env{'form.titleelement'})) {
$hidden_fields .= &hidden_field('titleelement');
}
if (exists($env{'form.mode'})) {
$hidden_fields .= &hidden_field('mode');
}
if (exists($env{'form.area'})) {
$hidden_fields .= &hidden_field('area');
}
##
## Configure dynamic components of interface
##
if ($env{'form.catalogmode'} eq 'interactive') {
$closebutton="
END
} else {
$closebutton = '';
$importbutton = '';
}
##
## Sanity checks on form elements
##
if (!defined($env{'form.viewselect'})) {
$env{'form.viewselect'} ="summary";
}
$env{'form.phase'} = 'disp_basic' if (! exists($env{'form.phase'}));
$env{'form.show'} = 20 if (! exists($env{'form.show'}));
#
$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';
} elsif ($env{'form.phase'} eq 'course_search') {
$env{'form.searchmode'} = 'course_search';
}
#
if ($env{'form.searchmode'} eq 'advanced') {
my $srchtype = 'Catalog';
if ($env{'form.area'} eq 'portfolio') {
$srchtype = 'Portfolio';
}
&Apache::lonhtmlcommon::add_breadcrumb
({href=>'/adm/searchcat?phase=disp_adv&'.
'catalogmode='.$env{'form.catalogmode'}.
'&launch='.$env{'form.launch'}.
'&mode='.$env{'form.mode'},
text=>"Advanced $srchtype Search",
bug=>'Searching',});
} elsif ($env{'form.searchmode'} eq 'course search') {
&Apache::lonhtmlcommon::add_breadcrumb
({href=>'/adm/searchcat?phase=disp_adv&'.
'catalogmode='.$env{'form.catalogmode'}.
'&launch='.$env{'form.launch'}.
'&mode='.$env{'form.mode'},
text=>"Course Search",
bug=>'Searching',});
}
##
## Switch on the phase
##
if ($env{'form.phase'} eq 'disp_basic') {
&print_basic_search_form($r,$closebutton,$hidden_fields);
} elsif ($env{'form.phase'} eq 'disp_adv') {
&print_advanced_search_form($r,$closebutton,$hidden_fields);
} elsif ($env{'form.phase'} eq 'results') {
&display_results($r,$importbutton,$closebutton,$diropendb,
$env{'form.area'});
} elsif ($env{'form.phase'} =~ /^(sort|run_search)$/) {
my ($query,$customquery,$customshow,$libraries,$pretty_string) =
&get_persistent_data($persistent_db_file,
['query','customquery','customshow',
'libraries','pretty_string']);
if ($env{'form.phase'} eq 'sort') {
&print_sort_form($r,$pretty_string);
} elsif ($env{'form.phase'} eq 'run_search') {
&run_search($r,$query,$customquery,$customshow,
$libraries,$pretty_string,$env{'form.area'});
}
} elsif ($env{'form.phase'} eq 'course_search') {
&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($env{'form.area'}))) {
my $errorstring=&Apache::lonmysql::get_error();
&Apache::lonnet::logthis('lonsearchcat.pm: Unable to create '.
'needed table. lonmysql error:'.
$errorstring);
my $msg =
'Unable to create table in which to save search results. '.
'The search has been aborted.';
&Apache::loncommon::simple_error_page($r,'Search Error',
$msg);
return OK;
}
delete($env{'form.launch'});
if (! &make_form_data_persistent($r,$persistent_db_file)) {
my $msg=
'Unable to properly save search information. '.
'The search has been aborted.';
&Apache::loncommon::simple_error_page($r,'Search Error',
$msg);
return OK;
}
##
## Print out the frames interface
##
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 ''.$/;
}
######################################################################
######################################################################
##
## Course Search
##
######################################################################
######################################################################
{ # 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 $pretty_search_string = ''.$env{'form.courseexp'}.'';
my $search_string = $env{'form.courseexp'};
my @New_Words;
undef(%alreadyseen);
if ($env{'form.crsrelated'}) {
($search_string,@New_Words) = &related_version($env{'form.courseexp'});
if (@New_Words) {
$pretty_search_string .= ' '.&mt("with related words").": @New_Words.";
} else {
$pretty_search_string .= ' '.&mt('with no related words').".";
}
}
my $fulltext=$env{'form.crsfulltext'};
my $discuss=$env{'form.crsdiscuss'};
my @allwords=($search_string,@New_Words);
$totalfound=0;
$r->print(&Apache::loncommon::start_page('Course Search').
'
'.
$pretty_search_string.'
'.
''.&mt('Course content').': ');
$r->rflush();
# ======================================================= Go through the course
my $c=$r->connection;
if (tie(%hash,'GDBM_File',$env{'request.course.fn'}.".db",
&GDBM_READER(),0640)) {
foreach (sort(keys(%hash))) {
if ($c->aborted()) { last; }
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);
}
unless ($totalfound) {
$r->print('
'.&mt('No matches found in resources').'.
');
}
# Check discussions if requested
if ($discuss) {
my $totaldiscussions = 0;
$r->print('
'.&mt('Discussion postings').': ');
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/$LONCAPA::domain_re/$LONCAPA::username_re)/(\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 .= &escape($resource->symb());
my $title = $resource->compTitle();
$r->print(' '.
($title?$title:$url).' - '.$disctype.' ');
$totaldiscussions++;
} else {
$r->print(' .');
}
}
unless ($totaldiscussions) {
$r->print('
'.&mt('No matches found in postings').'.
');
}
}
# =================================================== Done going through course
$r->print(&Apache::loncommon::end_page());
}
# =============================== This pulls up a resource and its dependencies
sub checkonthis {
my ($r,$id,$url,$level,$title,$fulltext,$symb,@allwords)=@_;
$alreadyseen{$id}=1;
if (&Apache::loncommon::connection_aborted($r)) { return; }
$r->rflush();
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='.&escape($symb));
}
$result=~s/\s+/ /gs;
my $applies = 0;
$applies = &checkwords($result,$applies,@allwords);
# Does this resource apply?
if ($applies) {
$r->print(' ');
for (my $i=0;$i<=$level*5;$i++) {
$r->print(' ');
}
my $href=$url;
if ($hash{'encrypted_'.$id} && !$env{'request.role.adv'}) {
$href=&Apache::lonenc::encrypted($href)
.'?symb='.&Apache::lonenc::encrypted($symb);
} else {
$href.='?symb='.&escape($symb);
}
$r->print(''.($title?$title:$url).
' ');
$totalfound++;
} elsif ($fulltext) {
$r->print(' .');
}
$r->rflush();
# Check also the dependencies of this one
my $dependencies=
&Apache::lonnet::metadata($url,'dependencies');
foreach (split(/\,/,$dependencies)) {
if (($_=~/^\/res\//) && (!$alreadyseen{$id})) {
&checkonthis($r,$id,$_,$level+1,'',$fulltext,undef,@allwords);
}
}
}
sub checkwords {
my ($result,$applies,@allwords) = @_;
foreach (@allwords) {
if ($_=~/\w/) {
if ($result=~/$_/si) {
$applies++;
}
}
}
return $applies;
}
sub untiehash {
if (tied(%hash)) {
untie(%hash);
}
}
} # End of course search scoping
######################################################################
######################################################################
=pod
=item &print_basic_search_form()
Prints the form for the basic search. Sorry the name is so cryptic.
=cut
######################################################################
######################################################################
sub print_basic_search_form {
my ($r,$closebutton,$hidden_fields) = @_;
my $result = ($env{'form.catalogmode'} ne 'import');
my $bread_crumb =
&Apache::lonhtmlcommon::breadcrumbs('Searching','Search_Basic',
$env{'form.catalogmode'} ne 'import');
my $scrout = &Apache::loncommon::start_page('Search').$bread_crumb;
# Search form for resource space
if (&Apache::lonnet::allowed('bre',$env{'request.role.domain'})) {
$scrout .= &setup_basic_search($r,'res',$hidden_fields,$closebutton);
$scrout .= ' ';
}
# Search form for accessible portfolio files
$scrout.= &setup_basic_search($r,'portfolio',$hidden_fields,$closebutton);
if ($env{'request.course.id'}) {
my %lt=&Apache::lonlocal::texthash('srch' => 'Search',
'header' => 'Course Search',
'note' => 'Enter terms or phrases, then press "Search" below',
'use' => 'use related words',
'full' =>'fulltext search (time consuming)',
'disc' => 'search discussion postings (resources and bulletin boards)',
);
$scrout.=(<
$lt{'header'}
$hidden_fields
$lt{'note'}.
ENDCOURSESEARCH
$scrout.=' '.
&Apache::lonhtmlcommon::textbox('courseexp',
$env{'form.courseexp'},40);
my $crscheckbox =
&Apache::lonhtmlcommon::checkbox('crsfulltext',
$env{'form.crsfulltext'});
my $relcheckbox =
&Apache::lonhtmlcommon::checkbox('crsrelated',
$env{'form.crsrelated'});
my $discheckbox =
&Apache::lonhtmlcommon::checkbox('crsdiscuss',
$env{'form.crsrelated'});
$scrout.=(<
ENDENDCOURSE
}
$scrout .= &Apache::loncommon::end_page();
$r->print($scrout);
return;
}
sub setup_basic_search {
my ($r,$area,$hidden_fields,$closebutton) = @_;
# Define interface components
my %lt = &Apache::lonlocal::texthash (
res => 'LON-CAPA Catalog Search',
portfolio => 'Portfolio Search',
);
my ($userelatedwords,$onlysearchdomain,$inclext,$adv_search_link,$scrout);
$userelatedwords = '';
$onlysearchdomain = '';
if ($area eq 'res') {
$inclext= '';
}
$adv_search_link = ''.&mt('Advanced Search').'';
#
$scrout.='';
return $scrout;
}
######################################################################
######################################################################
=pod
=item &advanced_search_form()
Prints the advanced search form.
=cut
######################################################################
######################################################################
sub print_advanced_search_form{
my ($r,$closebutton,$hidden_fields) = @_;
my $bread_crumb =
&Apache::lonhtmlcommon::breadcrumbs('Searching','Search_Advanced',
$env{'form.catalogmode'} ne 'import');
my %lt=&Apache::lonlocal::texthash('srch' => 'Search',
'reset' => 'Reset',
'help' => 'Help');
my $advanced_buttons=<<"END";
$closebutton
END
my $srchtype = 'Catalog';
my $jscript;
if ($env{'form.area'} eq 'portfolio') {
$srchtype = 'Portfolio';
$jscript = '';
}
my $scrout= &Apache::loncommon::start_page("Advanced $srchtype Search",
$jscript);
$scrout .= <<"ENDHEADER";
$bread_crumb
".
&Apache::loncommon::end_page());
&Apache::lonnet::logthis("lonmysql was unable to determine the status".
" of table ".$table);
return undef;
} elsif (! $table_check) {
$r->print("The table of results could not be found.");
&Apache::lonnet::logthis("The user requested a table, ".$table.
", that could not be found.");
return undef;
}
return 1;
}
######################################################################
######################################################################
=pod
=item &print_sort_form()
The sort feature is not implemented at this time. This form just prints
a link to change the search query.
=cut
######################################################################
######################################################################
sub print_sort_form {
my ($r,$pretty_query_string) = @_;
##
my %SortableFields=&Apache::lonlocal::texthash(
id => 'Default',
title => 'Title',
author => 'Author',
subject => 'Subject',
url => 'URL',
version => 'Version Number',
mime => 'Mime type',
lang => 'Language',
owner => 'Owner/Publisher',
copyright => 'Copyright',
hostname => 'Host',
creationdate => 'Creation Date',
lastrevisiondate => 'Revision Date'
);
##
my $table = $env{'form.table'};
return if (! &ensure_db_and_table($r,$table));
##
## Get the number of results
##
my $total_results = &Apache::lonmysql::number_of_rows($table);
if (! defined($total_results)) {
$r->print("A MySQL error has occurred.".
&Apache::loncommon::end_page());
&Apache::lonnet::logthis("lonmysql was unable to determine the number".
" of rows in table ".$table);
&Apache::lonnet::logthis(&Apache::lonmysql::get_error());
return;
}
my $js =<
function change_sort() {
var newloc = "/adm/searchcat?phase=results";
newloc += "&persistent_db_id=$env{'form.persistent_db_id'}";
newloc += "&sortby=";
newloc += document.forms.statusform.elements.sortby.value;
parent.resultsframe.location= newloc;
}
END
my $start_page = &Apache::loncommon::start_page('Results',$js,
{'no_title' => 1});
my $breadcrumbs=
&Apache::lonhtmlcommon::breadcrumbs('Searching','Searching',
$env{'form.catalogmode'} ne 'import');
my $result = <
END
#
Sort Results
#Sort by: \n";
my $revise = &revise_button();
$result.=<
There are $total_results matches to your query. $revise
Search: $pretty_query_string
END
$r->print($result.&Apache::loncommon::end_page());
return;
}
#####################################################################
#####################################################################
=pod
=item MySQL Table Description
MySQL table creation requires a precise description of the data to be
stored. The use of the correct types to hold data is vital to efficient
storage and quick retrieval of records. The columns must be described in
the following format:
=cut
#####################################################################
#####################################################################
#
# These should probably be scoped but I don't have time right now...
#
my @Datatypes;
my @Fullindicies;
######################################################################
######################################################################
=pod
=item &create_results_table()
Creates the table of search results by calling lonmysql. Stores the
table id in $env{'form.table'}
Inputs: search area - either res or portfolio
Returns: the identifier of the table on success, undef on error.
=cut
######################################################################
######################################################################
sub set_up_table_structure {
my ($tabletype) = @_;
my ($datatypes,$fullindicies) =
&LONCAPA::lonmetadata::describe_metadata_storage($tabletype);
# Copy the table description before modifying it...
@Datatypes = @{$datatypes};
unshift(@Datatypes,{name => 'id',
type => 'MEDIUMINT',
restrictions => 'UNSIGNED NOT NULL',
primary_key => 'yes',
auto_inc => 'yes' });
@Fullindicies = @{$fullindicies};
return;
}
sub create_results_table {
my ($area) = @_;
if ($area eq 'portfolio') {
&set_up_table_structure('portfolio_search');
} else {
&set_up_table_structure('metadata');
}
my $table = &Apache::lonmysql::create_table
( { columns => \@Datatypes,
FULLTEXT => [{'columns' => \@Fullindicies},],
} );
if (defined($table)) {
$env{'form.table'} = $table;
return $table;
}
return undef; # Error...
}
######################################################################
######################################################################
=pod
=item Search Status update functions
Each of the following functions changes the values of one of the
input fields used to display the search status to the user. The names
should be explanatory.
Inputs: Apache request handler ($r), text to display.
Returns: Nothing.
=over 4
=item &update_count_status()
=item &update_status()
=item &update_seconds()
=back
=cut
######################################################################
######################################################################
sub update_count_status {
my ($r,$text) = @_;
$text =~ s/\'/\\\'/g;
$r->print
("\n");
$r->rflush();
}
sub update_status {
my ($r,$text) = @_;
$text =~ s/\'/\\\'/g;
$r->print
("\n");
$r->rflush();
}
{
my $max_time = 300; # seconds for the search to complete
my $start_time = 0;
my $last_time = 0;
sub reset_timing {
$start_time = 0;
$last_time = 0;
}
sub time_left {
if ($start_time == 0) {
$start_time = time;
}
my $time_left = $max_time - (time - $start_time);
$time_left = 0 if ($time_left < 0);
return $time_left;
}
sub update_seconds {
my ($r) = @_;
my $time = &time_left();
if (($last_time-$time) > 0) {
$r->print("\n");
$r->rflush();
}
$last_time = $time;
}
}
######################################################################
######################################################################
=pod
=item &revise_button()
Inputs: None
Returns: html string for a 'revise search' button.
=cut
######################################################################
######################################################################
sub revise_button {
my $revise_phase = 'disp_basic';
$revise_phase = 'disp_adv' if ($env{'form.searchmode'} eq 'advanced');
my $newloc = '/adm/searchcat'.
'?persistent_db_id='.$env{'form.persistent_db_id'}.
'&cleargroupsort=1'.
'&phase='.$revise_phase;
my $result = qq{ };
return $result;
}
######################################################################
######################################################################
=pod
=item &run_search()
Executes a search query by sending it the the other servers and putting the
results into MySQL.
=cut
######################################################################
######################################################################
sub run_search {
my ($r,$query,$customquery,$customshow,$serverlist,
$pretty_string,$area) = @_;
my $tabletype = 'metadata';
if ($area eq 'portfolio') {
$tabletype = 'portfolio_search';
}
my $connection = $r->connection;
#
# Print run_search header
#
my $start_page = &Apache::loncommon::start_page('Search Status',undef,
{'no_title' => 1});
my $breadcrumbs =
&Apache::lonhtmlcommon::breadcrumbs('Searching','Searching',
$env{'form.catalogmode'} ne 'import');
$r->print(<
END
# 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],'.... ');
}
$r->print(&mt("Search: [_1]",$pretty_string));
$r->rflush();
#
# Determine the servers we need to contact.
my @Servers_to_contact;
if (defined($serverlist)) {
if (ref($serverlist) eq 'ARRAY') {
@Servers_to_contact = @$serverlist;
} else {
@Servers_to_contact = ($serverlist);
}
} else {
my %all_library_servers = &Apache::lonnet::all_library();
@Servers_to_contact = sort(keys(%all_library_servers));
}
my %Server_status;
#
# Check on the mysql table we will use to store results.
my $table =$env{'form.table'};
if (! defined($table) || $table eq '' || $table =~ /\D/ ) {
$r->print("Unable to determine table id to save search results in.".
"The search has been aborted.".
&Apache::loncommon::end_page());
return;
}
my $table_status = &Apache::lonmysql::check_table($table);
if (! defined($table_status)) {
$r->print("Unable to determine status of table.".
&Apache::loncommon::end_page());
&Apache::lonnet::logthis("Bogus table id of $table for ".
"$env{'user.name'} @ $env{'user.domain'}");
&Apache::lonnet::logthis("lonmysql error = ".
&Apache::lonmysql::get_error());
return;
}
if (! $table_status) {
&Apache::lonnet::logthis("lonmysql error = ".
&Apache::lonmysql::get_error());
&Apache::lonnet::logthis("lonmysql debug = ".
&Apache::lonmysql::get_debug());
&Apache::lonnet::logthis('table status = "'.$table_status.'"');
$r->print("The table id,$table, we tried to use is invalid.".
"The search has been aborted.".
&Apache::loncommon::end_page());
return;
}
##
## Prepare for the big loop.
my $hitcountsum;
my %matches;
my $server;
my $status;
my $revise = &revise_button();
$r->print(<
'.
mt('Results [_1] to [_2] out of [_3]',
$min,$max,$total_results).
"
\n");
}
##
## Get results from MySQL table
my $sort_command = 'id>='.$min.' AND id<='.$max;
my $order;
if (exists($env{'form.sortorder'})) {
if ($env{'form.sortorder'} eq 'asc') {
$order = 'ASC';
} elsif ($env{'form.sortorder'} eq 'desc') {
$order = 'DESC';
} else {
$order = '';
}
} else {
$order = '';
}
if ($env{'form.sortfield'} ne 'default' &&
exists($sort_fields{$env{'form.sortfield'}})) {
$sort_command = $env{'form.sortfield'}.' IS NOT NULL '.
'ORDER BY '.$env{'form.sortfield'}.' '.$order.
' LIMIT '.($min-1).','.($max-$min+1);
}
my @Results = &Apache::lonmysql::get_rows($table,$sort_command);
##
## Loop through the results and output them.
my $tabletype = 'metadata';
if ($area eq 'portfolio') {
$tabletype = 'portfolio_search';
}
foreach my $row (@Results) {
if ($connection->aborted()) {
&cleanup();
return;
}
my %Fields = %{&parse_row($tabletype,@$row)};
my $output="
\n";
if (! defined($Fields{'title'}) || $Fields{'title'} eq '') {
$Fields{'title'} = 'Untitled';
}
my $prefix=&catalogmode_output($Fields{'title'},$Fields{'url'},
$Fields{'id'},$checkbox_num++);
# Render the result into html
$output.= &$viewfunction($prefix,%Fields);
# Print them out as they come in.
$r->print($output);
$r->rflush();
}
if (@Results < 1) {
$r->print(&mt("There were no results matching your query"));
} else {
$r->print
('
\n"
);
}
$r->print("".&Apache::loncommon::end_page());
$r->rflush();
untie %groupsearch_db if (tied(%groupsearch_db));
return;
}
######################################################################
######################################################################
=pod
=item &catalogmode_output($title,$url,$fnum,$checkbox_num)
Returns html needed for the various catalog modes. Gets inputs from
$env{'form.catalogmode'}. Stores data in %groupsearch_db.
=cut
######################################################################
######################################################################
sub catalogmode_output {
my $output = '';
my ($title,$url,$fnum,$checkbox_num) = @_;
if ($env{'form.catalogmode'} eq 'interactive') {
$title=~ s/\'/\\\'/g;
if ($env{'form.catalogmode'} eq 'interactive') {
$output.=<
END
}
} elsif ($env{'form.catalogmode'} eq 'import') {
$groupsearch_db{"pre_${fnum}_link"}=$url;
$groupsearch_db{"pre_${fnum}_title"}=$title;
$output.=<
END
}
return $output;
}
######################################################################
######################################################################
=pod
=item &parse_row()
Parse a row returned from the database.
=cut
######################################################################
######################################################################
sub parse_row {
my ($tabletype,@Row) = @_;
my %Fields;
if (! scalar(@Datatypes)) {
&set_up_table_structure($tabletype);
}
for (my $i=0;$i<=$#Row;$i++) {
$Fields{$Datatypes[$i]->{'name'}}=&unescape($Row[$i]);
}
$Fields{'language'} =
&Apache::loncommon::languagedescription($Fields{'language'});
$Fields{'copyrighttag'} =
&Apache::loncommon::copyrightdescription($Fields{'copyright'});
$Fields{'mimetag'} =
&Apache::loncommon::filedescription($Fields{'mime'});
return \%Fields;
}
###########################################################
###########################################################
=pod
=item &parse_raw_result()
Takes a line from the file of results and parse it. Returns a hash
with keys according to column labels
In addition, the following tags are set by calling the appropriate
lonnet function: 'language', 'copyrighttag', 'mimetag'.
The 'title' field is set to "Untitled" if the title field is blank.
'abstract' and 'keywords' are truncated to 200 characters.
=cut
###########################################################
###########################################################
sub parse_raw_result {
my ($result,$hostname,$tabletype) = @_;
# conclude from self to others regarding fields
my %Fields=&LONCAPA::lonmetadata::metadata_col_to_hash
($tabletype,
map {
&unescape($_);
} (split(/\,/,$result)) );
return %Fields;
}
###########################################################
###########################################################
=pod
=item &handle_custom_fields()
=cut
###########################################################
###########################################################
sub handle_custom_fields {
my @results = @{shift()};
my $customshow='';
my $extrashow='';
my @customfields;
if ($env{'form.customshow'}) {
$customshow=$env{'form.customshow'};
$customshow=~s/[^\w\s]//g;
my @fields=map {
"$_:";
} split(/\s+/,$customshow);
@customfields=split(/\s+/,$customshow);
if ($customshow) {
$extrashow="
".join("
",@fields)."
\n";
}
}
my $customdata='';
my %customhash;
foreach my $result (@results) {
if ($result=~/^(custom\=.*)$/) { # grab all custom metadata
my $tmp=$result;
$tmp=~s/^custom\=//;
my ($k,$v)=map {&unescape($_);
} split(/\,/,$tmp);
$customhash{$k}=$v;
}
}
return ($extrashow,\@customfields,\%customhash);
}
######################################################################
######################################################################
=pod
=item &search_results_header()
Output the proper html headers and javascript code to deal with different
calling modes.
Takes most inputs directly from %env, except $mode.
=over 4
=item $mode is either (at this writing) 'Basic' or 'Advanced'
=back
The following environment variables are checked:
=over 4
=item 'form.catalogmode'
Checked for 'interactive' and 'import'.
=item 'form.mode'
Checked for existance & 'edit' mode.
=item 'form.form'
Contains the name of the form that has the input fields to set
=item 'form.element'
the name of the input field to put the URL into
=item 'form.titleelement'
the name of the input field to put the title into
=back
=cut
######################################################################
######################################################################
sub search_results_header {
my ($importbutton,$closebutton) = @_;
my $js;
# output beginning of search page
# conditional output of script functions dependent on the mode in
# which the search was invoked
if ($env{'form.catalogmode'} eq 'interactive'){
if (! exists($env{'form.mode'}) || $env{'form.mode'} ne 'edit') {
$js.=<
SCRIPT
} elsif ($env{'form.mode'} eq 'edit') {
my $form = $env{'form.form'};
my $element = $env{'form.element'};
my $titleelement = $env{'form.titleelement'};
my $changetitle;
if (!$titleelement) {
$changetitle='function changeTitle(val) {}';
} else {
$changetitle=<
function select_data(title,url) {
changeURL(url);
changeTitle(title);
parent.close();
}
$changetitle
function changeURL(val) {
if (parent.targetwin.document) {
parent.targetwin.document.forms["$form"].elements["$element"].value=val;
} else {
var url = 'forms[\"$form\"].elements[\"$element\"].value';
alert("Unable to transfer data to "+url);
}
}
SCRIPT
}
}
$js.=<
SCRIPT
my $start_page = &Apache::loncommon::start_page(undef,$js,
{'only_body' =>1});
my $result=<
$importbutton
END
return $result;
}
sub results_link {
my $basic_link = "/adm/searchcat?"."&table=".$env{'form.table'}.
"&persistent_db_id=".$env{'form.persistent_db_id'};
my $results_link = $basic_link."&phase=results".
"&pause=1"."&start=1";
return $results_link;
}
######################################################################
######################################################################
sub print_frames_interface {
my $r = shift;
my $basic_link = "/adm/searchcat?"."&table=".$env{'form.table'}.
"&persistent_db_id=".$env{'form.persistent_db_id'};
my $run_search_link = $basic_link."&phase=run_search";
my $results_link = &results_link();
my $js = <
var targetwin = opener;
var queue = '';
JS
my $start_page =
&Apache::loncommon::start_page('LON-CAPA Digital Library Search Results',
$js,
{'frameset' => 1,
'add_entries' => {
'rows' => "150,*",},});
my $end_page =
&Apache::loncommon::end_page({'frameset' => 1});
my $result = <<"ENDFRAMES";
$start_page
$end_page
ENDFRAMES
$r->print($result);
return;
}
######################################################################
######################################################################
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
Output is a HTML-ified string.
Input arguments are title, author, subject, url, keywords, version,
notes, short abstract, mime, language, creation date,
last revision date, owner, copyright, hostname, and
extra custom metadata to show.
=over 4
=item &detailed_citation_view()
=cut
######################################################################
######################################################################
sub detailed_citation_view {
my ($prefix,%values) = @_;
my $result;
my $jumpurl=$values{'url'};
$jumpurl=~s|^/ext/|http://|;
$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 .=
&Apache::lonhtmlcommon::crumbs(&Apache::lonnet::clutter($item),
'preview',
'',
(($env{'form.catalogmode'} eq 'import')?'parent.statusframe.document.forms.statusform':''),2,0,1);
}
} 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') {
if ($jumpurl=~/^http\:\/\//) {
$result.=''.$jumpurl.'';
} else {
$result .=
&Apache::lonhtmlcommon::crumbs($jumpurl,
'preview',
'',
(($env{'form.catalogmode'} eq 'import')?'parent.statusframe.document.forms.statusform':''),3,0,1);
}
} 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;
}
sub detailed_citation_preview {
my ($prefix,%values)=@_;
return '
$end_page
ENDPAGE
}
######################################################################
######################################################################
=pod
=item &output_blank_field_error()
Output a complete page that indicates the user has not filled in enough
information to do a search.
Inputs: $r (Apache request handle), $closebutton, $parms.
Returns: nothing
$parms is extra information to include in the 'Revise search request' link.
=cut
######################################################################
######################################################################
sub output_blank_field_error {
my ($r,$closebutton,$parms,$hidden_fields)=@_;
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');
my $start_page = &Apache::loncommon::start_page('Search');
my $end_page = &Apache::loncommon::end_page();
$r->print(<
$hidden_fields
$closebutton
$end_page
ENDPAGE
return;
}
######################################################################
######################################################################
=pod
=item &output_date_error()
Output a full html page with an error message.
Inputs:
$r, the request pointer.
$message, the error message for the user.
$closebutton, the specialized close button needed for groupsearch.
=cut
######################################################################
######################################################################
sub output_date_error {
my ($r,$message,$closebutton,$hidden_fields)=@_;
# make query information persistent to allow for subsequent revision
my $start_page = &Apache::loncommon::start_page('Search');
my $end_page = &Apache::loncommon::end_page();
$r->print(<
$hidden_fields
$closebutton
Error
$message
$end_page
RESULTS
}
######################################################################
######################################################################
=pod
=item &start_fresh_session()
Cleans the global %groupsearch_db by removing all fields which begin with
'pre_' or 'store'.
=cut
######################################################################
######################################################################
sub start_fresh_session {
delete $groupsearch_db{'mode_catalog'};
foreach (keys %groupsearch_db) {
if ($_ =~ /^pre_/) {
delete $groupsearch_db{$_};
}
if ($_ =~ /^store/) {
delete $groupsearch_db{$_};
}
}
}
1;
sub cleanup {
if (tied(%groupsearch_db)) {
unless (untie(%groupsearch_db)) {
&Apache::lonnet::logthis('Failed cleanup searchcat: groupsearch_db');
}
}
&untiehash();
&Apache::lonmysql::disconnect_from_db();
return OK;
}
__END__
=pod
=back
=cut