--- loncom/interface/lonnavmaps.pm 2002/10/08 20:39:43 1.71 +++ loncom/interface/lonnavmaps.pm 2002/10/29 20:20:45 1.92 @@ -1,7 +1,8 @@ + # The LearningOnline Network with CAPA # Navigate Maps Handler # -# $Id: lonnavmaps.pm,v 1.71 2002/10/08 20:39:43 www Exp $ +# $Id: lonnavmaps.pm,v 1.92 2002/10/29 20:20:45 bowersj2 Exp $ # # Copyright Michigan State University Board of Trustees # @@ -44,8 +45,8 @@ use strict; use Apache::Constants qw(:common :http); use Apache::lonnet(); use Apache::loncommon(); -use HTML::TokeParser; use GDBM_File; +use POSIX qw (floor strftime); # -------------------------------------------------------------- Module Globals my %hash; @@ -819,10 +820,10 @@ sub new_handle { &Apache::loncommon::no_cache($r); $r->send_http_header; - # Initialize the nav map + # Create the nav map the nav map my $navmap = Apache::lonnavmaps::navmap->new( $ENV{"request.course.fn"}.".db", - $ENV{"request.course.fn"}."_parms.db", 1); + $ENV{"request.course.fn"}."_parms.db", 1, 1); if (!defined($navmap)) { @@ -842,16 +843,15 @@ sub new_handle { $r->rflush(); if ($navmap->{LAST_CHECK}) { $r->print(' New discussion since '. - timeToHumanString($navmap->{LAST_CHECK}). + strftime("%A, %b %e at %I:%M %P", localtime($navmap->{LAST_CHECK})). '
New message (click to open)

'); } else { $r->print(' Discussions'. '
New message (click to open)

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

'); - #} + + # Now that we've displayed some stuff to the user, init the navmap + $navmap->init(); # Check that it's defined if (!($navmap->courseMapDefined())) { @@ -879,7 +879,7 @@ sub new_handle { $res->NOTHING_SET => '' ); # And a special case in the nav map; what to do when the assignment # is not yet done and due in less then 24 hours - my $hurryUpColor = "#FFCCCC"; + my $hurryUpColor = "#FF0000"; my %statusIconMap = ( $res->NETWORK_FAILURE => '', @@ -893,7 +893,7 @@ sub new_handle { $res->TRIES_LEFT => 'navmap.open.gif', $res->INCORRECT => 'navmap.wrong.gif', $res->OPEN => 'navmap.open.gif', - $res->ATTEMPTED => '' ); + $res->ATTEMPTED => 'navmap.open.gif' ); my %iconAltTags = ( 'navmap.correct.gif' => 'Correct', @@ -913,33 +913,133 @@ sub new_handle { } } + # Is this a new-style course? If so, we want to suppress showing the top-level + # maps in their own folders, in favor of "inlining" them. + my $topResource = $navmap->getById("0.0"); + my $inlineTopLevelMaps = $topResource->src() =~ m|^/uploaded/.*default\.sequence$|; + my $currenturl = $ENV{'form.postdata'}; $currenturl=~s/^http\:\/\///; $currenturl=~s/^[^\/]+//; - my $queryAdd = "postdata=" . &Apache::lonnet::escape($currenturl); - - $r->print('Show All Resources

'); - - # Begin the HTML table - # four cols: resource + indent, chat+feedback, icon, text string - $r->print('' ."\n"); + # alreadyHere allows us to only open the maps necessary to view + # the current location once, while at the same time remembering + # the current location. Without that check, the user would never + # be able to close those maps; the user would close it, and the + # currenturl scan would re-open it. + my $queryAdd = "postdata=" . &Apache::lonnet::escape($currenturl) . + "&alreadyHere=1"; my $condition = 0; if ($ENV{'form.condition'}) { $condition = 1; } - my $mapIterator = $navmap->getIterator(undef, undef, \%filterHash, $condition); + if ($condition) { + $r->print('Close All Folders

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

'); + } + + # Begin the HTML table + # four cols: resource + indent, chat+feedback, icon, text string + $r->print('
' ."\n"); + + # This needs to be updated to use symbs from the remote, + # instead of uris. The changes to this and the main rendering + # loop should be obvious. + # Here's a simple example of the iterator. + # Preprocess the map: Look for current URL, force inlined maps to display + + my $mapIterator = $navmap->getIterator(undef, undef, {}, 1); + my $found = 0; + my $depth = 1; + my $currentUrlIndex = 0; # keeps track of when the current resource is found, + # so we can back up a few and put the anchor above the + # current resource + $mapIterator->next(); # discard the first BEGIN_MAP my $curRes = $mapIterator->next(); + my $counter = 0; + + while ($depth > 0) { + if ($curRes == $mapIterator->BEGIN_MAP()) { + $depth++; + } + if ($curRes == $mapIterator->END_MAP()) { + $depth--; + } + + if (ref($curRes)) { $counter++; } + + my $mapStack = $mapIterator->getStack(); + if ($currenturl && !$ENV{'form.alreadyHere'} && ref($curRes) && + $curRes->src() eq $currenturl) { + # If this is the correct resource, be sure to + # show it by making sure the containing maps + # are open. + + $currentUrlIndex = $counter; + + for my $map (@{$mapStack}) { + if ($condition) { + undef $filterHash{$map->map_pc()}; + } else { + $filterHash{$map->map_pc()} = 1; + } + } + $ENV{'form.alreadyHere'} = 1; + } + + # Preprocessing: If we're inlining nav maps into the top-level display, + # make sure we show this map! + if ($inlineTopLevelMaps && ref($curRes) && $curRes->is_map && + scalar(@{$mapStack}) == 1) { + if ($condition) { + undef $filterHash{$curRes->map_pc()}; + } else { + $filterHash{$curRes->map_pc()} = 1; + } + } + + $curRes = $mapIterator->next(); + } + undef $res; # so we don't accidentally use it later - my $indentLevel = -1; + my $indentLevel = 0; my $indentString = "\"\""; my $isNewBranch = 0; my $now = time(); my $in24Hours = $now + 24 * 60 * 60; + my $displayedHereMarker = 0; + + # We know the first thing is a BEGIN_MAP (see "$self->{STARTED}" + # code in iterator->next), so ignore the first one + $mapIterator = $navmap->getIterator(undef, undef, \%filterHash, + $condition); + $mapIterator->next(); + $curRes = $mapIterator->next(); + my $deltadepth = 0; + $depth = 1; + + my @backgroundColors = ("#FFFFFF", "#F6F6F6"); + my $rowNum = 0; + + $counter = 0; + + while ($depth > 0) { + # If we're in a new style course, and this is a BEGIN_MAP, END_MAP, or + # map resource and the stack depth is only one, just plain ignore this resource + # entirely. (This has the effect of inlining the resources in that map + # in the nav map.) + if ($inlineTopLevelMaps && scalar(@{$mapIterator->getStack()}) == 1 && + ref($curRes) && $curRes->is_map()) { + # We let the normal depth stuff occur, but we need to shift everything + # over by one to the left to make it look right. + $deltadepth = -1; + $curRes = $mapIterator->next(); + next; + } - while ($curRes != $mapIterator->END_MAP()) { if ($curRes == $mapIterator->BEGIN_MAP() || $curRes == $mapIterator->BEGIN_BRANCH()) { $indentLevel++; @@ -951,6 +1051,17 @@ sub new_handle { if ($curRes == $mapIterator->BEGIN_BRANCH()) { $isNewBranch = 1; } + if ($curRes == $mapIterator->BEGIN_MAP()) { + $depth++; + } + if ($curRes == $mapIterator->END_MAP()) { + $depth--; + } + + if (ref($curRes)) { $counter++; } + + if ($depth == 1) { $deltadepth = 0; } # we're done shifting, because we're + # out of the inlined map # Is this resource being blotted out? if (ref($curRes) && !advancedUser() && $curRes->randomout()) { @@ -1022,9 +1133,16 @@ sub new_handle { } } else { - @parts[0] = "0"; # this is to get past foreach loop below + $parts[0] = "0"; # this is to get past foreach loop below # you can consider a non-problem resource as a resource - # with only one part without loss + # with only one part without loss + } + + # Is it a multipart problem with a single part, now in + # @parts with "0" filtered out? If so, forget it's a multi-part + # problem and treat it like a single-part problem. + if ( scalar(@parts) == 1 ) { + $multipart = 0; } # Display one part, in event of network error. @@ -1051,6 +1169,10 @@ sub new_handle { 'symb='.&Apache::lonnet::escape($curRes->symb()). '"'; my $title = $curRes->title(); + if (!$title) { + $title = $curRes->src(); + $title = substr ($title, rindex($title, "/") + 1); + } my $partLabel = ""; my $newBranchText = ""; @@ -1078,9 +1200,10 @@ sub new_handle { # Display the correct icon, link to open or shut map if ($curRes->is_map()) { my $mapId = $curRes->map_pc(); - my $nowOpen = !defined($filterHash{$mapId}); + my $nowOpen = (!defined($filterHash{$mapId})); + if ($condition) {$nowOpen = !$nowOpen;} $icon = $nowOpen ? - "folder_closed.gif" : "folder_opened.gif"; + "navmap.folder.closed.gif" : "navmap.folder.open.gif"; $icon = "\"\""; $linkopen = "is_problem()) { my $status = $curRes->status($part); - my $color = $colormap{$status}; + $color = $colormap{$status}; + + # Special case in the navmaps: If in less then + # 24 hours, give it a bit of urgency + if (($status == $curRes->OPEN() || $status == $curRes->ATTEMPTED() || + $status == $curRes->TRIES_LEFT()) + && $curRes->duedate() && + $curRes->duedate() < time()+(24*60*60) && + $curRes->duedate() > time()) { + $color = $hurryUpColor; + } + # Special case: If this is the last try, and there is + # more then one available, and it's not due yet, give a bit of urgency + my $tries = $curRes->tries($part); + my $maxtries = $curRes->maxtries($part); + if ($tries && $maxtries && $maxtries > 1 && + $maxtries - $tries == 1 && $curRes->duedate() && + $curRes->duedate() > time()) { + $color = $hurryUpColor; + } if ($color ne "") { $colorizer = "bgcolor=\"$color\""; } @@ -1104,16 +1247,37 @@ sub new_handle { $nonLinkedText .= ' (hidden) '; } - # FIRST COL: The resource indentation, branch icon, and name - $r->print(" "); + $r->print(""); # Is this the first displayed part of a multi-part problem # that has not been condensed, so we should suppress these two @@ -1163,27 +1327,30 @@ sub new_handle { my $icon = $statusIconMap{$curRes->status($part)}; my $alt = $iconAltTags{$icon}; if ($icon) { - $r->print("\n"); + $r->print("\n"); } else { - $r->print("\n"); + $r->print("\n"); } } else { # not problem, no icon - $r->print("\n"); + $r->print("\n"); } # FOURTH COL: Text description - $r->print("\n"); + $r->print(" \n"); } } $curRes = $mapIterator->next(); @@ -1258,20 +1425,20 @@ sub getDescription { return "Not currently assigned."; } if ($status == $res->OPEN_LATER) { - return "Opens: " . timeToHumanString($res->opendate($part)); + return "Open " . timeToHumanString($res->opendate($part)); } if ($status == $res->OPEN) { - if ($res->duedate()) { - return "Due: $status " . timeToHumanString($res->duedate($part)); + if ($res->duedate($part)) { + return "Due " . timeToHumanString($res->duedate($part)); } else { return "Open, no due date"; } } if ($status == $res->PAST_DUE_ANSWER_LATER) { - return "Answer opens: " . timeToHumanString($res->answerdate($part)); + return "Answer open " . timeToHumanString($res->answerdate($part)); } if ($status == $res->PAST_DUE_NO_ANSWER) { - return "Was Due: " . timeToHumanString($res->duedate($part)); + return "Was due " . timeToHumanString($res->duedate($part)); } if ($status == $res->ANSWER_OPEN) { return "Answer available"; @@ -1283,11 +1450,17 @@ sub getDescription { return "Not yet graded."; } if ($status == $res->TRIES_LEFT) { - my $tries = $res->tries(); - my $maxtries = $res->maxtries(); - my $triesString = "($tries of $maxtries tries used)"; + my $tries = $res->tries($part); + my $maxtries = $res->maxtries($part); + my $triesString = ""; + if ($tries && $maxtries) { + $triesString = "($tries of $maxtries tries used)"; + if ($maxtries > 1 && $maxtries - $tries == 1) { + $triesString = "$triesString"; + } + } if ($res->duedate()) { - return "Due: $status " . timeToHumanString($res->duedate($part)) . + return "Due " . timeToHumanString($res->duedate($part)) . " $triesString"; } else { return "No due date $triesString"; @@ -1299,16 +1472,108 @@ sub advancedUser { return $ENV{'user.adv'}; } -# I want to change this into something more human-friendly. For -# now, this is a simple call to localtime. The final function -# probably belongs in loncommon. + +# timeToHumanString takes a time number and converts it to a +# human-readable representation, meant to be used in the following +# manner: +# print "Due $timestring" +# print "Open $timestring" +# print "Answer available $timestring" +# Very, very, very, VERY English-only... goodness help a localizer on +# this func... sub timeToHumanString { my ($time) = @_; # zero, '0' and blank are bad times - if ($time) { - return localtime($time); - } else { - return 'Never'; + if (!$time) { + return 'never'; + } + + my $now = time(); + + my @time = localtime($time); + my @now = localtime($now); + + # Positive = future + my $delta = $time - $now; + + my $minute = 60; + my $hour = 60 * $minute; + my $day = 24 * $hour; + my $week = 7 * $day; + my $inPast = 0; + + # Logic in comments: + # Is it now? (extremely unlikely) + if ( $delta == 0 ) { + return "this instant"; + } + + if ($delta < 0) { + $inPast = 1; + $delta = -$delta; + } + + # Is it in the future? + if ( $delta > 0 ) { + # Is it less then a minute away? + my $tense = $inPast ? " ago" : ""; + my $prefix = $inPast ? "" : "in "; + if ( $delta < $minute ) { + if ($delta == 1) { return "${prefix}1 second$tense"; } + return "$prefix$delta seconds$tense"; + } + + # Is it less then an hour away? + if ( $delta < $hour ) { + # If so, use minutes + my $minutes = floor($delta / 60); + if ($minutes == 1) { return "${prefix}1 minute$tense"; } + return "$prefix$minutes minutes$tense"; + } + + # Is it less then 24 hours away? If so, + # display hours + minutes + if ( $delta < $hour * 24) { + my $hours = floor($delta / $hour); + my $minutes = floor(($delta % $hour) / $minute); + my $hourString = "$hours hours"; + my $minuteString = ", $minutes minutes"; + if ($hours == 1) { + $hourString = "1 hour"; + } + if ($minutes == 1) { + $minuteString = ", 1 minute"; + } + if ($minutes == 0) { + $minuteString = ""; + } + return "$prefix$hourString$minuteString$tense"; + } + + # Less then 5 days away, display day of the week and + # HH:MM + if ( $delta < $day * 5 ) { + my $timeStr = strftime("%A, %b %e at %I:%M %P", localtime($time)); + $timeStr =~ s/12:00 am/midnight/; + $timeStr =~ s/12:00 pm/noon/; + return ($inPast ? "last " : "next ") . + $timeStr; + } + + # Is it this year? + if ( $time[5] == $now[5]) { + # Return on Month Day, HH:MM meridian + my $timeStr = strftime("on %A, %b %e at %I:%M %P", localtime($time)); + $timeStr =~ s/12:00 am/midnight/; + $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/; + $timeStr =~ s/12:00 pm/noon/; + return $timeStr; } } @@ -1378,6 +1643,14 @@ sub new { $self->{PARM_HASH} = \%parmhash; $self->{HASH_TIED} = 1; + bless($self); + + return $self; +} + +sub init { + my $self = $self; + # If the course opt hash and the user opt hash should be generated, # generate them if ($self->{GENERATE_COURSE_USER_OPT}) { @@ -1480,10 +1753,8 @@ sub new { $self->{EMAIL_STATUS} = \%emailstatus; } - - bless($self); - - return $self; + + $self->{PARM_CACHE} = {}; } # Checks to see if coursemap is defined, matching test in old lonnavmaps @@ -1499,7 +1770,8 @@ sub courseMapDefined { sub getIterator { my $self = shift; my $iterator = Apache::lonnavmaps::iterator->new($self, shift, shift, - shift, undef, shift); + shift, undef, shift, + $ENV{'form.direction'}); return $iterator; } @@ -1601,6 +1873,21 @@ sub finishResource { sub parmval { my $self = shift; my ($what,$symb)=@_; + my $hashkey = $what."|||".$symb; + + if (defined($self->{PARM_CACHE}->{$hashkey})) { + return $self->{PARM_CACHE}->{$hashkey}; + } + + my $result = $self->parmval_real($what, $symb); + $self->{PARM_CACHE}->{$hashkey} = $result; + return $result; +} + +sub parmval_real { + my $self = shift; + my ($what,$symb) = @_; + my $cid=$ENV{'request.course.id'}; my $csec=$ENV{'request.course.sec'}; my $uname=$ENV{'user.name'}; @@ -1708,7 +1995,7 @@ getIterator behaves as follows: =over 4 -=item B(firstResource, finishResource, filterHash, condition): All parameters are optional. firstResource is a resource reference corresponding to where the iterator should start. It defaults to navmap->firstResource() for the corresponding nav map. finishResource corresponds to where you want the iterator to end, defaulting to navmap->finishResource(). filterHash is a hash used as a set containing strings representing the resource IDs, defaulting to empty. Condition is a 1 or 0 that sets what to do with the filter hash: If a 0, then only resource that exist IN the filterHash will be recursed on. If it is a 1, only resources NOT in the filterHash will be recursed on. Defaults to 0, which is to say, do not recurse unless explicitly asked to. +=item B(firstResource, finishResource, filterHash, condition, direction): All parameters are optional. firstResource is a resource reference corresponding to where the iterator should start. It defaults to navmap->firstResource() for the corresponding nav map. finishResource corresponds to where you want the iterator to end, defaulting to navmap->finishResource(). filterHash is a hash used as a set containing strings representing the resource IDs, defaulting to empty. Condition is a 1 or 0 that sets what to do with the filter hash: If a 0, then only resource that exist IN the filterHash will be recursed on. If it is a 1, only resources NOT in the filterHash will be recursed on. Defaults to 0, which is to say, do not recurse unless explicitly asked to. Direction specifies which direction to recurse, either FORWARD or BACKWARD, with FORWARD being default. Thus, by default, all resources will be shown. Change the condition to a 1 without changing the hash, and only the top level of the map will be shown. Changing the condition to 1 and including some values in the hash will allow you to selectively examine parts of the navmap, while leaving it on 0 and adding things to the hash will allow you to selectively ignore parts of the nav map. See the handler code for examples: By default, the condition is 0 and all folders are closed unless explicitly opened. Clicking "Show All Resources" will use a condition of 1 and an empty filterHash, resulting in all resources being shown. @@ -1738,6 +2025,8 @@ sub BEGIN_MAP { return 1; } # beginin sub END_MAP { return 2; } # end of the map sub BEGIN_BRANCH { return 3; } # beginning of a branch sub END_BRANCH { return 4; } # end of a branch +sub FORWARD { return 1; } # go forward +sub BACKWARD { return 2; } # Params: nav map, start resource, end resource, filter, condition, # already seen hash ref @@ -1768,6 +2057,7 @@ sub new { $self->{ALREADY_SEEN} = shift; if (!defined($self->{ALREADY_SEEN})) { $self->{ALREADY_SEEN} = {} }; $self->{CONDITION} = shift; + $self->{DIRECTION} = shift || FORWARD(); # Flag: Have we started yet? If not, the first action is to return BEGIN_MAP. $self->{STARTED} = 0; @@ -1792,7 +2082,11 @@ sub new { $self->{FORCE_NEXT} = undef; # Start with the first resource - push @{$self->{BRANCH_STACK}}, $self->{FIRST_RESOURCE}; + if ($self->{DIRECTION} == FORWARD) { + push @{$self->{BRANCH_STACK}}, $self->{FIRST_RESOURCE}; + } else { + push @{$self->{BRANCH_STACK}}, $self->{FINISH_RESOURCE}; + } $self->{BRANCH_STACK_SIZE} = 1; bless($self); @@ -1889,7 +2183,12 @@ sub next { $self->{ALREADY_SEEN}->{$self->{HERE}->{ID}} = 1; # Get the next possible resources - my $nextUnfiltered = $self->{HERE}->getNext(); + my $nextUnfiltered; + if ($self->{DIRECTION} == FORWARD()) { + $nextUnfiltered = $self->{HERE}->getNext(); + } else { + $nextUnfiltered = $self->{HERE}->getPrevious(); + } my $next = []; # filter the next possibilities to remove things we've @@ -1939,7 +2238,7 @@ sub next { $self->{RECURSIVE_ITERATOR} = Apache::lonnavmaps::iterator->new ($self->{NAV_MAP}, $firstResource, $finishResource, $self->{FILTER}, $self->{ALREADY_SEEN}, - $self->{CONDITION}); + $self->{CONDITION}, $self->{DIRECTION}); } return $self->{HERE}; @@ -1966,7 +2265,7 @@ sub populateStack { my $self=shift; my $stack = shift; - push @$stack, $self->{HERE}; + push @$stack, $self->{HERE} if ($self->{HERE}); if ($self->{RECURSIVE_ITERATOR_FLAG}) { $self->{RECURSIVE_ITERATOR}->populateStack($stack); @@ -2062,15 +2361,15 @@ These are methods that help you retrieve =item * B: Returns the "to" value from the compiled nav map. (It is likely you want to use B instead.) -=item * B: Returns the type of the resource, "start", "normal", or "finish". - =back =cut # These info functions can be used directly, as they don't return # resource information. +sub comesfrom { my $self=shift; return $self->navHash("comesfrom_", 1); } sub ext { my $self=shift; return $self->navHash("ext_", 1) eq 'true:'; } +sub from { my $self=shift; return $self->navHash("from_", 1); } sub goesto { my $self=shift; return $self->navHash("goesto_", 1); } sub kind { my $self=shift; return $self->navHash("kind_", 1); } sub randomout { my $self=shift; return $self->navHash("randomout_", 1); } @@ -2093,7 +2392,6 @@ sub symb { } sub title { my $self=shift; return $self->navHash("title_", 1); } sub to { my $self=shift; return $self->navHash("to_", 1); } -sub type { my $self=shift; return $self->navHash("type_", 1); } =pod @@ -2309,7 +2607,7 @@ sub getReturnHash { my $self = shift; if (!defined($self->{RETURN_HASH})) { - my %tmpHash = &Apache::lonnet::restore($self->symb()); + my %tmpHash = &Apache::lonnet::restore($self->symb()); $self->{RETURN_HASH} = \%tmpHash; } } @@ -2355,7 +2653,7 @@ sub hasDiscussion { sub getFeedback { my $self = shift; - return $self->{NAV_MAP}->getFeedback($self->symb()); + return $self->{NAV_MAP}->getFeedback($self->src()); } =pod @@ -2400,25 +2698,27 @@ sub extractParts { $self->{PARTS} = []; - # Retrieve part count - my $metadata = &Apache::lonnet::metadata($self->src(), 'allpossiblekeys'); - if (!$metadata) { - $self->{RESOURCE_ERROR} = 1; - $self->{PARTS} = []; - return; - } - - foreach (split(/\,/,$metadata)) { - if ($_ =~ /^parameter\_(.*)\_opendate$/) { - push @{$self->{PARTS}}, $1; + # Retrieve part count, if this is a problem + if ($self->is_problem()) { + my $metadata = &Apache::lonnet::metadata($self->src(), 'allpossiblekeys'); + if (!$metadata) { + $self->{RESOURCE_ERROR} = 1; + $self->{PARTS} = []; + return; + } + + foreach (split(/\,/,$metadata)) { + if ($_ =~ /^parameter\_(.*)\_opendate$/) { + push @{$self->{PARTS}}, $1; + } } + + + # Is this possible to do in one line? - Jeremy + my @sortedParts = sort @{$self->{PARTS}}; + $self->{PARTS} = \@sortedParts; } - - # Is this possible to do in one line? - Jeremy - my @sortedParts = sort @{$self->{PARTS}}; - $self->{PARTS} = \@sortedParts; - return; } @@ -2667,7 +2967,7 @@ sub status { # If it's WRONG... if ($completionStatus == INCORRECT || $completionStatus == INCORRECT_BY_OVERRIDE) { # and there are TRIES LEFT: - if ($self->tries() < $self->maxtries()) { + if ($self->tries($part) < $self->maxtries($part) || !$self->maxtries($part)) { return TRIES_LEFT; } return INCORRECT; # otherwise, return orange; student can't fix this @@ -2683,20 +2983,18 @@ sub status { =over 4 -=item * B(): Gets the next resource in the navmap after this one. +=item * B($alreadySeenHashRef): Retreive an array of the possible next resources after this one. Always returns an array, even in the one- or zero-element case. The "alreadySeenHashRef" is an optional parameter that can be passed in to the method. If $$alreadySeenHashRef{$res->id()} is true in that hash, getNext will not return it in the list. In other words, you can use it to suppress resources you've already seen in the getNext method directly. -=cut +=item * B($alreadySeenHashRef): Retreive an array of the possible previous resources from this one. Always returns an array, even in the one- or zero-element case. $alreadySeenHashRef is the same as in getNext. -# For the simple single-link case, to get from a resource to the next -# resource, you need to look up the "to_" link in the nav hash, then -# follow that with the "goesto_" link. +=cut sub getNext { my $self = shift; my $alreadySeenHash = shift; my @branches; my $to = $self->to(); - foreach my $branch ( split(/\,/, $to) ) { + foreach my $branch ( split(/,/, $to) ) { my $choice = $self->{NAV_MAP}->getById($branch); my $next = $choice->goesto(); $next = $self->{NAV_MAP}->getById($next); @@ -2712,6 +3010,28 @@ sub getNext { } return \@branches; } + +sub getPrevious { + my $self = shift; + my $alreadySeenHash = shift; + my @branches; + my $from = $self->from(); + foreach my $branch ( split /,/, $from) { + my $choice = $self->{NAV_MAP}->getById($branch); + my $prev = $choice->comesfrom(); + $prev = $self->{NAV_MAP}->getById($prev); + + # Skip it if we've already seen it or the user doesn't have + # browse privs + my $browsePriv = &Apache::lonnet::allowed('bre', $self->src); + if (!defined($alreadySeenHash) || + !defined($alreadySeenHash->{$prev->{ID}}) || + ($browsePriv ne '2' && $browsePriv ne 'F')) { + push @branches, $prev; + } + } + return \@branches; +} =pod
\n"); + $rowNum++; + my $backgroundColor = $backgroundColors[$rowNum % scalar(@backgroundColors)]; + + # FIRST COL: The resource indentation, branch icon, name, and anchor + $r->print("
\n"); + + # anchor for current resource... - 5 is deliberate: If it's that + # high on the screen, don't bother focusing on it. Also this will + # print multiple anchors if this is an expanded multi-part problem... + # who cares? + if ($counter == $currentUrlIndex - 5) { + $r->print(''); + } # print indentation - for (my $i = 0; $i < $indentLevel - $deltalevel; $i++) { + for (my $i = 0; $i < $indentLevel - $deltalevel + $deltadepth; $i++) { $r->print($indentString); } $r->print(" ${newBranchText}${linkopen}$icon${linkclose}\n"); + my $curMarkerBegin = ""; + my $curMarkerEnd = ""; + + # Is this the current resource? + if ($curRes->src() eq $currenturl && !$displayedHereMarker) { + $curMarkerBegin = '> '; + $curMarkerEnd = ' <'; + $displayedHereMarker = 1; + } + if ($curRes->is_problem() && $part ne "0" && !$condensed) { $partLabel = " (Part $part)"; $title = ""; @@ -1122,16 +1286,16 @@ sub new_handle { $nonLinkedText .= ' (' . $curRes->countParts() . ' parts)'; } - $r->print(" $title$partLabel $nonLinkedText"); + $r->print(" $curMarkerBegin$title$partLabel $curMarkerEnd $nonLinkedText"); if ($curRes->{RESOURCE_ERROR}) { $r->print(&Apache::loncommon::help_open_topic ("Navmap_Host_Down", - 'Host down')); + 'Host down')); } my $discussionHTML = ""; my $feedbackHTML = ""; - # SECOND COL: Is there text or feedback? + # SECOND COL: Is there text, feedback, errors?? if ($curRes->hasDiscussion()) { $discussionHTML = $linkopen . '' . @@ -1150,7 +1314,7 @@ sub new_handle { } } - $r->print("$discussionHTML$feedbackHTML$discussionHTML$feedbackHTML $linkopen\"$alt\"$linkclose$linkopen\"$alt\"$linkclose  \n"); + #$r->print("\n"); + $r->print("\n"); if ($curRes->kind() eq "res" && $curRes->is_problem() && !$firstDisplayed) { + $r->print ("") if ($color); $r->print (getDescription($curRes, $part)); + $r->print ("") if ($color); } if ($curRes->is_map() && advancedUser() && $curRes->randompick()) { $r->print('(randomly select ' . $curRes->randompick() .')'); } - $r->print("