--- loncom/interface/lonnavmaps.pm 2003/08/07 14:29:43 1.220 +++ loncom/interface/lonnavmaps.pm 2003/09/08 22:44:36 1.225 @@ -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.225 2003/09/08 22:44:36 bowersj2 Exp $ # # Copyright Michigan State University Board of Trustees # @@ -61,19 +61,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', @@ -129,7 +124,7 @@ sub real_handler { $r->send_http_header; # Create the nav map - my $navmap = Apache::lonnavmaps::navmap->new(1, 1); + my $navmap = Apache::lonnavmaps::navmap->new(); if (!defined($navmap)) { @@ -159,11 +154,6 @@ sub real_handler { $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('Coursemap undefined.' . @@ -173,23 +163,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) { @@ -207,16 +191,11 @@ sub real_handler { $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 +213,6 @@ sub real_handler { $ENV{'form.postsymb'} = $curRes->symb(); } } - } continue { - $curRes = $iterator->next(); } # If we found no problems, print a note to that effect. @@ -678,13 +655,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 +669,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 +854,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) = @_; @@ -1054,7 +1043,8 @@ 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"; @@ -1103,8 +1093,63 @@ sub render_long_status { return $result; } +my %statusColors = + ( + $resObj->CLOSED => '#000000', + $resObj->OPEN => '#000000', + $resObj->CORRECT => '#000000', + $resObj->INCORRECT => '#000000', + $resObj->ATTEMPTED => '#000000', + $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()) { return ''; } + if ($params->{showParts}) { + return ''; + } + + my $td = "\n"; + my $endtd = "\n"; + + # If there is a single part, just show the simple status + if ($resource->singlepart()) { + my $status = $resource->simpleStatus('0'); + return $td . "" + . $statusStrings{$status} . "" . $endtd; + } + + # 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]) { + push @counts, "" . $statusCount->[$slot] . ' ' + . $statusStrings{$status} . ""; + } + } + + return $td . join (', ', @counts) . $endtd; +} + 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) = @_; @@ -1170,10 +1215,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 +1235,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 (($curRes = $mapIterator->next()) && !$found) { if (ref($curRes) && $curRes->symb() eq $here) { my $mapStack = $mapIterator->getStack(); @@ -1216,8 +1255,6 @@ sub render { } $found = 1; } - - $curRes = $mapIterator->next(); } } @@ -1234,11 +1271,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'}) { @@ -1257,15 +1292,11 @@ sub render { # 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 +1307,6 @@ sub render { $args->{'currentJumpIndex'} = $counter; $foundJump = 1; } - - $curRes = $mapIterator->next(); } my $showParts = setDefault($args->{'showParts'}, 1); @@ -1355,7 +1384,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 +1416,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,10 +1423,8 @@ 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 + while ($curRes = $it->next()) { # Maintain indentation level. if ($curRes == $it->BEGIN_MAP() || $curRes == $it->BEGIN_BRANCH() ) { @@ -1553,8 +1577,6 @@ 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; @@ -1640,28 +1662,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 +1692,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 +1719,134 @@ 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; + 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 %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; + + $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,6 +1894,9 @@ 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}); @@ -1899,6 +1911,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 +1922,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}; } @@ -2021,6 +2037,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'}; @@ -2195,18 +2214,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 +2229,6 @@ sub retrieveResources { } } - } continue { - $curRes = $it->next(); } return @resources; @@ -2296,6 +2304,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 @@ -2326,12 +2341,33 @@ consisting entirely of empty resources e ending resource, will cause a lot of BRANCH_STARTs and BRANCH_ENDs, but only one resource will be returned. +=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. + =back =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 +2453,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,8 +2493,8 @@ sub new { $curRes->{DATA}->{DISPLAY_DEPTH} = $finalDepth; if ($finalDepth > $maxDepth) {$maxDepth = $finalDepth;} } - } continue { - $curRes = $iterator->next(); + + $curRes = $iterator->next(); } } @@ -2479,6 +2515,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}}, []; @@ -2496,6 +2533,10 @@ sub new { sub next { my $self = shift; + if ($self->{FINISHED}) { + return END_ITERATOR(); + } + # If we want to return the top-level map object, and haven't yet, # do so. if ($self->{RETURN_0} && !$self->{HAVE_RETURNED_0}) { @@ -2555,6 +2596,7 @@ sub next { $self->{CURRENT_DEPTH}--; return END_BRANCH(); } else { + $self->{FINISHED} = 1; return END_MAP(); } } @@ -3051,9 +3093,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; @@ -3299,6 +3341,7 @@ 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'}; } @@ -3488,6 +3531,11 @@ sub multipart { return $self->countParts() > 1; } +sub singlepart { + my $self = shift; + return $self->countParts() == 1; +} + sub responseType { my $self = shift; my $part = shift; @@ -3891,6 +3939,7 @@ 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'; @@ -3937,7 +3986,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 +3995,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