--- loncom/interface/lonnavmaps.pm 2002/11/08 18:44:02 1.99 +++ loncom/interface/lonnavmaps.pm 2004/09/16 16:56:37 1.291 @@ -1,8 +1,7 @@ - # The LearningOnline Network with CAPA # Navigate Maps Handler # -# $Id: lonnavmaps.pm,v 1.99 2002/11/08 18:44:02 bowersj2 Exp $ +# $Id: lonnavmaps.pm,v 1.291 2004/09/16 16:56:37 albertel Exp $ # # Copyright Michigan State University Board of Trustees # @@ -26,39 +25,145 @@ # # 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 Apache::Constants qw(:common :http); -use Apache::lonnet(); use Apache::loncommon(); -use GDBM_File; +use Apache::lonmenu(); +use Apache::lonlocal; use POSIX qw (floor strftime); +use Data::Dumper; # for debugging, not always used + +# 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 nav_control_js { + my $nav=($ENV{'environment.remotenavmap'} eq 'on'); + return (< + +ENDUPDATE +} sub handler { my $r = shift; + real_handler($r); +} - &Apache::loncommon::get_unprocessed_cgi($ENV{QUERY_STRING}); +sub real_handler { + my $r = shift; # Handle header-only request if ($r->header_only) { if ($ENV{'browser.mathml'}) { - $r->content_type('text/xml'); + &Apache::loncommon::content_type($r,'text/xml'); } else { - $r->content_type('text/html'); + &Apache::loncommon::content_type($r,'text/html'); } $r->send_http_header; return OK; @@ -66,18 +171,57 @@ sub handler { # Send header, don't cache this page if ($ENV{'browser.mathml'}) { - $r->content_type('text/xml'); + &Apache::loncommon::content_type($r,'text/xml'); } else { - $r->content_type('text/html'); + &Apache::loncommon::content_type($r,'text/html'); } &Apache::loncommon::no_cache($r); $r->send_http_header; - # Create the nav map the nav map - my $navmap = Apache::lonnavmaps::navmap->new( - $ENV{"request.course.fn"}.".db", - $ENV{"request.course.fn"}."_parms.db", 1, 1); + my %toplinkitems=(); + 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"); + + + + + + +ENDSUBM + return; + } + if ($ENV{QUERY_STRING} eq 'launchExternal') { + &Apache::lonnet::put('environment',{'remotenavmap' => 'on'}); + &Apache::lonnet::appenv('environment.remotenavmap' => 'on'); + } + + # Create the nav map + my $navmap = Apache::lonnavmaps::navmap->new(); if (!defined($navmap)) { my $requrl = $r->uri; @@ -85,539 +229,200 @@ sub handler { return HTTP_NOT_ACCEPTABLE; } - # Header - $r->print(&Apache::loncommon::bodytag('Navigate Course Map','', - '')); - $r->print(''); - my $desc=$ENV{'course.'.$ENV{'request.course.id'}.'.description'}; - $r->print(''); - my $date=localtime; - $r->print(''); - if ($navmap->{LAST_CHECK}) { - $r->print(''); - } else { - $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)

'. - '

  '. - ' Discussions'. - '   New message (click to open)'. - '
'); - my $condition = 0; - if ($ENV{'form.condition'}) { - $condition = 1; - } - - if ($condition) { - $r->print('Close All Folders'); + $r->print("\n"); + $r->print("".&mt('Navigate Course Contents').""); +# ------------------------------------------------------------ Get query string + &Apache::loncommon::get_unprocessed_cgi($ENV{'QUERY_STRING'},['register','sort','showOnlyHomework']); + +# ----------------------------------------------------- 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 { - $r->print('Open All Folders'); + $addentries=' onUnload="'.$more_unload.'"'; } - $r->print('
 '); + # Header + $r->print(''. + &Apache::loncommon::bodytag('Navigate Course Contents','', + $addentries,$body_only,'', + $ENV{'form.register'})); + $r->print(''); + $r->rflush(); - # Now that we've displayed some stuff to the user, init the navmap - $navmap->init(); - # 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; } - # Grab a resource object so we have access to the constants; this - # is technically not proper, but should be harmless - my $res = $navmap->firstResource(); - - # Defines a status->color mapping, null string means don't color - my %colormap = - ( $res->NETWORK_FAILURE => '', - $res->CORRECT => '', - $res->EXCUSED => '#BBBBFF', - $res->PAST_DUE_ANSWER_LATER => '', - $res->PAST_DUE_NO_ANSWER => '', - $res->ANSWER_OPEN => '#CCFFCC', - $res->OPEN_LATER => '', - $res->TRIES_LEFT => '', - $res->INCORRECT => '', - $res->OPEN => '', - $res->NOTHING_SET => '' ); - # 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"; - - my %statusIconMap = - ( $res->NETWORK_FAILURE => '', - $res->NOTHING_SET => '', - $res->CORRECT => 'navmap.correct.gif', - $res->EXCUSED => 'navmap.correct.gif', - $res->PAST_DUE_NO_ANSWER => 'navmap.wrong.gif', - $res->PAST_DUE_ANSWER_LATER => 'navmap.wrong.gif', - $res->ANSWER_OPEN => 'navmap.wrong.gif', - $res->OPEN_LATER => '', - $res->TRIES_LEFT => 'navmap.open.gif', - $res->INCORRECT => 'navmap.wrong.gif', - $res->OPEN => 'navmap.open.gif', - $res->ATTEMPTED => 'navmap.open.gif' ); - - my %iconAltTags = - ( 'navmap.correct.gif' => 'Correct', - 'navmap.wrong.gif' => 'Incorrect', - 'navmap.open.gif' => 'Open' ); - - my %condenseStatuses = - ( $res->NETWORK_FAILURE => 1, - $res->NOTHING_SET => 1, - $res->CORRECT => 1 ); - - my %filterHash; - # Figure out what we're not displaying - foreach (split(/\,/, $ENV{"form.filter"})) { - if ($_) { - $filterHash{$_} = "1"; - } - } - - # Is this a new-style course? If so, we want to suppress showing the top-level - # maps in their own folders, in favor of "inlining" them. - my $topResource = $navmap->getById("0.0"); - my $inlineTopLevelMaps = $topResource->src() =~ m|^/uploaded/.*default\.sequence$|; - - my $currenturl = $ENV{'form.postdata'}; - $currenturl=~s/^http\:\/\///; - $currenturl=~s/^[^\/]+//; - # alreadyHere allows us to only open the maps necessary to view - # the current location once, while at the same time remembering - # the current location. Without that check, the user would never - # be able to close those maps; the user would close it, and the - # currenturl scan would re-open it. - my $queryAdd = "postdata=" . &Apache::lonnet::escape($currenturl) . - "&alreadyHere=1"; - - # Begin the HTML table - # four cols: resource + indent, chat+feedback, icon, text string - $r->print('' ."\n"); - - # This needs to be updated to use symbs from the remote, - # instead of uris. The changes to this and the main rendering - # loop should be obvious. - # 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; - my $currentUrlIndex = 0; # keeps track of when the current resource is found, - # so we can back up a few and put the anchor above the - # current resource - $mapIterator->next(); # discard the first BEGIN_MAP - my $curRes = $mapIterator->next(); - my $counter = 0; - - while ($depth > 0) { - if ($curRes == $mapIterator->BEGIN_MAP()) { $depth++; } - if ($curRes == $mapIterator->END_MAP()) { $depth--; } - - if (ref($curRes)) { $counter++; } - - my $mapStack = $mapIterator->getStack(); - if ($currenturl && !$ENV{'form.alreadyHere'} && ref($curRes) && - $curRes->src() eq $currenturl) { - # If this is the correct resource, be sure to - # show it by making sure the containing maps - # are open. - - $currentUrlIndex = $counter; - - for my $map (@{$mapStack}) { - if ($condition) { - undef $filterHash{$map->map_pc()}; - } else { - $filterHash{$map->map_pc()} = 1; - } + # 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(); } - $ENV{'form.alreadyHere'} = 1; } - - # Preprocessing: If we're inlining nav maps into the top-level display, - # make sure we show this map! - if ($inlineTopLevelMaps && ref($curRes) && $curRes->is_map && - scalar(@{$mapStack}) == 1) { - if ($condition) { - undef $filterHash{$curRes->map_pc()}; - } else { - $filterHash{$curRes->map_pc()} = 1; - } + + 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"; } - - $curRes = $mapIterator->next(); } - - undef $res; # so we don't accidentally use it later - my $indentLevel = 0; - my $indentString = "\"\""; - my $isNewBranch = 0; - my $now = time(); - my $in24Hours = $now + 24 * 60 * 60; - my $displayedHereMarker = 0; - - # We know the first thing is a BEGIN_MAP (see "$self->{STARTED}" - # code in iterator->next), so ignore the first one - $mapIterator = $navmap->getIterator(undef, undef, \%filterHash, - $condition); - $mapIterator->next(); - $curRes = $mapIterator->next(); - my $deltadepth = 0; - $depth = 1; - - my @backgroundColors = ("#FFFFFF", "#F6F6F6"); - my $rowNum = 0; - - $counter = 0; - - while ($depth > 0) { - # If we're in a new style course, and this is a BEGIN_MAP, END_MAP, or - # map resource and the stack depth is only one, just plain ignore this resource - # entirely. (This has the effect of inlining the resources in that map - # in the nav map.) - if ($inlineTopLevelMaps && scalar(@{$mapIterator->getStack()}) == 1 && - ref($curRes) && $curRes->is_map()) { - # We let the normal depth stuff occur, but we need to shift everything - # over by one to the left to make it look right. - $deltadepth = -1; - $curRes = $mapIterator->next(); - next; - } - - if ($curRes == $mapIterator->BEGIN_MAP() || - $curRes == $mapIterator->BEGIN_BRANCH()) { - $indentLevel++; - } - if ($curRes == $mapIterator->END_MAP() || - $curRes == $mapIterator->END_BRANCH()) { - $indentLevel--; - } - if ($curRes == $mapIterator->BEGIN_BRANCH()) { - $isNewBranch = 1; - } - if ($curRes == $mapIterator->BEGIN_MAP()) { $depth++; } - if ($curRes == $mapIterator->END_MAP()) { $depth--; } - - if (ref($curRes)) { $counter++; } - - if ($depth == 1) { $deltadepth = 0; } # we're done shifting, because we're - # out of the inlined map - - # Is this resource being blotted out? - if (ref($curRes) && !advancedUser() && $curRes->randomout()) { - $curRes = $mapIterator->next(); - next; # and totally ignore this resource - } - - if (ref($curRes) && $curRes->src()) { - - # Step one: Decide which parts to show - my @parts = @{$curRes->parts()}; - my $multipart = scalar(@parts) > 1; - my $condensed = 0; - - if ($curRes->is_problem()) { - # Is it multipart? + 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); - if ($multipart) { - # If it's multipart, see if part 0 is "open" - # if it is, display all parts, if it isn't, - # just display first - if (!$curRes->opendate("0")) { - @parts = ("0"); # just display the zero-th part - $condensed = 1; - } else { - # Otherwise, only display part 0 if we want to - # attach feedback or email information to it - if ($curRes->hasDiscussion() || $curRes->getFeedback()) { - shift @parts; - } else { - # If there's discussion or feedback, that counts - # as a difference, so display the parts. - - # Now, we decide whether to condense the - # parts due to similarity - my $status = $curRes->status($parts[1]); - my $due = $curRes->duedate($parts[1]); - my $open = $curRes->opendate($parts[1]); - my $statusAllSame = 1; - my $dueAllSame = 1; - my $openAllSame = 1; - for (my $i = 2; $i < scalar(@parts); $i++) { - if ($curRes->status($parts[$i]) != $status){ - $statusAllSame = 0; - } - if ($curRes->duedate($parts[$i]) != $due ) { - $dueAllSame = 0; - } - if ($curRes->opendate($parts[$i]) != $open) { - $openAllSame = 0; - } - } - - # $allSame is true if all the statuses were - # the same. Now, if they are all the same and - # match one of the statuses to condense, or they - # are all open with the same due date, or they are - # all OPEN_LATER with the same open date, display the - # status of the first non-zero part (to get the 'correct' - # status right, since 0 is never 'correct' or 'open'). - if (($statusAllSame && defined($condenseStatuses{$status})) || - ($dueAllSame && $status == $curRes->OPEN && $statusAllSame)|| - ($openAllSame && $status == $curRes->OPEN_LATER && $statusAllSame) ){ - @parts = ($parts[1]); - $condensed = 1; - } - } - } + # Mark as both "here" and "jump" + $ENV{'form.postsymb'} = $curRes->symb(); } - - } else { - $parts[0] = "0"; # this is to get past foreach loop below - # you can consider a non-problem resource as a resource - # with only one part without loss - } - - # Is it a multipart problem with a single part, now in - # @parts with "0" filtered out? If so, forget it's a multi-part - # problem and treat it like a single-part problem. - if ( scalar(@parts) == 1 ) { - $multipart = 0; } + } - # Display one part, in event of network error. - # If this is a single part, we can at least show the correct - # status, but if it's multipart, we're lost. - if ($curRes->{RESOURCE_ERROR}) { - @parts = ("0"); - } - - # Step two: Show the parts - foreach my $part (@parts) { - - my $deltalevel = 0; # for inserting the branch icon - my $nonLinkedText = ""; # unlinked stuff after title - - # For each thing we're displaying... - - my $stack = $mapIterator->getStack(); - my $src = getLinkForResource($stack); - - my $srcHasQuestion = $src =~ /\?/; - my $link = $src. - ($srcHasQuestion?'&':'?') . - 'symb='.&Apache::lonnet::escape($curRes->symb()). - '"'; - my $title = $curRes->title(); - if (!$title) { - $title = $curRes->src(); - $title = substr ($title, rindex($title, "/") + 1); - } - my $partLabel = ""; - my $newBranchText = ""; - - # If this is a new branch, label it so - # (temporary, this should be an icon w/ alt text) - if ($isNewBranch) { - $newBranchText = ""; - $isNewBranch = 0; - $deltalevel = 1; - } - - # links to open and close the folders - my $linkopen = ""; - my $linkclose = ""; - - my $icon = "\"\""; - if ($curRes->is_problem()) { - if ($part eq "0" || $condensed) { - $icon = ''; - } else { - $icon = $indentString; - } - } - - # Display the correct icon, link to open or shut map - if ($curRes->is_map()) { - my $mapId = $curRes->map_pc(); - my $nowOpen = (!defined($filterHash{$mapId})); - if ($condition) {$nowOpen = !$nowOpen;} - $icon = $nowOpen ? - "navmap.folder.closed.gif" : "navmap.folder.open.gif"; - $icon = "\"\""; - $linkopen = ""; - $linkclose = ""; - - } - - my $colorizer = ""; - my $color; - if ($curRes->is_problem()) { - my $status = $curRes->status($part); - $color = $colormap{$status}; - - # Special case in the navmaps: If in less then - # 24 hours, give it a bit of urgency - if (($status == $curRes->OPEN() || $status == $curRes->ATTEMPTED() || - $status == $curRes->TRIES_LEFT()) - && $curRes->duedate() && - $curRes->duedate() < time()+(24*60*60) && - $curRes->duedate() > time()) { - $color = $hurryUpColor; - } - # Special case: If this is the last try, and there is - # more then one available, and it's not due yet, give a bit of urgency - my $tries = $curRes->tries($part); - my $maxtries = $curRes->maxtries($part); - if ($tries && $maxtries && $maxtries > 1 && - $maxtries - $tries == 1 && $curRes->duedate() && - $curRes->duedate() > time()) { - $color = $hurryUpColor; - } - if ($color ne "") { - $colorizer = "bgcolor=\"$color\""; - } - } - - if ($curRes->randomout()) { - $nonLinkedText .= ' (hidden) '; - } - - $rowNum++; - my $backgroundColor = $backgroundColors[$rowNum % scalar(@backgroundColors)]; - - # FIRST COL: The resource indentation, branch icon, name, and anchor - $r->print(" "); - - # Is this the first displayed part of a multi-part problem - # that has not been condensed, so we should suppress these two - # columns? - my $firstDisplayed = !$condensed && $multipart && $part eq "0"; - - # THIRD COL: Problem status icon - if ($curRes->is_problem() && - !$firstDisplayed) { - my $icon = $statusIconMap{$curRes->status($part)}; - my $alt = $iconAltTags{$icon}; - if ($icon) { - $r->print("\n"); - } else { - $r->print("\n"); - } - } else { # not problem, no icon - $r->print("\n"); - } - - # FOURTH COL: Text description - #$r->print("\n"); - } + # 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.

"); } - $curRes = $mapIterator->next(); } - $r->print("
\n"); - - # anchor for current resource... - 5 is deliberate: If it's that - # high on the screen, don't bother focusing on it. Also this will - # print multiple anchors if this is an expanded multi-part problem... - # who cares? - if ($counter == $currentUrlIndex - 5) { - $r->print(''); - } - - # print indentation - for (my $i = 0; $i < $indentLevel - $deltalevel + $deltadepth; $i++) { - $r->print($indentString); - } - - $r->print(" ${newBranchText}${linkopen}$icon${linkclose}\n"); - - my $curMarkerBegin = ""; - my $curMarkerEnd = ""; - - # Is this the current resource? - if ($curRes->src() eq $currenturl && !$displayedHereMarker) { - $curMarkerBegin = '> '; - $curMarkerEnd = ' <'; - $displayedHereMarker = 1; - } - - if ($curRes->is_problem() && $part ne "0" && !$condensed) { - $partLabel = " (Part $part)"; - $title = ""; - } - if ($multipart && $condensed) { - $nonLinkedText .= ' (' . $curRes->countParts() . ' parts)'; - } - - $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", - 'Host down')); - } - - my $discussionHTML = ""; my $feedbackHTML = ""; - - # SECOND COL: Is there text, feedback, errors?? - if ($curRes->hasDiscussion()) { - $discussionHTML = $linkopen . - '' . - $linkclose; - } - - if ($curRes->getFeedback()) { - my $feedback = $curRes->getFeedback(); - foreach (split(/\,/, $feedback)) { - if ($_) { - $feedbackHTML .= ' ' - . ''; - } - } - } - - $r->print("$discussionHTML$feedbackHTML $linkopen\"$alt\"$linkclose  \n"); - $r->print("\n"); - - if ($curRes->kind() eq "res" && - $curRes->is_problem() && - !$firstDisplayed) { - $r->print ("") if ($color); - $r->print (getDescription($curRes, $part)); - $r->print ("") if ($color); - } - if ($curRes->is_map() && advancedUser() && $curRes->randompick()) { - $r->print('(randomly select ' . $curRes->randompick() .')'); - } + # 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="locatnavmaps?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); + $navmap->untieHashes(); - $r->print(" 
"); - - $navmap->untieHashes(); + $r->print(""); + $r->rflush(); return OK; } @@ -652,8 +457,16 @@ sub getLinkForResource { # Check to see if there are any pages in the stack foreach $res (@$stack) { - if (defined($res) && $res->is_page()) { - return $res->src(); + if (defined($res)) { + if ($res->is_page()) { + return $res->src(); + } + # in case folder was skipped over as "only sequence" + my ($map,$id,$src)=&Apache::lonnet::decode_symb($res->symb()); + if ($map=~/\.page$/) { + return &Apache::lonnet::clutter($map).'#'. + &Apache::lonnet::escape(&Apache::lonnet::declutter($src)); + } } } @@ -668,9 +481,9 @@ sub getLinkForResource { return $res->src(); } -# Convenience function: This seperates the logic of how to create +# Convenience function: This separates the logic of how to create # the problem text strings ("Due: DATE", "Open: DATE", "Not yet assigned", -# etc.) into a seperate function. It takes a resource object as the +# 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. @@ -679,34 +492,36 @@ sub getDescription { my $part = shift; my $status = $res->status($part); - if ($status == $res->NETWORK_FAILURE) { return ""; } + if ($status == $res->NETWORK_FAILURE) { + return &mt("Having technical difficulties; please check status later"); + } if ($status == $res->NOTHING_SET) { - return "Not currently assigned."; + 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 "Due " . timeToHumanString($res->duedate($part)); + return &mt("Due")." " .timeToHumanString($res->duedate($part)); } else { - return "Open, no due date"; + return &mt("Open, no due date"); } } if ($status == $res->PAST_DUE_ANSWER_LATER) { - return "Answer open " . timeToHumanString($res->answerdate($part)); + return &mt("Answer open")." " . timeToHumanString($res->answerdate($part)); } if ($status == $res->PAST_DUE_NO_ANSWER) { - return "Was due " . timeToHumanString($res->duedate($part)); + return &mt("Was due")." " . timeToHumanString($res->duedate($part)); } if ($status == $res->ANSWER_OPEN) { - return "Answer available"; + return &mt("Answer available"); } if ($status == $res->EXCUSED) { - return "Excused by instructor"; + return &mt("Excused by instructor"); } if ($status == $res->ATTEMPTED) { - return "Not yet graded."; + return &mt("Answer submitted, not yet graded"); } if ($status == $res->TRIES_LEFT) { my $tries = $res->tries($part); @@ -718,17 +533,49 @@ sub getDescription { $triesString = "$triesString"; } } - if ($res->duedate()) { - return "Due " . timeToHumanString($res->duedate($part)) . + if ($res->duedate($part)) { + return &mt("Due")." " . timeToHumanString($res->duedate($part)) . " $triesString"; } else { - return "No due date $triesString"; + return &mt("No due date")." $triesString"; } } + if ($status == $res->ANSWER_SUBMITTED) { + return &mt('Answer submitted'); + } } +# 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 advancedUser { - return $ENV{'user.adv'}; + return $ENV{'request.role.adv'}; } @@ -744,9 +591,11 @@ sub timeToHumanString { my ($time) = @_; # zero, '0' and blank are bad times if (!$time) { - return 'never'; + return &mt('never'); } - + unless (&Apache::lonlocal::current_language()=~/^en/) { + return &Apache::lonlocal::locallocaltime($time); + } my $now = time(); my @time = localtime($time); @@ -772,17 +621,18 @@ sub timeToHumanString { $delta = -$delta; } - # Is it in the future? if ( $delta > 0 ) { - # Is it less then a minute away? + 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"; } - # Is it less then an hour away? + # Less then an hour if ( $delta < $hour ) { # If so, use minutes my $minutes = floor($delta / 60); @@ -813,7 +663,7 @@ sub timeToHumanString { # HH:MM if ( $delta < $day * 5 ) { my $timeStr = strftime("%A, %b %e at %I:%M %P", localtime($time)); - $timeStr =~ s/12:00 am/midnight/; + $timeStr =~ s/12:00 am/00:00/; $timeStr =~ s/12:00 pm/noon/; return ($inPast ? "last " : "next ") . $timeStr; @@ -823,42 +673,1356 @@ sub timeToHumanString { 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/midnight/; + $timeStr =~ s/12:00 am/00:00/; $timeStr =~ s/12:00 pm/noon/; return $timeStr; } # Not this year, so show the year - my $timeStr = strftime("on %A, %b %e %G at %I:%M %P", localtime($time)); - $timeStr =~ s/12:00 am/midnight/; + 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; } } + +=pod + +=head1 NAME + +Apache::lonnavmap - Subroutines to handle and render the navigation + maps + +=head1 SYNOPSIS + +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. + +All other parameters are ways of either changing how the columns +are printing, or which rows are shown. + +The pre-packaged column names are refered to by constants in the +Apache::lonnavmaps namespace. The following currently exist: + +=over 4 + +=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