--- loncom/interface/lonnavmaps.pm 2003/07/17 18:40:49 1.216 +++ loncom/interface/lonnavmaps.pm 2021/08/04 19:59:10 1.554 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Navigate Maps Handler # -# $Id: lonnavmaps.pm,v 1.216 2003/07/17 18:40:49 bowersj2 Exp $ +# $Id: lonnavmaps.pm,v 1.554 2021/08/04 19:59:10 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -25,549 +25,42 @@ # # 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 Apache::loncommon(); -use Apache::lonmenu(); -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->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 => 'navmap.ellipsis.gif' ); - -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 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; -} - -# 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) && $res->is_page()) { - return $res->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 seperates 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 -# 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 "Having technical difficulties; please check status later"; - } - if ($status == $res->NOTHING_SET) { - return "Not currently assigned."; - } - if ($status == $res->OPEN_LATER) { - return "Open " . timeToHumanString($res->opendate($part)); - } - if ($status == $res->OPEN) { - 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 open " . timeToHumanString($res->answerdate($part)); - } - if ($status == $res->PAST_DUE_NO_ANSWER) { - return "Was due " . timeToHumanString($res->duedate($part)); - } - if ($status == $res->ANSWER_OPEN) { - return "Answer available"; - } - if ($status == $res->EXCUSED) { - return "Excused by instructor"; - } - if ($status == $res->ATTEMPTED) { - return "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()) { - return "Due " . timeToHumanString($res->duedate($part)) . - " $triesString"; - } else { - return "No due date $triesString"; - } - } - if ($status == $res->ANSWER_SUBMITTED) { - return 'Answer submitted'; - } -} - -# Convenience function, so others can use it: Is the problem due in less then -# 24 hours, and still can be done? - -sub dueInLessThen24Hours { - 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(); -} - -# 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() && - $res->duedate() > 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 '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; - } - - 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/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; - } -} - +### =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 -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". 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". +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. 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 @@ -577,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. @@ -591,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. @@ -605,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 @@ -623,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 @@ -677,15 +179,21 @@ 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: +=item * B: Whether there is discussion on the resource, email for the user, or (lumped in here) perl errors in the execution of the problem. This is the second column in the main nav map. -=item B: +=item * B: An icon for the status of a problem, with five possible states: Correct, incorrect, open, awaiting grading (for a problem where the @@ -693,11 +201,24 @@ computer's grade is suppressed, or the c essay problem), or none (not open yet, not a problem). The third column of the standard navmap. -=item B: +=item * B: A text readout of the details of the current status of the problem, such as "Due in 22 hours". The fourth column of the standard navmap. +=item * B: + +A text readout summarizing the status of the problem. If it is a +single part problem, will display "Correct", "Incorrect", +"Not yet open", "Open", "Attempted", or "Error". If there are +multiple parts, this will output a string that in HTML will show a +status of how many parts are in each status, in color coding, trying +to match the colors of the icons within reason. + +Note this only makes sense if you are I showing parts. If +C is true (see below), this column will not output +anything. + =back If you add any others please be sure to document them here. @@ -716,9 +237,7 @@ to override vertical and horizontal alig =head2 Parameters -Most of these parameters are only useful if you are *not* using the -folder interface (i.e., the default first column), which is probably -the common case. If you are using this interface, then you should be +Minimally, you should be able to get away with just using 'cols' (to specify the columns shown), 'url' (necessary for the folders to link to the current screen correctly), and possibly 'queryString' if your app calls for it. In @@ -727,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 @@ -743,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 @@ -773,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 @@ -806,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 @@ -863,56 +387,630 @@ navmaps screen) uses this to display the =cut + +=head1 SUBROUTINES + +=over + +=item update() + +=item addToFilter() + +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 + +=item removeFromFilter() + +=item getLinkForResource() + +Convenience function: Given a stack returned from getStack on the iterator, +return the correct src() value. + +=item getDescription() + +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. + +=item dueInLessThan24Hours() + +Convenience function, so others can use it: Is the problem due in less than 24 hours, and still can be done? + +=item lastTry() + +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? + +=item advancedUser() + +This puts a human-readable name on the env variable. + +=item timeToHumanString() + +timeToHumanString takes a time number and converts it to a +human-readable representation, meant to be used in the following +manner: + +=over 4 + +=item * print "Due $timestring" + +=item * print "Open $timestring" + +=item * print "Answer available $timestring" + +=back + +Very, very, very, VERY English-only... goodness help a localizer on +this func... + +=item resource() + +returns 0 + +=item communication_status() + +returns 1 + +=item quick_status() + +returns 2 + +=item long_status() + +returns 3 + +=item part_status_summary() + +returns 4 + +=item render_resource() + +=item render_communication_status() + +=item render_quick_status() + +=item render_long_status() + +=item render_parts_summary_status() + +=item setDefault() + +=item cmp_title() + +=item render() + +=item add_linkitem() + +=item show_linkitems_toolbar() + +=back + +=cut + +package Apache::lonnavmaps; + +use strict; +use GDBM_File; +use Apache::loncommon(); +use Apache::lonenc(); +use Apache::lonlocal; +use Apache::lonnet; +use Apache::lonmap; + +use POSIX qw (ceil floor strftime); +use Time::HiRes qw( gettimeofday tv_interval ); +use LONCAPA; +use DateTime(); +use HTML::Entities; + +# For debugging + +#use Data::Dumper; + + +# 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 usesthe 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 = #texthash does not work here + ( 'navmap.correct.gif' => 'Correct', + 'navmap.wrong.gif' => 'Incorrect', + 'navmap.open.gif' => 'Is Open', + 'navmap.partial.gif' => 'Partially Correct', + 'navmap.ellipsis.gif' => 'Attempted', + ); + +# 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->CREDIT_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 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)); +} + +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; } } + if ($anchor->encrypted() && !&advancedUser()) { + $anchor='LC_'.$anchor->id(); + } else { + $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($res->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; } + } + + if ($res) { + return ($res->link(),$res->shown_symb()); + } + return; +} + + + +sub getDescription { + my $res = shift; + 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 &mt("Having technical difficulties; please check status later"); + } + if ($status == $res->NOTHING_SET) { + return &Apache::lonhtmlcommon::direct_parm_link(&mt('Not currently assigned'),$res->symb(),'opendate',$part); + } + if ($status == $res->OPEN_LATER) { + return &mt("Open [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($open,'start'),$res->symb(),'opendate',$part)); + } + my $slotinfo; + if ($res->simpleStatus($part) == $res->OPEN) { + unless (&Apache::lonnet::allowed('mgr',$env{'request.course.id'})) { + my ($slot_status,$slot_time,$slot_name)=$res->check_for_slot($part); + my $slotmsg; + if ($slot_status == $res->UNKNOWN) { + $slotmsg = &mt('Reservation status unknown'); + } elsif ($slot_status == $res->RESERVED) { + $slotmsg = &mt('Reserved - ends [_1]', + timeToHumanString($slot_time,'end')); + } elsif ($slot_status == $res->RESERVED_LOCATION) { + $slotmsg = &mt('Reserved - specific location(s) - ends [_1]', + timeToHumanString($slot_time,'end')); + } elsif ($slot_status == $res->RESERVED_LATER) { + $slotmsg = &mt('Reserved - next open [_1]', + timeToHumanString($slot_time,'start')); + } elsif ($slot_status == $res->RESERVABLE) { + $slotmsg = &mt('Reservable, reservations close [_1]', + timeToHumanString($slot_time,'end')); + } elsif ($slot_status == $res->NEEDS_CHECKIN) { + $slotmsg = &mt('Reserved, check-in needed - ends [_1]', + timeToHumanString($slot_time,'end')); + } elsif ($slot_status == $res->RESERVABLE_LATER) { + $slotmsg = &mt('Reservable, reservations open [_1]', + timeToHumanString($slot_time,'start')); + } elsif ($slot_status == $res->NOT_IN_A_SLOT) { + $slotmsg = &mt('Reserve a time/place to work'); + } elsif ($slot_status == $res->NOTRESERVABLE) { + $slotmsg = &mt('Reservation not available'); + } elsif ($slot_status == $res->WAITING_FOR_GRADE) { + $slotmsg = &mt('Submission in grading queue'); + } + if ($slotmsg) { + if ($res->is_task() || !$due) { + return $slotmsg; + } + $slotinfo = (' ' x 2).'('.$slotmsg.')'; + } + } + } + if ($status == $res->OPEN) { + if ($due) { + if ($res->is_practice()) { + return &mt("Closes [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($due,'start'),$res->symb(),'duedate',$part)).$slotinfo; + } else { + return &mt("Due [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($due,'end'),$res->symb(),'duedate',$part)).$slotinfo; + } + } else { + return &Apache::lonhtmlcommon::direct_parm_link(&mt("Open, no due date"),$res->symb(),'duedate',$part).$slotinfo; + } + } + if ($status == $res->PAST_DUE_ANSWER_LATER) { + return &mt("Answer open [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($answer,'start'),$res->symb(),'answerdate',$part)); + } + if ($status == $res->PAST_DUE_NO_ANSWER) { + if ($res->is_practice()) { + return &mt("Closed [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($due,'start'),$res->symb(),'answerdate,duedate',$part)); + } else { + return &mt("Was due [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($due,'end'),$res->symb(),'answerdate,duedate',$part)); + } + } + if (($status == $res->ANSWER_OPEN || $status == $res->PARTIALLY_CORRECT) + && $res->handgrade($part) ne 'yes') { + my $msg = &mt('Answer available'); + my $parmlist = 'answerdate,duedate'; + if (($res->is_tool) && ($res->is_gradable())) { + if (($status == $res->PARTIALLY_CORRECT) && ($res->parmval('retrypartial',$part))) { + $msg = &mt('Grade received'); + $parmlist = 'retrypartial'; + } else { + $msg = &mt('Grade available'); + } + } + return &Apache::lonhtmlcommon::direct_parm_link($msg,$res->symb(),$parmlist,$part); + } + if ($status == $res->EXCUSED) { + return &mt("Excused by instructor"); + } + if ($status == $res->ATTEMPTED) { + if ($res->is_anonsurvey($part) || $res->is_survey($part)) { + return &mt("Survey submission recorded"); + } else { + return &mt("Answer submitted, not yet graded"); + } + } + if ($status == $res->CREDIT_ATTEMPTED) { + if ($res->is_anonsurvey($part) || $res->is_survey($part)) { + return &mt("Credit for survey submission"); + } + } + if ($status == $res->TRIES_LEFT) { + my $tries = $res->tries($part); + my $maxtries = $res->maxtries($part); + my $triesString = ""; + if ($tries && $maxtries) { + $triesString = '('.&mt('[_1] of [quant,_2,try,tries] used',$tries,$maxtries).')'; + if ($maxtries > 1 && $maxtries - $tries == 1) { + $triesString = "$triesString"; + } + } + if ($due) { + return &mt("Due [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($due,'end'),$res->symb(),'duedate',$part)) . + " $triesString"; + } else { + return &Apache::lonhtmlcommon::direct_parm_link(&mt("No due date"),$res->symb(),'duedate',$part)." $triesString"; + } + } + if ($status == $res->ANSWER_SUBMITTED) { + return &mt('Answer submitted'); + } +} + + +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(); +} + + +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(); +} + + +sub advancedUser { + return $env{'request.role.adv'}; +} + +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(); + + # 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; or minutes, seconds (if format requires) + my $minutes = floor($delta / 60); + if (($format ne '') && ($format =~ /\%(T|S)/)) { + my $display; + if ($minutes == 1) { + $display = "${prefix}1 minute"; + } else { + $display = "$prefix$minutes minutes"; + } + my $seconds = $delta % $minute; + if ($seconds == 0) { + $display .= $tense; + } elsif ($seconds == 1) { + $display .= ", 1 second$tense"; + } else { + $display .= ", $seconds seconds$tense"; + } + return $display; + } + 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, (and + seconds, if format specified it) + 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 = ""; + } + if (($format ne '') && ($format =~ /\%(T|S)/)) { + my $display = "$prefix$hourString$minuteString"; + my $seconds = $delta-(($hours * $hour)+($minutes * $minute)); + if ($seconds == 0) { + $display .= $tense; + } elsif ($seconds == 1) { + $display .= ", 1 second$tense"; + } else { + $display .= ", $seconds seconds$tense"; + } + return $display; + } + return "$prefix$hourString$minuteString$tense"; + } + + # Date/time is more than 24 hours away + + my $dt = DateTime->from_epoch(epoch => $time) + ->set_time_zone(&Apache::lonlocal::gettimezone()); + + # If there's a caller supplied format, use it, unless it only displays + # H:M:S or H:M. + + if (($format ne '') && ($format ne '%T') && ($format ne '%R')) { + 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 = $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 " : "this ") . + $timeStr; + } + + my $conjunction='on'; + if ($type eq 'start') { + $conjunction='at'; + } elsif ($type eq 'end') { + $conjunction='by'; + } + # Is it this year? + 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 = $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 = + $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; + } +} + + sub resource { return 0; } sub communication_status { return 1; } sub quick_status { return 2; } sub long_status { return 3; } - -# Data for render_resource +sub part_status_summary { return 4; } sub render_resource { my ($resource, $part, $params) = @_; + my $editmapLink; my $nonLinkedText = ''; # stuff after resource title not in link my $link = $params->{"resourceLink"}; + if ($resource->ext()) { + $link =~ s/\#.+(\?)/$1/g; + } + + # The URL part is not escaped at this point, but the symb is... + my $src = $resource->src(); my $it = $params->{"iterator"}; my $filter = $it->{FILTER}; my $title = $resource->compTitle(); - if ($src =~ /^\/uploaded\//) { - $nonLinkedText=$title; - $title = ''; - } + my $partLabel = ""; my $newBranchText = ""; - + my $location=&Apache::loncommon::lonhttpdurl("/adm/lonIcons"); # If this is a new branch, label it so if ($params->{'isNewBranch'}) { - $newBranchText = ""; + $newBranchText = ".mt('Branch')."; } - # links to open and close the folder - my $linkopen = ""; - my $linkclose = ""; + my $whitespace = $location.'/whitespace_21.gif'; + my ($nomodal,$linkopen,$linkclose); + unless ($resource->is_map() || $params->{'resource_nolink'}) { + $linkopen = ""; + $linkclose = ""; + if (($params->{'modalLink'}) && (!$resource->is_sequence())) { + if ($link =~m{^(?:|/adm/wrapper)/ext/([^#]+)}) { + my $exturl = $1; + if (($ENV{'SERVER_PORT'} == 443) && ($exturl !~ /^https:/)) { + $nomodal = 1; + } + } elsif (($link eq "/public/$LONCAPA::match_domain/$LONCAPA::match_courseid/syllabus") && + ($env{'request.course.id'}) && ($ENV{'SERVER_PORT'} == 443) && + ($env{'course.'.$env{'request.course.id'}.'.externalsyllabus'} =~ m{^http://})) { + $nomodal = 1; + } + my $esclink = &js_escape($link); + if ($nomodal) { + $linkopen .= ""; + } else { + $linkopen .= ""; + } + } else { + $linkopen .= ""; + } + } # Default icon: unknown page - my $icon = ""; + my $icon = ""; if ($resource->is_problem()) { if ($part eq '0' || $params->{'condensed'}) { - $icon = ''; + $icon = ''.&mt('Task');
+	    } else {
+		$icon .= 'problem.gif'; } else { $icon = $params->{'indentString'}; } } else { - my $curfext= (split (/\./,$resource->src))[-1]; - my $embstyle = &Apache::loncommon::fileembstyle($curfext); - # The unless conditional that follows is a bit of overkill - if (!(!defined($embstyle) || $embstyle eq 'unk' || $embstyle eq 'hdn')) { - $icon = ""; - } + $icon = ""; } # Display the correct map icon to open or shut map @@ -922,42 +1020,89 @@ sub render_resource { if ($it->{CONDITION}) { $nowOpen = !$nowOpen; } - + my $folderType = $resource->is_sequence() ? 'folder' : 'page'; - + my $title=$resource->title; + $title=~s/\"/\&qout;/g; if (!$params->{'resource_no_folder_link'}) { $icon = "navmap.$folderType." . ($nowOpen ? 'closed' : 'open') . '.gif'; - $icon = ""; - - $linkopen = "{'queryString'} . '&filter='; + $icon = "" + ."\"""; + $linkopen = "{'url'} . '?' . + $params->{'queryString'} . '&filter='; $linkopen .= ($nowOpen xor $it->{CONDITION}) ? addToFilter($filter, $mapId) : removeFromFilter($filter, $mapId); - $linkopen .= "&condition=" . $it->{CONDITION} . '&hereType=' - . $params->{'hereType'} . '&here=' . - &Apache::lonnet::escape($params->{'here'}) . - '&jump=' . - &Apache::lonnet::escape($resource->symb()) . - "&folderManip=1'>"; + $linkopen .= "&condition=" . $it->{CONDITION} . '&hereType=' + . $params->{'hereType'} . '&here=' . + &escape($params->{'here'}) . + '&jump=' . + &escape($resource->symb()) . + "&folderManip=1\">"; + $linkclose = ''; } else { # Don't allow users to manipulate folder - $icon = "navmap.$folderType." . ($nowOpen ? 'closed' : 'open') . - '.nomanip.gif'; - $icon = ""; - - $linkopen = ""; - $linkclose = ""; + $icon = "navmap.$folderType." . ($nowOpen ? 'closed' : 'open') . '.gif'; + $icon = ""."\"".($nowOpen"; + if ($params->{'caller'} eq 'sequence') { + $linkopen = ""; + $linkclose = ''; + } else { + $linkopen = ""; + $linkclose = ""; + } + } + if (((&Apache::lonnet::allowed('mdc',$env{'request.course.id'})) || + (&Apache::lonnet::allowed('cev',$env{'request.course.id'}))) && + ($resource->symb=~/\_\_\_[^\_]+\_\_\_uploaded/)) { + if (!$params->{'map_no_edit_link'}) { + my $icon = &Apache::loncommon::lonhttpdurl('/res/adm/pages').'/editmap.png'; + $editmapLink=' '. + ''. + ''.&mt('Edit Content').''. + ''; + } + } + if ($params->{'mapHidden'} || $resource->randomout()) { + $nonLinkedText .= ' ('.&mt('hidden').') '; + } elsif ($params->{'mapUnlisted'}) { + $nonLinkedText .= ' ('.&mt('unlisted').') '; + } elsif ($params->{'mapHiddenDeepLink'} || $resource->deeplinkout()) { + $nonLinkedText .= ' ('.&mt('not shown').') '; + } + } else { + if ($resource->randomout()) { + $nonLinkedText .= ' ('.&mt('hidden').') '; + } elsif ($resource->deeplinkout()) { + $nonLinkedText .= ' ('.&mt('not shown').') '; + } else { + my $deeplink = $resource->deeplink($params->{caller}); + if ((($deeplink eq 'absent') || ($deeplink eq 'grades')) && + &advancedUser()) { + $nonLinkedText .= ' ('.&mt('unlisted').') '; + } elsif (($deeplink) && ($deeplink) ne 'full') { + if (&advancedUser()) { + $nonLinkedText .= ' ('.&mt('deep-link access'). + ') '; + } else { + $nonLinkedText .= ' ('.&mt('access via external site'). + ') '; + } + } } } - - if ($resource->randomout()) { - $nonLinkedText .= ' (hidden) '; + if (!$resource->condval()) { + $nonLinkedText .= ' ('.&mt('conditionally hidden').') '; + } + if (($resource->is_practice()) && ($resource->is_raw_problem())) { + $nonLinkedText .=' '.&mt('not graded').''; } - - # We're done preparing and finally ready to start the rendering - my $result = ""; + # We're done preparing and finally ready to start the rendering + my $result = ''; + my $newfolderType = $resource->is_sequence() ? 'folder' : 'page'; + my $indentLevel = $params->{'indentLevel'}; if ($newBranchText) { $indentLevel--; } @@ -975,27 +1120,39 @@ sub render_resource { # Is this the current resource? if (!$params->{'displayedHereMarker'} && $resource->symb() eq $params->{'here'} ) { - $curMarkerBegin = '> '; - $curMarkerEnd = '<'; - $params->{'displayedHereMarker'} = 1; + unless ($resource->is_map()) { + $curMarkerBegin = ''; + $curMarkerEnd = ''; + } + $params->{'displayedHereMarker'} = 1; } if ($resource->is_problem() && $part ne '0' && !$params->{'condensed'}) { - $partLabel = " (Part $part)"; + my $displaypart=$resource->part_display($part); + $partLabel = " (".&mt('Part: [_1]', $displaypart).")"; + if ($link!~/\#/) { $link.='#'.&escape($part); } $title = ""; } if ($params->{'condensed'} && $resource->countParts() > 1) { - $nonLinkedText .= ' (' . $resource->countParts() . ' parts)'; + $nonLinkedText .= ' ('.&mt('[_1] parts', $resource->countParts()).')'; } - if (!$params->{'resource_nolink'} && $src !~ /^\/uploaded\// && - !$resource->is_sequence()) { - $result .= " $curMarkerBegin$title$partLabel$curMarkerEnd $nonLinkedText"; - } else { - $result .= " $curMarkerBegin$title$partLabel$curMarkerEnd $nonLinkedText"; + if (!$params->{'resource_nolink'} && !$resource->is_sequence() && !$resource->is_empty_sequence) { + $linkclose = ''; + if ($params->{'modalLink'}) { + my $esclink = &js_escape($link); + if ($nomodal) { + $linkopen = ""; + } else { + $linkopen = ""; + } + } else { + $linkopen = ""; + } } + $result .= "$curMarkerBegin$linkopen$title$partLabel$linkclose$curMarkerEnd$editmapLink$nonLinkedText"; return $result; } @@ -1005,35 +1162,37 @@ sub render_communication_status { my $discussionHTML = ""; my $feedbackHTML = ""; my $errorHTML = ""; my $link = $params->{"resourceLink"}; - my $linkopen = ""; + my $linkopen = ""; my $linkclose = ""; + my $location=&Apache::loncommon::lonhttpdurl("/adm/lonMisc"); if ($resource->hasDiscussion()) { $discussionHTML = $linkopen . - '' . + ''.&mt('New Discussion').'' . $linkclose; } if ($resource->getFeedback()) { my $feedback = $resource->getFeedback(); - foreach (split(/\,/, $feedback)) { - if ($_) { + foreach my $msgid (split(/\,/, $feedback)) { + if ($msgid) { $feedbackHTML .= ' ' - . ''; + . &escape($msgid) . '">' + . ''.&mt('New E-mail').''; } } } if ($resource->getErrors()) { my $errors = $resource->getErrors(); - foreach (split(/,/, $errors)) { - if ($_) { + my $errorcount = 0; + foreach my $msgid (split(/,/, $errors)) { + last if ($errorcount>=10); # Only output 10 bombs maximum + if ($msgid) { + $errorcount++; $errorHTML .= ' ' - . ''; + . &escape($msgid) . '">' + . ''.&mt('New Error').''; } } } @@ -1041,8 +1200,7 @@ sub render_communication_status { if ($params->{'multipart'} && $part != '0') { $discussionHTML = $feedbackHTML = $errorHTML = ''; } - - return "$discussionHTML$feedbackHTML$errorHTML "; + return "$discussionHTML$feedbackHTML$errorHTML "; } sub render_quick_status { @@ -1052,49 +1210,62 @@ sub render_quick_status { $params->{'multipart'} && $part eq "0"; my $link = $params->{"resourceLink"}; - my $linkopen = ""; + my $linkopen = ""; my $linkclose = ""; - - if ($resource->is_problem() && + + $result .= ''; + if ($resource->is_gradable() && !$firstDisplayed) { - my $icon = $statusIconMap{$resource->status($part)}; + my $icon = $statusIconMap{$resource->simpleStatus($part)}; my $alt = $iconAltTags{$icon}; if ($icon) { - $result .= "$linkopen$alt$linkclose\n"; + my $location= + &Apache::loncommon::lonhttpdurl("/adm/lonIcons/$icon"); + $result .= $linkopen.''.&mt($alt).''.$linkclose; } else { - $result .= " \n"; + $result .= " "; } } else { # not problem, no icon - $result .= " \n"; + $result .= " "; } - + $result .= "\n"; return $result; } sub render_long_status { my ($resource, $part, $params) = @_; - my $result = "\n"; + my $result = ''; my $firstDisplayed = !$params->{'condensed'} && $params->{'multipart'} && $part eq "0"; my $color; - if ($resource->is_problem()) { + my $info = ''; + if ($resource->is_gradable() || $resource->is_practice()) { $color = $colormap{$resource->status}; - - if (dueInLessThen24Hours($resource, $part) || - lastTry($resource, $part)) { + + if (dueInLessThan24Hours($resource, $part)) { $color = $hurryUpColor; - } + $info = ' title="'.&mt('Due in less than 24 hours!').'"'; + } elsif (lastTry($resource, $part)) { + unless (($resource->problemstatus($part) eq 'no') || + ($resource->problemstatus($part) eq 'no_feedback_ever')) { + $color = $hurryUpColor; + $info = ' title="'.&mt('One try remaining!').'"'; + } + } } - - if ($resource->kind() eq "res" && - $resource->is_problem() && + + if (($resource->kind() eq "res") && + ($resource->is_raw_problem() || $resource->is_gradable()) && !$firstDisplayed) { - if ($color) {$result .= ""; } + if ($color) {$result .= ''; } $result .= getDescription($resource, $part); - if ($color) {$result .= ""; } + if ($color) {$result .= ""; } } - if ($resource->is_map() && advancedUser() && $resource->randompick()) { - $result .= '(randomly select ' . $resource->randompick() .')'; + if ($resource->is_map() && &advancedUser() && $resource->randompick()) { + $result .= &mt('(randomly select [_1])', $resource->randompick()); + } + if ($resource->is_map() && &advancedUser() && $resource->randomorder()) { + $result .= &mt('(randomly ordered)'); } # Debugging code @@ -1106,8 +1277,83 @@ sub render_long_status { return $result; } +# Colors obtained by taking the icons, matching the colors, and +# possibly reducing the Value (HSV) of the color, if it's too bright +# for text, generally by one third or so. +my %statusColors = + ( + $resObj->CLOSED => '#000000', + $resObj->OPEN => '#998b13', + $resObj->CORRECT => '#26933f', + $resObj->INCORRECT => '#c48207', + $resObj->ATTEMPTED => '#a87510', + $resObj->ERROR => '#000000' + ); +my %statusStrings = + ( + $resObj->CLOSED => 'Not yet open', + $resObj->OPEN => 'Open', + $resObj->CORRECT => 'Correct', + $resObj->INCORRECT => 'Incorrect', + $resObj->ATTEMPTED => 'Attempted', + $resObj->ERROR => 'Network Error' + ); +my @statuses = ($resObj->CORRECT, $resObj->ATTEMPTED, $resObj->INCORRECT, $resObj->OPEN, $resObj->CLOSED, $resObj->ERROR); + +sub render_parts_summary_status { + my ($resource, $part, $params) = @_; + if (!$resource->is_gradable() && !$resource->contains_problem) { return ''; } + if ($params->{showParts}) { + return ''; + } + + my $td = "\n"; + my $endtd = "\n"; + my @probs; + + if ($resource->contains_problem) { + @probs=$resource->retrieveResources($resource,sub { $_[0]->is_problem() },1,0); + } else { + @probs=($resource); + } + my $return; + my %overallstatus; + my $totalParts; + foreach my $resource (@probs) { + # If there is a single part, just show the simple status + if ($resource->singlepart()) { + my $status = $resource->simpleStatus(${$resource->parts}[0]); + $overallstatus{$status}++; + $totalParts++; + next; + } + # Now we can be sure the $part doesn't really matter. + my $statusCount = $resource->simpleStatusCount(); + my @counts; + foreach my $status (@statuses) { + # decouple display order from the simpleStatusCount order + my $slot = Apache::lonnavmaps::resource::statusToSlot($status); + if ($statusCount->[$slot]) { + $overallstatus{$status}+=$statusCount->[$slot]; + $totalParts+=$statusCount->[$slot]; + } + } + } + $return.= $td . $totalParts . ' parts: '; + foreach my $status (@statuses) { + if ($overallstatus{$status}) { + $return.='' . $overallstatus{$status} . ' ' + . $statusStrings{$status} . ''; + } + } + $return.= $endtd; + return $return; +} + my @preparedColumns = (\&render_resource, \&render_communication_status, - \&render_quick_status, \&render_long_status); + \&render_quick_status, \&render_long_status, + \&render_parts_summary_status); sub setDefault { my ($val, $default) = @_; @@ -1115,18 +1361,23 @@ sub setDefault { return $val; } +sub cmp_title { + my ($atitle,$btitle) = (lc($_[0]->compTitle),lc($_[1]->compTitle)); + $atitle=~s/^\s*//; + $btitle=~s/^\s*//; + return $atitle cmp $btitle; +} + sub render { my $args = shift; &Apache::loncommon::get_unprocessed_cgi($ENV{QUERY_STRING}); my $result = ''; - # Configure the renderer. my $cols = $args->{'cols'}; if (!defined($cols)) { # no columns, no nav maps. return ''; } - my $mustCloseNavMap = 0; my $navmap; if (defined($args->{'navmap'})) { $navmap = $args->{'navmap'}; @@ -1137,6 +1388,7 @@ sub render { my $jump = $args->{'jump'}; my $here = $args->{'here'}; my $suppressNavmap = setDefault($args->{'suppressNavmap'}, 0); + my $closeAllPages = setDefault($args->{'closeAllPages'}, 0); my $currentJumpDelta = 2; # change this to change how many resources are displayed # before the current resource when using #current @@ -1146,9 +1398,9 @@ sub render { # marker my $filterHash = {}; # Figure out what we're not displaying - foreach (split(/\,/, $ENV{"form.filter"})) { - if ($_) { - $filterHash->{$_} = "1"; + foreach my $item (split(/\,/, $env{"form.filter"})) { + if ($item) { + $filterHash->{$item} = "1"; } } @@ -1161,53 +1413,62 @@ sub render { # Without renaming the filterfunc, the server seems to go into # an infinite loop my $oldFilterFunc = $filterFunc; - $filterFunc = sub { my $res = shift; return !$res->randomout() && + $filterFunc = sub { my $res = shift; return !$res->randomout() && + ($res->deeplink($args->{'caller'}) ne 'absent') && + ($res->deeplink($args->{'caller'}) ne 'grades') && + !$res->deeplinkout() && &$oldFilterFunc($res);}; } my $condition = 0; - if ($ENV{'form.condition'}) { + if ($env{'form.condition'}) { $condition = 1; } - if (!$ENV{'form.folderManip'} && !defined($args->{'iterator'})) { + if (!$env{'form.folderManip'} && !defined($args->{'iterator'})) { # Step 1: Check to see if we have a navmap if (!defined($navmap)) { - $navmap = Apache::lonnavmaps::navmap->new( - $ENV{"request.course.fn"}.".db", - $ENV{"request.course.fn"}."_parms.db", 1, 1); - $mustCloseNavMap = 1; - } - $navmap->init(); + $navmap = Apache::lonnavmaps::navmap->new(); + if (!defined($navmap)) { + # no longer in course + return ''.&mt('No course selected').'
+ '.&mt('Select a course').'
'; + } + } # Step two: Locate what kind of here marker is necessary # Determine where the "here" marker is and where the screen jumps to. - if ($ENV{'form.postsymb'}) { - $here = $jump = $ENV{'form.postsymb'}; - } elsif ($ENV{'form.postdata'}) { + if ($env{'form.postsymb'} ne '') { + $here = $jump = &Apache::lonnet::symbclean($env{'form.postsymb'}); + } elsif ($env{'form.postdata'} ne '') { # couldn't find a symb, is there a URL? - my $currenturl = $ENV{'form.postdata'}; + my $currenturl = $env{'form.postdata'}; #$currenturl=~s/^http\:\/\///; #$currenturl=~s/^[^\/]+//; - - $here = $jump = &Apache::lonnet::symbread($currenturl); - } + unless ($args->{'caller'} eq 'sequence') { + $here = $jump = &Apache::lonnet::symbread($currenturl); + } + } + if (($here eq '') && ($args->{'caller'} ne 'sequence')) { + my $last; + if (tie(my %hash,'GDBM_File',$env{'request.course.fn'}.'_symb.db', + &GDBM_READER(),0640)) { + $last=$hash{'last_known'}; + untie(%hash); + } + if ($last) { $here = $jump = $last; } + } # Step three: Ensure the folders are open my $mapIterator = $navmap->getIterator(undef, undef, undef, 1); - my $depth = 1; - $mapIterator->next(); # discard the first BEGIN_MAP - my $curRes = $mapIterator->next(); + my $curRes; my $found = 0; # We only need to do this if we need to open the maps to show the # current position. This will change the counter so we can't count # for the jump marker with this loop. - while ($depth > 0 && !$found) { - if ($curRes == $mapIterator->BEGIN_MAP()) { $depth++; } - if ($curRes == $mapIterator->END_MAP()) { $depth--; } - + while ($here && ($curRes = $mapIterator->next()) && !$found) { if (ref($curRes) && $curRes->symb() eq $here) { my $mapStack = $mapIterator->getStack(); @@ -1221,58 +1482,55 @@ sub render { } $found = 1; } - - $curRes = $mapIterator->next(); } } - if ( !defined($args->{'iterator'}) && $ENV{'form.folderManip'} ) { # we came from a user's manipulation of the nav page + if ( !defined($args->{'iterator'}) && $env{'form.folderManip'} ) { # we came from a user's manipulation of the nav page # If this is a click on a folder or something, we want to preserve the "here" # from the querystring, and get the new "jump" marker - $here = $ENV{'form.here'}; - $jump = $ENV{'form.jump'}; + $here = $env{'form.here'}; + $jump = $env{'form.jump'}; } my $it = $args->{'iterator'}; if (!defined($it)) { - # Construct a default iterator based on $ENV{'form.'} information + # Construct a default iterator based on $env{'form.'} information # Step 1: Check to see if we have a navmap if (!defined($navmap)) { - $navmap = Apache::lonnavmaps::navmap->new($r, - $ENV{"request.course.fn"}.".db", - $ENV{"request.course.fn"}."_parms.db", 1, 1); - $mustCloseNavMap = 1; + $navmap = Apache::lonnavmaps::navmap->new(); + if (!defined($navmap)) { + # no longer in course + return ''.&mt('No course selected').'
+ '.&mt('Select a course').'
'; + } } - # Paranoia: Make sure it's ready - $navmap->init(); # See if we're being passed a specific map if ($args->{'iterator_map'}) { my $map = $args->{'iterator_map'}; $map = $navmap->getResourceByUrl($map); - my $firstResource = $map->map_start(); - my $finishResource = $map->map_finish(); - - $args->{'iterator'} = $it = $navmap->getIterator($firstResource, $finishResource, $filterHash, $condition); + if (ref($map)) { + my $firstResource = $map->map_start(); + my $finishResource = $map->map_finish(); + $args->{'iterator'} = $it = $navmap->getIterator($firstResource, $finishResource, $filterHash, $condition); + } else { + return; + } } else { - $args->{'iterator'} = $it = $navmap->getIterator(undef, undef, $filterHash, $condition); + $args->{'iterator'} = $it = $navmap->getIterator(undef, undef, $filterHash, $condition,undef,$args->{'include_top_level_map'}); } } - + # (re-)Locate the jump point, if any # Note this does not take filtering or hidden into account... need # to be fixed? my $mapIterator = $navmap->getIterator(undef, undef, $filterHash, 0); - my $depth = 1; - $mapIterator->next(); - my $curRes = $mapIterator->next(); + my $curRes; my $foundJump = 0; my $counter = 0; - while ($depth > 0 && !$foundJump) { - if ($curRes == $mapIterator->BEGIN_MAP()) { $depth++; } - if ($curRes == $mapIterator->END_MAP()) { $depth--; } + while (($curRes = $mapIterator->next()) && !$foundJump) { if (ref($curRes)) { $counter++; } if (ref($curRes) && $jump eq $curRes->symb()) { @@ -1283,8 +1541,6 @@ sub render { $args->{'currentJumpIndex'} = $counter; $foundJump = 1; } - - $curRes = $mapIterator->next(); } my $showParts = setDefault($args->{'showParts'}, 1); @@ -1295,23 +1551,23 @@ sub render { my $printKey = $args->{'printKey'}; my $printCloseAll = $args->{'printCloseAll'}; if (!defined($printCloseAll)) { $printCloseAll = 1; } - + # Print key? if ($printKey) { $result .= ''; - my $date=localtime; $result.=''; + my $location=&Apache::loncommon::lonhttpdurl("/adm/lonMisc"); if ($navmap->{LAST_CHECK}) { $result .= - ' New discussion since '. + ' '.&mt('New discussion since').' '. strftime("%A, %b %e at %I:%M %P", localtime($navmap->{LAST_CHECK})). ''; } else { $result .= ''; } @@ -1319,84 +1575,156 @@ sub render { } if ($printCloseAll && !$args->{'resource_no_folder_link'}) { + my ($link,$text); if ($condition) { - $result.="Close All Folders"; - } else { - $result.="Open All Folders"; + $link='navmaps?condition=0&filter=&'.$queryString. + '&here='.&escape($here); + $text='Close all folders'; + } else { + $link='navmaps?condition=1&filter=&'.$queryString. + '&here='.&escape($here); + $text='Open all folders'; + } + if ($env{'form.register'}) { + $link .= '&register='.$env{'form.register'}; + } + if ($args->{'caller'} eq 'navmapsdisplay') { + unless ($args->{'notools'}) { + &add_linkitem($args->{'linkitems'},'changefolder', + "location.href='$link'",$text); + } + } else { + $result.= ''.&mt($text).''; + } + $result .= "\n"; + } + + # Check for any unread discussions in all resources. + if (($args->{'caller'} eq 'navmapsdisplay') && (!$args->{'notools'})) { + &add_linkitem($args->{'linkitems'},'clearbubbles', + 'document.clearbubbles.submit()', + 'Mark all posts read'); + my $time=time; + my $querystr = &HTML::Entities::encode($ENV{'QUERY_STRING'},'<>&"'); + $result .= (< + + +END + if ($env{'form.register'}) { + $result .= ''; + } + if ($args->{'sort'} eq 'discussion') { + my $totdisc = 0; + my $haveDisc = ''; + my @allres=$navmap->retrieveResources(); + foreach my $resource (@allres) { + if ($resource->hasDiscussion()) { + $haveDisc .= $resource->wrap_symb().':'; + $totdisc ++; + } + } + if ($totdisc > 0) { + $haveDisc =~ s/:$//; + $result .= (< + +END + } + } + $result.=''; + } + if (($args->{'caller'} eq 'navmapsdisplay') && + ((&Apache::lonnet::allowed('mdc',$env{'request.course.id'})) || + (&Apache::lonnet::allowed('cev',$env{'request.course.id'})))) { + my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; + my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; + if ($env{'course.'.$env{'request.course.id'}.'.url'} eq + "uploaded/$cdom/$cnum/default.sequence") { + &add_linkitem($args->{'linkitems'},'edittoplevel', + "javascript:gocmd('/adm/coursedocs','editdocs');", + 'Content Editor'); } - $result .= "

\n"; - } + } + + if ($args->{'caller'} eq 'navmapsdisplay') { + $result .= &show_linkitems_toolbar($args,$condition); + } elsif ($args->{'sort_html'}) { + $result.=$args->{'sort_html'}; + } + #$result .= "
\n"; if ($r) { $r->print($result); $r->rflush(); $result = ""; } # End parameter setting - + + $result .= "
\n"; + # Data - $result .= '
Key:    '. - ' New message (click to open)

'. + ' '.&mt('New message (click to open)').'

'. '

  '. - ' Discussions'. - '   New message (click to open)'. + ' '.&mt('Discussions').''. + '   '.&mt('New message (click to open)'). '
' ."\n"; + $result.=&Apache::loncommon::start_data_table("LC_tableOfContent"); + my $res = "Apache::lonnavmaps::resource"; my %condenseStatuses = ( $res->NETWORK_FAILURE => 1, $res->NOTHING_SET => 1, $res->CORRECT => 1 ); - my @backgroundColors = ("#FFFFFF", "#F6F6F6"); # Shared variables $args->{'counter'} = 0; # counts the rows $args->{'indentLevel'} = 0; $args->{'isNewBranch'} = 0; - $args->{'condensed'} = 0; - $args->{'indentString'} = setDefault($args->{'indentString'}, ""); + $args->{'condensed'} = 0; + + my $location = &Apache::loncommon::lonhttpdurl("/adm/lonIcons/whitespace_21.gif"); + $args->{'indentString'} = setDefault($args->{'indentString'}, ""); $args->{'displayedHereMarker'} = 0; - # If we're suppressing empty sequences, look for them here. Use DFS for speed, - # since structure actually doesn't matter, except what map has what resources. - if ($args->{'suppressEmptySequences'}) { - my $dfsit = Apache::lonnavmaps::DFSiterator->new($navmap, - $it->{FIRST_RESOURCE}, - $it->{FINISH_RESOURCE}, - {}, undef, 1); - $depth = 0; - $dfsit->next(); - my $curRes = $dfsit->next(); - while ($depth > -1) { - if ($curRes == $dfsit->BEGIN_MAP()) { $depth++; } - if ($curRes == $dfsit->END_MAP()) { $depth--; } - - if (ref($curRes)) { - # Parallel pre-processing: Do sequences have non-filtered-out children? - if ($curRes->is_map()) { - $curRes->{DATA}->{HAS_VISIBLE_CHILDREN} = 0; - # Sequences themselves do not count as visible children, - # unless those sequences also have visible children. - # This means if a sequence appears, there's a "promise" - # that there's something under it if you open it, somewhere. - } else { - # Not a sequence: if it's filtered, ignore it, otherwise - # rise up the stack and mark the sequences as having children - if (&$filterFunc($curRes)) { - for my $sequence (@{$dfsit->getStack()}) { - $sequence->{DATA}->{HAS_VISIBLE_CHILDREN} = 1; - } + # If we're suppressing empty sequences, look for them here. + # We also do this even if $args->{'suppressEmptySequences'} + # is not true, so we can hide empty sequences for which the + # hiddenresource parameter is set to yes (at map level), or + # mark as hidden for users who have $userCanSeeHidden. + # Use DFS for speed, since structure actually doesn't matter, + # except what map has what resources. + + my $dfsit = Apache::lonnavmaps::DFSiterator->new($navmap, + $it->{FIRST_RESOURCE}, + $it->{FINISH_RESOURCE}, + {}, undef, 1); + my $depth = 0; + $dfsit->next(); + my $curRes = $dfsit->next(); + while ($depth > -1) { + if ($curRes == $dfsit->BEGIN_MAP()) { $depth++; } + if ($curRes == $dfsit->END_MAP()) { $depth--; } + + if (ref($curRes)) { + # Parallel pre-processing: Do sequences have non-filtered-out children? + if ($curRes->is_map()) { + $curRes->{DATA}->{HAS_VISIBLE_CHILDREN} = 0; + # Sequences themselves do not count as visible children, + # unless those sequences also have visible children. + # This means if a sequence appears, there's a "promise" + # that there's something under it if you open it, somewhere. + } elsif ($curRes->src()) { + # Not a sequence: if it's filtered, ignore it, otherwise + # rise up the stack and mark the sequences as having children + if (&$filterFunc($curRes)) { + for my $sequence (@{$dfsit->getStack()}) { + $sequence->{DATA}->{HAS_VISIBLE_CHILDREN} = 1; } } } - } continue { - $curRes = $dfsit->next(); } + } continue { + $curRes = $dfsit->next(); } my $displayedJumpMarker = 0; # Set up iteration. - $depth = 1; - $it->next(); # discard initial BEGIN_MAP - $curRes = $it->next(); my $now = time(); my $in24Hours = $now + 24 * 60 * 60; my $rownum = 0; @@ -1404,9 +1732,82 @@ sub render { # export "here" marker information $args->{'here'} = $here; - while ($depth > 0) { - if ($curRes == $it->BEGIN_MAP()) { $depth++; } - if ($curRes == $it->END_MAP()) { $depth--; } + $args->{'indentLevel'} = -1; # first BEGIN_MAP takes this to 0 + my @resources; + my $code='';# sub { !(shift->is_map();) }; + if ($args->{'sort'} eq 'title') { + my $oldFilterFunc = $filterFunc; + my $filterFunc= + sub { + my ($res)=@_; + if ($res->is_map()) { return 0;} + return &$oldFilterFunc($res); + }; + @resources=$navmap->retrieveResources(undef,$filterFunc); + @resources= sort { &cmp_title($a,$b) } @resources; + } elsif ($args->{'sort'} eq 'duedate') { + my $oldFilterFunc = $filterFunc; + my $filterFunc= + sub { + my ($res)=@_; + if (!$res->is_problem()) { return 0;} + return &$oldFilterFunc($res); + }; + @resources=$navmap->retrieveResources(undef,$filterFunc); + @resources= sort { + if ($a->duedate ne $b->duedate) { + return $a->duedate cmp $b->duedate; + } + my $value=&cmp_title($a,$b); + return $value; + } @resources; + } elsif ($args->{'sort'} eq 'discussion') { + my $oldFilterFunc = $filterFunc; + my $filterFunc= + sub { + my ($res)=@_; + if (!$res->hasDiscussion() && + !$res->getFeedback() && + !$res->getErrors()) { return 0;} + return &$oldFilterFunc($res); + }; + @resources=$navmap->retrieveResources(undef,$filterFunc); + @resources= sort { &cmp_title($a,$b) } @resources; + } else { + #unknow sort mechanism or default + undef($args->{'sort'}); + } + + # Determine if page will be served with https in case + # it contains a syllabus which uses an external URL + # which points at an http site. + + my ($is_ssl,$cdom,$cnum,$hostname); + if ($ENV{'SERVER_PORT'} == 443) { + $is_ssl = 1; + if ($r) { + $hostname = $r->hostname(); + } else { + $hostname = $ENV{'SERVER_NAME'}; + } + } + if ($env{'request.course.id'}) { + $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; + $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; + } + + my $inhibitmenu; + if ($args->{'modalLink'}) { + $inhibitmenu = '&inhibitmenu=yes'; + } + + while (1) { + if ($args->{'sort'}) { + $curRes = shift(@resources); + } else { + $curRes = $it->next($closeAllPages); + } + if (!$curRes) { last; } # Maintain indentation level. if ($curRes == $it->BEGIN_MAP() || @@ -1434,9 +1835,39 @@ sub render { } # If this is an empty sequence and we're filtering them, continue on - if ($curRes->is_map() && $args->{'suppressEmptySequences'} && - !$curRes->{DATA}->{HAS_VISIBLE_CHILDREN}) { - next; + $args->{'mapHidden'} = 0; + $args->{'mapUnlisted'} = 0; + $args->{'mapHiddenDeepLink'} = 0; + if (($curRes->is_map()) && (!$curRes->{DATA}->{HAS_VISIBLE_CHILDREN})) { + if ($args->{'suppressEmptySequences'}) { + next; + } else { + my $mapname = &Apache::lonnet::declutter($curRes->src()); + $mapname = &Apache::lonnet::deversion($mapname); + if (lc($navmap->get_mapparam(undef,$mapname,"0.hiddenresource")) eq 'yes') { + if ($userCanSeeHidden) { + $args->{'mapHidden'} = 1; + } else { + next; + } + } elsif ($curRes->deeplinkout) { + if ($userCanSeeHidden) { + $args->{'mapHiddenDeepLink'} = 1; + } else { + next; + } + } else { + my $deeplink = $navmap->get_mapparam(undef,$mapname,"0.deeplink"); + my ($state,$others,$listed) = split(/,/,$deeplink); + if (($listed eq 'absent') || ($listed eq 'grades')) { + if ($userCanSeeHidden) { + $args->{'mapUnlisted'} = 1; + } else { + next; + } + } + } + } } # If we're suppressing navmaps and this is a navmap, continue on @@ -1457,10 +1888,6 @@ sub render { $args->{'multipart'} = $curRes->multipart(); if ($condenseParts) { # do the condensation - if (!$curRes->opendate("0")) { - @parts = (); - $args->{'condensed'} = 1; - } if (!$args->{'condensed'}) { # Decide whether to condense based on similarity my $status = $curRes->status($parts[0]); @@ -1501,8 +1928,17 @@ sub render { $args->{'condensed'} = 1; } } - } - + } + # If deep-link parameter is set (and is not set to full) suppress link + # unless privileged user, tinyurl used for login resolved to a map, and + # the resource is within the map. + if ((!$curRes->deeplink($args->{'caller'})) || + ($curRes->deeplink($args->{'caller'}) eq 'full') || &advancedUser()) { + $args->{'resource_nolink'} = 0; + } else { + $args->{'resource_nolink'} = 1; + } + # If the multipart problem was condensed, "forget" it was multipart if (scalar(@parts) == 1) { $args->{'multipart'} = 0; @@ -1510,25 +1946,60 @@ sub render { # Add part 0 so we display it correctly. unshift @parts, '0'; } - + + { + my ($src,$symb,$anchor,$stack); + if ($args->{'sort'}) { + my $it = $navmap->getIterator(undef, undef, undef, 1); + while ( my $res=$it->next()) { + if (ref($res) && + $res->symb() eq $curRes->symb()) { last; } + } + $stack=$it->getStack(); + } else { + $stack=$it->getStack(); + } + ($src,$symb,$anchor)=getLinkForResource($stack); + my $srcHasQuestion = $src =~ /\?/; + if ($env{'request.course.id'}) { + if (($is_ssl) && ($src =~ m{^\Q/public/$cdom/$cnum/syllabus\E($|\?)}) && + ($env{'course.'.$env{'request.course.id'}.'.externalsyllabus'} =~ m{^http://})) { + unless ((&Apache::lonnet::uses_sts()) || (&Apache::lonnet::waf_allssl($hostname))) { + if ($hostname ne '') { + $src = 'http://'.$hostname.$src; + } + $src .= ($srcHasQuestion? '&' : '?') . 'usehttp=1'; + $srcHasQuestion = 1; + } + } elsif (($is_ssl) && ($src =~ m{^\Q/adm/wrapper/ext/\E(?!https:)})) { + unless ((&Apache::lonnet::uses_sts()) || (&Apache::lonnet::waf_allssl($hostname))) { + if ($hostname ne '') { + $src = 'http://'.$hostname.$src; + } + $src .= ($srcHasQuestion? '&' : '?') . 'usehttp=1'; + $srcHasQuestion = 1; + } + } + } + if (defined($anchor)) { $anchor='#'.$anchor; } + if (($args->{'caller'} eq 'sequence') && ($curRes->is_map())) { + $args->{"resourceLink"} = $src.($srcHasQuestion?'&':'?') .'navmap=1'; + } else { + $args->{"resourceLink"} = $src. + ($srcHasQuestion?'&':'?') . + 'symb=' . &escape($symb).$inhibitmenu.$anchor; + } + } # Now, we've decided what parts to show. Loop through them and # show them. foreach my $part (@parts) { $rownum ++; - my $backgroundColor = $backgroundColors[$rownum % scalar(@backgroundColors)]; - $result .= " \n"; + $result .= &Apache::loncommon::start_data_table_row(); # Set up some data about the parts that the cols might want my $filter = $it->{FILTER}; - my $stack = $it->getStack(); - my $src = getLinkForResource($stack); - - my $srcHasQuestion = $src =~ /\?/; - $args->{"resourceLink"} = $src. - ($srcHasQuestion?'&':'?') . - 'symb=' . &Apache::lonnet::escape($curRes->symb()); - + # Now, display each column. foreach my $col (@$cols) { my $colHTML = ''; @@ -1545,12 +2016,12 @@ sub render { $currentJumpDelta) { # Jam the anchor after the \n"; + $result .= &Apache::loncommon::end_data_table_row(); $args->{'isNewBranch'} = 0; } @@ -1560,18 +2031,17 @@ sub render { $r->rflush(); } } continue { - $curRes = $it->next(); - if ($r) { # If we have the connection, make sure the user is still connected my $c = $r->connection; if ($c->aborted()) { - Apache::lonnet::logthis("navmaps aborted"); # Who cares what we do, nobody will see it anyhow. return ''; } } } + + $result.=&Apache::loncommon::end_data_table(); # Print out the part that jumps to #curloc if it exists # delay needed because the browser is processing the jump before @@ -1581,50 +2051,150 @@ sub render { # it's quite likely this might fix other browsers, too, and # certainly won't hurt anything. if ($displayedJumpMarker) { - $result .= "\n"; + $result .= &Apache::lonhtmlcommon::scripttag(" +if (location.href.indexOf('#curloc')==-1) { + setTimeout(\"location += '#curloc';\", 0) +} +"); } - $result .= "
tag; # necessary for valid HTML (which Mozilla requires) - $colHTML =~ s/\>/\>\/; + $colHTML =~ s/\>/\>\\<\/a\>/; $displayedJumpMarker = 1; } $result .= $colHTML . "\n"; } - $result .= "
"; - if ($r) { $r->print($result); $result = ""; $r->rflush(); } - if ($mustCloseNavMap) { $navmap->untieHashes(); } + return $result; +} +sub add_linkitem { + my ($linkitems,$name,$cmd,$text)=@_; + $$linkitems{$name}{'cmd'}=$cmd; + $$linkitems{$name}{'text'}=&mt($text); +} + +sub show_linkitems_toolbar { + my ($args,$condition) = @_; + my $result; + if (ref($args) eq 'HASH') { + if (ref($args->{'linkitems'}) eq 'HASH') { + my $numlinks = scalar(keys(%{$args->{'linkitems'}})); + if ($numlinks > 1) { + $result = ''. + &Apache::loncommon::help_open_menu('Navigation Screen','Navigation_Screen', + undef,'RAT'). + ''. + ' '. + ''.&mt('Tools:').''; + } + $result .= ''."\n". + '
    '; + my @linkorder = ('firsthomework','everything','uncompleted', + 'changefolder','clearbubbles','edittoplevel'); + foreach my $link (@linkorder) { + if (ref($args->{'linkitems'}{$link}) eq 'HASH') { + if ($args->{'linkitems'}{$link}{'text'} ne '') { + $args->{'linkitems'}{$link}{'cmd'}=~s/"/'/g; + if ($args->{'linkitems'}{$link}{'cmd'}) { + my $link_id = 'LC_content_toolbar_'.$link; + if ($link eq 'changefolder') { + if ($condition) { + $link_id='LC_content_toolbar_changefolder_toggled'; + } else { + $link_id='LC_content_toolbar_changefolder'; + } + } + $result .= '
  • '. + '
  • '."\n"; + } + } + } + } + $result .= '
'. + ''; + if (($numlinks==1) && (exists($args->{'linkitems'}{'edittoplevel'}))) { + $result .= ''. + &mt('Content Editor').''; + } + } + if ($args->{'sort_html'}) { + $result .= '   '. + ''.$args->{'sort_html'}.''; + } + } + if ($result) { + $result = "$result
"; + } return $result; } 1; + + + + + + + + package Apache::lonnavmaps::navmap; =pod =head1 Object: Apache::lonnavmaps::navmap -You must obtain resource objects through the navmap object. +=head2 Overview -=head2 Creation +The navmap object's job is to provide access to the resources +in the course as Apache::lonnavmaps::resource objects, and to +query and manage the relationship between those resource objects. + +Generally, you'll use the navmap object in one of three basic ways. +In order of increasing complexity and power: + +=over 4 + +=item * C<$navmap-EgetByX>, where X is B, B or B and getResourceByUrl. This provides + various ways to obtain resource objects, based on various identifiers. + Use this when you want to request information about one object or + a handful of resources you already know the identities of, from some + other source. For more about Ids, Symbs, and MapPcs, see the + Resource documentation. Note that Url should be a B, + not your first choice; it only really works when there is only one + instance of the resource in the course, which only applies to + maps, and even that may change in the future (see the B + documentation for more details.) + +=item * CretrieveResources(args)>. This + retrieves resources matching some criterion and returns them + in a flat array, with no structure information. Use this when + you are manipulating a series of resources, based on what map + the are in, but do not care about branching, or exactly how + the maps and resources are related. This is the most common case. + +=item * C<$it = $navmap-EgetIterator(args)>. This allows you traverse + the course's navmap in various ways without writing the traversal + code yourself. See iterator documentation below. Use this when + you need to know absolutely everything about the course, including + branches and the precise relationship between maps and resources. + +=back + +=head2 Creation And Destruction + +To create a navmap object, use the following function: =over 4 -=item * B(navHashFile, parmHashFile, genCourseAndUserOptions, - genMailDiscussStatus, getUserData): +=item * Bnew>(): -Binds a new navmap object to the compiled nav map hash and parm hash -given as filenames. genCourseAndUserOptions is a flag saying whether -the course options and user options hash should be generated. This is -for when you are using the parameters of the resources that require -them; see documentation in resource object -documentation. genMailDiscussStatus causes the nav map to retreive -information about the email and discussion status of -resources. Returns the navmap object if this is successful, or -B if not. You must check for undef; errors will occur when you -try to use the other methods otherwise. getUserData, if true, will -retreive the user's performance data for various problems. +Creates a new navmap object. Returns the navmap object if this is +successful, or B if not. =back @@ -1640,18 +2210,22 @@ See iterator documentation below. use strict; use GDBM_File; +use Apache::lonnet; +use LONCAPA; sub new { # magic invocation to create a class instance my $proto = shift; my $class = ref($proto) || $proto; my $self = {}; + bless($self); # So we can call change_user if necessary + + $self->{USERNAME} = shift || $env{'user.name'}; + $self->{DOMAIN} = shift || $env{'user.domain'}; + $self->{CODE} = shift; + $self->{NOHIDE} = shift; + - $self->{NAV_HASH_FILE} = shift; - $self->{PARM_HASH_FILE} = shift; - $self->{GENERATE_COURSE_USER_OPT} = shift; - $self->{GENERATE_EMAIL_DISCUSS_STATUS} = shift; - $self->{GET_USER_DATA} = shift; # Resource cache stores navmap resources as we reference them. We generate # them on-demand so we don't pay for creating resources unless we use them. @@ -1661,148 +2235,244 @@ sub new { # failed $self->{NETWORK_FAILURE} = 0; - # tie the nav hash - - my %navmaphash; - my %parmhash; - if (!(tie(%navmaphash, 'GDBM_File', $self->{NAV_HASH_FILE}, - &GDBM_READER(), 0640))) { - return undef; + # We can only tie the nav hash as done below if the username/domain + # match the env one. Otherwise change_user does everything we need...since we can't + # assume there are course hashes for the specific requested user:domain + # Note: change_user is also called if we need the nav hash when printing CODEd + # assignments or printing an exam, in which the enclosing folder for the items in + # the exam has hidden set. + # + + if (($self->{USERNAME} eq $env{'user.name'}) && ($self->{DOMAIN} eq $env{'user.domain'}) && + !$self->{CODE} && !$self->{NOHIDE}) { + + # tie the nav hash + + my %navmaphash; + my %parmhash; + my $courseFn = $env{"request.course.fn"}; + if (!(tie(%navmaphash, 'GDBM_File', "${courseFn}.db", + &GDBM_READER(), 0640))) { + return undef; + } + + if (!(tie(%parmhash, 'GDBM_File', "${courseFn}_parms.db", + &GDBM_READER(), 0640))) + { + untie %{$self->{PARM_HASH}}; + return undef; + } + + $self->{NAV_HASH} = \%navmaphash; + $self->{PARM_HASH} = \%parmhash; + $self->{PARM_CACHE} = {}; + } else { + $self->change_user($self->{USERNAME}, $self->{DOMAIN}, $self->{CODE}, $self->{NOHIDE}); } + + return $self; +} + +# +# In some instances it is useful to be able to dynamically change the +# username/domain associated with a navmap (e.g. to navigate for someone +# else besides the current user...if sufficiently privileged. +# Parameters: +# user - New user. +# domain- Domain the user belongs to. +# code - Anonymous CODE in use. +# Implicit inputs: +# +sub change_user { + my $self = shift; + $self->{USERNAME} = shift; + $self->{DOMAIN} = shift; + $self->{CODE} = shift; + $self->{NOHIDE} = shift; + + # If the hashes are already tied make sure to break that bond: + + untie %{$self->{NAV_HASH}}; + untie %{$self->{PARM_HASH}}; + + # The assumption is that we have to + # use lonmap here to re-read the hash and from it reconstruct + # new big and parameter hashes. An implicit assumption at this time + # is that the course file is probably not created locally yet + # an that we will therefore just read without tying. + + my ($cdom, $cnum) = split(/\_/, $env{'request.course.id'}); + + my %big_hash; + &Apache::lonmap::loadmap($cnum, $cdom, $self->{USERNAME}, $self->{DOMAIN}, $self->{CODE}, $self->{NOHIDE}, \%big_hash); + $self->{NAV_HASH} = \%big_hash; + + + + # Now clear the parm cache and reconstruct the parm hash from the big_hash + # param.xxxx keys. + + $self->{PARM_CACHE} = {}; - if (!(tie(%parmhash, 'GDBM_File', $self->{PARM_HASH_FILE}, - &GDBM_READER(), 0640))) - { - untie %{$self->{PARM_HASH}}; - return undef; + my %parm_hash = {}; + foreach my $key (keys(%big_hash)) { + if ($key =~ /^param\./) { + my $param_key = $key; + $param_key =~ s/^param\.//; + $parm_hash{$param_key} = $big_hash{$key}; + } } - $self->{NAV_HASH} = \%navmaphash; - $self->{PARM_HASH} = \%parmhash; - $self->{INITED} = 0; + $self->{PARM_HASH} = \%parm_hash; - bless($self); - - return $self; } -sub init { +sub generate_course_user_opt { my $self = shift; - if ($self->{INITED}) { return; } + if ($self->{COURSE_USER_OPT_GENERATED}) { return; } - # If the course opt hash and the user opt hash should be generated, - # generate them - if ($self->{GENERATE_COURSE_USER_OPT}) { - my $uname=$ENV{'user.name'}; - my $udom=$ENV{'user.domain'}; - my $uhome=$ENV{'user.home'}; - my $cid=$ENV{'request.course.id'}; - my $chome=$ENV{'course.'.$cid.'.home'}; - my ($cdom,$cnum)=split(/\_/,$cid); - - my $userprefix=$uname.'_'.$udom.'_'; - - my %courserdatas; my %useropt; my %courseopt; my %userrdatas; - unless ($uhome eq 'no_host') { + my $uname=$self->{USERNAME}; + my $udom=$self->{DOMAIN}; + + my $cid=$env{'request.course.id'}; + my $cdom=$env{'course.'.$cid.'.domain'}; + my $cnum=$env{'course.'.$cid.'.num'}; + # ------------------------------------------------- Get coursedata (if present) - unless ((time-$courserdatas{$cid.'.last_cache'})<240) { - my $reply=&Apache::lonnet::reply('dump:'.$cdom.':'.$cnum. - ':resourcedata',$chome); - # Check for network failure - if ( $reply =~ /no.such.host/i || $reply =~ /con_lost/i) { - $self->{NETWORK_FAILURE} = 1; - } elsif ($reply!~/^error\:/) { - $courserdatas{$cid}=$reply; - $courserdatas{$cid.'.last_cache'}=time; - } - } - foreach (split(/\&/,$courserdatas{$cid})) { - my ($name,$value)=split(/\=/,$_); - $courseopt{$userprefix.&Apache::lonnet::unescape($name)}= - &Apache::lonnet::unescape($value); - } + my $courseopt=&Apache::lonnet::get_courseresdata($cnum,$cdom); + # Check for network failure + if (!ref($courseopt)) { + if ( $courseopt =~ /no.such.host/i || $courseopt =~ /con_lost/i) { + $self->{NETWORK_FAILURE} = 1; + } + undef($courseopt); + } + # --------------------------------------------------- Get userdata (if present) - unless ((time-$userrdatas{$uname.'___'.$udom.'.last_cache'})<240) { - my $reply=&Apache::lonnet::reply('dump:'.$udom.':'.$uname.':resourcedata',$uhome); - if ($reply!~/^error\:/) { - $userrdatas{$uname.'___'.$udom}=$reply; - $userrdatas{$uname.'___'.$udom.'.last_cache'}=time; - } - # check to see if network failed - elsif ( $reply=~/no.such.host/i || $reply=~/con.*lost/i ) - { - $self->{NETWORK_FAILURE} = 1; - } - } - foreach (split(/\&/,$userrdatas{$uname.'___'.$udom})) { - my ($name,$value)=split(/\=/,$_); - $useropt{$userprefix.&Apache::lonnet::unescape($name)}= - &Apache::lonnet::unescape($value); - } - $self->{COURSE_OPT} = \%courseopt; - $self->{USER_OPT} = \%useropt; - } - } - - if ($self->{GENERATE_EMAIL_DISCUSS_STATUS}) { - my $cid=$ENV{'request.course.id'}; - my ($cdom,$cnum)=split(/\_/,$cid); - - my %emailstatus = &Apache::lonnet::dump('email_status'); - my $logoutTime = $emailstatus{'logout'}; - my $courseLeaveTime = $emailstatus{'logout_'.$ENV{'request.course.id'}}; - $self->{LAST_CHECK} = (($courseLeaveTime > $logoutTime) ? - $courseLeaveTime : $logoutTime); - my %discussiontime = &Apache::lonnet::dump('discussiontimes', - $cdom, $cnum); - my %feedback=(); - my %error=(); - my $keys = &Apache::lonnet::reply('keys:'. - $ENV{'user.domain'}.':'. - $ENV{'user.name'}.':nohist_email', - $ENV{'user.home'}); - - foreach my $msgid (split(/\&/, $keys)) { - $msgid=&Apache::lonnet::unescape($msgid); - my $plain=&Apache::lonnet::unescape(&Apache::lonnet::unescape($msgid)); - if ($plain=~/(Error|Feedback) \[([^\]]+)\]/) { - my ($what,$url)=($1,$2); - my %status= - &Apache::lonnet::get('email_status',[$msgid]); - if ($status{$msgid}=~/^error\:/) { - $status{$msgid}=''; + + my $useropt=&Apache::lonnet::get_userresdata($uname,$udom); + # Check for network failure + if (!ref($useropt)) { + if ( $useropt =~ /no.such.host/i || $useropt =~ /con_lost/i) { + $self->{NETWORK_FAILURE} = 1; + } + undef($useropt); + } + + $self->{COURSE_OPT} = $courseopt; + $self->{USER_OPT} = $useropt; + + $self->{COURSE_USER_OPT_GENERATED} = 1; + + return; +} + + + +sub generate_email_discuss_status { + my $self = shift; + my $symb = shift; + if ($self->{EMAIL_DISCUSS_GENERATED}) { return; } + + my $cid=$env{'request.course.id'}; + my $cdom=$env{'course.'.$cid.'.domain'}; + my $cnum=$env{'course.'.$cid.'.num'}; + + my %emailstatus = &Apache::lonnet::dump('email_status',$self->{DOMAIN},$self->{USERNAME}); + my $logoutTime = $emailstatus{'logout'}; + my $courseLeaveTime = $emailstatus{'logout_'.$env{'request.course.id'}}; + $self->{LAST_CHECK} = (($courseLeaveTime > $logoutTime) ? + $courseLeaveTime : $logoutTime); + my %discussiontime = &Apache::lonnet::dump('discussiontimes', + $cdom, $cnum); + my %lastread = &Apache::lonnet::dump('nohist_'.$cid.'_discuss', + $self->{DOMAIN},$self->{USERNAME},'lastread'); + my %lastreadtime = (); + foreach my $key (keys(%lastread)) { + my $shortkey = $key; + $shortkey =~ s/_lastread$//; + $lastreadtime{$shortkey} = $lastread{$key}; + } + + my %feedback=(); + my %error=(); + my @keys = &Apache::lonnet::getkeys('nohist_email',$self->{DOMAIN}, + $self->{USERNAME}); + + foreach my $msgid (@keys) { + if ((!$emailstatus{$msgid}) || ($emailstatus{$msgid} eq 'new')) { + my ($sendtime,$shortsubj,$fromname,$fromdomain,$status,$fromcid, + $symb,$error) = &Apache::lonmsg::unpackmsgid(&LONCAPA::escape($msgid)); + &Apache::lonenc::check_decrypt(\$symb); + if (($fromcid ne '') && ($fromcid ne $cid)) { + next; + } + if (defined($symb)) { + if (defined($error) && $error == 1) { + $error{$symb}.=','.$msgid; + } else { + $feedback{$symb}.=','.$msgid; } - - if (($status{$msgid} eq 'new') || - (!$status{$msgid})) { - if ($what eq 'Error') { - $error{$url}.=','.$msgid; + } else { + my $plain= + &LONCAPA::unescape(&LONCAPA::unescape($msgid)); + if ($plain=~/ \[([^\]]+)\]\:/) { + my $url=$1; + if ($plain=~/\:Error \[/) { + $error{$url}.=','.$msgid; } else { $feedback{$url}.=','.$msgid; } } } - } - - $self->{FEEDBACK} = \%feedback; - $self->{ERROR_MSG} = \%error; # what is this? JB - $self->{DISCUSSION_TIME} = \%discussiontime; - $self->{EMAIL_STATUS} = \%emailstatus; - + } } + + #symbs of resources that have feedbacks (will be urls pre-2.3) + $self->{FEEDBACK} = \%feedback; + #or errors (will be urls pre 2.3) + $self->{ERROR_MSG} = \%error; + $self->{DISCUSSION_TIME} = \%discussiontime; + $self->{EMAIL_STATUS} = \%emailstatus; + $self->{LAST_READ} = \%lastreadtime; + + $self->{EMAIL_DISCUSS_GENERATED} = 1; +} + +sub get_user_data { + my $self = shift; + if ($self->{RETRIEVED_USER_DATA}) { return; } + + # Retrieve performance data on problems + my %student_data = Apache::lonnet::currentdump($env{'request.course.id'}, + $self->{DOMAIN}, + $self->{USERNAME}); + $self->{STUDENT_DATA} = \%student_data; + + $self->{RETRIEVED_USER_DATA} = 1; +} - if ($self->{GET_USER_DATA}) { - # Retreive performance data on problems - my %student_data = Apache::lonnet::currentdump($ENV{'request.course.id'}, - $ENV{'user.domain'}, - $ENV{'user.name'}); - $self->{STUDENT_DATA} = \%student_data; +sub get_discussion_data { + my $self = shift; + if ($self->{RETRIEVED_DISCUSSION_DATA}) { + return $self->{DISCUSSION_DATA}; } - $self->{PARM_CACHE} = {}; - $self->{INITED} = 1; + $self->generate_email_discuss_status(); + + my $cid=$env{'request.course.id'}; + my $cdom=$env{'course.'.$cid.'.domain'}; + my $cnum=$env{'course.'.$cid.'.num'}; + # Retrieve discussion data for resources in course + my %discussion_data = &Apache::lonnet::dumpstore($cid,$cdom,$cnum); + + + $self->{DISCUSSION_DATA} = \%discussion_data; + $self->{RETRIEVED_DISCUSSION_DATA} = 1; + return $self->{DISCUSSION_DATA}; } + # Internal function: Takes a key to look up in the nav hash and implements internal # memory caching of that key. sub navhash { @@ -1810,10 +2480,19 @@ sub navhash { return $self->{NAV_HASH}->{$key}; } +=pod + +=item * B(): Returns true if the course map is defined, + false otherwise. Undefined course maps indicate an error somewhere in + LON-CAPA, and you will not be able to proceed with using the navmap. + See the B