--- loncom/interface/lonnavmaps.pm 2002/09/24 03:48:18 1.53 +++ loncom/interface/lonnavmaps.pm 2005/06/17 21:17:09 1.329 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Navigate Maps Handler # -# $Id: lonnavmaps.pm,v 1.53 2002/09/24 03:48:18 bowersj2 Exp $ +# $Id: lonnavmaps.pm,v 1.329 2005/06/17 21:17:09 albertel Exp $ # # Copyright Michigan State University Board of Trustees # @@ -25,1076 +25,2048 @@ # # http://www.lon-capa.org/ # -# (Page Handler -# -# (TeX Content Handler -# -# 05/29/00,05/30 Gerd Kortemeyer) -# 08/30,08/31,09/06,09/14,09/15,09/16,09/19,09/20,09/21,09/23, -# 10/02,10/10,10/14,10/16,10/18,10/19,10/31,11/6,11/14,11/16 Gerd Kortemeyer) -# -# 3/1/1,6/1,17/1,29/1,30/1,2/8,9/21,9/24,9/25 Gerd Kortemeyer -# YEAR=2002 -# 1/1 Gerd Kortemeyer -# +### package Apache::lonnavmaps; use strict; +use GDBM_File; use Apache::Constants qw(:common :http); -use Apache::lonnet(); use Apache::loncommon(); -use HTML::TokeParser; -use GDBM_File; - -# -------------------------------------------------------------- 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 +use Apache::lonmenu(); +use Apache::lonenc(); +use Apache::lonlocal; +use Apache::lonnet; +use POSIX qw (floor strftime); +use Data::Dumper; # for debugging, not always +use Time::HiRes qw( gettimeofday tv_interval ); + +# symbolic constants +sub SYMB { return 1; } +sub URL { return 2; } +sub NOTHING { return 3; } + +# Some data + +my $resObj = "Apache::lonnavmaps::resource"; + +# Keep these mappings in sync with lonquickgrades, which uses the colors +# instead of the icons. +my %statusIconMap = + ( + $resObj->CLOSED => '', + $resObj->OPEN => 'navmap.open.gif', + $resObj->CORRECT => 'navmap.correct.gif', + $resObj->INCORRECT => 'navmap.wrong.gif', + $resObj->ATTEMPTED => 'navmap.ellipsis.gif', + $resObj->ERROR => '' + ); + +my %iconAltTags = + ( 'navmap.correct.gif' => 'Correct', + 'navmap.wrong.gif' => 'Incorrect', + 'navmap.open.gif' => 'Open' ); + +# Defines a status->color mapping, null string means don't color +my %colormap = + ( $resObj->NETWORK_FAILURE => '', + $resObj->CORRECT => '', + $resObj->EXCUSED => '#3333FF', + $resObj->PAST_DUE_ANSWER_LATER => '', + $resObj->PAST_DUE_NO_ANSWER => '', + $resObj->ANSWER_OPEN => '#006600', + $resObj->OPEN_LATER => '', + $resObj->TRIES_LEFT => '', + $resObj->INCORRECT => '', + $resObj->OPEN => '', + $resObj->NOTHING_SET => '', + $resObj->ATTEMPTED => '', + $resObj->ANSWER_SUBMITTED => '' + ); +# And a special case in the nav map; what to do when the assignment +# is not yet done and due in less then 24 hours +my $hurryUpColor = "#FF0000"; + +sub launch_win { + my ($mode,$script,$toplinkitems)=@_; + my $result; + if ($script ne 'no') { + $result.=''; + } + if ($mode eq 'link') { + &add_linkitem($toplinkitems,'launchnav','launch_navmapwin()', + "Launch navigation window"); + } + return $result; +} + +sub close { + if ($env{'environment.remotenavmap'} ne 'on') { return ''; } + return(< +window.status='Accessing Nav Control'; +menu=window.open("/adm/rat/empty.html","loncapanav", + "height=600,width=400,scrollbars=1"); +window.status='Closing Nav Control'; +menu.close(); +window.status='Done.'; + +ENDCLOSE +} + +sub update { + if ($env{'environment.remotenavmap'} ne 'on') { return ''; } + if (!$env{'request.course.id'}) { return ''; } + if ($ENV{'REQUEST_URI'}=~m|^/adm/navmaps|) { return ''; } + return(< + +ENDUPDATE +} -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; +sub handler { + my $r = shift; + real_handler($r); } -# --------------------------------------------------------------------- Parmval +sub real_handler { + my $r = shift; + #my $t0=[&gettimeofday()]; + # Handle header-only request + if ($r->header_only) { + if ($env{'browser.mathml'}) { + &Apache::loncommon::content_type($r,'text/xml'); + } else { + &Apache::loncommon::content_type($r,'text/html'); + } + $r->send_http_header; + return OK; + } -# -------------------------------------------- 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 -# + # Send header, don't cache this page + if ($env{'browser.mathml'}) { + &Apache::loncommon::content_type($r,'text/xml'); + } else { + &Apache::loncommon::content_type($r,'text/html'); + } + &Apache::loncommon::no_cache($r); + $r->send_http_header; -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'}; + my %toplinkitems=(); + &add_linkitem(\%toplinkitems,'blank','',"Select Action"); + if ($ENV{QUERY_STRING} eq 'collapseExternal') { + &Apache::lonnet::put('environment',{'remotenavmap' => 'off'}); + &Apache::lonnet::appenv('environment.remotenavmap' => 'off'); + my $menu=&Apache::lonmenu::reopenmenu(); + my $navstatus=&Apache::lonmenu::get_nav_status(); + if ($menu) { + $menu=(<print(<<"ENDSUBM"); + $html + + + + + +ENDSUBM + return; + } + if ($ENV{QUERY_STRING} eq 'launchExternal') { + &Apache::lonnet::put('environment',{'remotenavmap' => 'on'}); + &Apache::lonnet::appenv('environment.remotenavmap' => 'on'); + } - unless ($symb) { return ''; } - my $result=''; + # Create the nav map + my $navmap = Apache::lonnavmaps::navmap->new(); - my ($mapname,$id,$fn)=split(/\_\_\_/,$symb); + if (!defined($navmap)) { + my $requrl = $r->uri; + $env{'user.error.msg'} = "$requrl:bre:0:0:Course not initialized"; + return HTTP_NOT_ACCEPTABLE; + } + my $html=&Apache::lonxml::xmlbegin(); + $r->print("$html\n"); + $r->print("".&mt('Navigate Course Contents').""); +# ------------------------------------------------------------ Get query string + &Apache::loncommon::get_unprocessed_cgi($ENV{'QUERY_STRING'},['register','sort','showOnlyHomework','postsymb']); + +# ----------------------------------------------------- Force menu registration + my $addentries=''; + my $more_unload; + my $body_only=''; + if ($env{'environment.remotenavmap'} eq 'on') { + $r->print(''); +# FIXME need to be smarter to only catch window close events +# $more_unload="collapse()" + $body_only=1; + } + if ($env{'form.register'}) { + $addentries=' onLoad="'.&Apache::lonmenu::loadevents(). + '" onUnload="'.&Apache::lonmenu::unloadevents().';'. + $more_unload.'"'; + $r->print(&Apache::lonmenu::registerurl(1)); + } else { + $addentries=' onUnload="'.$more_unload.'"'; + } -# ----------------------------------------------------- Cascading lookup scheme - my $rwhat=$what; - $what=~s/^parameter\_//; - $what=~s/\_/\./; + # Header + $r->print(''. + &Apache::loncommon::bodytag('Navigate Course Contents','', + $addentries,$body_only,'', + $env{'form.register'})); + $r->print(''); + + $r->rflush(); + + # Check that it's defined + if (!($navmap->courseMapDefined())) { + $r->print(&Apache::loncommon::help_open_menu('','Navigation Screen','Navigation_Screen','',undef,'RAT')); + $r->print('Coursemap undefined.' . + ''); + return OK; + } - my $symbparm=$symb.'.'.$what; - my $mapparm=$mapname.'___(all).'.$what; - my $usercourseprefix=$uname.'_'.$udom.'_'.$cid; + # See if there's only one map in the top-level, if we don't + # already have a filter... if so, automatically display it + # (older code; should use retrieveResources) + if ($ENV{QUERY_STRING} !~ /filter/) { + my $iterator = $navmap->getIterator(undef, undef, undef, 0); + my $curRes; + my $sequenceCount = 0; + my $sequenceId; + while ($curRes = $iterator->next()) { + if (ref($curRes) && $curRes->is_sequence()) { + $sequenceCount++; + $sequenceId = $curRes->map_pc(); + } + } + + if ($sequenceCount == 1) { + # The automatic iterator creation in the render call + # will pick this up. We know the condition because + # the defined($env{'form.filter'}) also ensures this + # is a fresh call. + $env{'form.filter'} = "$sequenceId"; + } + } - my $seclevel= $usercourseprefix.'.['.$csec.'].'.$what; - my $seclevelr=$usercourseprefix.'.['.$csec.'].'.$symbparm; - my $seclevelm=$usercourseprefix.'.['.$csec.'].'.$mapparm; + if ($ENV{QUERY_STRING} eq 'launchExternal') { + $r->print(' +
+
'); + $r->print(' + '); + } + + if ($env{'environment.remotenavmap'} ne 'on') { + $r->print(&launch_win('link','yes',\%toplinkitems)); + } + if ($env{'environment.remotenavmap'} eq 'on') { + &add_linkitem(\%toplinkitems,'closenav','collapse()', + "Close navigation window"); + } + + my $jumpToFirstHomework = 0; + # Check to see if the student is jumping to next open, do-able problem + if ($ENV{QUERY_STRING} =~ /^jumpToFirstHomework/) { + $jumpToFirstHomework = 1; + # Find the next homework problem that they can do. + my $iterator = $navmap->getIterator(undef, undef, undef, 1); + my $curRes; + my $foundDoableProblem = 0; + my $problemRes; + + while (($curRes = $iterator->next()) && !$foundDoableProblem) { + if (ref($curRes) && $curRes->is_problem()) { + my $status = $curRes->status(); + if ($curRes->completable()) { + $problemRes = $curRes; + $foundDoableProblem = 1; + + # Pop open all previous maps + my $stack = $iterator->getStack(); + pop @$stack; # last resource in the stack is the problem + # itself, which we don't need in the map stack + my @mapPcs = map {$_->map_pc()} @$stack; + $env{'form.filter'} = join(',', @mapPcs); - my $courselevel= $usercourseprefix.'.'.$what; - my $courselevelr=$usercourseprefix.'.'.$symbparm; - my $courselevelm=$usercourseprefix.'.'.$mapparm; + # Mark as both "here" and "jump" + $env{'form.postsymb'} = $curRes->symb(); + } + } + } -# ---------------------------------------------------------- first, check user - if ($uname) { - if ($useropt{$courselevelr}) { return $useropt{$courselevelr}; } - if ($useropt{$courselevelm}) { return $useropt{$courselevelm}; } - if ($useropt{$courselevel}) { return $useropt{$courselevel}; } + # If we found no problems, print a note to that effect. + if (!$foundDoableProblem) { + $r->print("All homework assignments have been completed.

"); + } + } else { + &add_linkitem(\%toplinkitems,'firsthomework', + 'location.href="navmaps?jumpToFirstHomework"', + "Show Me My First Homework Problem"); + } + + my $suppressEmptySequences = 0; + my $filterFunc = undef; + my $resource_no_folder_link = 0; + + # Display only due homework. + my $showOnlyHomework = 0; + if ($env{'form.showOnlyHomework'} eq "1") { + $showOnlyHomework = 1; + $suppressEmptySequences = 1; + $filterFunc = sub { my $res = shift; + return $res->completable() || $res->is_map(); + }; + &add_linkitem(\%toplinkitems,'everything', + 'location.href="navmaps?sort='.$env{'form.sort'}.'"', + "Show Everything"); + $r->print("

".&mt("Uncompleted Homework")."

"); + $env{'form.filter'} = ''; + $env{'form.condition'} = 1; + $resource_no_folder_link = 1; + } else { + &add_linkitem(\%toplinkitems,'uncompleted', + 'location.href="navmaps?sort='.$env{'form.sort'}. + '&showOnlyHomework=1"', + "Show Only Uncompleted Homework"); + } + + my %selected=($env{'form.sort'} => 'selected=on'); + my $sort_html=("
+ + + + + +
"); + # renderer call + my $renderArgs = { 'cols' => [0,1,2,3], + 'sort' => $env{'form.sort'}, + 'url' => '/adm/navmaps', + 'navmap' => $navmap, + 'suppressNavmap' => 1, + 'suppressEmptySequences' => $suppressEmptySequences, + 'filterFunc' => $filterFunc, + 'resource_no_folder_link' => $resource_no_folder_link, + 'sort_html'=> $sort_html, + 'r' => $r, + 'caller' => 'navmapsdisplay', + 'linkitems' => \%toplinkitems}; + my $render = render($renderArgs); + + # If no resources were printed, print a reassuring message so the + # user knows there was no error. + if ($renderArgs->{'counter'} == 0) { + if ($showOnlyHomework) { + $r->print("

".&mt("All homework is currently completed").".

"); + } else { # both jumpToFirstHomework and normal use the same: course must be empty + $r->print("

This course is empty.

"); + } } + #my $td=&tv_interval($t0); + #$r->print("
$td"); -# ------------------------------------------------------- second, check course - if ($csec) { - if ($courseopt{$seclevelr}) { return $courseopt{$seclevelr}; } - if ($courseopt{$seclevelm}) { return $courseopt{$seclevelm}; } - if ($courseopt{$seclevel}) { return $courseopt{$seclevel}; } - } + $r->print(""); + $r->rflush(); - if ($courseopt{$courselevelr}) { return $courseopt{$courselevelr}; } - if ($courseopt{$courselevelm}) { return $courseopt{$courselevelm}; } - if ($courseopt{$courselevel}) { return $courseopt{$courselevel}; } + return OK; +} -# ----------------------------------------------------- third, check map parms +# Convenience functions: Returns a string that adds or subtracts +# the second argument from the first hash, appropriate for the +# query string that determines which folders to recurse on +sub addToFilter { + my $hashIn = shift; + my $addition = shift; + my %hash = %$hashIn; + $hash{$addition} = 1; - my $thisparm=$parmhash{$symbparm}; - if ($thisparm) { return $thisparm; } + return join (",", keys(%hash)); +} -# ----------------------------------------------------- fourth , check default +sub removeFromFilter { + my $hashIn = shift; + my $subtraction = shift; + my %hash = %$hashIn; - my $default=&Apache::lonnet::metadata($fn,$rwhat.'.default'); - if ($default) { return $default} + delete $hash{$subtraction}; + return join(",", keys(%hash)); +} -# --------------------------------------------------- fifth , cascade up parts +# Convenience function: Given a stack returned from getStack on the iterator, +# return the correct src() value. +sub getLinkForResource { + my $stack = shift; + my $res; - 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 ($partgeneral) { return $partgeneral; } - } else { - my $resourcegeneral=&parmval("0.$qualifier",$symb); - if ($resourcegeneral) { return $resourcegeneral; } + # Check to see if there are any pages in the stack + foreach $res (@$stack) { + if (defined($res)) { + my $anchor; + if ($res->is_page()) { + foreach (@$stack) { if (defined($_)) { $anchor = $_; } } + $anchor=&Apache::lonnet::escape($anchor->shown_symb()); + return ($res->link(),$res->shown_symb(),$anchor); + } + # in case folder was skipped over as "only sequence" + my ($map,$id,$src)=&Apache::lonnet::decode_symb($res->symb()); + if ($map=~/\.page$/) { + my $url=&Apache::lonnet::clutter($map); + $anchor=&Apache::lonnet::escape($src->shown_symb()); + return ($url,$res->shown_symb(),$anchor); + } } } - return ''; + + # Failing that, return the src of the last resource that is defined + # (when we first recurse on a map, it puts an undefined resource + # on the bottom because $self->{HERE} isn't defined yet, and we + # want the src for the map anyhow) + foreach (@$stack) { + if (defined($_)) { $res = $_; } + } + + return ($res->link(),$res->shown_symb()); } +# Convenience function: This separates the logic of how to create +# the problem text strings ("Due: DATE", "Open: DATE", "Not yet assigned", +# etc.) into a separate function. It takes a resource object as the +# first parameter, and the part number of the resource as the second. +# It's basically a big switch statement on the status of the resource. +sub getDescription { + my $res = shift; + my $part = shift; + my $status = $res->status($part); -# ------------------------------------------------------------- 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}; - } + if ($status == $res->NETWORK_FAILURE) { + return &mt("Having technical difficulties; please check status later"); + } + if ($status == $res->NOTHING_SET) { + return &mt("Not currently assigned."); + } + if ($status == $res->OPEN_LATER) { + return "Open " . timeToHumanString($res->opendate($part)); + } + if ($status == $res->OPEN) { + if ($res->duedate($part)) { + return &mt("Due")." " .timeToHumanString($res->duedate($part)); + } else { + return &mt("Open, no due date"); } } - 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); - } + if ($status == $res->PAST_DUE_ANSWER_LATER) { + return &mt("Answer open")." " . timeToHumanString($res->answerdate($part)); + } + if ($status == $res->PAST_DUE_NO_ANSWER) { + return &mt("Was due")." " . timeToHumanString($res->duedate($part)); + } + if ($status == $res->ANSWER_OPEN) { + return &mt("Answer available"); + } + if ($status == $res->EXCUSED) { + return &mt("Excused by instructor"); + } + if ($status == $res->ATTEMPTED) { + return &mt("Answer submitted, not yet graded"); + } + if ($status == $res->TRIES_LEFT) { + my $tries = $res->tries($part); + my $maxtries = $res->maxtries($part); + my $triesString = ""; + if ($tries && $maxtries) { + $triesString = "($tries of $maxtries tries used)"; + if ($maxtries > 1 && $maxtries - $tries == 1) { + $triesString = "$triesString"; } - } 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'; + if ($res->duedate($part)) { + return &mt("Due")." " . timeToHumanString($res->duedate($part)) . + " $triesString"; } else { - if ($code==0) { $code=1; } + return &mt("No due date")." $triesString"; } } + if ($status == $res->ANSWER_SUBMITTED) { + return &mt('Answer submitted'); + } +} - return 'p'.$code.$tcode.'"'.$ctext.'"'; +# Convenience function, so others can use it: Is the problem due in less then +# 24 hours, and still can be done? + +sub dueInLessThan24Hours { + my $res = shift; + my $part = shift; + my $status = $res->status($part); + + return ($status == $res->OPEN() || + $status == $res->TRIES_LEFT()) && + $res->duedate($part) && $res->duedate($part) < time()+(24*60*60) && + $res->duedate($part) > time(); +} + +# Convenience function, so others can use it: Is there only one try remaining for the +# part, with more then one try to begin with, not due yet and still can be done? +sub lastTry { + my $res = shift; + my $part = shift; + + my $tries = $res->tries($part); + my $maxtries = $res->maxtries($part); + return $tries && $maxtries && $maxtries > 1 && + $maxtries - $tries == 1 && $res->duedate($part) && + $res->duedate($part) > time(); } +# This puts a human-readable name on the env variable. -sub addresource { - my ($resource,$sofar,$rid,$showtypes,$indent,$linkid)=@_; - if ($showtypes eq 'problems') { - if ($resource!~/\.(problem|exam|quiz|assess|survey|form)$/) { - return; - } +sub advancedUser { + return $env{'request.role.adv'}; +} + + +# timeToHumanString takes a time number and converts it to a +# human-readable representation, meant to be used in the following +# manner: +# print "Due $timestring" +# print "Open $timestring" +# print "Answer available $timestring" +# Very, very, very, VERY English-only... goodness help a localizer on +# this func... +sub timeToHumanString { + my ($time) = @_; + # zero, '0' and blank are bad times + if (!$time) { + return &mt('never'); + } + unless (&Apache::lonlocal::current_language()=~/^en/) { + return &Apache::lonlocal::locallocaltime($time); + } + my $now = time(); + + my @time = localtime($time); + my @now = localtime($now); + + # Positive = future + my $delta = $time - $now; + + my $minute = 60; + my $hour = 60 * $minute; + my $day = 24 * $hour; + my $week = 7 * $day; + my $inPast = 0; + + # Logic in comments: + # Is it now? (extremely unlikely) + if ( $delta == 0 ) { + return "this instant"; } - 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; - } - } + + if ($delta < 0) { + $inPast = 1; + $delta = -$delta; } -} -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; } + if ( $delta > 0 ) { + + my $tense = $inPast ? " ago" : ""; + my $prefix = $inPast ? "" : "in "; + + # Less then a minute + if ( $delta < $minute ) { + if ($delta == 1) { return "${prefix}1 second$tense"; } + return "$prefix$delta seconds$tense"; } - } - 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; + + # Less then an hour + if ( $delta < $hour ) { + # If so, use minutes + my $minutes = floor($delta / 60); + if ($minutes == 1) { return "${prefix}1 minute$tense"; } + return "$prefix$minutes minutes$tense"; + } + + # Is it less then 24 hours away? If so, + # display hours + minutes + if ( $delta < $hour * 24) { + my $hours = floor($delta / $hour); + my $minutes = floor(($delta % $hour) / $minute); + my $hourString = "$hours hours"; + my $minuteString = ", $minutes minutes"; + if ($hours == 1) { + $hourString = "1 hour"; + } + if ($minutes == 1) { + $minuteString = ", 1 minute"; + } + if ($minutes == 0) { + $minuteString = ""; } + return "$prefix$hourString$minuteString$tense"; } - $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; + # Less then 5 days away, display day of the week and + # HH:MM + if ( $delta < $day * 5 ) { + my $timeStr = strftime("%A, %b %e at %I:%M %P", localtime($time)); + $timeStr =~ s/12:00 am/00:00/; + $timeStr =~ s/12:00 pm/noon/; + return ($inPast ? "last " : "next ") . + $timeStr; } - 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=''; } + + # Is it this year? + if ( $time[5] == $now[5]) { + # Return on Month Day, HH:MM meridian + my $timeStr = strftime("on %A, %b %e at %I:%M %P", localtime($time)); + $timeStr =~ s/12:00 am/00:00/; + $timeStr =~ s/12:00 pm/noon/; + return $timeStr; } - } else { - &addresource($hash{'src_'.$rid},\$sofar,$rid,$showtypes, - $indent,$linkid); - } - if (defined($hash{'to_'.$rid})) { - $further=&followlinks($rid,$sofar,$beenhere,$further,$showtypes, - $indent,$linkid); + # Not this year, so show the year + my $timeStr = strftime("on %A, %b %e %Y at %I:%M %P", localtime($time)); + $timeStr =~ s/12:00 am/00:00/; + $timeStr =~ s/12:00 pm/noon/; + return $timeStr; } - - return $further; } -# ================================================================ Main Handler -sub handler { - my $r=shift; +=pod - &Apache::loncommon::get_unprocessed_cgi($ENV{QUERY_STRING}); +=head1 NAME - if ($ENV{'form.jtest'} ne "1") - { - return new_handle($r); - } +Apache::lonnavmap - Subroutines to handle and render the navigation + maps -# ------------------------------------------- Set document type for header only +=head1 SYNOPSIS - 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; - } +The main handler generates the navigational listing for the course, +the other objects export this information in a usable fashion for +other modules. + +=head1 OVERVIEW + +X When a user enters a course, LON-CAPA examines the +course structure and caches it in what is often referred to as the +"big hash" X. You can see it if you are logged into +LON-CAPA, in a course, by going to /adm/test. (You may need to +tweak the /home/httpd/lonTabs/htpasswd file to view it.) The +content of the hash will be under the heading "Big Hash". + +Big Hash contains, among other things, how resources are related +to each other (next/previous), what resources are maps, which +resources are being chosen to not show to the student (for random +selection), and a lot of other things that can take a lot of time +to compute due to the amount of data that needs to be collected and +processed. + +Apache::lonnavmaps provides an object model for manipulating this +information in a higher-level fashion then directly manipulating +the hash. It also provides access to several auxilary functions +that aren't necessarily stored in the Big Hash, but are a per- +resource sort of value, like whether there is any feedback on +a given resource. + +Apache::lonnavmaps also abstracts away branching, and someday, +conditions, for the times where you don't really care about those +things. + +Apache::lonnavmaps also provides fairly powerful routines for +rendering navmaps, and last but not least, provides the navmaps +view for when the user clicks the NAV button. + +B: Apache::lonnavmaps I works for the "currently +logged in user"; if you want things like "due dates for another +student" lonnavmaps can not directly retrieve information like +that. You need the EXT function. This module can still help, +because many things, such as the course structure, are constant +between users, and Apache::lonnavmaps can help by providing +symbs for the EXT call. + +The rest of this file will cover the provided rendering routines, +which can often be used without fiddling with the navmap object at +all, then documents the Apache::lonnavmaps::navmap object, which +is the key to accessing the Big Hash information, covers the use +of the Iterator (which provides the logic for traversing the +somewhat-complicated Big Hash data structure), documents the +Apache::lonnavmaps::Resource objects that are returned by + +=head1 Subroutine: render + +The navmap renderer package provides a sophisticated rendering of the +standard navigation maps interface into HTML. The provided nav map +handler is actually just a glorified call to this. + +Because of the large number of parameters this function accepts, +instead of passing it arguments as is normal, pass it in an anonymous +hash with the desired options. + +The package provides a function called 'render', called as +Apache::lonnavmaps::render({}). + +=head2 Overview of Columns + +The renderer will build an HTML table for the navmap and return +it. The table is consists of several columns, and a row for each +resource (or possibly each part). You tell the renderer how many +columns to create and what to place in each column, optionally using +one or more of the prepared columns, and the renderer will assemble +the table. + +Any additional generally useful column types should be placed in the +renderer code here, so anybody can use it anywhere else. Any code +specific to the current application (such as the addition of +elements in a column) should be placed in the code of the thing using +the renderer. + +At the core of the renderer is the array reference COLS (see Example +section below for how to pass this correctly). The COLS array will +consist of entries of one of two types of things: Either an integer +representing one of the pre-packaged column types, or a sub reference +that takes a resource reference, a part number, and a reference to the +argument hash passed to the renderer, and returns a string that will +be inserted into the HTML representation as it. -# ------------------------------------------------------------------- Hash tied +All other parameters are ways of either changing how the columns +are printing, or which rows are shown. - if ($ENV{'browser.mathml'}) { - $r->content_type('text/xml'); - } else { - $r->content_type('text/html'); - } - &Apache::loncommon::no_cache($r); - $r->send_http_header; +The pre-packaged column names are refered to by constants in the +Apache::lonnavmaps namespace. The following currently exist: - 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 { +=over 4 -# ----------------------------------------------------------------- Render page -# -------------------------------------------------------------- Set parameters +=item * B: +The general info about the resource: Link, icon for the type, etc. The +first column in the standard nav map display. This column provides the +indentation effect seen in the B