--- loncom/interface/lonnavmaps.pm 2001/01/06 13:45:37 1.2 +++ loncom/interface/lonnavmaps.pm 2007/04/27 17:59:50 1.398 @@ -1,247 +1,4776 @@ # The LearningOnline Network with CAPA # Navigate Maps Handler # -# (Page Handler +# $Id: lonnavmaps.pm,v 1.398 2007/04/27 17:59:50 banghart Exp $ # -# (TeX Content Handler +# Copyright Michigan State University Board of Trustees # -# 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) +# This file is part of the LearningOnline Network with CAPA (LON-CAPA). # -# 3/1/1,6/1 Gerd Kortemeyer +# LON-CAPA is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# LON-CAPA is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with LON-CAPA; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# /home/httpd/html/adm/gpl.txt +# +# http://www.lon-capa.org/ +# +### package Apache::lonnavmaps; use strict; -use Apache::Constants qw(:common :http); -use Apache::lonnet(); -use HTML::TokeParser; use GDBM_File; +use Apache::loncommon(); +use Apache::lonenc(); +use Apache::lonlocal; +use Apache::lonnet; +use POSIX qw (floor strftime); +use Data::Dumper; # for debugging, not always +use Time::HiRes qw( gettimeofday tv_interval ); +use LONCAPA; + +# symbolic constants +sub SYMB { return 1; } +sub URL { return 2; } +sub NOTHING { return 3; } + +# Some data + +my $resObj = "Apache::lonnavmaps::resource"; + +# Keep these mappings in sync with lonquickgrades, which uses the colors +# instead of the icons. +my %statusIconMap = + ( + $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', + 'navmap.wrong.gif' => 'Incorrect', + 'navmap.open.gif' => 'Open' ); + +# Defines a status->color mapping, null string means don't color +my %colormap = + ( $resObj->NETWORK_FAILURE => '', + $resObj->CORRECT => '', + $resObj->EXCUSED => '#3333FF', + $resObj->PAST_DUE_ANSWER_LATER => '', + $resObj->PAST_DUE_NO_ANSWER => '', + $resObj->ANSWER_OPEN => '#006600', + $resObj->OPEN_LATER => '', + $resObj->TRIES_LEFT => '', + $resObj->INCORRECT => '', + $resObj->OPEN => '', + $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 than 24 hours +my $hurryUpColor = "#FF0000"; + +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 +# the second argument from the first hash, appropriate for the +# query string that determines which folders to recurse on +sub addToFilter { + my $hashIn = shift; + my $addition = shift; + my %hash = %$hashIn; + $hash{$addition} = 1; + + return join (",", keys(%hash)); +} + +sub removeFromFilter { + my $hashIn = shift; + my $subtraction = shift; + my %hash = %$hashIn; -# -------------------------------------------------------------- Module Globals -my %hash; -my @rows; - -# ------------------------------------------------------------------ Euclid gcd - -sub euclid { - my ($e,$f)=@_; - my $a; my $b; my $r; - if ($e>$f) { $b=$e; $r=$f; } else { $r=$e; $b=$f; } - while ($r!=0) { - $a=$b; $b=$r; - $r=$a%$b; - } - return $b; -} - -# ------------------------------------------------------------ Build page table - -sub tracetable { - my ($sofar,$rid,$beenhere)=@_; - my $further=$sofar; - unless ($beenhere=~/\&$rid\&/) { - $beenhere.=$rid.'&'; - - if (defined($hash{'is_map_'.$rid})) { - if ((defined($hash{'map_start_'.$hash{'src_'.$rid}})) && - (defined($hash{'map_finish_'.$hash{'src_'.$rid}}))) { - my $frid=$hash{'map_finish_'.$hash{'src_'.$rid}}; - $sofar= - &tracetable($sofar,$hash{'map_start_'.$hash{'src_'.$rid}}, - '&'.$frid.'&'); - $sofar++; - if ($hash{'src_'.$frid}) { - my $brepriv=&Apache::lonnet::allowed('bre',$hash{'src_'.$frid}); - if (($brepriv eq '2') || ($brepriv eq 'F')) { - if (defined($rows[$sofar])) { - $rows[$sofar].='&'.$frid; - } else { - $rows[$sofar]=$frid; - } - } - } - } - } else { - $sofar++; - if ($hash{'src_'.$rid}) { - my $brepriv=&Apache::lonnet::allowed('bre',$hash{'src_'.$rid}); - if (($brepriv eq '2') || ($brepriv eq 'F')) { - if (defined($rows[$sofar])) { - $rows[$sofar].='&'.$rid; - } else { - $rows[$sofar]=$rid; - } - } - } - } - - if (defined($hash{'to_'.$rid})) { - my $mincond=1; - my $next=''; - map { - my $thiscond= - &Apache::lonnet::directcondval($hash{'condid_'.$hash{'undercond_'.$_}}); - if ($thiscond>=$mincond) { - if ($next) { - $next.=','.$_.':'.$thiscond; - } else { - $next=$_.':'.$thiscond; - } - if ($thiscond>$mincond) { $mincond=$thiscond; } - } - } split(/\,/,$hash{'to_'.$rid}); - map { - my ($linkid,$condval)=split(/\:/,$_); - if ($condval>=$mincond) { - my $now=&tracetable($sofar,$hash{'goesto_'.$linkid},$beenhere); - if ($now>$further) { $further=$now; } - } - } split(/\,/,$next); - - } - } - return $further; -} - -# ================================================================ Main Handler - -sub handler { - my $r=shift; - - -# ------------------------------------------- Set document type for header only - - 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; - } - - my $requrl=$r->uri; -# ----------------------------------------------------------------- Tie db file - if ($ENV{'request.course.fn'}) { - my $fn=$ENV{'request.course.fn'}; - if (-e "$fn.db") { - if (tie(%hash,'GDBM_File',"$fn.db",&GDBM_READER,0640)) { -# ------------------------------------------------------------------- Hash tied - my $firstres=$hash{'map_start_/res/'.$ENV{'request.course.uri'}}; - my $lastres=$hash{'map_finish_/res/'.$ENV{'request.course.uri'}}; - if (($firstres) && ($lastres)) { -# ----------------------------------------------------------------- Render page - - @rows=(); - - &tracetable(0,$firstres,'&'.$lastres.'&'); - if ($hash{'src_'.$lastres}) { - my $brepriv= - &Apache::lonnet::allowed('bre',$hash{'src_'.$lastres}); - if (($brepriv eq '2') || ($brepriv eq 'F')) { - $rows[$#rows+1]=''.$lastres; - } - } - -# ------------------------------------------------------------ Add to symb list - - my $i; - my %symbhash=(); - for ($i=0;$i<=$#rows;$i++) { - if ($rows[$i]) { - my @colcont=split(/\&/,$rows[$i]); - map { - $symbhash{$hash{'src_'.$_}}=''; - } @colcont; - } - } - &Apache::lonnet::symblist($requrl,%symbhash); - -# ------------------------------------------------------------------ Page parms - - my $j; - my $lcm=1; - my $contents=0; - -# ---------------------------------------------- Go through table to get layout - - for ($i=0;$i<=$#rows;$i++) { - if ($rows[$i]) { - $contents++; - my @colcont=split(/\&/,$rows[$i]); - $lcm*=($#colcont+1)/euclid($lcm,($#colcont+1)); - } - } - - unless ($contents) { - $r->content_type('text/html'); - $r->send_http_header; - $r->print('Empty Map.'); - } else { -# ------------------------------------------------------------------ Build page - -# ---------------------------------------------------------------- Send headers - - $r->content_type('text/html'); - $r->send_http_header; - $r->print( - 'Navigate LON-CAPA Maps'); - - $r->print(''. - '

Navigate Course Map

'); - -# ----------------------------------------------------------------- Start table - $r->print(''); - for ($i=0;$i<=$#rows;$i++) { - if ($rows[$i]) { - $r->print("\n"); - my @colcont=split(/\&/,$rows[$i]); - my $avespan=$lcm/($#colcont+1); - for ($j=0;$j<=$#colcont;$j++) { - my $rid=$colcont[$j]; - $r->print(''); - } - $r->print(''); - } - } - $r->print("\n
'. - $hash{'title_'.$rid}.''); - $r->print('
"); - - $r->print(''); -# -------------------------------------------------------------------- End page - } -# ------------------------------------------------------------- End render page - } else { - $r->content_type('text/html'); - $r->send_http_header; - $r->print('Coursemap undefined.'); - } -# ------------------------------------------------------------------ Untie hash - unless (untie(%hash)) { - &Apache::lonnet::logthis("WARNING: ". - "Could not untie coursemap $fn (browse)."); - } -# -------------------------------------------------------------------- All done - return OK; -# ----------------------------------------------- Errors, hash could no be tied - } - } - } - $ENV{'user.error.msg'}="$requrl:bre:0:0:Course not initialized"; - return HTTP_NOT_ACCEPTABLE; + delete $hash{$subtraction}; + return join(",", keys(%hash)); +} + +# Convenience function: Given a stack returned from getStack on the iterator, +# return the correct src() value. +sub getLinkForResource { + my $stack = shift; + my $res; + + # Check to see if there are any pages in the stack + foreach $res (@$stack) { + 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); + } + } + } + + # Failing that, return the src of the last resource that is defined + # (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 my $item (@$stack) { + if (defined($item)) { $res = $item; } + } + + return ($res->link(),$res->shown_symb()); +} + +# Convenience function: This separates the logic of how to create +# the problem text strings ("Due: DATE", "Open: DATE", "Not yet assigned", +# 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. + +sub getDescription { + my $res = shift; + my $part = shift; + my $status = $res->status($part); + + if ($status == $res->NETWORK_FAILURE) { + return &mt("Having technical difficulties; please check status later"); + } + if ($status == $res->NOTHING_SET) { + return &mt("Not currently assigned."); + } + if ($status == $res->OPEN_LATER) { + return "Open " . timeToHumanString($res->opendate($part),'start'); + } + if ($status == $res->OPEN) { + if ($res->duedate($part)) { + return &mt("Due")." " .timeToHumanString($res->duedate($part),'end'); + } else { + return &mt("Open, no due date"); + } + } + if ($status == $res->PAST_DUE_ANSWER_LATER) { + return &mt("Answer open")." " . timeToHumanString($res->answerdate($part),'start'); + } + if ($status == $res->PAST_DUE_NO_ANSWER) { + return &mt("Was due")." " . timeToHumanString($res->duedate($part),'end'); + } + if (($status == $res->ANSWER_OPEN || $status == $res->PARTIALLY_CORRECT) + && $res->handgrade($part) ne 'yes') { + return &mt("Answer available"); + } + if ($status == $res->EXCUSED) { + return &mt("Excused by instructor"); + } + if ($status == $res->ATTEMPTED) { + 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)"; + if ($maxtries > 1 && $maxtries - $tries == 1) { + $triesString = "$triesString"; + } + } + if ($res->duedate($part)) { + return &mt("Due")." " . timeToHumanString($res->duedate($part),'end') . + " $triesString"; + } else { + return &mt("No due date")." $triesString"; + } + } + if ($status == $res->ANSWER_SUBMITTED) { + return &mt('Answer submitted'); + } +} + +# Convenience function, so others can use it: Is the problem due in less than +# 24 hours, and still can be done? + +sub dueInLessThan24Hours { + my $res = shift; + my $part = shift; + my $status = $res->status($part); + + return ($status == $res->OPEN() || + $status == $res->TRIES_LEFT()) && + $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 than one try to begin with, not due yet and still can be done? +sub lastTry { + my $res = shift; + my $part = shift; + + my $tries = $res->tries($part); + my $maxtries = $res->maxtries($part); + return $tries && $maxtries && $maxtries > 1 && + $maxtries - $tries == 1 && $res->duedate($part) && + $res->duedate($part) > time(); +} + +# This puts a human-readable name on the env variable. + +sub advancedUser { + return $env{'request.role.adv'}; +} + + +# 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,$type,$format) = @_; + + # zero, '0' and blank are bad times + if (!$time) { + 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; + + 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; + } + + if ( $delta > 0 ) { + + my $tense = $inPast ? " ago" : ""; + my $prefix = $inPast ? "" : "in "; + + # Less than a minute + if ( $delta < $minute ) { + if ($delta == 1) { return "${prefix}1 second$tense"; } + return "$prefix$delta seconds$tense"; + } + + # Less than an hour + 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 than 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"; + } + + # 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/00:00/; + $timeStr =~ s/12:00 pm/noon/; + 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("$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.&Apache::lonlocal::gettimezone($time); + } + + # Not this year, so show the year + 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.&Apache::lonlocal::gettimezone($time); + } +} + + +=pod + +=head1 NAME + +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. + +=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 accepts, +instead of passing it arguments as is normal, pass it in an anonymous +hash with the desired options. + +The package provides a function called 'render', called as +Apache::lonnavmaps::render({}). + +=head2 Overview of Columns + +The renderer will build an HTML table for the navmap and return +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 +the table. + +Any additional generally useful column types should be placed in the +renderer code here, so anybody can use it anywhere else. Any code +specific to the current application (such as the addition of +elements in a column) should be placed in the code of the thing using +the renderer. + +At the core of the renderer is the array reference COLS (see Example +section below for how to pass this correctly). The COLS array will +consist of entries of one of two types of things: Either an integer +representing one of the pre-packaged column types, or a sub reference +that takes a resource reference, a part number, and a reference to the +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: + +The general info about the resource: Link, icon for the type, etc. The +first column in the standard nav map display. This column provides the +indentation effect seen in the B