--- loncom/interface/lonnavmaps.pm 2003/08/07 14:29:43 1.220 +++ loncom/interface/lonnavmaps.pm 2004/10/04 21:35:52 1.299 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Navigate Maps Handler # -# $Id: lonnavmaps.pm,v 1.220 2003/08/07 14:29:43 bowersj2 Exp $ +# $Id: lonnavmaps.pm,v 1.299 2004/10/04 21:35:52 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -25,20 +25,7 @@ # # 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 -# Oct-Nov Jeremy Bowers -# YEAR=2003 -# Jeremy Bowers ... lots of days +### package Apache::lonnavmaps; @@ -46,6 +33,7 @@ use strict; use Apache::Constants qw(:common :http); use Apache::loncommon(); use Apache::lonmenu(); +use Apache::lonlocal; use POSIX qw (floor strftime); use Data::Dumper; # for debugging, not always used @@ -61,19 +49,14 @@ my $resObj = "Apache::lonnavmaps::resour # Keep these mappings in sync with lonquickgrades, which uses the colors # instead of the icons. my %statusIconMap = - ( $resObj->NETWORK_FAILURE => '', - $resObj->NOTHING_SET => '', - $resObj->CORRECT => 'navmap.correct.gif', - $resObj->EXCUSED => 'navmap.correct.gif', - $resObj->PAST_DUE_NO_ANSWER => 'navmap.wrong.gif', - $resObj->PAST_DUE_ANSWER_LATER => 'navmap.wrong.gif', - $resObj->ANSWER_OPEN => 'navmap.wrong.gif', - $resObj->OPEN_LATER => '', - $resObj->TRIES_LEFT => 'navmap.open.gif', - $resObj->INCORRECT => 'navmap.wrong.gif', - $resObj->OPEN => 'navmap.open.gif', - $resObj->ATTEMPTED => 'navmap.ellipsis.gif', - $resObj->ANSWER_SUBMITTED => 'navmap.ellipsis.gif' ); + ( + $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', @@ -100,6 +83,73 @@ my %colormap = # 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); @@ -111,9 +161,9 @@ sub real_handler { # 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; @@ -121,16 +171,57 @@ sub real_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 - my $navmap = Apache::lonnavmaps::navmap->new(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; @@ -139,33 +230,45 @@ sub real_handler { } $r->print("\n"); - $r->print("Navigate Course Contents"); + $r->print("".&mt('Navigate Course Contents').""); # ------------------------------------------------------------ Get query string - &Apache::loncommon::get_unprocessed_cgi($ENV{'QUERY_STRING'},['register']); + &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().'"'; - $r->print(&Apache::lonmenu::registerurl(1)); + $addentries=' onLoad="'.&Apache::lonmenu::loadevents(). + '" onUnload="'.&Apache::lonmenu::unloadevents().';'. + $more_unload.'"'; + $r->print(&Apache::lonmenu::registerurl(1)); + } else { + $addentries=' onUnload="'.$more_unload.'"'; } # Header $r->print(''. &Apache::loncommon::bodytag('Navigate Course Contents','', - $addentries,'','',$ENV{'form.register'})); + $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(); - + $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; @@ -173,23 +276,17 @@ sub real_handler { # 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 $depth = 1; - $iterator->next(); - my $curRes = $iterator->next(); + my $curRes; my $sequenceCount = 0; my $sequenceId; - while ($depth > 0) { - if ($curRes == $iterator->BEGIN_MAP()) { $depth++; } - if ($curRes == $iterator->END_MAP()) { $depth--; } - + while ($curRes = $iterator->next()) { if (ref($curRes) && $curRes->is_sequence()) { $sequenceCount++; $sequenceId = $curRes->map_pc(); } - - $curRes = $iterator->next(); } if ($sequenceCount == 1) { @@ -201,22 +298,36 @@ sub real_handler { } } + 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} eq 'jumpToFirstHomework') { + 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 $depth = 1; - $iterator->next(); - my $curRes = $iterator->next(); + my $curRes; my $foundDoableProblem = 0; my $problemRes; - while ($depth > 0 && !$foundDoableProblem) { - if ($curRes == $iterator->BEGIN_MAP()) { $depth++; } - if ($curRes == $iterator->END_MAP()) { $depth--; } - + while (($curRes = $iterator->next()) && !$foundDoableProblem) { if (ref($curRes) && $curRes->is_problem()) { my $status = $curRes->status(); if ($curRes->completable()) { @@ -234,8 +345,6 @@ sub real_handler { $ENV{'form.postsymb'} = $curRes->symb(); } } - } continue { - $curRes = $iterator->next(); } # If we found no problems, print a note to that effect. @@ -243,8 +352,9 @@ sub real_handler { $r->print("All homework assignments have been completed.

"); } } else { - $r->print("" . - "Go To My First Homework Problem    "); + &add_linkitem(\%toplinkitems,'firsthomework', + 'location.href="navmaps?jumpToFirstHomework"', + "Show Me My First Homework Problem"); } my $suppressEmptySequences = 0; @@ -253,30 +363,52 @@ sub real_handler { # Display only due homework. my $showOnlyHomework = 0; - if ($ENV{QUERY_STRING} eq 'showOnlyHomework') { + if ($ENV{'form.showOnlyHomework'} eq "1") { $showOnlyHomework = 1; $suppressEmptySequences = 1; $filterFunc = sub { my $res = shift; return $res->completable() || $res->is_map(); }; - $r->print("

Uncompleted Homework

"); + &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 { - $r->print("" . - "Show Only Uncompleted Homework    "); - } - + &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, - 'r' => $r}; + 'sort_html'=> $sort_html, + 'r' => $r, + 'caller' => 'navmapsdisplay', + 'linkitems' => \%toplinkitems}; my $render = render($renderArgs); $navmap->untieHashes(); @@ -284,7 +416,7 @@ sub real_handler { # user knows there was no error. if ($renderArgs->{'counter'} == 0) { if ($showOnlyHomework) { - $r->print("

All homework is currently completed.

"); + $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.

"); } @@ -326,8 +458,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)); + } } } @@ -342,9 +482,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. @@ -354,35 +494,35 @@ sub getDescription { my $status = $res->status($part); if ($status == $res->NETWORK_FAILURE) { - return "Having technical difficulties; please check status later"; + 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 "Answer submitted, not yet graded."; + return &mt("Answer submitted, not yet graded"); } if ($status == $res->TRIES_LEFT) { my $tries = $res->tries($part); @@ -394,30 +534,30 @@ 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 '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 dueInLessThen24Hours { +sub dueInLessThan24Hours { my $res = shift; my $part = shift; my $status = $res->status($part); return ($status == $res->OPEN() || $status == $res->TRIES_LEFT()) && - $res->duedate() && $res->duedate() < time()+(24*60*60) && - $res->duedate() > time(); + $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 @@ -429,8 +569,8 @@ sub lastTry { my $tries = $res->tries($part); my $maxtries = $res->maxtries($part); return $tries && $maxtries && $maxtries > 1 && - $maxtries - $tries == 1 && $res->duedate() && - $res->duedate() > time(); + $maxtries - $tries == 1 && $res->duedate($part) && + $res->duedate($part) > time(); } # This puts a human-readable name on the ENV variable. @@ -452,9 +592,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); @@ -522,7 +664,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; @@ -532,14 +674,14 @@ 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; } @@ -678,13 +820,13 @@ can't close or open folders when this is =back -=item B: +=item * B: Whether there is discussion on the resource, email for the user, or (lumped in here) perl errors in the execution of the problem. This is the second column in the main nav map. -=item B: +=item * B: An icon for the status of a problem, with five possible states: Correct, incorrect, open, awaiting grading (for a problem where the @@ -692,11 +834,24 @@ computer's grade is suppressed, or the c essay problem), or none (not open yet, not a problem). The third column of the standard navmap. -=item B: +=item * B: A text readout of the details of the current status of the problem, such as "Due in 22 hours". The fourth column of the standard navmap. +=item * B: + +A text readout summarizing the status of the problem. If it is a +single part problem, will display "Correct", "Incorrect", +"Not yet open", "Open", "Attempted", or "Error". If there are +multiple parts, this will output a string that in HTML will show a +status of how many parts are in each status, in color coding, trying +to match the colors of the icons within reason. + +Note this only makes sense if you are I showing parts. If +C is true (see below), this column will not output +anything. + =back If you add any others please be sure to document them here. @@ -864,8 +1019,7 @@ sub resource { return 0; } sub communication_status { return 1; } sub quick_status { return 2; } sub long_status { return 3; } - -# Data for render_resource +sub part_status_summary { return 4; } sub render_resource { my ($resource, $part, $params) = @_; @@ -878,16 +1032,13 @@ sub render_resource { my $filter = $it->{FILTER}; my $title = $resource->compTitle(); - if ($src =~ /^\/uploaded\//) { - $nonLinkedText=$title; - $title = ''; - } + my $partLabel = ""; my $newBranchText = ""; - + my $location=&Apache::loncommon::lonhttpdurl("/adm/lonIcons"); # If this is a new branch, label it so if ($params->{'isNewBranch'}) { - $newBranchText = ""; + $newBranchText = ""; } # links to open and close the folder @@ -895,21 +1046,16 @@ sub render_resource { my $linkclose = ""; # Default icon: unknown page - my $icon = ""; + my $icon = ""; if ($resource->is_problem()) { if ($part eq '0' || $params->{'condensed'}) { - $icon = ''; + $icon =''; } else { $icon = $params->{'indentString'}; } } else { - my $curfext= (split (/\./,$resource->src))[-1]; - my $embstyle = &Apache::loncommon::fileembstyle($curfext); - # The unless conditional that follows is a bit of overkill - if (!(!defined($embstyle) || $embstyle eq 'unk' || $embstyle eq 'hdn')) { - $icon = ""; - } + $icon = ""; } # Display the correct map icon to open or shut map @@ -924,7 +1070,7 @@ sub render_resource { if (!$params->{'resource_no_folder_link'}) { $icon = "navmap.$folderType." . ($nowOpen ? 'closed' : 'open') . '.gif'; - $icon = ""; + $icon = ""; $linkopen = "{'queryString'} . '&filter='; @@ -941,7 +1087,7 @@ sub render_resource { # Don't allow users to manipulate folder $icon = "navmap.$folderType." . ($nowOpen ? 'closed' : 'open') . '.nomanip.gif'; - $icon = ""; + $icon = ""; $linkopen = ""; $linkclose = ""; @@ -979,7 +1125,9 @@ sub render_resource { if ($resource->is_problem() && $part ne '0' && !$params->{'condensed'}) { - $partLabel = " (Part $part)"; + my $displaypart=$resource->part_display($part); + $partLabel = " (Part: $displaypart)"; + $link.='#'.&Apache::lonnet::escape($part); $title = ""; } @@ -987,9 +1135,12 @@ sub render_resource { $nonLinkedText .= ' (' . $resource->countParts() . ' parts)'; } - if (!$params->{'resource_nolink'} && $src !~ /^\/uploaded\// && - !$resource->is_sequence()) { - $result .= " $curMarkerBegin$title$partLabel$curMarkerEnd $nonLinkedText"; + my $target; + if ($ENV{'environment.remotenavmap'} eq 'on') { + $target=' target="loncapaclient" '; + } + if (!$params->{'resource_nolink'} && !$resource->is_sequence() && !$resource->is_empty_sequence) { + $result .= " $curMarkerBegin$title$partLabel$curMarkerEnd $nonLinkedText"; } else { $result .= " $curMarkerBegin$title$partLabel$curMarkerEnd $nonLinkedText"; } @@ -1004,10 +1155,10 @@ sub render_communication_status { my $link = $params->{"resourceLink"}; my $linkopen = ""; my $linkclose = ""; - + my $location=&Apache::loncommon::lonhttpdurl("/adm/lonMisc"); if ($resource->hasDiscussion()) { $discussionHTML = $linkopen . - '' . + '' . $linkclose; } @@ -1017,7 +1168,7 @@ sub render_communication_status { if ($_) { $feedbackHTML .= ' ' - . ''; } } @@ -1025,11 +1176,14 @@ sub render_communication_status { if ($resource->getErrors()) { my $errors = $resource->getErrors(); + my $errorcount = 0; foreach (split(/,/, $errors)) { + last if ($errorcount>=10); # Only output 10 bombs maximum if ($_) { + $errorcount++; $errorHTML .= ' ' - . ''; } } @@ -1054,10 +1208,13 @@ sub render_quick_status { if ($resource->is_problem() && !$firstDisplayed) { - my $icon = $statusIconMap{$resource->status($part)}; + + my $icon = $statusIconMap{$resource->simpleStatus($part)}; my $alt = $iconAltTags{$icon}; if ($icon) { - $result .= "$linkopen$alt$linkclose\n"; + my $location= + &Apache::loncommon::lonhttpdurl("/adm/lonIcons/$icon"); + $result .= "$linkopen$alt$linkclose\n"; } else { $result .= " \n"; } @@ -1077,7 +1234,7 @@ sub render_long_status { if ($resource->is_problem()) { $color = $colormap{$resource->status}; - if (dueInLessThen24Hours($resource, $part) || + if (dueInLessThan24Hours($resource, $part) || lastTry($resource, $part)) { $color = $hurryUpColor; } @@ -1103,8 +1260,84 @@ sub render_long_status { return $result; } +# Colors obtained by taking the icons, matching the colors, and +# possibly reducing the Value (HSV) of the color, if it's too bright +# for text, generally by one third or so. +my %statusColors = + ( + $resObj->CLOSED => '#000000', + $resObj->OPEN => '#998b13', + $resObj->CORRECT => '#26933f', + $resObj->INCORRECT => '#c48207', + $resObj->ATTEMPTED => '#a87510', + $resObj->ERROR => '#000000' + ); +my %statusStrings = + ( + $resObj->CLOSED => 'Not yet open', + $resObj->OPEN => 'Open', + $resObj->CORRECT => 'Correct', + $resObj->INCORRECT => 'Incorrect', + $resObj->ATTEMPTED => 'Attempted', + $resObj->ERROR => 'Network Error' + ); +my @statuses = ($resObj->CORRECT, $resObj->ATTEMPTED, $resObj->INCORRECT, $resObj->OPEN, $resObj->CLOSED, $resObj->ERROR); + +use Data::Dumper; +sub render_parts_summary_status { + my ($resource, $part, $params) = @_; + if (!$resource->is_problem() && !$resource->contains_problem) { return ''; } + if ($params->{showParts}) { + return ''; + } + + my $td = "\n"; + my $endtd = "\n"; + my @probs; + + if ($resource->contains_problem) { + @probs=$resource->retrieveResources($resource,sub { $_[0]->is_problem() },1,0); + } else { + @probs=($resource); + } + my $return; + my %overallstatus; + my $totalParts; + foreach my $resource (@probs) { + # If there is a single part, just show the simple status + if ($resource->singlepart()) { + my $status = $resource->simpleStatus(${$resource->parts}[0]); + $overallstatus{$status}++; + $totalParts++; + next; + } + # Now we can be sure the $part doesn't really matter. + my $statusCount = $resource->simpleStatusCount(); + my @counts; + foreach my $status (@statuses) { + # decouple display order from the simpleStatusCount order + my $slot = Apache::lonnavmaps::resource::statusToSlot($status); + if ($statusCount->[$slot]) { + $overallstatus{$status}+=$statusCount->[$slot]; + $totalParts+=$statusCount->[$slot]; + } + } + } + $return.= $td . $totalParts . ' parts: '; + foreach my $status (@statuses) { + if ($overallstatus{$status}) { + $return.="" . $overallstatus{$status} . ' ' + . $statusStrings{$status} . ""; + } + } + $return.= $endtd; + return $return; +} + my @preparedColumns = (\&render_resource, \&render_communication_status, - \&render_quick_status, \&render_long_status); + \&render_quick_status, \&render_long_status, + \&render_parts_summary_status); sub setDefault { my ($val, $default) = @_; @@ -1112,11 +1345,17 @@ sub setDefault { return $val; } +sub cmp_title { + my ($atitle,$btitle) = (lc($_[0]->compTitle),lc($_[1]->compTitle)); + $atitle=~s/^\s*//; + $btitle=~s/^\s*//; + return $atitle cmp $btitle; +} + sub render { my $args = shift; &Apache::loncommon::get_unprocessed_cgi($ENV{QUERY_STRING}); my $result = ''; - # Configure the renderer. my $cols = $args->{'cols'}; if (!defined($cols)) { @@ -1134,6 +1373,7 @@ sub render { my $jump = $args->{'jump'}; my $here = $args->{'here'}; my $suppressNavmap = setDefault($args->{'suppressNavmap'}, 0); + my $closeAllPages = setDefault($args->{'closeAllPages'}, 0); my $currentJumpDelta = 2; # change this to change how many resources are displayed # before the current resource when using #current @@ -1170,10 +1410,9 @@ sub render { if (!$ENV{'form.folderManip'} && !defined($args->{'iterator'})) { # Step 1: Check to see if we have a navmap if (!defined($navmap)) { - $navmap = Apache::lonnavmaps::navmap->new(1, 1); + $navmap = Apache::lonnavmaps::navmap->new(); $mustCloseNavMap = 1; } - $navmap->init(); # Step two: Locate what kind of here marker is necessary # Determine where the "here" marker is and where the screen jumps to. @@ -1191,18 +1430,13 @@ sub render { # Step three: Ensure the folders are open my $mapIterator = $navmap->getIterator(undef, undef, undef, 1); - my $depth = 1; - $mapIterator->next(); # discard the first BEGIN_MAP - my $curRes = $mapIterator->next(); + my $curRes; my $found = 0; # We only need to do this if we need to open the maps to show the # current position. This will change the counter so we can't count # for the jump marker with this loop. - while ($depth > 0 && !$found) { - if ($curRes == $mapIterator->BEGIN_MAP()) { $depth++; } - if ($curRes == $mapIterator->END_MAP()) { $depth--; } - + while ($here && ($curRes = $mapIterator->next()) && !$found) { if (ref($curRes) && $curRes->symb() eq $here) { my $mapStack = $mapIterator->getStack(); @@ -1216,8 +1450,6 @@ sub render { } $found = 1; } - - $curRes = $mapIterator->next(); } } @@ -1234,11 +1466,9 @@ sub render { # Step 1: Check to see if we have a navmap if (!defined($navmap)) { - $navmap = Apache::lonnavmaps::navmap->new(1, 1); + $navmap = Apache::lonnavmaps::navmap->new(); $mustCloseNavMap = 1; } - # Paranoia: Make sure it's ready - $navmap->init(); # See if we're being passed a specific map if ($args->{'iterator_map'}) { @@ -1252,20 +1482,16 @@ sub render { $args->{'iterator'} = $it = $navmap->getIterator(undef, undef, $filterHash, $condition); } } - + # (re-)Locate the jump point, if any # Note this does not take filtering or hidden into account... need # to be fixed? my $mapIterator = $navmap->getIterator(undef, undef, $filterHash, 0); - my $depth = 1; - $mapIterator->next(); - my $curRes = $mapIterator->next(); + my $curRes; my $foundJump = 0; my $counter = 0; - while ($depth > 0 && !$foundJump) { - if ($curRes == $mapIterator->BEGIN_MAP()) { $depth++; } - if ($curRes == $mapIterator->END_MAP()) { $depth--; } + while (($curRes = $mapIterator->next()) && !$foundJump) { if (ref($curRes)) { $counter++; } if (ref($curRes) && $jump eq $curRes->symb()) { @@ -1276,8 +1502,6 @@ sub render { $args->{'currentJumpIndex'} = $counter; $foundJump = 1; } - - $curRes = $mapIterator->next(); } my $showParts = setDefault($args->{'showParts'}, 1); @@ -1288,23 +1512,24 @@ sub render { my $printKey = $args->{'printKey'}; my $printCloseAll = $args->{'printCloseAll'}; if (!defined($printCloseAll)) { $printCloseAll = 1; } - + # Print key? if ($printKey) { $result .= ''; my $date=localtime; $result.=''; + my $location=&Apache::loncommon::lonhttpdurl("/adm/lonMisc"); if ($navmap->{LAST_CHECK}) { $result .= - ' New discussion since '. + ' '.&mt('New discussion since').' '. strftime("%A, %b %e at %I:%M %P", localtime($navmap->{LAST_CHECK})). ''; } else { $result .= ''; } @@ -1312,18 +1537,87 @@ sub render { } if ($printCloseAll && !$args->{'resource_no_folder_link'}) { + my ($link,$text); if ($condition) { - $result.="Close All Folders"; + $link='"navmaps?condition=0&filter=&'.$queryString. + '&here='.&Apache::lonnet::escape($here).'"'; + $text='Close All Folders'; } else { - $result.="Open All Folders"; + $link='"navmaps?condition=1&filter=&'.$queryString. + '&here='.&Apache::lonnet::escape($here).'"'; + $text='Open All Folders'; + } + if ($args->{'caller'} eq 'navmapsdisplay') { + &add_linkitem($args->{'linkitems'},'changefolder', + 'location.href='.$link,$text); + } else { + $result.=''.&mt($text).''; + } + $result .= "\n"; + } + + # Check for any unread discussions in all resources. + if ($args->{'caller'} eq 'navmapsdisplay') { + &add_linkitem($args->{'linkitems'},'clearbubbles', + 'document.clearbubbles.submit()', + 'Mark all posts read'); + my $time=time; + $result .= (< + + +END + if ($args->{'sort'} eq 'discussion') { + my $totdisc = 0; + my $haveDisc = ''; + my @allres=$navmap->retrieveResources(); + foreach my $resource (@allres) { + if ($resource->hasDiscussion()) { + my $ressymb; + if ($resource->symb() =~ m-(___adm/\w+/\w+)/(\d+)/bulletinboard$-) { + $ressymb = 'bulletin___'.$2.$1.'/'.$2.'/bulletinboard'; + } else { + $ressymb = $resource->symb(); + } + $haveDisc .= $ressymb.':'; + $totdisc ++; + } + } + if ($totdisc > 0) { + $haveDisc =~ s/:$//; + $result .= (< + +END + } + } + $result.=''; + } + + if ($args->{'caller'} eq 'navmapsdisplay') { + $result .= '
Key:    '. - ' New message (click to open)

'. + ' '.&mt('New message (click to open)').'

'. '

  '. - ' Discussions'. - '   New message (click to open)'. + ' '.&mt('Discussions').''. + '   '.&mt('New message (click to open)'). '
'; + if ($ENV{'environment.remotenavmap'} ne 'on') { + $result .= ''; + } else { + $result .= ''; } - $result .= "

\n"; - } + $result.=&show_linkitems($args->{'linkitems'}); + if ($args->{'sort_html'}) { + if ($ENV{'environment.remotenavmap'} ne 'on') { + $result.=''. + ''; + } else { + $result.=''; + } + } + $result .= '
'. + &Apache::loncommon::help_open_menu('','Navigation Screen','Navigation_Screen','',undef,'RAT').' 
   '.$args->{'sort_html'}.'

'. + $args->{'sort_html'}.'
'; + } elsif ($args->{'sort_html'}) { + $result.=$args->{'sort_html'}; + } + $result .= "
\n"; if ($r) { $r->print($result); $r->rflush(); @@ -1345,7 +1639,9 @@ sub render { $args->{'indentLevel'} = 0; $args->{'isNewBranch'} = 0; $args->{'condensed'} = 0; - $args->{'indentString'} = setDefault($args->{'indentString'}, ""); + my $location= + &Apache::loncommon::lonhttpdurl("/adm/lonIcons/whitespace1.gif"); + $args->{'indentString'} = setDefault($args->{'indentString'}, ""); $args->{'displayedHereMarker'} = 0; # If we're suppressing empty sequences, look for them here. Use DFS for speed, @@ -1355,7 +1651,7 @@ sub render { $it->{FIRST_RESOURCE}, $it->{FINISH_RESOURCE}, {}, undef, 1); - $depth = 0; + my $depth = 0; $dfsit->next(); my $curRes = $dfsit->next(); while ($depth > -1) { @@ -1387,9 +1683,6 @@ sub render { my $displayedJumpMarker = 0; # Set up iteration. - $depth = 1; - $it->next(); # discard initial BEGIN_MAP - $curRes = $it->next(); my $now = time(); my $in24Hours = $now + 24 * 60 * 60; my $rownum = 0; @@ -1397,9 +1690,60 @@ sub render { # export "here" marker information $args->{'here'} = $here; - while ($depth > 0) { - if ($curRes == $it->BEGIN_MAP()) { $depth++; } - if ($curRes == $it->END_MAP()) { $depth--; } + $args->{'indentLevel'} = -1; # first BEGIN_MAP takes this to 0 + my @resources; + my $code='';# sub { !(shift->is_map();) }; + if ($args->{'sort'} eq 'title') { + my $oldFilterFunc = $filterFunc; + my $filterFunc= + sub { + my ($res)=@_; + if ($res->is_map()) { return 0;} + return &$oldFilterFunc($res); + }; + @resources=$navmap->retrieveResources(undef,$filterFunc); + @resources= sort { &cmp_title($a,$b) } @resources; + } elsif ($args->{'sort'} eq 'duedate') { + my $oldFilterFunc = $filterFunc; + my $filterFunc= + sub { + my ($res)=@_; + if (!$res->is_problem()) { return 0;} + return &$oldFilterFunc($res); + }; + @resources=$navmap->retrieveResources(undef,$filterFunc); + @resources= sort { + if ($a->duedate ne $b->duedate) { + return $a->duedate cmp $b->duedate; + } + my $value=&cmp_title($a,$b); + return $value; + } @resources; + } elsif ($args->{'sort'} eq 'discussion') { + my $oldFilterFunc = $filterFunc; + my $filterFunc= + sub { + my ($res)=@_; + if (!$res->hasDiscussion() && + !$res->getFeedback() && + !$res->getErrors()) { return 0;} + return &$oldFilterFunc($res); + }; + @resources=$navmap->retrieveResources(undef,$filterFunc); + @resources= sort { &cmp_title($a,$b) } @resources; + } else { + #unknow sort mechanism or default + undef($args->{'sort'}); + } + + + while (1) { + if ($args->{'sort'}) { + $curRes = shift(@resources); + } else { + $curRes = $it->next($closeAllPages); + } + if (!$curRes) { last; } # Maintain indentation level. if ($curRes == $it->BEGIN_MAP() || @@ -1514,14 +1858,23 @@ sub render { # Set up some data about the parts that the cols might want my $filter = $it->{FILTER}; - my $stack = $it->getStack(); - my $src = getLinkForResource($stack); - + my $src; + if ($args->{'sort'}) { + $src = $curRes->src(); # FIXME this is wrong for .pages + } else { + my $stack = $it->getStack(); + $src=getLinkForResource($stack); + } + my $anchor=''; + if ($src=~s/(\#.*)$//) { + $anchor=$1; + } my $srcHasQuestion = $src =~ /\?/; $args->{"resourceLink"} = $src. ($srcHasQuestion?'&':'?') . - 'symb=' . &Apache::lonnet::escape($curRes->symb()); - + 'symb=' . &Apache::lonnet::escape($curRes->symb()). + $anchor; + # Now, display each column. foreach my $col (@$cols) { my $colHTML = ''; @@ -1553,13 +1906,10 @@ sub render { $r->rflush(); } } continue { - $curRes = $it->next(); - if ($r) { # If we have the connection, make sure the user is still connected my $c = $r->connection; if ($c->aborted()) { - Apache::lonnet::logthis("navmaps aborted"); # Who cares what we do, nobody will see it anyhow. return ''; } @@ -1574,7 +1924,12 @@ sub render { # it's quite likely this might fix other browsers, too, and # certainly won't hurt anything. if ($displayedJumpMarker) { - $result .= "\n"; + $result .= " +"; } $result .= ""; @@ -1590,6 +1945,46 @@ sub render { return $result; } +sub add_linkitem { + my ($linkitems,$name,$cmd,$text)=@_; + $$linkitems{$name}{'cmd'}=$cmd; + $$linkitems{$name}{'text'}=&mt($text); +} + +sub show_linkitems { + my ($linkitems)=@_; + my @linkorder = ("launchnav","closenav","firsthomework","everything", + "uncompleted","changefolder","clearbubbles"); + + my $result .= (< + +
+   +
'."\n"; + + return $result; +} + 1; package Apache::lonnavmaps::navmap; @@ -1640,28 +2035,13 @@ To create a navmap object, use the follo =over 4 -=item * Bnew>( - genCourseAndUserOptions, genMailDiscussStatus, getUserData): +=item * Bnew>(): -Creates a new navmap object. genCourseAndUserOptions is a flag saying whether -the course options and user options hash should be generated. This is -for when you are using the parameters of the resources that require -them; see documentation in resource object -documentation. genMailDiscussStatus causes the nav map to retreive -information about the email and discussion status of -resources. Returns the navmap object if this is successful, or -B if not. You must check for undef; errors will occur when you -try to use the other methods otherwise. getUserData, if true, will -retreive the user's performance data for various problems. +Creates a new navmap object. Returns the navmap object if this is +successful, or B if not. =back -Once you have the $navmap object, call ->init() on it when you are ready -to use it. This allows you to check if the course map is defined (see -B below) before engaging in potentially expensive -initialization routines for the genCourseAndUserOptions and -genMailDiscussStatus option. - When you are done with the $navmap object, you I call $navmap->untieHashes(), or you'll prevent the current user from using that course until the web server is restarted. (!) @@ -1685,10 +2065,6 @@ sub new { my $class = ref($proto) || $proto; my $self = {}; - $self->{GENERATE_COURSE_USER_OPT} = shift; - $self->{GENERATE_EMAIL_DISCUSS_STATUS} = shift; - $self->{GET_USER_DATA} = shift; - # Resource cache stores navmap resources as we reference them. We generate # them on-demand so we don't pay for creating resources unless we use them. $self->{RESOURCE_CACHE} = {}; @@ -1716,128 +2092,141 @@ sub new { $self->{NAV_HASH} = \%navmaphash; $self->{PARM_HASH} = \%parmhash; - $self->{INITED} = 0; + $self->{PARM_CACHE} = {}; bless($self); return $self; } -sub init { +sub generate_course_user_opt { my $self = shift; - if ($self->{INITED}) { return; } + if ($self->{COURSE_USER_OPT_GENERATED}) { return; } - # If the course opt hash and the user opt hash should be generated, - # generate them - if ($self->{GENERATE_COURSE_USER_OPT}) { - 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.'_'; - - my %courserdatas; my %useropt; my %courseopt; my %userrdatas; - unless ($uhome eq 'no_host') { + 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.'_'; + + my %courserdatas; my %useropt; my %courseopt; my %userrdatas; + 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); - # Check for network failure - if ( $reply =~ /no.such.host/i || $reply =~ /con_lost/i) { - $self->{NETWORK_FAILURE} = 1; - } elsif ($reply!~/^error\:/) { - $courserdatas{$cid}=$reply; - $courserdatas{$cid.'.last_cache'}=time; - } - } - foreach (split(/\&/,$courserdatas{$cid})) { - my ($name,$value)=split(/\=/,$_); - $courseopt{$userprefix.&Apache::lonnet::unescape($name)}= - &Apache::lonnet::unescape($value); - } + unless ((time-$courserdatas{$cid.'.last_cache'})<240) { + my $reply=&Apache::lonnet::reply('dump:'.$cdom.':'.$cnum. + ':resourcedata',$chome); + # Check for network failure + if ( $reply =~ /no.such.host/i || $reply =~ /con_lost/i) { + $self->{NETWORK_FAILURE} = 1; + } elsif ($reply!~/^error\:/) { + $courserdatas{$cid}=$reply; + $courserdatas{$cid.'.last_cache'}=time; + } + } + 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; - } - # check to see if network failed - elsif ( $reply=~/no.such.host/i || $reply=~/con.*lost/i ) - { - $self->{NETWORK_FAILURE} = 1; - } - } - foreach (split(/\&/,$userrdatas{$uname.'___'.$udom})) { - my ($name,$value)=split(/\=/,$_); - $useropt{$userprefix.&Apache::lonnet::unescape($name)}= - &Apache::lonnet::unescape($value); - } - $self->{COURSE_OPT} = \%courseopt; - $self->{USER_OPT} = \%useropt; - } - } - - if ($self->{GENERATE_EMAIL_DISCUSS_STATUS}) { - my $cid=$ENV{'request.course.id'}; - my ($cdom,$cnum)=split(/\_/,$cid); - - my %emailstatus = &Apache::lonnet::dump('email_status'); - my $logoutTime = $emailstatus{'logout'}; - my $courseLeaveTime = $emailstatus{'logout_'.$ENV{'request.course.id'}}; - $self->{LAST_CHECK} = (($courseLeaveTime > $logoutTime) ? - $courseLeaveTime : $logoutTime); - my %discussiontime = &Apache::lonnet::dump('discussiontimes', - $cdom, $cnum); - my %feedback=(); - my %error=(); - my $keys = &Apache::lonnet::reply('keys:'. - $ENV{'user.domain'}.':'. - $ENV{'user.name'}.':nohist_email', - $ENV{'user.home'}); - - foreach my $msgid (split(/\&/, $keys)) { - $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; - } - } - } - } - - $self->{FEEDBACK} = \%feedback; - $self->{ERROR_MSG} = \%error; # what is this? JB - $self->{DISCUSSION_TIME} = \%discussiontime; - $self->{EMAIL_STATUS} = \%emailstatus; - + 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; + } + # check to see if network failed + elsif ( $reply=~/no.such.host/i || $reply=~/con.*lost/i ) + { + $self->{NETWORK_FAILURE} = 1; + } + } + foreach (split(/\&/,$userrdatas{$uname.'___'.$udom})) { + my ($name,$value)=split(/\=/,$_); + $useropt{$userprefix.&Apache::lonnet::unescape($name)}= + &Apache::lonnet::unescape($value); + } + $self->{COURSE_OPT} = \%courseopt; + $self->{USER_OPT} = \%useropt; } - if ($self->{GET_USER_DATA}) { - # Retreive performance data on problems - my %student_data = Apache::lonnet::currentdump($ENV{'request.course.id'}, - $ENV{'user.domain'}, - $ENV{'user.name'}); - $self->{STUDENT_DATA} = \%student_data; + $self->{COURSE_USER_OPT_GENERATED} = 1; + + return; +} + +sub generate_email_discuss_status { + my $self = shift; + my $symb = shift; + if ($self->{EMAIL_DISCUSS_GENERATED}) { return; } + + my $cid=$ENV{'request.course.id'}; + my ($cdom,$cnum)=split(/\_/,$cid); + + my %emailstatus = &Apache::lonnet::dump('email_status'); + my $logoutTime = $emailstatus{'logout'}; + my $courseLeaveTime = $emailstatus{'logout_'.$ENV{'request.course.id'}}; + $self->{LAST_CHECK} = (($courseLeaveTime > $logoutTime) ? + $courseLeaveTime : $logoutTime); + my %discussiontime = &Apache::lonnet::dump('discussiontimes', + $cdom, $cnum); + my %lastread = &Apache::lonnet::dump('nohist_'.$cid.'_discuss', + $ENV{'user.domain'},$ENV{'user.name'},'lastread'); + my %lastreadtime = (); + foreach (keys %lastread) { + my $key = $_; + $key =~ s/_lastread$//; + $lastreadtime{$key} = $lastread{$_}; + } + + my %feedback=(); + my %error=(); + my $keys = &Apache::lonnet::reply('keys:'. + $ENV{'user.domain'}.':'. + $ENV{'user.name'}.':nohist_email', + $ENV{'user.home'}); + + foreach my $msgid (split(/\&/, $keys)) { + $msgid=&Apache::lonnet::unescape($msgid); + if ((!$emailstatus{$msgid}) || ($emailstatus{$msgid} eq 'new')) { + my $plain= + &Apache::lonnet::unescape(&Apache::lonnet::unescape($msgid)); + if ($plain=~/(Error|Feedback) \[([^\]]+)\]/) { + my ($what,$url)=($1,$2); + if ($what eq 'Error') { + $error{$url}.=','.$msgid; + } else { + $feedback{$url}.=','.$msgid; + } + } + } } + + #url's of resources that have feedbacks + $self->{FEEDBACK} = \%feedback; + #or errors + $self->{ERROR_MSG} = \%error; + $self->{DISCUSSION_TIME} = \%discussiontime; + $self->{EMAIL_STATUS} = \%emailstatus; + $self->{LAST_READ} = \%lastreadtime; + + $self->{EMAIL_DISCUSS_GENERATED} = 1; +} - $self->{PARM_CACHE} = {}; - $self->{INITED} = 1; +sub get_user_data { + my $self = shift; + if ($self->{RETRIEVED_USER_DATA}) { return; } + + # Retrieve performance data on problems + my %student_data = Apache::lonnet::currentdump($ENV{'request.course.id'}, + $ENV{'user.domain'}, + $ENV{'user.name'}); + $self->{STUDENT_DATA} = \%student_data; + + $self->{RETRIEVED_USER_DATA} = 1; } # Internal function: Takes a key to look up in the nav hash and implements internal @@ -1885,11 +2274,27 @@ sub untieHashes { sub hasDiscussion { my $self = shift; my $symb = shift; + + $self->generate_email_discuss_status(); + if (!defined($self->{DISCUSSION_TIME})) { return 0; } #return defined($self->{DISCUSSION_TIME}->{$symb}); - return $self->{DISCUSSION_TIME}->{$symb} > - $self->{LAST_CHECK}; + +# backward compatibility (bulletin boards used to be 'wrapped') + my $ressymb = $symb; + if ($ressymb =~ m|adm/(\w+)/(\w+)/(\d+)/bulletinboard$|) { + unless ($ressymb =~ m|adm/wrapper/adm|) { + $ressymb = 'bulletin___'.$3.'___adm/wrapper/adm/'.$1.'/'.$2.'/'.$3.'/bulletinboard'; + } + } + + if ( defined ( $self->{LAST_READ}->{$ressymb} ) ) { + return $self->{DISCUSSION_TIME}->{$ressymb} > $self->{LAST_READ}->{$ressymb}; + } else { +# return $self->{DISCUSSION_TIME}->{$ressymb} > $self->{LAST_CHECK}; # v.1.1 behavior + return $self->{DISCUSSION_TIME}->{$ressymb} > 0; # in 1.2 will display speech bubble icons for all items with posts until marked as read (even if read in v 1.1). + } } # Private method: Does the given resource (as a symb string) have @@ -1899,6 +2304,8 @@ sub getFeedback { my $self = shift; my $symb = shift; + $self->generate_email_discuss_status(); + if (!defined($self->{FEEDBACK})) { return ""; } return $self->{FEEDBACK}->{$symb}; @@ -1908,7 +2315,9 @@ sub getFeedback { sub getErrors { my $self = shift; my $src = shift; - + + $self->generate_email_discuss_status(); + if (!defined($self->{ERROR_MSG})) { return ""; } return $self->{ERROR_MSG}->{$src}; } @@ -1956,9 +2365,14 @@ sub getById { sub getBySymb { my $self = shift; my $symb = shift; - my ($mapUrl, $id, $filename) = split (/___/, $symb); + + my ($mapUrl, $id, $filename) = &Apache::lonnet::decode_symb($symb); my $map = $self->getResourceByUrl($mapUrl); - return $self->getById($map->map_pc() . '.' . $id); + my $returnvalue = undef; + if (ref($map)) { + $returnvalue = $self->getById($map->map_pc() .'.'.$id); + } + return $returnvalue; } sub getByMapPc { @@ -2021,6 +2435,9 @@ sub parmval_real { my $self = shift; my ($what,$symb,$recurse) = @_; + # Make sure the {USER_OPT} and {COURSE_OPT} hashes are populated + $self->generate_course_user_opt(); + my $cid=$ENV{'request.course.id'}; my $csec=$ENV{'request.course.sec'}; my $uname=$ENV{'user.name'}; @@ -2029,7 +2446,7 @@ sub parmval_real { unless ($symb) { return ''; } my $result=''; - my ($mapname,$id,$fn)=split(/\_\_\_/,$symb); + my ($mapname,$id,$fn)=&Apache::lonnet::decode_symb($symb); # ----------------------------------------------------- Cascading lookup scheme my $rwhat=$what; @@ -2079,7 +2496,11 @@ sub parmval_real { # ----------------------------------------------------- fourth , check default - my $default=&Apache::lonnet::metadata($fn,$rwhat.'.default'); + my $meta_rwhat=$rwhat; + $meta_rwhat=~s/\./_/g; + my $default=&Apache::lonnet::metadata($fn,$meta_rwhat); + if (defined($default)) { return $default} + $default=&Apache::lonnet::metadata($fn,'parameter_'.$meta_rwhat); if (defined($default)) { return $default} # --------------------------------------------------- fifth , cascade up parts @@ -2134,7 +2555,7 @@ want to know is if I resources matc parameter will allow you to avoid potentially expensive enumeration of all matching resources. -=item * B(map, filterFunc, recursive): +=item * B(map, filterFunc, recursive): Convience method for @@ -2145,6 +2566,7 @@ in the filter function. =cut + sub getResourceByUrl { my $self = shift; my $resUrl = shift; @@ -2195,18 +2617,9 @@ sub retrieveResources { my @resources = (); # Run down the iterator and collect the resources. - my $depth = 1; - $it->next(); - my $curRes = $it->next(); - - while ($depth > 0) { - if ($curRes == $it->BEGIN_MAP()) { - $depth++; - } - if ($curRes == $it->END_MAP()) { - $depth--; - } - + my $curRes; + + while ($curRes = $it->next()) { if (ref($curRes)) { if (!&$filterFunc($curRes)) { next; @@ -2219,8 +2632,6 @@ sub retrieveResources { } } - } continue { - $curRes = $it->next(); } return @resources; @@ -2296,6 +2707,13 @@ new branch. The possible tokens are: =over 4 +=item * B: + +The iterator has returned all that it's going to. Further calls to the +iterator will just produce more of these. This is a "false" value, and +is the only false value the iterator which will be returned, so it can +be used as a loop sentinel. + =item * B: A new map is being recursed into. This is returned I the map @@ -2328,10 +2746,31 @@ but only one resource will be returned. =back +=head2 Normal Usage + +Normal usage of the iterator object is to do the following: + + my $it = $navmap->getIterator([your params here]); + my $curRes; + while ($curRes = $it->next()) { + [your logic here] + } + +Note that inside of the loop, it's frequently useful to check if +"$curRes" is a reference or not with the reference function; only +resource objects will be references, and any non-references will +be the tokens described above. + +Also note there is some old code floating around that trys to track +the depth of the iterator to see when it's done; do not copy that +code. It is difficult to get right and harder to understand then +this. They should be migrated to this new style. + =cut # Here are the tokens for the iterator: +sub END_ITERATOR { return 0; } sub BEGIN_MAP { return 1; } # begining of a new map sub END_MAP { return 2; } # end of the map sub BEGIN_BRANCH { return 3; } # beginning of a branch @@ -2417,13 +2856,13 @@ sub new { # prime the recursion $self->{$firstResourceName}->{DATA}->{$valName} = 0; - my $depth = 0; - $iterator->next(); + $iterator->next(); my $curRes = $iterator->next(); - while ($depth > -1) { - if ($curRes == $iterator->BEGIN_MAP()) { $depth++; } - if ($curRes == $iterator->END_MAP()) { $depth--; } - + my $depth = 1; + while ($depth > 0) { + if ($curRes == $iterator->BEGIN_MAP()) { $depth++; } + if ($curRes == $iterator->END_MAP()) { $depth--; } + if (ref($curRes)) { # If there's only one resource, this will save it # we have to filter empty resources from consideration here, @@ -2457,13 +2896,13 @@ sub new { $curRes->{DATA}->{DISPLAY_DEPTH} = $finalDepth; if ($finalDepth > $maxDepth) {$maxDepth = $finalDepth;} } - } continue { - $curRes = $iterator->next(); + + $curRes = $iterator->next(); } } # Check: Was this only one resource, a map? - if ($resourceCount == 1 && $resource->is_map() && !$self->{FORCE_TOP}) { + if ($resourceCount == 1 && $resource->is_sequence() && !$self->{FORCE_TOP}) { my $firstResource = $resource->map_start(); my $finishResource = $resource->map_finish(); return @@ -2479,6 +2918,7 @@ sub new { $self->{MAX_DEPTH} = $maxDepth; $self->{STACK} = []; $self->{RECURSIVE_ITERATOR_FLAG} = 0; + $self->{FINISHED} = 0; # When true, the iterator has finished for (my $i = 0; $i <= $self->{MAX_DEPTH}; $i++) { push @{$self->{STACK}}, []; @@ -2495,6 +2935,10 @@ sub new { sub next { my $self = shift; + my $closeAllPages=shift; + if ($self->{FINISHED}) { + return END_ITERATOR(); + } # If we want to return the top-level map object, and haven't yet, # do so. @@ -2505,7 +2949,7 @@ sub next { if ($self->{RECURSIVE_ITERATOR_FLAG}) { # grab the next from the recursive iterator - my $next = $self->{RECURSIVE_ITERATOR}->next(); + my $next = $self->{RECURSIVE_ITERATOR}->next($closeAllPages); # is it a begin or end map? If so, update the depth if ($next == BEGIN_MAP() ) { $self->{RECURSIVE_DEPTH}++; } @@ -2555,6 +2999,7 @@ sub next { $self->{CURRENT_DEPTH}--; return END_BRANCH(); } else { + $self->{FINISHED} = 1; return END_MAP(); } } @@ -2618,7 +3063,7 @@ sub next { # 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() && + if ( ($self->{HERE}->is_sequence() || (!$closeAllPages && $self->{HERE}->is_page())) && (defined($self->{FILTER}->{$self->{HERE}->map_pc()}) xor $self->{CONDITION})) { $self->{RECURSIVE_ITERATOR_FLAG} = 1; my $firstResource = $self->{HERE}->map_start(); @@ -2636,7 +3081,7 @@ sub next { my $browsePriv = $self->{HERE}->browsePriv(); if (!$self->{HERE}->src() || (!($browsePriv eq 'F') && !($browsePriv eq '2')) ) { - return $self->next(); + return $self->next($closeAllPages); } return $self->{HERE}; @@ -2688,7 +3133,7 @@ package Apache::lonnavmaps::DFSiterator; # 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 +# but this might as well be left separate, since it is possible some other # use might be found for it. - Jeremy # Unlike the main iterator, this DOES return all resources, even blank ones. @@ -3051,9 +3496,9 @@ sub symb { my $self=shift; (my $first, my $second) = $self->{ID} =~ /(\d+).(\d+)/; my $symbSrc = &Apache::lonnet::declutter($self->src()); - return &Apache::lonnet::declutter( - $self->navHash('map_id_'.$first)) + my $symb = &Apache::lonnet::declutter($self->navHash('map_id_'.$first)) . '___' . $second . '___' . $symbSrc; + return &Apache::lonnet::symbclean($symb); } sub title { my $self=shift; @@ -3105,6 +3550,15 @@ Returns true if the resource is a sequen =cut +sub hasResource { + my $self = shift; + return $self->{NAV_MAP}->hasResource(@_); +} + +sub retrieveResources { + my $self = shift; + return $self->{NAV_MAP}->retrieveResources(@_); +} sub is_html { my $self=shift; @@ -3121,7 +3575,15 @@ sub is_page { sub is_problem { my $self=shift; my $src = $self->src(); - return ($src =~ /problem$/); + return ($src =~ /\.(problem|exam|quiz|assess|survey|form|library)$/) +} +sub contains_problem { + my $self=shift; + if ($self->is_page()) { + my $hasProblem=$self->hasResource($self,sub { $_[0]->is_problem() },1); + return $hasProblem; + } + return 0; } sub is_sequence { my $self=shift; @@ -3129,6 +3591,23 @@ sub is_sequence { return $self->navHash("is_map_", 1) && $self->navHash("map_type_" . $self->map_pc()) eq 'sequence'; } +sub is_survey { + my $self = shift(); + my $part = shift(); + if ($self->parmval('type',$part) eq 'survey') { + return 1; + } + if ($self->src() =~ /\.(survey)$/) { + return 1; + } + return 0; +} + +sub is_empty_sequence { + my $self=shift; + my $src = $self->src(); + return !$self->is_page() && $self->navHash("is_map_", 1) && !$self->navHash("map_type_" . $self->map_pc()); +} # Private method: Shells out to the parmval in the nav map, handler parts. sub parmval { @@ -3299,11 +3778,17 @@ sub answerdate { } sub awarded { my $self = shift; my $part = shift; + $self->{NAV_MAP}->get_user_data(); if (!defined($part)) { $part = '0'; } return $self->{NAV_MAP}->{STUDENT_DATA}->{$self->symb()}->{'resource.'.$part.'.awarded'}; } sub duedate { (my $self, my $part) = @_; + my $interval=$self->parmval("interval", $part); + if ($interval) { + my $first_access=&Apache::lonnet::get_first_access('map',$self->symb); + if ($first_access) { return ($first_access+$interval); } + } return $self->parmval("duedate", $part); } sub maxtries { @@ -3320,7 +3805,7 @@ sub opendate { } sub problemstatus { (my $self, my $part) = @_; - return $self->parmval("problemstatus", $part); + return lc $self->parmval("problemstatus", $part); } sub sig { (my $self, my $part) = @_; @@ -3347,7 +3832,16 @@ sub weight { $self->symb(), $ENV{'user.domain'}, $ENV{'user.name'}, $ENV{'request.course.sec'}); - +} +sub part_display { + my $self= shift(); my $partID = shift(); + if (! defined($partID)) { $partID = '0'; } + my $display=&Apache::lonnet::EXT('resource.'.$partID.'.display', + $self->symb); + if (! defined($display) || $display eq '') { + $display = $partID; + } + return $display; } # Multiple things need this @@ -3437,6 +3931,16 @@ Returns the number of parts of the probl for single part problems, returns 1. For multipart, it returns the number of parts in the problem, not including psuedo-part 0. +=item * B(): + +Returns the total number of responses in the problem a student can answer. + +=item * B(): + +Returns a hash whose keys are the response types. The values are the number +of times each response type is used. This is for the I problem, not +just a single part. + =item * B(): Returns true if the problem is multipart, false otherwise. Use this instead @@ -3483,17 +3987,46 @@ sub countParts { return scalar(@{$parts}); # + $delta; } +sub countResponses { + my $self = shift; + my $count; + foreach my $part (@{$self->parts()}) { + $count+= scalar($self->responseIds($part)); + } + return $count; +} + +sub responseTypes { + my $self = shift; + my %responses; + foreach my $part ($self->parts()) { + foreach my $responsetype ($self->responseType($part)) { + $responses{$responsetype}++ if (defined($responsetype)); + } + } + return %responses; +} + sub multipart { my $self = shift; return $self->countParts() > 1; } +sub singlepart { + my $self = shift; + return $self->countParts() == 1; +} + sub responseType { my $self = shift; my $part = shift; $self->extractParts(); - return $self->{RESPONSE_TYPES}->{$part}; + if (defined($self->{RESPONSE_TYPES}->{$part})) { + return @{$self->{RESPONSE_TYPES}->{$part}}; + } else { + return undef; + } } sub responseIds { @@ -3501,7 +4034,11 @@ sub responseIds { my $part = shift; $self->extractParts(); - return $self->{RESPONSE_IDS}->{$part}; + if (defined($self->{RESPONSE_IDS}->{$part})) { + return @{$self->{RESPONSE_IDS}->{$part}}; + } else { + return undef; + } } # Private function: Extracts the parts information, both part names and @@ -3518,33 +4055,46 @@ sub extractParts { # Retrieve part count, if this is a problem if ($self->is_problem()) { + my $partorder = &Apache::lonnet::metadata($self->src(), 'partorder'); my $metadata = &Apache::lonnet::metadata($self->src(), 'packages'); - if (!$metadata) { - $self->{RESOURCE_ERROR} = 1; - $self->{PARTS} = []; - $self->{PART_TYPE} = {}; - return; - } - foreach (split(/\,/,$metadata)) { - if ($_ =~ /^part_(.*)$/) { - my $part = $1; - # This floods the logs if it blows up - if (defined($parts{$part})) { - Apache::lonnet::logthis("$part multiply defined in metadata for " . $self->symb()); - } - - # check to see if part is turned off. - if (!Apache::loncommon::check_if_partid_hidden($part, $self->symb())) { - $parts{$part} = 1; - } - } + if ($partorder) { + my @parts; + for my $part (split (/,/,$partorder)) { + if (!Apache::loncommon::check_if_partid_hidden($part, $self->symb())) { + push @parts, $part; + $parts{$part} = 1; + } + } + $self->{PARTS} = \@parts; + } else { + if (!$metadata) { + $self->{RESOURCE_ERROR} = 1; + $self->{PARTS} = []; + $self->{PART_TYPE} = {}; + return; + } + foreach (split(/\,/,$metadata)) { + if ($_ =~ /^part_(.*)$/) { + my $part = $1; + # This floods the logs if it blows up + if (defined($parts{$part})) { + &Apache::lonnet::logthis("$part multiply defined in metadata for " . $self->symb()); + } + + # check to see if part is turned off. + + if (!Apache::loncommon::check_if_partid_hidden($part, $self->symb())) { + $parts{$part} = 1; + } + } + } + my @sortedParts = sort keys %parts; + $self->{PARTS} = \@sortedParts; } - - my @sortedParts = sort keys %parts; - $self->{PARTS} = \@sortedParts; + # These hashes probably do not need names that end with "Hash".... my %responseIdHash; my %responseTypeHash; @@ -3555,7 +4105,7 @@ sub extractParts { } # Now, the unfortunate thing about this is that parts, part name, and - # response if are delimited by underscores, but both the part + # response id are delimited by underscores, but both the part # name and response id can themselves have underscores in them. # So we have to use our knowlege of part names to figure out # where the part names begin and end, and even then, it is possible @@ -3567,7 +4117,6 @@ sub extractParts { my $partIdSoFar = ''; my @partChunks = split /_/, $partStuff; my $i = 0; - for ($i = 0; $i < scalar(@partChunks); $i++) { if ($partIdSoFar) { $partIdSoFar .= '_'; } $partIdSoFar .= $partChunks[$i]; @@ -3575,13 +4124,35 @@ sub extractParts { my @otherChunks = @partChunks[$i+1..$#partChunks]; my $responseId = join('_', @otherChunks); push @{$responseIdHash{$partIdSoFar}}, $responseId; - $responseTypeHash{$partIdSoFar} = $responseType; - last; + push @{$responseTypeHash{$partIdSoFar}}, $responseType; } } } } - + my $resorder = &Apache::lonnet::metadata($self->src(),'responseorder'); + # + # Reorder the arrays in the %responseIdHash and %responseTypeHash + if ($resorder) { + my @resorder=split(/,/,$resorder); + foreach my $part (keys(%responseIdHash)) { + my $i=0; + my %resids = map { ($_,$i++) } @{ $responseIdHash{$part} }; + my @neworder; + foreach my $possibleid (@resorder) { + if (exists($resids{$possibleid})) { + push(@neworder,$resids{$possibleid}); + } + } + my @ids; + my @type; + foreach my $element (@neworder) { + push (@ids,$responseIdHash{$part}->[$element]); + push (@type,$responseTypeHash{$part}->[$element]); + } + $responseIdHash{$part}=\@ids; + $responseTypeHash{$part}=\@type; + } + } $self->{RESPONSE_IDS} = \%responseIdHash; $self->{RESPONSE_TYPES} = \%responseTypeHash; } @@ -3773,8 +4344,9 @@ sub getCompletionStatus { my $status = $self->queryRestoreHash('solved', shift); - # Left as seperate if statements in case we ever do more with this + # Left as separate if statements in case we ever do more with this if ($status eq 'correct_by_student') {return $self->CORRECT;} + if ($status eq 'correct_by_scantron') {return $self->CORRECT;} if ($status eq 'correct_by_override') {return $self->CORRECT_BY_OVERRIDE; } if ($status eq 'incorrect_attempted') {return $self->INCORRECT; } if ($status eq 'incorrect_by_override') {return $self->INCORRECT_BY_OVERRIDE; } @@ -3891,9 +4463,15 @@ sub status { # dimension and 5 entries on the other, which we want to colorize, # plus network failure and "no date data at all". + #if ($self->{RESOURCE_ERROR}) { return NETWORK_FAILURE; } if ($completionStatus == NETWORK_FAILURE) { return NETWORK_FAILURE; } - my $suppressFeedback = lc($self->parmval("problemstatus", $part)) eq 'no'; + my $suppressFeedback = $self->problemstatus($part) eq 'no'; + # If there's an answer date and we're past it, don't + # suppress the feedback; student should know + if ($self->answerdate($part) && $self->answerdate($part) < time()) { + $suppressFeedback = 0; + } # There are a few whole rows we can dispose of: if ($completionStatus == CORRECT || @@ -3919,7 +4497,7 @@ sub status { if ($dateStatus == PAST_DUE_ANSWER_LATER || $dateStatus == PAST_DUE_NO_ANSWER ) { - return $dateStatus; + return $suppressFeedback ? ANSWER_SUBMITTED : $dateStatus; } if ($dateStatus == ANSWER_OPEN) { @@ -3937,7 +4515,7 @@ sub status { if ($completionStatus == INCORRECT || $completionStatus == INCORRECT_BY_OVERRIDE) { # and there are TRIES LEFT: if ($self->tries($part) < $self->maxtries($part) || !$self->maxtries($part)) { - return TRIES_LEFT; + return $suppressFeedback ? ANSWER_SUBMITTED : TRIES_LEFT; } return $suppressFeedback ? ANSWER_SUBMITTED : INCORRECT; # otherwise, return orange; student can't fix this } @@ -3946,6 +4524,96 @@ sub status { return OPEN; } +sub CLOSED { return 23; } +sub ERROR { return 24; } + +=pod + +B + +Convenience method B provides a "simple status" for the resource. +"Simple status" corresponds to "which icon is shown on the +Navmaps". There are six "simple" statuses: + +=over 4 + +=item * B: The problem is currently closed. (No icon shown.) + +=item * B: The problem is open and unattempted. + +=item * B: The problem is correct for any reason. + +=item * B: The problem is incorrect and can still be +completed successfully. + +=item * B: The problem has been attempted, but the student +does not know if they are correct. (The ellipsis icon.) + +=item * B: There is an error retrieving information about this +problem. + +=back + +=cut + +# This hash maps the composite status to this simple status, and +# can be used directly, if you like +my %compositeToSimple = + ( + NETWORK_FAILURE() => ERROR, + NOTHING_SET() => CLOSED, + CORRECT() => CORRECT, + EXCUSED() => CORRECT, + PAST_DUE_NO_ANSWER() => INCORRECT, + PAST_DUE_ANSWER_LATER() => INCORRECT, + ANSWER_OPEN() => INCORRECT, + OPEN_LATER() => CLOSED, + TRIES_LEFT() => OPEN, + INCORRECT() => INCORRECT, + OPEN() => OPEN, + ATTEMPTED() => ATTEMPTED, + ANSWER_SUBMITTED() => ATTEMPTED + ); + +sub simpleStatus { + my $self = shift; + my $part = shift; + my $status = $self->status($part); + return $compositeToSimple{$status}; +} + +=pod + +B will return an array reference containing, in +this order, the number of OPEN, CLOSED, CORRECT, INCORRECT, ATTEMPTED, +and ERROR parts the given problem has. + +=cut + +# This maps the status to the slot we want to increment +my %statusToSlotMap = + ( + OPEN() => 0, + CLOSED() => 1, + CORRECT() => 2, + INCORRECT() => 3, + ATTEMPTED() => 4, + ERROR() => 5 + ); + +sub statusToSlot { return $statusToSlotMap{shift()}; } + +sub simpleStatusCount { + my $self = shift; + + my @counts = (0, 0, 0, 0, 0, 0, 0); + foreach my $part (@{$self->parts()}) { + $counts[$statusToSlotMap{$self->simpleStatus($part)}]++; + } + + return \@counts; +} + =pod B