--- loncom/interface/lonnavmaps.pm 2004/08/27 20:34:13 1.280 +++ loncom/interface/lonnavmaps.pm 2022/06/11 03:58:58 1.556 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Navigate Maps Handler # -# $Id: lonnavmaps.pm,v 1.280 2004/08/27 20:34:13 albertel Exp $ +# $Id: lonnavmaps.pm,v 1.556 2022/06/11 03:58:58 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -27,681 +27,40 @@ # ### -package Apache::lonnavmaps; - -use strict; -use Apache::Constants qw(:common :http); -use Apache::loncommon(); -use Apache::lonmenu(); -use Apache::lonlocal; -use POSIX qw (floor strftime); -use Data::Dumper; # for debugging, not always used - -# 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->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 => '' - ); -# 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 = "#FF0000"; - -sub launch_win { - my ($mode,$script)=@_; - my $result; - if ($script ne 'no') { - $result.=''; - } - if ($mode eq 'link') { - $result.='' - .&mt("Launch navigation window")."    "; - } - return $result; -} - -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 nav_control_js { - my $nav=($ENV{'environment.remotenavmap'} eq 'on'); - return (< - -ENDUPDATE -} - -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'}) { - &Apache::loncommon::content_type($r,'text/xml'); - } else { - &Apache::loncommon::content_type($r,'text/html'); - } - $r->send_http_header; - return OK; - } - - # Send header, don't cache this page - if ($ENV{'browser.mathml'}) { - &Apache::loncommon::content_type($r,'text/xml'); - } else { - &Apache::loncommon::content_type($r,'text/html'); - } - &Apache::loncommon::no_cache($r); - $r->send_http_header; - - if ($ENV{QUERY_STRING} eq 'collapseExternal') { - &Apache::lonnet::put('environment',{'remotenavmap' => 'off'}); - &Apache::lonnet::appenv('environment.remotenavmap' => 'off'); - my $menu=&Apache::lonmenu::reopenmenu(); - my $navstatus=&Apache::lonmenu::get_nav_status(); - if ($menu) { - $menu=(<print(<<"ENDSUBM"); - - - - - - -ENDSUBM - return; - } - if ($ENV{QUERY_STRING} eq 'launchExternal') { - &Apache::lonnet::put('environment',{'remotenavmap' => 'on'}); - &Apache::lonnet::appenv('environment.remotenavmap' => 'on'); - } - - # Create the nav map - my $navmap = Apache::lonnavmaps::navmap->new(); - - 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("".&mt('Navigate Course Contents').""); -# ------------------------------------------------------------ Get query string - &Apache::loncommon::get_unprocessed_cgi($ENV{'QUERY_STRING'},['register','sort','showOnlyHomework']); - -# ----------------------------------------------------- Force menu registration - my $addentries=''; - my $more_unload; - my $body_only=''; - if ($ENV{'environment.remotenavmap'} eq 'on') { - $r->print(''); -# FIXME need to be smarter to only catch window close events -# $more_unload="collapse()" - $body_only=1; - } - if ($ENV{'form.register'}) { - $addentries=' onLoad="'.&Apache::lonmenu::loadevents(). - '" onUnload="'.&Apache::lonmenu::unloadevents().';'. - $more_unload.'"'; - $r->print(&Apache::lonmenu::registerurl(1)); - } else { - $addentries=' onUnload="'.$more_unload.'"'; - } - - # Header - $r->print(''. - &Apache::loncommon::bodytag('Navigate Course Contents','', - $addentries,$body_only,'', - $ENV{'form.register'})); - $r->print(''. - &Apache::loncommon::help_open_menu('','Navigation Screen','Navigation_Screen','',undef,'RAT')); - - $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 - # (older code; should use retrieveResources) - if ($ENV{QUERY_STRING} !~ /filter/) { - my $iterator = $navmap->getIterator(undef, undef, undef, 0); - my $curRes; - my $sequenceCount = 0; - my $sequenceId; - while ($curRes = $iterator->next()) { - if (ref($curRes) && $curRes->is_sequence()) { - $sequenceCount++; - $sequenceId = $curRes->map_pc(); - } - } - - 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"; - } - } - - if ($ENV{QUERY_STRING} eq 'launchExternal') { - $r->print(' -
-
'); - $r->print(' - '); - } - - if ($ENV{'environment.remotenavmap'} ne 'on') { - $r->print(&launch_win('link','yes')); - } - if ($ENV{'environment.remotenavmap'} eq 'on') { -# $r->print("" . - $r->print("" . - &mt("Close navigation window"). - "    "); - } - - 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 $curRes; - my $foundDoableProblem = 0; - my $problemRes; - - while (($curRes = $iterator->next()) && !$foundDoableProblem) { - 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(); - } - } - } - - # If we found no problems, print a note to that effect. - if (!$foundDoableProblem) { - $r->print("All homework assignments have been completed.

"); - } - } else { - $r->print("" . - &mt("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{'form.showOnlyHomework'} eq "1") { - $showOnlyHomework = 1; - $suppressEmptySequences = 1; - $filterFunc = sub { my $res = shift; - return $res->completable() || $res->is_map(); - }; - $r->print("" . - &mt("Show Everything")."    "); - $r->print("

".&mt("Uncompleted Homework")."

"); - $ENV{'form.filter'} = ''; - $ENV{'form.condition'} = 1; - $resource_no_folder_link = 1; - } else { - $r->print("" . - &mt("Show Only Uncompleted Homework")."    "); - } - - my %selected=($ENV{'form.sort'} => 'selected=on'); - my $sort_html=("
- - - - - -
"); - # renderer call - my $renderArgs = { 'cols' => [0,2,3], - 'sort' => $ENV{'form.sort'}, - 'url' => '/adm/navmaps', - 'navmap' => $navmap, - 'suppressNavmap' => 1, - 'suppressEmptySequences' => $suppressEmptySequences, - 'filterFunc' => $filterFunc, - 'resource_no_folder_link' => $resource_no_folder_link, - 'sort_html'=> $sort_html, - '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("

".&mt("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; -} - -# 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; - - delete $hash{$subtraction}; - return join(",", keys(%hash)); -} - -# 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)) { - if ($res->is_page()) { - return $res->src(); - } - # in case folder was skipped over as "only sequence" - my ($map,$id,$src)=&Apache::lonnet::decode_symb($res->symb()); - if ($map=~/\.page$/) { - return &Apache::lonnet::clutter($map).'#'. - &Apache::lonnet::escape(&Apache::lonnet::declutter($src)); - } - } - } - - # 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 (@$stack) { - if (defined($_)) { $res = $_; } - } - - return $res->src(); -} - -# 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)); - } - if ($status == $res->OPEN) { - if ($res->duedate($part)) { - return &mt("Due")." " .timeToHumanString($res->duedate($part)); - } else { - return &mt("Open, no due date"); - } - } - if ($status == $res->PAST_DUE_ANSWER_LATER) { - return &mt("Answer open")." " . timeToHumanString($res->answerdate($part)); - } - if ($status == $res->PAST_DUE_NO_ANSWER) { - return &mt("Was due")." " . timeToHumanString($res->duedate($part)); - } - if ($status == $res->ANSWER_OPEN) { - 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)) . - " $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 then -# 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 then 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) = @_; - # 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 then a minute - if ( $delta < $minute ) { - if ($delta == 1) { return "${prefix}1 second$tense"; } - return "$prefix$delta seconds$tense"; - } - - # Less then 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 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, %b %e at %I:%M %P", localtime($time)); - $timeStr =~ s/12:00 am/00:00/; - $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/00:00/; - $timeStr =~ s/12:00 pm/noon/; - return $timeStr; - } - - # Not this year, so show the year - my $timeStr = strftime("on %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; - } -} - - =pod =head1 NAME -Apache::lonnavmap - Subroutines to handle and render the navigation - maps +Apache::lonnavmaps - Subroutines to handle and render the navigation =head1 SYNOPSIS +Handles navigational maps. + The main handler generates the navigational listing for the course, the other objects export this information in a usable fashion for other modules. + +This is part of the LearningOnline Network with CAPA project +described at http://www.lon-capa.org. + + =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". +LON-CAPA, in a course, by going to /adm/test. The content of +the hash will be under the heading "Big Hash". + +Access to /adm/test is controlled by a domain configuration, +which a Domain Coordinator will set for a server's default domain +via: Main Menu > Set domain configuration > Display (Access to +server status pages checked), and entering a username:domain +or IP address in the "Show user environment" row. Users with +an unexpired domain coordinator role in the server's domain +automatically receive access to /adm/test. Big Hash contains, among other things, how resources are related to each other (next/previous), what resources are maps, which @@ -711,8 +70,8 @@ to compute due to the amount of data tha processed. Apache::lonnavmaps provides an object model for manipulating this -information in a higher-level fashion then directly manipulating -the hash. It also provides access to several auxilary functions +information in a higher-level fashion than directly manipulating +the hash. It also provides access to several auxiliary 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. @@ -725,11 +84,18 @@ Apache::lonnavmaps also provides fairly 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 +B: Apache::lonnavmaps by default will show information +for the "currently logged in user". However, if information +about resources is needed for a different user, e.g., a bubblesheet +exam which uses randomorder, or randompick needs to be printed or +graded for named user(s) or specific CODEs, then the username, +domain, or CODE can be passed as arguments when creating a new +navmap object. + +Note if you want things like "due dates for another student", +you would use the EXT function instead of lonnavmaps. +That said, the lonnavmaps module can still help, because many +things, such as the course structure, are usually constant between users, and Apache::lonnavmaps can help by providing symbs for the EXT call. @@ -739,7 +105,9 @@ all, then documents the Apache::lonnavma 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 +Apache::lonnavmaps::Resource objects that are returned singularly +by: getBySymb(), getById(), getByMapPc(), and getResourceByUrl() +(can also be as an array), or in an array by retrieveResources(). =head1 Subroutine: render @@ -757,7 +125,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 @@ -811,6 +179,12 @@ If true, the resource's folder will not it. Default is false. True implies printCloseAll is false, since you can't close or open folders when this is on anyhow. +=item * B: + +If true, the title of the folder or page will not be followed by an +icon/link to direct editing of a folder or composite page, originally +added via the Course Editor. + =back =item * B: @@ -872,13 +246,13 @@ automatically. =over 4 -=item * B: default: constructs one from %ENV +=item * B: default: constructs one from %env A reference to a fresh ::iterator to use from the navmaps. The rendering will reflect the options passed to the iterator, so you can use that to just render a certain part of the course, if you like. If one is not passed, the renderer will attempt to construct one from -ENV{'form.filter'} and ENV{'form.condition'} information, plus the +env{'form.filter'} and env{'form.condition'} information, plus the 'iterator_map' parameter if any. =item * B: default: not used @@ -888,11 +262,16 @@ instruct the renderer to render only a p the source of the map you want to process, like '/res/103/jerf/navmap.course.sequence'. -=item * B: default: constructs one from %ENV +=item * B: default: false + +If you need to include the top level map (meaning the course) in the +rendered output set this to true + +=item * B: default: constructs one from %env A reference to a navmap, used only if an iterator is not passed in. If this is necessary to make an iterator but it is not passed in, a new -one will be constructed based on ENV info. This is useful to do basic +one will be constructed based on env info. This is useful to do basic error checking before passing it off to render. =item * B: default: must be passed in @@ -918,12 +297,12 @@ then only one line will be displayed for all parts will always be displayed. If showParts is 0, this is ignored. -=item * B: default: determined from %ENV +=item * B: default: determined from %env A string identifying the URL to place the anchor 'curloc' at. It is the responsibility of the renderer user to ensure that the #curloc is in the URL. By default, determined through -the use of the ENV{} 'jump' information, and should normally "just +the use of the env{} 'jump' information, and should normally "just work" correctly. =item * B: default: empty string @@ -951,7 +330,7 @@ are allowing the user to open and close Describes the currently-open row number to cause the browser to jump to, because the user just opened that folder. By default, pulled from -the Jump information in the ENV{'form.*'}. +the Jump information in the env{'form.*'}. =item * B: default: false @@ -978,7 +357,7 @@ user into thinking that if the sequence under it; for example, see the "Show Uncompleted Homework" view on the B