--- loncom/interface/lonnavmaps.pm 2002/10/01 19:39:11 1.60 +++ loncom/interface/lonnavmaps.pm 2002/10/07 21:07:47 1.68 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Navigate Maps Handler # -# $Id: lonnavmaps.pm,v 1.60 2002/10/01 19:39:11 bowersj2 Exp $ +# $Id: lonnavmaps.pm,v 1.68 2002/10/07 21:07:47 bowersj2 Exp $ # # Copyright Michigan State University Board of Trustees # @@ -819,9 +819,6 @@ sub new_handle { &Apache::loncommon::no_cache($r); $r->send_http_header; - # Header - $r->print(&Apache::loncommon::bodytag('Navigate Course Map','', - '')); # Initialize the nav map my $navmap = Apache::lonnavmaps::navmap->new( $ENV{"request.course.fn"}.".db", @@ -834,6 +831,28 @@ sub new_handle { return HTTP_NOT_ACCEPTABLE; } + # Header + $r->print(&Apache::loncommon::bodytag('Navigate Course Map','', + '')); + $r->print(''); + my $desc=$ENV{'course.'.$ENV{'request.course.id'}.'.description'}; + if (defined($desc)) { $r->print("

$desc

\n"); } + my $date=localtime; + $r->print("

$date

\n"); + $r->rflush(); + if ($navmap->{LAST_CHECK}) { + $r->print(' New discussion since '. + timeToHumanString($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

'); + #} + # Check that it's defined if (!($navmap->courseMapDefined())) { $r->print('Coursemap undefined.' . @@ -863,23 +882,22 @@ sub new_handle { my $hurryUpColor = "#FFCCCC"; my %statusIconMap = - ( $res->NETWORK_FAILURE => '', - $res->NOTHING_SET => '', - $res->CORRECT => 'navmap.correct.gif', - $res->EXCUSED => 'navmap.correct.gif', - $res->PAST_DUE_NO_ANSWER => 'navmap.wrong.gif', - $res->PAST_DUE_ANSWER_LATER => 'navmap.wrong.gif', - $res->ANSWER_OPEN => 'navmap.wrong.gif', - $res->OPEN_LATER => '', - $res->TRIES_LEFT => 'navmap.open.gif', - $res->INCORRECT => 'navmap.wrong.gif', - $res->OPEN => 'navmap.open.gif' ); + ( $res->NETWORK_FAILURE => '', + $res->NOTHING_SET => '', + $res->CORRECT => 'navmap.correct.gif', + $res->EXCUSED => 'navmap.correct.gif', + $res->PAST_DUE_NO_ANSWER => 'navmap.wrong.gif', + $res->PAST_DUE_ANSWER_LATER => 'navmap.wrong.gif', + $res->ANSWER_OPEN => 'navmap.wrong.gif', + $res->OPEN_LATER => '', + $res->TRIES_LEFT => 'navmap.open.gif', + $res->INCORRECT => 'navmap.wrong.gif', + $res->OPEN => 'navmap.open.gif' ); my %condenseStatuses = - ( $res->NETWORK_FAILURE => 1, - $res->NOTHING_SET => 1, - $res->CORRECT => 1, - $res->OPEN => 1 ); + ( $res->NETWORK_FAILURE => 1, + $res->NOTHING_SET => 1, + $res->CORRECT => 1 ); my %filterHash; # Figure out what we're not displaying @@ -889,15 +907,27 @@ sub new_handle { } } + 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"); - my $mapIterator = $navmap->getIterator(undef, undef, \%filterHash); + my $condition = 0; + if ($ENV{'form.condition'}) { + $condition = 1; + } + + my $mapIterator = $navmap->getIterator(undef, undef, \%filterHash, $condition); my $curRes = $mapIterator->next(); undef $res; # so we don't accidentally use it later my $indentLevel = -1; - my $indentString = ""; + my $indentString = "\"\""; my $isNewBranch = 0; my $now = time(); @@ -916,51 +946,74 @@ sub new_handle { $isNewBranch = 1; } + # Is this resource being blotted out? + if (ref($curRes) && !advancedUser() && $curRes->randomout()) { + $curRes = $mapIterator->next(); + next; # and totally ignore this resource + } + if (ref($curRes) && $curRes->src()) { - # Step one: Decide which parts to show - my @parts; + # Step one: Decide which parts to show + my @parts = @{$curRes->parts()}; + my $multipart = scalar(@parts) > 1; + my $condensed = 0; + if ($curRes->is_problem()) { - # Is it multipart? - @parts = @{$curRes->parts()}; + # Is it multipart? - my $multipart = scalar(@parts) > 1; - - if ($multipart) { - # If it's multipart, see if part 0 is "open" - # if it is, display all parts, if it isn't, - # just display first - if (!$curRes->opendate("0")) { - @parts = ("0"); # just display the zero-th part - } else { - # Otherwise, only display part 0 if we want to - # attach feedback or email information to it - if (!$curRes->hasDiscussion() && !$curRes->getFeedback()) { - shift @parts; - } else { - # If there's discussion or feedback, that counts - # as a difference, so display the parts. - - # Now, we decide whether to condense the - # parts due to similarity - my $status = $curRes->status($parts[0]); - my $allSame = 1; - for (my $i = 1; $i < scalar(@parts); $i++) { - if ($curRes->status($parts[$i]) != $status){ - $allSame = 0; - } - } - - # $allSame is true if all the statuses were - # the same. Now, if they are all the same and - # match one of the statuses to condense, then - # only display part 0. - if ($allSame && defined($condenseStatuses{$status})){ - @parts = ("0"); - } - } - } - } + if ($multipart) { + # If it's multipart, see if part 0 is "open" + # if it is, display all parts, if it isn't, + # just display first + if (!$curRes->opendate("0")) { + @parts = ("0"); # just display the zero-th part + $condensed = 1; + } else { + # Otherwise, only display part 0 if we want to + # attach feedback or email information to it + if ($curRes->hasDiscussion() || $curRes->getFeedback()) { + shift @parts; + } else { + # If there's discussion or feedback, that counts + # as a difference, so display the parts. + + # Now, we decide whether to condense the + # parts due to similarity + my $status = $curRes->status($parts[1]); + my $due = $curRes->duedate($parts[1]); + my $open = $curRes->opendate($parts[1]); + my $statusAllSame = 1; + my $dueAllSame = 1; + my $openAllSame = 1; + for (my $i = 2; $i < scalar(@parts); $i++) { + if ($curRes->status($parts[$i]) != $status){ + $statusAllSame = 0; + } + if ($curRes->duedate($parts[$i]) != $due ) { + $dueAllSame = 0; + } + if ($curRes->opendate($parts[$i]) != $open) { + $openAllSame = 0; + } + } + + # $allSame is true if all the statuses were + # the same. Now, if they are all the same and + # match one of the statuses to condense, or they + # are all open with the same due date, or they are + # all OPEN_LATER with the same open date, display the + # status of the first non-zero part (to get the 'correct' + # status right, since 0 is never 'correct' or 'open'). + if (($statusAllSame && defined($condenseStatuses{$status})) || + ($dueAllSame && $status == $curRes->OPEN && $statusAllSame)|| + ($openAllSame && $status == $curRes->OPEN_LATER && $statusAllSame) ){ + @parts = ($parts[1]); + $condensed = 1; + } + } + } + } } else { @parts[0] = "0"; # this is to get past foreach loop below @@ -968,10 +1021,18 @@ sub new_handle { # with only one part without loss } - # Step two: Show the parts + # Display one part, in event of network error. + # If this is a single part, we can at least show the correct + # status, but if it's multipart, we're lost. + if ($curRes->{RESOURCE_ERROR}) { + @parts = ("0"); + } + + # Step two: Show the parts foreach my $part (@parts) { - my $deltalevel = 0; # for inserting the branch icon + my $deltalevel = 0; # for inserting the branch icon + my $nonLinkedText = ""; # unlinked stuff after title # For each thing we're displaying... @@ -992,16 +1053,20 @@ sub new_handle { if ($isNewBranch) { $newBranchText = ""; $isNewBranch = 0; - $deltalevel = 1; + $deltalevel = 1; } # links to open and close the folders my $linkopen = ""; my $linkclose = ""; - my $icon = "html.gif"; + my $icon = "\"\""; if ($curRes->is_problem()) { - $icon = "problem.gif"; + if ($part eq "0" || $condensed) { + $icon = ''; + } else { + $icon = $indentString; + } } # Display the correct icon, link to open or shut map @@ -1009,14 +1074,15 @@ sub new_handle { my $mapId = $curRes->map_pc(); my $nowOpen = !defined($filterHash{$mapId}); $icon = $nowOpen ? - "folder_opened.gif" : "folder_closed.gif"; + "folder_closed.gif" : "folder_opened.gif"; + $icon = "\"\""; $linkopen = ""; + $linkopen .= "&condition=$condition&$queryAdd\">"; $linkclose = ""; - + } my $colorizer = ""; @@ -1028,65 +1094,89 @@ sub new_handle { } } - # FIRST ROW: The resource indentation, branch icon, and name - $r->print(" "); + # SECOND COL: Is there text or feedback? + if ($curRes->hasDiscussion()) { + $discussionHTML = $linkopen . + '' . + $linkclose; + } + + if ($curRes->getFeedback()) { + my $feedback = $curRes->getFeedback(); + foreach (split(/\,/, $feedback)) { + if ($_) { + $feedbackHTML .= ' ' + . ''; + } + } + } - my $discussionHTML = ""; my $feedbackHTML = ""; + $r->print(""); - # SECOND ROW: Is there text or feedback? - if ($curRes->hasDiscussion()) { - $discussionHTML = $linkopen . - '' . - $linkclose; - } - - if ($curRes->getFeedback()) { - my $feedback = $curRes->getFeedback(); - foreach (split(/\,/, $feedback)) { - if ($_) { - $feedbackHTML .= ' ' - . ''; - } - } - } - - $r->print(""); - - # THIRD ROW: Problem status icon - if ($curRes->is_problem()) { - my $icon = $statusIconMap{$curRes->status()}; - if ($icon) { - $r->print("\n"); - } else { - $r->print("\n"); - } - } else { # not problem, no icon - $r->print("\n"); - } + # Is this the first displayed part of a multi-part problem + # that has not been condensed, so we should suppress these two + # columns? + my $firstDisplayed = !$condensed && $multipart && $part eq "0"; + + # THIRD ROW: Problem status icon + if ($curRes->is_problem() && + !$firstDisplayed) { + my $icon = $statusIconMap{$curRes->status($part)}; + if ($icon) { + $r->print("\n"); + } else { + $r->print("\n"); + } + } else { # not problem, no icon + $r->print("\n"); + } - # FOURTH ROW: Text description - $r->print("\n"); + $r->print("\n"); } } $curRes = $mapIterator->next(); @@ -1164,11 +1254,11 @@ sub getDescription { return "Opens: " . timeToHumanString($res->opendate($part)); } if ($status == $res->OPEN) { - if ($res->duedate()) { - return "Due: $status " . timeToHumanString($res->duedate($part)); - } else { - return "Open, no due date"; - } + if ($res->duedate()) { + return "Due: $status " . timeToHumanString($res->duedate($part)); + } else { + return "Open, no due date"; + } } if ($status == $res->PAST_DUE_ANSWER_LATER) { return "Answer opens: " . timeToHumanString($res->answerdate($part)); @@ -1180,21 +1270,25 @@ sub getDescription { return "Answer available"; } if ($status == $res->EXCUSED) { - return "Excused by instructor"; + return "Excused by instructor"; } if ($status == $res->TRIES_LEFT) { - my $tries = $res->tries(); - my $maxtries = $res->maxtries(); - my $triesString = "($tries of $maxtries tries used)"; - if ($res->duedate()) { - return "Due: $status " . timeToHumanString($res->duedate($part)) . - " $triesString"; - } else { - return "No due date $triesString"; - } + my $tries = $res->tries(); + my $maxtries = $res->maxtries(); + my $triesString = "($tries of $maxtries tries used)"; + if ($res->duedate()) { + return "Due: $status " . timeToHumanString($res->duedate($part)) . + " $triesString"; + } else { + return "No due date $triesString"; + } } } +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. @@ -1202,9 +1296,9 @@ sub timeToHumanString { my ($time) = @_; # zero, '0' and blank are bad times if ($time) { - return localtime($time); + return localtime($time); } else { - return 'Never'; + return 'Never'; } } @@ -1272,7 +1366,7 @@ sub new { if (!(tie(%parmhash, 'GDBM_File', $self->{PARM_HASH_FILE}, &GDBM_READER(), 0640))) { - untie $self->{PARM_HASH}; + untie $self->{PARM_HASH}; return undef; } $self->{PARM_HASH} = \%parmhash; @@ -1399,7 +1493,7 @@ sub courseMapDefined { sub getIterator { my $self = shift; my $iterator = Apache::lonnavmaps::iterator->new($self, shift, shift, - shift, shift); + shift, undef, shift); return $iterator; } @@ -1425,7 +1519,7 @@ sub hasDiscussion { if (!defined($self->{DISCUSSION_TIME})) { return 0; } return $self->{DISCUSSION_TIME}->{$symb} > - $self->{LAST_CHECK}; + $self->{LAST_CHECK}; } # Private function: Does the given resource (as a symb string) have @@ -1608,7 +1702,7 @@ getIterator behaves as follows: =over 4 -=item B(nav_map, firstResource, finishResource, filterHash): 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(). It is your responsibility to ensure that the iterator will actually get there. 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 1, then only resource that exist IN the filterHash will be recursed on. If it is a 0, only resources NOT in the filterHash will be recursed on. Defaults to 0. +=item B(nav_map, 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(). It is your responsibility to ensure that the iterator will actually get there. 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. 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. @@ -1660,6 +1754,7 @@ sub new { # A hash, used as a set, of resource already seen $self->{ALREADY_SEEN} = shift; if (!defined($self->{ALREADY_SEEN})) { $self->{ALREADY_SEEN} = {} }; + $self->{CONDITION} = shift; # Flag: Have we started yet? If not, the first action is to return BEGIN_MAP. $self->{STARTED} = 0; @@ -1691,21 +1786,33 @@ sub new { return $self; } +# FIXME: Document this. +sub cancelTopRecursion { + my $self = shift; + + if (!$self->{RECURSIVE_ITERATOR_FLAG}) {return;} + + # is this the iterator we want to kill? + if ($self->{RECURSIVE_ITERATOR_FLAG} && + !$self->{RECURSIVE_ITERATOR}->{RECURSIVE_ITERATOR_FLAG}) { + $self->{RECURSIVE_ITERATOR_FLAG} = 0; + undef $self->{RECURSIVE_ITERATOR}; + return; + } + + $self->{RECURSIVE_ITERATOR}->cancelTopRecursion(); +} + # Note... this function is *touchy*. I strongly recommend tracing # through it with the debugger a few times on a non-trivial map before # modifying it. Order is *everything*. +# FIXME: Doc that skipMap will prevent the recursion, if any. sub next { my $self = shift; + my $skipMap = shift; # Iterator logic goes here - # Is this return value pre-determined? - if (defined($self->{FORCE_NEXT})) { - my $tmp = $self->{FORCE_NEXT}; - $self->{FORCE_NEXT} = undef; - return $tmp; - } - # Are we using a recursive iterator? If so, pull from that and # watch the depth; we want to resume our level at the correct time. if ($self->{RECURSIVE_ITERATOR_FLAG}) @@ -1725,6 +1832,13 @@ sub next { return $next; } + # Is this return value pre-determined? + if (defined($self->{FORCE_NEXT})) { + my $tmp = $self->{FORCE_NEXT}; + $self->{FORCE_NEXT} = undef; + return $tmp; + } + # Is there a current resource to grab? If not, then return # END_BRANCH and END_MAP in succession. if (scalar(@{$self->{BRANCH_STACK}}) == 0) { @@ -1813,7 +1927,6 @@ sub next { $self->{FORCE_NEXT} = $self->END_BRANCH(); $self->{BRANCH_DEPTH}--; } - return $self->{HERE}; } while (@$next) { @@ -1829,20 +1942,16 @@ sub next { } # If this is a map and we want to recurse down it... (not filtered out) - if ($self->{HERE}->is_map() && - !defined($self->{FILTER}->{$self->{HERE}->map_pc()})) { + if ($self->{HERE}->is_map() && !$skipMap && + (defined($self->{FILTER}->{$self->{HERE}->map_pc()}) xor $self->{CONDITION})) { $self->{RECURSIVE_ITERATOR_FLAG} = 1; my $firstResource = $self->{HERE}->map_start(); my $finishResource = $self->{HERE}->map_finish(); - # Odd perl syntax here; $self->new allows one to create a new iterator - # can't figure out how to ref this package directly correctly - # isn't MAIN::new, __PACKAGE__::new or Apache::lonnavmaps::iterator->new $self->{RECURSIVE_ITERATOR} = Apache::lonnavmaps::iterator->new ($self->{NAV_MAP}, $firstResource, - $finishResource, $self->{FILTER}, $self->{ALREADY_SEEN}); - # prime the new iterator with the first resource - #push @{$self->{RECURSIVE_ITERATOR}->{BRANCH_STACK}}, $firstResource; + $finishResource, $self->{FILTER}, $self->{ALREADY_SEEN}, + $self->{CONDITION}); } return $self->{HERE}; @@ -1915,6 +2024,7 @@ sub new { # Store this new resource in the parent nav map's cache. $self->{NAV_MAP}->{RESOURCE_CACHE}->{$self->{ID}} = $self; + $self->{RESOURCE_ERROR} = 0; # A hash that can be used by two-pass algorithms to store data # about this resource in. Not used by the resource object @@ -1958,6 +2068,13 @@ sub goesto { my $self=shift; return $sel # "to" can return a comma seperated list for branches sub to { my $self=shift; return $self->navHash("to_", 1); } sub kind { my $self=shift; return $self->navHash("kind_", 1); } +sub ext { my $self=shift; return $self->navHash("ext_", 1) eq 'true:'; } +sub randomout { my $self=shift; return $self->navHash("randomout_", 1); } +sub randompick { + my $self = shift; + return $self->{NAV_MAP}->{PARM_HASH}->{$self->symb . + '.0.parameter_randompick'}; +} sub src { my $self=shift; return $self->navHash("src_", 1); @@ -2207,6 +2324,8 @@ sub getFeedback { sub parts { my $self = shift; + if ($self->ext) { return ['0']; } + $self->extractParts(); return $self->{PARTS}; } @@ -2215,8 +2334,12 @@ sub countParts { my $self = shift; my $parts = $self->parts(); - - if (scalar(@{$parts}) == 1) { return 1;} + + if ($self->{RESOURCE_ERROR}) { + return 0; + } + + if (scalar(@{$parts}) < 2) { return 1;} return scalar(@{$parts}) - 1; } @@ -2226,16 +2349,24 @@ sub extractParts { my $self = shift; return if ($self->{PARTS}); + return if ($self->ext); $self->{PARTS} = []; # Retrieve part count - foreach (split(/\,/,&Apache::lonnet::metadata($self->src(), - 'allpossiblekeys'))) { + 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}}; @@ -2509,9 +2640,12 @@ sub getNext { my $next = $choice->goesto(); $next = $self->{NAV_MAP}->getById($next); - # Don't remember it if we've already seen it + # Don't remember it if we've already seen it or if + # the student doesn't have browse priviledges + my $browsePriv = &Apache::lonnet::allowed('bre', $self->src); if (!defined($alreadySeenHash) || - !defined($alreadySeenHash->{$next->{ID}})) { + !defined($alreadySeenHash->{$next->{ID}}) || + ($browsePriv ne '2' && $browsePriv ne 'F')) { push @branches, $next; } }
\n"); + if ($curRes->randomout()) { + $nonLinkedText .= ' (hidden) '; + } + + # FIRST COL: The resource indentation, branch icon, and name + $r->print("
\n"); # print indentation for (my $i = 0; $i < $indentLevel - $deltalevel; $i++) { $r->print($indentString); } - $r->print(" ${newBranchText}${linkopen}\"\"${linkclose}\n"); + $r->print(" ${newBranchText}${linkopen}$icon${linkclose}\n"); + + if ($curRes->is_problem() && $part != "0" && !$condensed) { + $partLabel = " (Part $part)"; + $title = ""; + } + if ($multipart && $condensed) { + $nonLinkedText .= ' (' . $curRes->countParts() . ' parts)'; + } + + $r->print(" $title$partLabel $nonLinkedText"); + + if ($curRes->{RESOURCE_ERROR}) { + $r->print(&Apache::loncommon::help_open_topic ("Navmap_Host_Down", + 'Host down')); + } - if ($curRes->is_problem() && $part != "0") { $partLabel = " (Part $part)"; } + my $discussionHTML = ""; my $feedbackHTML = ""; - $r->print(" $title$partLabel$discussionHTML$feedbackHTML$discussionHTML$feedbackHTML$linkopen$linkclose$linkopen$linkclose\n"); + # FOURTH ROW: Text description + $r->print("\n"); - if ($curRes->kind() eq "res" and - $curRes->is_problem() ) { + if ($curRes->kind() eq "res" && + $curRes->is_problem() && + !$firstDisplayed) { $r->print (getDescription($curRes, $part)); } + if ($curRes->is_map() && advancedUser() && $curRes->randompick()) { + $r->print('(randomly select ' . $curRes->randompick() .')'); + } - $r->print("