--- loncom/interface/lonnavmaps.pm 2002/10/03 19:40:33 1.63 +++ loncom/interface/lonnavmaps.pm 2002/10/08 18:54:39 1.69 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Navigate Maps Handler # -# $Id: lonnavmaps.pm,v 1.63 2002/10/03 19:40:33 bowersj2 Exp $ +# $Id: lonnavmaps.pm,v 1.69 2002/10/08 18:54:39 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,22 +882,28 @@ 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', + $res->ATTEMPTED => '' ); + + my %iconAltTags = + ( 'navmap.correct.gif' => 'Correct', + 'navmap.wrong.gif' => 'Incorrect', + 'navmap.open.gif' => 'Open' ); my %condenseStatuses = - ( $res->NETWORK_FAILURE => 1, - $res->NOTHING_SET => 1, - $res->CORRECT => 1 ); + ( $res->NETWORK_FAILURE => 1, + $res->NOTHING_SET => 1, + $res->CORRECT => 1 ); my %filterHash; # Figure out what we're not displaying @@ -893,14 +918,18 @@ sub new_handle { $currenturl=~s/^[^\/]+//; my $queryAdd = "postdata=" . &Apache::lonnet::escape($currenturl); - $r->print("$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, 0); + 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; @@ -923,67 +952,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 = @{$curRes->parts()}; - my $multipart = scalar(@parts) > 1; - my $condensed = 0; - + # 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? + # Is it multipart? - 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[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; - } - } - } - } + 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 @@ -991,10 +1027,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... @@ -1015,7 +1059,7 @@ sub new_handle { if ($isNewBranch) { $newBranchText = ""; $isNewBranch = 0; - $deltalevel = 1; + $deltalevel = 1; } # links to open and close the folders @@ -1024,11 +1068,11 @@ sub new_handle { my $icon = "\"\""; if ($curRes->is_problem()) { - if ($part eq "0" || $condensed) { - $icon = ''; - } else { - $icon = $indentString; - } + if ($part eq "0" || $condensed) { + $icon = ''; + } else { + $icon = $indentString; + } } # Display the correct icon, link to open or shut map @@ -1037,14 +1081,14 @@ sub new_handle { my $nowOpen = !defined($filterHash{$mapId}); $icon = $nowOpen ? "folder_closed.gif" : "folder_opened.gif"; - $icon = "\"\""; + $icon = "\"\""; $linkopen = ""; + $linkopen .= "&condition=$condition&$queryAdd\">"; $linkclose = ""; - + } my $colorizer = ""; @@ -1056,8 +1100,12 @@ sub new_handle { } } - # FIRST ROW: The resource indentation, branch icon, and name - $r->print(" "); - - my $discussionHTML = ""; my $feedbackHTML = ""; - - # 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(""); - - # 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"); - } + if ($curRes->is_problem() && $part ne "0" && !$condensed) { + $partLabel = " (Part $part)"; + $title = ""; + } + if ($multipart && $condensed) { + $nonLinkedText .= ' (' . $curRes->countParts() . ' parts)'; + } - # FOURTH ROW: Text description - $r->print(""); + + # Is this the first displayed part of a multi-part problem + # that has not been condensed, so we should suppress these two + # columns? + my $firstDisplayed = !$condensed && $multipart && $part eq "0"; + + # THIRD COL: Problem status icon + if ($curRes->is_problem() && + !$firstDisplayed) { + my $icon = $statusIconMap{$curRes->status($part)}; + my $alt = $iconAltTags{$icon}; + if ($icon) { + $r->print("\n"); + } else { + $r->print("\n"); + } + } else { # not problem, no icon + $r->print("\n"); + } + + # FOURTH COL: Text description + $r->print("\n"); + $r->print("\n"); } } $curRes = $mapIterator->next(); @@ -1191,7 +1251,7 @@ sub getLinkForResource { sub getDescription { my $res = shift; my $part = shift; - my $status = $res->getDateStatus(); + my $status = $res->status($part); if ($status == $res->NETWORK_FAILURE) { return ""; } if ($status == $res->NOTHING_SET) { @@ -1201,11 +1261,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)); @@ -1217,21 +1277,28 @@ sub getDescription { return "Answer available"; } if ($status == $res->EXCUSED) { - return "Excused by instructor"; + return "Excused by instructor"; + } + if ($status == $res->ATTEMPTED) { + return "Not yet graded."; } 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. @@ -1239,9 +1306,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'; } } @@ -1309,7 +1376,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; @@ -1462,7 +1529,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 @@ -1729,21 +1796,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}) @@ -1763,6 +1842,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) { @@ -1851,7 +1937,6 @@ sub next { $self->{FORCE_NEXT} = $self->END_BRANCH(); $self->{BRANCH_DEPTH}--; } - return $self->{HERE}; } while (@$next) { @@ -1867,19 +1952,16 @@ sub next { } # If this is a map and we want to recurse down it... (not filtered out) - if ($self->{HERE}->is_map() && + 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}, - $self->{CONDITION}); + $self->{CONDITION}); } return $self->{HERE}; @@ -1952,6 +2034,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 @@ -1995,6 +2078,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); @@ -2244,6 +2334,8 @@ sub getFeedback { sub parts { my $self = shift; + if ($self->ext) { return ['0']; } + $self->extractParts(); return $self->{PARTS}; } @@ -2252,8 +2344,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; } @@ -2263,16 +2359,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}}; @@ -2395,6 +2499,8 @@ B =item * B: Information not available due to network failure. +=item * B: Attempted, and not yet graded. + =back =cut @@ -2405,6 +2511,7 @@ sub INCORRECT_BY_OVERRIDE { return 12; } sub CORRECT { return 13; } sub CORRECT_BY_OVERRIDE { return 14; } sub EXCUSED { return 15; } +sub ATTEMPTED { return 16; } sub getCompletionStatus { my $self = shift; @@ -2423,6 +2530,7 @@ sub getCompletionStatus { if ($status eq 'incorrect_attempted') {return $self->INCORRECT; } if ($status eq 'incorrect_by_override') {return $self->INCORRECT_BY_OVERRIDE; } if ($status eq 'excused') {return $self->EXCUSED; } + if ($status eq 'ungraded_attempted') {return $self->ATTEMPTED; } return $self->NOT_ATTEMPTED; } @@ -2456,6 +2564,8 @@ Along with directly returning the date o =item * OPEN: The item is open and not yet tried. +=item * ATTEMPTED: The problem has been attempted. + =back =cut @@ -2478,7 +2588,11 @@ sub status { # There are a few whole rows we can dispose of: if ($completionStatus == CORRECT || $completionStatus == CORRECT_BY_OVERRIDE ) { - return CORRECT(); + return CORRECT; + } + + if ($completionStatus == ATTEMPTED) { + return ATTEMPTED; } # If it's EXCUSED, then return that no matter what @@ -2490,8 +2604,8 @@ sub status { return NOTHING_SET; } - # Now we're down to a 3 (incorrect, incorrect_override, not_attempted) - # by 4 matrix (date status). + # Now we're down to a 4 (incorrect, incorrect_override, not_attempted) + # by 4 matrix (date statuses). if ($dateStatus == PAST_DUE_ANSWER_LATER || $dateStatus == PAST_DUE_NO_ANSWER ) { @@ -2547,11 +2661,11 @@ sub getNext { $next = $self->{NAV_MAP}->getById($next); # 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); + # the student doesn't have browse priviledges + my $browsePriv = &Apache::lonnet::allowed('bre', $self->src); if (!defined($alreadySeenHash) || !defined($alreadySeenHash->{$next->{ID}}) || - ($browsePriv ne '2' && $browsePriv ne 'F')) { + ($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++) { @@ -1066,64 +1114,76 @@ sub new_handle { $r->print(" ${newBranchText}${linkopen}$icon${linkclose}\n"); - if ($curRes->is_problem() && $part != "0" && !$condensed) { - $partLabel = " (Part $part)"; - $title = ""; - } - - $r->print(" $title$partLabel$discussionHTML$feedbackHTML$linkopen$linkclose\n"); + $r->print(" $title$partLabel $nonLinkedText"); + + if ($curRes->{RESOURCE_ERROR}) { + $r->print(&Apache::loncommon::help_open_topic ("Navmap_Host_Down", + 'Host down')); + } + + my $discussionHTML = ""; my $feedbackHTML = ""; + + # SECOND COL: Is there text or feedback? + if ($curRes->hasDiscussion()) { + $discussionHTML = $linkopen . + '' . + $linkclose; + } + + if ($curRes->getFeedback()) { + my $feedback = $curRes->getFeedback(); + foreach (split(/\,/, $feedback)) { + if ($_) { + $feedbackHTML .= ' ' + . ''; + } + } + } + + $r->print("$discussionHTML$feedbackHTML$linkopen\"$alt\"$linkclose\n"); if ($curRes->kind() eq "res" && $curRes->is_problem() && - !$firstDisplayed) { + !$firstDisplayed) { $r->print (getDescription($curRes, $part)); } + if ($curRes->is_map() && advancedUser() && $curRes->randompick()) { + $r->print('(randomly select ' . $curRes->randompick() .')'); + } - $r->print("