--- loncom/interface/lonnavmaps.pm 2003/05/29 17:39:41 1.194 +++ loncom/interface/lonnavmaps.pm 2008/12/12 22:27:20 1.415.2.2 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Navigate Maps Handler # -# $Id: lonnavmaps.pm,v 1.194 2003/05/29 17:39:41 bowersj2 Exp $ +# $Id: lonnavmaps.pm,v 1.415.2.2 2008/12/12 22:27:20 raeburn 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 Time::HiRes qw( gettimeofday tv_interval ); +use LONCAPA; +use DateTime(); # 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.open.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', @@ -92,204 +79,41 @@ my %colormap = $resObj->TRIES_LEFT => '', $resObj->INCORRECT => '', $resObj->OPEN => '', - $resObj->NOTHING_SET => '' ); + $resObj->NOTHING_SET => '', + $resObj->ATTEMPTED => '', + $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->print('
 '); - $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; - # 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_sequence(); - }; - $r->print("

Uncompleted Homework

"); - $ENV{'form.filter'} = ''; - $ENV{'form.condition'} = 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, - '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 @@ -315,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); + } } } @@ -331,16 +166,19 @@ 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(); + if ($res) { + return ($res->link(),$res->shown_symb()); + } + return; } -# 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. @@ -349,75 +187,88 @@ sub getDescription { my $part = shift; my $status = $res->status($part); + my $open = $res->opendate($part); + my $due = $res->duedate($part); + my $answer = $res->answerdate($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 &mt("Open ") .timeToHumanString($open,'start'); } if ($status == $res->OPEN) { - if ($res->duedate($part)) { - return "Due " . timeToHumanString($res->duedate($part)); + if ($due) { + if ($res->is_practice()) { + return &mt("Closes ")." " .timeToHumanString($due,'start'); + } else { + return &mt("Due")." " .timeToHumanString($due,'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($answer,'start'); } if ($status == $res->PAST_DUE_NO_ANSWER) { - return "Was due " . timeToHumanString($res->duedate($part)); - } - if ($status == $res->ANSWER_OPEN) { - return "Answer available"; + if ($res->is_practice()) { + return &mt("Closed")." " . timeToHumanString($due,'start'); + } else { + return &mt("Was due")." " . timeToHumanString($due,'end'); + } + } + 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 "Not yet graded."; + return &mt("Answer submitted, not yet graded"); } if ($status == $res->TRIES_LEFT) { my $tries = $res->tries($part); my $maxtries = $res->maxtries($part); my $triesString = ""; if ($tries && $maxtries) { - $triesString = "($tries of $maxtries tries used)"; + $triesString = '('.&mt('[_1] of [quant,_2,try,tries] used',$tries,$maxtries).')'; if ($maxtries > 1 && $maxtries - $tries == 1) { $triesString = "$triesString"; } } - if ($res->duedate()) { - return "Due " . timeToHumanString($res->duedate($part)) . + if ($due) { + return &mt("Due")." " . timeToHumanString($due,'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->ATTEMPTED() || + 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; @@ -425,14 +276,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'}; } @@ -444,18 +295,20 @@ 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); - my @now = localtime($now); - # Positive = future my $delta = $time - $now; @@ -481,13 +334,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); @@ -495,7 +348,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); @@ -514,28 +367,48 @@ sub timeToHumanString { return "$prefix$hourString$minuteString$tense"; } - # Less then 5 days away, display day of the week and + my $dt = DateTime->from_epoch(epoch => $time) + ->set_time_zone(&Apache::lonlocal::gettimezone()); + + # If there's a caller supplied format, use it. + + if ($format ne '') { + my $timeStr = $dt->strftime($format); + return $timeStr.' ('.$dt->time_zone_short_name().')'; + } + + # 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/; + my $timeStr = $dt->strftime("%A, %b %e at %I:%M %P (%Z)"); + $timeStr =~ s/12:00 am/00:00/; $timeStr =~ s/12:00 pm/noon/; - return ($inPast ? "last " : "next ") . + return ($inPast ? "last " : "this ") . $timeStr; } + my $conjunction='on'; + if ($type eq 'start') { + $conjunction='at'; + } elsif ($type eq 'end') { + $conjunction='by'; + } # Is it this year? - if ( $time[5] == $now[5]) { + my $dt_now = DateTime->from_epoch(epoch => $now) + ->set_time_zone(&Apache::lonlocal::gettimezone()); + if ( $dt->year() == $dt_now->year()) { # 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 = $dt->strftime("$conjunction %A, %b %e at %I:%M %P (%Z)"); + $timeStr =~ s/12:00 am/00:00/; $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/; + my $timeStr = + $dt->strftime("$conjunction %A, %b %e %Y at %I:%M %P (%Z)"); + $timeStr =~ s/12:00 am/00:00/; $timeStr =~ s/12:00 pm/noon/; return $timeStr; } @@ -546,37 +419,82 @@ 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 The main handler generates the navigational listing for the course, the other objects export this information in a usable fashion for -other modules +other modules. -=head1 Object: render +=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::renderer->render({}). +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 preparent columns, and the renderer will assemble +one or more of the prepared columns, and the renderer will assemble the table. Any additional generally useful column types should be placed in the @@ -593,29 +511,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::renderer namespace. The following currently exist: +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 -the following parameter in the renderer hash: +first column in the standard nav map display. This column provides the +indentation effect seen in the B