--- loncom/interface/lonnavmaps.pm 2002/10/29 20:20:45 1.92 +++ loncom/interface/lonnavmaps.pm 2002/11/08 19:10:58 1.100 @@ -2,7 +2,7 @@ # The LearningOnline Network with CAPA # Navigate Maps Handler # -# $Id: lonnavmaps.pm,v 1.92 2002/10/29 20:20:45 bowersj2 Exp $ +# $Id: lonnavmaps.pm,v 1.100 2002/11/08 19:10:58 bowersj2 Exp $ # # Copyright Michigan State University Board of Trustees # @@ -48,758 +48,11 @@ use Apache::loncommon(); use GDBM_File; use POSIX qw (floor strftime); -# -------------------------------------------------------------- Module Globals -my %hash; -my @rows; - -# -# These cache hashes need to be independent of user, resource and course -# (user and course can/should be in the keys) -# - -my %courserdatas; -my %userrdatas; - -# -# These global hashes are dependent on user, course and resource, -# and need to be initialized every time when a sheet is calculated -# -my %courseopt; -my %useropt; -my %parmhash; - -# This parameter keeps track of whether obtaining the user's information -# failed, which the colorizer in astatus can use -my $networkFailedFlag = 0; - -# ------------------------------------------------------------------ Euclid gcd - -sub euclid { - my ($e,$f)=@_; - my $a; my $b; my $r; - if ($e>$f) { $b=$e; $r=$f; } else { $r=$e; $b=$f; } - while ($r!=0) { - $a=$b; $b=$r; - $r=$a%$b; - } - return $b; -} - -# --------------------------------------------------------------------- Parmval - -# -------------------------------------------- Figure out a cascading parameter -# -# For this function to work -# -# * parmhash needs to be tied -# * courseopt and useropt need to be initialized for this user and course -# - -sub parmval { - my ($what,$symb)=@_; - my $cid=$ENV{'request.course.id'}; - my $csec=$ENV{'request.course.sec'}; - my $uname=$ENV{'user.name'}; - my $udom=$ENV{'user.domain'}; - - unless ($symb) { return ''; } - my $result=''; - - my ($mapname,$id,$fn)=split(/\_\_\_/,$symb); - -# ----------------------------------------------------- Cascading lookup scheme - my $rwhat=$what; - $what=~s/^parameter\_//; - $what=~s/\_/\./; - - my $symbparm=$symb.'.'.$what; - my $mapparm=$mapname.'___(all).'.$what; - my $usercourseprefix=$uname.'_'.$udom.'_'.$cid; - - my $seclevel= $usercourseprefix.'.['.$csec.'].'.$what; - my $seclevelr=$usercourseprefix.'.['.$csec.'].'.$symbparm; - my $seclevelm=$usercourseprefix.'.['.$csec.'].'.$mapparm; - - my $courselevel= $usercourseprefix.'.'.$what; - my $courselevelr=$usercourseprefix.'.'.$symbparm; - my $courselevelm=$usercourseprefix.'.'.$mapparm; - -# ---------------------------------------------------------- first, check user - if (defined($uname)) { - if (defined($useropt{$courselevelr})) { return $useropt{$courselevelr}; } - if (defined($useropt{$courselevelm})) { return $useropt{$courselevelm}; } - if (defined($useropt{$courselevel})) { return $useropt{$courselevel}; } - } - -# ------------------------------------------------------- second, check course - if (defined($csec)) { - if (defined($courseopt{$seclevelr})) { return $courseopt{$seclevelr}; } - if (defined($courseopt{$seclevelm})) { return $courseopt{$seclevelm}; } - if (defined($courseopt{$seclevel})) { return $courseopt{$seclevel}; } - } - - if (defined($courseopt{$courselevelr})) { return $courseopt{$courselevelr}; } - if (defined($courseopt{$courselevelm})) { return $courseopt{$courselevelm}; } - if (defined($courseopt{$courselevel})) { return $courseopt{$courselevel}; } - -# ----------------------------------------------------- third, check map parms - - my $thisparm=$parmhash{$symbparm}; - if (defined($thisparm)) { return $thisparm; } - -# ----------------------------------------------------- fourth , check default - - my $default=&Apache::lonnet::metadata($fn,$rwhat.'.default'); - if (defined($default)) { return $default} - -# --------------------------------------------------- fifth , cascade up parts - - my ($space,@qualifier)=split(/\./,$rwhat); - my $qualifier=join('.',@qualifier); - unless ($space eq '0') { - my ($part,$id)=split(/\_/,$space); - if ($id) { - my $partgeneral=&parmval($part.".$qualifier",$symb); - if (defined($partgeneral)) { return $partgeneral; } - } else { - my $resourcegeneral=&parmval("0.$qualifier",$symb); - if (defined($resourcegeneral)) { return $resourcegeneral; } - } - } - return ''; -} - - - -# ------------------------------------------------------------- Find out status -# return codes -# tcode (timecode) -# 1: will open later -# 2: is open and not past due yet -# 3: is past due date -# 4: due in the next 24 hours -# -# code (curent solved status) -# 1: not attempted -# 2: attempted but wrong, or incorrect by instructor -# 3: solved or correct by instructor -# 4: partially correct (one or more parts correct) -# "excused" needs to be supported, but is not yet. -sub astatus { - my $rid=shift; - my $code=0; - my $ctext=''; - $rid=~/(\d+)\.(\d+)/; - my $symb=&Apache::lonnet::declutter($hash{'map_id_'.$1}).'___'.$2.'___'. - &Apache::lonnet::declutter($hash{'src_'.$rid}); - my %duedate=(); - my %opendate=(); - my %answerdate=(); - # need to always check part 0's open/due/answer status - foreach (sort(split(/\,/,&Apache::lonnet::metadata($hash{'src_'.$rid},'allpo\ssiblekeys')))) { - if ($_=~/^parameter\_(.*)\_opendate$/) { - my $part=$1; - $duedate{$part}=&parmval($part.'.duedate',$symb); - $opendate{$part}=&parmval($part.'.opendate',$symb); - $answerdate{$part}=&parmval($part.'.answerdate',$symb); - if (&parmval($part.'.opendate.type',$symb) eq 'date_interval') { - $opendate{$part}=$duedate{$part}-$opendate{$part}; - } - if (&parmval($part.'.answerdate.type',$symb) eq 'date_interval') { - $answerdate{$part}=$duedate{$part}+$answerdate{$part}; - } - } - } - my $now=time; - my $tcode=0; - - my %returnhash=&Apache::lonnet::restore($symb); - - foreach (sort(keys(%opendate))) { - my $duedate=$duedate{$_}; - my $opendate=$opendate{$_}; - my $answerdate=$answerdate{$_}; - my $preface=''; - unless ($_ eq '0') { $preface=' Part: '.$_.' '; } - if ($opendate) { - if ($now<$duedate || (!$duedate)) { - unless ($tcode==4) { $tcode=2; } - if ($duedate) { - $ctext.=$preface.'Due: '.localtime($duedate); - } else { - $ctext.=$preface.'No Due Date'; - } - if ($now<$opendate) { - unless ($tcode) { $tcode=1; } - $ctext.=$preface.'Open: '.localtime($opendate); - } - if ($duedate && $duedate-$now<86400) { - $tcode=4; - $ctext.=$preface.'Due: '.localtime($duedate); - } - } else { - unless (($tcode==4) || ($tcode eq 2)) { $tcode=3; } - if ($now<$answerdate) { - $ctext.='Answer: '.localtime($duedate); - } - } - } else { - unless (($tcode==2) || ($tcode==4)) { $tcode=1; } - } - - my $status=$returnhash{'resource.'.$_.'.solved'}; - - if ($status eq 'correct_by_student') { - if ($code==0||$code==3) { $code=3; } else { $code=4; } - $ctext.=' solved'; - } elsif ($status eq 'correct_by_override') { - if ($code==0||$code==3) { $code=3; } else { $code=4; } - $ctext.=' override'; - } elsif ($status eq 'incorrect_attempted') { - if ($code!=4 && $code!=3) { $code=2; } - if ($code==3) { $code=4; } - $ctext.=' ('. - ($returnhash{'resource.'.$_.'.tries'}? - $returnhash{'resource.'.$_.'.tries'}:'0'); - my $numtries = &parmval($_.'.maxtries',$symb); - if ($numtries) { $ctext.='/'.$numtries.' tries'; } - $ctext.=')'; - } elsif ($status eq 'incorrect_by_override') { - if ($code!=4 && $code!=3) { $code=2; } - if ($code==3) { $code=4; } - $ctext.=' override'; - } elsif ($status eq 'excused') { - if ($code==0||$code==3) { $code=3; } else { $code=4; } - $ctext.=' excused'; - } else { - if ($code==0) { $code=1; } - } - } - - return 'p'.$code.$tcode.'"'.$ctext.'"'; -} - - -sub addresource { - my ($resource,$sofar,$rid,$showtypes,$indent,$linkid)=@_; - if ($showtypes eq 'problems') { - if ($resource!~/\.(problem|exam|quiz|assess|survey|form)$/) { - return; - } - } - my $brepriv=&Apache::lonnet::allowed('bre',$resource); - if ($hash{'src_'.$rid}) { - if (($brepriv eq '2') || ($brepriv eq 'F')) { - my $pprefix=''; - if ($resource=~/\.(problem|exam|quiz|assess|survey|form)$/) { - $pprefix=&astatus($rid); - } - $$sofar++; - if ($indent) { $pprefix='i'.$indent.','.$pprefix; } - if ($linkid) { $pprefix='l'.$linkid.','.$pprefix; } - if (defined($rows[$$sofar])) { - $rows[$$sofar].='&'.$pprefix.$rid; - } else { - $rows[$$sofar]=$pprefix.$rid; - } - } - } -} - -sub followlinks () { - my ($rid,$sofar,$beenhere,$further,$showtypes,$indent,$linkid)=@_; - my $mincond=1; - my $next=''; - foreach (split(/\,/,$hash{'to_'.$rid})) { - my $thiscond= - &Apache::lonnet::directcondval($hash{'condid_'.$hash{'undercond_'.$_}}); - if ($thiscond>=$mincond) { - if ($next) { - $next.=','.$_.':'.$thiscond; - } else { - $next=$_.':'.$thiscond; - } - if ($thiscond>$mincond) { $mincond=$thiscond; } - } - } - my $col=0; - &Apache::lonxml::debug("following links -$next-"); - foreach (split(/\,/,$next)) { - my ($nextlinkid,$condval)=split(/\:/,$_); - if ($condval>=$mincond) { - my $now=&tracetable($sofar,$hash{'goesto_'.$nextlinkid}, - $beenhere,$showtypes,$indent,$linkid); - if ($now>$further) { - if ($col>0) { - my $string; - for(my $i=0;$i<$col;$i++) { $string.='&'; } - for(my $i=$further+1;$now-1>$i;$i++) { - $rows[$i]=$string.$rows[$i]; - } - } - $further=$now; - } - } - $col++; - } - return $further; -} -# ------------------------------------------------------------ Build page table - -sub tracetable { - my ($sofar,$rid,$beenhere,$showtypes,$indent,$linkid)=@_; - my $newshowtypes=$showtypes; - my $further=$sofar; -# $Apache::lonxml::debug=1; - &Apache::lonxml::debug("$rid ; $linkid ; $sofar ; $beenhere ; ".$hash{'src_'.$rid}); - if ($beenhere=~/\&$rid\&/) { return $further; } - $beenhere.=$rid.'&'; - - if (defined($hash{'is_map_'.$rid})) { - $sofar++; - my $tprefix=''; - if ($hash{'map_type_'.$hash{'map_pc_'.$hash{'src_'.$rid}}} - eq 'sequence') { - $tprefix='h'; - } elsif ($hash{'map_type_'.$hash{'map_pc_'.$hash{'src_'.$rid}}} - eq 'page') { - $tprefix='j'; - if ($indent) { $tprefix='i'.$indent.','.$tprefix; } - if ($linkid) { $tprefix='l'.$linkid.','.$tprefix; } - $newshowtypes='problems'; - $indent++; - #if in a .page continue to link the encompising .page - if (!$linkid) { $linkid=$rid; } - } - if (defined($rows[$sofar])) { - $rows[$sofar].='&'.$tprefix.$rid; - } else { - $rows[$sofar]=$tprefix.$rid; - } - if ((defined($hash{'map_start_'.$hash{'src_'.$rid}})) && - (defined($hash{'map_finish_'.$hash{'src_'.$rid}}))) { - my $frid=$hash{'map_finish_'.$hash{'src_'.$rid}}; - $sofar=&tracetable($sofar,$hash{'map_start_'.$hash{'src_'.$rid}}, - '&'.$frid.'&',$newshowtypes,$indent,$linkid); - &addresource($hash{'src_'.$frid},\$sofar,$frid,$newshowtypes, - $indent,$linkid); - if ($tprefix =~ /j$/) { $indent--; $linkid=''; } - } - } else { - &addresource($hash{'src_'.$rid},\$sofar,$rid,$showtypes, - $indent,$linkid); - } - - if (defined($hash{'to_'.$rid})) { - $further=&followlinks($rid,$sofar,$beenhere,$further,$showtypes, - $indent,$linkid); - } - - return $further; -} - -# ================================================================ Main Handler - sub handler { - my $r=shift; + my $r = shift; &Apache::loncommon::get_unprocessed_cgi($ENV{QUERY_STRING}); - if ($ENV{'form.jtest'} ne "1") - { - return new_handle($r); - } - -# ------------------------------------------- Set document type for header only - - if ($r->header_only) { - if ($ENV{'browser.mathml'}) { - $r->content_type('text/xml'); - } else { - $r->content_type('text/html'); - } - $r->send_http_header; - return OK; - } - my $requrl=$r->uri; - my $hashtied; -# ----------------------------------------------------------------- Tie db file - my $fn; - if ($ENV{'request.course.fn'}) { - $fn=$ENV{'request.course.fn'}; - if (-e "$fn.db") { - if ((tie(%hash,'GDBM_File',"$fn.db",&GDBM_READER(),0640)) && - (tie(%parmhash,'GDBM_File', - $ENV{'request.course.fn'}.'_parms.db', - &GDBM_READER(),0640))) { - $hashtied=1; - } - } - } - if (!$hashtied) { - $ENV{'user.error.msg'}="$requrl:bre:0:0:Course not initialized"; - return HTTP_NOT_ACCEPTABLE; - } - -# ------------------------------------------------------------------- Hash tied - - if ($ENV{'browser.mathml'}) { - $r->content_type('text/xml'); - } else { - $r->content_type('text/html'); - } - &Apache::loncommon::no_cache($r); - $r->send_http_header; - - my $firstres=$hash{'map_start_'. - &Apache::lonnet::clutter($ENV{'request.course.uri'})}; - my $lastres=$hash{'map_finish_'. - &Apache::lonnet::clutter($ENV{'request.course.uri'})}; - if (!(($firstres) && ($lastres))) { - $r->print('Coursemap undefined.'); - } else { - -# ----------------------------------------------------------------- Render page -# -------------------------------------------------------------- Set parameters - - -# ---------------------------- initialize coursedata and userdata for this user - undef %courseopt; - undef %useropt; - - my $uname=$ENV{'user.name'}; - my $udom=$ENV{'user.domain'}; - my $uhome=$ENV{'user.home'}; - my $cid=$ENV{'request.course.id'}; - my $chome=$ENV{'course.'.$cid.'.home'}; - my ($cdom,$cnum)=split(/\_/,$cid); - - my $userprefix=$uname.'_'.$udom.'_'; - - unless ($uhome eq 'no_host') { -# ------------------------------------------------- Get coursedata (if present) - unless ((time-$courserdatas{$cid.'.last_cache'})<240) { - my $reply=&Apache::lonnet::reply('dump:'.$cdom.':'.$cnum. - ':resourcedata',$chome); - if ($reply!~/^error\:/) { - $courserdatas{$cid}=$reply; - $courserdatas{$cid.'.last_cache'}=time; - } - # check to see if network failed - elsif ( $reply=~/no.such.host/i || $reply=~/con.*lost/i ) - { - $networkFailedFlag = 1; - } - } - foreach (split(/\&/,$courserdatas{$cid})) { - my ($name,$value)=split(/\=/,$_); - $courseopt{$userprefix.&Apache::lonnet::unescape($name)}= - &Apache::lonnet::unescape($value); - } -# --------------------------------------------------- Get userdata (if present) - unless ((time-$userrdatas{$uname.'___'.$udom.'.last_cache'})<240) { - my $reply=&Apache::lonnet::reply('dump:'.$udom.':'.$uname.':resourcedata',$uhome); - if ($reply!~/^error\:/) { - $userrdatas{$uname.'___'.$udom}=$reply; - $userrdatas{$uname.'___'.$udom.'.last_cache'}=time; - } - } - foreach (split(/\&/,$userrdatas{$uname.'___'.$udom})) { - my ($name,$value)=split(/\=/,$_); - $useropt{$userprefix.&Apache::lonnet::unescape($name)}= - &Apache::lonnet::unescape($value); - } - } - - @rows=(); - - &tracetable(0,$firstres,'&','',0); - -# ------------------------------------------------------------------ Page parms - - my $j; - my $i; - my $lcm=1; - my $contents=0; - -# ---------------------------------------------- Go through table to get layout - - for ($i=0;$i<=$#rows;$i++) { - if ($rows[$i]) { - &Apache::lonxml::debug("Row $i is:".$rows[$i]); - $contents++; - my @colcont=split(/\&/,$rows[$i]); - $lcm*=($#colcont+1)/euclid($lcm,($#colcont+1)); - } - } - - - unless ($contents) { - $r->print('Empty Map.'); - } else { - -# ------------------------------------------------------------------ Build page - - my $currenturl=$ENV{'form.postdata'}; - $currenturl=~s/^http\:\/\///; - $currenturl=~s/^[^\/]+//; - -# ---------------------------------------------------------------- Send headers - - my $date=localtime; - my $now=time; -# ----------------------------------------- Get email status and discussiontime - - my %emailstatus=&Apache::lonnet::dump('email_status'); - my $logouttime=$emailstatus{'logout'}; - my $courseleave=$emailstatus{'logout_'.$ENV{'request.course.id'}}; - my $lastcheck=($courseleave>$logouttime?$courseleave:$logouttime); - - my %discussiontimes=&Apache::lonnet::dump('discussiontimes', - $cdom,$cnum); - - my %feedback=(); - my %error=(); - foreach my $msgid (split(/\&/,&Apache::lonnet::reply('keys:'. - $ENV{'user.domain'}.':'. - $ENV{'user.name'}.':nohist_email', - $ENV{'user.home'}))) { - $msgid=&Apache::lonnet::unescape($msgid); - my $plain=&Apache::lonnet::unescape(&Apache::lonnet::unescape($msgid)); - if ($plain=~/(Error|Feedback) \[([^\]]+)\]/) { - my ($what,$url)=($1,$2); - my %status= - &Apache::lonnet::get('email_status',[$msgid]); - if ($status{$msgid}=~/^error\:/) { - $status{$msgid}=''; - } - - if (($status{$msgid} eq 'new') || - (!$status{$msgid})) { - if ($what eq 'Error') { - $error{$url}.=','.$msgid; - } else { - $feedback{$url}.=','.$msgid; - } - } - } - } -# ----------------------------------------------------------- Start Page Output - my $bodytagadd=''; - $r->print( - 'Navigate Course Map'); - if (($currenturl=~/^\/res/) && - ($currenturl!~/^\/res\/adm/)) { - $bodytagadd='onLoad="window.location.hash='."'curloc'".'"'; - } - $r->print(&Apache::loncommon::bodytag('Navigate Course Map','', - $bodytagadd)); - $r->print(''); - my $desc=$ENV{'course.'.$ENV{'request.course.id'}.'.description'}; - if (defined($desc)) { $r->print("

$desc

\n"); } - $r->print("

$date

\n"); - $r->rflush(); - $r->print(' New discussion since '. - localtime($lastcheck). - '
New message (click to open)

'); - if (($currenturl=~/^\/res/) && - ($currenturl!~/^\/res\/adm/)) { - $r->print('Current Location

'); - } - - # Handle a network error - - if ($networkFailedFlag) - { - $r->print('

LON-CAPA network failure.

'."\n"); - $r->print("

LON-CAPA's network is having difficulties, some problem" . - " information, such as due dates, will not be available."); - } -# ----------------------------------------------------- The little content list - for ($i=0;$i<=$#rows;$i++) { - if ($rows[$i]) { - my @colcont=split(/\&/,$rows[$i]); - my $avespan=$lcm/($#colcont+1); - for ($j=0;$j<=$#colcont;$j++) { - my $rid=$colcont[$j]; - if ($rid=~/^h(.+)/) { - $rid=$1; - $r->print('   '.$hash{'title_'.$rid}. - '
'); - } - } - } - } -# ----------------------------------------------------------------- Start table - $r->print('


'); - for ($i=0;$i<=$#rows;$i++) { - if ($rows[$i]) { - $r->print("\n"); - my @colcont=split(/\&/,$rows[$i]); - my $avespan=$lcm/($#colcont+1); - - # for each item I wish to print on this row... - for ($j=0;$j<=$#colcont;$j++) { - my $indent;my $indentstr; - my $linkid; - my $rid=$colcont[$j]; - $rid=~/(\d+)\.(\d+)$/; - my $src= - &Apache::lonnet::declutter($hash{'src_'.$1.'.'.$2}); - my $symb= - &Apache::lonnet::declutter($hash{'map_id_'.$1}).'___'.$2.'___'.$src; - my $add=''; - my $hwk=''; - my $hwke=''; - if ($rid=~/^l(\d+\.\d+),(.+)/) { $linkid=$1; $rid=$2; } - if ($rid=~/^i(\d+),(.+)/) { $indent=$1; $rid=$2; } - if ($rid=~/^h(.+)/) { - $rid=$1; - $add=''; - if (($ENV{'user.adv'}) && - ($parmhash{$symb.'.0.parameter_randompick'})) { - $adde=' (randomly select '. - $parmhash{$symb.'.0.parameter_randompick'}. - ')'; - } - } - if ($rid=~/^j(.+)/) { $rid=$1; } - if ($rid=~/^p(\d)(\d)\"([\w\: \(\)\/\,]*)\"(.+)/) { - # sub astatus describes what code/tcode mean - my $code=$1; - my $tcode=$2; - my $ctext=$3; - $rid=$4; - - # will open later - if ($tcode eq '1') { - $add=''; - } - } - $hwk=''; - $hwke=''; - if ($code eq '1') { - $hwke=' ('.$ctext.')'; - } - if ($code eq '2' || $code eq '4') { - $hwk=''; - $hwke=' ('.$ctext.')'; - } - if ($code eq '3') { - $hwk=''; - $hwke=' ('.$ctext.')'; - } - if ($networkFailedFlag) - { - $hwke=' (status unavailable)'; - } - } - if ($rid && $hash{'src_'.$rid} eq $currenturl) { - $add=$add.''. - '> '; - $adde= - ' <'.$adde; - } - if ($discussiontimes{$symb}>$lastcheck) { - $adde= - ''. - $adde; - } - if ($error{$src}) { - foreach (split(/\,/,$error{$src})) { - if ($_) { - $adde= - ' ' - .$adde; - } - } - } - if ($feedback{$src}) { - foreach (split(/\,/,$feedback{$src})) { - if ($_) { - $adde= - ' ' - .$adde; - } - } - } - if ($indent) { - my $is="  "; - for(my $i=-1;$i<$indent;$i++) { $indentstr.=$is; } - } - if (!$linkid) { $linkid=$rid; } - if ($hash{'randomout_'.$rid}) { - $adde=' (hidden)'.$adde; - } - $r->print($add.$indentstr); - if ($rid) { - $r->print(''. - $hwk.$hash{'title_'.$rid}.$hwke.''); - } - $r->print($adde); - } - $r->print(''); - } - } - $r->print("\n
'; - my $adde=''; - $adde=''; - } - - # solved/correct - if ($code eq '3') { - $add=''; - } elsif ($code eq '4') { # partially correct - $add=''; - } else { - # not attempted - - # we end up here on network failure, so pick a neutral - # color if the network failed instead of bright red. - if ( $networkFailedFlag ) - { - $add = ''; - } - else - { - $add=''; - } - - if ($tcode eq '2') { # open, not past due - $add=''; - } - - if ($tcode eq '4') { # due in next 24 hours - $add=''; - $adde='
"); - $r->print(''); -# -------------------------------------------------------------------- End page - } -# ------------------------------------------------------------- End render page - } -# ------------------------------------------------------------------ Untie hash - unless (untie(%hash)) { - &Apache::lonnet::logthis("WARNING: ". - "Could not untie coursemap $fn (browse)."); - } - unless (untie(%parmhash)) { - &Apache::lonnet::logthis("WARNING: ". - "Could not untie parmhash (browse)."); - } - return OK; -} - -sub new_handle { - my $r = shift; - # Handle header-only request if ($r->header_only) { if ($ENV{'browser.mathml'}) { @@ -837,19 +90,37 @@ sub new_handle { '')); $r->print(''); my $desc=$ENV{'course.'.$ENV{'request.course.id'}.'.description'}; - if (defined($desc)) { $r->print("

$desc

\n"); } + $r->print(''); my $date=localtime; - $r->print("

$date

\n"); - $r->rflush(); + $r->print(''); if ($navmap->{LAST_CHECK}) { - $r->print(' New discussion since '. + $r->print(''); } else { - $r->print(' Discussions'. - '
New message (click to open)

'); + $r->print('

'); + } + $r->print('
Key:    '. + ' New discussion since '. strftime("%A, %b %e at %I:%M %P", localtime($navmap->{LAST_CHECK})). - '
New message (click to open)

'); + '

  '. + ' New message (click to open)

'. + '

  '. + ' Discussions'. + '   New message (click to open)'. + '
'); + my $condition = 0; + if ($ENV{'form.condition'}) { + $condition = 1; } + if ($condition) { + $r->print('Close All Folders'); + } else { + $r->print('Open All Folders'); + } + + $r->print('
 '); + $r->rflush(); + # Now that we've displayed some stuff to the user, init the navmap $navmap->init(); @@ -929,17 +200,6 @@ sub new_handle { my $queryAdd = "postdata=" . &Apache::lonnet::escape($currenturl) . "&alreadyHere=1"; - my $condition = 0; - if ($ENV{'form.condition'}) { - $condition = 1; - } - - if ($condition) { - $r->print('Close All Folders

'); - } else { - $r->print('Open All Folders

'); - } - # Begin the HTML table # four cols: resource + indent, chat+feedback, icon, text string $r->print('' ."\n"); @@ -950,6 +210,10 @@ sub new_handle { # Here's a simple example of the iterator. # Preprocess the map: Look for current URL, force inlined maps to display + # This currently does very little... + #my $mapEventualIterator = Apache::lonnavmaps::iterator->new($navmap, undef, undef, {}, + #undef, $condition); + my $mapIterator = $navmap->getIterator(undef, undef, {}, 1); my $found = 0; my $depth = 1; @@ -961,12 +225,8 @@ sub new_handle { my $counter = 0; while ($depth > 0) { - if ($curRes == $mapIterator->BEGIN_MAP()) { - $depth++; - } - if ($curRes == $mapIterator->END_MAP()) { - $depth--; - } + if ($curRes == $mapIterator->BEGIN_MAP()) { $depth++; } + if ($curRes == $mapIterator->END_MAP()) { $depth--; } if (ref($curRes)) { $counter++; } @@ -1051,12 +311,8 @@ sub new_handle { if ($curRes == $mapIterator->BEGIN_BRANCH()) { $isNewBranch = 1; } - if ($curRes == $mapIterator->BEGIN_MAP()) { - $depth++; - } - if ($curRes == $mapIterator->END_MAP()) { - $depth--; - } + if ($curRes == $mapIterator->BEGIN_MAP()) { $depth++; } + if ($curRes == $mapIterator->END_MAP()) { $depth--; } if (ref($curRes)) { $counter++; } @@ -1287,6 +543,9 @@ sub new_handle { } $r->print(" $curMarkerBegin$title$partLabel $curMarkerEnd $nonLinkedText"); + #$r->print(" TDV:" . $curRes->{DATA}->{TOP_DOWN_VAL}); # temp + #$r->print(" BUV:" . $curRes->{DATA}->{BOT_UP_VAL}); # temp + #$r->print(" DD:" . $curRes->{DATA}->{DISPLAY_DEPTH}); # temp if ($curRes->{RESOURCE_ERROR}) { $r->print(&Apache::loncommon::help_open_topic ("Navmap_Host_Down", @@ -1649,7 +908,7 @@ sub new { } sub init { - my $self = $self; + my $self = shift; # If the course opt hash and the user opt hash should be generated, # generate them @@ -1663,7 +922,7 @@ sub init { my $userprefix=$uname.'_'.$udom.'_'; - my %courserdatas; my %useropt; my %courseopt; + my %courserdatas; my %useropt; my %courseopt; my %userrdatas; unless ($uhome eq 'no_host') { # ------------------------------------------------- Get coursedata (if present) unless ((time-$courserdatas{$cid.'.last_cache'})<240) { @@ -2028,8 +1287,10 @@ sub END_BRANCH { return 4; } # end of sub FORWARD { return 1; } # go forward sub BACKWARD { return 2; } -# Params: nav map, start resource, end resource, filter, condition, -# already seen hash ref +sub min { + (my $a, my $b) = @_; + if ($a < $b) { return $a; } else { return $b; } +} sub new { # magic invocation to create a class instance @@ -2057,9 +1318,273 @@ sub new { $self->{ALREADY_SEEN} = shift; if (!defined($self->{ALREADY_SEEN})) { $self->{ALREADY_SEEN} = {} }; $self->{CONDITION} = shift; + + # Now, we need to pre-process the map, by walking forward and backward + # over the parts of the map we're going to look at. + + # The processing steps are exactly the same, except for a few small + # changes, so I bundle those up in the following list of two elements: + # (direction_to_iterate, VAL_name, next_resource_method_to_call, + # first_resource). + # This prevents writing nearly-identical code twice. + my @iterations = ( [FORWARD(), 'TOP_DOWN_VAL', 'getNext', + 'FIRST_RESOURCE'], + [BACKWARD(), 'BOT_UP_VAL', 'getPrevious', + 'FINISH_RESOURCE'] ); + + my $maxDepth = 0; # tracks max depth + + foreach my $pass (@iterations) { + my $direction = $pass->[0]; + my $valName = $pass->[1]; + my $nextResourceMethod = $pass->[2]; + my $firstResourceName = $pass->[3]; + + my $iterator = Apache::lonnavmaps::DFSiterator->new($self->{NAV_MAP}, + $self->{FIRST_RESOURCE}, + $self->{FINISH_RESOURCE}, + {}, undef, 0, $direction); + + # prime the recursion + $self->{$firstResourceName}->{DATA}->{$valName} = 0; + my $depth = 0; + $iterator->next(); + my $curRes = $iterator->next(); + while ($depth > -1) { + if ($curRes == $iterator->BEGIN_MAP()) { $depth++; } + if ($curRes == $iterator->END_MAP()) { $depth--; } + + if (ref($curRes)) { + my $resultingVal = $curRes->{DATA}->{$valName}; + my $nextResources = $curRes->$nextResourceMethod(); + my $resourceCount = scalar(@{$nextResources}); + + if ($resourceCount == 1) { + my $current = $nextResources->[0]->{DATA}->{$valName} || 999999999; + $nextResources->[0]->{DATA}->{$valName} = min($resultingVal, $current); + } + + if ($resourceCount > 1) { + foreach my $res (@{$nextResources}) { + my $current = $res->{DATA}->{$valName} || 999999999; + $res->{DATA}->{$valName} = min($current, $resultingVal + 1); + } + } + } + + # Assign the final val + if (ref($curRes) && $direction == BACKWARD()) { + my $finalDepth = min($curRes->{DATA}->{TOP_DOWN_VAL}, + $curRes->{DATA}->{BOT_UP_VAL}); + + $curRes->{DATA}->{DISPLAY_DEPTH} = $finalDepth; + if ($finalDepth > $maxDepth) {$maxDepth = $finalDepth;} + } + $curRes = $iterator->next(); + } + } + + # Set up some bookkeeping information. + $self->{CURRENT_DEPTH} = 0; + $self->{MAX_DEPTH} = $maxDepth; + $self->{STACK} = []; + $self->{RECURSIVE_ITERATOR_FLAG} = 0; + + for (my $i = 0; $i <= $self->{MAX_DEPTH}; $i++) { + push @{$self->{STACK}}, []; + } + + # Prime the recursion w/ the first resource + push @{$self->{STACK}->[0]}, $self->{FIRST_RESOURCE}; + $self->{ALREADY_SEEN}->{$self->{FIRST_RESOURCE}->{ID}} = 1; + + bless ($self); + + return $self; +} + +sub next { + my $self = shift; + + if ($self->{RECURSIVE_ITERATOR_FLAG}) { + # grab the next from the recursive iterator + my $next = $self->{RECURSIVE_ITERATOR}->next(); + + # is it a begin or end map? If so, update the depth + if ($next == BEGIN_MAP() ) { $self->{RECURSIVE_DEPTH}++; } + if ($next == END_MAP() ) { $self->{RECURSIVE_DEPTH}--; } + + # Are we back at depth 0? If so, stop recursing + if ($self->{RECURSIVE_DEPTH} == 0) { + $self->{RECURSIVE_ITERATOR_FLAG} = 0; + } + + return $next; + } + + if (defined($self->{FORCE_NEXT})) { + my $tmp = $self->{FORCE_NEXT}; + $self->{FORCE_NEXT} = undef; + return $tmp; + } + + # Have we not yet begun? If not, return BEGIN_MAP and + # remember we've started. + if ( !$self->{STARTED} ) { + $self->{STARTED} = 1; + return $self->BEGIN_MAP(); + } + + # Here's the guts of the iterator. + + # Find the next resource, if any. + my $found = 0; + my $i = $self->{MAX_DEPTH}; + my $newDepth; + my $here; + while ( $i >= 0 && !$found ) { + if ( scalar(@{$self->{STACK}->[$i]}) > 0 ) { + $here = $self->{HERE} = shift @{$self->{STACK}->[$i]}; + $found = 1; + $newDepth = $i; + } + $i--; + } + + # If we still didn't find anything, we're done. + if ( !$found ) { + # We need to get back down to the correct branch depth + if ( $self->{CURRENT_DEPTH} > 0 ) { + $self->{CURRENT_DEPTH}--; + return END_BRANCH(); + } else { + return END_MAP(); + } + } + + # Get to the right level + if ( $self->{CURRENT_DEPTH} > $newDepth ) { + push @{$self->{STACK}->[$newDepth]}, $here; + $self->{CURRENT_DEPTH}--; + return END_BRANCH(); + } + if ( $self->{CURRENT_DEPTH} < $newDepth) { + push @{$self->{STACK}->[$newDepth]}, $here; + $self->{CURRENT_DEPTH}++; + return BEGIN_BRANCH(); + } + + # If we made it here, we have the next resource, and we're at the + # right branch level. So let's examine the resource for where + # we can get to from here. + + # So we need to look at all the resources we can get to from here, + # categorize them if we haven't seen them, remember if we have a new + my $nextUnfiltered = $here->getNext(); + + for (@$nextUnfiltered) { + if (!defined($self->{ALREADY_SEEN}->{$_->{ID}})) { + push @{$self->{STACK}->[$_->{DATA}->{DISPLAY_DEPTH}]}, $_; + $self->{ALREADY_SEEN}->{$_->{ID}} = 1; + } + } + + # That ends the main iterator logic. Now, do we want to recurse + # down this map (if this resource is a map)? + if ($self->{HERE}->is_map() && + (defined($self->{FILTER}->{$self->{HERE}->map_pc()}) xor $self->{CONDITION})) { + $self->{RECURSIVE_ITERATOR_FLAG} = 1; + my $firstResource = $self->{HERE}->map_start(); + my $finishResource = $self->{HERE}->map_finish(); + + $self->{RECURSIVE_ITERATOR} = + Apache::lonnavmaps::iterator->new($self->{NAV_MAP}, $firstResource, + $finishResource, $self->{FILTER}, + $self->{ALREADY_SEEN}, $self->{CONDITION}); + } + + return $self->{HERE}; + +} + +=pod + +The other method available on the iterator is B, which returns an array populated with the current 'stack' of maps, as references to the resource objects. Example: This is useful when making the navigation map, as we need to check whether we are under a page map to see if we need to link directly to the resource, or to the page. The first elements in the array will correspond to the top of the stack (most inclusive map). + +=cut + +sub getStack { + my $self=shift; + + my @stack; + + $self->populateStack(\@stack); + + return \@stack; +} + +# Private method: Calls the iterators recursively to populate the stack. +sub populateStack { + my $self=shift; + my $stack = shift; + + push @$stack, $self->{HERE} if ($self->{HERE}); + + if ($self->{RECURSIVE_ITERATOR_FLAG}) { + $self->{RECURSIVE_ITERATOR}->populateStack($stack); + } +} + +1; + +package Apache::lonnavmaps::DFSiterator; + +# Not documented in the perldoc: This is a simple iterator that just walks +# through the nav map and presents the resources in a depth-first search +# fashion, ignorant of conditionals, randomized resources, etc. It presents +# BEGIN_MAP and END_MAP, but does not understand branches at all. It is +# useful for pre-processing of some kind, and is in fact used by the main +# iterator that way, but that's about it. +# One could imagine merging this into the init routine of the main iterator, +# but this might as well be left seperate, since it is possible some other +# use might be found for it. - Jeremy + +sub BEGIN_MAP { return 1; } # begining of a new map +sub END_MAP { return 2; } # end of the map +sub FORWARD { return 1; } # go forward +sub BACKWARD { return 2; } + +# Params: Nav map ref, first resource id/ref, finish resource id/ref, +# filter hash ref (or undef), already seen hash or undef, condition +# (as in main iterator), direction FORWARD or BACKWARD (undef->forward). +sub new { + # magic invocation to create a class instance + my $proto = shift; + my $class = ref($proto) || $proto; + my $self = {}; + + $self->{NAV_MAP} = shift; + return undef unless ($self->{NAV_MAP}); + + $self->{FIRST_RESOURCE} = shift || $self->{NAV_MAP}->firstResource(); + $self->{FINISH_RESOURCE} = shift || $self->{NAV_MAP}->finishResource(); + + # If the given resources are just the ID of the resource, get the + # objects + if (!ref($self->{FIRST_RESOURCE})) { $self->{FIRST_RESOURCE} = + $self->{NAV_MAP}->getById($self->{FIRST_RESOURCE}); } + if (!ref($self->{FINISH_RESOURCE})) { $self->{FINISH_RESOURCE} = + $self->{NAV_MAP}->getById($self->{FINISH_RESOURCE}); } + + $self->{FILTER} = shift; + + # A hash, used as a set, of resource already seen + $self->{ALREADY_SEEN} = shift; + if (!defined($self->{ALREADY_SEEN})) { $self->{ALREADY_SEEN} = {} }; + $self->{CONDITION} = shift; $self->{DIRECTION} = shift || FORWARD(); - # Flag: Have we started yet? If not, the first action is to return BEGIN_MAP. + # Flag: Have we started yet? $self->{STARTED} = 0; # Should we continue calling the recursive iterator, if any? @@ -2073,38 +1598,25 @@ sub new { $self->{RECURSIVE_DEPTH} = 0; # For keeping track of our branches, we maintain our own stack - $self->{BRANCH_STACK} = []; - # If the size shrinks, we exhausted a branch - $self->{BRANCH_STACK_SIZE} = 0; - $self->{BRANCH_DEPTH} = 0; - - # For returning two things in a forced sequence - $self->{FORCE_NEXT} = undef; + $self->{STACK} = []; # Start with the first resource if ($self->{DIRECTION} == FORWARD) { - push @{$self->{BRANCH_STACK}}, $self->{FIRST_RESOURCE}; + push @{$self->{STACK}}, $self->{FIRST_RESOURCE}; } else { - push @{$self->{BRANCH_STACK}}, $self->{FINISH_RESOURCE}; + push @{$self->{STACK}}, $self->{FINISH_RESOURCE}; } - $self->{BRANCH_STACK_SIZE} = 1; bless($self); return $self; } -# Note... this function is *touchy*. I strongly recommend tracing -# through it with the debugger a few times on a non-trivial map before -# modifying it. Order is *everything*. sub next { my $self = shift; - # Iterator logic goes here - # Are we using a recursive iterator? If so, pull from that and # watch the depth; we want to resume our level at the correct time. - if ($self->{RECURSIVE_ITERATOR_FLAG}) - { + if ($self->{RECURSIVE_ITERATOR_FLAG}) { # grab the next from the recursive iterator my $next = $self->{RECURSIVE_ITERATOR}->next(); @@ -2120,23 +1632,10 @@ sub next { return $next; } - # Is this return value pre-determined? - if (defined($self->{FORCE_NEXT})) { - my $tmp = $self->{FORCE_NEXT}; - $self->{FORCE_NEXT} = undef; - return $tmp; - } - # Is there a current resource to grab? If not, then return - # END_BRANCH and END_MAP in succession. - if (scalar(@{$self->{BRANCH_STACK}}) == 0) { - if ($self->{BRANCH_DEPTH} > 0) { - $self->{FORCE_NEXT} = $self->END_MAP(); - $self->{BRANCH_DEPTH}--; - return $self->END_BRANCH(); - } else { - return $self->END_MAP(); - } + # END_MAP, which will end the iterator. + if (scalar(@{$self->{STACK}}) == 0) { + return $self->END_MAP(); } # Have we not yet begun? If not, return BEGIN_MAP and @@ -2146,40 +1645,10 @@ sub next { return $self->BEGIN_MAP; } - # Did the branch stack shrink since last run? If so, - # we exhausted a branch last time, therefore, we're about - # to start a new one. (We know because we already checked to see - # if the stack was empty.) - if ( scalar (@{$self->{BRANCH_STACK}}) < $self->{BRANCH_STACK_SIZE}) { - $self->{BRANCH_STACK_SIZE} = scalar(@{$self->{BRANCH_STACK}}); - $self->{BRANCH_DEPTH}++; - return $self->BEGIN_BRANCH(); - } - - # Remember the size for comparision next time. - $self->{BRANCH_STACK_SIZE} = scalar(@{$self->{BRANCH_STACK}}); - - # If the next resource we mean to return is going to need - # a lower branch level, terminate branches until we get - # there. - # Get the next resource in the branch - $self->{HERE} = pop @{$self->{BRANCH_STACK}}; + $self->{HERE} = pop @{$self->{STACK}}; - # Are we at the right depth? If not, close a branch and return - # the current resource onto the branch stack - if (defined($self->{HERE}->{DATA}->{ITERATOR_DEPTH}) - && $self->{HERE}->{DATA}->{ITERATOR_DEPTH} < - $self->{BRANCH_DEPTH} ) { - $self->{BRANCH_DEPTH}--; - # return it so we can pick it up eventually - push @{$self->{BRANCH_STACK}}, $self->{HERE}; - return $self->END_BRANCH(); - } - - # We always return it after this point and never before - # (proof: look at just the return statements), so we - # remember that we've seen this. + # remember that we've seen this, so we don't return it again later $self->{ALREADY_SEEN}->{$self->{HERE}->{ID}} = 1; # Get the next possible resources @@ -2192,40 +1661,16 @@ sub next { my $next = []; # filter the next possibilities to remove things we've - # already seen. Also, remember what branch depth they should - # be displayed at, since there's no other reliable way to tell. + # already seen. foreach (@$nextUnfiltered) { if (!defined($self->{ALREADY_SEEN}->{$_->{ID}})) { push @$next, $_; - $_->{DATA}->{ITERATOR_DEPTH} = - $self->{BRANCH_DEPTH} + 1; } } - # Handle branch cases: - # Nothing is available next: BRANCH_END - # 1 thing next: standard non-branch - # 2+ things next: have some branches - my $nextCount = scalar(@$next); - if ($nextCount == 0) { - # Return this and on the next run, close the branch up if we're - # in a branch - if ($self->{BRANCH_DEPTH} > 0 ) { - $self->{FORCE_NEXT} = $self->END_BRANCH(); - $self->{BRANCH_DEPTH}--; - } - } - while (@$next) { - # copy the next possibilities over to the branch stack - # in the right order - push @{$self->{BRANCH_STACK}}, shift @$next; - } - - if ($nextCount >= 2) { - $self->{FORCE_NEXT} = $self->BEGIN_BRANCH(); - $self->{BRANCH_DEPTH}++; - return $self->{HERE}; + # copy the next possibilities over to the stack + push @{$self->{STACK}}, shift @$next; } # If this is a map and we want to recurse down it... (not filtered out) @@ -2236,7 +1681,7 @@ sub next { my $finishResource = $self->{HERE}->map_finish(); $self->{RECURSIVE_ITERATOR} = - Apache::lonnavmaps::iterator->new ($self->{NAV_MAP}, $firstResource, + Apache::lonnavmaps::DFSiterator->new ($self->{NAV_MAP}, $firstResource, $finishResource, $self->{FILTER}, $self->{ALREADY_SEEN}, $self->{CONDITION}, $self->{DIRECTION}); } @@ -2244,34 +1689,6 @@ sub next { return $self->{HERE}; } -=pod - -The other method available on the iterator is B, which returns an array populated with the current 'stack' of maps, as references to the resource objects. Example: This is useful when making the navigation map, as we need to check whether we are under a page map to see if we need to link directly to the resource, or to the page. The first elements in the array will correspond to the top of the stack (most inclusive map). - -=cut - -sub getStack { - my $self=shift; - - my @stack; - - $self->populateStack(\@stack); - - return \@stack; -} - -# Private method: Calls the iterators recursively to populate the stack. -sub populateStack { - my $self=shift; - my $stack = shift; - - push @$stack, $self->{HERE} if ($self->{HERE}); - - if ($self->{RECURSIVE_ITERATOR_FLAG}) { - $self->{RECURSIVE_ITERATOR}->populateStack($stack); - } -} - 1; package Apache::lonnavmaps::resource;