--- loncom/interface/lonnavmaps.pm 2002/10/08 20:39:43 1.71 +++ loncom/interface/lonnavmaps.pm 2002/10/14 18:48:13 1.81 @@ -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.81 2002/10/14 18:48:13 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; @@ -822,7 +823,7 @@ sub new_handle { # Initialize 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,7 +843,7 @@ 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'. @@ -916,7 +917,13 @@ sub new_handle { my $currenturl = $ENV{'form.postdata'}; $currenturl=~s/^http\:\/\///; $currenturl=~s/^[^\/]+//; - my $queryAdd = "postdata=" . &Apache::lonnet::escape($currenturl); + # 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"; $r->print('Show All Resources

'); @@ -929,17 +936,57 @@ sub new_handle { $condition = 1; } - my $mapIterator = $navmap->getIterator(undef, undef, \%filterHash, $condition); - my $curRes = $mapIterator->next(); + # 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. + # If there is a current resource + if ($currenturl && !$ENV{'form.alreadyHere'}) { + # Give me every resource... + my $mapIterator = $navmap->getIterator(undef, undef, {}, 1); + my $found != 0; + my $depth = 1; + $mapIterator->next(); # discard the first BEGIN_MAP + my $curRes = $mapIterator->next(); + + while ($depth > 0 && !$found) { + if (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. + + my $mapStack = $mapIterator->getStack(); + for my $map (@{$mapStack}) { + if ($condition) { + undef $filterHash{$map->map_pc()}; + } else { + $filterHash{$map->map_pc()} = 1; + } + } + $found = 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 $depth = 1; + my $displayedHereMarker = 0; + + # We know the first thing is a BEGIN_MAP (see "$self->{STARTED}" + # code in iterator->next), so ignore the first one + my $mapIterator = $navmap->getIterator(undef, undef, \%filterHash, + $condition); + $mapIterator->next(); + my $curRes = $mapIterator->next(); - while ($curRes != $mapIterator->END_MAP()) { + while ($depth > 0) { if ($curRes == $mapIterator->BEGIN_MAP() || $curRes == $mapIterator->BEGIN_BRANCH()) { $indentLevel++; @@ -951,6 +998,12 @@ sub new_handle { if ($curRes == $mapIterator->BEGIN_BRANCH()) { $isNewBranch = 1; } + if ($curRes == $mapIterator->BEGIN_MAP()) { + $depth++; + } + if ($curRes == $mapIterator->END_MAP()) { + $depth--; + } # Is this resource being blotted out? if (ref($curRes) && !advancedUser() && $curRes->randomout()) { @@ -1024,7 +1077,7 @@ sub new_handle { } 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 + # with only one part without loss } # Display one part, in event of network error. @@ -1051,6 +1104,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 = ""; @@ -1080,7 +1137,7 @@ sub new_handle { my $mapId = $curRes->map_pc(); my $nowOpen = !defined($filterHash{$mapId}); $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}; + + # Special case in the navmaps: If in less then + # 24 hours, give it a bit of urgency + if ($status == $curRes->OPEN() && $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, give a bit of urgency + my $tries = $curRes->tries($part); + my $maxtries = $curRes->maxtries($part); + if ($tries && $maxtries && $maxtries > 1 && + $maxtries - $tries == 1) { + $color = $hurryUpColor; + } if ($color ne "") { $colorizer = "bgcolor=\"$color\""; } @@ -1105,7 +1178,7 @@ sub new_handle { } # FIRST COL: The resource indentation, branch icon, and name - $r->print(" \n"); + $r->print(" \n"); # print indentation for (my $i = 0; $i < $indentLevel - $deltalevel; $i++) { @@ -1114,6 +1187,16 @@ sub new_handle { $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,7 +1205,7 @@ 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", @@ -1150,7 +1233,7 @@ sub new_handle { } } - $r->print("$discussionHTML$feedbackHTML"); + $r->print("$discussionHTML$feedbackHTML"); # Is this the first displayed part of a multi-part problem # that has not been condensed, so we should suppress these two @@ -1163,7 +1246,7 @@ sub new_handle { my $icon = $statusIconMap{$curRes->status($part)}; my $alt = $iconAltTags{$icon}; if ($icon) { - $r->print("$linkopen\"$alt\"$linkclose\n"); + $r->print("$linkopen\"$alt\"$linkclose\n"); } else { $r->print("\n"); } @@ -1172,7 +1255,7 @@ sub new_handle { } # FOURTH COL: Text description - $r->print("\n"); + $r->print("\n"); if ($curRes->kind() eq "res" && $curRes->is_problem() && @@ -1258,20 +1341,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 +1366,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 +1388,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 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; } } @@ -2667,7 +2848,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