--- loncom/interface/lonnavmaps.pm 2002/10/01 07:20:31 1.58 +++ 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.58 2002/10/01 07:20:31 albertel 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.' . @@ -848,16 +867,43 @@ sub new_handle { # Defines a status->color mapping, null string means don't color my %colormap = ( $res->NETWORK_FAILURE => '', - $res->CORRECT => '#BBFFBB', + $res->CORRECT => '', $res->EXCUSED => '#BBBBFF', - $res->PAST_DUE_ANSWER_LATER => '#FFAA00', - $res->PAST_DUE_NO_ANSWER => '#FFAA00', - $res->ANSWER_OPEN => '#FF00AA', + $res->PAST_DUE_ANSWER_LATER => '', + $res->PAST_DUE_NO_ANSWER => '', + $res->ANSWER_OPEN => '#CCFFCC', $res->OPEN_LATER => '', - $res->TRIES_LEFT => '#FFFF00', - $res->INCORRECT => '#FFAA00', - $res->OPEN => '#FFFF88', + $res->TRIES_LEFT => '', + $res->INCORRECT => '', + $res->OPEN => '', $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 %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->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 ); my %filterHash; # Figure out what we're not displaying @@ -867,17 +913,31 @@ 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 - # two columns: the title/icon on the left, the info on the right + # 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(); + my $in24Hours = $now + 24 * 60 * 60; while ($curRes != $mapIterator->END_NAV_MAP) { if ($curRes == $mapIterator->BEGIN_MAP() || @@ -892,19 +952,93 @@ 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()) { - 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()) { - @parts = @{$curRes->parts()}; - if (scalar(@parts) > 1) { shift(@parts); } + # 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 + $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 # you can consider a non-problem resource as a resource # with only one part without loss } + # 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 $nonLinkedText = ""; # unlinked stuff after title # For each thing we're displaying... @@ -923,17 +1057,22 @@ sub new_handle { # If this is a new branch, label it so # (temporary, this should be an icon w/ alt text) if ($isNewBranch) { - $newBranchText = "NB -> "; + $newBranchText = ""; $isNewBranch = 0; + $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 @@ -941,14 +1080,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 = ""; @@ -960,25 +1100,90 @@ sub new_handle { } } - $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"); } } $curRes = $mapIterator->next(); @@ -986,6 +1191,8 @@ sub new_handle { $r->print("
\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; $i++) { + 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 ne "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\n"); + # 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" 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("
"); + $navmap->untieHashes(); + return OK; } @@ -1044,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) { @@ -1054,10 +1261,14 @@ sub getDescription { return "Opens: " . timeToHumanString($res->opendate($part)); } if ($status == $res->OPEN) { - return "Due: $status " . timeToHumanString($res->duedate($part)); + 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: " . timeToHumanString($res->answerdate($part)); + return "Answer opens: " . timeToHumanString($res->answerdate($part)); } if ($status == $res->PAST_DUE_NO_ANSWER) { return "Was Due: " . timeToHumanString($res->duedate($part)); @@ -1065,6 +1276,27 @@ sub getDescription { if ($status == $res->ANSWER_OPEN) { return "Answer available"; } + if ($status == $res->EXCUSED) { + 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"; + } + } +} + +sub advancedUser { + return $ENV{'user.adv'}; } # I want to change this into something more human-friendly. For @@ -1074,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'; } } @@ -1144,6 +1376,7 @@ sub new { if (!(tie(%parmhash, 'GDBM_File', $self->{PARM_HASH_FILE}, &GDBM_READER(), 0640))) { + untie $self->{PARM_HASH}; return undef; } $self->{PARM_HASH} = \%parmhash; @@ -1270,15 +1503,15 @@ sub courseMapDefined { sub getIterator { my $self = shift; my $iterator = Apache::lonnavmaps::iterator->new($self, shift, shift, - shift, shift); + shift, undef, shift); return $iterator; } # unties the hash when done sub untieHashes { my $self = shift; - untie $self->{NAV_HASH} if ($self->{HASH_TIED}); - untie $self->{PARM_HASH} if ($self->{HASH_TIED}); + untie %{$self->{NAV_HASH}} if ($self->{HASH_TIED}); + untie %{$self->{PARM_HASH}} if ($self->{HASH_TIED}); $self->{HASH_TIED} = 0; } @@ -1288,6 +1521,29 @@ sub DESTROY { $self->untieHashes(); } +# Private function: Does the given resource (as a symb string) have +# current discussion? Returns 0 if chat/mail data not extracted. +sub hasDiscussion { + my $self = shift; + my $symb = shift; + if (!defined($self->{DISCUSSION_TIME})) { return 0; } + + return $self->{DISCUSSION_TIME}->{$symb} > + $self->{LAST_CHECK}; +} + +# Private function: Does the given resource (as a symb string) have +# current feedback? Returns the string in the feedback hash, which +# will be false if it does not exist. +sub getFeedback { + my $self = shift; + my $symb = shift; + + if (!defined($self->{FEEDBACK})) { return ""; } + + return $self->{FEEDBACK}->{$symb}; +} + =pod =item * B(id): Based on the ID of the resource (1.1, 3.2, etc.), get a resource object for that resource. This method, or other methods that use it (as in the resource object) is the only proper way to obtain a resource object. @@ -1456,7 +1712,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. @@ -1508,6 +1764,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; @@ -1539,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}) @@ -1573,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) { @@ -1661,7 +1937,6 @@ sub next { $self->{FORCE_NEXT} = $self->END_BRANCH(); $self->{BRANCH_DEPTH}--; } - return $self->{HERE}; } while (@$next) { @@ -1677,20 +1952,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}; @@ -1763,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 @@ -1806,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); @@ -2020,6 +2299,30 @@ Misc. functions for the resource. =over 4 +=item * B: Returns a false value if there has been discussion since the user last logged in, true if there has. Always returns false if the discussion data was not extracted when the nav map was constructed. + +=item * B: Gets the feedback for the resource and returns the raw feedback string for the resource, or the null string if there is no feedback or the email data was not extracted when the nav map was constructed. Usually used like this: + + for (split(/\,/, $res->getFeedback())) { + my $link = &Apache::lonnet::escape($_); + ... + +and use the link as appropriate. + +=cut + +sub hasDiscussion { + my $self = shift; + return $self->{NAV_MAP}->hasDiscussion($self->symb()); +} + +sub getFeedback { + my $self = shift; + return $self->{NAV_MAP}->getFeedback($self->symb()); +} + +=pod + =item * B(): Returns a list reference containing sorted strings corresponding to each part of the problem. To count the number of parts, use the list in a scalar context, and subtract one if greater then two. (One part problems have a part 0. Multi-parts have a part 0, plus a part for each part. You may or may not wish to filter out part 0.) =item * B(): Returns the number of parts of the problem a student can answer. Thus, for single part problems, returns 1. For multipart, it returns the number of parts in the problem, not including psuedo-part 0. @@ -2031,6 +2334,8 @@ Misc. functions for the resource. sub parts { my $self = shift; + if ($self->ext) { return ['0']; } + $self->extractParts(); return $self->{PARTS}; } @@ -2039,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; } @@ -2050,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}}; @@ -2087,7 +2404,7 @@ Idiomatic usage of these two methods wou =over 4 -=item * B($part): ($part defaults to 0). A convenience function that returns a symbolic constant telling you about the date status of the part, with the following possible results: +=item * B($part): ($part defaults to 0). A convenience function that returns a symbolic constant telling you about the date status of the part. You should still check to see if there is a due date at all, if you care about that. The possible return values are: =back @@ -2099,6 +2416,7 @@ B =item * B: Open and not yet due. + =item * B: The due date has passed, but the answer date has not yet arrived. =item * B: The due date has passed and there is no answer opening date set. @@ -2146,10 +2464,10 @@ sub getDateStatus { # should this be the same as "open later"? think multipart. return $self->NOTHING_SET; } - if (!$open || $now < $open) {return $self->OPEN_LATER}; - if (!$due || $now < $due) {return $self->OPEN}; - if ($answer && $now < $answer) {return $self->PAST_DUE_ANSWER_LATER}; - if ($answer) { return $self->ANSWER_OPEN; }; + if (!$open || $now < $open) {return $self->OPEN_LATER} + if (!$due || $now < $due) {return $self->OPEN} + if ($answer && $now < $answer) {return $self->PAST_DUE_ANSWER_LATER} + if ($answer) { return $self->ANSWER_OPEN; } return PAST_DUE_NO_ANSWER; } @@ -2181,6 +2499,8 @@ B =item * B: Information not available due to network failure. +=item * B: Attempted, and not yet graded. + =back =cut @@ -2191,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; @@ -2209,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; } @@ -2222,7 +2544,7 @@ Along with directly returning the date o =item * NETWORK_FAILURE: The network has failed and the information is not available. -=item * NOTHING_SET: No dates have been set for this problem (part) at all. (Because only certain parts of a multi-part problem may be assigned, this can not be collapsed into "open later", as we don't know a given part will EVER be opened.) +=item * NOTHING_SET: No dates have been set for this problem (part) at all. (Because only certain parts of a multi-part problem may be assigned, this can not be collapsed into "open later", as we don't know a given part will EVER be opened. For single part, this is the same as "OPEN_LATER".) =item * CORRECT: For any reason at all, the part is considered correct. @@ -2242,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 @@ -2264,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 @@ -2276,11 +2604,11 @@ 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) { + $dateStatus == PAST_DUE_NO_ANSWER ) { return $dateStatus; } @@ -2332,9 +2660,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; } }