--- loncom/interface/lonnavmaps.pm 2003/06/23 15:53:35 1.209 +++ loncom/interface/lonnavmaps.pm 2007/08/29 00:55:37 1.400 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Navigate Maps Handler # -# $Id: lonnavmaps.pm,v 1.209 2003/06/23 15:53:35 bowersj2 Exp $ +# $Id: lonnavmaps.pm,v 1.400 2007/08/29 00:55:37 albertel Exp $ # # Copyright Michigan State University Board of Trustees # @@ -25,29 +25,20 @@ # # http://www.lon-capa.org/ # -# (Page Handler -# -# (TeX Content Handler -# -# 05/29/00,05/30 Gerd Kortemeyer) -# 08/30,08/31,09/06,09/14,09/15,09/16,09/19,09/20,09/21,09/23, -# 10/02,10/10,10/14,10/16,10/18,10/19,10/31,11/6,11/14,11/16 Gerd Kortemeyer) -# -# 3/1/1,6/1,17/1,29/1,30/1,2/8,9/21,9/24,9/25 Gerd Kortemeyer -# YEAR=2002 -# 1/1 Gerd Kortemeyer -# Oct-Nov Jeremy Bowers -# YEAR=2003 -# Jeremy Bowers ... lots of days +### package Apache::lonnavmaps; use strict; -use Apache::Constants qw(:common :http); +use GDBM_File; use Apache::loncommon(); -use Apache::lonmenu(); +use Apache::lonenc(); +use Apache::lonlocal; +use Apache::lonnet; use POSIX qw (floor strftime); -use Data::Dumper; # for debugging, not always used +use Data::Dumper; # for debugging, not always +use Time::HiRes qw( gettimeofday tv_interval ); +use LONCAPA; # symbolic constants sub SYMB { return 1; } @@ -61,19 +52,15 @@ my $resObj = "Apache::lonnavmaps::resour # Keep these mappings in sync with lonquickgrades, which uses the colors # instead of the icons. my %statusIconMap = - ( $resObj->NETWORK_FAILURE => '', - $resObj->NOTHING_SET => '', - $resObj->CORRECT => 'navmap.correct.gif', - $resObj->EXCUSED => 'navmap.correct.gif', - $resObj->PAST_DUE_NO_ANSWER => 'navmap.wrong.gif', - $resObj->PAST_DUE_ANSWER_LATER => 'navmap.wrong.gif', - $resObj->ANSWER_OPEN => 'navmap.wrong.gif', - $resObj->OPEN_LATER => '', - $resObj->TRIES_LEFT => 'navmap.open.gif', - $resObj->INCORRECT => 'navmap.wrong.gif', - $resObj->OPEN => 'navmap.open.gif', - $resObj->ATTEMPTED => 'navmap.ellipsis.gif', - $resObj->ANSWER_SUBMITTED => '' ); + ( + $resObj->CLOSED => '', + $resObj->OPEN => 'navmap.open.gif', + $resObj->CORRECT => 'navmap.correct.gif', + $resObj->PARTIALLY_CORRECT => 'navmap.partial.gif', + $resObj->INCORRECT => 'navmap.wrong.gif', + $resObj->ATTEMPTED => 'navmap.ellipsis.gif', + $resObj->ERROR => '' + ); my %iconAltTags = ( 'navmap.correct.gif' => 'Correct', @@ -94,208 +81,39 @@ my %colormap = $resObj->OPEN => '', $resObj->NOTHING_SET => '', $resObj->ATTEMPTED => '', - $resObj->ANSWER_SUBMITTED => '' + $resObj->ANSWER_SUBMITTED => '', + $resObj->PARTIALLY_CORRECT => '#006600' ); # 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 +# is not yet done and due in less than 24 hours my $hurryUpColor = "#FF0000"; -sub handler { - my $r = shift; - real_handler($r); -} - -sub real_handler { - my $r = shift; - - # Handle header-only request - if ($r->header_only) { - if ($ENV{'browser.mathml'}) { - $r->content_type('text/xml'); - } else { - $r->content_type('text/html'); - } - $r->send_http_header; - return OK; - } - - # Send header, don't cache this page - if ($ENV{'browser.mathml'}) { - $r->content_type('text/xml'); - } else { - $r->content_type('text/html'); - } - &Apache::loncommon::no_cache($r); - $r->send_http_header; - - # Create the nav map - my $navmap = Apache::lonnavmaps::navmap->new( - $ENV{"request.course.fn"}.".db", - $ENV{"request.course.fn"}."_parms.db", 1, 1); - - - if (!defined($navmap)) { - my $requrl = $r->uri; - $ENV{'user.error.msg'} = "$requrl:bre:0:0:Course not initialized"; - return HTTP_NOT_ACCEPTABLE; - } - - $r->print("\n"); - $r->print("Navigate Course Contents"); -# ------------------------------------------------------------ Get query string - &Apache::loncommon::get_unprocessed_cgi($ENV{'QUERY_STRING'},['register']); - -# ----------------------------------------------------- Force menu registration - my $addentries=''; - if ($ENV{'form.register'}) { - $addentries=' onLoad="'.&Apache::lonmenu::loadevents(). - '" onUnload="'.&Apache::lonmenu::unloadevents().'"'; - $r->print(&Apache::lonmenu::registerurl(1)); - } - - # Header - $r->print(''. - &Apache::loncommon::bodytag('Navigate Course Contents','', - $addentries,'','',$ENV{'form.register'})); - $r->print(''); - - $r->rflush(); - - # Now that we've displayed some stuff to the user, init the navmap - $navmap->init(); - - $r->rflush(); - - # Check that it's defined - if (!($navmap->courseMapDefined())) { - $r->print('Coursemap undefined.' . - ''); - return OK; - } - - # See if there's only one map in the top-level, if we don't - # already have a filter... if so, automatically display it - if ($ENV{QUERY_STRING} !~ /filter/) { - my $iterator = $navmap->getIterator(undef, undef, undef, 0); - my $depth = 1; - $iterator->next(); - my $curRes = $iterator->next(); - my $sequenceCount = 0; - my $sequenceId; - while ($depth > 0) { - if ($curRes == $iterator->BEGIN_MAP()) { $depth++; } - if ($curRes == $iterator->END_MAP()) { $depth--; } - - if (ref($curRes) && $curRes->is_sequence()) { - $sequenceCount++; - $sequenceId = $curRes->map_pc(); - } - - $curRes = $iterator->next(); - } - - if ($sequenceCount == 1) { - # The automatic iterator creation in the render call - # will pick this up. We know the condition because - # the defined($ENV{'form.filter'}) also ensures this - # is a fresh call. - $ENV{'form.filter'} = "$sequenceId"; - } - } - - my $jumpToFirstHomework = 0; - # Check to see if the student is jumping to next open, do-able problem - if ($ENV{QUERY_STRING} eq 'jumpToFirstHomework') { - $jumpToFirstHomework = 1; - # Find the next homework problem that they can do. - my $iterator = $navmap->getIterator(undef, undef, undef, 1); - my $depth = 1; - $iterator->next(); - my $curRes = $iterator->next(); - my $foundDoableProblem = 0; - my $problemRes; - - while ($depth > 0 && !$foundDoableProblem) { - if ($curRes == $iterator->BEGIN_MAP()) { $depth++; } - if ($curRes == $iterator->END_MAP()) { $depth--; } - - if (ref($curRes) && $curRes->is_problem()) { - my $status = $curRes->status(); - if ($curRes->completable()) { - $problemRes = $curRes; - $foundDoableProblem = 1; - - # Pop open all previous maps - my $stack = $iterator->getStack(); - pop @$stack; # last resource in the stack is the problem - # itself, which we don't need in the map stack - my @mapPcs = map {$_->map_pc()} @$stack; - $ENV{'form.filter'} = join(',', @mapPcs); - - # Mark as both "here" and "jump" - $ENV{'form.postsymb'} = $curRes->symb(); - } - } - } continue { - $curRes = $iterator->next(); - } - - # If we found no problems, print a note to that effect. - if (!$foundDoableProblem) { - $r->print("All homework assignments have been completed.

"); - } - } else { - $r->print("" . - "Go To My First Homework Problem    "); - } - - my $suppressEmptySequences = 0; - my $filterFunc = undef; - my $resource_no_folder_link = 0; - - # Display only due homework. - my $showOnlyHomework = 0; - if ($ENV{QUERY_STRING} eq 'showOnlyHomework') { - $showOnlyHomework = 1; - $suppressEmptySequences = 1; - $filterFunc = sub { my $res = shift; - return $res->completable() || $res->is_map(); - }; - $r->print("

Uncompleted Homework

"); - $ENV{'form.filter'} = ''; - $ENV{'form.condition'} = 1; - $resource_no_folder_link = 1; - } else { - $r->print("" . - "Show Only Uncompleted Homework    "); - } - - # renderer call - my $renderArgs = { 'cols' => [0,1,2,3], - 'url' => '/adm/navmaps', - 'navmap' => $navmap, - 'suppressNavmap' => 1, - 'suppressEmptySequences' => $suppressEmptySequences, - 'filterFunc' => $filterFunc, - 'resource_no_folder_link' => $resource_no_folder_link, - 'r' => $r}; - my $render = render($renderArgs); - $navmap->untieHashes(); - - # If no resources were printed, print a reassuring message so the - # user knows there was no error. - if ($renderArgs->{'counter'} == 0) { - if ($showOnlyHomework) { - $r->print("

All homework is currently completed.

"); - } else { # both jumpToFirstHomework and normal use the same: course must be empty - $r->print("

This course is empty.

"); - } - } - - $r->print(""); - $r->rflush(); - - return OK; +sub close { + if ($env{'environment.remotenavmap'} ne 'on') { return ''; } + return(< +window.status='Accessing Nav Control'; +menu=window.open("/adm/rat/empty.html","loncapanav", + "height=600,width=400,scrollbars=1"); +window.status='Closing Nav Control'; +menu.close(); +window.status='Done.'; + +ENDCLOSE +} + +sub update { + if ($env{'environment.remotenavmap'} ne 'on') { return ''; } + if (!$env{'request.course.id'}) { return ''; } + if ($ENV{'REQUEST_URI'}=~m|^/adm/navmaps|) { return ''; } + return(< + +ENDUPDATE } # Convenience functions: Returns a string that adds or subtracts @@ -321,15 +139,26 @@ sub removeFromFilter { # Convenience function: Given a stack returned from getStack on the iterator, # return the correct src() value. -# Later, this should add an anchor when we start putting anchors in pages. sub getLinkForResource { my $stack = shift; my $res; # Check to see if there are any pages in the stack foreach $res (@$stack) { - if (defined($res) && $res->is_page()) { - return $res->src(); + if (defined($res)) { + my $anchor; + if ($res->is_page()) { + foreach my $item (@$stack) { if (defined($item)) { $anchor = $item; } } + $anchor=&escape($anchor->shown_symb()); + return ($res->link(),$res->shown_symb(),$anchor); + } + # in case folder was skipped over as "only sequence" + my ($map,$id,$src)=&Apache::lonnet::decode_symb($res->symb()); + if ($map=~/\.page$/) { + my $url=&Apache::lonnet::clutter($map); + $anchor=&escape($src->shown_symb()); + return ($url,$res->shown_symb(),$anchor); + } } } @@ -337,16 +166,16 @@ sub getLinkForResource { # (when we first recurse on a map, it puts an undefined resource # on the bottom because $self->{HERE} isn't defined yet, and we # want the src for the map anyhow) - foreach (@$stack) { - if (defined($_)) { $res = $_; } + foreach my $item (@$stack) { + if (defined($item)) { $res = $item; } } - return $res->src(); + return ($res->link(),$res->shown_symb()); } -# Convenience function: This seperates the logic of how to create +# Convenience function: This separates the logic of how to create # the problem text strings ("Due: DATE", "Open: DATE", "Not yet assigned", -# etc.) into a seperate function. It takes a resource object as the +# etc.) into a separate function. It takes a resource object as the # first parameter, and the part number of the resource as the second. # It's basically a big switch statement on the status of the resource. @@ -356,35 +185,36 @@ sub getDescription { my $status = $res->status($part); if ($status == $res->NETWORK_FAILURE) { - return "Having technical difficulties; please check status later"; + return &mt("Having technical difficulties; please check status later"); } if ($status == $res->NOTHING_SET) { - return "Not currently assigned."; + return &mt("Not currently assigned."); } if ($status == $res->OPEN_LATER) { - return "Open " . timeToHumanString($res->opendate($part)); + return "Open " . timeToHumanString($res->opendate($part),'start'); } if ($status == $res->OPEN) { if ($res->duedate($part)) { - return "Due " . timeToHumanString($res->duedate($part)); + return &mt("Due")." " .timeToHumanString($res->duedate($part),'end'); } else { - return "Open, no due date"; + return &mt("Open, no due date"); } } if ($status == $res->PAST_DUE_ANSWER_LATER) { - return "Answer open " . timeToHumanString($res->answerdate($part)); + return &mt("Answer open")." " . timeToHumanString($res->answerdate($part),'start'); } if ($status == $res->PAST_DUE_NO_ANSWER) { - return "Was due " . timeToHumanString($res->duedate($part)); + return &mt("Was due")." " . timeToHumanString($res->duedate($part),'end'); } - if ($status == $res->ANSWER_OPEN) { - return "Answer available"; + if (($status == $res->ANSWER_OPEN || $status == $res->PARTIALLY_CORRECT) + && $res->handgrade($part) ne 'yes') { + return &mt("Answer available"); } if ($status == $res->EXCUSED) { - return "Excused by instructor"; + return &mt("Excused by instructor"); } if ($status == $res->ATTEMPTED) { - return "Answer submitted, not yet graded."; + return &mt("Answer submitted, not yet graded"); } if ($status == $res->TRIES_LEFT) { my $tries = $res->tries($part); @@ -396,34 +226,34 @@ sub getDescription { $triesString = "$triesString"; } } - if ($res->duedate()) { - return "Due " . timeToHumanString($res->duedate($part)) . + if ($res->duedate($part)) { + return &mt("Due")." " . timeToHumanString($res->duedate($part),'end') . " $triesString"; } else { - return "No due date $triesString"; + return &mt("No due date")." $triesString"; } } if ($status == $res->ANSWER_SUBMITTED) { - return 'Answer submitted'; + return &mt('Answer submitted'); } } -# Convenience function, so others can use it: Is the problem due in less then +# Convenience function, so others can use it: Is the problem due in less than # 24 hours, and still can be done? -sub dueInLessThen24Hours { +sub dueInLessThan24Hours { my $res = shift; my $part = shift; my $status = $res->status($part); return ($status == $res->OPEN() || $status == $res->TRIES_LEFT()) && - $res->duedate() && $res->duedate() < time()+(24*60*60) && - $res->duedate() > time(); + $res->duedate($part) && $res->duedate($part) < time()+(24*60*60) && + $res->duedate($part) > time(); } # Convenience function, so others can use it: Is there only one try remaining for the -# part, with more then one try to begin with, not due yet and still can be done? +# part, with more than one try to begin with, not due yet and still can be done? sub lastTry { my $res = shift; my $part = shift; @@ -431,14 +261,14 @@ sub lastTry { my $tries = $res->tries($part); my $maxtries = $res->maxtries($part); return $tries && $maxtries && $maxtries > 1 && - $maxtries - $tries == 1 && $res->duedate() && - $res->duedate() > time(); + $maxtries - $tries == 1 && $res->duedate($part) && + $res->duedate($part) > time(); } -# This puts a human-readable name on the ENV variable. +# This puts a human-readable name on the env variable. sub advancedUser { - return $ENV{'request.role.adv'}; + return $env{'request.role.adv'}; } @@ -450,13 +280,18 @@ sub advancedUser { # print "Answer available $timestring" # Very, very, very, VERY English-only... goodness help a localizer on # this func... + + sub timeToHumanString { - my ($time) = @_; + my ($time,$type,$format) = @_; + # zero, '0' and blank are bad times if (!$time) { - return 'never'; + return &mt('never'); } - + unless (&Apache::lonlocal::current_language()=~/^en/) { + return &Apache::lonlocal::locallocaltime($time); + } my $now = time(); my @time = localtime($time); @@ -487,13 +322,13 @@ sub timeToHumanString { my $tense = $inPast ? " ago" : ""; my $prefix = $inPast ? "" : "in "; - # Less then a minute + # Less than a minute if ( $delta < $minute ) { if ($delta == 1) { return "${prefix}1 second$tense"; } return "$prefix$delta seconds$tense"; } - # Less then an hour + # Less than an hour if ( $delta < $hour ) { # If so, use minutes my $minutes = floor($delta / 60); @@ -501,7 +336,7 @@ sub timeToHumanString { return "$prefix$minutes minutes$tense"; } - # Is it less then 24 hours away? If so, + # Is it less than 24 hours away? If so, # display hours + minutes if ( $delta < $hour * 24) { my $hours = floor($delta / $hour); @@ -520,30 +355,44 @@ sub timeToHumanString { return "$prefix$hourString$minuteString$tense"; } - # Less then 5 days away, display day of the week and + # If there's a caller supplied format, use it. + + if($format ne '') { + my $timeStr = strftime($format, localtime($time)); + return $timeStr.&Apache::lonlocal::gettimezone($time); + } + + # Less than 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 am/00:00/; $timeStr =~ s/12:00 pm/noon/; - return ($inPast ? "last " : "next ") . - $timeStr; + return ($inPast ? "last " : "this ") . + $timeStr.&Apache::lonlocal::gettimezone($time); } + my $conjunction='on'; + if ($type eq 'start') { + $conjunction='at'; + } elsif ($type eq 'end') { + $conjunction='by'; + } # 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/; + my $timeStr = strftime("$conjunction %A, %b %e at %I:%M %P", localtime($time)); + $timeStr =~ s/12:00 am/00:00/; $timeStr =~ s/12:00 pm/noon/; - return $timeStr; + return $timeStr.&Apache::lonlocal::gettimezone($time); } # 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/; + my $timeStr = strftime("$conjunction %A, %b %e %Y at %I:%M %P", localtime($time)); + $timeStr =~ s/12:00 am/00:00/; $timeStr =~ s/12:00 pm/noon/; - return $timeStr; + return $timeStr.&Apache::lonlocal::gettimezone($time); } } @@ -552,7 +401,8 @@ sub timeToHumanString { =head1 NAME -Apache::lonnavmap - Subroutines to handle and render the navigation maps +Apache::lonnavmap - Subroutines to handle and render the navigation + maps =head1 SYNOPSIS @@ -560,18 +410,62 @@ The main handler generates the navigatio the other objects export this information in a usable fashion for other modules. +=head1 OVERVIEW + +X When a user enters a course, LON-CAPA examines the +course structure and caches it in what is often referred to as the +"big hash" X. You can see it if you are logged into +LON-CAPA, in a course, by going to /adm/test. (You may need to +tweak the /home/httpd/lonTabs/htpasswd file to view it.) The +content of the hash will be under the heading "Big Hash". + +Big Hash contains, among other things, how resources are related +to each other (next/previous), what resources are maps, which +resources are being chosen to not show to the student (for random +selection), and a lot of other things that can take a lot of time +to compute due to the amount of data that needs to be collected and +processed. + +Apache::lonnavmaps provides an object model for manipulating this +information in a higher-level fashion than directly manipulating +the hash. It also provides access to several auxilary functions +that aren't necessarily stored in the Big Hash, but are a per- +resource sort of value, like whether there is any feedback on +a given resource. + +Apache::lonnavmaps also abstracts away branching, and someday, +conditions, for the times where you don't really care about those +things. + +Apache::lonnavmaps also provides fairly powerful routines for +rendering navmaps, and last but not least, provides the navmaps +view for when the user clicks the NAV button. + +B: Apache::lonnavmaps I works for the "currently +logged in user"; if you want things like "due dates for another +student" lonnavmaps can not directly retrieve information like +that. You need the EXT function. This module can still help, +because many things, such as the course structure, are constant +between users, and Apache::lonnavmaps can help by providing +symbs for the EXT call. + +The rest of this file will cover the provided rendering routines, +which can often be used without fiddling with the navmap object at +all, then documents the Apache::lonnavmaps::navmap object, which +is the key to accessing the Big Hash information, covers the use +of the Iterator (which provides the logic for traversing the +somewhat-complicated Big Hash data structure), documents the +Apache::lonnavmaps::Resource objects that are returned by + =head1 Subroutine: render The navmap renderer package provides a sophisticated rendering of the standard navigation maps interface into HTML. The provided nav map handler is actually just a glorified call to this. -Because of the large number of parameters this function presents, +Because of the large number of parameters this function accepts, instead of passing it arguments as is normal, pass it in an anonymous -hash with the given options. This is because there is no obvious order -you may wish to override these in and a hash is easier to read and -understand then "undef, undef, undef, 1, undef, undef, renderButton, -undef, 0" when you mostly want default behaviors. +hash with the desired options. The package provides a function called 'render', called as Apache::lonnavmaps::render({}). @@ -579,7 +473,7 @@ Apache::lonnavmaps::render({}). =head2 Overview of Columns The renderer will build an HTML table for the navmap and return -it. The table is consists of several columns, and a row for each +it. The table consists of several columns, and a row for each resource (or possibly each part). You tell the renderer how many columns to create and what to place in each column, optionally using one or more of the prepared columns, and the renderer will assemble @@ -599,29 +493,33 @@ that takes a resource reference, a part argument hash passed to the renderer, and returns a string that will be inserted into the HTML representation as it. +All other parameters are ways of either changing how the columns +are printing, or which rows are shown. + The pre-packaged column names are refered to by constants in the Apache::lonnavmaps namespace. The following currently exist: =over 4 -=item * B: +=item * B: The general info about the resource: Link, icon for the type, etc. The -first column in the standard nav map display. This column also accepts +first column in the standard nav map display. This column provides the +indentation effect seen in the B